kali-decoder/midnight-skills

compact

Write, reason about, and debug Compact smart contracts for the Midnight blockchain. Use this skill whenever a user asks about Compact syntax, ledger state, witnesses, circuits, disclosure, ZK patterns, data types, standard library functions, security patterns, nullifier design, Merkle trees, or anything related to writing .compact files. Also trigger for questions about compiling contracts, debugging circuit errors, choosing between ledger ADTs, and implementing privacy-preserving patterns like…

First seen Jul 2, 2026

Installation

$ npx skills add kali-decoder/midnight-skills --skill compact

Summary

  • Write, reason about, and debug Compact smart contracts for the Midnight blockchain.
  • Use this skill whenever a user asks about Compact syntax, ledger state, witnesses, circuits, disclosure, ZK patterns, data types, standard library functions, security patterns, nullifier design, Merkle trees, or anything related to writing .compact files.
  • Also trigger for questions about compiling contracts, debugging circuit errors, choosing between ledger ADTs, and implementing privacy-preserving patterns like commitments, hash-based auth, and anonymous membership proofs.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from kali-decoder/midnight-skills · top by installs.

npx skills add kali-decoder/midnight-skills

Browse all from kali-decoder/midnight-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 16
License LICENSE
Default branch main
Open issues 5
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 18,406 B
  • docs SUMMARY.md 577 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 131 installs

SKILL.md

Compact Language Skill

Compact is Midnight's smart contract language. It looks like TypeScript but compiles to zero-knowledge circuits. The compiler handles all cryptographic machinery — you write business logic.

Primary references:

  • compact-book.vercel.app — structured example-driven reference (your best source)
  • docs.midnight.network/compact — official spec (testing/security pages are "coming soon" as of 2026)

1) The Mental Model

Three non-negotiables before writing anything:

1. A circuit is a constraint system, not a function.
   It declares relationships that MUST hold. The proof proves they held.

2. The ledger is public. Everything in `export ledger` is plaintext on-chain.
   Private data lives in witnesses. It never touches the chain.

3. `disclose()` is a compile-time annotation, not encryption.
   It tells the compiler "I am intentionally making this public."
   The compiler prevents accidental disclosure. No runtime cost.

The two worlds:

World Where Who sees it Updated by
export ledger Every network node Everyone ZK proof
witness (private) User's local storage Only the owner Never (stays local)

2) Contract Structure

Every Compact contract has four mandatory pieces + one optional:

pragma language_version >= 0.22;      // 1. Language version

import CompactStandardLibrary;         // optional, needed for hashing, Merkle, etc.

export ledger counter: Uint<64>;       // 2. Public state (on-chain, readable by all)

witness callerSecret(): Bytes<32>;     // 3. Private input (body lives in TypeScript)

constructor() {                        // 5. OPTIONAL: runs once on deployment
  counter = 0;
}

export circuit increment(): [] {       // 4. Logic (compiles to ZK circuit)
  counter = disclose((counter + 1) as Uint<64>);
}
Piece Keyword Purpose
Public state export ledger On-chain, readable by all
Private inputs witness Callbacks the DApp provides (body in TypeScript)
Logic export circuit Compiles to ZK circuits
Initialization constructor Runs once on deploy
Organization module / import Namespace and file management

3) Data Types

Primitives

Type Range/Size Notes
Boolean true / false No truthy/falsy coercion
Field 0 to prime field order Only == and != — no <, >
Uint<n> n-bit unsigned, wraps on overflow Uint<8> = 0..255
Uint<0..n> Bounded integer, 0 to n-1 Supports <, >. Cast can fail at runtime
Bytes<n> Exactly n bytes Use pad(n, str) for fixed-length from strings
Opaque<"string"> Foreign JS data Circuits see only hash, not contents. Not hidden on-chain

Critical: Use Field only for equality checks. Use Uint<0..n> whenever you need ordering (<, <=, >, >=).

Composite Types

// Tuple
const pair: [Field, Boolean] = [42, true];
const first = pair[0];

// Vector (homogeneous, fixed-length)
const v: Vector<4, Uint<8>> = [1, 2, 3, 4];

// Struct (nominal typing — shape isn't enough, name matters)
struct Point { x: Uint<32>, y: Uint<32> }
const p = Point { x: 10, y: 20 };

// Enum (first variant = default)
enum State { VACANT, OCCUPIED }

