Introduction

Welcome to Learn Compact, the comprehensive guide to Midnight’s zero-knowledge smart contract language.

What is Compact?

Compact is a domain-specific language designed for writing smart contracts on the Midnight blockchain. What makes Compact special is its native support for zero-knowledge proofs (ZKPs), enabling you to build applications where users can prove facts about their data without revealing the data itself.

What is Midnight?

Midnight is a data-protection blockchain that uses zero-knowledge cryptography to enable confidential transactions and private smart contracts. Unlike traditional public blockchains where all transaction data is visible, Midnight allows developers to choose what information is public and what remains private. This is called “programmable privacy“.

What are Zero-Knowledge Proofs?

A zero-knowledge proof is a cryptographic method that allows one party (the prover) to prove to another party (the verifier) that a statement is true, without revealing any additional information beyond the validity of the statement.

Example: Imagine proving you’re over 21 without showing your ID. A ZK proof can verify “user is over 21” without revealing your actual birthdate.

In Compact, ZK circuits are generated automatically when you compile. Actual ZK proofs are then generated locally at execution time to enable selective disclosure, which ensures the underlying data remains private.

How This Book is Organized

This book is divided into seven parts:

  1. Foundations - Installation, setup, and basic concepts
  2. The Type System - All about Compact’s types
  3. Core Language Features - Circuits, witnesses, control flow
  4. State Management - Working with ledger state
  5. Advanced Concepts - Standard library, modules, TypeScript integration
  6. Projects - Build real applications
  7. Best Practices & Reference - Common mistakes, testing, and appendixes

Each chapter includes:

  • 📖 Explanations - Clear descriptions of concepts
  • 💻 Code Examples - Runnable Compact code
  • 🏋️ Exercises - Practice what you’ve learned (in the exercises/ folder)

Prerequisites

To get the most out of this book, you should have:

  • Programming experience in any language
  • TypeScript/JavaScript knowledge (recommended but not required)
  • Basic understanding of blockchain concepts (helpful but we’ll cover basics)

No prior experience with zero-knowledge proofs or cryptography is required.

How to Use This Book

We recommend:

  1. Read sequentially - Chapters build on each other
  2. Type the code - Don’t just copy-paste
  3. Do the exercises - They reinforce learning
  4. Build the projects - Apply what you’ve learned

Version Information

This book covers:

  • Compact Language: v0.16+ (current toolchain implements v0.23)
  • Compiler: Compact toolchain 0.31.1, installed via the compact devtools CLI 0.5.1
  • Last Updated: July 2026

Let’s begin!

Getting Started

In this chapter, you’ll set up everything needed to develop Compact smart contracts:

  1. Install the Compact compiler
  2. Set up the Lace wallet for testing
  3. Get test tokens from the faucet
  4. Configure Docker for the proof server
  5. Install the VS Code extension
  6. Write and compile your first contract

By the end of this chapter, you’ll have a complete development environment and your first compiled Compact contract.

Let’s get started!

Installing the Compact Compiler

The Compact compiler transforms your Compact source code into ZK circuits that can run on the Midnight blockchain.

Understanding the Tools

Midnight provides two command-line tools:

ToolPurposeStatus
compactThe Compact developer tools — toolchain management, compiling, formatting✅ Use this
compactcThe Compact compiler (standalone)⚠️ Deprecated

In practice:

  • Use compact compile for compilation: compact compile contract.compact output/
  • Use compact update / compact check to manage toolchain versions, compact format to format code, and compact self check to update the devtools themselves (run compact --help for the full list — there is no scaffolding subcommand; new projects are created with npx create-mn-app)

⚠️ Important: The standalone compactc command is deprecated. Always use compact compile instead.

For the exercises in this book, we’ll use compact compile for compilation.

System Requirements

  • Operating System: macOS or Linux (Windows via WSL2)
  • Node.js: v22 or later
  • Docker: For running the proof server

⚠️ Windows Users: You must use WSL2. PowerShell and Command Prompt are not supported.

Installation

Windows/WSL Only: you may need to install curl first:

sudo apt-get update && sudo apt-get install -y curl

Other OS: Install the Compact CLI using the official installer script:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh

This downloads and installs the compact CLI, which includes the compactc compiler.

Update Your Shell PATH

The installer usually adds the binary to your PATH automatically. If not, add it manually:

export PATH="$HOME/.compact/bin:$PATH"

Then reload your shell:

source ~/.zshrc  # or source ~/.bashrc

Verifying Installation

After installation, verify that the tools are working:

compact --version

You should see output similar to:

compact 0.5.1

Test compilation works:

compact compile --help

Updating the Compiler

To update to the latest version:

compact update

Troubleshooting

Command not found

If you see “command not found”, add the Compact binary to your PATH:

export PATH="$HOME/.compact/bin:$PATH"

Add this line to your ~/.bashrc or ~/.zshrc to make it permanent.

Permission denied

If you encounter permission errors:

chmod +x ~/.compact/bin/compact

WSL-Specific Issues

For Windows users on WSL2, see the comprehensive troubleshooting guide in the Environment Setup chapter.

Next Steps

With the compiler installed, let’s set up the rest of your development environment.

Setting Up Your Environment

Beyond the compiler, you’ll need several tools to develop and test Compact contracts.

Lace Wallet

The Lace wallet is a browser extension that manages your Midnight accounts and signs transactions. For Midnight, it is available as a Chrome or Edge extension.

Installation

  1. Install the Lace wallet extension from the Chrome Web Store or the Edge Add-ons store
  2. Click Add to Chrome (or Get on Edge) and confirm
  3. Pin the extension to your toolbar for easy access (recommended)

Create Your Wallet

  1. Click the Lace wallet icon in your toolbar
  2. Select Create a new wallet — Midnight appears as a network option
  3. Choose a strong password
  4. Write down your seed phrase on paper and store it securely offline
  5. Confirm your seed phrase to complete setup
  6. Set the Network to Preprod (or Preview if you want the bleeding-edge test network)

⚠️ Warning: Never store your seed phrase digitally or share it with anyone.

Getting Test Tokens (tNIGHT → tDUST)

Midnight’s test networks use two test assets, mirroring Mainnet’s NIGHT and DUST:

  • tNIGHT — the test NIGHT token. You get this from the faucet.
  • tDUST — the resource that pays transaction fees. You don’t get tDUST from the faucet; your wallet generates it from your tNIGHT holdings.

Neither has real-world value.

  1. In your Lace wallet, copy your Unshielded wallet address
  2. Go to the faucet for your network:
  3. Paste your address into the form and click Request tokens — the faucet sends 1000 tNIGHT within a couple of minutes
  4. In Lace, go to Tokens, click Generate tDUST, and confirm the transaction
  5. Wait 1–2 minutes for tDUST to start accruing — it generates continuously from your tNIGHT

Verification: Your Lace wallet shows a tNIGHT balance and a growing tDUST balance.

Note: Without tDUST, every transaction fails with an insufficient-funds error — requesting tNIGHT alone is not enough. See the official Get faucet tokens guide for troubleshooting.

Docker Setup

The proof server runs in Docker and generates zero-knowledge proofs for your transactions.

Install Docker

If you don’t have Docker installed:

macOS:

brew install --cask docker

Linux (Ubuntu/Debian):

sudo apt-get update
sudo apt-get install docker.io docker-compose
sudo usermod -aG docker $USER

Windows (WSL2):

  1. Download and install Docker Desktop for Windows.
  2. In Docker Desktop “Settings” >“General” ensure “Use the WSL 2 based engine” is checked.
  3. Go to “Resources” > WSL Integration and enable the toggle for your specific Linux distro (e.g., Ubuntu).

    Note: If you do not see a distro listed, open PowerShell and run wsl –install -d Ubuntu to create one.

Verify Docker

docker --version
docker compose version

Run the Proof Server

Start the proof server in your terminal:

docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v

This command occupies the terminal window while running.

Verification: The terminal displays logs indicating the server is running and listening at http://localhost:6300.

