| name | midnight-compact |
| description | Comprehensive guide to the Compact programming language (v0.22+) for writing privacy-preserving smart contracts on Midnight Network. Use when users need to write Compact smart contracts with zero-knowledge proofs, understand Compact syntax and language features, implement ZK circuit patterns and optimizations, generate contract boilerplate and project scaffolding, learn best practices for secure contract development, access Compact standard library functions, and compile and test Compact contracts. |
Midnight Compact Language (v0.22+)
Complete guide to writing privacy-preserving smart contracts with Compact.
What is Compact?
Compact is a purpose-built programming language for zero-knowledge smart contracts. Unlike adapting existing languages, Compact was designed from scratch to make privacy-preserving programming natural and secure.
Key Design Goals:
- Compile directly to efficient ZK circuits
- Type-safe cryptographic operations
- Familiar syntax for developers (TypeScript-like)
- Automatic proof generation
- Bounded for finite proving circuits
Key Features:
- Strong static typing (no bypass via unsafe casts)
- Generic type and numeric parameters
- Explicit disclosure via
disclose() wrappers
- Module-based namespace management
Quick Start
Minimal Working Contract (v0.22+)
pragma language_version >= 0.20;
import CompactStandardLibrary;
// Ledger state (individual declarations)
export ledger counter: Counter;
export ledger owner: Bytes<32>;
// Witness for private data
witness local_secret_key(): Bytes<32>;
// Circuit (returns [] not Void)
export circuit increment(): [] {
counter.increment(1);
}
Full Counter with Read/Write
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger count: Counter;
export circuit increment(): [] {
count.increment(1);
}
export circuit decrement(): [] {
count.decrement(1);
}
export circuit getCount(): Uint<64> {
return count.read();
}
export circuit setCount(newValue: Uint<64>): [] {
count.write(newValue);
}
Core Concepts
Three-Part Contract Structure
Each Compact contract has three components:
- Replicated component on a public ledger
- Zero-knowledge circuit that confidentially proves correct execution
- Local, off-chain component for arbitrary code
Program Elements
A Compact program can contain:
module / import for namespace management
struct / enum / type for program-defined types
export ledger for public state storage
witness for callback functions (private state)
circuit for the operational core
constructor for initialization
Comprehensive Syntax Reference
✅ CORRECT vs ❌ WRONG Examples
Pragma:
✅ pragma language_version >= 0.20;
❌ pragma language_version >= 0.16.0; // Outdated version
❌ pragma version "0.20"; // Wrong format
Ledger Declarations:
✅ export ledger counter: Counter;
✅ export ledger owner: Bytes<32>;
✅ export ledger balances: Map<Address, Uint<64>>;
❌ ledger { counter: Counter; } // Block syntax deprecated
❌ ledger counter = Counter; // Missing colon
Circuit Return Types:
✅ export circuit increment(): [] { }
✅ export circuit getBalance(): Uint<64> { return 0; }
✅ export circuit getData(): [Uint<32>, Boolean] { return [0, true]; }
❌ export circuit increment(): Void { } // Void doesn't exist
❌ export circuit noReturn() { } // Missing return type
Enum Access:
✅ if (choice == Choice.rock) { }
❌ if (choice == Choice::rock) { } // Rust-style doesn't work
❌ if (choice == "rock") { } // String not enum variant
Witness Declarations:
✅ witness local_secret_key(): Bytes<32>;
✅ witness verify_signature(msg: Bytes<32>, sig: Bytes<64>): Boolean;
❌ witness get_key(): Bytes<32> { return ...; } // No body allowed
❌ witness calculate(): Uint<64> = 42; // No body or default
Counter Operations:
✅ const val = counter.read();
✅ counter.increment(1);
✅ counter.decrement(1);
✅ counter.write(100);
❌ const val = counter.value(); // Method doesn't exist
❌ counter.add(1); // Wrong method name
Pure Functions:
✅ pure circuit helper(x: Field): Field { return x + 1; }
✅ circuit helper(x: Field): Field { return x + 1; }
❌ pure function helper(x: Field): Field { ... } // 'function' keyword doesn't exist
❌ pure fn helper(x: Field): Field { ... } // 'fn' keyword doesn't exist
Disclosure in Conditionals:
✅ if (disclose(witness_val == x)) { ... }
❌ if (witness_val == x) { ... } // Implicit disclosure error
❌ disclose(witness_val == x); // Statement form without if
Vector/Tuple Operations:
✅ const first = vec[0];
✅ const len = vec.len();
✅ const [a, b] = tuple;
✅ for (i: Uint<64>, item: vec) { ... }
❌ const last = vec[-1]; // No negative indexing
❌ vec.push(item); // No push - vectors are immutable
Common Mistakes to Avoid
| ❌ Wrong | ✅ Correct | Explanation |
|---|
ledger { field: Type; } | export ledger field: Type; | Individual declarations |
circuit fn(): Void | circuit fn(): [] | Empty return uses [] |
enum State { ... } | export enum State { ... } | Enums need export |
counter.value() | counter.read() | Use read() method |
Choice::rock | Choice.rock | Dot, not :: |
Cell<T> | Field | Cell type deprecated |
Vector.push(x) | Create new vector | Vectors immutable |
disclose(a == b) alone | if (disclose(a == b)) | Needs conditional |
Uint without size | Uint<64> | Size required |
Fn or function | circuit | Use circuit keyword |
Type System
Primitive Types
| Type | Description | Example Values |
|---|
Boolean | Boolean values | true, false |
Field | Prime field integers | 0 to field max |
Uint<N> | N-bit unsigned | Uint<8>, Uint<32>, Uint<64> |
Uint<M..N> | Bounded unsigned | Uint<0..100> |
Bytes<N> | N-byte vector | Bytes<32> for hashes |
Vector<N, T> | N-element homogeneous | Vector<10, Uint<32>> |
[T1, T2, ...] | Heterogeneous tuple | [Uint<32>, Boolean] |
Opaque<"string"> | Opaque string | Debug strings |
Opaque<"Uint8Array"> | Opaque bytes | Binary data |
User-Defined Types
Struct:
struct Point {
x: Field,
y: Field,
}
struct Token<T> {
amount: Uint<64>,
data: T,
}
Enum:
export enum Direction { up, down, left, right }
export enum Status { pending, active, completed }
Type Alias:
type Address = Bytes<32>;
new type UserId = Uint<64>; // Nominal (distinct)
type Vec3<T> = Vector<3, T>; // Generic
Subtyping Rules
Uint<0..N> subtype of Uint<0..M> if N ≤ M
Uint<0..N> subtype of Field if N ≤ field max
- Tuple subtypes when elements are subtypes
Generic Parameters
module M<T, #N> {
export circuit foo<A>(x: T, v: Vector<N, A>): Vector<N, [A, T]> {
return map((y) => [y, x], v);
}
}
import M<Boolean, 3>;
export circuit bar(): Vector<3, [Uint<8>, Boolean]> {
return foo<Uint<8>>(true, [101, 103, 107]);
}
Ledger State Types
Seven ledger state types for public data storage:
1. Counter
export ledger myCounter: Counter;
counter.increment(n);
counter.decrement(n);
counter.read();
counter.write(value);
2. Cell (Deprecated - use Field)
// Old way (avoid)
export ledger data: Cell<Uint<32>>;
// New way
export ledger data: Field;
3. Set
export ledger members: Set<Address>;
members.insert(addr);
members.contains(addr);
members.remove(addr);
4. Map
export ledger balances: Map<Address, Uint<64>>;
balances.get(addr);
balances.set(addr, value);
balances.contains(addr);
balances.remove(addr);
5. List
export ledger items: List<Uint<32>>;
items.push(value);
items.get(index);
items.len();
6. MerkleTree
export ledger tree: MerkleTree<Depth>;
tree.insert(value);
tree.getRoot();
tree.getPath(index);
tree.verifyPath(path, root);
7. HistoricMerkleTree
export ledger history: HistoricMerkleTree<Depth>;
history.insert(value);
history.getRoot();
history.getHistoricRoot(blockHeight);
history.getPath(index);
Standard Library
Data Types
Maybe<T> // Optional values (some/none)
Either<L, R> // Result types (left/right)
NativePoint // BLS12-381 points
MerkleTreeDigest // Tree hash digest
MerkleTreePath // Proof path
ContractAddress // Contract addressing
ZswapCoinPublicKey // Shielded keys
UserAddress // User addressing
Coin Management (Zswap)
tokenType() -> TokenType
nativeToken() -> TokenType
ownPublicKey() -> CoinPublicKey
createZswapInput(coin: ShieldedCoinInfo) -> ZswapInput
createZswapOutput(recipient: Recipient, amount: Uint<64>) -> ZswapOutput
mintShieldedToken(amount: Uint<64>, recipient: Recipient): [] { }
sendShielded(amount: Uint<64>, recipient: Recipient): [] { }
receiveShielded(input: ZswapInput): [] { }
mintUnshieldedToken(amount: Uint<64>): [] { }
sendUnshielded(amount: Uint<64>, recipient: Address): [] { }
unshieldedBalance(): Uint<64>
Hashing Functions
transientHash(data: Field): Field
transientCommit(data: Field, randomness: Field): Field
persistentHash(key: Field, value: Field): Field
persistentCommit(key: Field, value: Field): Field
degradeToTransient(value: Field): Field
Elliptic Curve
ecAdd(p1: Point, p2: Point): Point
ecMul(point: Point, scalar: Field): Point
ecMulGenerator(scalar: Field): Point
hashToCurve(data: Field): Point
upgradeFromTransient(value: Field): Field
Merkle Trees
merkleTreePathRoot(path: MerkleTreePath): Field
merkleTreePathRootNoLeafHash(path: MerkleTreePath): Field
Block Time
blockTimeLt(time: Uint<64>): Boolean
blockTimeGte(time: Uint<64>): Boolean
blockTimeGt(time: Uint<64>): Boolean
blockTimeLte(time: Uint<64>): Boolean
Control Flow
If/Else
if (condition) {
// then branch
} else {
// else branch
}
For Loop
// For each element
for (i: Uint<64>, item: vector) {
process(item);
}
// With index
for (i: Uint<64>, item: vector) {
log(i);
log(item);
}
Match (for enums)
match direction {
Direction.up => 1,
Direction.down => 2,
Direction.left => 3,
Direction.right => 4,
}
Pure Circuit Functions
pure circuit addOne(x: Uint<32>): Uint<32> {
return x + 1;
}
Explicit Disclosure
Private data must be explicitly disclosed:
witness secret(): Uint<32>;
export circuit checkEqual(x: Uint<32>): Boolean {
// Must disclose private comparison
return disclose(secret() == x);
}
// Valid patterns:
if (disclose(secret() > 100)) { ... }
const revealed = disclose(secret() + publicValue);
Development Workflow
1. Initialize Project
npx create-midnight-app my-project
cd my-project
mkdir my-contract && cd my-contract
npm init -y
npm install @midnight-ntwrk/compact-tools
2. Write Contract
Create contract.compact:
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger counter: Counter;
export circuit increment(): [] {
counter.increment(1);
}
export circuit getCount(): Uint<64> {
return counter.read();
}
3. Compile
npx compact build contract.compact
4. Generate TypeScript
5. Deploy
npx midnight-deploy --network testnet
Real-World Contract Examples
Private Token (Zswap)
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger totalSupply: Uint<64>;
export ledger balances: Map<Address, Uint<64>>;
export circuit mint(to: Address, amount: Uint<64>): [] {
const current = balances.getOrDefault(to, 0);
balances.set(to, current + amount);
totalSupply.set(totalSupply.read() + amount);
}
export circuit burn(from: Address, amount: Uint<64>): [] {
const current = balances.getOrDefault(from, 0);
assert(current >= amount, "Insufficient balance");
balances.set(from, current - amount);
totalSupply.set(totalSupply.read() - amount);
}
export circuit transfer(from: Address, to: Address, amount: Uint<64>): [] {
const fromBal = balances.getOrDefault(from, 0);
assert(fromBal >= amount, "Insufficient balance");
const toBal = balances.getOrDefault(to, 0);
balances.set(from, fromBal - amount);
balances.set(to, toBal + amount);
}
export circuit balanceOf(owner: Address): Uint<64> {
return balances.getOrDefault(owner, 0);
}
Private Voting
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger votes: Map<Bytes<32>, Uint<64>>;
export ledger voted: Set<Address>;
export circuit castVote(voter: Address, candidate: Bytes<32>): [] {
assert(!voted.contains(voter), "Already voted");
voted.insert(voter);
const current = votes.getOrDefault(candidate, 0);
votes.set(candidate, current + 1);
}
export circuit getVoteCount(candidate: Bytes<32>): Uint<64> {
return votes.getOrDefault(candidate, 0);
}
Access Control
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger owner: Address;
export ledger admins: Set<Address>;
export circuit onlyOwner(): [] {
assert(msgSender() == owner, "Not owner");
}
export circuit onlyOwnerOrAdmin(): [] {
const sender = msgSender();
assert(sender == owner || admins.contains(sender), "Not authorized");
}
witness msgSender(): Address;
// Note: msgSender is typically provided by runtime
OpenZeppelin Contracts
Standard implementations from @OpenZeppelin/compact-contracts:
- FungibleToken: ERC-20 equivalent
- MultiToken: ERC-1155 equivalent (fungible + NFT)
- NonFungibleToken: NFT implementation
// Using OpenZeppelin
import OpenZeppelin.FungibleToken;
export ledger token: FungibleToken;
export circuit mint(to: Address, amount: Uint<64>): [] {
token.mint(to, amount);
}
export circuit transfer(to: Address, amount: Uint<64>): [] {
token.transfer(to, amount);
}
Awesome Midnight DApps Reference
Real-world implementations for learning:
Getting Started (Official)
Finance & DeFi
Identity & Privacy
Gaming
Developer Tools
Midnight MCP: AI-Assisted Development
Midnight MCP is an MCP server (Model Context Protocol) purpose-built for Midnight development. It gives AI assistants real compiler validation and semantic search across 102 Midnight repositories.
Why Use MCP?
AI assistants hallucinate Compact syntax. Without MCP:
- They invent
Int instead of Uint<32>
- They use
Void instead of [] for empty returns
- They use
state instead of ledger
With MCP, AI generates verified working code.
Installation
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"midnight": {
"command": "npx",
"args": ["-y", "midnight-mcp@latest"]
}
}
}
Cursor (.cursor/mcp.json):
{
"mcpServers": {
"midnight": {
"command": "npx",
"args": ["-y", "midnight-mcp@latest"]
}
}
}
29 Available Tools
| Category | Tools |
|---|
| Search | midnight-search-compact, midnight-search-docs, midnight-search-typescript, midnight-fetch-docs |
| Validation | midnight-compile-contract, midnight-analyze-contract |
| Analysis | midnight-review-contract, midnight-extract-contract-structure |
| Generation | midnight-generate-contract, midnight-document-contract |
| Repository | midnight-get-file, midnight-get-latest-syntax, midnight-list-examples |
| Version | midnight-check-breaking-changes, midnight-get-migration-guide |
Version Management
Use exact versions (e.g., 0.30.0) for reproducibility. Latest versions available on GitHub releases.
CI/CD Integration
setup-compact-action
Official GitHub Action for installing and caching the Compact compiler: midnightntwrk/setup-compact-action
Features:
- Intelligent caching for fast subsequent runs (~2-5 seconds cached vs ~30-60 seconds fresh)
- Version pinning support (e.g.,
0.30.0) or use latest
- name: Setup Compact Compiler
uses: midnightntwrk/setup-compact-action@v1
with:
compact-version: '0.30.0'
- name: Compile Compact code
run: compact compile src/*.compact
| Input | Description | Required | Default |
|---|
compact-version | Version of Compact compiler | No | latest |
cache-enabled | Enable caching | No | true |
Outputs: compact-version, cache-hit
Additional Resources
Official Documentation
Learning Resources
Cross Reference Skills
This skill covers Compact language and contracts. For supporting areas see these related skills.
- midnight-concepts — Zero knowledge architecture, Kachina, privacy mechanisms
- midnight-api — SDK integration, deployment APIs, verification, code quality
- midnight-network — Local devnet, compiler tooling, Docker setup
- midnight-wallet — Wallet SDK, DUST, key management for contract testing
- midnight-dapp-dev — Frontend scaffolding, wallet connect UI, React patterns
- midnight-expert — Health checks, version matrix, contract verification
Troubleshooting
- Compilation errors: Check language version pragma
- Type errors: Verify type annotations
- Disclosure errors: Use
disclose() in conditionals
- Ledger errors: Import CompactStandardLibrary