compact
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompact Language Skill
Compact语言技能
Compact is Midnight's smart contract language. It looks like TypeScript but compiles to zero-knowledge circuits. The compiler handles all cryptographic machinery — you write business logic.
Primary references:
- — structured example-driven reference (your best source)
compact-book.vercel.app - — official spec (testing/security pages are "coming soon" as of 2026)
docs.midnight.network/compact
Compact是Midnight的智能合约语言。它外观类似TypeScript,但会编译为零知识(ZK)电路。编译器会处理所有密码学机制——你只需编写业务逻辑。
主要参考资料:
- —— 结构化的示例驱动型参考资料(最佳来源)
compact-book.vercel.app - —— 官方规范(截至2026年,测试/安全页面“即将推出”)
docs.midnight.network/compact
1) The Mental Model
1) 心智模型
Three non-negotiables before writing anything:
1. A circuit is a constraint system, not a function.
It declares relationships that MUST hold. The proof proves they held.
2. The ledger is public. Everything in `export ledger` is plaintext on-chain.
Private data lives in witnesses. It never touches the chain.
3. `disclose()` is a compile-time annotation, not encryption.
It tells the compiler "I am intentionally making this public."
The compiler prevents accidental disclosure. No runtime cost.The two worlds:
| World | Where | Who sees it | Updated by |
|---|---|---|---|
| Every network node | Everyone | ZK proof |
| User's local storage | Only the owner | Never (stays local) |
编写代码前必须明确的三点:
1. 电路是约束系统,而非函数。
它声明必须成立的关系。证明用于验证这些关系确实成立。
2. 账本是公开的。`export ledger`中的所有内容都是链上明文。
私有数据存储在witness中,永远不会上链。
3. `disclose()`是编译时注解,而非加密。
它告知编译器“我有意将此内容公开”。
编译器会防止意外披露,无运行时成本。两个环境:
| 环境 | 位置 | 可见者 | 更新方式 |
|---|---|---|---|
| 每个网络节点 | 所有人 | ZK证明 |
| 用户本地存储 | 仅所有者 | 永不更新(始终保存在本地) |
2) Contract Structure
2) 合约结构
Every Compact contract has four mandatory pieces + one optional:
compact
pragma language_version >= 0.22; // 1. Language version
import CompactStandardLibrary; // optional, needed for hashing, Merkle, etc.
export ledger counter: Uint<64>; // 2. Public state (on-chain, readable by all)
witness callerSecret(): Bytes<32>; // 3. Private input (body lives in TypeScript)
constructor() { // 5. OPTIONAL: runs once on deployment
counter = 0;
}
export circuit increment(): [] { // 4. Logic (compiles to ZK circuit)
counter = disclose((counter + 1) as Uint<64>);
}| Piece | Keyword | Purpose |
|---|---|---|
| Public state | | On-chain, readable by all |
| Private inputs | | Callbacks the DApp provides (body in TypeScript) |
| Logic | | Compiles to ZK circuits |
| Initialization | | Runs once on deploy |
| Organization | | Namespace and file management |
每个Compact合约包含四个必填部分 + 一个可选部分:
compact
pragma language_version >= 0.22; // 1. 语言版本
import CompactStandardLibrary; // 可选,哈希、Merkle树等功能需要
export ledger counter: Uint<64>; // 2. 公开状态(链上,所有人可读)
witness callerSecret(): Bytes<32>; // 3. 私有输入(实现体在TypeScript中)
constructor() { // 5. 可选:部署时运行一次
counter = 0;
}
export circuit increment(): [] { // 4. 逻辑(编译为ZK电路)
counter = disclose((counter + 1) as Uint<64>);
}| 部分 | 关键字 | 用途 |
|---|---|---|
| 公开状态 | | 链上存储,所有人可读 |
| 私有输入 | | DApp提供的回调函数(实现体在TypeScript中) |
| 逻辑 | | 编译为ZK电路 |
| 初始化 | | 部署时运行一次 |
| 组织管理 | | 命名空间和文件管理 |
3) Data Types
3) 数据类型
Primitives
基本类型
| Type | Range/Size | Notes |
|---|---|---|
| | No truthy/falsy coercion |
| 0 to prime field order | Only |
| n-bit unsigned, wraps on overflow | |
| Bounded integer, 0 to n-1 | Supports |
| Exactly n bytes | Use |
| Foreign JS data | Circuits see only hash, not contents. Not hidden on-chain |
Critical: Use only for equality checks. Use whenever you need ordering (, , , ).
FieldUint<0..n><<=>>=| 类型 | 范围/大小 | 说明 |
|---|---|---|
| | 无真假值隐式转换 |
| 0到素域阶 | 仅支持 |
| n位无符号整数,溢出时循环 | |
| 有界整数,0到n-1 | 支持 |
| 恰好n字节 | 从字符串生成固定长度时使用 |
| 外部JS数据 | 电路仅能看到哈希值,无法查看内容。链上不会隐藏 |
关键提示: 仅在需要相等检查时使用。需要排序(、、、)时,务必使用。
Field<<=>>=Uint<0..n>Composite Types
复合类型
compact
// Tuple
const pair: [Field, Boolean] = [42, true];
const first = pair[0];
// Vector (homogeneous, fixed-length)
const v: Vector<4, Uint<8>> = [1, 2, 3, 4];
// Struct (nominal typing — shape isn't enough, name matters)
struct Point { x: Uint<32>, y: Uint<32> }
const p = Point { x: 10, y: 20 };
// Enum (first variant = default)
enum State { VACANT, OCCUPIED }compact
// 元组
const pair: [Field, Boolean] = [42, true];
const first = pair[0];
// 向量(同类型,固定长度)
const v: Vector<4, Uint<8>> = [1, 2, 3, 4];
// 结构体(标称类型 —— 仅形状不够,名称也很重要)
struct Point { x: Uint<32>, y: Uint<32> }
const p = Point { x: 10, y: 20 };
// 枚举(第一个变体为默认值)
enum State { VACANT, OCCUPIED }Type Aliases
类型别名
compact
type Hash = Bytes<32>; // structural alias — fully interchangeable
new type UserId = Bytes<32>; // nominal alias — requires explicit castcompact
type Hash = Bytes<32>; // 结构别名 —— 完全可互换
new type UserId = Bytes<32>; // 标称别名 —— 需要显式转换Default Values
默认值
Every type has a zero default: , , zero bytes, first enum variant. Access with .
false0ndefault<T>()每种类型都有零值默认值:、、n个零字节、枚举的第一个变体。使用获取。
false0default<T>()4) Ledger State (Public) and disclose()
disclose()4) 账本状态(公开)与disclose()
disclose()Ledger Modifiers
账本修饰符
compact
ledger val: Field; // private to contract, not exported
export ledger cnt: Counter; // readable from TypeScript/DApp
sealed ledger config: Uint<32>; // write-once (only in constructor)
export sealed ledger mapping: Map<Boolean, Field>;compact
ledger val: Field; // 合约私有,不对外导出
export ledger cnt: Counter; // 可从TypeScript/DApp读取
sealed ledger config: Uint<32>; // 仅可写入一次(仅在constructor中)
export sealed ledger mapping: Map<Boolean, Field>;The disclose()
Rule
disclose()disclose()
规则
disclose()If data flows from a to a ledger write (or circuit return), it must be wrapped in . The compiler tracks witness taint through every operation and will error if untainted disclosure reaches the ledger.
witnessdisclose()compact
// ❌ COMPILER ERROR — undisclosed witness data reaching ledger
export circuit record(): [] {
stored = getSecret();
}
// ✅ CORRECT — explicitly disclosed
export circuit record(): [] {
stored = disclose(getSecret());
}disclose()如果数据从流向账本写入(或电路返回值),则必须用包裹。编译器会跟踪每个操作中的witness污染,若未污染的披露数据到达账本,会抛出错误。
witnessdisclose()compact
// ❌ 编译错误 —— 未披露的witness数据流入账本
export circuit record(): [] {
stored = getSecret();
}
// ✅ 正确 —— 显式披露
export circuit record(): [] {
stored = disclose(getSecret());
}disclose()Ledger ADT Chooser
账本ADT选择器
| Use case | Type |
|---|---|
| Single mutable value | |
| Monotonically growing counter | |
| Membership tracking (reveals which) | |
| Per-key storage | |
| Ordered queue | |
| Anonymous membership proofs (current root) | |
| Anonymous membership proofs (past roots) | |
| Block time, tokens, contract address | |
| 使用场景 | 类型 |
|---|---|
| 单个可变值 | |
| 单调递增计数器 | |
| 成员跟踪(会暴露具体成员) | |
| 按键存储 | |
| 有序队列 | |
| 匿名成员证明(当前根) | |
| 匿名成员证明(历史根) | |
| 区块时间、代币、合约地址 | |
5) Circuits and Witnesses
5) 电路与Witness
Circuits
电路
compact
// Exported = callable from DApp
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}
// Pure = no ledger access, no side effects
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "domain:"), seq, sk]);
}
// Internal (no export keyword) — not callable from DApp
circuit hashTokenData(owner: Bytes<32>, meta: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, meta]);
}- as return type = no return value (procedure-style)
[] - = runtime guard, transaction fails if condition is false
assert(condition, "message") - Always validate witness outputs with — witnesses are untrusted
assert
compact
// 导出的电路 = 可从DApp调用
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}
// 纯电路 = 不访问账本,无副作用
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "domain:"), seq, sk]);
}
// 内部电路(无export关键字)—— 不可从DApp调用
circuit hashTokenData(owner: Bytes<32>, meta: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, meta]);
}- 作为返回类型 = 无返回值(过程式)
[] - = 运行时守卫,若条件为假则交易失败
assert(condition, "message") - 务必用验证witness输出 —— witness不可信
assert
Witnesses
Witness
A declares a private input callback. The signature is in Compact, the body is in TypeScript.
witnesscompact
// Compact side — declaration only
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;typescript
// TypeScript side — implementation
function callerAddress(context: WitnessContext): Uint8Array {
return context.coinPublicKey; // or wallet-derived address
}
function findMerklePath(
context: WitnessContext,
leaf: Uint8Array,
): MerkleTreePath<Uint8Array> {
return context.ledger.items.findPathForLeaf(leaf)!;
}Witness data never touches the chain. Only the ZK proof that the circuit ran correctly goes on-chain.
witnesscompact
// Compact侧 —— 仅声明
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;typescript
// TypeScript侧 —— 实现
function callerAddress(context: WitnessContext): Uint8Array {
return context.coinPublicKey; // 或钱包派生地址
}
function findMerklePath(
context: WitnessContext,
leaf: Uint8Array,
): MerkleTreePath<Uint8Array> {
return context.ledger.items.findPathForLeaf(leaf)!;
}Witness数据永远不会上链。只有电路正确运行的ZK证明会上链。
6) Standard Library — Key Functions
6) 标准库 —— 核心函数
compact
import CompactStandardLibrary;compact
import CompactStandardLibrary;Hashing
哈希
| Function | Output | Survives upgrade? | Witness-tainted? | Use for |
|---|---|---|---|---|
| | No | Yes | Temporary computations |
| | No | No | Temp computation without |
| | Yes | Yes | Keys, IDs on-chain |
| | Yes | No | Hiding sensitive values on-chain |
Key distinction:
- = hash of value only. Brute-forcible for small value spaces.
*Hash - = hash of value + random nonce. Infeasible to brute-force even for small values.
*Commit - Persistent = survives contract upgrades. Use for ledger storage.
- Transient = temporary. Does NOT survive upgrades.
| 函数 | 输出 | 合约升级后是否保留? | 是否受witness污染? | 用途 |
|---|---|---|---|---|
| | 否 | 是 | 临时计算 |
| | 否 | 否 | 无需 |
| | 是 | 是 | 链上密钥、ID |
| | 是 | 否 | 在链上隐藏敏感值 |
核心区别:
- = 仅对值进行哈希。小值域内可被暴力破解。
*Hash - = 对值+随机一次性数进行哈希。即使是小值域也无法暴力破解。
*Commit - Persistent = 合约升级后仍保留。用于账本存储。
- Transient = 临时数据。合约升级后不会保留。
Padding and Conversion
填充与转换
compact
pad(32, "midnight:domain:separator") // Creates Bytes<32> from stringcompact
pad(32, "midnight:domain:separator") // 从字符串创建Bytes<32>Merkle Trees
Merkle树
compact
// Check that a path validates to the current root
assert(
items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
"invalid merkle path"
);
// TypeScript: get the path
const path = context.ledger.items.findPathForLeaf(leaf); // O(n) scan
const path = context.ledger.items.pathForLeaf(index); // O(1) with indexcompact
// 验证路径是否匹配当前根
assert(
items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
"invalid merkle path"
);
// TypeScript:获取路径
const path = context.ledger.items.findPathForLeaf(leaf); // O(n)扫描
const path = context.ledger.items.pathForLeaf(index); // 已知索引时O(1)Maybe / Option
Maybe / 可选类型
compact
export ledger message: Maybe<Opaque<"string">>;
message = some<Opaque<"string">>(newMessage); // has value
message = none<Opaque<"string">>(); // empty
const val = message.value; // access inner value (only when present)compact
export ledger message: Maybe<Opaque<"string">>;
message = some<Opaque<"string">>(newMessage); // 有值
message = none<Opaque<"string">>(); // 空值
const val = message.value; // 访问内部值(仅当存在时)7) Security Patterns
7) 安全模式
The official security docs are "coming soon." Use these patterns from the compact-book.
官方安全文档“即将推出”。使用compact-book中的以下模式。
What's Public (assume everything is)
哪些内容是公开的(假设所有内容都是)
| Operation | What it reveals |
|---|---|
| The value |
| Key |
| |
| Does NOT reveal |
| Circuit arguments | All of them |
| Nothing (stays local) |
| 操作 | 会暴露什么 |
|---|---|
| 值 |
| 键 |
| |
| 不会暴露 |
| 电路参数 | 所有参数 |
| 无(始终保存在本地) |
Pattern 1: Commitment (Store Hash, Not Value)
模式1:承诺(存储哈希,而非值)
compact
export ledger balanceCommitments: Map<Bytes<32>, Bytes<32>>;
export circuit commitBalance(value: Uint<64>): [] {
const nonce = freshNonce();
const commitment = persistentCommit<Uint<64>>(value, nonce);
balanceCommitments.insert(disclose(callerAddress()), disclose(commitment));
}Nonce reuse = privacy catastrophe. Two commitments with same nonce + value are identical on-chain. Always fresh nonce.
compact
export ledger balanceCommitments: Map<Bytes<32>, Bytes<32>>;
export circuit commitBalance(value: Uint<64>): [] {
const nonce = freshNonce();
const commitment = persistentCommit<Uint<64>>(value, nonce);
balanceCommitments.insert(disclose(callerAddress()), disclose(commitment));
}重复使用一次性数 = 隐私灾难。 相同一次性数+值的两个承诺在链上完全相同。务必使用全新的一次性数。
Pattern 2: Hash-Based Auth (ZK Signatures via Hashing)
模式2:基于哈希的认证(通过哈希实现ZK签名)
compact
witness secretKey(): Bytes<32>;
export ledger organizer: Bytes<32>;
constructor() {
organizer = disclose(publicKey(secretKey()));
}
export circuit adminAction(): [] {
assert(organizer == publicKey(secretKey()), "not authorized");
// ... do the thing
}
pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>(
[pad(32, "myapp:admin:v1"), sk]
);
}Domain separator is critical. Same secret key → different public keys for different purposes. Never reuse a public key across domains.
compact
witness secretKey(): Bytes<32>;
export ledger organizer: Bytes<32>;
constructor() {
organizer = disclose(publicKey(secretKey()));
}
export circuit adminAction(): [] {
assert(organizer == publicKey(secretKey()), "not authorized");
// ... 执行操作
}
pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>(
[pad(32, "myapp:admin:v1"), sk]
);
}域分隔符至关重要。 相同私钥在不同用途下会生成不同公钥。切勿跨域重用公钥。
Pattern 3: Merkle Tree for Anonymous Membership
模式3:用于匿名成员证明的Merkle树
Set<T>MerkleTree<n, T>compact
export ledger allowlist: MerkleTree<16, Bytes<32>>;
witness findPath(leaf: Bytes<32>): MerkleTreePath<16, Bytes<32>>;
export circuit addToAllowlist(commitment: Bytes<32>): [] {
allowlist.insert(disclose(commitment)); // insert still requires disclose
}
export circuit proveAllowlisted(): [] {
const sk = secretKey();
const leaf = publicKey(sk);
const path = findPath(leaf);
assert(
allowlist.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in allowlist"
);
// proof: caller is in the allowlist. Nothing else revealed.
}Depth guide: each level adds ~1 to circuit depth. 16–20 is typical. Use minimum depth needed.
Set<T>MerkleTree<n, T>compact
export ledger allowlist: MerkleTree<16, Bytes<32>>;
witness findPath(leaf: Bytes<32>): MerkleTreePath<16, Bytes<32>>;
export circuit addToAllowlist(commitment: Bytes<32>): [] {
allowlist.insert(disclose(commitment)); // 插入仍需使用disclose
}
export circuit proveAllowlisted(): [] {
const sk = secretKey();
const leaf = publicKey(sk);
const path = findPath(leaf);
assert(
allowlist.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in allowlist"
);
// 证明:调用者在白名单中。无其他信息暴露。
}深度指南:每增加一层,电路深度约增加1。通常使用16–20层。使用满足需求的最小深度即可。
Pattern 4: Commitment/Nullifier (Single-Use Anonymous Tokens)
模式4:承诺/Nullifier(一次性匿名代币)
compact
export ledger commitments: HistoricMerkleTree<16, Bytes<32>>;
export ledger nullifiers: Set<Bytes<32>>;
export circuit spend(): [] {
const sk = secretKey();
const commitment = makeCommitment(sk);
const path = findCommitmentPath(commitment);
// 1. Prove membership
assert(
commitments.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in commitments tree"
);
// 2. Prevent reuse
const nul = makeNullifier(sk);
assert(!nullifiers.member(nul), "already spent");
nullifiers.insert(disclose(nul));
}
// CRITICAL: commitment and nullifier MUST use different domain separators
pure circuit makeCommitment(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:commit:v1"), sk]);
}
pure circuit makeNullifier(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:nullifier:v1"), sk]);
}Use only when proofs must verify against past roots (i.e., items may be added after the proof was computed). Otherwise use plain .
HistoricMerkleTreeMerkleTreecompact
export ledger commitments: HistoricMerkleTree<16, Bytes<32>>;
export ledger nullifiers: Set<Bytes<32>>;
export circuit spend(): [] {
const sk = secretKey();
const commitment = makeCommitment(sk);
const path = findCommitmentPath(commitment);
// 1. 证明成员身份
assert(
commitments.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in commitments tree"
);
// 2. 防止重复使用
const nul = makeNullifier(sk);
assert(!nullifiers.member(nul), "already spent");
nullifiers.insert(disclose(nul));
}
// 关键:承诺和Nullifier必须使用不同的域分隔符
pure circuit makeCommitment(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:commit:v1"), sk]);
}
pure circuit makeNullifier(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:nullifier:v1"), sk]);
}仅当证明需要验证历史根(即证明生成后可能添加新项)时,才使用。否则使用普通。
HistoricMerkleTreeMerkleTree8) Complete Contract Examples
8) 完整合约示例
Minimal Bulletin Board (No Privacy)
极简公告板(无隐私保护)
compact
pragma language_version >= 0.22;
export ledger message: Opaque<"string">;
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}compact
pragma language_version >= 0.22;
export ledger message: Opaque<"string">;
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}Bulletin Board with Ownership (Privacy Pattern)
带所有权的公告板(隐私保护模式)
compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
export enum State { VACANT, OCCUPIED }
export ledger state: State;
export ledger message: Maybe<Opaque<"string">>;
export ledger sequence: Counter;
export ledger owner: Bytes<32>;
witness localSecretKey(): Bytes<32>;
constructor() {
state = State.VACANT;
message = none<Opaque<"string">>();
sequence.increment(1);
}
export circuit post(newMessage: Opaque<"string">): [] {
assert(state == State.VACANT, "Board occupied");
owner = disclose(publicKey(localSecretKey(), sequence as Field as Bytes<32>));
message = disclose(some<Opaque<"string">>(newMessage));
state = State.OCCUPIED;
}
export circuit takeDown(): Opaque<"string"> {
assert(state == State.OCCUPIED, "Board empty");
assert(owner == publicKey(localSecretKey(), sequence as Field as Bytes<32>), "Not owner");
const msg = message.value;
state = State.VACANT;
sequence.increment(1);
message = none<Opaque<"string">>();
return msg;
}
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "bboard:pk:"), seq, sk]);
}Why sequence? Prevents replay attacks — same secret key produces different public key each round.
compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
export enum State { VACANT, OCCUPIED }
export ledger state: State;
export ledger message: Maybe<Opaque<"string">>;
export ledger sequence: Counter;
export ledger owner: Bytes<32>;
witness localSecretKey(): Bytes<32>;
constructor() {
state = State.VACANT;
message = none<Opaque<"string">>();
sequence.increment(1);
}
export circuit post(newMessage: Opaque<"string">): [] {
assert(state == State.VACANT, "Board occupied");
owner = disclose(publicKey(localSecretKey(), sequence as Field as Bytes<32>));
message = disclose(some<Opaque<"string">>(newMessage));
state = State.OCCUPIED;
}
export circuit takeDown(): Opaque<"string"> {
assert(state == State.OCCUPIED, "Board empty");
assert(owner == publicKey(localSecretKey(), sequence as Field as Bytes<32>), "Not owner");
const msg = message.value;
state = State.VACANT;
sequence.increment(1);
message = none<Opaque<"string">>();
return msg;
}
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "bboard:pk:"), seq, sk]);
}为什么需要sequence? 防止重放攻击 —— 相同私钥每次循环生成不同的公钥。
NFT Contract (Commitment-Based Ownership)
NFT合约(基于承诺的所有权)
compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
export ledger totalSupply: Uint<64>;
export ledger nextTokenId: Uint<64>;
export ledger tokenCommitments: Map<Uint<64>, Bytes<32>>;
witness callerAddress(): Bytes<32>;
constructor() {
totalSupply = 0;
nextTokenId = 1;
}
circuit hashTokenData(owner: Bytes<32>, metaHash: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, metaHash]);
}
export circuit mint(metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const tokenId = nextTokenId;
const commitment = hashTokenData(caller, metaHash);
tokenCommitments.insert(tokenId, disclose(commitment));
totalSupply = disclose((totalSupply + 1) as Uint<64>);
nextTokenId = disclose((tokenId + 1) as Uint<64>);
}
export circuit transfer(tokenId: Uint<64>, newOwner: Bytes<32>, metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const expected = hashTokenData(caller, metaHash);
const pubId = disclose(tokenId);
assert(tokenCommitments.member(pubId), "Token does not exist");
assert(tokenCommitments.lookup(pubId) == expected, "Not the owner");
const nextCommitment = hashTokenData(disclose(newOwner), metaHash);
tokenCommitments.insert(pubId, disclose(nextCommitment));
}compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
export ledger totalSupply: Uint<64>;
export ledger nextTokenId: Uint<64>;
export ledger tokenCommitments: Map<Uint<64>, Bytes<32>>;
witness callerAddress(): Bytes<32>;
constructor() {
totalSupply = 0;
nextTokenId = 1;
}
circuit hashTokenData(owner: Bytes<32>, metaHash: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, metaHash]);
}
export circuit mint(metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const tokenId = nextTokenId;
const commitment = hashTokenData(caller, metaHash);
tokenCommitments.insert(tokenId, disclose(commitment));
totalSupply = disclose((totalSupply + 1) as Uint<64>);
nextTokenId = disclose((tokenId + 1) as Uint<64>);
}
export circuit transfer(tokenId: Uint<64>, newOwner: Bytes<32>, metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const expected = hashTokenData(caller, metaHash);
const pubId = disclose(tokenId);
assert(tokenCommitments.member(pubId), "Token does not exist");
assert(tokenCommitments.lookup(pubId) == expected, "Not the owner");
const nextCommitment = hashTokenData(disclose(newOwner), metaHash);
tokenCommitments.insert(pubId, disclose(nextCommitment));
}9) Compilation and Tooling
9) 编译与工具
bash
undefinedbash
undefinedCompile a contract
编译合约
compact compile contracts/your-contract.compact contracts/managed/your-contract
compact compile contracts/your-contract.compact contracts/managed/your-contract
Output structure
输出结构
contracts/managed/your-contract/
keys/
circuitName.prover # 2–10 MB
circuitName.verifier # ~2 KB
zkir/
circuitName.bzkir # 1–3 KB
TypeScript bindings are also generated — import the `Contract` object from the managed output folder.
**Circuit size on preview:** ProofStation requires minimum circuit size (`k ≥ 6`). Error: `prove: no SRS params for k=6` means the circuit is too small. Pad with additional dummy ledger fields to increase size.
---contracts/managed/your-contract/
keys/
circuitName.prover # 2–10 MB
circuitName.verifier # ~2 KB
zkir/
circuitName.bzkir # 1–3 KB
同时会生成TypeScript绑定 —— 从托管输出文件夹导入`Contract`对象即可使用。
**预览版电路大小限制:** ProofStation要求最小电路大小(`k ≥ 6`)。若出现错误`prove: no SRS params for k=6`,说明电路过小。可添加额外的虚拟账本字段来增大电路大小。
---10) Common Mistakes (The Full List)
10) 常见错误(完整列表)
Disclosure mistakes:
- Writing witness data to the ledger without → compiler error (good, caught early)
disclose() - Returning witness data from without
export circuit→ compiler errordisclose() - Thinking encrypts data → it doesn't, it's plaintext
disclose()
Type mistakes:
- Using when you need
Field,<→ use>insteadUint<0..n> - cast at runtime → can fail if value is out of range (transaction error, not compile error)
as Uint<0..n> - Confusing (structural, interchangeable) with
type Hash = Bytes<32>(nominal, requires cast)new type UserId = Bytes<32>
Privacy mistakes:
- Using for small value spaces (brute-forcible) → use
persistentHashwith fresh noncepersistentCommit - Reusing nonces → two same-value commitments are identical on-chain
- Using when you need anonymity →
Set<T>revealsSet.member(v). UsevinsteadMerkleTree - Same domain separator for commitment and nullifier → potential collision
- Using for ledger storage → transient values don't survive contract upgrades
transientHash - Putting per-user data in → privacy leak
export ledger
Ledger mistakes:
- Forgetting on witness outputs → witnesses are untrusted, validate them
assert - Writing logic in that should be in circuits → constructor runs once and is done
constructor - Using unnecessarily → adds storage cost; use
HistoricMerkleTreeunless past roots are neededMerkleTree - on internal helper circuits → exposes surface area unnecessarily
export
披露错误:
- 将witness数据写入账本时未使用→ 编译错误(很好,提前发现)
disclose() - 从返回witness数据时未使用
export circuit→ 编译错误disclose() - 认为会加密数据 → 不会,数据是明文
disclose()
类型错误:
- 需要、
<时使用>→ 改用FieldUint<0..n> - 运行时使用转换 → 若值超出范围会失败(交易错误,非编译错误)
as Uint<0..n> - 混淆(结构别名,可互换)与
type Hash = Bytes<32>(标称别名,需要显式转换)new type UserId = Bytes<32>
隐私错误:
- 对小值域使用(可被暴力破解)→ 使用带全新一次性数的
persistentHashpersistentCommit - 重复使用一次性数 → 相同值的两个承诺在链上完全相同
- 需要匿名性时使用→
Set<T>会暴露Set.member(v)。改用vMerkleTree - 承诺和Nullifier使用相同的域分隔符 → 可能发生碰撞
- 对账本存储使用→ 临时数据无法在合约升级后保留
transientHash - 将用户专属数据放入→ 隐私泄露
export ledger
账本错误:
- 忘记用验证witness输出 → witness不可信,必须验证
assert - 应在电路中编写的逻辑却放在中 →
constructor仅运行一次constructor - 不必要地使用→ 增加存储成本;除非需要历史根,否则使用
HistoricMerkleTreeMerkleTree - 对内部辅助电路使用→ 不必要地暴露调用接口
export
11) Keyword Quick Reference
11) 关键字速查
| Keyword | Purpose |
|---|---|
| Declare minimum language version |
| Import a module |
| Public on-chain state field |
| Write-once state (constructor only) |
| Private input callback (body in TypeScript) |
| Callable ZK circuit |
| Pure function (no ledger access) |
| Runs once on deployment |
| Mark intentional public disclosure |
| Runtime guard (tx fails if false) |
| Zero/empty value for type T |
| Create |
| Maybe/Option constructors |
| Nominal type alias |
| Structural type alias |
| 关键字 | 用途 |
|---|---|
| 声明最低语言版本 |
| 导入模块 |
| 链上公开状态字段 |
| 仅可写入一次的状态(仅在constructor中) |
| 私有输入回调函数(实现体在TypeScript中) |
| 可调用的ZK电路 |
| 纯函数(不访问账本) |
| 部署时运行一次 |
| 标记有意公开的内容 |
| 运行时守卫(条件为假则交易失败) |
| 类型T的零值/空值 |
| 从字符串创建 |
| Maybe/可选类型构造函数 |
| 标称类型别名 |
| 结构类型别名 |