Tip: To use a local proof-server with Lace wallet, go to Settings → Midnight and select Local (http://localhost:6300).

For local development without relying on public testnets, the community has created midnight-local-network — a self-contained local Midnight node with wallet funding capabilities.

🎬 Watch the Video Explainer & Demo

Why Use a Local Network?

Public testnets can be:

  • Unavailable or undergoing maintenance
  • Rate-limited
  • Unstable for automated tests
  • Unsuitable for offline workflows

A local network gives you:

  • ✅ Fully isolated, predictable environment
  • ✅ No dependence on faucets
  • ✅ Perfect for CI/CD pipelines
  • ✅ Works offline

Quick Setup

# Clone the repository
git clone git@github.com:bricktowers/midnight-local-network.git
cd midnight-local-network

# Use Node 22+
nvm install 22
nvm use 22

# Install dependencies
yarn install

# Start the local network
docker compose up -d

Connect Lace Wallet to Local Network

  1. Open Lace Wallet Settings → Midnight
  2. Switch network to “Undeployed”
  3. Save and switch to the local network

Fund Your Wallet

The local network includes a funding script (no faucet needed!):

# Fund using your wallet's mnemonic (funds both shielded + unshielded)
yarn fund "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

# Or fund a specific shielded address
yarn fund mn_shield-addr_undeployed1q....

# Or fund a specific unshielded address
yarn fund mn_addr_undeployed1q....

Local Network Ports

ServicePort
Proof Server6300
Node9944
Indexer8088

Configure Your dApp for Local Network

If using CLI tooling instead of the dApp connector:

export class LocalNetworkConfig implements Config {
  indexer = "http://127.0.0.1:8088/api/v1/graphql";
  indexerWS = "ws://127.0.0.1:8088/api/v1/graphql/ws";
  node = "http://127.0.0.1:9944";
  proofServer = "http://127.0.0.1:6300";

  setNetworkId() {
    setNetworkId(NetworkId.Undeployed);
  }
}

Credits: bricktowers/midnight-local-network is a community contribution by Brick Towers.

VS Code Extension

The Compact VS Code extension provides syntax highlighting and code snippets.

Installation

  1. Download the VSIX package from the Compact VS Code extension release page
  2. In VS Code, go to Extensions → click ... menu → Install from VSIX
  3. Select the downloaded extension file

Windows Only: In VS Code, go to Extensions and install the “WSL” extension by Microsoft.

Launch it by typing on WSL Terminal:

code .

You can now add the compact extension to the WSL VS Code

Verification: You now see “Compact Language Support” in your installed VS Code extensions.

Features

  • Syntax highlighting for .compact files
  • Code snippets for common patterns

Project Structure

Create a new directory for your Compact projects:

mkdir ~/compact-projects
cd ~/compact-projects

A typical project structure looks like:

my-project/
├── contract/
│   └── src/
│       └── contract.compact
├── cli/
│   ├── src/
│   │   └── index.ts
│   └── package.json
└── README.md

Troubleshooting for Windows (WSL) Users

If you are following this guide on Windows using WSL2 (Ubuntu), you might encounter specific environment issues. Here are the common errors and their solutions.

1. Docker & WSL Setup

Q: Docker says “daemon not running” or “cannot connect to the Docker daemon” inside my terminal.

The Issue: You likely tried to install Docker using the Linux commands (apt-get install docker.io). This usually fails on WSL because it lacks a systemd service by default.

The Fix:

  1. Uninstall the Linux version: sudo apt-get remove docker docker-engine docker.io
  2. Install Docker Desktop for Windows on your host machine.
  3. In Docker Desktop Settings > Resources > WSL Integration, toggle the switch ON for your Ubuntu distro.

Q: Docker Desktop says “You don’t have any WSL 2 distros installed.”

The Fix: You might be running WSL 1 or have no distro installed.

  1. Open PowerShell and run: wsl --list --verbose
  2. If the version is 1, upgrade it: wsl --set-version Ubuntu 2
  3. If no distro is listed, install one: wsl --install -d Ubuntu

2. VS Code & File Path Issues

Q: error opening source file... no such file or directory

The Issue: You might have created the file in Windows (e.g., VS Code connected to C:) but are trying to run the command from the Linux root folder (~). Linux cannot automatically see files on your Windows Desktop unless you navigate to /mnt/c/....

The Fix: Connect VS Code directly to WSL:

  1. Install the “WSL” extension in VS Code.
  2. In your Ubuntu terminal, type: code .
  3. Create the counter.compact file inside that new window.

3. Compilation & Syntax Errors

Q: parse error: found "ledger" or unbound identifier Void

The Issue: You are likely using code examples from an older version of Compact, but you have installed a newer compiler (e.g., v0.30.0 or later).

The Fix: Update your syntax to match the modern standard.

  • Old: contract Counter { ledger x: ... }
  • New: export ledger x: ... (Top-level declarations)
  • Old: circuit increment(): Void
  • New: export circuit increment(): []

Q: expected right-hand side of = to have type Uint<32> (Overflow Protection)

The Issue: Compact is strict about math safety. x + 1 could technically overflow a 32-bit integer, so the compiler demands you explicitly handle that risk.

The Fix: Cast the result explicitly:

x = (x + 1) as Uint<32>;

Next Steps

Your environment is ready! Let’s write your first Compact contract.

Your First Contract

Let’s write a simple counter contract - the “Hello World” of Compact.

Creating the Contract File

Create a new file called counter.compact:

mkdir -p my-first-contract
cd my-first-contract
touch counter.compact

Writing the Contract

Open counter.compact and add the following code:

pragma language_version >= 0.16;

import CompactStandardLibrary;

// Our first ledger state - a counter
export ledger counter: Counter;

// A circuit to increment the counter
export circuit increment(): [] {
    counter.increment(1);
}

// A circuit to decrement the counter
export circuit decrement(): [] {
    counter.decrement(1);
}

// A circuit to read the current count
export circuit getCount(): Uint<64> {
    return counter.read();
}

// A circuit to check if counter is below a threshold
export circuit isLessThan(threshold: Uint<64>): Boolean {
    // disclose() is required because comparing a witness value (threshold)
    // with ledger state could reveal information about the witness
    return counter.lessThan(disclose(threshold));
}

// A circuit to reset the counter
export circuit reset(): [] {
    counter.resetToDefault();
}

Let’s break down each part:

Pragma Statement

pragma language_version >= 0.16;

This declares which Compact language versions your code is compatible with. Always include this at the top of your files.

Import Statement

import CompactStandardLibrary;

This imports the Compact standard library, which provides types like Counter, Map, Set, and utility functions.

Ledger Declaration

export ledger counter: Counter;

This declares a public (export) ledger variable named counter of type Counter. Ledger state is stored on the blockchain.

⚠️ CRITICAL: Each ledger field must be declared individually. There is NO block syntax like ledger { }.

Circuit Declaration

export ledger counter: Counter;

export circuit increment(): [] {
    counter.increment(1);
}

Circuits are the functions of Compact. Key points:

  • export makes it callable from outside the contract
  • circuit is the keyword (not function)
  • [] is the return type for circuits that return nothing (not void or Void)
  • The body contains the operations

The disclose() Function

Notice the isLessThan circuit uses disclose():

export ledger counter: Counter;

export circuit isLessThan(threshold: Uint<64>): Boolean {
    return counter.lessThan(disclose(threshold));
}

In Compact, parameters to exported circuits are witness values (private inputs). When you use a witness value in a way that could reveal information about it (like comparing with public ledger state), Compact requires you to explicitly acknowledge this potential disclosure using disclose().

This is a safety feature - Compact won’t let you accidentally leak private data. By calling disclose(), you’re telling the compiler “I understand this value may be revealed, and that’s okay.”

Compiling the Contract

Compile your contract using the compact compile command:

compact compile counter.compact managed/counter

⚠️ Note: The older compactc command is deprecated. Always use compact compile.

If successful, you’ll see a managed output directory structure created (verified with toolchain 0.31.1):

managed/counter/
├── contract/                 # TypeScript bindings
│   ├── index.js              # JavaScript implementation
│   ├── index.js.map          # Source map
│   └── index.d.ts            # TypeScript type definitions
├── zkir/                     # Circuit representations
│   ├── increment.zkir        # One .zkir + .bzkir per circuit
│   └── ...
├── keys/                     # Proving and verifying keys
│   ├── increment.prover      # One .prover + .verifier per circuit
│   ├── increment.verifier
│   └── ...
└── compiler/
    └── contract-info.json    # Compiler metadata

Understanding the Output

The compiler generates a managed directory containing:

  1. contract/ - TypeScript/JavaScript code to interact with the contract
    • Deploy the contract
    • Call circuit functions
    • Read ledger state
  2. zkir/ - Zero-knowledge intermediate representation of your circuits
  3. keys/ - Cryptographic keys for proving and verification
  4. compiler/ - Build metadata and artifacts

Common First-Time Errors

ErrorCauseFix
unbound identifier VoidUsing Void as return typeUse [] instead
parse error: found "ledger"Using ledger { } block syntaxDeclare each field separately
unbound identifier CounterMissing importAdd import CompactStandardLibrary;
potential witness-value disclosure must be declaredUsing witness value in ledger operationWrap the value with disclose()

Exercises

Complete the exercises in exercises/01_basics/ to practice:

  1. Writing pragma statements
  2. Importing the standard library
  3. Declaring ledger state
  4. Creating basic circuits

Next Steps

Congratulations! You’ve written and compiled your first Compact contract. In the next chapter, we’ll explore the basic concepts of the language in more detail.

Basic Concepts

This chapter covers the fundamental building blocks of Compact programs:

  • Program structure and organization
  • Pragma statements and imports
  • Comments and coding style

Understanding these basics will prepare you for diving into the type system and more advanced features.

Program Structure

Every Compact program follows a consistent structure. Understanding this structure helps you write well-organized contracts.

Basic Structure

A typical Compact file has these sections:

// 1. Pragma declaration (required, must be first)
pragma language_version >= 0.16;

// 2. Imports (must come after pragma)
import CompactStandardLibrary;

// 3. Type definitions (structs, enums)
export struct Person {
    name: Bytes<32>,
    age: Uint<8>
}

export enum Status { Active, Inactive }

// 4. Ledger state declarations
export ledger registry: Map<Bytes<32>, Person>;
export ledger count: Counter;

// 5. Witness declarations
witness getSecretKey(): Bytes<32>;

// 6. Circuits
export circuit register(name: Bytes<32>, age: Uint<8>): [] {
    // implementation
}

pure circuit validateAge(age: Uint<8>): Boolean {
    return age >= 18;
}

Note: The pragma must come first, followed by imports. Beyond that, the ordering of types, ledger, witnesses, and circuits is flexible—the order shown above is a recommended convention for readability, not a language requirement.

Modules and Imports

Compact supports modules for organizing code. You can import from modules using path syntax:

import CompactStandardLibrary;
import "mymodule/helpers";

Note: The path import is shown for illustration — it only compiles if mymodule/helpers.compact actually exists in your project.

For details on modules and the import mechanism, see the Modules chapter.

Naming Conventions

While Compact doesn’t enforce naming conventions, following consistent patterns improves readability. This book uses:

ElementConventionExample
CircuitscamelCasegetBalance, transferFunds
Types/StructsPascalCasePerson, PlayerStats
Ledger fieldscamelCasebalance, totalSupply
Enum variantslowercaseStatus.pending, State.active
ConstantscamelCasemaxValue, threshold

Note: These are conventions used throughout this book for consistency. Choose a style that works for your project and apply it consistently.

Visibility

The export keyword controls what’s accessible from outside:

// Public - accessible from TypeScript/externally
export ledger balance: Counter;
export circuit deposit(): [] { }

// Private - only accessible within the contract
ledger internal_state: Field;
circuit helper(): Field {
    return internal_state;
}

Exported circuits become entry points callable from TypeScript. Exported ledger fields are visible via the generated ledger() function.

Key Points

  1. Pragma first, imports second - These must come at the top in this order
  2. One declaration per line - No block syntax for ledger (e.g., no ledger { })
  3. Export consciously - Only expose what needs to be public
  4. Organize logically - Group related code together (recommended, not required)

Pragma and Imports

Pragma Statement

The pragma statement declares which Compact language versions your code supports.

Syntax

// pragma language_version >= 0.16 && <= 0.23;

This means:

  • Minimum version: 0.16
  • Maximum version: 0.23

Why Version Constraints?

Compact is evolving. New versions may:

  • Add new features
  • Change existing syntax
  • Deprecate old patterns

By specifying version bounds, you ensure:

  1. Your code compiles with compatible compilers
  2. Future breaking changes don’t silently break your contract
  3. Users know which compiler version to use

If the Compact compiler does not support the language versions specified, it will signal a compile-time error.

Version Constraint Options

// Exact version match (version number only)
pragma language_version 0.23;

// Minimum version (open-ended)
pragma language_version >= 0.16;

// Version range (recommended)
pragma language_version >= 0.16 && <= 0.23;

Tip: Your installed compiler implements exactly one language version — check it with compact compile --language-version (currently 0.23.0 with toolchain 0.31.1). A pragma whose range doesn’t include that version fails with a language version ... mismatch error, so keep the upper bound at or above your compiler’s language version.

Best Practices

Note: These are recommended conventions, not official rules from the Midnight documentation.

// ✅ Recommended - specific version range
pragma language_version >= 0.16 && <= 0.23;

// ✅ Exact version - for strict compatibility
pragma language_version 0.23;

// ⚠️ Caution - open-ended (may break with future versions)
pragma language_version >= 0.16;

Note: Compact doesn’t support == for version comparison. For an exact version match, simply write the version number without any comparison operator: pragma language_version 0.23;

Import Statement

The import statement brings external modules into scope.

Standard Library

Every Compact program should import the standard library:

import CompactStandardLibrary;

This provides ledger ADTs and useful helper types and functions.

Import Syntax

// Import entire module
import CompactStandardLibrary;

// Import from a path (only compiles if the file exists in your project)
import "path/to/module";

The import mechanism supports:

  • import M; — import a module
  • import "path/to/M"; — import from a path
  • Optional prefix and type parameters

Module Contents

After importing CompactStandardLibrary, you have access to:

CategoryItems
Ledger ADTsCounter, Map<K,V>, MerkleTree<N,T>
Optional TypesMaybe<T>, Either<L,R>, JubjubPoint, MerkleTreeDigest, etc.
Hash FunctionspersistentHash<T>(), persistentCommit<T>(), transientHash<T>(), transientCommit<T>()
Helperssome<T>(), none<T>(), left<L,R>(), right<L,R>(), pad()

Note: The standard library also includes EC functions, Merkle functions, and coin-management functions. See the Compact Standard Library Reference for the complete list.

Complete Template

Here’s a minimal complete Compact file (matches the official counter example pattern):

pragma language_version >= 0.16;

import CompactStandardLibrary;

export ledger value: Counter;

export circuit increment(): [] {
    value.increment(1);
}

Exercises

Practice pragma and import statements in exercises/01_basics/02_pragma_imports/.

Comments and Style

Good code style makes your contracts readable and maintainable.

Note: The Compact language reference does not specify a formal style guide. The conventions in this chapter are recommended practices for this tutorial, based on patterns seen in official examples.

Comments

Compact supports single-line comments using //:

// This is a comment
export ledger counter: Counter; // Inline comment

// Multi-line comments use multiple single-line comments
// Each line needs its own // prefix
// Like this

Documentation Comments

Use comments to document your code:

// Transfers tokens from sender to recipient
// Parameters:
//   - recipient: The address receiving tokens
//   - amount: Number of tokens to transfer
// Returns: [] (modifies ledger state)
export circuit transfer(recipient: Bytes<32>, amount: Uint<64>): [] {
    // Verify sender has sufficient balance
    // ... implementation
}

What to Comment

export ledger counter: Counter;

witness secretKey(): Bytes<32>;

// ✅ Good - explains WHY
// Hash the secret key so a public identifier can be shared
// without revealing the key itself
export circuit publicKey(): Bytes<32> {
    return disclose(persistentHash<Bytes<32>>(secretKey()));
}

// ❌ Avoid - just restates WHAT (the code is clear)
export circuit bump(): [] {
    // Increment counter by 1
    counter.increment(1);
}

Code Formatting

The following are recommended conventions for this tutorial, not official Compact requirements.

Indentation

We recommend 4 spaces for indentation:

export circuit process(value: Uint<64>): Uint<64> {
    if (value > 100) {
        return (value * 2) as Uint<64>;
    } else {
        return value;
    }
}

Line Length

Keep lines readable—we suggest under 100 characters:

// ✅ Good - readable
export circuit transfer(
    recipient: Bytes<32>,
    amount: Uint<64>
): [] {
    // ...
}

// ❌ Hard to read
export circuit transfer(recipient: Bytes<32>, amount: Uint<64>, memo: Bytes<64>, timestamp: Uint<64>): [] { }

Spacing

Use consistent spacing around operators:

// ✅ Good
const result = a + b * c;
if (value >= threshold && isActive) { }

// ❌ Inconsistent
const result=a+b*c;
if(value>=threshold&&isActive){}

Blank Lines

Use blank lines to separate logical sections (this pattern is consistent with official examples):

pragma language_version >= 0.16;

import CompactStandardLibrary;

// Types
export struct Token {
    symbol: Bytes<8>,
    amount: Uint<64>
}

// State
export ledger balance: Map<Bytes<32>, Uint<64>>;
export ledger totalSupply: Counter;

// Circuits
export circuit mint(to: Bytes<32>, amount: Uint<64>): [] {
    balance.insert(disclose(to), disclose(amount));
    totalSupply.increment(disclose(amount) as Uint<16>);
}

export circuit burn(owner: Bytes<32>, amount: Uint<64>): [] {
    balance.remove(disclose(owner));
    totalSupply.decrement(disclose(amount) as Uint<16>);
}

Style Summary

These are project conventions for learn-compact, not official Compact rules:

ElementConvention
Comments// single-line
Indentation4 spaces
Line length< 100 characters
Blank linesBetween sections
OperatorsSpaces around them

Next Steps

You now understand the basics of Compact programs. Let’s dive into the type system!

Primitive Types

This chapter covers Compact’s primitive types - the building blocks for all data in your contracts.

Overview

Compact provides several primitive types:

TypeDescriptionExample
BooleanTrue/false valuestrue, false
FieldElements of the proving system’s scalar fieldUsed in ZK proof arithmetic
Uint<n>Sized unsigned integers (up to n bits, max width 248)Uint<64>, Uint<128>
Uint<0..n>Bounded unsigned integers from 0 up to (not including) nUint<0..101> for percentages 0-100
Bytes<n>Fixed-length byte arraysBytes<32> for hashes or identifiers
Opaque<s>Opaque values tagged by a string literal ("string", "Uint8Array")Opaque<"string">

Note: Uint<n> and Uint<0..n> are the same type family. Uint<n> is exactly equivalent to Uint<0..2^n> (exclusive upper bound).

Each type serves a specific purpose in zero-knowledge circuits. Let’s explore them in detail.

Boolean and Field Types

Boolean

The Boolean type is a primitive type with only two values: true and false. Value bindings like these live inside circuits:

export pure circuit flags(): Boolean {
    const isActive: Boolean = true;
    const isComplete: Boolean = false;
    return isActive && !isComplete;
}

Boolean Operations

Compact supports standard boolean operators:

export pure circuit logic(a: Boolean, b: Boolean): Boolean {
    // Logical AND
    const both = a && b;

    // Logical OR
    const either = a || b;

    // Logical NOT
    const opposite = !a;

    return both || either || opposite;
}

Note: The boolean operators &&, ||, ! follow standard semantics. The && and || operators use short-circuit evaluation.

Boolean in Conditionals

Booleans are used in if statements and conditional expressions:

export pure circuit choose(
    condition: Boolean,
    valueIfTrue: Field,
    valueIfFalse: Field
): Field {
    if (condition) {
        // do something
    }

    // Conditional expression: e0 ? e1 : e2 where e0 is Boolean
    const result = condition ? valueIfTrue : valueIfFalse;
    return result;
}

Field

The Field type is “the type of elements in the scalar prime field of the zero-knowledge proving system.”

export pure circuit fieldExample(): Field {
    const x: Field = 42;
    return x;
}

Converting to Field

Casting from Uint<0..m> to Field is a static cast (allowed, no runtime cost):

export pure circuit toField(): Field {
    const amount: Uint<64> = 100;
    const amountAsField = amount as Field;
    return amountAsField;
}

When to Use Field vs Uint

Note: The following is usage guidance for this tutorial, not official documentation.

Use CaseRecommended Type
Amounts, countsUint<64>
Cryptographic operationsField
Hashing inputsOften Field or Bytes
General arithmeticUint<n>

Exercises

Complete the exercises in exercises/02_types/01_boolean/ and exercises/02_types/04_field/.

Unsigned Integers

Compact defines one unsigned integer type constructor, Uint, with two equivalent forms for expressing the same family of types.

Two Forms, One Type Family

1. Sized Form: Uint<n>

The sized form specifies how many bits the integer can use:

export ledger value: Uint<64>;  // 64-bit unsigned integer
TypeBitsRangeUse Case (guidance)
Uint<8>80 to 255Small counters, enum-like values
Uint<16>160 to 65,535Medium values
Uint<32>320 to ~4 billionTimestamps, IDs
Uint<64>640 to ~18 quintillionBalances, amounts
Uint<128>128Very largeHigh-precision values
Uint<248>248Maximum widthLargest supported sized integer

Note: The “Use Case” column is guidance—the docs define the types and semantics, but don’t prescribe these specific patterns.

⚠️ Maximum width: Uint<248> is the largest valid sized integer — Uint<249> and above (including Uint<256>) fail to compile (verified with toolchain 0.31.1).

2. Bounded Form: Uint<0..n>

The bounded form specifies the upper bound directly. ⚠️ The upper bound is exclusive — Uint<0..n> holds values 0 to n-1 (verified with toolchain 0.31.1: a literal 100 does not fit Uint<0..100>, whose largest value is 99):

circuit f(x: Uint<0..100>): [] {
    // x is guaranteed to be between 0 and 99 (upper bound exclusive)
}

Note: The lower bound must currently be 0.

They’re the Same Type!

Uint<n> is exactly equivalent to Uint<0..2^n> (exclusive upper bound). For example, Uint<8> is the same type as Uint<0..256>, not Uint<0..255> — verified with toolchain 0.31.1, where a Uint<8> value assigns to Uint<0..256> but is a type mismatch against Uint<0..255>:

export pure circuit sameType(x: Uint<8>): Uint<0..256> {
    // Uint<8> and Uint<0..256> are the SAME type (upper bound exclusive)
    return x;
}

The sized form is just a convenience notation—any Uint<n> can be rewritten as Uint<0..2^n>.

Declaration

export pure circuit declarations(): Uint<64> {
    const age: Uint<8> = 25;
    const balance: Uint<64> = 1000000;
    const percentage: Uint<0..101> = 75;  // Bounded: holds 0-100 (upper bound exclusive)
    return balance;
}

Arithmetic Operations

Compact supports three arithmetic operators for unsigned integers: +, -, *.

export pure circuit arithmetic(): Field {
    const a: Uint<64> = 100;
    const b: Uint<64> = 50;

    // Addition
    const sum = a + b;  // 150

    // Subtraction
    const diff = a - b;  // 50

    // Multiplication
    const product = a * b;  // 5000

    return product as Field;
}

Important Behavior

According to the official docs:

  • Addition/Multiplication: Cannot overflow—the result type automatically widens to accommodate the result
  • Subtraction: Causes a runtime error if the result would be negative (right operand > left operand)
export pure circuit subtract(a: Uint<64>, b: Uint<64>): Uint<64> {
    // ⚠️ Runtime error if b > a!
    const diff = (a - b) as Uint<64>;  // Fails if b is greater than a
    return diff;
}

⚠️ Note: Compact has no division operator — / is a parse error (verified with toolchain 0.31.1; the docs list only +, -, * as binary arithmetic operators). If you need division, compute it off-chain and pass the result in as a witness, then constrain it in the circuit.

Comparison Operations

Compact provides six relational operators:

OperatorName
==equals
!=not equals
<less than
>greater than
<=less than or equals
>=greater than or equals

Source: Relational expressions

export pure circuit comparisons(): Boolean {
    const a: Uint<32> = 100;
    const b: Uint<32> = 50;

    const eq = a == b;   // false (equal)
    const ne = a != b;   // true  (not equal)
    const gt = a > b;    // true  (greater than)
    const ge = a >= b;   // true  (greater or equal)
    const lt = a < b;    // false (less than)
    const le = a <= b;   // false (less or equal)

    return ne && gt && ge;
}

Note: The <, >, <=, >= operators require both operands to have unsigned integer types.

When to Use Which Form? (guidance)

This table is usage advice, not from the docs. The docs only define the types and their semantics.

Use CaseRecommended FormExample
Standard storage (balances, IDs)Sized Uint<n>Uint<64>
Percentage or constrained valueBounded Uint<0..n>Uint<0..101>
Matching specific bit-widthSized Uint<n>Uint<128>
Domain-specific limitsBounded Uint<0..n>Uint<0..1000>

Type Casting

Convert between integer sizes:

export pure circuit widen(): Uint<64> {
    const small: Uint<32> = 100;
    const big: Uint<64> = small as Uint<64>;
    return big;
}

Exercises

Complete exercises/02_types/02_integers/.

Bytes and Opaque Types

Bytes<n>

Bytes<n> represents a fixed-length byte array of exactly n bytes. According to the docs, Bytes types are used in the Compact standard library for hashing, and string literals also have a Bytes type (where n is the number of bytes in the UTF-8 encoding).

Common Uses (guidance)

export pure circuit shapes(
    hash: Bytes<32>,   // Hashes (32 bytes = 256 bits)
    id: Bytes<8>,      // Short identifiers
    data: Bytes<256>   // Longer data
): Bytes<32> {
    return hash;
}

Note: The “hashes / identifiers” wording above is convention, not mandated by the docs.

Creating Bytes

In practice, Bytes values often come from:

  • Witnesses (private input)
  • Hashing functions
  • String literals (automatically typed as Bytes<n>)
  • Parameters passed to circuits
witness getHash(): Bytes<32>;

export circuit example(): [] {
    const h = getHash();
    // use h...
}

Bytes Operations

Bytes values support equality comparison with == and != (verified with toolchain 0.31.1):

export pure circuit compareBytes(hash1: Bytes<32>, hash2: Bytes<32>): Boolean {
    const same = hash1 == hash2;
    const different = hash1 != hash2;
    return same;
}

Hashing to Bytes

The standard library’s persistentHash<T> hashes a value of type T and returns Bytes<32> — no cast needed (verified with toolchain 0.31.1):

export pure circuit hashIt(someValue: Bytes<32>): Bytes<32> {
    const hash: Bytes<32> = persistentHash<Bytes<32>>(someValue);
    return hash;
}

Opaque Types

Opaque<s> represents opaque values tagged by a string literal s. Opaque values can be manipulated in witnesses but they are opaque to circuits—they are represented in circuits as their hash.

Note: The only allowed tags are currently "string" and "Uint8Array".

Opaque<“string”>

For string data:

// Declare a field that holds string data
export ledger message: Opaque<"string">;

Opaque<“Uint8Array”>

For arbitrary binary data:

export ledger binaryData: Opaque<"Uint8Array">;

Important Limitations

⚠️ Opaque values are opaque to circuits!

They are represented in circuits as their hash, so you cannot inspect their actual content in circuit logic.

Note: Opaque values are only opaque within Compact. In your DApp’s TypeScript code they are just string or Uint8Array, and on-chain they are stored as bytes/UTF-8 (not encrypted).

Source: Opaque data

// Opaque values can be stored and compared by hash
// (assuming a Map with Opaque value type)
export ledger messages: Map<Bytes<32>, Opaque<"string">>;

export circuit storeMessage(key: Bytes<32>, opaqueValue: Opaque<"string">): [] {
    // Parameters are private inputs — writing them to public ledger
    // state requires an explicit disclose()
    messages.insert(disclose(key), disclose(opaqueValue));
}

When to Use What (guidance)

TypeUse Case
Bytes<32>Hashes, fixed-size identifiers
Bytes<n>Fixed-size binary data you compute on
Opaque<"string">Human-readable text (opaque to circuits)
Opaque<"Uint8Array">Binary blobs (opaque to circuits)

Sources: Primitive types; Opaque data

Exercises

Complete exercises/02_types/03_bytes/.

Type Casting

Type casting converts values from one type to another using the syntax e as T.

Note: TypeScript-style casts of the form <T>e are not supported in Compact.

Basic Casting Syntax

const result = value as TargetType;

Cast Types

The Compact docs define three kinds of casts:

  • static: Only changes the static type, no runtime effect
  • conversion: Always succeeds but converts between representations
  • checked: Checked at runtime and can fail

Official Casting Table

Based on the Compact language reference:

FROM → TOFieldUint<0..n>BooleanBytes<n>
Fieldstaticchecked12
Uint<0..m>static3conversion7
enum typeconversionconversionnono
Booleanconversion4—no
Bytes<m>59no6

Note: Numbers in the table refer to the notes below. no means the cast is not allowed.

Important Notes

  1. Field → Boolean: Conversion. 0 converts to false, all other values to true.

  2. Field → Bytes<n>: Conversion to little-endian bytes. Runtime error if value doesn’t fit in n bytes.

  3. Uint<0..m> → Uint<0..n>: Static if m ≤ n, otherwise checked (fails at runtime if value > n).

  4. Boolean → Uint<0..n>: Conversion. false → 0, true → 1. If n is 0, checked (fails on true).

  5. Bytes<m> → Field: Conversion from little-endian bytes. Runtime error if result exceeds max Field value.

  6. Bytes<m> → Bytes<n>: Only allowed if m == n (static cast).

  7. Uint<0..m> → Bytes<n>: Works as a direct conversion to little-endian bytes (verified with toolchain 0.31.1; older docs marked this as not allowed).

  8. Integer → enum: A checked cast — compiles and runs, but fails at runtime if the value exceeds the last variant index (e.g., 5 is greater than maximum enum value 2).

  9. Bytes<m> → Uint<0..n>: Works as a little-endian conversion (verified with toolchain 0.31.1; older docs marked this as not allowed).

⚠️ Important: Casts marked with notes 2 and 5 can fail at runtime even though they’re conversions.

Common Conversions

Integer Size Conversion

export pure circuit resize(): Uint<32> {
    const small: Uint<32> = 100;
    const big: Uint<64> = small as Uint<64>;  // ✅ Static cast (widening)

    const large: Uint<64> = 100;
    const smaller: Uint<32> = large as Uint<32>;  // ⚠️ Checked (may fail at runtime)
    return smaller;
}

Integer to Field

export pure circuit intToField(): Field {
    const amount: Uint<64> = 1000;
    const amountField: Field = amount as Field;  // Static cast
    return amountField;
}

Field to Bytes

export pure circuit fieldToBytes(): Bytes<32> {
    const fieldValue: Field = 42;
    const bytes: Bytes<32> = fieldValue as Bytes<32>;  // Conversion (little-endian)
    // ⚠️ Runtime error if fieldValue doesn't fit in 32 bytes!
    return bytes;
}

Uint to Bytes

Uint casts directly to Bytes<n> as a little-endian conversion (verified with toolchain 0.31.1 — e.g., 42 becomes 0x2a00…00). Going through Field first also works but is not required:

export pure circuit uintToBytes(): Bytes<32> {
    const amount: Uint<64> = 100;

    // ✅ Direct cast (little-endian)
    const bytes: Bytes<32> = amount as Bytes<32>;

    // ✅ Equivalent two-step cast through Field
    const bytes2: Bytes<32> = (amount as Field) as Bytes<32>;

    return bytes;
}

Boolean Conversions

export pure circuit boolCasts(): Field {
    const flag: Boolean = true;

    // Boolean to Uint (conversion - note 4)
    const flagInt: Uint<8> = flag as Uint<8>;  // true → 1, false → 0

    // Or use conditional for clarity
    const value: Uint<8> = flag ? 1 : 0;

    // Boolean to Field (conversion): true → 1, false → 0
    const flagField: Field = flag as Field;

    return flagField;
}

Note: Boolean → Field is a conversion cast per the official table (verified with toolchain 0.31.1): false → 0, true → 1. Going through Uint first also works, but is not required.

Enum to Field

export enum Choice { a, b, c }

export pure circuit choiceIndex(): Field {
    const choice: Choice = Choice.b;
    const index: Field = choice as Field;  // 1 (conversion)
    return index;
}

Best Practices

  1. Understand cast types - Know whether a cast is static, conversion, or checked
  2. Check ranges - Checked casts can fail at runtime
  3. Use Field as intermediate - When direct cast isn’t allowed
  4. Handle potential failures - Checked casts may raise runtime exceptions

Exercises

Practice type casting in the type exercises.

Composite Types

While Compact doesn’t use the term “compound types” explicitly, it provides several composite (non-primitive) types built from other types. This chapter covers the built-in composite types:

  • Tuples - Fixed-length, heterogeneous collections
  • Vectors - Shorthand for homogeneous tuples (Vector<n, T> = [T, T, ..., T])
  • Bytes - Fixed-length byte arrays (Bytes<n>)
  • Standard Library Structs - Maybe<T>, Either<A, B>, and others

Note: User-defined composite types (struct and enum) are covered in Chapter 5: User-Defined Types.

Overview

TypeDescriptionExample
[T1, T2, ...]Tuple - heterogeneous, fixed-length[Uint<64>, Boolean]
[]Unit type (empty tuple)[]
Vector<n, T>Same as [T, T, ..., T] (n times)Vector<10, Uint<8>>
Bytes<n>Fixed-length byte arrayBytes<32>
Maybe<T>Optional value (std library)Maybe<Uint<64>>
Either<A, B>One of two types (std library)Either<Uint<64>, Boolean>

Key Concepts

Tuples and Vectors

  • Tuples can hold different types: [Uint<64>, Boolean, Bytes<32>]
  • Vectors are shorthand for tuples of the same type: Vector<3, Field> is exactly [Field, Field, Field]
  • Values are created with [e1, e2, ...] syntax

Source: Primitive types; Tuple creation

Bytes

Bytes<n> is a fixed-length byte array heavily used throughout the standard library for:

  • Hashes and addresses
  • Merkle trees
  • Contract addresses (ContractAddress { bytes: Bytes<32>; })

Source: Primitive types

Standard Library Composite Structs

The standard library defines reusable composite types (shapes shown for reference — they’re already defined, so don’t redeclare them):

// Optional value
struct Maybe<T> { is_some: Boolean; value: T; }

// Disjoint union
struct Either<A, B> { is_left: Boolean; left: A; right: B; }

// Merkle tree proof path
struct MerkleTreePath<#n, T> { leaf: T; path: Vector<n, MerkleTreePathEntry>; }

// Contract address wrapper
struct ContractAddress { bytes: Bytes<32>; }

To use these, import the standard library:

import CompactStandardLibrary;

Source: Standard library structs

Tuples and Vectors

Tuples

A tuple is a fixed-length, heterogeneous sequence of values.

Source: Primitive types

Syntax

export pure circuit tupleSyntax(hash: Bytes<32>): [Uint<64>, Boolean] {
    // Tuple type: [T1, T2, ...]
    const pair: [Uint<64>, Boolean] = [100, true];
    const triple: [Field, Uint<8>, Bytes<32>] = [42, 255, hash];
    return pair;
}

Tuple values are created with [e1, e2, ...] syntax.

Source: Tuple creation

The Unit Type []

The empty tuple [] is called the unit type. It’s commonly used as the return type for circuits that don’t return a value:

export circuit doSomething(): [] {
    // perform operations
}

Source: Writing a contract

Tuple Element Access

Access tuple elements using bracket notation with a numeric literal:

export pure circuit access(): Uint<64> {
    const pair: [Uint<64>, Boolean] = [100, true];
    const first = pair[0];   // 100
    const second = pair[1];  // true
    return first;
}

Note: The index must be a numeric literal known at compile time.

Source: Element and member access expressions

Default Values

The default value of a tuple is a tuple where each element has its type’s default value:

  • [Uint<64>, Boolean] defaults to [0, false]

Source: Default values


Vectors

Vector<n, T> is a shorthand for a tuple of n elements all of type T. It’s exactly the same type as [T, T, ..., T] (n times).

Source: Primitive types

Syntax

export pure circuit vectors(): Vector<5, Uint<8>> {
    // Vector<n, T> is shorthand for [T, T, ..., T]
    const nums: Vector<5, Uint<8>> = [1, 2, 3, 4, 5];
    const flags: Vector<3, Boolean> = [true, false, true];

    // These are equivalent types:
    // Vector<3, Field> === [Field, Field, Field]
    return nums;
}

Vector Element Access

Same as tuples—use bracket notation with a numeric literal:

export pure circuit pick(): Uint<8> {
    const nums: Vector<5, Uint<8>> = [1, 2, 3, 4, 5];
    const third = nums[2];  // 3
    return third;
}

Source: Element and member access expressions

Vectors and Tuples Subtyping

The relationship between vectors and tuples follows these rules:

  • A tuple type [T, ...] has a vector type Vector<n, S> when a least upper bound S exists for all element types
  • When a tuple has a vector type, the tuple is a subtype of that vector type
  • A vector type Vector<n, T> is a subtype of a tuple type [S, ...] of length n if T is a subtype of each S

This means a vector can be passed where a compatible tuple is expected:

pure circuit processPair(p: [Uint<8>, Uint<8>]): Uint<16> {
    return ((p[0] as Uint<16>) + (p[1] as Uint<16>)) as Uint<16>;
}

export circuit example(): Uint<16> {
    const v: Vector<2, Uint<8>> = [10, 20];
    return processPair(v);  // Vector passed as tuple
}

Source: Subtyping and least upper bounds

TypeScript Representation

Both tuples and vectors are represented as TypeScript tuples or arrays:

// Compact: [Uint<64>, Boolean]
// TypeScript: [bigint, boolean]

// Compact: Vector<5, Uint<8>>
// TypeScript: bigint[]

Source: Representations in TypeScript

Exercises

Complete exercises/04_compound/01_tuples_vectors/.

Maybe Type

The Maybe<T> type represents an optional value—either a value of type T exists, or it doesn’t.

Structure Definition

The standard library defines Maybe<T> with these fields (note the snake_case is_some):

struct Maybe<T> {
    is_some: Boolean;
    value: T;
}

Source: Standard library structs. Field names verified against compiler 0.31.1 — isSome does not exist.

Creating Maybe Values

Use the some() and none() constructor functions:

export pure circuit makeMaybes(): Maybe<Uint<64>> {
    // Create a Maybe with a value
    const hasValue: Maybe<Uint<64>> = some<Uint<64>>(42);

    // Create an empty Maybe (must specify type)
    const noValue: Maybe<Uint<64>> = none<Uint<64>>();

    return hasValue;
}

Checking for Values

Always check is_some before accessing value:

export circuit processOptional(opt: Maybe<Uint<64>>): Uint<64> {
    if (opt.is_some) {
        return opt.value;
    }
    return 0;  // Default value
}

⚠️ Important: If is_some is false, the value field contains a default value (0 for numeric types). Accessing it without checking is_some won’t cause an error but may lead to incorrect logic.

Common Use Cases

Optional Parameters

Use Maybe for optional circuit parameters:

export circuit transfer(
    sender: Bytes<32>,
    to: Bytes<32>,
    amount: Uint<64>,
    memo: Maybe<Bytes<64>>
): [] {
    // Transfer logic...
    if (memo.is_some) {
        // Process the memo
    }
}

Note: from is a reserved keyword in Compact and cannot be used as a parameter name — hence sender above.

Optional Return Values

Use Maybe when a circuit might not have a meaningful result:

export circuit findValue(id: Uint<32>): Maybe<Uint<64>> {
    // Return some(value) if found, none<Uint<64>>() if not
    if (id == 0) {
        return none<Uint<64>>();
    }
    return some<Uint<64>>(((id as Uint<64>) * 100) as Uint<64>);
}

Note: Multiplication widens the result type, so the product must be cast back to Uint<64> before passing it to some<Uint<64>>().

Default Values

The default value of Maybe<T> is equivalent to none<T>():

  • is_some = false
  • value = default value of T

Source: Default values

TypeScript Representation

In TypeScript, Maybe<T> is represented as (verified against compiler 0.31.1 generated types):

interface Maybe<T> {
  is_some: boolean;
  value: T; // T's TypeScript equivalent
}

Example:

// Compact: Maybe<Uint<64>>
// TypeScript: { is_some: boolean; value: bigint }

Source: Representations in TypeScript

Exercises

Complete exercises/04_compound/02_maybe/.

Either Type

The Either<A, B> type represents a value that can be one of two possible types—either type A (left) or type B (right).

Structure Definition

The standard library defines Either<A, B> with these fields (note the snake_case is_left):

struct Either<A, B> {
    is_left: Boolean;
    left: A;
    right: B;
}

Source: Standard library structs. Field names verified against compiler 0.31.1 — isLeft does not exist.

Creating Either Values

Use the left() and right() constructor functions with explicit type parameters:

export pure circuit makeEithers(): Either<Uint<64>, Boolean> {
    // Create a left value (type A) - must specify both types
    const leftVal: Either<Uint<64>, Boolean> = left<Uint<64>, Boolean>(42);

    // Create a right value (type B) - must specify both types
    const rightVal: Either<Uint<64>, Boolean> = right<Uint<64>, Boolean>(true);

    return leftVal;
}

Checking Which Value

Check is_left to determine which value is present:

export circuit processEither(e: Either<Uint<64>, Boolean>): Uint<64> {
    if (e.is_left) {
        return e.left;
    }
    if (e.right) {
        return 1;
    }
    return 0;
}

⚠️ Important: Like Maybe, both fields contain values, but only one is meaningful based on is_left. The other contains a default value.

Common Use Cases

Conditional Results

Use Either to return one of two different result types based on a condition:

// Left = error code, Right = success value
export pure circuit validateAmount(
    amount: Uint<64>
): Either<Uint<8>, Uint<64>> {
    if (amount == 0) {
        return left<Uint<8>, Uint<64>>(1);  // Error code 1: zero amount
    }
    return right<Uint<8>, Uint<64>>(amount);
}

Type-Safe Unions

Model data that can be one of two types:

// A value can be either a small number or a large number
struct NumberValue {
    value: Either<Uint<8>, Uint<64>>;  // Left = small, Right = large
}

export pure circuit createSmall(n: Uint<8>): NumberValue {
    return NumberValue { value: left<Uint<8>, Uint<64>>(n) };
}

export pure circuit createLarge(n: Uint<64>): NumberValue {
    return NumberValue { value: right<Uint<8>, Uint<64>>(n) };
}

Binary Choice

Represent a choice between two options:

// Left = option A chosen, Right = option B chosen
export pure circuit chooseOption(
    selectA: Boolean,
    valueA: Uint<64>,
    valueB: Uint<64>
): Either<Uint<64>, Uint<64>> {
    if (selectA) {
        return left<Uint<64>, Uint<64>>(valueA);
    }
    return right<Uint<64>, Uint<64>>(valueB);
}

Default Values

The default value of Either<A, B> is:

  • is_left = false
  • left = default value of A
  • right = default value of B

Source: Default values

TypeScript Representation

In TypeScript, Either<A, B> is represented as (verified against compiler 0.31.1 generated types):

interface Either<A, B> {
  is_left: boolean;
  left: A; // A's TypeScript equivalent
  right: B; // B's TypeScript equivalent
}

Example:

// Compact: Either<Uint<64>, Boolean>
// TypeScript: { is_left: boolean; left: bigint; right: boolean }

Source: Representations in TypeScript

Either vs Maybe

AspectMaybe<T>Either<A, B>
PurposeOptional valueOne of two types
Indicatoris_some: Booleanis_left: Boolean
Value fields1 (value: T)2 (left: A, right: B)

Source: Standard library structs

Exercises

Complete exercises/04_compound/03_either/.

User-Defined Types

Beyond built-in types, Compact allows you to define custom types to model your domain. This chapter covers the ways to create user-defined types:

  • Structs - Custom record types with named fields
  • Enums - Named enumeration types with distinct variants
  • Generic Types - Parameterized types for reusable abstractions

Overview

DeclarationPurposeExample
struct Name { ... }Record with named fieldsstruct Point { x: Field, y: Field }
enum Name { ... }Enumeration with variantsenum Status { pending, approved }

Key Concepts

Structs

Structs group related data into a single type with named fields:

struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

Access fields with dot notation: player.score

Source: User-defined types

Enums

Enums define a type with a fixed set of named values:

enum GameState { waiting, playing, finished }

Access variants with dot notation: GameState.waiting

⚠️ Important: Use dot notation (GameState.waiting), not Rust-style GameState::waiting.

Source: User-defined types

Exporting Types

To use custom types from TypeScript, export them:

export struct Player { ... }
export enum GameState { ... }

Source: Top-level exports

When to Use Each

Use CaseRecommended Type
Group related fieldsstruct
Finite set of statesenum
Reusable containerGeneric struct

What’s Next

  • Structs - Creating and using struct types
  • Enums - Defining enumeration types
  • Generic Types - Parameterized type definitions

Structs

Structs (structures) let you create custom composite types with named fields. They’re ideal for grouping related data together.

Source: User-defined types

Declaring a Struct

Use the struct keyword followed by the type name and field definitions:

struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

Fields are separated by commas (or semicolons). A trailing separator is allowed but not required.

Source: User-defined types

Creating Struct Values

Create a struct value using the struct name followed by field values in braces:

struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

export pure circuit createPlayers(ownerKey: Bytes<32>): Player {
    // Named fields (recommended for clarity)
    const player = Player {
        address: ownerKey,
        score: 0,
        isActive: true
    };

    // Positional fields (order must match declaration)
    const player2 = Player { ownerKey, 0, true };

    // Mixed: positional first, then named
    const player3 = Player { ownerKey, 0, isActive: true };

    return player;
}

Source: Structure creation

Accessing Fields

Use dot notation to access struct fields:

struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

export pure circuit readFields(ownerKey: Bytes<32>): Boolean {
    const player = Player { address: ownerKey, score: 100, isActive: true };

    const addr = player.address;      // Bytes<32>
    const points = player.score;      // Uint<64>
    const active = player.isActive;   // Boolean

    return active;
}

Source: Element and member access expressions

Default Values

The default value of a struct has all fields set to their type’s default:

struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

export pure circuit emptyPlayer(): Player {
    const empty = default<Player>;
    // Equivalent to: Player { address: default<Bytes<32>>, score: 0, isActive: false }
    return empty;
}

Source: Default values

Nested Structs

Structs can contain other structs. Use chained dot notation to access nested fields:

struct Coordinate {
    x: Field,
    y: Field
}

struct Rectangle {
    topLeft: Coordinate,
    bottomRight: Coordinate
}

export pure circuit area(rect: Rectangle): Field {
    const width = rect.bottomRight.x - rect.topLeft.x;
    const height = rect.bottomRight.y - rect.topLeft.y;
    return width * height;
}

Source: User-defined types

Structs in Maps

Structs can be used as values in Map ledger types:

struct UserData {
    balance: Uint<64>,
    lastAction: Uint<64>,
    isVerified: Boolean
}

export ledger users: Map<Bytes<32>, UserData>;

Source: Nested state types in Map

Exporting Structs

To use a struct from TypeScript, export it:

export struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

Source: Top-level exports

TypeScript Representation

Exported structs map to TypeScript objects with the same field names:

// Compact
export struct Player {
    address: Bytes<32>,
    score: Uint<64>,
    isActive: Boolean
}

// TypeScript representation
interface Player {
    address: Uint8Array;  // Bytes<32>
    score: bigint;        // Uint<64>
    isActive: boolean;    // Boolean
}

Source: Representations in TypeScript

Recursive Structs Are Not Allowed

Structs cannot contain themselves directly or indirectly:

// ❌ ERROR: Recursive struct
struct Node {
    value: Field,
    next: Node  // Not allowed!
}

// ❌ ERROR: Mutually recursive
struct Even {
    predecessor: Odd
}

struct Odd {
    predecessor: Even  // Creates cycle - not allowed!
}

Source: User-defined types

Complete Example

pragma language_version >= 0.16;

// Define structs
struct PlayerStats {
    wins: Uint<32>,
    losses: Uint<32>,
    draws: Uint<32>
}

struct Player {
    address: Bytes<32>,
    name: Bytes<32>,
    stats: PlayerStats
}

// Pure circuit to create a new player
export pure circuit createPlayer(
    address: Bytes<32>,
    name: Bytes<32>
): Player {
    return Player {
        address: address,
        name: name,
        stats: PlayerStats { wins: 0, losses: 0, draws: 0 }
    };
}

// Pure circuit to update stats after a win
export pure circuit recordWin(player: Player): Player {
    const newStats = PlayerStats {
        wins: (player.stats.wins + 1) as Uint<32>,
        losses: player.stats.losses,
        draws: player.stats.draws
    };
    return Player {
        address: player.address,
        name: player.name,
        stats: newStats
    };
}

Exercises

Complete exercises/05_user_types/01_structs/.

Enums

Enums (enumerations) define a type with a fixed set of named values. They’re perfect for representing states, choices, or categories.

Source: User-defined types

Declaring an Enum

Use the enum keyword followed by the type name and variant names:

enum GameState { waiting, playing, finished }

Variants are separated by commas. A trailing comma is allowed but not required.

enum Direction {
    north,
    east,
    south,
    west,
}

Accessing Enum Variants

Use dot notation to access enum variants:

enum GameState { waiting, playing, finished }
enum Direction { north, east, south, west }

export pure circuit variants(): GameState {
    const state = GameState.waiting;
    const dir = Direction.north;
    return state;
}

Source: User-defined types

Note: Use dot notation to access enum variants: GameState.waiting

Comparing Enums

Compare enum values with == and !=:

enum Status { pending, approved, rejected }

export pure circuit isApproved(status: Status): Boolean {
    return status == Status.approved;
}

export pure circuit notRejected(status: Status): Boolean {
    return status != Status.rejected;
}

Source: Writing a contract

Enums in Control Flow

Enums work naturally in conditionals:

enum GameState { waiting, playing, finished }

export pure circuit canJoin(state: GameState): Boolean {
    if (state == GameState.waiting) {
        return true;
    }
    return false;
}

export pure circuit getStateMessage(state: GameState): Uint<8> {
    if (state == GameState.waiting) {
        return 0;  // "Waiting for players"
    }
    if (state == GameState.playing) {
        return 1;  // "Game in progress"
    }
    return 2;  // "Game finished"
}

Default Values

The default value of an enum is its first variant:

enum Status { pending, approved, rejected }

export pure circuit defaultStatus(): Status {
    const s = default<Status>;  // Status.pending (first variant)
    return s;
}

Source: Default values

Enums in Ledger State

Enums can be stored directly in ledger fields:

enum ContractState { initialized, active, paused, terminated }

export ledger state: ContractState;

Source: Declaring public state

Enums in Structs

Enums work well as struct fields:

enum OrderStatus { pending, processing, shipped, delivered, cancelled }

struct Order {
    orderId: Uint<64>,
    customerId: Bytes<32>,
    amount: Uint<64>,
    status: OrderStatus
}

export ledger orders: Map<Uint<64>, Order>;

Source: User-defined types

Casting Enums to Field

Enums can be cast to Field type:

enum Choice { rock, paper, scissors }

export pure circuit paperIndex(): Field {
    // Enum to Field (by variant index: rock=0, paper=1, scissors=2)
    const choiceField = Choice.paper as Field;  // 1
    return choiceField;
}

Note: Enums also cast to Uint<n> (conversion, yielding the variant index), and integers cast back to enums as a checked cast — it compiles and runs, but fails at runtime if the value exceeds the last variant index (e.g., 5 is greater than maximum enum value 2). Verified with toolchain 0.31.1.

Source: Type casts

Exporting Enums

To use an enum from TypeScript, you must export it:

// ✅ Accessible from TypeScript
export enum GameState { waiting, playing, finished }

// ❌ Not accessible from TypeScript (internal only)
enum InternalState { a, b, c }

Source: Top-level exports

TypeScript Representation

Enum values are represented as numbers at runtime in JavaScript/TypeScript:

// Compact
export enum GameState {
  waiting,
  playing,
  finished,
}

// TypeScript runtime representation - numeric values
// waiting = 0, playing = 1, finished = 2

Source: Contract constructor

Common Patterns

State Machine

Enums are ideal for representing state machines:

enum AuctionState { open, closed, finalized }

export ledger auctionState: AuctionState;

// Check state in pure circuits
export pure circuit isOpen(state: AuctionState): Boolean {
    return state == AuctionState.open;
}

export pure circuit isClosed(state: AuctionState): Boolean {
    return state == AuctionState.closed;
}

Multiple Choice

enum Vote { yes, no, abstain }

struct Ballot {
    voter: Bytes<32>,
    choice: Vote
}

export ledger ballots: Map<Bytes<32>, Ballot>;

// Pure circuit to check vote
export pure circuit isYesVote(vote: Vote): Boolean {
    return vote == Vote.yes;
}

export pure circuit isNoVote(vote: Vote): Boolean {
    return vote == Vote.no;
}

Complete Example

pragma language_version >= 0.16;

// Game states
export enum GamePhase { setup, betting, reveal, payout }

// Player choices
export enum Choice { rock, paper, scissors }

// Game result
export enum Result { player1Wins, player2Wins, draw }

// Determine winner based on choices
export pure circuit determineWinner(p1: Choice, p2: Choice): Result {
    // Draw if same choice
    if (p1 == p2) {
        return Result.draw;
    }

    // Rock beats scissors
    if (p1 == Choice.rock) {
        if (p2 == Choice.scissors) {
            return Result.player1Wins;
        }
        return Result.player2Wins;
    }

    // Paper beats rock
    if (p1 == Choice.paper) {
        if (p2 == Choice.rock) {
            return Result.player1Wins;
        }
        return Result.player2Wins;
    }

    // Scissors beats paper
    if (p2 == Choice.paper) {
        return Result.player1Wins;
    }
    return Result.player2Wins;
}

// Check if game is in specific phase
export pure circuit isSetupPhase(phase: GamePhase): Boolean {
    return phase == GamePhase.setup;
}

export pure circuit isBettingPhase(phase: GamePhase): Boolean {
    return phase == GamePhase.betting;
}

Exercises

Complete exercises/05_user_types/02_enums/.

Generic Types

Generic types let you write reusable type definitions that work with multiple concrete types. Compact supports generics for structs and circuits.

Source: User-defined types

Generic Structs

A generic struct has one or more type parameters in angle brackets:

struct Pair<A, B> {
    first: A,
    second: B
}

struct Container<T> {
    value: T,
    isSet: Boolean
}

Using Generic Structs

When using a generic struct, provide concrete type arguments:

struct Pair<A, B> {
    first: A,
    second: B
}

export pure circuit example(): Pair<Uint<64>, Boolean> {
    const p: Pair<Uint<64>, Boolean> = Pair<Uint<64>, Boolean> {
        first: 42,
        second: true
    };
    return p;
}

Multiple Type Parameters

Structs can have multiple type parameters:

struct Triple<A, B, C> {
    first: A,
    second: B,
    third: C
}

struct KeyValue<K, V> {
    key: K,
    value: V
}

Source: User-defined types

Size Parameters

In addition to type parameters, generics can have size parameters for specifying numeric values like array lengths. Size parameters are prefixed with #:

// T is a type parameter, N is a size parameter
struct FixedArray<#N, T> {
    data: Vector<N, T>,
    length: Uint<32>
}

