Imagine you open a private voting app. You cast a vote, your vote is counted, and the
result is public for everyone to see — but nobody, not even the machine counting the
votes, ever sees which vote was yours.
That sounds impossible. And on most blockchains it kind of is.
Midnight does it with something called a witness. This post explains what a witness
is, how it works, and why it matters — using plain language and one running example you
can follow from start to finish. No cryptography degree needed.
The core idea: use a secret without giving it away
Every blockchain is basically a shared public notebook. When you write to it, everyone in
the world can read what you wrote. That's great for honesty — but terrible if you want to
keep something private.
Here's the trick Midnight pulls off: you can let the network check something about your
private data without ever seeing the data itself.
Think of it like a lie-detector test for math. You whisper your answer to me. I don't
tell anyone what you said. Instead, I stand up and say: "I can prove this person's
answer was valid, without showing you the answer." If everyone trusts me, they accept
your answer without ever learning it.
In Midnight, your private data never leaves your phone. Only a proof that the data is
correct travels to the network. And the person who "whispers" your secret into the proof
is called a witness.
What, exactly, is a witness?
A witness is a helper that runs on your own device, not on the blockchain.
Here's the analogy that clicks for most people:
Your private data is a key hidden in your pocket. A witness is your hand — it reaches
into your pocket, grabs the key, uses it to open a lock, and pulls out a receipt that
proves the door opened. Everyone sees the receipt. Nobody sees the key.
In Midnight's programming language (called Compact), a witness is declared inside the
smart contract but its actual logic lives in your app's code, running locally.
// This tells the contract: "hey, there's a helper that can grab the secret key"
witness localSecretKey(): Bytes<32>;
witness localVote(): Uint<8>;
Notice: there's no implementation here. Compact just declares that a witness exists.
Your app provides the actual "reach into the pocket and grab the key" part later. The
secret itself is never compiled into the on-chain contract, so it can never leak.
The "private oracle" — a fancy name for a simple idea
Midnight borrows a term: the private oracle. It just means two things working together:
- Private state — a small file on your device holding your secrets (your key, your vote, your balance).
- Witness functions — the helpers that read from that file.
That's it. When a circuit needs your secret, it calls a witness, the witness reads your
local file, and hands the value into the math that builds a proof. The secret stays on
your device the whole time.
A running example: your private score
Let's make this real. Say you're building an app where players have a score, and you
want to prove your score is legit on the blockchain — without ever revealing what the
score is, or who you are.
Step 1: Tell Compact about your secrets
witness localSecretKey(): Bytes<32>; // your private key
witness localScore(): Uint<64>; // your score
Step 2: Turn the score into a "locked box"
You don't put the raw score on-chain. You put a hash of it — a scrambled, one-way
fingerprint. It's like sealing your score inside a box and writing only the seal's
serial number on the wall. Anyone can see the serial number. Nobody can open the box.
circuit commitment(sk: Bytes<32>, score: Uint<64>): Bytes<32> {
return persistentHash(...); // the "seal"
}
export circuit commitScore(): [] {
const _sk = localSecretKey(); // the hand reaches into the pocket
const score = localScore(); // grabs the secret score
scoreCommitment = disclose(commitment(_sk, score)); // only the seal goes on-chain
}
Step 3: Write the witnesses in your app's code
Now the part you actually write as a developer:
export const witnesses = {
// "Reach into the pocket, grab the secret key"
localSecretKey: ({ privateState }) => [privateState, privateState.secretKey],
// "Reach into the pocket, grab the score"
localScore: ({ privateState }) => [privateState, privateState.score],
};
Each witness returns two things: the (maybe-updated) private state, and the value the
circuit asked for. The score and key are read straight from the user's own device.
What ends up on the blockchain?
Just a hash — a meaningless-looking string of characters. Nobody can reverse it to
find your score. Your private data never left your device. Yet the network can still
verify your commitment is legitimate whenever you reveal it later, by re-checking the
hash.
Why disclose() is the guardrail
Midnight is paranoid about privacy on purpose. The language refuses to let you accidentally
publish private data.
By default, anything that came from a witness is treated as private. If you try to
write it to the public ledger without saying so, the compiler stops you:
// This won't even compile. It would leak private data.
scoreCommitment = commitment(sk, score);
// This compiles: you're explicitly saying "yes, publish this on purpose."
scoreCommitment = disclose(commitment(sk, score));
The word disclose() is essentially billingual to the compiler: "I know this came from
private data. I'm doing it on purpose. Let me through."
Even clever attempts to hide it get caught. For example, if you take a secret, run it
through some math, compare it to a public value, and then try to publish the result of
that comparison — the compiler traces the whole path back to the secret and refuses.
Here's how to read one rule of thumb:
- If a value is private, treat it like it's radioactive.
- The only way to let it touch public things is to wrap it in
disclose().
One big warning: don't trust the witness
Here's the part that surprises people: the witness is not magic, and it is not trustworthy
by itself.
Remember: the witness runs on your device, in your app. If you're the one writing the
app, you write the honest witness. But the blockchain can't assume that. Anyone could ship
a witness that returns fake values.
That's why the smart contract must double-check whatever the witness hands over —
against data that's already on the chain. In our example:
- First, we put the commitment (the "seal") on the chain.
- Later, when you reveal, the contract recomputes the seal and compares it to what's on the chain.
- If a witness tried to lie, the seals wouldn't match, and the contract rejects it.
A witness can tell you anything. The math on-chain is what actually holds it accountable.
When should you use a witness?
Use a witness when you need to touch private data or do hard off-device math:
- Read a secret key, vote, or balance that must never be public
- Do division or other math the blockchain can't do cheaply, then prove the result
- Update private state based on what happened in a circuit
Don't use a witness as a shortcut around good design — anything important it returns must
eventually be checked against on-chain truth.
Tell me the whole thing again, simply
Here's the entire idea in one paragraph:
A witness is a helper that runs on your own device. When your smart contract needs a
secret, it calls the witness, which pulls the secret from your private local file and
feeds it into a zero-knowledge proof. Only the proof goes to the blockchain — never the
secret itself. And disclose() is your explicit, compiler-enforced "yes I meant to share
this" button for the rare times something does become public.
A quick glossary in plain words
- Witness — a helper on your device that supplies private data to a proof
- Private state — the secrets living in a local file on your device
- Private oracle — private state + the witnesses that read it, working together
- Hash / commitment — a one-way "seal" of your data; public to see, impossible to open
- disclose() — the explicit "share this" tag the compiler requires before private data can touch the public ledger
Top comments (0)