A Proof Is Not a Promise: closing the trust hole in a Midnight age-check circuit (with compiled Compact code)

Hi everyone,

I’ve published the next article in my Midnight learning series, and this one is about a question that I think trips up almost everyone who starts with zero-knowledge proofs.

We had this circuit from the previous article:

witness birthYear(): Uint<16>;

export circuit proveOver18(currentYear: Uint<16>): [] {
  // logic comes later
}

It has a hole. currentYear is a circuit argument the user types in, and birthYear() is a witness the user’s own app implements. Both are private by default, which is great for privacy, but it also means the contract is taking the user’s word for both. A valid proof here only means “for the numbers I chose, the math works out.”

The article covers:

  1. Naming the two problems precisely (the user holds the clock, the user holds the birth certificate), with the exact quotes from the Midnight docs that describe why witness data cannot be trusted.

  2. Why Midnight cannot just check, and how this connects back to “consensus gives agreement, not truth.”

  3. Fix 1: block time. Using blockTimeGte() so the chain, not the user, bounds the date. Includes the actual compiler error you get if you forget disclose() here, and why that error is the compiler being helpful.

  4. Fix 2: attestation. A trusted issuer signs the birth date; the circuit verifies the signature with jubjubSchnorrVerify(). This is the same approach as the official ZK Loan example.

  5. Fix 3: binding the credential to a derived identity so it can’t be copied from someone else’s device.

  6. What is public and what is private after the fix, item by item.

  7. Honest limits: trust moved to the issuer rather than disappearing, block time is coarse, and toolchain version notes (the final contract compiles on 0.34.0 and 0.35.0; 0.31.1, which the compatibility matrix currently lists for public networks, does not have jubjubSchnorrVerify in the standard library).

Both the naive and the fixed contracts are included as .compact files and were compiled with the Compact toolchain before publishing.

Link: A Proof Is Not a Promise: Closing the Hole in Our Midnight Age Check | Eightblock

I’d really welcome corrections or pushback, especially from anyone who has shipped attestation-based flows on Midnight. If there’s a cleaner way to bind the credential to the caller than the derived-identity hash, I’d like to hear it.

Thanks for reading.