| name | midnight-dapp:proof-handling |
| description | Use when building witness data for ZK proofs, showing proof generation progress to users, implementing disclosure consent flows, handling proof timeouts and retries, or explaining privacy guarantees. |
Proof Handling
Build and present zero-knowledge proofs in your Midnight DApp, from constructing witnesses to showing proof generation progress and handling privacy disclosures.
When to Use
- Building witness data for contract transactions
- Showing proof generation progress to users
- Implementing disclosure consent flows
- Handling proof timeouts and retries
- Explaining privacy guarantees to users
Key Concepts
Witness Construction
Witnesses are TypeScript functions that provide private data to ZK circuits. The data stays local - only the cryptographic proof goes on-chain.
const witnesses = {
get_secret: ({ privateState }: WitnessContext<PrivateState>): bigint => {
return privateState.secret;
}
};
Client-Side Proofs
All proof generation happens locally on the user's device:
- Never on remote servers - Private data never leaves the browser
- Proof server runs locally - Docker container on port 6300
- Takes seconds, not milliseconds - Show progress UI to users
- Can timeout - Implement retry logic for reliability
Disclosure UX
When circuits use disclose(), users must understand what they're revealing:
const disclosedAge = disclose(userAge);
References
Examples
Quick Start
1. Define Witness Types
interface PrivateState {
secretKey: Uint8Array;
credentials: Map<string, Credential>;
balance: bigint;
}
2. Implement Witnesses
import type { WitnessContext } from "@midnight-ntwrk/midnight-js-types";
const witnesses = {
get_balance: ({ privateState }: WitnessContext<PrivateState>): bigint => {
return privateState.balance;
},
get_credential: (
{ privateState }: WitnessContext<PrivateState>,
credentialId: Uint8Array
): Credential => {
const id = bytesToHex(credentialId);
const credential = privateState.credentials.get(id);
if (!credential) {
throw new WitnessError("Credential not found", "NOT_FOUND");
}
return credential;
}
};
3. Show Proof Progress
function TransferButton({ onTransfer }) {
const { status, progress, error, generateProof } = useProofStatus();
const handleClick = async () => {
const result = await generateProof(async () => {
return contract.callTx.transfer(recipient, amount, witnesses);
});
if (result.success) {
onTransfer(result.transaction);
}
};
return (
<div>
<button onClick={handleClick} disabled={status === "generating"}>
{status === "generating" ? "Generating Proof..." : "Transfer"}
</button>
{status === "generating" && <ProgressBar value={progress} />}
{error && <ErrorMessage error={error} />}
</div>
);
}
4. Handle Disclosure Consent
function DisclosureFlow({ disclosures, onConfirm, onCancel }) {
return (
<DisclosureModal
disclosures={disclosures}
onConfirm={onConfirm}
onCancel={onCancel}
>
<p>This transaction will reveal the following information:</p>
<ul>
{disclosures.map((d) => (
<li key={d.field}>
<strong>{d.label}</strong>: {d.value}
</li>
))}
</ul>
</DisclosureModal>
);
}
Common Patterns
Witness with Validation
const witnesses = {
get_credential: (
{ privateState }: WitnessContext<PrivateState>,
credentialId: Uint8Array
): Credential => {
const credential = privateState.credentials.get(bytesToHex(credentialId));
if (!credential) {
throw new WitnessError("Credential not found", "NOT_FOUND");
}
if (credential.expiry < BigInt(Date.now())) {
throw new WitnessError("Credential expired", "EXPIRED");
}
return credential;
}
};
Async Witness for External Data
const witnesses = {
get_oracle_price: async (
{ privateState }: WitnessContext<PrivateState>,
tokenId: Uint8Array
): Promise<bigint> => {
const response = await fetch(`/api/price/${bytesToHex(tokenId)}`);
if (!response.ok) {
throw new WitnessError("Price unavailable", "ORACLE_ERROR");
}
const { price } = await response.json();
return BigInt(price);
}
};
Proof Retry Logic
async function submitWithRetry(
buildTx: () => Promise<Transaction>,
maxRetries = 3
): Promise<string> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const tx = await buildTx();
const provenTx = await walletAPI.balanceAndProveTransaction(tx, newCoins);
return await walletAPI.submitTransaction(provenTx);
} catch (error) {
lastError = error as Error;
if (error.message.includes("rejected")) {
throw error;
}
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
}
throw lastError ?? new Error("Proof generation failed");
}
Security Considerations
- Never persist witness data - Keep private state in memory only
- Clear sensitive data - Zero out arrays after use when possible
- Validate all inputs - Check parameter validity in witnesses
- Handle errors gracefully - Don't leak information in error messages
- User consent for disclosures - Always show what will be revealed
Related Skills
wallet-integration - Wallet connection required before proof generation
state-management - Managing private state for witnesses
transaction-flows - Submitting proven transactions
error-handling - Proof error messages and recovery
Related Commands
/dapp-check - Validates proof server configuration
/dapp-debug proofs - Diagnose proof generation issues