Live
Black Hat USADark ReadingBlack Hat AsiaAI BusinessNvidia Needs to Remind Itself What PC Gamers Actually WantGizmodoAI’s affect on communities, students, staff - USI | student newspaperGoogle News: Generative AI2 Artificial Intelligence (AI) Stocks I'd Buy With $1,000 Before They Rebound From the Tech Sell-Off - The Motley FoolGoogle News: AIGoogle Updates Gemini API Pricing Tiers for Optimization - Intellectia AIGoogle News: GeminiIran Says It Hit Oracle Facilities in UAEGizmodoInside the ethics of artificial intelligence - New Day NW - KING5.comGoogle News: AIMicrosoft Generative AI Report: The 40 Jobs Most Disrupted Jobs & The 40 Most Secure Jobs - HackerNoonGoogle News: Generative AIGeopolitics, AI, and Cybersecurity: Insights From RSAC 2026Dark ReadingQualcomm joins MassRobotics, to support startups with Dragonwing Robotics HubRobotics Business ReviewDisney, OpenAI Eye Future Deal After Sora Shutdown - lamag.comGoogle News: OpenAIMarc Andreessen Is Right That AI Isn't Killing Jobs. Interest Rate Hikes AreHacker News AI TopOne New Thing: Louisiana Universities Pave the Way for Widespread AI Education - U.S. News & World ReportGNews AI educationBlack Hat USADark ReadingBlack Hat AsiaAI BusinessNvidia Needs to Remind Itself What PC Gamers Actually WantGizmodoAI’s affect on communities, students, staff - USI | student newspaperGoogle News: Generative AI2 Artificial Intelligence (AI) Stocks I'd Buy With $1,000 Before They Rebound From the Tech Sell-Off - The Motley FoolGoogle News: AIGoogle Updates Gemini API Pricing Tiers for Optimization - Intellectia AIGoogle News: GeminiIran Says It Hit Oracle Facilities in UAEGizmodoInside the ethics of artificial intelligence - New Day NW - KING5.comGoogle News: AIMicrosoft Generative AI Report: The 40 Jobs Most Disrupted Jobs & The 40 Most Secure Jobs - HackerNoonGoogle News: Generative AIGeopolitics, AI, and Cybersecurity: Insights From RSAC 2026Dark ReadingQualcomm joins MassRobotics, to support startups with Dragonwing Robotics HubRobotics Business ReviewDisney, OpenAI Eye Future Deal After Sora Shutdown - lamag.comGoogle News: OpenAIMarc Andreessen Is Right That AI Isn't Killing Jobs. Interest Rate Hikes AreHacker News AI TopOne New Thing: Louisiana Universities Pave the Way for Widespread AI Education - U.S. News & World ReportGNews AI education
AI NEWS HUBbyEIGENVECTOREigenvector

Advanced Compact Patterns for Web3 Developers

DEV Communityby Nasihudeen JimohApril 2, 202614 min read0 views
Source Quiz

Introduction If you've spent years building on EVM chains, Midnight's architecture might feel like a paradigm shift. On Ethereum, you push computation onto the blockchain itself. On Midnight, you do the opposite you move computation off-chain and prove it correctly using zero-knowledge proofs. This isn't just a different implementation detail. It fundamentally changes how you think about state management, data disclosure, and circuit design. Samantha's foundational guide introduced the three-part structure of Midnight contracts: the public ledger, zero-knowledge circuits, and local computation. But understanding the basics and architecting production systems are two different challenges. This guide dives into the patterns that separate working prototypes from robust systems. We'll explore

Introduction

If you've spent years building on EVM chains, Midnight's architecture might feel like a paradigm shift. On Ethereum, you push computation onto the blockchain itself. On Midnight, you do the opposite you move computation off-chain and prove it correctly using zero-knowledge proofs.

This isn't just a different implementation detail. It fundamentally changes how you think about state management, data disclosure, and circuit design.

