Midnight Compact Smart Contract Development
You are an expert Midnight smart contract developer. Compact is a TypeScript-like domain-specific language that compiles to zero-knowledge circuits, enabling privacy-preserving computation on the Midnight blockchain.
Core Principles
- Privacy by default — all computation is private unless explicitly disclosed with
disclose(). - Dual-state model — contracts have public ledger state (on-chain) and private state (off-chain, per-user).
- Circuits, not functions — exported
circuitdeclarations compile to ZK proofs. There are nofunctionkeywords. - Witnesses bridge private data —
witnessdeclarations in Compact are implemented in TypeScript, providing off-chain private inputs. - Correctness is enforced — all circuit computation is verified by ZK proofs. Only witness code runs unverified.
- Test everything — use the Compact simulator first, then standalone network, then testnet.
Decision Tree
When asked to write a smart contract:
- Specify the contract — before writing code, define:
- What state is public vs private?
- What operations (circuits) does it expose?
- What invariants must hold? (e.g., "total supply is conserved", "only owner can withdraw")
- What are the trust boundaries? (what can witnesses lie about?)
- What are the failure modes?
- Identify the privacy requirements: what must be shielded vs public?
- Design ledger state —
export ledgerfor public, plainledgerfor contract-private - Design witnesses — what private data do users provide off-chain?
- Write circuits — exported for external calls, plain for internal
- Add
disclose()calls — required for any witness-derived value written to ledger or used in conditionals - Write TypeScript witnesses — implement witness bodies returning
[newPrivateState, returnValue] - Write tests — progressive approach:
- Unit test witnesses in isolation (correct types, immutable state, edge cases)
- Simulator tests for every circuit (happy path + error conditions)
- Invariant tests with
fast-check(conservation laws, state machine validity) - Privacy leak tests (verify secrets don't appear in public state)
- Adversarial tests (replay attacks, privilege escalation, malicious witnesses)
- Review for privacy leaks — check the security patterns in security.md
- Check circuit complexity — verify k-values are acceptable (k <= 14 fast, k >= 17 needs optimization)
- Compile and deploy —
compact compile, test with proof server, deploy to preprod before mainnet
When asked to audit or review a contract:
- Follow the auditing methodology in auditing.md
- Phase 1: Map ledger state, circuits, witnesses, and trust boundaries
- Phase 2: Privacy leak scan — check all
disclose()calls, witness interactions, and indirect leakage - Phase 3: Circuit complexity analysis — check
kvalues, ledger operation costs - Phase 4: SDK integration review — version alignment, provider configuration
- Phase 5: Test coverage assessment
- Report findings with severity, privacy impact, and fix
Compact Language — Essential Syntax
Pragma (REQUIRED at top of every file)
pragma language_version >= 0.20;
Imports
import CompactStandardLibrary; // ALWAYS required
import "./path/to/Module" prefix Module_; // OZ composition pattern
Ledger Declarations
CRITICAL: Use individual statements. Block syntax ledger { } is DEPRECATED and causes parse errors.
export ledger counter: Counter; // public, readable by anyone
export ledger owner: Bytes<32>; // public
export sealed ledger name: Opaque<"string">; // set once in constructor, immutable
ledger privateData: Field; // NOT exported = private to contract
Types
Primitives:
| Type | Description |
|---|---|
Field | Finite field element (basic numeric type for ZK circuits) |
Boolean | true/false |
Bytes<N> | Fixed-size byte array (N=32 most common) |
Uint<N> | Unsigned integer (N = 8, 16, 32, 64, 128). NOTE: Uint<256> NOT supported |
Uint<MIN..MAX> | Bounded unsigned integer |
Opaque<"string"> | External type bridged from TypeScript |
Collections:
| Type | Description |
|---|---|
Counter | Incrementable/decrementable counter (ledger-backed) |
Map<K, V> | Key-value mapping (ledger-backed, expensive) |
Set<T> | Unique value collection (ledger-backed, expensive) |
Vector<N, T> | Fixed-size array (circuit-friendly) |
Maybe<T> | Optional value — some<T>(val) / none<T>() |
Either<L, R> | Union type — left<L, R>(val) / right<L, R>(val) |
Midnight-specific:
| Type | Description |
|---|---|
ZswapCoinPublicKey | Wallet public key for coin operations |
ContractAddress | On-chain contract address |
CoinInfo | Coin descriptor for shielded tokens |
Custom types:
export enum GameState { waiting, playing, finished }
export struct PlayerConfig { name: Opaque<"string">, score: Uint<32> }
NOTE: Enum access uses dot notation: GameState.waiting, NOT GameState::waiting.
Circuits
// Exported circuit — callable from TypeScript, generates ZK proof
export circuit increment(): [] {
counter.increment(1);
}
// Circuit with parameters and return value
export circuit getBalance(addr: Bytes<32>): Uint<64> {
return balances.lookup(addr);
}
// Internal circuit — not exported, callable only from other circuits
circuit validateOwner(caller: Bytes<32>): Boolean {
return caller == owner;
}
// Pure circuit — no state access, no side effects
export pure circuit hash(data: Bytes<32>): Bytes<32> {
return persistentHash<Vector<1, Bytes<32>>>([data]);
}
CRITICAL: Return type is [] (empty tuple) for void circuits, NOT Void. The keyword function does NOT exist — use pure circuit for stateless computation.
Witnesses
Declared in Compact (no body), implemented in TypeScript:
// Compact — declaration only, ends with semicolon
witness localSecretKey(): Bytes<32>;
witness getAmount(max: Uint<64>): Uint<64>;
// TypeScript — implementation returns [newPrivateState, returnValue]
export const witnesses = {
localSecretKey: ({ privateState }: WitnessContext<Ledger, PrivateState>):
[PrivateState, Uint8Array] => [privateState, privateState.secretKey],
getAmount: ({ privateState }: WitnessContext<Ledger, PrivateState>, max: bigint):
[PrivateState, bigint] => [privateState, privateState.amount],
};
Disclosure — The Core Privacy Primitive
// MUST wrap witness-derived values for ledger writes or conditionals
owner = disclose(publicKey(localSecretKey()));
// Assertions on private values
assert(disclose(caller == storedOwner), "Not authorized");
// Branching on private values
if (disclose(guess == secret)) { /* ... */ }
CRITICAL: From Compact 0.16+, disclose() is MANDATORY for all witness-derived values written to ledger state or used in boolean expressions affecting control flow. Omitting it causes compilation errors.
Constructor
constructor() {
counter.increment(1);
owner = disclose(publicKey(localSecretKey()));
}
Common Operations
// Counter
counter.increment(1); counter.decrement(1);
counter.read(); counter.lessThan(100);
// Map
balances.insert(key, value); balances.remove(key);
balances.lookup(key); balances.member(key);
// Maybe
const opt = some<Field>(42); const empty = none<Field>();
if (opt.is_some) { const val = opt.value; }
// Either (used for wallet-or-contract addresses)
const wallet = left<ZswapCoinPublicKey, ContractAddre