struct Matrix<#Rows, #Cols, T> {
    data: Vector<Rows, Vector<Cols, T>>
}

Source: Primitive types

Using Size Parameters

struct Buffer<#N> {
    data: Bytes<N>,
    used: Uint<32>
}

export pure circuit createBuffer(): Buffer<64> {
    return Buffer<64> {
        data: default<Bytes<64>>,
        used: 0
    };
}

Source: Structure creation

Standard Library Generics

The standard library uses generics extensively:

// From CompactStandardLibrary:
// struct Maybe<T> { is_some: Boolean; value: T; }
// struct Either<A, B> { is_left: Boolean; left: A; right: B; }

import CompactStandardLibrary;

export pure circuit wrapValue(x: Uint<64>): Maybe<Uint<64>> {
    return some<Uint<64>>(x);
}

export pure circuit makeEither(flag: Boolean, val: Field): Either<Field, Uint<64>> {
    if (flag) {
        return left<Field, Uint<64>>(val);
    }
    return right<Field, Uint<64>>(100);
}

Source: Standard library structs, Standard library circuits

Generic Circuits

Circuits can also be generic. Note that generic circuits cannot be exported from the top level — the compiler rejects export on a type-parameterized circuit. Keep them un-exported and call them from exported, concrete circuits:

// Generic pure circuits (not exported — the compiler forbids
// exporting type-parameterized circuits from the top level)
pure circuit first<A, B>(pair: [A, B]): A {
    return pair[0];
}