Samantha's foundational guide introduced the three-part structure of Midnight contracts: the public ledger, zero-knowledge circuits, and local computation. But understanding the basics and architecting production systems are two different challenges.

This guide dives into the patterns that separate working prototypes from robust systems. We'll explore how witnesses enable privacy boundaries, why commitments matter more than direct state, how to optimize circuits for real-world constraints, and how to compose multiple private contracts without leaking metadata.

By the end, you'll have concrete strategies for building systems that maintain privacy guarantees while managing the practical tradeoffs of Web3 applications.

Witnesses & Selective Disclosure

Understanding Witnesses Beyond Function Calls

In EVM contracts, all data available to a function is deterministic. The blockchain is your single source of truth. In Compact, witnesses invert this model: witnesses are the only source of truth the contract doesn't control.

// A witness declares a contract's dependency on external data witness getUserSecret(): Bytes<32>; witness getProofOfAssets(userId: Uint<64>): AssetsProof;

Enter fullscreen mode

Exit fullscreen mode

When you declare a witness, you're saying: "This contract's logic depends on data I cannot verify on-chain. It's the application's responsibility to provide this correctly."

This creates a critical security boundary. The contract trusts the application to supply honest witnesses, but the proof system validates that the application used those witnesses correctly.

The Witness-Disclosure Loop

Real-world contracts don't just consume witnesses they combine witness data with disclosed state to create privacy preserving outcomes.

Consider an age verification system:

pragma language_version 0.22; import CompactStandardLibrary;

// Public ledger: only record that someone proved they're eligible export ledger ageVerified: Map, Boolean>; export ledger verificationRound: Counter;

// Private witness: the user's actual birthdate (never on-chain) witness getUserBirthDate(): Uint<32>; // Unix timestamp

// Derived public key with round counter to prevent replay circuit derivePublicIdentity(round: Field, secret: Bytes<32>): Bytes<32> { return persistentHash>>( [round as Bytes<32>, secret] ); }

// Main circuit: prove age without revealing birthdate export circuit verifyAge(secret: Bytes<32>, minAge: Uint<32>): [] { // Get private birthdate (witness - not on-chain) const birthDate = getUserBirthDate();

// Compute age const currentTimestamp: Uint<32> = 1704067200; // Updated by app const age = currentTimestamp - birthDate;

// Private check: verify age requirement assert(age >= minAge, "Age requirement not met");

// Public disclosure: only record that verification happened const identity = derivePublicIdentity(verificationRound.roundNumber, secret); ageVerified = disclose(identity, true); verificationRound.increment(1); }`

Enter fullscreen mode

Exit fullscreen mode

Key pattern: The witness data (getUserBirthDate) is never directly disclosed. Instead, you compute a predicate over it (age >= minAge), and then disclose only the outcome the user consents to.

The tradeoff: The application code that supplies the witness must be trusted. If a malicious DApp sends a false birthdate, the proof system can't detect it—but it will prove the user accepted false data. This is why witness sourcing matters as much as circuit logic.

Practical Consideration: Witness Sourcing

Where do witnesses come from in real applications?

  • User-held secrets: API keys, private keys, personal data

  • External APIs: Proof-of-reserve attestations, oracle feeds, credential issuers

  • Zero-knowledge proofs themselves: A sub-proof generated off-chain that proves something about external data

  • Trusted hardware: TEE attestations or trusted execution environment outputs

Each source has different security properties. A witness from a user's secret key is as strong as that key's protection. A witness from an untrusted API might need cryptographic verification itself.

For example, if your witness comes from an API like "get current asset price," the application must either:

  • Trust the API (weak)

  • Verify the API response against multiple oracles (medium)

  • Require the API to provide a signature from a trusted source (better)

  • Use sub-proofs to prove the API data meets certain criteria without revealing it (best)

Commitments & Zero-Knowledge Proof Architecture