Type Aliases

type Hash = Bytes<32>;        // structural alias — fully interchangeable
new type UserId = Bytes<32>;  // nominal alias — requires explicit cast

Default Values

Every type has a zero default: false, 0, n zero bytes, first enum variant. Access with default<T>().


4) Ledger State (Public) and disclose()

Ledger Modifiers

ledger val: Field;                          // private to contract, not exported
export ledger cnt: Counter;                 // readable from TypeScript/DApp
sealed ledger config: Uint<32>;             // write-once (only in constructor)
export sealed ledger mapping: Map<Boolean, Field>;

The disclose() Rule

If data flows from a witness to a ledger write (or circuit return), it must be wrapped in disclose(). The compiler tracks witness taint through every operation and will error if untainted disclosure reaches the ledger.

// ❌ COMPILER ERROR — undisclosed witness data reaching ledger
export circuit record(): [] {
  stored = getSecret();
}

// ✅ CORRECT — explicitly disclosed
export circuit record(): [] {
  stored = disclose(getSecret());
}

disclose() is not encryption. It is a compile-time signal. The disclosed value is plaintext on-chain.

Ledger ADT Chooser

Use case Type
Single mutable value ledger f: T (Cell)
Monotonically growing counter Counter (use .increment(n))
Membership tracking (reveals which) Set<T>
Per-key storage Map<K, V>
Ordered queue List<T>
Anonymous membership proofs (current root) MerkleTree<n, T>
Anonymous membership proofs (past roots) HistoricMerkleTree<n, T>
Block time, tokens, contract address Kernel

5) Circuits and Witnesses

Circuits

// Exported = callable from DApp
export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}

// Pure = no ledger access, no side effects
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<3, Bytes<32>>>([pad(32, "domain:"), seq, sk]);
}

// Internal (no export keyword) — not callable from DApp
circuit hashTokenData(owner: Bytes<32>, meta: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([owner, meta]);
}
  • [] as return type = no return value (procedure-style)
  • assert(condition, "message") = runtime guard, transaction fails if condition is false
  • Always validate witness outputs with assert — witnesses are untrusted

Witnesses

A witness declares a private input callback. The signature is in Compact, the body is in TypeScript.

// Compact side — declaration only
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;
// TypeScript side — implementation
function callerAddress(context: WitnessContext): Uint8Array {
  return context.coinPublicKey;  // or wallet-derived address
}

function findMerklePath(
  context: WitnessContext,
  leaf: Uint8Array,
): MerkleTreePath<Uint8Array> {
  return context.ledger.items.findPathForLeaf(leaf)!;
}

Witness data never touches the chain. Only the ZK proof that the circuit ran correctly goes on-chain.


6) Standard Library — Key Functions

import CompactStandardLibrary;

Hashing

Function Output Survives upgrade? Witness-tainted? Use for
transientHash<T>(v) Field No Yes Temporary computations
transientCommit<T>(v, rand) Field No No Temp computation without disclose()
persistentHash<T>(v) Bytes<32> Yes Yes Keys, IDs on-chain
persistentCommit<T>(v, rand) Bytes<32> Yes No Hiding sensitive values on-chain

Key distinction:

  • *Hash = hash of value only. Brute-forcible for small value spaces.
  • *Commit = hash of value + random nonce. Infeasible to brute-force even for small values.
  • Persistent = survives contract upgrades. Use for ledger storage.
  • Transient = temporary. Does NOT survive upgrades.

Padding and Conversion

pad(32, "midnight:domain:separator")   // Creates Bytes<32> from string

Merkle Trees

// Check that a path validates to the current root
assert(
  items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
  "invalid merkle path"
);

// TypeScript: get the path
const path = context.ledger.items.findPathForLeaf(leaf);  // O(n) scan
const path = context.ledger.items.pathForLeaf(index);      // O(1) with index

Maybe / Option

export ledger message: Maybe<Opaque<"string">>;

message = some<Opaque<"string">>(newMessage);   // has value
message = none<Opaque<"string">>();              // empty

const val = message.value;  // access inner value (only when present)

7) Security Patterns

The official security docs are "coming soon." Use these patterns from the compact-book.

What's Public (assume everything is)

Operation What it reveals
ledger.insert(v) The value v
ledger.lookup(k) Key k and the returned value
set.member(f(x)) f(x), not x
merkleTree.insert(v) Does NOT reveal v
Circuit arguments All of them
witness return values Nothing (stays local)