pure circuit second<A, B>(pair: [A, B]): B {
    return pair[1];
}

// Using generic circuits from an exported, concrete circuit
export pure circuit example(): Field {
    const p: [Field, Boolean] = [42, true];
    return first<Field, Boolean>(p);
}

Source: Generic parameter references, Circuit and witness calls

Constraints and Limitations

No Exported Generic Circuits

A type-parameterized circuit cannot be exported from the top level (cannot export type-parameterized function from the top level, verified with toolchain 0.31.1). Export concrete wrapper circuits instead.

Type Parameter Bounds

Compact doesn’t support trait bounds or type constraints like some languages. Any type can be used as a type argument.

Generic Structs Must Be Fully Specialized

When using a generic struct as a type or in structure creation, all type and size parameters must be provided:

struct Box<T> {
    value: T
}

export pure circuit boxed(): Box<Field> {
    // All type arguments must be specified
    const b: Box<Field> = Box<Field> { value: 42 };
    return b;
}

Source: User-defined types, Structure creation

Nested Generics

Generics can be nested:

struct Node<T> {
    value: T,
    children: Vector<2, Maybe<T>>
}

struct MapEntry<K, V> {
    key: K,
    value: Maybe<V>
}

Generic Types in Ledger

Generic types work in ledger declarations:

struct Entry<V> {
    value: V,
    timestamp: Uint<64>
}

export ledger entries: Map<Bytes<32>, Entry<Uint<64>>>;

Source: Declaring public state

TypeScript Representation

Generic structs map to TypeScript generics:

// Compact
struct Pair<A, B> {
    first: A,
    second: B
}

// TypeScript (generated)
interface Pair<A, B> {
    first: A;
    second: B;
}

Source: Representations in TypeScript

Complete Example

pragma language_version >= 0.16;

import CompactStandardLibrary;

// Generic result type
struct Result<T, E> {
    success: Boolean,
    value: T,
    error: E
}

// Helper to create success result
pure circuit ok<T, E>(value: T): Result<T, E> {
    return Result<T, E> {
        success: true,
        value: value,
        error: default<E>
    };
}

// Helper to create error result
pure circuit err<T, E>(error: E): Result<T, E> {
    return Result<T, E> {
        success: false,
        value: default<T>,
        error: error
    };
}

// Generic pair operations
struct Pair<T> {
    first: T,
    second: T
}

pure circuit makePair<T>(a: T, b: T): Pair<T> {
    return Pair<T> { first: a, second: b };
}

pure circuit swap<T>(p: Pair<T>): Pair<T> {
    return Pair<T> { first: p.second, second: p.first };
}