From State Mutation to Commitment Schemes

EVM developers are accustomed to direct state mutations:

// Ethereum: modify state directly mapping(address => uint256) balance; balance[user] += amount;

Enter fullscreen mode

Exit fullscreen mode

In Midnight, public state mutations must be proven by a zero-knowledge circuit. This means your public state must be designed around commitment schemes cryptographic structures that let you prove you know a value without revealing it.

Here's the conceptual bridge:

  • EVM thinking: State is a mutable cell. Update it directly.

  • Midnight thinking: State is a commitment to a value. Prove you know the value, then update the commitment.

Building a Private Ledger with Commitments

Let's walk through a private token transfer system:

pragma language_version 0.22; import CompactStandardLibrary;

// Public state: only commitments to balances, never actual amounts export ledger balanceCommitment: Map, Field>; export ledger totalSupply: Uint<128>; export ledger transferRound: Counter;

// Private data structure (never on-chain, only proven) struct Account { owner: Bytes<32>, balance: Uint<128>, nonce: Uint<64> }

// Witness: the actual account data, held privately by user witness getAccount(): Account; witness getNullifierSecret(): Bytes<32>;

// Helper: derive a nullifier to prevent double-spending circuit deriveNullifier(nonce: Uint<64>, secret: Bytes<32>): Field { return persistentHash>( [persistentHash>(nonce as Bytes<32>), persistentHash>(secret)] ) as Field; }

// Helper: commitment to an account circuit commitToAccount(account: Account, salt: Bytes<32>): Field { return persistentHash>( [persistentHash(account), persistentHash>(salt)] ) as Field; }

// Main circuit: prove a valid token transfer export circuit transfer( recipient: Bytes<32>, amount: Uint<128>, salt: Bytes<32>, newSalt: Bytes<32> ): [] { // Load private account data const account = getAccount(); const nullifierSecret = getNullifierSecret();

// Verify the account commitment exists const oldCommitment = commitToAccount(account, salt); assert( balanceCommitment[account.owner] == oldCommitment, "Account commitment mismatch" );

// Private verification: user has sufficient balance assert(account.balance >= amount, "Insufficient balance");

// Compute new account state (private) const newAccount: Account = [ owner: account.owner, balance: account.balance - amount, nonce: account.nonce + 1 ];

// Create nullifier to prevent replay const nullifier = deriveNullifier(account.nonce, nullifierSecret);

// Update public state with new commitment const newCommitment = commitToAccount(newAccount, newSalt); balanceCommitment = disclose(account.owner, newCommitment);

// Record nullifier to prevent double-spend // In a real system, this would be a set of spent nullifiers // For now, we disclose it as proof of spending disclose(nullifier);

// Recipient balance update (simplified: assume recipient pre-existed) // In production, you'd handle account creation transferRound.increment(1); }`

Enter fullscreen mode

Exit fullscreen mode

The Commitment Tradeoff

This approach provides strong privacy but requires careful design:

Advantages:

  • Balances are never visible on-chain (only commitments)

  • Transfers reveal no information except that a transfer occurred

  • The system is composable with other private circuits

Costs:

  • Every balance update requires a full commitment recomputation

  • Clients must store balance commitments locally (or query from a private oracle)

  • Replay protection requires tracking spent nullifiers

  • The circuit is more complex, leading to larger proofs and longer proving times

Real world consideration: For most applications, you won't implement full commitment schemes from scratch. You'll use Midnight's standard library, which provides optimized versions. But understanding the underlying structure helps you choose the right patterns for your use case.

Circuit Optimization

Why Circuit Optimization Matters

Compact circuits must be bounded at compile time. You can't have unbounded loops or recursive calls. This constraint exists because every circuit must compile to a fixed-size zero-knowledge proof.

For EVM developers, this is a significant mindset shift. On Ethereum, you pay gas for computation. On Midnight, you accept predetermined computation bounds.

