DEV Community

richard202605
richard202605

Posted on

Security Checklist for Midnight dApps Before Deployment: A Developer's Guide

Security Checklist for Midnight dApps Before Deployment

Midnight Network brings privacy-preserving smart contracts to Web3 through zero-knowledge proofs and programmable confidentiality. But with great privacy power comes great security responsibility. This checklist will help you catch common vulnerabilities before your dApp goes live.


Why This Checklist Matters

Midnight's Compact language enforces privacy by default — all data stays private unless you explicitly disclose() it. This is powerful, but it also means:

  • One misplaced disclose() can leak sensitive data permanently
  • Witness functions run outside ZK circuits and can be manipulated
  • The ownPublicKey() function has a known vulnerability that many developers miss
  • Replay protection requires careful implementation of nonces and nullifiers

Let's walk through each security area systematically.


Pre-Deployment Checklist

✅ 1. disclose() Audit — No Secret Leaks

disclose() is the only mechanism for moving private data to the public ledger. Every disclose() call in your code is a conscious decision to make something public.

What to check:

// ❌ BAD: Early disclosure exposes the value for all subsequent operations
export circuit store(flag: Boolean): [] {
  const secret = disclose(getSecret());  // Public too early!
  const derived = computeValue(secret);  // `secret` is now visible
  result = disclose(flag) ? derived : 0;
}

// ✅ GOOD: Disclose only at the point of use
export circuit store(flag: Boolean): [] {
  const secret = getSecret();
  const derived = computeValue(secret);  // Still private
  result = disclose(flag) ? disclose(derived) : 0;  // Specific, targeted disclosure
}
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • [ ] Search your codebase for every disclose() call
  • [ ] Verify each disclosure is intentional and minimal
  • [ ] Ensure disclose() is positioned as close to the usage point as possible
  • [ ] Check that no code path accidentally discloses values that should remain private
  • [ ] Use underscore prefixes (_sk, _secret) for values that must never be disclosed

Pro tip: The Compact compiler will catch attempts to assign private values to ledger fields without disclose(), but it won't catch premature disclosures. Manual review is essential.


✅ 2. ownPublicKey() Usage Review (Known Vulnerability)

This is one of the most common security mistakes in Midnight development.

⚠️ WARNING: Do NOT use ownPublicKey() for caller verification!

ownPublicKey() is technically a witness function. This means each user's frontend can produce a malicious return value. An attacker can modify their local witness implementation to return any public key they choose.

// ❌ CRITICAL VULNERABILITY: Using ownPublicKey() for authorization
export circuit transfer(to: Bytes<32>, amount: Uint<64>): [] {
  const sender = ownPublicKey();  // Attacker can return ANY key!
  assert(balances.member(sender), "No balance");
  // ... transfer logic using spoofed sender
}

// ✅ CORRECT: Hash-based authentication
witness secretKey(): Bytes<32>;

circuit publicKey(_sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([
    pad(32, "midnight:auth:pk"),
    _sk
  ]);
}

export circuit authorizedOperation(newValue: Bytes<32>): [] {
  const _sk = secretKey();
  const pk = publicKey(_sk);
  assert(disclose(pk) == authority, "Authorization failed");
  ledgerValue = disclose(newValue);
}
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • [ ] Search for all ownPublicKey() calls in your codebase
  • [ ] Verify none are used as the sole authorization mechanism
  • [ ] Replace any authorization logic with hash-based patterns
  • [ ] If you must use ownPublicKey(), add additional verification layers

✅ 3. Replay Protection Verification (Nonces and Nullifiers)

Midnight uses the commitment/nullifier pattern (from Zerocash/Zswap) to prevent double-spending without revealing which resource was spent.

How nullifiers work:

export ledger usedNullifiers: Set<Bytes<32>>;

circuit nullifier(secretKey: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([
    pad(32, "nullifier-domain"),  // Domain separator!
    secretKey
  ]);
}

export circuit spend(secretKey: Bytes<32>): [] {
  const nul = nullifier(secretKey);
  assert(!usedNullifiers.member(nul), "Already spent");
  usedNullifiers.insert(disclose(nul));
}
Enter fullscreen mode Exit fullscreen mode

Critical requirements:

  • [ ] Domain separators: Commitments and nullifiers MUST use different domain prefixes to prevent hash collision attacks
    • Good: "nullifier-my-dapp-v1" vs "commitment-my-dapp-v1"
    • Bad: Using the same prefix for both
  • [ ] No randomness reuse: Never reuse randomness across commitments — this enables linking and breaks privacy
  • [ ] Use persistentHash: For values stored in ledger state (guaranteed consistent across upgrades)
  • [ ] Round counters: Include time-bound counters for operations to prevent replay of old proofs

✅ 4. Exported Ledger Field Review

Every export ledger field is publicly visible on-chain. Review each one:

export ledger balance: Counter;
export ledger authority: Bytes<32>;
export ledger usedNullifiers: Set<Bytes<32>>;
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • [ ] Verify each ledger field genuinely needs to be public
  • [ ] Check that no private data is accidentally stored in ledger fields
  • [ ] Ensure ledger field types match their intended use (Counter vs Uint, etc.)
  • [ ] Review access patterns — who can read and modify each field?

✅ 5. Witness Implementation Correctness

Witnesses are off-chain TypeScript functions that run outside the ZK circuit. They are NOT cryptographically verified.

// Witness implementation in TypeScript
export const witnesses = {
  secretKey: ({ privateState }: WitnessContext<Ledger, PrivateState>) =>
    [privateState, privateState.secretKey],

  getUserBalance: ({ privateState }: WitnessContext<Ledger, PrivateState>) =>
    [privateState, privateState.balance],
};
Enter fullscreen mode Exit fullscreen mode

Security implications:

  • Each user provides their own witness implementation
  • An attacker can return any value from a witness function
  • Contract logic must never trust witness values without validation

Checklist:

  • [ ] Every witness output is validated with assert before use in circuits
  • [ ] No critical logic depends solely on witness return values
  • [ ] Witness functions return consistent [PrivateState, ReturnValue] tuples
  • [ ] No external dependencies in witnesses that might change between invocations
  • [ ] Test witness implementations for consistency across multiple calls

✅ 6. Version Compatibility Confirmation

Midnight's toolchain evolves rapidly. Before deployment:

Checklist:

  • [ ] Verify pragma language_version matches your installed Compact compiler
  • [ ] Check that all dependencies (midnight-mcp, proof-server) are compatible versions
  • [ ] Review the Midnight changelog for breaking changes
  • [ ] Test compilation with the exact version specified in your contract

✅ 7. Proof Generation Testing on Testnet

Before mainnet, verify your proof generation pipeline works end-to-end.

Testnet tiers:
| Tier | Purpose | Tokens |
|------|---------|--------|
| Local Devnet | Development | Unlimited |
| Preview | Integration testing | Test NIGHT |
| Preprod | Final validation | Preprod faucet |

Checklist:

  • [ ] Deploy contract to Local Devnet and verify all circuits
  • [ ] Test proof generation for every circuit path
  • [ ] Verify proof-server handles concurrent requests
  • [ ] Test edge cases: zero values, maximum values, empty inputs
  • [ ] Run the full transaction lifecycle: deploy → interact → verify state
  • [ ] Test on Preprod before mainnet deployment

Proof server verification:

# Check proof server is running
curl http://localhost:6300/health

# Test proof generation
compact prove --contract ./managed/my-contract \
  --circuit myCircuit \
  --inputs ./test-inputs.json
Enter fullscreen mode Exit fullscreen mode

Common Security Pitfalls

Pitfall 1: Trusting Frontend Input

Never trust data from the frontend without on-chain verification. The user controls their client.

Pitfall 2: Missing Domain Separation

Always use unique domain separators for different hash operations:

// Good: Different domains for different purposes
persistentHash<Vector<2, Bytes<32>>>([pad(32, "commitment-v1"), data])
persistentHash<Vector<2, Bytes<32>>>([pad(32, "nullifier-v1"), data])
Enter fullscreen mode Exit fullscreen mode

Pitfall 3: Premature Disclosure

Position disclose() as late as possible in the computation chain.

Pitfall 4: Unvalidated Witness Data

Always assert witness outputs before using them in circuit logic.

Pitfall 5: Ignoring Linkability

Use round counters or other mechanisms to break transaction linkability when privacy matters.


Quick Reference: Security Patterns

Pattern Use Case Key Point
Hash-based auth Caller verification Don't use ownPublicKey() alone
Commit-reveal Sealed bids, auctions Two-phase commitment
Domain separators All hash operations Unique prefixes per purpose
Round counters Breaking linkability Invalidate old keys
Late disclosure All disclose() calls Minimize exposure window

Resources


Summary

Before deploying your Midnight dApp, verify:

  1. disclose() audit — Every disclosure is intentional and minimal
  2. ownPublicKey() review — No reliance on this for authorization
  3. Replay protection — Proper nonces, nullifiers, and domain separators
  4. Ledger fields — No accidental public exposure of private data
  5. Witness validation — All witness outputs are asserted before use
  6. Version compatibility — Compiler and dependencies aligned
  7. Testnet verification — Full proof generation pipeline tested

Privacy-preserving dApps are powerful, but they require extra diligence. Run this checklist before every deployment, and your users' data will stay safe.


📌 Disclaimer: This guide is for educational purposes. Always stay updated with the latest Midnight Network documentation and security advisories. The security landscape evolves rapidly.


Found this helpful? Follow for more Web3 security content. Questions? Drop a comment below.

Top comments (0)