midnightntwrk/midnight-expert/compact-privacy-disclosure

Claude Code and agent skill by midnightntwrk/midnight-expert

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

This skill should be used when the user asks about Midnight's privacy model, the disclose() function and disclosure rules, how to fix disclosure compiler errors, privacy-by-default design, witness protection program, commitment schemes (persistentCommit, transientCommit), nullifier patterns for double-spend prevention, MerkleTree membership proofs for anonymous authentication, unlinkable actions via round-based keys, selective disclosure, commit-reveal schemes, shielded vs transparent state des…

Install

npx vskill@latest install midnightntwrk/midnight-expert/compact-privacy-disclosure

SKILL.md

Compact Privacy & Disclosure

Last verified: 2026-09-18 — disclosure mechanics compile-verified against Compact compiler 0.31.1 / language 0.23 / runtime 0.16.0: persistentCommit/transientCommit clear witness taint (store without disclose() compiles) while persistentHash/transientHash do not (rejected with the documented disclosure error); commit/hash signatures confirmed.

This skill covers the privacy dimension of Compact: understanding what is private by default, when and how to disclose, and how to build contracts that preserve user privacy. For visibility rules per ledger operation, see compact-ledger. For contract anatomy and circuit/witness design, see compact-structure. For standard library function signatures, see compact-standard-library. For shielded token privacy, see compact-tokens.

Midnight's Privacy Model

Privacy is the default in Compact. All witness-derived data is private unless explicitly disclosed. The compiler's Witness Protection Program (an abstract interpreter) tracks which values contain witness data at every point in the program. When a tagged value reaches a public boundary (ledger write, conditional, exported circuit return, cross-contract call) without a disclose() wrapper, the compiler halts with an error reporting the full path from witness source to disclosure point.

disclose() is a compiler annotation, not a runtime operation. It does not encrypt, hash, or transform the value. Placing disclose(x) simply marks x as "okay to make public." The programmer is asserting: "I understand this value will be visible on-chain, and that is intentional."

Visibility vs. Taint (Two Different Properties)

These are routinely conflated and must be kept separate:

  • Witness taint (compile-time concept). The compiler tags every value returned from a witness function, every exported circuit parameter, and every constructor parameter as "potentially private." Taint propagates through arithmetic, struct construction, circuit calls, casts, lambda captures. The compiler enforces taint at every public boundary, where disclose() is the only way to clear it.
  • Public transcript visibility (on-chain reality). A value only enters the public transaction transcript if the source code crosses a public boundary with that value while it is still tainted (or via an unprotected return / ledger write / cross-contract call / public conditional). Values that are not disclosed remain PLONK private inputs to the proof and are never observable on-chain.

Exported circuit parameters are private by default. They are PLONK private inputs. They become publicly visible only when the circuit body crosses a public boundary with them. A parameter that is consumed only by witness calls, commitments, internal hashes, or private asserts is never observable on-chain. Verified empirically: a contract with export circuit addPrivately(amount, rand) { lastHash = persistentCommit(amount, rand); } produces a transaction whose publicTranscript contains only the 32-byte commitment hash — the raw amount value is absent.

The corollary: a value being "tagged with witness taint" does NOT mean it is publicly visible — it means the compiler is watching it. Tagging is a safety mechanism; visibility is what actually reaches the chain.

Privacy Decision Tree

What to ProtectApproachKey Primitives
Hide a value on-chainCommitmentpersistentCommit<T> / transientCommit<T>
Prove membership anonymouslyMerkleTree + ZK path (assert on the result, see below)HistoricMerkleTree + merkleTreePathRoot<N, T>
Prevent double-actionsNullifierpersistentHash<T>([domain, secret]) + Set<Bytes<32>>
Hide who is actingUnlinkable authCounter + rotated persistentHash
Multi-step hidden valueCommit-revealCommit phase + reveal phase
Private token balancesShielded tokenszswap infrastructure (see compact-tokens)
Share specific data onlySelective disclosuredisclose() on boolean result, not the value

Disclosure Rules Quick Reference

When `disclose()` IS required:

ContextExampleWhy
Ledger write (direct)owner = disclose(pk)Value becomes public on-chain
Ledger write (ADT method)map.insert(disclose(key), val)Arguments to ADT ops are public
Conditional (if) with ledger writesif (disclose(x == y)) { balance = ... }Branch choice reveals information when it contains ledger writes
Return from exported circuitreturn disclose(value)Return value leaves the ZK proof
Cross-contract callCalling another contract's circuitArguments cross trust boundary
Constructor sealed fieldowner = disclose(pk)Sealed values are set publicly

When `disclose()` is NOT required:

ContextExampleWhy
Pure witness computationconst h = persistentHash<Bytes<32>>(sk)Result stays within circuit
Internal circuit callshelper(witness_val)Non-exported, stays in proof
Intermediate variablesconst x = a + bNo public boundary crossed
Commitment inputspersistentCommit<Field>(secret, rand)Commitment cryptographically hides input
Exported circuit parameter used only internallylastHash = persistentCommit(amount, rand) where amount is a circuit paramParam is a PLONK private input; raw value never enters the public transcript unless disclosed at a boundary

Safe Stdlib Routines

