midnight-compact
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/importfor namespace managementstruct/enum/typefor program-defined typesexport ledgerfor public state storagewitnessfor callback functions (private state)circuitfor the operational coreconstructorfor 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 ofUint<0..M>if N ≤ MUint<0..N>subtype ofFieldif 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
# Using official CLI
npx create-midnight-app my-project
cd my-project
# Or manual setup
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
# Output in src/ directory
5. Deploy
# Using deployment script
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
- LunarSwap - UTXO-based DEX
- Hydra Stake - Liquid staking
- Statera Protocol - Stablecoin
Identity & Privacy
- Midnames - ZK DID
- Midnight Cloak - ZK verification SDK
- KYC Midnight - KYC attestations
Gaming
- Midnight Starship - Space shooter
- Midnight Seabattle - Sea battle
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
Intinstead ofUint<32> - They use
Voidinstead of[]for empty returns - They use
stateinstead ofledger
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 uselatest
- 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
- Learn Compact - Interactive book
- Compact By Example
- Midnight MCP - AI assistant integration
Troubleshooting
- Compilation errors: Check language version pragma
- Type errors: Verify type annotations
- Disclosure errors: Use
disclose()in conditionals - Ledger errors: Import CompactStandardLibrary
More from mzf11125/midnight_agent_skills
midnight-network
Guide to Midnight Network infrastructure, validators, indexers, and network operations. Use when users need to run Midnight validators and participate in consensus, set up and configure Midnight indexers for blockchain data, configure network nodes and infrastructure, monitor validator performance and network health, understand network parameters and configuration, deploy and manage network infrastructure, troubleshoot network issues, and access node release information and compatibility.
9midnight-api
Comprehensive guide to Midnight Network APIs (v8.0+) for building decentralized applications. Use when users need to integrate Midnight APIs including Compact Runtime, DApp Connector, ZSwap, Wallet, and Ledger APIs, connect DApps to Midnight wallets, generate and verify zero-knowledge proofs programmatically, manage transactions and blockchain state, deploy and interact with Compact smart contracts, query blockchain data via indexer, implement wallet functionality, handle Zswap private transactions, and build complete web3 applications on Midnight.
8midnight-concepts
Foundational knowledge about Midnight Network zero-knowledge blockchain technology, privacy mechanisms, and architecture. Use when users need to understand zero-knowledge proofs, privacy mechanisms like Zswap and selective disclosure, partner chain architecture, real-world use cases for private DeFi and voting, when to use Midnight for privacy-preserving applications, and core concepts of the Midnight ecosystem.
7