// Size-parameterized buffer
struct FixedBuffer<#N> {
    data: Bytes<N>,
    length: Uint<32>
}

// Use the generic types
export pure circuit example(): Result<Uint<64>, Uint<8>> {
    const pair = makePair<Field>(10, 20);
    const swapped = swap<Field>(pair);

    if (swapped.first == 20) {
        return ok<Uint<64>, Uint<8>>(100);
    }
    return err<Uint<64>, Uint<8>>(1);
}

Exercises

Complete exercises/05_user_types/03_generics/.

Circuits

Circuits are the main operational units of a Midnight smart contract. They correspond closely to functions in other languages, but are compiled directly into zero-knowledge circuits.

Source: Circuits

In this chapter, you’ll learn:

  • How to declare circuits
  • Parameters and return values
  • Pure circuits for helper functions
  • Visibility and exports

Why “Circuits”?

The Compact compiler outputs zero-knowledge circuits used to prove correctness of ledger interactions. When you write a Compact circuit, it gets transformed into an actual ZK circuit that can generate and verify cryptographic proofs.

Source: Writing a contract

Think of circuits as special functions that:

  1. Define business logic - What the contract can do and how state changes
  2. Generate ZK proofs - Compiled into circuits that underpin proof generation and verification
  3. Are callable from TypeScript - Exposed via the generated Contract class and Circuits/PureCircuits types when marked export

Source: Writing a contract, TypeScript target

Circuit Declaration

Basic Syntax

circuit c(a: A, b: B): R {
    // circuit body
}

export circuit name(): [] {
    // exported circuit body
}

Each path through the body must end with a return unless the return type is [].

Source: Circuits

Let’s break this down:

PartMeaning
exportMakes the circuit publicly callable
circuitKeyword declaring a circuit
nameThe circuit’s identifier
()Parameter list (empty here)
[]Return type (empty tuple = no return)
{ }Circuit body

⚠️ Critical: Return Type Syntax

Circuits that don’t return a value use [] (empty tuple), NOT Void:

export ledger counter: Counter;

// ✅ CORRECT
export circuit doSomething(): [] {
    counter.increment(1);
}
export ledger counter: Counter;

// ❌ WRONG - fails with "unbound identifier Void" (try it!)
export circuit doSomething(): Void {
    counter.increment(1);
}

Visibility

Exported Circuits

Use export to make circuits callable from TypeScript:

// Can be called from TypeScript
export circuit publicAction(): [] {
    // ...
}

Internal Circuits

Without export, circuits are internal (callable only within Compact):

// Only callable from other circuits
circuit internalHelper(): Field {
    return 42;
}

Source: Exported circuits

Generic Circuits

Circuits can have type parameters, making them reusable with different types:

circuit id<T>(value: T): T {
    return value;
}

circuit first<A, B>(pair: [A, B]): A {
    return pair[0];
}

Source: Generic parameter references

Complete Example

pragma language_version >= 0.16;

import CompactStandardLibrary;

export ledger balance: Counter;

// Public circuit - callable from TypeScript
export circuit deposit(amount: Uint<16>): [] {
    balance.increment(disclose(amount));
}

// Internal circuit - helper for other circuits
circuit getMinimum(): Uint<16> {
    return 10;
}

// Public circuit using internal helper
export circuit depositMinimum(): [] {
    const min = getMinimum();
    balance.increment(min);
}

Source: Writing a contract

Exercises

Complete exercises/03_circuits/01_basic_circuit/.

Parameters and Return Values

Parameters

Circuits can take typed parameters. The return type is written after :, and use [] for “no value”.

Source: Circuits

export circuit transfer(to: Bytes<32>, amount: Uint<64>): [] {
    // use 'to' and 'amount'
}

Multiple Parameters

export circuit multiParam(
    a: Uint<64>,
    b: Uint<64>,
    flag: Boolean
): [] {
    // ...
}

Parameter Types

Parameters can be any Compact type:

  • Primitives: Boolean, Field, Uint<N>, Bytes<N>
  • Compounds: [T, U] (tuples), Vector<N, T>
  • User-defined: structs, enums
  • Standard library types: Maybe<T>, Either<A, B> (requires import CompactStandardLibrary)

Source: Primitive types, User-defined types

Return Values

Single Return

export circuit getBalance(): Uint<64> {
    return 100;  // Returns a Uint<64>
}

No Return (Empty Tuple)

export ledger counter: Counter;

export circuit performAction(): [] {
    // Do something, return nothing
    counter.increment(1);
}

Example storing a message with no return value:

export ledger message: Opaque<"string">;

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

Source: Hello world

Tuple Return

Return multiple values using tuples:

export circuit getStats(): [Uint<64>, Boolean] {
    return [100, true];
}

Struct Return

export struct Result {
    value: Uint<64>,
    success: Boolean
}

export circuit compute(): Result {
    return Result {
        value: 42,
        success: true
    };
}

Using Return Values

From other circuits:

export ledger counter: Counter;

circuit calculate(): Uint<16> {
    return 100;
}

export circuit useCalculation(): [] {
    const result = calculate();
    counter.increment(result);  // Counter.increment expects Uint<16>
}

Source: Circuit and witness calls

From TypeScript (after deployment):

Exported circuits are accessible via the generated Contract class, which exposes circuits and impureCircuits properties. The exact call pattern depends on your SDK version, but return types map to their TypeScript equivalents.

Source: TypeScript target, Representations in TypeScript

Exercises

Complete exercises/03_circuits/02_parameters/ and exercises/03_circuits/03_return_values/.

Pure Circuits

A circuit is considered pure if it computes its outputs from its inputs without reference to or modification of public state (via the ledger) or private state (via witnesses).

Source: Circuits

Syntax

The pure modifier must follow export (if present):

pure circuit helper(x: Field): Field {
    return x * 2;
}

export pure circuit publicHelper(x: Field): Field {
    return x * 2;
}

If a pure circuit actually uses ledger operations or witnesses, the compiler rejects it. Marking a circuit pure ensures it appears in the generated PureCircuits type and pureCircuits constant in the TypeScript output.

Source: Circuits, TypeScript target

What Makes a Circuit Impure?

According to the docs, the compiler considers a circuit to be impure if:

  1. The body contains a ledger operation
  2. The body calls any impure circuit
  3. The body calls a witness

Note: Some standard library functions are witnesses and make the caller impure.

What Pure Circuits Can Do

  • ✅ Perform calculations
  • ✅ Call other pure circuits
  • ✅ Call pure standard library functions
  • ✅ Use parameters and local variables
  • ❌ Read ledger state
  • ❌ Modify ledger state
  • ❌ Call witnesses
  • ❌ Call impure circuits

Use Cases

Mathematical Operations

pure circuit triple(amount: Uint<64>): Uint<64> {
    return (amount * 3) as Uint<64>;
}

⚠️ Note: Compact has no division operator — / is a parse error. Percentage-style math (e.g., a 3% fee) can’t be written as amount * 3 / 100; compute the quotient off-chain, pass it in as a witness, and constrain it in the circuit (see Chapter 7: Witnesses).

Validation Logic

pure circuit isValidAge(age: Uint<8>): Boolean {
    return age >= 18 && age <= 120;
}

Type Conversions

pure circuit toBytes(value: Uint<64>): Bytes<32> {
    return (value as Field) as Bytes<32>;
}

Using Pure Circuits

Call pure circuits from any circuit:

export ledger counter: Counter;

pure circuit validate(x: Uint<64>): Boolean {
    return x > 0 && x < 1000;
}

export circuit process(amount: Uint<64>): [] {
    assert(validate(amount), "Invalid amount");
    counter.increment(disclose(amount as Uint<16>));
}

TypeScript Integration

The Compact compiler exports pure circuits via pureCircuits in the TypeScript module, allowing them to be used directly without blockchain interaction.

Source: TypeScript target

Benefits of Pure Circuits

  1. Verification - Compiler enforces purity constraints
  2. Reusability - Share logic across circuits
  3. Off-chain use - Available via pureCircuits in TypeScript
  4. Clarity - Clear separation of stateful vs stateless logic

Exercises

Complete exercises/03_circuits/04_pure_circuits/.

Witnesses

Witnesses are the bridge between private off-chain data and your Compact contract.

In this chapter, you’ll learn:

  • What witnesses are and why they matter
  • How to declare witnesses in Compact
  • How to implement witnesses in TypeScript

Understanding Witnesses

What is a Witness?

In zero-knowledge proofs, a witness is private input that the prover knows but doesn’t reveal to the verifier.

Example: Proving you know a password without revealing it.

In Compact, witnesses provide private data to your circuits:

  • Secret keys
  • Private amounts
  • Hidden choices
  • Any data you want to use but not expose

Witness Declaration

Witnesses are declared with just a signature - no body:

witness getSecretKey(): Bytes<32>;

⚠️ Critical Syntax

// ✅ CORRECT - Declaration only, no body
witness myWitness(): Bytes<32>;
// ❌ WRONG - Witnesses have NO body in Compact
// (fails with a parse error - try it!)
witness myWitness(): Bytes<32> {
    return something;
}

The implementation happens in TypeScript, not Compact!

How Witnesses Work

  1. Compact declares the witness signature
  2. TypeScript implements the witness function
  3. At proof time, the witness provides private data
  4. The proof verifies computations on that data
  5. The witness value stays private
┌─────────────────┐      ┌──────────────────┐
│  TypeScript     │      │    Compact       │
│  (implements)   │ ───▶ │  (declares)      │
│  secretKey()    │      │  witness secret  │
└─────────────────┘      └──────────────────┘
         │                        │
         │    Private Data        │
         └──────────┬─────────────┘
                    ▼
            ┌───────────────┐
            │  ZK Proof     │
            │  (data stays  │
            │   hidden)     │
            └───────────────┘

⚠️ Security Warning

Do not assume that the code of any witness function is the code that you wrote in your own implementation. Any DApp may provide any implementation that it wants for your witness functions. Results from witnesses should be treated as untrusted input.

Always validate witness results in your circuit logic!

Validating Witness Results

Since witnesses are untrusted, you must verify their outputs make sense. Here are the key patterns:

What CAN Be Trusted