Pattern 1: Commitment (Store Hash, Not Value)

export ledger balanceCommitments: Map<Bytes<32>, Bytes<32>>;

export circuit commitBalance(value: Uint<64>): [] {
  const nonce = freshNonce();
  const commitment = persistentCommit<Uint<64>>(value, nonce);
  balanceCommitments.insert(disclose(callerAddress()), disclose(commitment));
}

Nonce reuse = privacy catastrophe. Two commitments with same nonce + value are identical on-chain. Always fresh nonce.

Pattern 2: Hash-Based Auth (ZK Signatures via Hashing)

witness secretKey(): Bytes<32>;
export ledger organizer: Bytes<32>;

constructor() {
  organizer = disclose(publicKey(secretKey()));
}

export circuit adminAction(): [] {
  assert(organizer == publicKey(secretKey()), "not authorized");
  // ... do the thing
}

pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>(
    [pad(32, "myapp:admin:v1"), sk]
  );
}

Domain separator is critical. Same secret key → different public keys for different purposes. Never reuse a public key across domains.

Pattern 3: Merkle Tree for Anonymous Membership

Set<T> reveals which commitment was checked. MerkleTree<n, T> only reveals that some value was proven.

export ledger allowlist: MerkleTree<16, Bytes<32>>;
witness findPath(leaf: Bytes<32>): MerkleTreePath<16, Bytes<32>>;

export circuit addToAllowlist(commitment: Bytes<32>): [] {
  allowlist.insert(disclose(commitment));  // insert still requires disclose
}

export circuit proveAllowlisted(): [] {
  const sk = secretKey();
  const leaf = publicKey(sk);
  const path = findPath(leaf);
  assert(
    allowlist.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
    "not in allowlist"
  );
  // proof: caller is in the allowlist. Nothing else revealed.
}

Depth guide: each level adds ~1 to circuit depth. 16–20 is typical. Use minimum depth needed.

Pattern 4: Commitment/Nullifier (Single-Use Anonymous Tokens)

export ledger commitments: HistoricMerkleTree<16, Bytes<32>>;
export ledger nullifiers: Set<Bytes<32>>;

export circuit spend(): [] {
  const sk = secretKey();
  const commitment = makeCommitment(sk);
  const path = findCommitmentPath(commitment);
  
  // 1. Prove membership
  assert(
    commitments.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
    "not in commitments tree"
  );
  
  // 2. Prevent reuse
  const nul = makeNullifier(sk);
  assert(!nullifiers.member(nul), "already spent");
  nullifiers.insert(disclose(nul));
}

// CRITICAL: commitment and nullifier MUST use different domain separators
pure circuit makeCommitment(sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:commit:v1"), sk]);
}

pure circuit makeNullifier(sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:nullifier:v1"), sk]);
}

Use HistoricMerkleTree only when proofs must verify against past roots (i.e., items may be added after the proof was computed). Otherwise use plain MerkleTree.


8) Complete Contract Examples

Minimal Bulletin Board (No Privacy)

pragma language_version >= 0.22;

export ledger message: Opaque<"string">;

export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}

Bulletin Board with Ownership (Privacy Pattern)

pragma language_version >= 0.22;

import CompactStandardLibrary;

export enum State { VACANT, OCCUPIED }

export ledger state: State;
export ledger message: Maybe<Opaque<"string">>;
export ledger sequence: Counter;
export ledger owner: Bytes<32>;

witness localSecretKey(): Bytes<32>;

constructor() {
  state = State.VACANT;
  message = none<Opaque<"string">>();
  sequence.increment(1);
}

export circuit post(newMessage: Opaque<"string">): [] {
  assert(state == State.VACANT, "Board occupied");
  owner = disclose(publicKey(localSecretKey(), sequence as Field as Bytes<32>));
  message = disclose(some<Opaque<"string">>(newMessage));
  state = State.OCCUPIED;
}

export circuit takeDown(): Opaque<"string"> {
  assert(state == State.OCCUPIED, "Board empty");
  assert(owner == publicKey(localSecretKey(), sequence as Field as Bytes<32>), "Not owner");
  const msg = message.value;
  state = State.VACANT;
  sequence.increment(1);
  message = none<Opaque<"string">>();
  return msg;
}

pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<3, Bytes<32>>>([pad(32, "bboard:pk:"), seq, sk]);
}

Why sequence? Prevents replay attacks — same secret key produces different public key each round.

NFT Contract (Commitment-Based Ownership)

pragma language_version >= 0.22;

import CompactStandardLibrary;

export ledger totalSupply: Uint<64>;
export ledger nextTokenId: Uint<64>;
export ledger tokenCommitments: Map<Uint<64>, Bytes<32>>;

witness callerAddress(): Bytes<32>;

constructor() {
  totalSupply = 0;
  nextTokenId = 1;
}

circuit hashTokenData(owner: Bytes<32>, metaHash: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([owner, metaHash]);
}

export circuit mint(metaHash: Bytes<32>): [] {
  const caller = disclose(callerAddress());
  const tokenId = nextTokenId;
  const commitment = hashTokenData(caller, metaHash);
  tokenCommitments.insert(tokenId, disclose(commitment));
  totalSupply = disclose((totalSupply + 1) as Uint<64>);
  nextTokenId = disclose((tokenId + 1) as Uint<64>);
}

export circuit transfer(tokenId: Uint<64>, newOwner: Bytes<32>, metaHash: Bytes<32>): [] {
  const caller = disclose(callerAddress());
  const expected = hashTokenData(caller, metaHash);
  const pubId = disclose(tokenId);
  assert(tokenCommitments.member(pubId), "Token does not exist");
  assert(tokenCommitments.lookup(pubId) == expected, "Not the owner");
  const nextCommitment = hashTokenData(disclose(newOwner), metaHash);
  tokenCommitments.insert(pubId, disclose(nextCommitment));
}

9) Compilation and Tooling

# Compile a contract
compact compile contracts/your-contract.compact contracts/managed/your-contract

# Output structure
contracts/managed/your-contract/
  keys/
    circuitName.prover      # 2–10 MB
    circuitName.verifier    # ~2 KB
  zkir/
    circuitName.bzkir       # 1–3 KB

TypeScript bindings are also generated — import the Contract object from the managed output folder.

Circuit size on preview: ProofStation requires minimum circuit size (k ≥ 6). Error: prove: no SRS params for k=6 means the circuit is too small. Pad with additional dummy ledger fields to increase size.


10) Common Mistakes (The Full List)

Disclosure mistakes:

  • Writing witness data to the ledger without disclose() → compiler error (good, caught early)
  • Returning witness data from export circuit without disclose() → compiler error
  • Thinking disclose() encrypts data → it doesn't, it's plaintext

Type mistakes:

  • Using Field when you need <, > → use Uint<0..n> instead
  • as Uint<0..n> cast at runtime → can fail if value is out of range (transaction error, not compile error)
  • Confusing type Hash = Bytes<32> (structural, interchangeable) with new type UserId = Bytes<32> (nominal, requires cast)

Privacy mistakes:

  • Using persistentHash for small value spaces (brute-forcible) → use persistentCommit with fresh nonce
  • Reusing nonces → two same-value commitments are identical on-chain
  • Using Set<T> when you need anonymity → Set.member(v) reveals v. Use MerkleTree instead
  • Same domain separator for commitment and nullifier → potential collision
  • Using transientHash for ledger storage → transient values don't survive contract upgrades
  • Putting per-user data in export ledger → privacy leak

Ledger mistakes:

  • Forgetting assert on witness outputs → witnesses are untrusted, validate them
  • Writing logic in constructor that should be in circuits → constructor runs once and is done
  • Using HistoricMerkleTree unnecessarily → adds storage cost; use MerkleTree unless past roots are needed
  • export on internal helper circuits → exposes surface area unnecessarily

11) Keyword Quick Reference

Keyword Purpose
pragma language_version >= n Declare minimum language version
import ModuleName Import a module
export ledger f: T Public on-chain state field
sealed ledger f: T Write-once state (constructor only)
witness f(): T Private input callback (body in TypeScript)
export circuit f(): T Callable ZK circuit
pure circuit f(): T Pure function (no ledger access)
constructor() Runs once on deployment
disclose(v) Mark intentional public disclosure
assert(cond, msg) Runtime guard (tx fails if false)
default<T>() Zero/empty value for type T
pad(n, str) Create Bytes<n> from string
some<T>(v) / none<T>() Maybe/Option constructors
new type A = B Nominal type alias
type A = B Structural type alias