e following implementation demonstrates the architecture using a new codebase structure with equivalent functionality but distinct naming and organization.
1. Contract Layer: State Transition Rules
The Compact contract defines what can change, who can change it, and how state is committed. Public data remains visible; private data is excluded.
// contract/src/ledger.compact
contract SignalLedger {
state {
public_message: String,
sequence: u64,
owner: Address
}
init(initial_owner: Address) {
state.public_message = "";
state.sequence = 0;
state.owner = initial_owner;
}
function update_message(new_msg: String, auth_witness: AuthProof) public {
require(auth_witness.verify(state.owner), "Unauthorized update");
state.public_message = new_msg;
state.sequence += 1;
}
}
Rationale: The contract stores only the public message, sequence counter, and owner address. Authorization is delegated to a witness proof rather than inline signature verification. This keeps the contract lightweight and ensures private credentials never enter public state.
Witnesses supply local values required for circuit evaluation. They must never serialize sensitive data to logs or expose it to the UI layer.
// contract/src/witness-context.ts
import { AuthProof, Address } from '@midnight-ntwrk/compact-runtime';
export interface WitnessPayload {
ownerAddress: Address;
privateKeySeed: Uint8Array;
timestamp: number;
}
export function generateAuthWitness(payload: WitnessPayload): AuthProof {
// Circuit evaluation happens locally
// Private seed is used to derive a zero-knowledge proof
// Never return raw seed to calling context
const proof = AuthProof.fromSeed(payload.privateKeySeed, payload.ownerAddress);
return proof;
}
Rationale: The witness function accepts a payload containing sensitive material, generates the proof locally, and returns only the cryptographic artifact. The UI or API layer never handles the raw seed. This prevents accidental leakage through browser devtools, network logs, or state management libraries.
3. API Bridge: Typed Contract Interface
A dedicated TypeScript package wraps the generated contract bindings, enforcing type safety and preventing direct contract calls from UI components.
// api/src/contract-bridge.ts
import { LedgerContract, LedgerState } from '@midnight-ntwrk/generated-ledger';
import { AuthProof } from '@midnight-ntwrk/compact-runtime';
export class LedgerBridge {
private contract: LedgerContract;
constructor(contractInstance: LedgerContract) {
this.contract = contractInstance;
}
async fetchCurrentState(): Promise<LedgerState> {
return this.contract.queryState();
}
async submitUpdate(
message: string,
proof: AuthProof
): Promise<{ sequence: number; txHash: string }> {
const result = await this.contract.updateMessage(message, proof);
return {
sequence: result.newSequence,
txHash: result.transactionId
};
}
}
Rationale: The bridge abstracts generated bindings behind a stable interface. UI components interact with LedgerBridge instead of raw contract methods. This enables mocking for tests, centralized error handling, and consistent state shape across CLI, scripts, and browser clients.
4. Metadata Service: Off-Chain Operational Context
A lightweight Node HTTP service exposes configuration, network labels, and documentation references. It never modifies contract state.
// metadata-api/src/context-server.ts
import { createServer } from 'http';
const PORT = 4001;
const operationalContext = {
appName: 'SignalBoard',
network: 'preprod',
proofServerUrl: 'http://127.0.0.1:6300',
privacyModel: 'selective-disclosure',
contractSurfaces: ['SignalLedger']
};
const server = createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/health') {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok' }));
} else if (req.url === '/context') {
res.writeHead(200);
res.end(JSON.stringify(operationalContext));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'not_found' }));
}
});
server.listen(PORT, () => {
console.log(`Context service listening on port ${PORT}`);
});
Rationale: The metadata service is read-only and intentionally decoupled from contract logic. It provides the UI with network labels, proof server endpoints, and privacy model descriptors. In production, this service can host moderation tags, support links, or chain explorer references without influencing state transitions.
5. React UI: Boundary Visualization
The frontend consumes both the contract bridge and metadata service, rendering a clear separation between authoritative state and operational context.
// bboard-ui/src/components/Dashboard.tsx
import { useEffect, useState } from 'react';
import { LedgerBridge } from '@midnight-ntwrk/ledger-bridge';
import type { ContextResponse } from '../types/context';
interface DashboardProps {
bridge: LedgerBridge;
contextUrl: string;
}
export function Dashboard({ bridge, contextUrl }: DashboardProps) {
const [state, setState] = useState<{ message: string; sequence: number } | null>(null);
const [context, setContext] = useState<ContextResponse | null>(null);
useEffect(() => {
bridge.fetchCurrentState().then(s => setState({ message: s.public_message, sequence: s.sequence }));
fetch(`${contextUrl}/context`).then(r => r.json()).then(setContext);
}, [bridge, contextUrl]);
if (!state || !context) return <div>Loading boundaries...</div>;
return (
<main className="grid grid-cols-2 gap-6 p-4">
<section className="border p-4 rounded">
<h2>Contract State (Authoritative)</h2>
<p>Message: {state.message}</p>
<p>Sequence: {state.sequence}</p>
</section>
<section className="border p-4 rounded bg-slate-50">
<h2>Operational Context (Off-Chain)</h2>
<p>Network: {context.network}</p>
<p>Proof Server: {context.proofServerUrl}</p>
<p>Privacy Model: {context.privacyModel}</p>
</section>
</main>
);
}
Rationale: The UI explicitly separates contract-backed state from off-chain metadata. This prevents developers from accidentally treating configuration data as authoritative and gives auditors a clear view of where trust boundaries exist.
Pitfall Guide
1. Compiler/Runtime Version Drift
Explanation: The Compact compiler generates TypeScript bindings that depend on specific runtime APIs. If the compiler version (e.g., 0.31.0) and @midnight-ntwrk/compact-runtime version drift, type-checking may pass but proof generation or contract calls will fail at runtime.
Fix: Pin versions explicitly in package.json and enforce alignment in CI. Use workspace-level version scripts to verify compatibility before merging.
Explanation: Off-chain metadata services are mutable and untrusted by the contract. If the UI uses metadata to override contract state or skip authorization checks, the application becomes vulnerable to configuration tampering.
Fix: Treat metadata as display-only. Always fetch authoritative state from the contract bridge. Validate metadata responses against known schemas and reject mismatches gracefully.
3. Leaking Private Data in Witness Serialization
Explanation: Witness payloads often contain seeds, keys, or sensitive inputs. Logging these objects, storing them in browser state, or transmitting them to analytics services compromises privacy guarantees.
Fix: Sanitize witness outputs before returning them to calling contexts. Use JSON.stringify guards, avoid console logging sensitive payloads, and clear memory references after proof generation.
4. Ignoring Proof Server Lifecycle
Explanation: The proof server (midnightntwrk/proof-server:8.0.3) must be running and healthy before contract interactions that require zero-knowledge proofs. UI components that assume immediate proof availability will timeout or crash.
Fix: Implement health checks (/health endpoint) before initiating proof-dependent transactions. Cache proof server status in UI state and display fallback messages when the service is unreachable.
5. Monorepo Workspace Dependency Conflicts
Explanation: Shared packages in a monorepo can pull mismatched versions of @midnight-ntwrk/compact-runtime or generated bindings, causing duplicate class instances or interface mismatches at runtime.
Fix: Use npm workspaces with explicit version ranges. Run npm ls @midnight-ntwrk/compact-runtime regularly to detect duplicates. Pin transitive dependencies if necessary.
6. Bypassing the Typed API Bridge
Explanation: UI components that call generated contract methods directly skip error handling, state normalization, and mocking capabilities. This creates inconsistent behavior across CLI, scripts, and browser clients.
Fix: Enforce architecture boundaries in linting rules. Route all contract interactions through the bridge package. Write integration tests that verify bridge behavior independently of UI components.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Public announcement board | Store message + sequence on-chain; use metadata for labels | Minimizes on-chain storage while preserving auditability | Low gas, medium off-chain hosting |
| Anonymous incident reporting | Commit hash on-chain; witness handles identity proof; metadata stores moderation tags | Preserves privacy while enabling governance | Medium proof latency, low storage |
| Internal team feedback | All state off-chain; contract used only for cryptographic attestation | Reduces chain load; metadata handles routing | Zero chain cost, high off-chain dependency |
| Credential-gated submissions | Witness verifies credential; contract stores only result flag | Prevents credential leakage; keeps contract lightweight | Medium proof cost, low storage |
Configuration Template
# bboard-ui/.env.production
VITE_METADATA_API_URL=https://context.yourdomain.com
VITE_PROOF_SERVER_URL=https://proof.yourdomain.com
VITE_NETWORK_LABEL=preprod
VITE_PRIVACY_MODEL=selective-disclosure
# metadata-api/.env
PORT=4001
NODE_ENV=production
LOG_LEVEL=warn
ALLOWED_ORIGINS=https://app.yourdomain.com
# docker-compose.yml (local dev)
version: '3.8'
services:
proof-server:
image: midnightntwrk/proof-server:8.0.3
ports:
- "6300:6300"
restart: unless-stopped
metadata-api:
build: ./metadata-api
ports:
- "4001:4001"
environment:
- PORT=4001
- NODE_ENV=development
restart: unless-stopped
Quick Start Guide
- Initialize workspaces: Run
npm install at the repository root to link all packages and verify dependency resolution.
- Compile contract & generate bindings: Navigate to
contract/ and execute npm run ci. This compiles Compact source, generates TypeScript interfaces, and runs unit tests.
- Start infrastructure: Launch the proof server with
docker run --rm -p 6300:6300 midnightntwrk/proof-server:8.0.3 and start the metadata service with cd metadata-api && npm run build && npm start.
- Run UI: Execute
cd bboard-ui && npm run dev -- --host 127.0.0.1. The dashboard will fetch contract state and operational context, rendering the privacy boundary explicitly. Verify that metadata changes do not affect contract sequence or authorization.