Compact Is Not Solidity: A Developer's Field Guide to ZK Smart Contracts
If you approach Compact with a Solidity mental model, you will write code that compiles but misses the point. Compact is not a smart contract language that happens to support privacy — it's a zero-knowledge circuit language that happens to look like a smart contract language. The distinction matters for every design decision you'll make.
The fundamental difference
In Solidity, you write code that runs on the EVM. The execution is transparent — anyone can see the inputs, the state changes, and the outputs. Privacy is something you add through off-chain computation, commit-reveal schemes, or layer-2 solutions.
In Compact, you write code that compiles to a zero-knowledge circuit via the Halo2 compiler. The circuit proves that a computation was performed correctly without revealing the inputs. Privacy isn't a feature — it's the execution model. You don't add privacy to a Compact contract. You'd have to actively remove it.
What you lose
Coming from Solidity, these constraints will frustrate you until you understand why they exist:
No arbitrary loops
In Solidity, for (uint i = 0; i < n; i++) is routine. In Compact, it's impossible. Circuit size must be deterministic at compile time. A loop with a variable bound would produce a circuit whose size depends on runtime input — and circuit size must be fixed before any execution happens.
Workaround: Use fixed-size iterations or recursive proof composition. If you need to process a variable number of items, structure your contract so each item is processed in a separate transaction.
No dynamic memory
Solidity has mappings, dynamic arrays, and arbitrary storage slots. Compact has fixed data structures. You declare what you'll store at compile time, and the circuit is built around that declaration.
Workaround: Use hash-based references instead of storing data directly. DPO2U's ComplianceRegistry stores only hashes, CIDs, scores, and timestamps — never the underlying documents.
No string manipulation
Compact operates on field elements, not byte arrays. String comparison, concatenation, and parsing don't exist in the circuit model.
Workaround: Hash strings off-chain and pass the hash as a field element. Comparison becomes equality check on hashes.
What you gain
The constraints aren't arbitrary — they're the price of three guarantees:
Deterministic circuit size
Because there are no variable-length loops or dynamic memory, the compiler knows the exact circuit size at compile time. This means:
- Proof generation time is predictable (no surprise 10-minute proofs)
- Gas costs are predictable (circuit size directly determines proof cost)
- Verification time is constant regardless of input complexity
Privacy by default
Every state in a Compact contract is private unless explicitly declared public. This is the inverse of Solidity, where everything is public unless you architect privacy around it.
In DPO2U's ComplianceRegistry, the public outputs are: company hash, CID, score, timestamp, and agent DID. The private inputs — the actual compliance documents, policy details, DPO contact information — never leave the prover's machine. The circuit proves the score was computed correctly from those inputs without revealing them.
Native proof composition
Compact contracts can reference proofs from other contracts. This enables a modular architecture where each contract proves one thing, and a higher-level contract composes those proofs into a compound attestation.
Mapping DPO2U's contracts
DPO2U has seven Compact contracts. Here's how each maps from Solidity thinking to Compact thinking:
| Contract | Solidity approach | Compact approach |
|---|---|---|
| ComplianceRegistry | Store attestation structs with all fields | Store only hashes + score, prove validation via circuit |
| DocumentTimestamp | block.timestamp on storage write | ZK proof of temporal existence — proves document existed at time T |
| AgentWalletFactory | new AgentWallet() via CREATE2 | Programmatic wallet initialization via HD Wallet SDK |
| FeeDistributor | transfer() with calculated amounts | Circuit proves correct fee split without revealing amounts |
| Treasury | ERC-20 approve/transferFrom pattern | $NIGHT native token handling with role-based constraints |
| AgentRegistry | Mapping of addresses to structs | DID-based identity with permission bitfield |
| PaymentGateway | payable function + event emission | $NIGHT acceptance with automatic distribution proof |
Practical advice
After rewriting four Solidity contracts in Compact, here's what I wish someone had told me:
1. Design for hashes, not data. If you're storing more than a hash and a score on-chain, you're probably doing it wrong. The chain records proofs. IPFS stores data. The CID bridges them.
2. Think in circuits, not functions. A Compact function isn't code that runs — it's a circuit that gets proven. Every input is a wire. Every operation adds gates. Fewer operations = smaller circuit = faster proof = cheaper gas.
3. Use vacant witnesses when you don't need private data. DPO2U's ComplianceRegistry doesn't use private state — attestations are public by design. Using CompiledContract.withVacantWitnesses simplifies the deploy pipeline significantly.
4. Read the example-counter repo before the docs. Midnight's reference implementation is more accurate than the written documentation. Study the signing pattern, the provider setup, and the deploy flow.
5. Budget for wallet sync time. The HD Wallet SDK syncs the full UTXO set from genesis — ~76 seconds on preprod. Design your pipeline to handle this without user-facing latency.
The mindset shift
Solidity developers think in terms of state and mutations: "this function changes this mapping." Compact developers think in terms of proofs and attestations: "this circuit proves that a computation was performed correctly."
The shift is not just syntactic — it's epistemic. In Solidity, the chain knows everything. In Compact, the chain knows only what you prove. The privacy isn't a constraint — it's the product.
For DPO2U's contract architecture, see Smart Contracts. For the broader context of why we chose Midnight, see About DPO2U.