FunctionSignatureCryptographically Hides Input?Why
persistentCommit<T>(value: T, rand: Bytes<32>): Bytes<32>YesCommitment cryptographically hides input
transientCommit<T>(value: T, rand: Field): FieldYesSame hiding property, circuit-efficient
persistentHash<T>(value: T): Bytes<32>NoHash could theoretically be brute-forced
transientHash<T>(value: T): FieldNoSame reasoning as persistentHash

Commit functions (persistentCommit, transientCommit) clear witness taint on both the input and the output. The commitment result does not carry witness taint, so disclose() is not required when storing it in ledger state. Hash functions (persistentHash, transientHash) do NOT clear taint — values derived from hashes still require disclose().

compact
const commitment = persistentCommit<Field>(secretValue, randomness);
storedCommitment = commitment; // No disclose() needed — persistentCommit clears taint
const hash = persistentHash<Field>(secretValue);
storedHash = disclose(hash); // disclose() required — persistentHash does NOT clear taint

checkRoot Publishes Its Result

checkRoot is a ledger operation, so it has two disclosure surfaces, not one. The argument is the widely-known part. The return value is the part that is easy to miss: the call compiles to a member check against the tree's historic roots followed by popeq, and popeq writes the outcome into the public transcript, tagged with the index of the ledger field that was checked.

Omitting disclose() makes the compiler name all three channels at once:

text
nature of the disclosure:
ledger operation might disclose a hash of the witness value
nature of the disclosure:
ledger operation might disclose a hash of the boolean value of the witness value
nature of the disclosure:
ledger operation might disclose a hash of a modulus of a hash of the witness value

The middle channel is the result of the check. A single disclose() clears all three, and the message reads as though it concerns only the root, so the second line is easy to skip past.

This is why every membership example in this skill asserts immediately:

compact
assert(path.leaf == pk, "Path is not for this caller");
assert(members.checkRoot(disclose(digest)), "Not a member");

Asserting makes the published bit constant. A caller whose check fails never lands a transaction, so every transcript on-chain carries the same value and it reveals nothing.

The first line is a separate requirement and it is not about disclosure. merkleTreePathRoot hashes the path's own leaf field, and a witness return is unconstrained, so checkRoot alone proves that some leaf is in the tree rather than the one the circuit just recomputed. Bind the path to that value before the root check. examples/NullifierDoubleSpend.compact shows the bound form; see compact-patterns/references/identity-membership-patterns.md for the full pattern.

Capturing the result and branching on it removes that property:

compact
// Anti-pattern: leaks which trees the caller belongs to
const inA = treeA.checkRoot(disclose(merkleTreePathRoot<10, Bytes<32>>(pathA(leaf))));
const inB = treeB.checkRoot(disclose(merkleTreePathRoot<10, Bytes<32>>(pathB(leaf))));
assert(inA || inB, "not a member of either");

inA || inB is never disclosed, but each result reached the transcript separately. Reading proofData.publicTranscript for two callers, one enrolled only in tree A and one only in tree B:

Callercheck vs treeAcheck vs treeB
A-onlypopeq -> [1]popeq -> []
B-onlypopeq -> []popeq -> [1]

No inference is required; the answers are recorded in order.

The rule
Every checkRoot call in a circuit must return the same value for every honest
caller. If a call can legitimately return false for some callers, that false
is public.

For "N of M" designs, use one shared tree with the category folded into the leaf, hash(domain, identity, categoryTag), prove N memberships against that single tree, and assert inside the circuit that the tags differ. Every caller then publishes the same constant.

The corollary is worth stating plainly: supplying a deliberately invalid path so a check can return false without failing the proof is the only way to express optional membership across several trees, and it is exactly what makes the published bit vary. Optional membership and unlinkability are not simultaneously achievable this way.

Verified against Compact 0.31.1 and @midnight-ntwrk/compact-runtime 0.16.0. Reproduction: https://github.com/tomiin/checkroot-transcript-probe

Common Disclosure Mistakes

WrongCorrectWhy
balance = getBalance()balance = disclose(getBalance())Ledger write requires disclosure
if (getFlag()) { ... }if (disclose(getFlag())) { ... }Conditional requires disclosure
return computeResult()return disclose(computeResult())Exported circuit return requires disclosure
disclose(getBalance()); ...; balance = xbalance = disclose(x)Disclose at disclosure point, not at source
Set for private membershipMerkleTree + ZK path proofSet reveals which element is tested
const ok = tree.checkRoot(...) then branch on okassert(tree.checkRoot(...), "...")The result is written to the public transcript; asserting keeps it constant
Same domain for commitment and nullifierDifferent domainsSame domain enables linking attack
persistentHash(secret) to "hide" witnesspersistentCommit(secret, rand)Hash doesn't clear witness taint; commit does

Reference Routing

TopicReference File
How disclose() works, Witness Protection Program, safe routines, placement best practicesreferences/disclosure-mechanics.md
Commitments, nullifiers, MerkleTree auth, unlinkability, threat model, anti-patternsreferences/privacy-patterns.md
Fixing disclosure compiler errors step-by-step, common error patternsreferences/debugging-disclosure.md

Examples

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
47
Repository forks
14
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/midnightntwrk/midnight-expert/compact-privacy-disclosure/badge)](https://verified-skill.com/skills/midnightntwrk/midnight-expert/compact-privacy-disclosure)

Something wrong with this skill or its page? Tell us.