mcpField
C
compact

compact

github.com/kali-decoder開発18142 回インストール1 日前に更新
リポジトリ

compact について

Compact Language Skillは、Midnightのスマートコントラクト言語Compactを使う開発者向けのリファレンスと実践ガイドです。回路、公開台帳とプライベートなwitness、型、安全設計、コンパイルを解説します。

主な機能

  1. 01回路とwitnessのモデル解説
  2. 02台帳状態とdisclose()の説明
  3. 03データ型と標準ライブラリの紹介
  4. 04プライバシーと安全設計のパターン
  5. 05コントラクトのコンパイルと一般的なエラー

活用シーン

  1. 01Compactコントラクトの公開・非公開データの設計
  2. 02コミットメントやMerkle木によるプライバシー設計
  3. 03回路、witness、型の使い方の確認
  4. 04コントラクトのコンパイルと一般的なエラーの理解

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kali-decoder/midnight-skills --skill compact

インストール後、Claude Code で/compactを入力して読み込まれたか確認します。

詳細

Compact 言語スキル

Compact は Midnight のスマートコントラクト言語です。TypeScript のような構文ですが、ゼロ知識回路にコンパイルされます。暗号処理はすべてコンパイラーが行うため、ビジネスロジックを記述するだけで済みます。

主な参考資料:

  • compact-book.vercel.app — 構造化された、例を中心とするリファレンス(最も役立つ情報源です)
  • docs.midnight.network/compact — 公式仕様(2026 年時点で、テストとセキュリティのページは「近日公開」です)

1) 基本的な考え方

コードを書く前に押さえておくべき、譲れない 3 つの原則:

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.

2 つの世界:

世界場所誰が見られるか更新するもの
export ledgerすべてのネットワークノード全員ZK 証明
witness(プライベート)ユーザーのローカルストレージ所有者のみなし(ローカルに保持)

2) コントラクトの構造

Compact コントラクトには、必須の要素が 4 つと、任意の要素が 1 つあります。

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>);
}
要素キーワード用途
公開状態export ledgerオンチェーンで、誰でも読み取り可能
プライベート入力witnessDApp が提供するコールバック(本体は TypeScript で記述)
ロジックexport circuitZK 回路にコンパイルされる
初期化constructorデプロイ時に 1 回実行される
整理module / import名前空間とファイルの管理

3) データ型

プリミティブ型
型範囲/サイズ備考
Booleantrue / falsetruthy/falsy の型変換はありません
Field0 から素数体の位数まで== と != のみ使用可能です。<、> は使用できません
Uint<n>n ビット符号なし整数。オーバーフロー時は折り返しますUint<8> = 0..255
Uint<0..n>0 から n-1 までの範囲付き整数<、> をサポートします。キャストは実行時に失敗することがあります
Bytes<n>正確に n バイト文字列を固定長にするには pad(n, str) を使用します
Opaque<"string">外部の JS データ回路から見えるのはハッシュのみで、内容は見えません。オンチェーンでは秘匿されません

重要: Field は等価性チェックにのみ使用してください。順序比較(<、<=、>、>=)が必要な場合は、必ず Uint<0..n> を使用してください。

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

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

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

// Enum (first variant = default)
enum State { VACANT, OCCUPIED }
型エイリアス
type Hash = Bytes<32>;        // structural alias — fully interchangeable
new type UserId = Bytes<32>;  // nominal alias — requires explicit cast
デフォルト値

すべての型にはゼロ値のデフォルトがあります。false、0、ゼロバイト n 個、列挙型の最初のバリアントです。default<T>() で取得できます。


4) Ledger の状態(公開)と disclose()

Ledger の修飾子
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>;
disclose() のルール

データが witness から ledger への書き込み(または回路の戻り値)に流れる場合、disclose() でラップする必要があります。コンパイラーはすべての操作を通じて witness の汚染を追跡し、汚染されていない値の開示が ledger に到達するとエラーを出します。

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

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

