midnight-compact-guide

Comprehensive guide to writing Compact smart contracts for Midnight Network. Use this skill when writing, reviewing, debugging, or learning Compact code. Triggers on "write a contract", "Compact syntax", "Midnight smart contract", "ledger state", "circuit function", or "ZK proof".

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "midnight-compact-guide" with this command: npx skills add uvroxx/midnight-agent-skills/uvroxx-midnight-agent-skills-midnight-compact-guide

Midnight Compact Language Reference (v0.19+)

CRITICAL: This reference is derived from actual compiling contracts in the Midnight ecosystem (MeshJS starter template). Always verify syntax against this reference before generating contracts.

Quick Start Template

Use this as a starting point - it compiles successfully:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Ledger state (individual declarations, NOT a block)
export ledger counter: Counter;
export ledger owner: Bytes<32>;

// Witness for private/off-chain data (declaration only)
witness local_secret_key(): Bytes<32>;

// Circuit (returns [] not Void)
export circuit increment(): [] {
  counter.increment(1);
}

1. Pragma (Version Declaration)

CORRECT - simple minimum version:

pragma language_version >= 0.19;

WRONG - these will cause issues:

pragma language_version >= 0.14.0;           // ❌ outdated version
pragma language_version >= 0.16 && <= 0.18;  // ❌ outdated, use >= 0.19

2. Imports

Always import the standard library:

import CompactStandardLibrary;

For modular code:

import "path/to/module";
import { SomeType } from "other/module";

3. Ledger Declarations

CORRECT - individual declarations with export ledger:

export ledger counter: Counter;
export ledger owner: Bytes<32>;
export ledger balances: Map<Bytes<32>, Uint<64>>;

// Private state (off-chain only)
ledger secretValue: Field;  // no export = private

WRONG - block syntax is DEPRECATED:

// ❌ This causes parse error: found "{" looking for an identifier
ledger {
  counter: Counter;
  owner: Bytes<32>;
}

Ledger Modifiers

export ledger publicData: Field;           // Public, readable by anyone
export sealed ledger immutableData: Field; // Set once in constructor, cannot change
ledger privateData: Field;                 // Private, not exported

4. Data Types

Primitive Types

TypeDescriptionExample
FieldFinite field element (basic numeric)amount: Field
BooleanTrue or falseisActive: Boolean
Bytes<N>Fixed-size byte arrayhash: Bytes<32>
Uint<N>Unsigned integer (N = 8, 16, 32, 64, 128, 256)balance: Uint<64>
Uint<MIN..MAX>Bounded unsigned integerscore: Uint<0..100>

⚠️ Uint Type Equivalence: Uint<N> and Uint<0..MAX> are the SAME type family.

  • Uint<8> = Uint<0..255>
  • Uint<16> = Uint<0..65535>
  • Uint<64> = Uint<0..18446744073709551615>

Collection Types

TypeDescriptionExample
CounterIncrementable/decrementablecount: Counter
Map<K, V>Key-value mappingMap<Bytes<32>, Uint<64>>
Set<T>Unique value collectionSet<Bytes<32>>
Vector<N, T>Fixed-size arrayVector<3, Field>
List<T>Dynamic listList<Bytes<32>>
Maybe<T>Optional valueMaybe<Bytes<32>>
Either<L, R>Union typeEither<Field, Bytes<32>>
Opaque<"type">External type from TypeScriptOpaque<"string">

Custom Types

Enums - must use export to access from TypeScript:

export enum GameState { waiting, playing, finished }
export enum Choice { rock, paper, scissors }

Enum Access - use DOT notation (not Rust-style ::):

// ✅ CORRECT - dot notation
if (choice == Choice.rock) { ... }
game_state = GameState.waiting;

// ❌ WRONG - Rust-style double colon
if (choice == Choice::rock) { ... }  // Parse error!

Structs:

export struct PlayerConfig {
  name: Opaque<"string">,
  score: Uint<32>,
  isActive: Boolean,
}

5. Circuits

Circuits are on-chain functions that generate ZK proofs.

