AgentStack
SKILL verified MIT Self-run

Midnight Compact Guide

skill-uvroxx-midnight-agent-skills-midnight-compact-guide · by UvRoxx

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".

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-uvroxx-midnight-agent-skills-midnight-compact-guide

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Midnight Compact Guide? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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;

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

// 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.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;
export ledger balances: Map, Uint>;

// 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;
}

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

| Type | Description | Example | |------|-------------|---------| | Field | Finite field element (basic numeric) | amount: Field | | Boolean | True or false | isActive: Boolean | | Bytes | Fixed-size byte array | hash: Bytes | | Uint | Unsigned integer (N = 8, 16, 32, 64, 128, 256) | balance: Uint | | Uint | Bounded unsigned integer | score: Uint |

⚠️ Uint Type Equivalence: Uint and Uint are the SAME type family.

  • Uint = Uint
  • Uint = Uint
  • Uint = Uint

Collection Types

| Type | Description | Example | |------|-------------|---------| | Counter | Incrementable/decrementable | count: Counter | | Map | Key-value mapping | Map, Uint> | | Set | Unique value collection | Set> | | Vector | Fixed-size array | Vector | | List | Dynamic list | List> | | Maybe | Optional value | Maybe> | | Either | Union type | Either> | | Opaque | External type from TypeScript | Opaque |

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,
  score: Uint,
  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, amount: Uint): [] {
  assert(amount > 0, "Amount must be positive");
  // ... logic
}

// ✅ CORRECT - with return value
export circuit getBalance(addr: Bytes): Uint {
  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  // 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;
witness get_merkle_path(leaf: Bytes): MerkleTreePath>;
witness store_locally(data: Field): [];
witness find_user(id: Bytes): Maybe;

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

7. Constructor

Optional - initializes sealed ledger fields at deploy time:

export sealed ledger owner: Bytes;
export sealed ledger nonce: Bytes;

constructor(initNonce: Bytes) {
  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)
counter.decrement(1);           // Decrease by amount (Uint)
const val = counter.read();     // Get current value (returns Uint)
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

// 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

Maybe Operations

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

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

Type Casting

const bytes: Bytes = myField as Bytes;  // Field to Bytes
const num: Uint = myField as Uint;      // 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>>([data1, data2]);

// Persistent commit (hiding commitment)
const commit = persistentCommit(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;

// IMPORTANT: public_key() is NOT a builtin - use this pattern
circuit get_public_key(sk: Bytes): Bytes {
  return persistentHash>>([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;
export ledger revealed_value: Field;
export ledger is_revealed: Boolean;

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

// Helper: compute commitment hash
circuit compute_commitment(value: Field, salt: Bytes): Bytes {
  const value_bytes = value as Bytes;
  return persistentHash>>([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

| Mistake | Correct | |---------|---------| | ledger { field: Type; } | export ledger field: Type; | | circuit fn(): Void | circuit fn(): [] | | pragma >= 0.16.0 | pragma language_version >= 0.19; | | enum State { ... } | export enum State { ... } | | if (witness_val == x) | if (disclose(witness_val == x)) | | Cell | Field (Cell is deprecated) | | counter.value() | counter.read() | | pure function helper() | pure circuit helper() | | Choice::rock | Choice.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 & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.