Only the types can be assumed valid. If a witness returns Uint<32>, you know the result will be a Uint<32> (or it will fail an assert earlier). But you have no way of knowing that the witness was honestly computed.

What CANNOT Be Trusted

  • That the witness was computed using your DApp’s implementation
  • That any invariants hold (e.g., not all values of Uint<32> may be valid for your use case)
  • That the computation was done correctly

Pattern 1: Offloading Expensive Computation

When using witnesses to perform operations that are expensive or impossible in circuits (like division), verify the result makes mathematical sense:

pragma language_version >= 0.16;

// Witness performs division - expensive/impossible in circuit
witness _divMod(x: Uint<32>, y: Uint<32>): [Uint<32>, Uint<32>];

// Safe division: calls witness, then VERIFIES the result
export circuit div(x: Uint<32>, y: Uint<32>): Uint<32> {
  const res = disclose(_divMod(x, y));
  const quotient = res[0];
  const remainder = res[1];

  // Verify the witness gave us a valid answer!
  // If x = y * quotient + remainder, the division was correct
  assert(remainder < y && x == y * quotient + remainder,
         "Invalid divMod witness impl");

  return quotient;
}

export circuit mod(x: Uint<32>, y: Uint<32>): Uint<32> {
  const res = disclose(_divMod(x, y));
  const quotient = res[0];
  const remainder = res[1];

  assert(remainder < y && x == y * quotient + remainder,
         "Invalid divMod witness impl");

  return remainder;
}

The TypeScript implementation:

// witnesses.ts
_divMod: (
  context: WitnessContext<Ledger, PrivateState>,
  x: bigint,
  y: bigint
): [PrivateState, [bigint, bigint]] => {
  const xn = Number(x);
  const yn = Number(y);
  const remainder = xn % yn;
  const quotient = Math.floor(xn / yn);
  return [context.privateState, [BigInt(quotient), BigInt(remainder)]];
};

Key insight: We never use _divMod() directly in the contract. We wrap it in div() and mod() which call the witness and verify the result is mathematically correct.

Pattern 2: Verifying Private State (e.g., Secret Keys)

When a witness provides private state like a user’s secret key, verify it against known public data:

// Registered public key, visible on-chain
export ledger userPublicKey: Bytes<32>;

// Witness provides the user's secret key
witness secretKeyWitness(): Bytes<32>;

export circuit authenticate(): [] {
  // Get the secret from the witness — no disclose() here,
  // the secret must stay private!
  const secret = secretKeyWitness();

  // Derive the public key from the secret
  const derivedPubKey = persistentHash<Bytes<32>>(secret);

  // Verify against the stored public key — asserting on a
  // witness-derived hash does not disclose the secret
  assert(derivedPubKey == userPublicKey, "User verification failed");
}

The verification proves the user knows the secret that corresponds to their registered public key, without revealing the secret itself.

Why Validation Matters

A malicious user could:

  1. Interact from a different client - Not using your DApp at all
  2. Provide a modified witness implementation - Returns incorrect values
  3. Try to exploit business logic - By providing “valid type but invalid value” data

Your circuit’s asserts are your only line of defense against malicious witness implementations.

Why Not Just Pass Parameters?

ParametersWitnesses
Visible on-chainPrivate to prover
Anyone can provideOnly prover provides
Part of transactionPart of proof

Use witnesses for anything that should remain private.

Exercises

Complete exercises/04_witnesses/01_witness_declaration/.

TypeScript Implementation

Witnesses are declared in Compact but implemented in TypeScript. The implementation lives in a separate TypeScript file, not in your .compact file.

Workflow

Step 1: Declare the Witness in Compact

In your .compact file, declare what the witness looks like:

pragma language_version >= 0.16;

import CompactStandardLibrary;

// Declaration only - no body
witness getSecretKey(): Bytes<32>;

export ledger owner: Bytes<32>;

export circuit setOwner(): [] {
    const key = getSecretKey();
    owner = disclose(key);
}

Step 2: Compile

compact compile my-contract.compact managed/my-contract

This generates TypeScript types in managed/my-contract/.

Step 3: Implement in TypeScript

Create a .ts file with the actual implementation:

export type MyPrivateState = {
  secretKey: Uint8Array;
};

export const witnesses = {
  getSecretKey: ({ privateState }) => {
    return [privateState, privateState.secretKey];
  },
};

Step 4: Use in Your DApp

import { witnesses, MyPrivateState } from "./witnesses";

const initialPrivateState: MyPrivateState = {
  secretKey: new Uint8Array(32),
};

Project Structure

your-project/
├── contracts/
│   └── my-contract.compact       # Step 1: Compact code
├── contracts/managed/
│   └── my-contract/              # Step 2: Generated files (after compile)
│       ├── contract/
│       └── ...
└── src/
    └── witnesses.ts              # Step 3: Your TypeScript implementation

Source: Create MN App

Note: The Compact compiler only checks .compact syntax. To test the full witness flow, you need a complete DApp with the Midnight runtime.

Generated API

When you compile, the TypeScript target generates a Witnesses<T> type. Each witness becomes a function:

type Witness<PS> = (
  context: WitnessContext<any, PS>,
  ...rest: any[]
) => [PS, any];

Source: Witness type

WitnessContext provides:

  • ledger - Current ledger state
  • privateState - Your private state
  • contractAddress - The contract’s address

Source: WitnessContext

Example from BBoard DApp

Compact:

witness localSecretKey(): Bytes<32>;

TypeScript:

export type BBoardPrivateState = {
  secretKey: Uint8Array;
};

export const witnesses = createBBoardWitnesses<BBoardPrivateState>({
  localSecretKey: ({ privateState }) => {
    return [privateState, privateState.secretKey];
  },
});

Source: BBoard DApp

Key Points

  1. First argument is WitnessContext<Ledger, PrivateState>
  2. Return [newPrivateState, result]
  3. Don’t mutate privateState - return updated state

Contracts Without Witnesses

If your contract has no witnesses, provide an empty object:

export type CounterPrivateState = {
  privateCounter: number;
};

export const witnesses = {};

Source: Counter DApp

How the Runtime Uses Witnesses

When you instantiate the contract, you pass your witnesses. The runtime:

  1. Calls the witness function when Compact code invokes it
  2. Passes current privateState via context.privateState
  3. Receives [updatedPrivateState, value] back
  4. Threads the updated state through subsequent calls

Source: TypeScript target

Next Steps

Now that you understand witnesses, learn about Control Flow.

Control Flow

Compact provides a small, statically analyzable set of control flow constructs:

  • if / if...else statements
  • for loops (bounded)
  • return
  • assert
  • blocks { ... }
  • const bindings
  • conditional expressions e0 ? e1 : e2

Source: Statements

In this chapter:

Conditionals

If Statements

Two forms:

if (condition)
  <statement>

if (condition)
  <statement>
else
  <statement>

The condition must be Boolean. Only the taken branch executes.

Source: Statements

Example

pragma language_version >= 0.16;

import CompactStandardLibrary;

export ledger status: Uint<8>;

export circuit updateStatus(value: Uint<8>): [] {
    if (disclose(value) > (10 as Uint<8>)) {
        status = 1 as Uint<8>;
    } else {
        status = 0 as Uint<8>;
    }
}

Conditional Expressions

Compact has a ternary conditional expression:

e0 ? e1 : e2
  • e0 must be Boolean
  • e1 and e2 must have compatible types
  • Only the selected branch is evaluated

Source: Conditional expressions

Example

pragma language_version >= 0.16;

import CompactStandardLibrary;

export pure circuit max(a: Uint<64>, b: Uint<64>): Uint<64> {
    return a > b ? a : b;
}

export pure circuit clamp(value: Uint<64>, minVal: Uint<64>, maxVal: Uint<64>): Uint<64> {
    return value < minVal ? minVal : (value > maxVal ? maxVal : value);
}

Return

Every control-flow path must end in a return, unless the return type is [].

// For [] return type
return;

// For other return types
return <expr>;

Source: Circuits

Next Steps

Loops

Compact supports bounded for loops. Loops must have statically known bounds.

Source: Statements

Two Forms

for (const i of <lower>..<upper>) <statement>

for (const item of <vector>) <statement>

Iterating Over a Range

pragma language_version >= 0.16;

import CompactStandardLibrary;

export ledger results: Map<Field, Field>;

export circuit storeRange(): [] {
    for (const i of 0..5) {
        results.insert(disclose(i), disclose(i) * (2 as Field));
    }
}

Iterating Over a Vector

pragma language_version >= 0.16;

import CompactStandardLibrary;

export ledger total: Counter;

export circuit processVector(values: Vector<5, Uint<16>>): [] {
    for (const v of values) {
        total.increment(disclose(v));
    }
}

Accumulation with Fold

Since Compact only has const bindings (no mutable variables), use fold for accumulation:

pragma language_version >= 0.16;

import CompactStandardLibrary;

export pure circuit sumVector(values: Vector<4, Uint<64>>): Uint<64> {
    const result = fold(
        (acc: Uint<64>, val: Uint<64>): Uint<64> => {
            return (acc + val) as Uint<64>;
        },
        0 as Uint<64>,
        values
    );
    return result;
}

Const Bindings and Blocks

Within loops, you can use const bindings:

const x = <expr>;
const x: T = <expr>, y = <expr>;

Rules:

  • Bindings are evaluated left-to-right
  • No use-before-definition
  • No rebinding in the same block
  • Shadowing allowed in nested blocks

Source: Statements

Next Steps

Learn about Functions in Compact.

Assertions

Assertions check that a Boolean condition holds at runtime and are enforced in the zero-knowledge circuit.

Syntax

assert(<condition>, "error message");
  • <condition> must evaluate to Boolean
  • If true, execution continues
  • If false, the transaction fails with the given message
  • The assertion is constrained in-circuit (becomes part of the ZK proof)

Source: Statements

Examples

State Preconditions

pragma language_version >= 0.16;

import CompactStandardLibrary;

export enum State { unset, set }

export ledger state: State;
export ledger value: Uint<64>;

export circuit get(): Uint<64> {
    assert(state == State.set, "Attempted to get uninitialized value");
    return value;
}

Source: Writing a contract

Authorization

The BBoard contract uses assertions for authorization. Note that publicKey is a custom circuit defined in the contract:

// From the BBoard contract (simplified)
export circuit post(newMessage: Opaque<"string">): [] {
    assert(state == State.VACANT, "Attempted to post to an occupied board");
    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 is empty");
    assert(owner == publicKey(localSecretKey(), sequence as Field as Bytes<32>),
           "Not the current owner");
    // ... rest of circuit
}

Source: BBoard contract

Typical Uses

  • State preconditions - ensure contract is in correct state before proceeding
  • Authorization - verify caller has permission (e.g., derived key matches owner)
  • Witness validation - check untrusted witness results before using them

Runtime and Circuit Behavior

Assertions serve two purposes:

  1. Runtime guard - if false, the transaction aborts with the error message
  2. Circuit constraint - the condition is encoded into the ZK proof

This means any valid proof must satisfy all assertions in the circuit.

Source: Statements

Next Steps

Learn about Functions in Compact.

Expressions & Operators

Expressions in Compact produce values and have static types. Every expression either evaluates to a value or raises an exception.

Expression Categories

Compact supports several categories of expressions:

CategoryExamples
Literals42, true, "hello"
Arithmetica + b, x - y, n * m
Comparisona == b, x < y, n >= m
Logicala && b, x || y
Conditionalcond ? a : b
Type Castvalue as Uint<64>

Evaluation Rules

Expressions evaluate their subexpressions in order. If any subexpression raises an exception, the entire expression stops and raises that exception.

// Type errors are caught at compile time
const x: Uint<0..255> = 300;  // Error: 300 exceeds range 0..255

// Runtime exceptions propagate up
const result = checked_operation();  // May fail at runtime

In This Chapter

Arithmetic Operators

Compact provides three arithmetic operators: addition, subtraction, and multiplication. Division is not supported.

Note: The official Midnight documentation uses bounded notation like Uint<0..n> to describe types. In code examples, we use the shorthand Uint<64>, which is equivalent to Uint<0..2^64> — the upper bound of the bounded form is exclusive (verified with toolchain 0.31.1).

Available Operators

OperatorNameResult Type (for Uint<0..m> and Uint<0..n>, exclusive bounds)
+AdditionUint<0..m+n-1> (max value (m-1)+(n-1))
-SubtractionUint<0..m> (left operand’s bound)
*MultiplicationUint<0..(m-1)*(n-1)+1> (max value (m-1)*(n-1))

Verified with toolchain 0.31.1: Uint<0..10> + Uint<0..20> infers Uint<0..29>, and Uint<0..10> * Uint<0..20> infers Uint<0..172> — the compiler widens to exactly cover the largest possible result.

Basic Arithmetic

pragma language_version >= 0.16;

export circuit add(a: Uint<64>, b: Uint<64>): Uint<64> {
  return (a + b) as Uint<64>;
}

export circuit subtract(a: Uint<64>, b: Uint<64>): Uint<64> {
  return (a - b) as Uint<64>;
}

export circuit multiply(a: Uint<64>, b: Uint<64>): Uint<64> {
  return (a * b) as Uint<64>;
}

