The last post in this series covered the surface illusion: Compact looks like TypeScript, but it isn't. Same syntax, completely different model.
This post goes one level deeper. Into the most specific version of that mistake, the one that actually stalls people in the middle of building something.
"A circuit is just a function."
It isn't. And the gap between those two things is where most of the confusion lives.
Why the confusion is so sticky
The syntax is deliberately familiar:
export circuit transfer(amount: Uint<64>): [] {
assert(balance >= amount, "Insufficient balance");
balance = balance - amount;
}
You read this and your brain fires the usual pattern. Signature, body, assertions, state update. Looks like a function. Reads like a function. Must behave like one.
Here's what that assumption gets wrong: functions execute. Circuits constrain.
These are not the same thing. They're not even close.
The actual difference
A function describes a sequence of steps. You give it inputs, it runs through statements top to bottom, it produces an output. The machine does what you told it to do.
A circuit describes a set of relationships that must hold true. When you call it from your dApp, the proof system generates a zero-knowledge proof that those relationships were satisfied. What gets submitted on-chain isn't the result of running your code. It's a proof that the constraints were met.
Function: Input → Execute steps → Output
Circuit: Input → Declare constraints → Generate proof → Verify proof
There is no "execution" in the traditional sense. There's constraint satisfaction, proof generation, and verification.
What return a + b actually means in a circuit
In a function, return a + b says: compute the sum and hand it back.
In a circuit, return a + b says: the output is constrained to equal a + b. The proof proves this relationship held for the actual inputs, without revealing what those inputs were.
Same syntax. Completely different meaning.
This is why Compact works for private data at all. The proof system doesn't need to see your inputs to verify that your circuit's constraints were satisfied. It can confirm correctness without exposure. That's the point.
What assert actually is
In a function, assert is a runtime guard. It checks a condition and throws if it fails.
In a circuit, assert is a constraint declaration. It doesn't "throw", it defines a requirement that the proof must satisfy. If the condition is false, the proof can't be generated. There's no proof to submit. The transaction doesn't happen.
Every assert in your circuit is part of the constraint system. You're not checking inputs. You're specifying what valid inputs look like.
assert(balance >= amount, "Insufficient balance");
This doesn't say: check if balance is high enough, then continue.
It says: a proof for this circuit cannot exist if balance is less than amount.
That's a different tool. Use it differently.
Why the constraints exist (and why this isn't limiting)
Compact is deliberately bounded:
- No recursion
- No unbounded loops
- Fixed type sizes at compile time
- Immutable variable bindings
The first reaction is usually: these feel arbitrary.
They're causal. ZK proofs require finite circuits. A circuit is a fixed structure of gates and wires determined entirely at compile time. Unbounded computation can't become a circuit. If the compiler can't determine the full structure of your constraint system before runtime, there's nothing to prove.
The bounds aren't restrictions on what Compact can do. They're what makes proof generation possible at all.
This is also why return can't appear inside a for loop, and why variables can't be reassigned after binding. In a constraint system, reassignment is ambiguous. What does it mean to constrain a value that changes? It doesn't mean anything well-defined. So it's not allowed.
map and fold exist specifically for this. Transformation and accumulation without the ambiguity.
The debugging shift
This has a practical implication that catches people off guard: you can't debug circuits the way you debug functions.
With a function, you ask: what line failed? You add logging. You trace execution. You find where the state went wrong.
With a circuit, there's no execution to trace. You ask: which constraint is unsatisfied? You look at what relationships your circuit declares and figure out which one the proof can't satisfy given the actual inputs. Then you look at whether your witness is providing what the circuit expects.
Different question. Different process.
Most people hit this the hard way, they try to debug a constraint failure like a runtime error and get nowhere. The shift is: treat every failure as a constraint problem, not an execution problem.
Where this leads
Once this model is solid, the rest of Compact becomes consistent.
Witnesses are the private inputs the prover uses to generate the proof. They're not function arguments in the normal sense, they're the private side of the constraint system.
disclose() is the explicit act of moving a private constraint into public view. Not encryption. Not transmission. Just a compiler-enforced boundary that marks intentional exposure.
Ledger state is the public constraint, what everyone on-chain can see is asserted to be true.
Every concept in Compact is a piece of the same constraint model. Once you see it that way, the language is internally consistent. Before that, it seems to have arbitrary rules that don't connect.
The one sentence that changes how you read Compact
You are not writing code that runs. You are describing valid states and proving you were in them.
With that sentence as the frame:
export circuit transfer(amount: Uint<64>): [] {
assert(balance >= amount, "Insufficient balance");
balance = balance - amount;
}
...reads differently. You're not executing a transfer. You're describing what a valid transfer looks like and generating a proof that this one was valid. The on-chain state updates because the proof verified, not because code "ran."
The full circuits chapter is in the book
Mental models, examples, how map and fold replace return-in-loop patterns, how generics work, the full proof flow. It's structured to build the right frame first, then fill in the syntax.
Post #3 covers public vs. private state, the two-world model and what actually goes on-chain. That one connects directly to the mistakes people make once they've grasped circuits but haven't yet internalized where data lives.
Top comments (0)