midnight-compact

Installation
SKILL.md

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:

  1. Replicated component on a public ledger
  2. Zero-knowledge circuit that confidentially proves correct execution
  3. 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

# 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

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

Troubleshooting

  • Compilation errors: Check language version pragma
  • Type errors: Verify type annotations
  • Disclosure errors: Use disclose() in conditionals
  • Ledger errors: Import CompactStandardLibrary
Related skills

More from mzf11125/midnight_agent_skills

Installs
8
GitHub Stars
4
First Seen
Feb 22, 2026