DEV Community

Cover image for How Midnight's Compact Compiler Enforces Privacy at Compile Time
barnazaka
barnazaka

Posted on

How Midnight's Compact Compiler Enforces Privacy at Compile Time

Most blockchain privacy tools trust the developer to handle data correctly. Midnight does not. And I found that out the hard way while building NightScore, a privacy-preserving credit attestation oracle on the Midnight Network.

Let me walk you through what happened.


What I Was Building

NightScore lets borrowers prove their creditworthiness without revealing their financial data. The idea is simple: a borrower inputs their on-chain repayment history, wallet age, transaction volume, and default count locally. A Compact circuit computes a credit tier (Bronze, Silver, Gold) from that data. A ZK proof is generated. Only the tier gets written to the public Midnight ledger. The raw numbers never leave the device.

That is the design. The contract looked clean. Then I tried to compile it.


The Error

Exception: nightscore.compact line 46 char 17:
  potential witness-value disclosure must be declared but is not:
    witness value potentially disclosed:
      the return value of witness onChainRepayments at line 14 char 1
    nature of the disclosure:
      ledger operation might disclose the boolean value of the result 
      of a comparison involving the witness value
Enter fullscreen mode Exit fullscreen mode

Six errors. All pointing at the same line: creditRegistry.insert(borrower, tier).

This is not a runtime error. The compiler performed a static analysis of my code, traced every data dependency from my private witness inputs through the circuit logic to the ledger write, and refused to compile because it detected that private data could be inferred from what I was writing publicly.

That is a different kind of safety guarantee than anything I had worked with before.


What the Compiler Was Actually Catching

Here is the thing. I was not writing the raw financial numbers to the ledger. I was writing a CreditTier enum value. That felt safe.

But the compiler traced this path:

  1. onChainRepayments is a private witness value
  2. It gets passed into computeTier
  3. Inside the circuit, it is compared against thresholds: repayments >= 20, repayments >= 10, repayments >= 3
  4. Those comparisons determine which CreditTier gets returned
  5. That tier gets written to the ledger via creditRegistry.insert

If someone observes a GOLD tier on the ledger, they know repayments >= 20, wallet age >= 365 days, and volume >= $50,000. The raw number is not there but its relationship to the thresholds is visible. That is a disclosure. It is indirect, partial, but the compiler flags it anyway and requires you to acknowledge it explicitly.


The Fix: disclose()

The disclose() keyword is how you tell the compiler: I know this value is derived from private data and I am intentionally making it public.

export circuit attestCreditTier(borrower: ZswapCoinPublicKey): [] {
  const repayments = onChainRepayments();
  const age = walletAgeInDays();
  const volume = totalVolumeUSD();
  const numDefaults = defaultCount();
  const tier = computeTier(repayments, age, volume, numDefaults);
  creditRegistry.insert(disclose(borrower), disclose(tier));
}

export circuit getCreditTier(borrower: ZswapCoinPublicKey): CreditTier {
  return disclose(creditRegistry.lookup(disclose(borrower)));
}
Enter fullscreen mode Exit fullscreen mode

Two things get wrapped in disclose():

tier because the credit tier is the whole point. DeFi protocols need to read it. I accept that it reveals the borrower's position relative to the thresholds.

borrower because the public key is being associated with a credit tier on-chain and that association is intentional.

What does NOT get wrapped in disclose() is the raw witness values: the repayments, wallet age, volume, and default count. Those stay private. The compiler guarantees they never reach the ledger.

With those two calls in place, both circuits compiled clean:

circuit "attestCreditTier" (k=10, rows=653)
circuit "getCreditTier"    (k=9,  rows=305)
Enter fullscreen mode Exit fullscreen mode

Why This Matters

Most privacy systems in Web3 operate on trust. The developer promises not to log the data. The protocol promises not to expose it. Users take both on faith.

Compact does something different. The compiler performs information flow analysis on every circuit. Every data dependency from private inputs through all computational paths to every public ledger write is tracked statically. If it finds a path where private data can be inferred, it refuses to compile and tells you exactly where the problem is.

disclose() is not just a fix. It is documentation. Every disclose() call in a Compact contract is a statement about what the contract intentionally makes public. When someone reads your source code, the disclose() calls tell them exactly what information your contract reveals. Nothing more, nothing less.

For applications like NightScore, this changes everything. The credit registry reveals a borrower's public key and their computed tier. The raw financial data is proven correct through the ZK proof but never exposed. That guarantee is not a policy or a best practice. It is enforced at compile time before a single transaction ever touches the network.


A Few Things I Learned the Hard Way

defaults is a reserved keyword in Compact. Name your parameters something like numDefaults or you will get a confusing parse error.

The pragma version must match your toolchain. compact update 0.28.0 expects pragma language_version 0.20.0. Using a different version throws a mismatch error immediately.

Witness functions in TypeScript are synchronous, not async. They return [PrivateState, bigint] not a Promise. And Uint<64> in Compact maps to bigint in TypeScript, not number. Always use BigInt().


When I hit that first compiler error I thought something was broken. Turns out the compiler was doing exactly what it was supposed to do. It caught a disclosure I had not thought through and made me be explicit about it. That is the kind of tooling that actually helps you build privacy-preserving applications correctly instead of just hoping you got it right.

NightScore source code: github.com/barnazaka/nightscore

Follow @MidnightNtwrk on X and Midnight on LinkedIn for ecosystem updates.

Top comments (0)