// This won't compile unbounded recursion circuit traverse(node: TreeNode): Uint<64> {  if (node.left == null) {  return node.value;  } else {  return traverse(node.left);  } }

// This works—bounded by tree depth export circuit traverseFixed<#DEPTH>( node: TreeNode, path: Vector<#DEPTH, Boolean> ): Uint<64> { let current = node; for (let i = 0; i < #DEPTH; i++) { if (path[i]) { current = current.right; // Assumes node structure allows this } else { current = current.left; } } return current.value; }`

Enter fullscreen mode

Exit fullscreen mode

Optimization Strategies

1: Vectorization and Batching

For operations on multiple items, vectorize instead of looping:

// Less efficient: separate proofs for each item export circuit verifyAge(secret: Bytes<32>, minAge: Uint<32>): [] {  const birthDate = getUserBirthDate();  assert(currentTime - birthDate >= minAge, "Too young"); }

// More efficient: batch verification export circuit verifyAgesInBatch<#N>( secrets: Vector<#N, Bytes<32>>, minAges: Vector<#N, Uint<32>> ): [] { for (let i = 0; i < #N; i++) { // Witness supplies ages for all users const birthDates = getUserBirthDates(i); assert( currentTime - birthDates >= minAges[i], "Age check failed" ); } }`

Enter fullscreen mode

Exit fullscreen mode

2: Lazy Evaluation with Merkle Trees

Instead of processing all data inline, use Merkle trees to prove membership:

// Direct approach: verify all items (O(n) circuit size) export circuit verifyAllBalances<#N>(  balances: Vector<#N, Uint<128>>,  totalRequired: Uint<128> ): [] {  let sum: Uint<128> = 0;  for (let i = 0; i < #N; i++) {  sum = sum + balances[i];  }  assert(sum >= totalRequired, "Insufficient total"); }

// Optimized: verify membership in Merkle tree (O(log n) circuit size) export circuit verifyBalanceProof( balance: Uint<128>, merkleProof: Vector<32, Field>, // Log2(2^32) = 32 levels merkleRoot: Field, leaf_index: Uint<32> ): [] { // Recompute leaf and verify path const leaf = persistentHash>(balance) as Field; let current = leaf;

for (let i = 0; i < 32; i++) { const proofElement = merkleProof[i]; // Combine in canonical order to prevent tree structure attacks if (leaf_index & (1 << i) == 0) { current = persistentHash>([current, proofElement]) as Field; } else { current = persistentHash>([proofElement, current]) as Field; } }

assert(current == merkleRoot, "Merkle proof failed"); }`

Enter fullscreen mode

Exit fullscreen mode

3: Proof Aggregation

When you have multiple privacy preserving properties to prove, you have two choices: prove them all in one circuit (larger proof), or split into separate circuits (multiple proofs, sequential verification).

// Single circuit: proves age AND asset ownership // Proof size: large, proving time: high export circuit verifyAgeAndAssets(  secret: Bytes<32>,  minAge: Uint<32>,  assetsProof: AssetsProof ): [] {  const birthDate = getUserBirthDate();  assert(currentTime - birthDate >= minAge, "Too young");

const assets = getAssets(assetsProof); assert(assets.value >= 100000, "Insufficient assets"); }

// Split circuits: separate concerns, compose on app level // Proof size: smaller per circuit, proving time: faster export circuit verifyAge(secret: Bytes<32>, minAge: Uint<32>): Bytes<32> { const birthDate = getUserBirthDate(); assert(currentTime - birthDate >= minAge, "Too young"); return disclose(persistentHash>(secret)); }

export circuit verifyAssets(assetsProof: AssetsProof): Bytes<32> { const assets = getAssets(assetsProof); assert(assets.value >= 100000, "Insufficient assets"); return disclose(persistentHash>(assetsProof)); }

// Application composes both proofs // Tradeoff: two proofs to verify, but faster to prove each one`

