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:
- Foundations - Installation, setup, and basic concepts
- The Type System - All about Compact’s types
- Core Language Features - Circuits, witnesses, control flow
- State Management - Working with ledger state
- Advanced Concepts - Standard library, modules, TypeScript integration
- Projects - Build real applications
- 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:
- Read sequentially - Chapters build on each other
- Type the code - Don’t just copy-paste
- Do the exercises - They reinforce learning
- 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
compactdevtools 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:
- Install the Compact compiler
- Set up the Lace wallet for testing
- Get test tokens from the faucet
- Configure Docker for the proof server
- Install the VS Code extension
- 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:
| Tool | Purpose | Status |
|---|---|---|
compact | The Compact developer tools — toolchain management, compiling, formatting | ✅ Use this |
compactc | The Compact compiler (standalone) | ⚠️ Deprecated |
In practice:
- Use
compact compilefor compilation:compact compile contract.compact output/ - Use
compact update/compact checkto manage toolchain versions,compact formatto format code, andcompact self checkto update the devtools themselves (runcompact --helpfor the full list — there is no scaffolding subcommand; new projects are created withnpx create-mn-app)
⚠️ Important: The standalone
compactccommand is deprecated. Always usecompact compileinstead.
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
- Install the Lace wallet extension from the Chrome Web Store or the Edge Add-ons store
- Click Add to Chrome (or Get on Edge) and confirm
- Pin the extension to your toolbar for easy access (recommended)
Create Your Wallet
- Click the Lace wallet icon in your toolbar
- Select Create a new wallet — Midnight appears as a network option
- Choose a strong password
- Write down your seed phrase on paper and store it securely offline
- Confirm your seed phrase to complete setup
- 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.
- In your Lace wallet, copy your Unshielded wallet address
- Go to the faucet for your network:
- Paste your address into the form and click Request tokens — the faucet sends 1000 tNIGHT within a couple of minutes
- In Lace, go to Tokens, click Generate tDUST, and confirm the transaction
- 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):
- Download and install Docker Desktop for Windows.
- In Docker Desktop “Settings” >“General” ensure “Use the WSL 2 based engine” is checked.
- 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).
Local Development Network (Recommended)
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.
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
- Open Lace Wallet Settings → Midnight
- Switch network to “Undeployed”
- 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
| Service | Port |
|---|---|
| Proof Server | 6300 |
| Node | 9944 |
| Indexer | 8088 |
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
- Download the VSIX package from the Compact VS Code extension release page
- In VS Code, go to Extensions → click
...menu → Install from VSIX - 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
.compactfiles - 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:
- Uninstall the Linux version:
sudo apt-get remove docker docker-engine docker.io - Install Docker Desktop for Windows on your host machine.
- 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.
- Open PowerShell and run:
wsl --list --verbose - If the version is
1, upgrade it:wsl --set-version Ubuntu 2 - 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:
- Install the “WSL” extension in VS Code.
- In your Ubuntu terminal, type:
code . - Create the
counter.compactfile 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:
exportmakes it callable from outside the contractcircuitis the keyword (notfunction)[]is the return type for circuits that return nothing (notvoidorVoid)- 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
compactccommand is deprecated. Always usecompact 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:
- contract/ - TypeScript/JavaScript code to interact with the contract
- Deploy the contract
- Call circuit functions
- Read ledger state
- zkir/ - Zero-knowledge intermediate representation of your circuits
- keys/ - Cryptographic keys for proving and verification
- compiler/ - Build metadata and artifacts
Common First-Time Errors
| Error | Cause | Fix |
|---|---|---|
unbound identifier Void | Using Void as return type | Use [] instead |
parse error: found "ledger" | Using ledger { } block syntax | Declare each field separately |
unbound identifier Counter | Missing import | Add import CompactStandardLibrary; |
potential witness-value disclosure must be declared | Using witness value in ledger operation | Wrap the value with disclose() |
Exercises
Complete the exercises in exercises/01_basics/ to practice:
- Writing pragma statements
- Importing the standard library
- Declaring ledger state
- 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.compactactually 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:
| Element | Convention | Example |
|---|---|---|
| Circuits | camelCase | getBalance, transferFunds |
| Types/Structs | PascalCase | Person, PlayerStats |
| Ledger fields | camelCase | balance, totalSupply |
| Enum variants | lowercase | Status.pending, State.active |
| Constants | camelCase | maxValue, 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
- Pragma first, imports second - These must come at the top in this order
- One declaration per line - No block syntax for ledger (e.g., no
ledger { }) - Export consciously - Only expose what needs to be public
- 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:
- Your code compiles with compatible compilers
- Future breaking changes don’t silently break your contract
- 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(currently0.23.0with toolchain 0.31.1). A pragma whose range doesn’t include that version fails with alanguage version ... mismatcherror, 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 moduleimport "path/to/M";— import from a path- Optional prefix and type parameters
Module Contents
After importing CompactStandardLibrary, you have access to:
| Category | Items |
|---|---|
| Ledger ADTs | Counter, Map<K,V>, MerkleTree<N,T> |
| Optional Types | Maybe<T>, Either<L,R>, JubjubPoint, MerkleTreeDigest, etc. |
| Hash Functions | persistentHash<T>(), persistentCommit<T>(), transientHash<T>(), transientCommit<T>() |
| Helpers | some<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:
| Element | Convention |
|---|---|
| Comments | // single-line |
| Indentation | 4 spaces |
| Line length | < 100 characters |
| Blank lines | Between sections |
| Operators | Spaces 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:
| Type | Description | Example |
|---|---|---|
Boolean | True/false values | true, false |
Field | Elements of the proving system’s scalar field | Used 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) n | Uint<0..101> for percentages 0-100 |
Bytes<n> | Fixed-length byte arrays | Bytes<32> for hashes or identifiers |
Opaque<s> | Opaque values tagged by a string literal ("string", "Uint8Array") | Opaque<"string"> |
Note:
Uint<n>andUint<0..n>are the same type family.Uint<n>is exactly equivalent toUint<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 Case | Recommended Type |
|---|---|
| Amounts, counts | Uint<64> |
| Cryptographic operations | Field |
| Hashing inputs | Often Field or Bytes |
| General arithmetic | Uint<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
| Type | Bits | Range | Use Case (guidance) |
|---|---|---|---|
Uint<8> | 8 | 0 to 255 | Small counters, enum-like values |
Uint<16> | 16 | 0 to 65,535 | Medium values |
Uint<32> | 32 | 0 to ~4 billion | Timestamps, IDs |
Uint<64> | 64 | 0 to ~18 quintillion | Balances, amounts |
Uint<128> | 128 | Very large | High-precision values |
Uint<248> | 248 | Maximum width | Largest 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 (includingUint<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:
| Operator | Name |
|---|---|
== | 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 Case | Recommended Form | Example |
|---|---|---|
| Standard storage (balances, IDs) | Sized Uint<n> | Uint<64> |
| Percentage or constrained value | Bounded Uint<0..n> | Uint<0..101> |
| Matching specific bit-width | Sized Uint<n> | Uint<128> |
| Domain-specific limits | Bounded 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
stringorUint8Array, 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)
| Type | Use 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>eare 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 → TO | Field | Uint<0..n> | Boolean | Bytes<n> |
|---|---|---|---|---|
Field | static | checked | 1 | 2 |
Uint<0..m> | static | 3 | conversion | 7 |
enum type | conversion | conversion | no | no |
Boolean | conversion | 4 | — | no |
Bytes<m> | 5 | 9 | no | 6 |
Note: Numbers in the table refer to the notes below.
nomeans the cast is not allowed.
Important Notes
-
Field → Boolean: Conversion.
0converts tofalse, all other values totrue. -
Field → Bytes<n>: Conversion to little-endian bytes. Runtime error if value doesn’t fit in n bytes.
-
Uint<0..m> → Uint<0..n>: Static if
m ≤ n, otherwise checked (fails at runtime if value > n). -
Boolean → Uint<0..n>: Conversion.
false→ 0,true→ 1. Ifnis 0, checked (fails ontrue). -
Bytes<m> → Field: Conversion from little-endian bytes. Runtime error if result exceeds max Field value.
-
Bytes<m> → Bytes<n>: Only allowed if
m == n(static cast). -
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).
-
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). -
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 → Fieldis a conversion cast per the official table (verified with toolchain 0.31.1):false→ 0,true→ 1. Going throughUintfirst 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
- Understand cast types - Know whether a cast is static, conversion, or checked
- Check ranges - Checked casts can fail at runtime
- Use Field as intermediate - When direct cast isn’t allowed
- 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 (
structandenum) are covered in Chapter 5: User-Defined Types.
Overview
| Type | Description | Example |
|---|---|---|
[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 array | Bytes<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 typeVector<n, S>when a least upper boundSexists 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 lengthnifTis a subtype of eachS
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_someisfalse, thevaluefield contains a default value (0 for numeric types). Accessing it without checkingis_somewon’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:
fromis a reserved keyword in Compact and cannot be used as a parameter name — hencesenderabove.
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 tosome<Uint<64>>().
Default Values
The default value of Maybe<T> is equivalent to none<T>():
is_some=falsevalue= default value ofT
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 onis_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=falseleft= default value ofAright= default value ofB
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
| Aspect | Maybe<T> | Either<A, B> |
|---|---|---|
| Purpose | Optional value | One of two types |
| Indicator | is_some: Boolean | is_left: Boolean |
| Value fields | 1 (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
| Declaration | Purpose | Example |
|---|---|---|
struct Name { ... } | Record with named fields | struct Point { x: Field, y: Field } |
enum Name { ... } | Enumeration with variants | enum 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-styleGameState::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 Case | Recommended Type |
|---|---|
| Group related fields | struct |
| Finite set of states | enum |
| Reusable container | Generic 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:
- Define business logic - What the contract can do and how state changes
- Generate ZK proofs - Compiled into circuits that underpin proof generation and verification
- Are callable from TypeScript - Exposed via the generated
Contractclass andCircuits/PureCircuitstypes when markedexport
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:
| Part | Meaning |
|---|---|
export | Makes the circuit publicly callable |
circuit | Keyword declaring a circuit |
name | The 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>(requiresimport 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:
- The body contains a ledger operation
- The body calls any impure circuit
- 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 asamount * 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
- Verification - Compiler enforces purity constraints
- Reusability - Share logic across circuits
- Off-chain use - Available via
pureCircuitsin TypeScript - 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
- Compact declares the witness signature
- TypeScript implements the witness function
- At proof time, the witness provides private data
- The proof verifies computations on that data
- 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:
- Interact from a different client - Not using your DApp at all
- Provide a modified witness implementation - Returns incorrect values
- 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?
| Parameters | Witnesses |
|---|---|
| Visible on-chain | Private to prover |
| Anyone can provide | Only prover provides |
| Part of transaction | Part 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
.compactsyntax. 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 stateprivateState- Your private statecontractAddress- 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
- First argument is
WitnessContext<Ledger, PrivateState> - Return
[newPrivateState, result] - 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:
- Calls the witness function when Compact code invokes it
- Passes current
privateStateviacontext.privateState - Receives
[updatedPrivateState, value]back - 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...elsestatementsforloops (bounded)returnassert- blocks
{ ... } constbindings- conditional expressions
e0 ? e1 : e2
Source: Statements
In this chapter:
- Conditionals -
ifstatements and ternary expressions - Loops - bounded
forloops
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
e0must beBooleane1ande2must 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 - bounded
forloops - Assertions - enforce conditions in circuits
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 toBoolean- 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:
- Runtime guard - if false, the transaction aborts with the error message
- 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:
| Category | Examples |
|---|---|
| Literals | 42, true, "hello" |
| Arithmetic | a + b, x - y, n * m |
| Comparison | a == b, x < y, n >= m |
| Logical | a && b, x || y |
| Conditional | cond ? a : b |
| Type Cast | value 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 - Addition, subtraction, multiplication and type widening
- Comparison & Boolean - Relational operators and logical expressions
- Type Conversions - The
ascast operator and its rules
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 shorthandUint<64>, which is equivalent toUint<0..2^64>— the upper bound of the bounded form is exclusive (verified with toolchain 0.31.1).
Available Operators
| Operator | Name | Result Type (for Uint<0..m> and Uint<0..n>, exclusive bounds) |
|---|---|---|
+ | Addition | Uint<0..m+n-1> (max value (m-1)+(n-1)) |
- | Subtraction | Uint<0..m> (left operand’s bound) |
* | Multiplication | Uint<0..(m-1)*(n-1)+1> (max value (m-1)*(n-1)) |
Verified with toolchain 0.31.1:
Uint<0..10> + Uint<0..20>infersUint<0..29>, andUint<0..10> * Uint<0..20>infersUint<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 isUint<0..m+n-1>(covers max value(m-1)+(n-1)) - For
Uint<0..m> * Uint<0..n>, the result type isUint<0..(m-1)*(n-1)+1>(covers max value(m-1)*(n-1)) - For
Uint<0..m> - Uint<0..n>, the result type isUint<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
- No division operator - Compact doesn’t support
/ - Result types are computed from bounds -
+and*produce wider types; casts may be needed depending on usage - Field arithmetic wraps - Overflow/underflow wrap modulo the field modulus
- Subtraction can underflow - If
b > aat runtime,a - braises a runtime error (no wraparound forUint)
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 shorthandUint<64>, which is equivalent toUint<0..2^64>— the upper bound of the bounded form is exclusive (verified with toolchain 0.31.1).
Relational Operators
| Operator | Name | Returns |
|---|---|---|
== | Equals | Boolean |
!= | Not equals | Boolean |
< | Less than | Boolean |
> | Greater than | Boolean |
<= | Less than or equals | Boolean |
>= | Greater than or equals | Boolean |
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
| Operator | Name | Behavior |
|---|---|---|
&& | Logical AND | Short-circuit: stops if left is false |
|| | Logical OR | Short-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- Ifaisfalse,bis not evaluateda || b- Ifaistrue,bis 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
- All comparisons return
Boolean - Ordering operators (
<,>,<=,>=) only work on unsigned integers - Logical operators short-circuit - right side may not execute
- 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 shorthandUint<64>, which is equivalent toUint<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:
| Category | Description | Runtime Effect |
|---|---|---|
| Static | Changes only the static type | None |
| Conversion | Converts between representations | Always succeeds |
| Checked | Validates at runtime | May fail |
Allowed Casts Reference
The following table summarizes the allowed casts between types:
| From | To | Behavior |
|---|---|---|
Uint<0..m> | Uint<0..n> where m ≤ n | Static |
Uint<0..m> | Uint<0..n> where m > n | Checked (fails if value > n) |
Uint<0..m> | Field | Static |
Field | Uint<0..n> | Checked |
Field | Boolean | Conversion (0→false, others→true) |
Field | Bytes<n> | Conversion (runtime error if doesn’t fit) |
Bytes<m> | Field | Conversion (LSB first, checked if exceeds max Field) |
Bytes<m> | Bytes<n> where m == n | Static |
enum type | Field | Conversion |
enum type | Uint<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 type | Checked (fails at runtime past the last variant) |
Not allowed:
Bytes<m>→Bytes<n>wherem ≠ 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 castField→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 ifm == 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
- Use
asfor all casts — TypeScript-style<T>is not supported - Upcasts are always safe — Casting to a supertype never fails
Uint<0..m>→Uint<0..n>— Static ifm ≤ n, checked otherwiseUint→Field— Static castField→Uint— Checked cast (may fail)enum→Uint— Conversion (variant index); integer → enum is checked- 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:
| Component | Version | Purpose |
|---|---|---|
Compact devtools (compact) | 0.5.1 | Installs compilers, compiles contracts |
Compact toolchain (compact compile) | 0.31.1 | Contract compiler for Midnight |
| Compact runtime | 0.16.0 | Runtime library for contracts |
| Compact JS | 2.5.1 | TypeScript execution environment for compiled contracts |
| Platform JS | 2.2.4 | Core abstractions and types |
| On-chain runtime | 3.0.0 | On-chain runtime support (v3) |
SDKs & APIs
Also the same across all three networks:
| Component | Version | Purpose |
|---|---|---|
| Wallet SDK | 1.2.0 | SDK for building wallet integrations |
| Midnight.js | 4.1.1 | DApp framework: contracts, types, providers |
| testkit-js | 4.1.1 | E2E testing framework |
| DApp Connector API | 4.0.1 | Interface between DApps and wallets |
Network & Infrastructure
These components differ by network:
| Component | Preview | Preprod | Mainnet | Notes |
|---|---|---|---|---|
| Node (Midnight) | 1.0.0 | 1.0.0 | 0.22.5 | Network node |
| Midnight Indexer | 4.3.3 | 4.3.3 | 4.0.1 | Blockchain indexer |
| Proof server | 8.1.0 | 8.1.0 | 8.0.3 | ZKP 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.