INTRODUCTION
Blockchain transparency is a double edged sword. Every transaction, every state change, and every interaction sits on a public ledger for anyone to inspect. For many applications this is a feature. For applications handling sensitive data it is a dealbreaker.
Midnight solves this with a dual state architecture. Your DApp maintains both public state visible on chain and private state stored locally with users. The bridge between these two worlds is selective disclosure: the ability to prove something is true about your private data without revealing the data itself.
This tutorial covers the patterns that make selective disclosure work in Compact. You will learn how to use disclose() correctly, understand what information is safe to expose, implement domain separated hashing to prevent user tracking across different properties, and apply a systematic privacy audit to your own contracts.
Prerequisites
Before you continue, make sure you have:
- The Compact toolchain installed (follow the installation guide)
- A basic understanding of Compact syntax (review the Hello World tutorial)
- The Midnight MCP tool available for validating your code
What Is Selective Disclosure?
In traditional smart contract development every input to a function becomes public the moment a transaction is submitted. There is no privacy. The contract sees everything and so does everyone else.
Compact changes this. Circuit parameters are private by default. Consider this simple function signature:
export circuit storeMessage(newMessage: Opaque<"string">): [] {
// newMessage is private here
}
The Opaque<"string"> type tells the compiler that newMessage is a private input. It exists in the user's local execution environment but never appears on chain in its raw form.
Selective disclosure happens when you decide to move data from the private context to the public ledger. You do this with the disclose() function:
export circuit storeMessage(newMessage: Opaque<"string">): [] {
message = disclose(newMessage);
}
This single line makes a deliberate choice. The message content becomes public state stored in the message ledger variable. Everyone can now read it.
But selective disclosure is about more than just toggling visibility. It is about revealing proofs instead of raw data. You can disclose that a condition is met without disclosing the condition itself. You can disclose a hash commitment now and reveal the preimage later. You can disclose aggregated results while keeping individual inputs hidden.
disclose() Usage Patterns
The disclose() function is the only mechanism for moving data from private context to public state. Understanding its patterns is fundamental to building privacy preserving DApps.
Pattern 1: Direct Disclosure
The simplest pattern. You have private data and you want it on chain.
pragma language_version 0.22;
export ledger publicCounter: Counter;
export circuit increment(amount: Opaque<Uint<32>>): [] {
let increment_by = disclose(amount);
publicCounter.increment(increment_by);
}
Here the user provides a private amount. The circuit discloses it and increments the public counter. The amount becomes visible to everyone. Use this pattern only when the data is intentionally non sensitive.
Pattern 2: Conditional Disclosure with Assertions
Often you want to disclose something only after verifying it meets certain conditions.
pragma language_version 0.22;
export ledger allowedAmount: Uint<32>;
export circuit submitAmount(amount: Opaque<Uint<32>>): [] {
let value = disclose(amount);
assert(value <= allowedAmount, "Amount exceeds allowed limit");
// Proceed with business logic using the disclosed value
}
The assertion runs in the ZK circuit. If it fails the entire transaction reverts. The amount is disclosed but only after proving it satisfies the constraint.
Pattern 3: Disclosing Derived Values
This is where selective disclosure becomes powerful. Instead of disclosing raw inputs you disclose a computed result that reveals only what is necessary.
pragma language_version 0.22;
import CompactStandardLibrary;
export ledger userEligibility: Map<Bytes<32>, bool>;
export circuit proveEligibility(
birthYear: Opaque<Uint<16>>,
currentYear: Uint<16>
): [] {
let age = currentYear as Uint<32> - (birthYear as Uint<32>);
let isAdult = age >= 18;
const _sk = localSk();
let pubKey = getDappPublicKey(_sk);
userEligibility.insert(disclose(pubKey), disclose(isAdult));
}
The user provides their birth year as a private input. The circuit computes their age and determines adulthood status. Only the boolean result isAdult is disclosed. The actual birth year never leaves the private context.
Pattern 4: Commit Now, Reveal Later
This two phase pattern is essential for auctions, voting, and any scenario requiring a commitment period.
pragma language_version 0.22;
import CompactStandardLibrary;
export ledger commitment: Bytes<32>;
export ledger revealedValue: Uint<32>;
export circuit commit(value: Opaque<Uint<32>>, salt: Opaque<Bytes<32>>): [] {
let hash = persistentHash<Vector<2, Bytes<32>>>([
value as Bytes<32>,
salt
]);
commitment = disclose(hash);
}
export circuit reveal(value: Opaque<Uint<32>>, salt: Opaque<Bytes<32>>): [] {
let computedHash = persistentHash<Vector<2, Bytes<32>>>([
value as Bytes<32>,
salt
]);
assert(computedHash == commitment, "Commitment mismatch");
revealedValue = disclose(value);
}
In the commit phase the user submits a hash of their value and a salt. In the reveal phase they provide both original inputs. The circuit verifies the hash matches before disclosing the value. This prevents front running and ensures users cannot change their submission after seeing others.
What Is Safe to Disclose vs What Leaks Privacy
The line between safe and unsafe disclosure is not always obvious. Here is a framework for evaluating your disclosure decisions.
Safe to Disclose
Aggregates and counts. The total number of participants in an event reveals nothing about individual identities.
Boolean results of private checks. Knowing that someone is over 18 does not reveal their exact age.
Public keys derived with DApp specific hashing. See the domain separation section below for why this matters.
Hashes and commitments. A hash without its preimage reveals nothing about the underlying data assuming sufficient entropy.
Enumerated states. Disclosing that an auction is OPEN or CLOSED is necessary for coordination.
Unsafe to Disclose
Direct personal identifiers. Names, email addresses, government ID numbers. Never put these on chain even in encrypted form without careful consideration.
Linkable pseudonyms. If you disclose the same public key across multiple interactions within the same DApp you create a linkage profile. Always use DApp specific key derivation.
Precise numeric values when only a range is needed. Disclosing an exact salary of 84750 when you only need to prove it exceeds 50000 leaks unnecessary information.
Correlatable timestamps. The exact block height of a user's interaction combined with other disclosed data can create timing correlation attacks.
Raw private keys or seeds. This should be obvious but it bears repeating. Never write disclose(localSk()) in your contract.
The Linkability Problem
Consider this vulnerable pattern:
// DO NOT USE - Vulnerable to cross interaction tracking
export circuit registerUser(): [] {
const _sk = localSk();
let pubKey = publicKey(_sk); // Standard public key derivation
registeredUsers.insert(disclose(pubKey));
}
export circuit submitVote(): [] {
const _sk = localSk();
let pubKey = publicKey(_sk);
// Same pubKey used across circuits
}
If the same public key appears in multiple contracts or even multiple circuits within the same contract, an observer can link all of that user's activity together. This defeats the privacy guarantees you are trying to provide.
Domain Separated Hashing for Cross Property Unlinkability
Domain separation is the technique of generating different identifiers for the same user across different contexts. It ensures that an observer cannot link a user's activity in one part of your DApp to their activity in another.
The Pattern
The Midnight example contracts demonstrate this pattern consistently. Here is the implementation from the Private Guest List contract:
export circuit getDappPublicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "guest-list:pk:"),
_sk
]);
}
The user's private key is hashed with a domain specific string. The resulting public key is unique to this DApp. Even if the same user interacts with another DApp their identifier will be completely different because the domain string differs.
Multiple Domains Within a Single DApp
You can extend this pattern to create separate unlinkable identities for different features within the same DApp:
pragma language_version 0.22;
import CompactStandardLibrary;
export circuit getVoterPublicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "election:voter:"),
_sk
]);
}
export circuit getCandidatePublicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "election:candidate:"),
_sk
]);
}
A user who is both a voter and a candidate in the same election DApp will have two different unlinkable public keys. Their voting activity cannot be correlated with their candidate registration.
The Battleship Example
The Battleship contract uses domain separation to prevent cross game tracking:
circuit getDappPubKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "battleship:pk:"),
_sk
]);
}
Each game instance could further extend this by incorporating a game ID:
circuit getGameSpecificPubKey(_sk: Bytes<32>, gameId: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([
pad(32, "battleship:game:"),
gameId,
_sk
]);
}
Now the same player has a different unlinkable identity in every game they play.
Privacy Audit Checklist for Developers
Before deploying a Compact contract conduct this systematic privacy audit. Each question forces you to examine a specific aspect of your disclosure patterns.
1. Disclosure Inventory
List every occurrence of disclose() in your contract. For each one answer:
- What specific data is being disclosed?
- Why must this data be public?
- Could a proof of a property replace the raw data?
If you cannot justify why something must be public reconsider whether it belongs on chain at all.
2. Linkability Analysis
For each disclosed identifier ask:
- Does this identifier appear in multiple circuits within the same contract?
- Does this identifier appear in other contracts deployed by this DApp?
- Could an observer correlate this identifier with activity in other DApps?
If any answer is yes implement domain separation using persistentHash with a unique domain string.
3. Timing Attack Surface
Examine your contract for timing sensitive disclosures:
- Are there state transitions that reveal when specific users act?
- Do you disclose counts or sizes that change in predictable ways based on user behavior?
- Could block height combined with other disclosures create a timing fingerprint?
Consider adding random delays or batching operations where appropriate.
4. Commitment Scheme Validation
If your contract uses commit reveal patterns verify:
- Is a salt or nonce included in the commitment hash?
- Does the salt have sufficient entropy? Hardcoded or predictable salts offer no protection.
- Is the reveal phase protected against replay attacks?
- What prevents a user from refusing to reveal after the commitment period?
5. Aggregation Check
When disclosing aggregate data verify:
- Is the aggregate sufficiently large to provide k anonymity? A group of two is not private.
- Could an attacker manipulate inputs to isolate a specific user's contribution?
- Are you disclosing the aggregate before all contributions are finalized?
6. Cross Circuit Information Flow
Consider what an observer learns by comparing the public outputs of different circuits:
- Does circuit A disclose something that circuit B later proves about the same user?
- Could timing analysis across circuits create a linkage?
- Do different circuits use the same derived public key?
7. Emergency Stop Consideration
Ask yourself:
- If a privacy vulnerability is discovered can the contract be paused?
- Do users have a way to withdraw or nullify their private data if needed?
- Is there a mechanism for users to rotate their identifiers?
8. Documentation Completeness
Finally verify that your contract includes:
- Comments explaining what each disclosed value represents
- Documentation of the privacy guarantees and limitations
- Clear warnings about what information becomes public and when
A Complete Example: Private Age Verification
Let us put these patterns together into a complete contract that demonstrates selective disclosure principles correctly.
pragma language_version 0.22;
import CompactStandardLibrary;
export ledger verifiedAdults: Set<Bytes<32>>;
export ledger verificationCount: Counter;
witness localSk(): Bytes<32>;
constructor() {
// No constructor initialization needed
}
export circuit verifyAge(
birthYear: Opaque<Uint<16>>,
currentYear: Uint<16>
): [] {
// Compute age without disclosing birth year
let age = currentYear as Uint<32> - (birthYear as Uint<32>);
let isAdult = age >= 18;
// Only disclose the boolean result
assert(disclose(isAdult), "You must be 18 or older");
// Generate DApp specific public key for unlinkability
const _sk = localSk();
let dappPubKey = getDappPublicKey(_sk);
// Record verification without linking to other DApps
verifiedAdults.insert(disclose(dappPubKey));
verificationCount.increment(1);
}
export circuit getDappPublicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "age-verification:pk:"),
_sk
]);
}
This contract demonstrates several best practices:
- The user's birth year is private and never disclosed
- Only the boolean adulthood status is disclosed through the assertion
- The user is recorded using a DApp specific public key preventing cross DApp tracking
- The verification count is public allowing transparency about total verifications without revealing who was verified
Next Steps
Follow-up the clear further explanation README of my commit and gets your code compiled easily: https://github.com/Ameerabdulaleem/midnight_network_selective_disclosure_patterns.git
Selective disclosure is a skill that improves with practice. Here are concrete ways to deepen your understanding:
Study the official examples. The Private Guest List, Election, and Private Reserve Auction contracts all demonstrate sophisticated disclosure patterns.
Use the Midnight MCP tool. Before submitting any work, run your Compact code through
midnight-mcpto validate compilation and catch common mistakes.Join the community. The Midnight Discord and Developer Forum are active with developers working through the same privacy challenges you are.
Apply the audit checklist to your own contracts. The best way to internalize these patterns is to critically examine your own code.
Selective disclosure is what makes Midnight uniquely suited for real world applications that require both decentralization and data protection. Master these patterns and you will build DApps that are not just functional but genuinely private.

Top comments (0)