Enter fullscreen mode

Exit fullscreen mode

Benchmarking Your Circuits

Different circuit structures have dramatic performance differences. Use this framework to evaluate:

Structure Pros Cons Use When

Direct computation Simple, straightforward Large proof, slow Small bounded operations

Merkle proof verification Logarithmic size, scales well Higher cryptographic complexity Membership checks in large sets

Vectorized batching Efficient for repeated ops Requires uniform structure Batch processing many similar items

Split circuits Faster per-circuit proving Coordination overhead When proofs are logically independent

Multi-Contract Privacy Architecture

Composing Private Contracts

Midnight's biggest strength is that multiple private contracts can interact while maintaining privacy boundaries. However, composing contracts introduces new considerations:

Problem: The Metadata Leak

Even if all data is encrypted, metadata can leak information:

// Privacy leak: contract call pattern reveals intent export circuit buyAsset(assetId: Uint<64>): Bytes<32> {  // If specific assetIds always correlate with specific users,  // blockchain analysis can link buyers to assets even without seeing amounts  const proof = getOwnershipProof(assetId);  verify(proof);  return disclose(persistentHash>(assetId)); }

// Mitigated: hide specific asset, batch with dummy calls export circuit batchBuyAssets<#N>( assetIds: Vector<#N, Uint<64>>, proofs: Vector<#N, Bytes<32>>, isReal: Vector<#N, Boolean> ): Vector<#N, Bytes<32>> { let results: Vector<#N, Bytes<32>> = []; for (let i = 0; i < #N; i++) { // Verify proof only if real purchase (circuits execute either way) if (isReal[i]) { verify(proofs[i]); } results[i] = disclose(persistentHash>(assetIds[i])); } return results; }`

Enter fullscreen mode

Exit fullscreen mode

Pattern: Shielded Contract Composition

When one private contract depends on another, you need a protocol for safe interaction:

pragma language_version 0.22; import CompactStandardLibrary;

// Contract A: Identity registry export ledger identityCommitment: Map, Field>;

export circuit registerIdentity(publicKey: Bytes<32>): [] { const commitment = persistentHash>(publicKey) as Field; identityCommitment = disclose(publicKey, commitment); }

// Contract B: Private voting (depends on Contract A) export ledger voteCommitment: Map; // (identityHash, voteHash)

export circuit castVote( publicKey: Bytes<32>, voteChoice: Uint<8>, salt: Bytes<32> ): [] { // Prove participation in Contract A without revealing identity const commitment = persistentHash>(publicKey) as Field; assert(identityCommitment[publicKey] == commitment, "Not registered");

// Cast vote privately const voteHash = persistentHash>( [persistentHash>(voteChoice) as Field, persistentHash>(salt) as Field] ) as Field;

voteCommitment = disclose(commitment, voteHash); }`

Enter fullscreen mode

Exit fullscreen mode

Key insight: Contract B proves it respects Contract A's invariants (the user is registered) without revealing the user's identity. This is the foundation of composable privacy.

Considerations: State Consistency

When multiple contracts touch shared state, you must be careful about ordering:

// Race condition possible: State updated after verification export circuit transferWithFeeShare(  amount: Uint<128>,  feeRecipient: Bytes<32> ): [] {  const balance = getBalance(); // Witness  assert(balance >= amount + fee, "Insufficient");

// Race condition: if another proof updates fee rate before on-chain execution, // this assertion might have been based on stale assumptions const currentFee = feeContract.queryFee(); }

// Fixed: Include fee data in the proof export circuit transferWithFeeShare( amount: Uint<128>, feeRecipient: Bytes<32>, expectedFeeRate: Uint<16>, // App provides expected fee feeProof: Bytes<32> // Proof that fee rate matches ): [] { const balance = getBalance();

// Verify fee was what we expected at proof time verify(feeProof);

const fee = (amount * expectedFeeRate) / 100000; assert(balance >= amount + fee, "Insufficient"); }`*