disclose() は暗号化ではありません。コンパイル時のシグナルです。開示された値はオンチェーンで平文になります。

Ledger ADT の選び方
用途型
単一の変更可能な値ledger f: T(Cell)
単調増加するカウンターCounter(.increment(n) を使用)
メンバーシップの追跡(どの要素かを明らかにする)Set<T>
キーごとのストレージMap<K, V>
順序付きキューList<T>
匿名メンバーシップ証明(現在のルート)MerkleTree<n, T>
匿名メンバーシップ証明(過去のルート)HistoricMerkleTree<n, T>
ブロック時刻、トークン、コントラクトアドレスKernel

5) 回路と Witness

回路
// 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]);
}
  • 戻り値の型に [] を指定すると、戻り値のないプロシージャ形式になります
  • assert(condition, "message") = 実行時ガード。condition が false の場合、トランザクションは失敗します
  • witness の出力は信頼できないため、必ず assert で検証してください
Witness

witness はプライベート入力のコールバックを宣言します。シグネチャは Compact で、本体は TypeScript で記述します。

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

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

Witness のデータがチェーンに送られることはありません。オンチェーンに送られるのは、回路が正しく実行されたことを示す ZK 証明だけです。


6) 標準ライブラリ — 主な関数

import CompactStandardLibrary;
ハッシュ化
関数出力アップグレード後も保持される?witness による汚染?用途
transientHash<T>(v)Fieldいいえはい一時的な計算
transientCommit<T>(v, rand)Fieldいいえいいえdisclose() を使わない一時的な計算
persistentHash<T>(v)Bytes<32>はいはいオンチェーンのキー、ID
persistentCommit<T>(v, rand)Bytes<32>はいいいえオンチェーンで機密値を隠す

主な違い:

  • *Hash = 値のみのハッシュです。値の候補が少ない場合、総当たりで特定できます。
  • *Commit = 値とランダムな nonce のハッシュです。値の候補が少なくても、総当たりで特定するのは現実的ではありません。
  • Persistent = コントラクトのアップグレード後も保持されます。台帳への保存に使用してください。
  • Transient = 一時的なものです。アップグレード後は保持されません。
パディングと変換
pad(32, "midnight:domain:separator")   // Creates Bytes<32> from string
Merkle ツリー
// Check that a path validates to the current root
assert(
  items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
  "invalid merkle path"
);

// TypeScript: get the path
const path = context.ledger.items.findPathForLeaf(leaf);  // O(n) scan
const path = context.ledger.items.pathForLeaf(index);      // O(1) with index
Maybe / Option
export ledger message: Maybe<Opaque<"string">>;

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

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

7) セキュリティパターン

公式のセキュリティドキュメントは「近日公開」です。compact-book の以下のパターンを使用してください。

公開される情報(すべて公開されると想定してください)
操作明らかになる情報
ledger.insert(v)値 v
ledger.lookup(k)キー k と返される値
set.member(f(x))x ではなく f(x)
merkleTree.insert(v)v は明らかになりません
回路の引数すべて
witness の戻り値何もありません(ローカルにとどまります)
パターン 1:コミットメント(値ではなくハッシュを保存)
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 の再利用はプライバシーに壊滅的な影響を及ぼします。 同じ nonce と値を使った 2 つのコミットメントは、オンチェーンで同一になります。必ず新しい nonce を使用してください。

パターン 2:ハッシュベースの認証(ハッシュ化による ZK 署名)
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]
  );
}

ドメインセパレーターは非常に重要です。 同じ秘密鍵でも、用途が異なれば異なる公開鍵になります。異なるドメイン間で公開鍵を再利用しないでください。

パターン 3:匿名メンバーシップのための Merkle ツリー

Set<T> はどのコミットメントがチェックされたかを明らかにします。MerkleTree<n, T> では、証明された値が 何らかの値 であることだけが明らかになります。

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

深さの目安:レベルごとに回路の深さが約 1 増えます。一般的な深さは 16~20 です。必要最小限の深さを使用してください。