Type Widening

When you perform arithmetic on unsigned integers, Compact computes a result type based on the operand bounds:

  • For Uint<0..m> + Uint<0..n>, the result type is Uint<0..m+n-1> (covers max value (m-1)+(n-1))
  • For Uint<0..m> * Uint<0..n>, the result type is Uint<0..(m-1)*(n-1)+1> (covers max value (m-1)*(n-1))
  • For Uint<0..m> - Uint<0..n>, the result type is Uint<0..m>

If the computed bound exceeds the implementation’s maximum unsigned integer, it is a static type error.

When the result type doesn’t match the expected type, you may need a cast:

pragma language_version >= 0.16;

export circuit sumExample(): Uint<64> {
  const a: Uint<64> = 100;
  const b: Uint<64> = 200;

  // The result of a + b has a wider type than Uint<64>
  // Cast to fit the return type
  const result: Uint<64> = (a + b) as Uint<64>;
  return result;
}

Field Arithmetic

If either operand has type Field, the result type is Field. Field arithmetic uses modular arithmetic - overflow and underflow wrap modulo the field modulus:

pragma language_version >= 0.16;

export circuit fieldMath(): Field {
  const a: Field = 100;
  const b: Field = 200;

  // Result type is Field
  const sum: Field = a + b;
  const product: Field = a * b;

  return product;
}

Practical Example: Counter

pragma language_version >= 0.16;

export ledger count: Uint<64>;

export circuit increment(): [] {
  count = (count + 1) as Uint<64>;
}

export circuit addAmount(amount: Uint<64>): [] {
  // Disclose parameter before using in ledger operation
  const dAmount = disclose(amount);
  count = (count + dAmount) as Uint<64>;
}

export circuit getCount(): Uint<64> {
  return count;
}

Key Points

  1. No division operator - Compact doesn’t support /
  2. Result types are computed from bounds - + and * produce wider types; casts may be needed depending on usage
  3. Field arithmetic wraps - Overflow/underflow wrap modulo the field modulus
  4. Subtraction can underflow - If b > a at runtime, a - b raises a runtime error (no wraparound for Uint)

Comparison and Boolean

Compact provides relational operators for comparing values and logical operators for combining Boolean expressions.

Note: The official Midnight documentation uses bounded notation like Uint<0..n> to describe types. In code examples, we use the shorthand Uint<64>, which is equivalent to Uint<0..2^64> — the upper bound of the bounded form is exclusive (verified with toolchain 0.31.1).

Relational Operators

OperatorNameReturns
==EqualsBoolean
!=Not equalsBoolean
<Less thanBoolean
>Greater thanBoolean
<=Less than or equalsBoolean
>=Greater than or equalsBoolean

Equality Comparisons

== and != work on any comparable types:

pragma language_version >= 0.16;

export circuit equalityExamples(): Boolean {
  const a: Uint<64> = 42;
  const b: Uint<64> = 42;
  const c: Uint<64> = 100;

  const equal: Boolean = a == b;      // true
  const notEqual: Boolean = a != c;   // true

  return equal && notEqual;
}

Ordering Comparisons

<, >, <=, >= require unsigned integer operands:

pragma language_version >= 0.16;

export circuit orderingExamples(): Boolean {
  const x: Uint<64> = 10;
  const y: Uint<64> = 20;

  const less: Boolean = x < y;         // true
  const greater: Boolean = y > x;      // true
  const lessEq: Boolean = x <= 10;     // true
  const greaterEq: Boolean = y >= 20;  // true

  return less && greater;
}

Logical Operators

OperatorNameBehavior
&&Logical ANDShort-circuit: stops if left is false
||Logical ORShort-circuit: stops if left is true

Both operators require Boolean operands and return Boolean.

Short-Circuit Evaluation

Logical operators use short-circuit evaluation:

  • a && b - If a is false, b is not evaluated
  • a || b - If a is true, b is not evaluated
pragma language_version >= 0.16;

export circuit shortCircuit(): Boolean {
  const x: Uint<64> = 0;

  // Safe: second condition not evaluated when x == 0
  const safe: Boolean = x != 0 && x > 5;

  // OR short-circuits on first true
  const quick: Boolean = true || false;

  return safe || quick;
}

Conditional Expressions

Compact supports the ternary conditional operator ? ::

pragma language_version >= 0.16;

export circuit max(a: Uint<64>, b: Uint<64>): Uint<64> {
  return a > b ? a : b;
}

export circuit min(a: Uint<64>, b: Uint<64>): Uint<64> {
  return a < b ? a : b;
}

export circuit absDiff(a: Uint<64>, b: Uint<64>): Uint<64> {
  return a > b ? (a - b) as Uint<64> : (b - a) as Uint<64>;
}

Combining Conditions

pragma language_version >= 0.16;

export circuit inRange(value: Uint<64>, minVal: Uint<64>, maxVal: Uint<64>): Boolean {
  return value >= minVal && value <= maxVal;
}

export circuit isValid(amount: Uint<64>, limit: Uint<64>): Boolean {
  const nonZero: Boolean = amount > 0;
  const withinLimit: Boolean = amount <= limit;
  return nonZero && withinLimit;
}

Key Points

  1. All comparisons return Boolean
  2. Ordering operators (<, >, <=, >=) only work on unsigned integers
  3. Logical operators short-circuit - right side may not execute
  4. Conditional expression a ? b : c - both branches must have compatible types

Type Conversions

Compact uses the as keyword for type casts. The cast rules determine when conversions are allowed and whether they may fail at runtime.

Note: The official Midnight documentation uses bounded notation like Uint<0..n> to describe types. In code examples, we use the shorthand Uint<64>, which is equivalent to Uint<0..2^64> — the upper bound of the bounded form is exclusive (verified with toolchain 0.31.1).

Cast Syntax

expression as Type

TypeScript-style casts like <Type>expression are not supported.

Cast Categories

The documentation defines three behaviors for allowed casts:

CategoryDescriptionRuntime Effect
StaticChanges only the static typeNone
ConversionConverts between representationsAlways succeeds
CheckedValidates at runtimeMay fail

Allowed Casts Reference

The following table summarizes the allowed casts between types:

FromToBehavior
Uint<0..m>Uint<0..n> where m ≤ nStatic
Uint<0..m>Uint<0..n> where m > nChecked (fails if value > n)
Uint<0..m>FieldStatic
FieldUint<0..n>Checked
FieldBooleanConversion (0→false, others→true)
FieldBytes<n>Conversion (runtime error if doesn’t fit)
Bytes<m>FieldConversion (LSB first, checked if exceeds max Field)
Bytes<m>Bytes<n> where m == nStatic
enum typeFieldConversion
enum typeUint<0..n>Conversion (variant index; verified 0.31.1)
Uint<0..m>Bytes<n>Conversion (little-endian; verified 0.31.1)
Bytes<m>Uint<0..n>Conversion (little-endian; verified 0.31.1)
Uint<0..m>enum typeChecked (fails at runtime past the last variant)

Not allowed:

  • Bytes<m> → Bytes<n> where m ≠ n

Upcasts (Always Safe)

Upcasts—casts from a type to a supertype—are always allowed and never result in a static or runtime error.

For Uint types, casting from a smaller range to a larger range is a static cast:

pragma language_version >= 0.16;

export circuit upcastExample(): Field {
  const small: Uint<32> = 100;

  // Uint<32> to Uint<64>: static cast (range fits)
  const bigger: Uint<64> = small as Uint<64>;

  // Uint to Field: static cast
  const f: Field = bigger as Field;

  return f;
}

Checked Casts (May Fail)

When casting from a larger range to a smaller range, the cast is checked at runtime:

pragma language_version >= 0.16;

export circuit checkedCastExample(): Uint<32> {
  const big: Uint<64> = 50;

  // Uint<64> to Uint<32>: checked cast
  // Fails at runtime if value exceeds Uint<32> range
  const smaller: Uint<32> = big as Uint<32>;

  return smaller;
}

Field Conversions

The cast rules for Field:

  • Uint<0..m> → Field: static cast
  • Field → Uint<0..n>: checked cast (may fail if value too large)
  • Field → Boolean: conversion (0 becomes false, non-zero becomes true)
pragma language_version >= 0.16;

export circuit fieldToUint(): Uint<64> {
  const f: Field = 42;

  // Field to Uint: checked cast
  const num: Uint<64> = f as Uint<64>;

  return num;
}

Bytes and Field

The cast rules for Bytes:

  • Bytes<m> → Field: conversion (LSB first, checked if exceeds max Field)
  • Field → Bytes<n>: conversion (runtime error if doesn’t fit)
  • Bytes<m> → Bytes<n>: static only if m == n
pragma language_version >= 0.16;

export circuit bytesToField(): Field {
  // Create Bytes using Bytes[...] syntax
  const b: Bytes<4> = Bytes[1, 2, 3, 4];

  // Bytes to Field: conversion (LSB first)
  const f: Field = b as Field;

  return f;
}

Enum to Field

Enums can be cast to Field — and also directly to Uint<n> (both conversions yield the variant index):

pragma language_version >= 0.16;

export enum Status { pending, active, completed }

export circuit enumToField(): Field {
  const s: Status = Status.active;

  // Enum to Field: conversion
  const f: Field = s as Field;

  return f;
}

Note: enum → Uint<n> is also a valid conversion, and integer → enum works as a checked cast that fails at runtime past the last variant index (verified with toolchain 0.31.1).

Key Points

  1. Use as for all casts — TypeScript-style <T> is not supported
  2. Upcasts are always safe — Casting to a supertype never fails
  3. Uint<0..m> → Uint<0..n> — Static if m ≤ n, checked otherwise
  4. Uint → Field — Static cast
  5. Field → Uint — Checked cast (may fail)
  6. enum → Uint — Conversion (variant index); integer → enum is checked
  7. Field conversions - Uint to Field is safe; Field to Uint is checked

Ledger State

Coming Soon.

Declaring Ledger Fields

Coming Soon.

Counter Type

Coming Soon.

Map and Set Types

Coming Soon.

MerkleTree Type

Coming Soon.

The disclose() Function

Coming Soon.

Understanding Disclosure

Coming Soon.

Disclosure Patterns

Coming Soon.

Standard Library

Coming Soon.

Hash Functions

Coming Soon.

Utility Functions

Coming Soon.

Modules and Organization

Coming Soon.

Creating Modules

Coming Soon.

Imports and Exports

Constructors

TypeScript Integration

Type Mappings

Implementing Witnesses

SDK Usage

Deployment

Project: Counter DApp

Contract Implementation

TypeScript CLI

Deployment and Testing

Project: Bulletin Board

Privacy Model

Contract Implementation

React UI

Project: Private Voting

Coming Soon.

Design

Coming Soon.

Implementation

Coming Soon.

Frontend

Coming Soon.

Common Mistakes

Coming Soon.

Testing Contracts

Coming Soon.

Security Considerations

Coming Soon.

Appendix A: Compact Keywords

Coming Soon.

Appendix B: Operators Reference

Coming Soon.

Appendix C: Standard Library API

Coming Soon.

Appendix D: Error Messages

Coming Soon.

Appendix E: Version History

This page mirrors the Midnight Support Matrix for quick reference. The authoritative source is the Midnight docs — always check there if you hit a compatibility issue.

Last synced: July 2026

Current Supported Versions

From the official matrix:

This matrix only reflects the latest tested versions. Earlier versions may still work, but we do not guarantee compatibility or provide support for them.

Runtime & Contracts

These versions are the same across Preview, Preprod, and Mainnet:

ComponentVersionPurpose
Compact devtools (compact)0.5.1Installs compilers, compiles contracts
Compact toolchain (compact compile)0.31.1Contract compiler for Midnight
Compact runtime0.16.0Runtime library for contracts
Compact JS2.5.1TypeScript execution environment for compiled contracts
Platform JS2.2.4Core abstractions and types
On-chain runtime3.0.0On-chain runtime support (v3)

SDKs & APIs

Also the same across all three networks:

ComponentVersionPurpose
Wallet SDK1.2.0SDK for building wallet integrations
Midnight.js4.1.1DApp framework: contracts, types, providers
testkit-js4.1.1E2E testing framework
DApp Connector API4.0.1Interface between DApps and wallets

Network & Infrastructure

These components differ by network:

ComponentPreviewPreprodMainnetNotes
Node (Midnight)1.0.01.0.00.22.5Network node
Midnight Indexer4.3.34.3.34.0.1Blockchain indexer
Proof server8.1.08.1.08.0.3ZKP proof generation

Language Version Pragma

The Compact language version (used in pragma language_version) is distinct from the compiler version above. Toolchain 0.31.1 implements language version 0.23 (check yours with compact compile --language-version). This book’s runnable examples target:

pragma language_version >= 0.16;

Any compiler from the 0.16 language line onward — including the current 0.23 line — accepts these programs. See Chapter 2: Pragma and Imports for the full range of pragma forms.

Compatibility Note

If your installed toolchain doesn’t match what’s listed here, run compact update to pull the latest, or consult the Midnight Support Matrix for the current state of the world.