Enter fullscreen mode

Exit fullscreen mode

Real-World Tradeoffs

Decision Matrix

Use Case Pattern Witness Source Proof Strategy Notes

Privacy-first tokens Commitments + Nullifiers User secret key Split by operation type Requires nullifier tracking

KYC/AML compliance Age/identity verification Credential issuer Selective disclosure Issuer must be trusted

DAO voting Shielded voting + identity registry User secret, registry contract Batched dummy votes Metadata still visible (voting time)

Asset swaps DEX with private pricing** Oracle feeds, user orders Batch matching Requires MEV-resistant ordering**

Debugging & Testing Advanced Patterns

Console Logging in Compact

While developing, use logging carefully (it's not available in production proofs):

export circuit debugTransfer(  recipient: Bytes<32>,  amount: Uint<128> ): [] {  const balance = getBalance();

// Debug logging helps during development assert(balance >= amount, "Insufficient");

// The proof doesn't include debug output, but your app runner sees it }`

Enter fullscreen mode

Exit fullscreen mode

Testing Strategy for Circuits

Since circuits must be proven, test thoroughly before deployment:

// TypeScript test harness import { Contract } from '@midnight-protocol/sdk';

const contract = new Contract(compiledCircuit);

// Test 1: Valid transfer const validTransfer = await contract.transfer({ recipient: publicKey, amount: 1000n, salt: randomSalt, newSalt: newRandomSalt });

assert(validTransfer.proof != null, 'Valid transfer should produce proof');

// Test 2: Insufficient balance should fail const invalidTransfer = await contract.transfer({ recipient: publicKey, amount: 999999999999n, salt: randomSalt, newSalt: newRandomSalt });

assert(invalidTransfer.proof == null, 'Invalid transfer should not produce proof');`

Enter fullscreen mode

Exit fullscreen mode

Conclusion

Advanced Compact patterns aren't just optimizations they're architectural decisions that shape what's possible in your application.

As you move from learning Compact to building production systems, keep these principles in mind:

  • Witnesses are your privacy boundary. Choose witness sources carefully; they determine your security model.

  • Commitments enable privacy at scale. Direct state disclosure doesn't mix with zero-knowledge proofs; use commitments instead.

  • Circuits must be bounded, but cleverly. Vectorization, Merkle trees, and proof aggregation let you handle complexity without exceeding bounds.

  • Composition requires explicit coordination. Multiple private circuits can interact, but metadata and state consistency need careful handling.

  • Privacy and usability are in tension. Batching dummy transactions protects metadata but increases proof sizes. Splitting circuits proves faster but requires coordination. Choose based on your threat model.

Midnight gives you powerful tools for building privacy-first applications. Mastering these patterns lets you use them effectively.

Further Reading

  • Midnight Language Reference: Full Compact syntax and semantics

  • Explicit Disclosure Deep Dive: Understanding the disclose() wrapper and threat models

  • Zero Knowledge Proof Fundamentals: If you want to understand the cryptography behind Compact circuits

  • Previous in this series: Learning Web3 from the Ground Up by Samantha Holstine

Was this article helpful?

Sign in to highlight and annotate this article

AI
Ask AI about this article
Powered by Eigenvector · full article context loaded
Ready

Conversation starters

Ask anything about this article…

Daily AI Digest

Get the top 5 AI stories delivered to your inbox every morning.

Knowledge Map

Knowledge Map
TopicsEntitiesSource
Advanced Co…modelbenchmarkavailableversionupdateproductDEV Communi…

Connected Articles — Knowledge Graph

This article is connected to other articles through shared AI topics and tags.

Knowledge Graph100 articles · 157 connections
Scroll to zoom · drag to pan · click to open

Discussion

Sign in to join the discussion

No comments yet — be the first to share your thoughts!