パターン 4:コミットメント/ヌリファイア(1 回限りの匿名トークン)
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]);
}

証明の検証に過去のルートが必要な場合(つまり、証明の計算後に項目が追加される可能性がある場合)に限り、HistoricMerkleTree を使用してください。それ以外の場合は、通常の MerkleTree を使用してください。


8) コントラクトの完全な例

最小限の掲示板(プライバシーなし)
pragma language_version >= 0.22;

export ledger message: Opaque<"string">;

export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}
所有権付きの掲示板(プライバシーパターン)
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]);
}

なぜシーケンスを使うのですか? リプレイ攻撃を防ぎます。同じ秘密鍵でも、ラウンドごとに異なる公開鍵が生成されます。

NFT コントラクト(コミットメントベースの所有権)
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) コンパイルとツール

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

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

TypeScript バインディングも生成されます。管理対象の出力フォルダーから Contract オブジェクトをインポートしてください。

プレビューでの回路サイズ: ProofStation には最小回路サイズ(k ≥ 6)が必要です。エラー prove: no SRS params for k=6 は、回路が小さすぎることを意味します。ダミーの台帳フィールドを追加してパディングし、サイズを増やしてください。


10) よくある間違い(完全版)

開示に関する間違い:

  • disclose() を使わずに証人データを台帳に書き込む → コンパイルエラー(適切に早期検出されます)
  • disclose() を使わずに export circuit から証人データを返す → コンパイルエラー
  • disclose() がデータを暗号化すると考える → 暗号化されず、平文のままです

型に関する間違い:

  • <、> が必要な場面で Field を使う → 代わりに Uint<0..n> を使います
  • 実行時に as Uint<0..n> でキャストする → 値が範囲外の場合、失敗することがあります(コンパイルエラーではなくトランザクションエラー)
  • type Hash = Bytes<32>(構造型で、相互に置き換え可能)と new type UserId = Bytes<32>(名目的型で、キャストが必要)を混同する

プライバシーに関する間違い:

  • 値の種類が少ない場合に persistentHash を使う(総当たりで推測可能) → 新しい nonce を使って persistentCommit を使います
  • nonce を再利用する → 同じ値のコミットメントは、オンチェーンで同一になります
  • 匿名性が必要な場面で Set<T> を使う → Set.member(v) は v を明らかにします。代わりに MerkleTree を使います
  • コミットメントと nullifier で同じドメインセパレーターを使う → 衝突の可能性があります
  • 台帳への保存に transientHash を使う → 一時的な値はコントラクトのアップグレード後に保持されません
  • ユーザーごとのデータを export ledger に入れる → プライバシー漏えいにつながります

台帳に関する間違い:

  • witness の出力に assert を付け忘れる → witness は信頼できないため、検証してください
  • コンストラクターで行うべきでないロジックを constructor に書く → コンストラクターは一度実行されると終了します
  • 不要に HistoricMerkleTree を使う → ストレージコストが増えるため、過去のルートが必要な場合を除き MerkleTree を使います
  • 内部ヘルパー回路に export を付ける → 不必要に公開範囲が広がります

11) キーワード早見表

キーワード用途
pragma language_version >= n最小言語バージョンを宣言します
import ModuleNameモジュールをインポートします
export ledger f: Tオンチェーンで公開される状態フィールド
sealed ledger f: T一度だけ書き込み可能な状態(コンストラクターのみ)
witness f(): Tプライベート入力のコールバック(本体は TypeScript に記述)
export circuit f(): T呼び出し可能な ZK 回路
pure circuit f(): T純粋関数(台帳へのアクセスなし)
constructor()デプロイ時に一度だけ実行されます
disclose(v)意図的な公開を明示します
assert(cond, msg)実行時のガード(false の場合、トランザクションは失敗します)
default<T>()型 T のゼロ値または空の値
pad(n, str)文字列から Bytes<n> を作成します
some<T>(v) / none<T>()Maybe/Option コンストラクター
new type A = B名目的型エイリアス
type A = B構造型エイリアス

コメント