CRITICAL: Return type is [] (empty tuple), NOT Void:

// ✅ CORRECT - returns []
export circuit increment(): [] {
  counter.increment(1);
}

// ✅ CORRECT - with parameters
export circuit transfer(to: Bytes<32>, amount: Uint<64>): [] {
  assert(amount > 0, "Amount must be positive");
  // ... logic
}

// ✅ CORRECT - with return value
export circuit getBalance(addr: Bytes<32>): Uint<64> {
  return balances.lookup(addr);
}

// ❌ WRONG - Void does not exist
export circuit broken(): Void {  // Parse error!
  counter.increment(1);
}

Circuit Modifiers

export circuit publicFn(): []      // Callable externally
circuit internalFn(): []           // Internal only, not exported
export pure circuit hash(x: Field): Bytes<32>  // No state access

6. Witnesses

Witnesses provide off-chain/private data to circuits. They run locally, not on-chain.

CRITICAL: Witnesses are declarations only - NO implementation body in Compact! The implementation goes in your TypeScript prover.

// ✅ CORRECT - declaration only, semicolon at end
witness local_secret_key(): Bytes<32>;
witness get_merkle_path(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;
witness store_locally(data: Field): [];
witness find_user(id: Bytes<32>): Maybe<UserData>;

// ❌ WRONG - witnesses cannot have bodies
witness get_caller(): Bytes<32> {
  return public_key(local_secret_key());  // ERROR!
}

7. Constructor

Optional - initializes sealed ledger fields at deploy time:

export sealed ledger owner: Bytes<32>;
export sealed ledger nonce: Bytes<32>;

constructor(initNonce: Bytes<32>) {
  owner = disclose(public_key(local_secret_key()));
  nonce = disclose(initNonce);
}

8. Pure Circuits (Helper Functions)

Use pure circuit for helper functions that don't modify ledger state:

// ✅ CORRECT - use "pure circuit"
pure circuit determine_winner(p1: Choice, p2: Choice): Result {
  if (p1 == p2) {
    return Result.draw;
  }
  // ... logic
}

// ❌ WRONG - "function" keyword doesn't exist
pure function determine_winner(p1: Choice, p2: Choice): Result {
  // ERROR: unbound identifier "function"
}

9. Common Operations

Counter Operations

counter.increment(1);           // Increase by amount (Uint<16>)
counter.decrement(1);           // Decrease by amount (Uint<16>)
const val = counter.read();     // Get current value (returns Uint<64>)
const low = counter.lessThan(100); // Compare with threshold (Boolean)
counter.resetToDefault();       // Reset to zero

// ⚠️ WRONG: counter.value() does NOT exist - use counter.read()

Map Operations

// Insert/update operations
balances.insert(address, 100);           // insert(key, value): []
balances.insertDefault(address);         // insertDefault(key): []

// Query operations (all work in circuits ✅)
const balance = balances.lookup(address);  // lookup(key): value_type
const exists = balances.member(address);   // member(key): Boolean
const empty = balances.isEmpty();          // isEmpty(): Boolean
const count = balances.size();             // size(): Uint<64>

// Remove operations
balances.remove(address);                // remove(key): []
balances.resetToDefault();               // resetToDefault(): []

Set Operations

// Insert/remove operations
members.insert(address);                    // insert(elem): []
members.remove(address);                    // remove(elem): []
members.resetToDefault();                   // resetToDefault(): []

// Query operations (all work in circuits ✅)
const isMember = members.member(address);   // member(elem): Boolean
const empty = members.isEmpty();            // isEmpty(): Boolean
const count = members.size();               // size(): Uint<64>

Maybe Operations

const opt: Maybe<Field> = some<Field>(42);
const empty: Maybe<Field> = none<Field>();

if (opt.is_some) {
  const val = opt.value;
}

Type Casting

const bytes: Bytes<32> = myField as Bytes<32>;  // Field to Bytes
const num: Uint<64> = myField as Uint<64>;      // Field to Uint (bounds not checked!)
const field: Field = myUint as Field;           // Uint to Field (safe)

Hashing

// Persistent hash (same input = same output across calls)
const hash = persistentHash<Vector<2, Bytes<32>>>([data1, data2]);

// Persistent commit (hiding commitment)
const commit = persistentCommit<Field>(value);

10. Assertions

assert(condition, "Error message");
assert(amount > 0, "Amount must be positive");
assert(disclose(caller == owner), "Not authorized");

11. Common Patterns

Authentication Pattern

witness local_secret_key(): Bytes<32>;

// IMPORTANT: public_key() is NOT a builtin - use this pattern
circuit get_public_key(sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:pk:"), sk]);
}

export circuit authenticated_action(): [] {
  const sk = local_secret_key();
  const caller = get_public_key(sk);
  assert(disclose(caller == owner), "Not authorized");
  // ... action
}

Commit-Reveal Pattern

pragma language_version >= 0.19;

import CompactStandardLibrary;

export ledger commitment: Bytes<32>;
export ledger revealed_value: Field;
export ledger is_revealed: Boolean;

witness local_secret_key(): Bytes<32>;
witness store_secret_value(v: Field): [];
witness get_secret_value(): Field;

// Helper: compute commitment hash
circuit compute_commitment(value: Field, salt: Bytes<32>): Bytes<32> {
  const value_bytes = value as Bytes<32>;
  return persistentHash<Vector<2, Bytes<32>>>([value_bytes, salt]);
}

// Commit phase
export circuit commit(value: Field): [] {
  const salt = local_secret_key();
  store_secret_value(value);
  commitment = disclose(compute_commitment(value, salt));
  is_revealed = false;
}

// Reveal phase
export circuit reveal(): Field {
  const salt = local_secret_key();
  const value = get_secret_value();
  const expected = compute_commitment(value, salt);
  assert(disclose(expected == commitment), "Value doesn't match commitment");
  assert(disclose(!is_revealed), "Already revealed");

  revealed_value = disclose(value);
  is_revealed = true;
  return disclose(value);
}

Disclosure in Conditionals

When branching on witness values, wrap comparisons in disclose():

// ✅ CORRECT
export circuit check(guess: Field): Boolean {
  const secret = get_secret();  // witness
  if (disclose(guess == secret)) {
    return true;
  }
  return false;
}

// ❌ WRONG - will not compile
export circuit check_broken(guess: Field): Boolean {
  const secret = get_secret();
  if (guess == secret) {  // implicit disclosure error
    return true;
  }
  return false;
}

12. Common Mistakes to Avoid

MistakeCorrect
ledger { field: Type; }export ledger field: Type;
circuit fn(): Voidcircuit fn(): []
pragma >= 0.16.0pragma language_version >= 0.19;
enum State { ... }export enum State { ... }
if (witness_val == x)if (disclose(witness_val == x))
Cell<Field>Field (Cell is deprecated)
counter.value()counter.read()
pure function helper()pure circuit helper()
Choice::rockChoice.rock (use dot, not ::)

13. Exports for TypeScript

To use types/values in TypeScript, they must be exported:

// These are accessible from TypeScript
export enum GameState { waiting, playing }
export struct Config { value: Field }
export ledger counter: Counter;
export circuit play(): []

// Standard library re-exports (if needed in TS)
export { Maybe, Either, CoinInfo };

Reference Contracts

These contracts compile successfully and demonstrate correct patterns:

  1. Counter (beginner): midnightntwrk/example-counter
  2. Bulletin Board (intermediate): midnightntwrk/example-bboard
  3. Naval Battle Game (advanced): ErickRomeroDev/naval-battle-game_v2
  4. Sea Battle (advanced): bricktowers/midnight-seabattle

When in doubt, reference these repos for working syntax.


Rules

See /rules/ directory for detailed pattern documentation:

  • privacy-selective-disclosure.md - ZK disclosure patterns
  • tokens-shielded-unshielded.md - Token vault patterns

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

midnight-test-runner

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

midnight-deploy

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

midnight-infra-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

midnight-sdk-guide

No summary provided by upstream source.

Repository SourceNeeds Review