zk-proofs について
Stellar のスマートコントラクトでゼロ知識検証を実装する開発者向けのスキルです。プロトコルと SDK の要件に沿って暗号プリミティブや証明方式を整理し、実装手順とセキュリティ上の注意点を示します。
主な機能
- 01Stellar の ZK 機能とバージョン確認
- 02Groth16 検証器の実装ガイド
- 03Circom、Noir、RISC Zero の手順
- 04BN254、BLS12-381、Poseidon の解説
- 05検証器の構成とセキュリティ上の注意
活用シーン
- 01証明プリミティブ選定前のネットワーク対応確認
- 02Circom の証明を Stellar コントラクトに接続
- 03Noir UltraHonk の検証条件の評価
- 04プライバシープールとリプレイ対策の設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ stellar/ stellar-dev-skill --skill zk-proofsインストール後、Claude Code で/zk-proofsを入力して読み込まれたか確認します。
詳細
ゼロ知識証明とプライバシー
Stellar での ZK 検証です。利用できる機能はプロトコルと SDK のバージョンに依存します。プリミティブに依存する前に、必ず CAP のステータス、ネットワークのバージョン、soroban-sdk のホスト関数サポートを確認してください。
Protocol 29 に対する最終確認(メインネット、2026 年 10 月)。CAP-0074/0075 は Protocol 25「X-Ray」で、CAP-0080(BN254 MSM、Fr 演算、曲線上の点のチェック)は Protocol 26 で導入されました。BN254 や Poseidon が「提案中」と記載されたドキュメントは、それ以前のものです。
このスキルを使う場面
- Stellar スマートコントラクトとして Groth16(またはその他の SNARK)検証器を実装する
- Circom、Noir、RISC Zero の出力をオンチェーン検証に接続する
- プライバシープール、機密トークン、またはマークルツリーを利用したコミットメントを構築する
- BN254 / Poseidon のホスト関数を使用する(Protocol 25 以降)
関連スキル
- コントラクトのパターンとデプロイ →
../smart-contracts/development.md - 検証器のセキュリティレビュー →
../smart-contracts/security.md - ここで参照する CAP →
../standards/SKILL.md
利用可能な機能 — 実装前に確認してください
| プリミティブ | CAP | ステータス |
|---|---|---|
| BLS12-381 演算(G1/G2 の加算、乗算、MSM、ペアリングチェック、ハッシュから曲線への写像、Fr 演算) | CAP-0059 | 利用可能(Protocol 22 以降) |
BN254 の基本演算(G1 の加算/乗算、ペアリングチェック — crypto::bn254) | CAP-0074 | 利用可能(Protocol 25 以降) |
BN254 G1 MSM、Fr 演算(fr_add/sub/mul/pow/inv)、曲線上の点のチェック | CAP-0080 | 利用可能(Protocol 26 以降) |
| Poseidon/Poseidon2 置換プリミティブ(完全なハッシュ関数ではありません) | CAP-0075 | 利用可能(Protocol 25 以降) |
実装前に、必ず次の項目を確認してください。
- プリアンブルに記載された CAP のステータス(ドラフトではなく
Accepted/Implementedか) - 対象ネットワークのプロトコルバージョン(ソフトウェアバージョン)
- 必要なホスト関数をサポートする
soroban-sdkのリリース
両方の曲線で Protocol 25 以降、基本ホスト関数を利用できます。曲線だけではすべてが決まりません。重要なのは証明システムです。Groth16 はどちらの曲線でも小さなコントラクトで検証できますが、UltraHonk には専用のコントラクト内検証器が必要です(実装例があります。Noir のチュートリアルを参照してください)。ただし、CAP-0080 のホスト関数に依存し、現行リリースは soroban-sdk 28 をベースに構築されているため、Protocol 28 以降が必要です。
| ツールチェーン | 証明システム | 曲線 | Stellar 上でのオンチェーン対応 |
|---|---|---|---|
Circom + snarkjs(-p bls12381) | Groth16 | BLS12-381 | ✅ CAP-0059 経由(Protocol 22 以降) |
Circom + snarkjs(デフォルトの bn128) | Groth16 | BN254 | ✅ CAP-0074 経由(Protocol 25 以降) |
| Noir + Barretenberg | UltraHonk | BN254 | ✅ CAP-0074 + CAP-0080 経由(現行の検証器リリースでは Protocol 28 以降) — rs-soroban-ultrahonk 検証器コントラクトが必要です。ZK ではない証明のみです(Noir のチュートリアルを参照) |
| RISC Zero(STARK → Groth16 ラップ) | Groth16 | BN254 | ✅ CAP-0074 経由(Protocol 25 以降) |
オンチェーン検証器(BLS12-381 上の Groth16)
公式の groth16_verifier の例が標準的な実装です。コントラクト全体を以下に示します。
#![no_std]
use soroban_sdk::{
contract, contracterror, contractimpl, contracttype,
crypto::bls12_381::{Fr, G1Affine, G2Affine},
vec, Env, Vec,
};
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Groth16Error {
MalformedVerifyingKey = 0,
}
#[derive(Clone)]
#[contracttype]
pub struct VerificationKey {
pub alpha: G1Affine,
pub beta: G2Affine,
pub gamma: G2Affine,
pub delta: G2Affine,
pub ic: Vec<G1Affine>,
}
#[derive(Clone)]
#[contracttype]
pub struct Proof {
pub a: G1Affine,
pub b: G2Affine,
pub c: G1Affine,
}
#[contract]
pub struct Groth16Verifier;
#[contractimpl]
impl Groth16Verifier {
pub fn verify_proof(
env: Env,
vk: VerificationKey,
proof: Proof,
pub_signals: Vec<Fr>,
) -> Result<bool, Groth16Error> {
let bls = env.crypto().bls12_381();
// vk_x = ic[0] + sum(pub_signals[i] * ic[i+1])
if pub_signals.len() + 1 != vk.ic.len() {
return Err(Groth16Error::MalformedVerifyingKey);
}
let mut vk_x = vk.ic.get(0).unwrap();
for (s, v) in pub_signals.iter().zip(vk.ic.iter().skip(1)) {
let prod = bls.g1_mul(&v, &s);
vk_x = bls.g1_add(&vk_x, &prod);
}
// e(-A, B) * e(alpha, beta) * e(vk_x, gamma) * e(C, delta) == 1
let neg_a = -proof.a;
let vp1 = vec![&env, neg_a, vk.alpha, vk_x, proof.c];
let vp2 = vec![&env, proof.b, vk.beta, vk.gamma, vk.delta];
Ok(bls.pairing_check(vp1, vp2))
}
}
点のエンコーディングは非圧縮のビッグエンディアンです。G1Affine は 96 バイト、G2Affine は 192 バイト、Fr は 32 バイトをラップします。この例のテストスイートには、arkworks の型(ark-bls12-381 + ark-serialize)からの正確な変換方法が示されています。証明生成ツールチェーンの JSON 出力からフィクスチャを作成する際に再利用してください。
本番環境では、この検証器をアプリケーションロジックでラップしてください。VerificationKey は呼び出し引数として受け取るのではなく、デプロイ時(コンストラクター)に固定し、証明をコンテキストにバインドします(注意点を参照)。
チュートリアル:Circom → オンチェーン検証
Circom はどちらの曲線向けにもコンパイルできます。このチュートリアルでは、上記の標準的な検証コントラクトに合わせて BLS12-381 を対象とします。Protocol 25 以降は、デフォルトの bn128(BN254)出力も crypto::bn254 経由で同様に検証できます。ただし、回路の曲線と検証コントラクトが一致していることを確認してください。
# 1. Circuit
cat > multiplier.circom <<'EOF'
pragma circom 2.1.6;
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a * b;
}
component main = Multiplier();
EOF
# 2. Compile for BLS12-381 to match the verifier contract above
# (the default bn128 also verifies on-chain since Protocol 25 — via a BN254 verifier)
circom multiplier.circom --r1cs --wasm -p bls12381
# 3. Trusted setup (powers of tau on bls12-381, then circuit-specific phase 2)
snarkjs powersoftau new bls12-381 12 pot12_0000.ptau
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="contrib" -e="random"
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau
snarkjs groth16 setup multiplier.r1cs pot12_final.ptau multiplier.zkey
snarkjs zkey export verificationkey multiplier.zkey verification_key.json
# 4. Witness + proof
echo '{"a": 3, "b": 11}' > input.json
node multiplier_js/generate_witness.js multiplier_js/multiplier.wasm input.json witness.wtns
snarkjs groth16 prove multiplier.zkey witness.wtns proof.json public.json
# 5. Sanity-check off-chain before going on-chain
snarkjs groth16 verify verification_key.json public.json proof.json
次に、proof.json / verification_key.json(10 進数文字列の座標)をコントラクトの型に変換します。各点を非圧縮のビッグエンディアン形式で 96/192 バイトのレイアウトにシリアライズします。たとえば、サンプルのテストと同様に arkworks を使います。そのうえで verify_proof を呼び出します。公開シグナル(public.json)は Vec<Fr> 引数になります。コントラクトでは、それらのシグナルが何を意味するかも検証する必要があります(落とし穴を参照)。
実際のアプリケーションでの証明ごとのフローは次のとおりです。クライアントがローカルで証明し(WASM 証明器またはネイティブ)、コントラクト呼び出しで (proof, public_signals) を送信し、コントラクトが検証し、ポリシーを適用して、状態を更新します。
ウォークスルー:Noir(UltraHonk、Protocol 28+ でオンチェーン検証可能)
Noir の標準バックエンド(Barretenberg)は、BN254 上で UltraHonk 証明を生成します。この曲線の基本演算は Protocol 25(CAP-0074)からネイティブでサポートされており、UltraHonk の検証は Nethermind の rs-soroban-ultrahonk 検証コントラクトを通じてオンチェーンで実行されます。このコントラクトのホスト呼び出しには、env.crypto().bn254() の g1_msm、Fr 算術演算(fr_add/sub/mul/pow/inv)、pairing_check が含まれます。MSM と Fr 関数は CAP-0080(Protocol 26)で導入されました。現行リリースのクレート ultrahonk_soroban_verifier 0.1.0 は soroban-sdk ^28 に依存しており、README には、これを使ってビルドしたコントラクトには Protocol 28+ が必要と記載されています。これは、OpenZeppelin の Confidential Tokens 開発者プレビュー が(テストネットで)動作するスタックです。OpenZeppelin は以前のコミット時点の検証コントラクトを監査しており(2026-09-18 の監査報告書、Critical/High/Medium の指摘なし、すべての指摘に対応済み)、リリースではそれ以降の SDK アップグレードが追加されているため、監査対象のリビジョンとリリースのリビジョンは異なります。メインネットで使用する前に、この差異と監査報告書を確認してください。
非 ZK のみです。 rs-soroban-ultrahonk 検証コントラクトは、Barretenberg の非ゼロ知識フレーバー(
UltraKeccakFlavor)を実装しています。README に記載されているように、証明には証人を秘匿する保証はありません。回路のプライベート入力は、パブリック入力でないというだけで秘密になるわけではありません。以下の例では、証明によってageが保護されることはありません。この検証コントラクトは、ある命題が成り立つことの証明に使い、データの秘匿には使わないでください。現在、オンチェーンで入力を秘匿するには、Circom などの Groth16 スタックを使用してください。ZK 検証はPR #38で進行中です。
検証コントラクトの README では、Keccak オラクルを使う場合に互換性のある証明ツールとして Nargo 1.0.0-beta.9 + Barretenberg 0.87.0 が記載されています(noirup -v 1.0.0-beta.9、bbup -v 0.87.0)。bb のデフォルトは Poseidon2 オラクルですが、この検証コントラクトでは拒否されるため、すべての bb コマンドに --oracle_hash keccak を渡してください。--zk は渡さないでください。これは ZK フレーバーを選択しますが、検証コントラクトではサポートされていません。0.87.0 を使い続けてください。bb 1.0.0 以降では、デフォルトが ZK フレーバーになり、--zk は --disable_zk に置き換わるため、以下のコマンドでは検証コントラクトに拒否される証明が生成されます。
# Local proving workflow (Nargo 1.0.0-beta.9, Barretenberg 0.87.0)
nargo new age_check && cd age_check
cat > src/main.nr <<'EOF'
fn main(age: u64, threshold: pub u64) {
assert(age >= threshold);
}
EOF
cat > Prover.toml <<'EOF'
age = "30"
threshold = "18"
EOF
nargo execute witness # compiles target/age_check.json, writes target/witness.gz
# -o is an output directory: prove writes target/proof + target/public_inputs, write_vk writes target/vk
bb prove --scheme ultra_honk --oracle_hash keccak -b target/age_check.json -w target/witness.gz -o target
bb write_vk --scheme ultra_honk --oracle_hash keccak -b target/age_check.json -o target
bb verify --scheme ultra_honk --oracle_hash keccak -k target/vk -p target/proof -i target/public_inputs # off-chain check
Noir の命題をオンチェーンにする方法は、優先度順に 3 つあります。
- オンチェーン UltraHonk 検証(現行の検証コントラクトのリリースでは Protocol 28+、CAP-0074 + CAP-0080):rs-soroban-ultrahonk ベースの検証コントラクトをデプロイするか呼び出し、
(proof, public inputs)を直接送信します。トラストレスで、オラクルは不要です。 - オンチェーン部分の証明スタックを切り替える:オンチェーンで重要な命題を Circom/Groth16 回路として表現し(上記のウォークスルーを参照)、オフチェーンのコンポーネントには Noir を使います。ツールチェーンが 2 つ必要になりますが、検証コントラクトがより小さく、実績もあります。
- アテステーションオラクル(古いプロトコルを対象とする場合、または検証コントラクトの監査に懸念がある場合のフォールバック):サービスがオフチェーンで
bb verifyを実行し、署名付きアテステーションを送信します。コントラクトはアテスターにrequire_auth()を要求し、ポリシーを適用します。信頼の前提(アテスター)を明示し、文書化する必要があります。これはトラストレスな ZK ではなく、検証可能計算のオラクルです。
ウォークスルー:RISC Zero(Groth16 ラップ、Protocol 25 以降はオンチェーンで検証可能)
RISC Zero は任意の Rust 実行(zkVM)を証明し、その STARK レシートを BN254 上の Groth16 証明にラップできます(「STARK から SNARK への変換」)。これにより、BN254 がサポートされている環境ではオンチェーン検証に十分なサイズになります。
// Guest (runs inside the zkVM): the computation being proven
use risc0_zkvm::guest::env;
fn main() {
let input: u64 = env::read();
let result = expensive_check(input);
env::commit(&result); // becomes part of the public journal
}
// Host: produce and verify a receipt locally
let receipt = prover.prove(env, ELF)?.receipt;
receipt.verify(IMAGE_ID)?; // off-chain verification
Protocol 25(CAP-0074)以降、Groth16 でラップされたレシートはネイティブにオンチェーンで検証できます。これは、上記の BLS12-381 用のものに対応する BN254 検証コントラクトです。その公開入力は、IMAGE_ID とジャーナルダイジェストを直接指定するものではありません。RISC Zero の Groth16 検証器は、制御ルート(2 つに分割)、レシートクレームダイジェスト(こちらも 2 つに分割)、および BN254 制御 ID をエンコードした 5 つのフィールド要素を受け取ります。ペアリングチェックの前に、オフチェーン(またはコントラクトコード内)でレシートクレームを再構築する際、イメージ ID とジャーナルが入力になります。これらを誤ってバインドすると、有効なシールが拒否されたり、誤ったステートメントが証明されたりします。ラップのワークフローとクレームダイジェストの構築については、RISC Zero のドキュメントを参照してください。STARK のみのレシート(Groth16 ラップなし)や Protocol 25 より前の環境では、アテステーションパターンが引き続きフォールバックになります。
アーキテクチャパターン
- 検証ゲートウェイ:暗号学的チェックを専用の検証コントラクト/モジュールに分離します。入力を正規化して検証し、成功/失敗を明示するイベントを発行します。監査対象が小さくなり、アップグレードも整理しやすくなります。
- ポリシーと証明の分離:
Verifier(暗号学的な有効性)→Policy(ビジネス/コンプライアンスルール)→Application(状態遷移)。それぞれを独立してテストおよびアップグレードできます。 - 機能に基づく制御:必要なプリミティブが利用可能だと確認できた環境でのみ ZK フローを有効にし、決定論的なフォールバックを用意して、サポート対象のネットワーク/プロトコルの対応表を文書化します。
マークルツリーのコミットメント(プライバシープール、許可リスト)について:CAP-0075(Protocol 25 以降)で公開されるのは Poseidon/Poseidon2 の置換関数であり、そのまま使えるハッシュ関数ではありません。オンチェーンで Poseidon のマークルルートとコミットメントを再計算するには、ゲストコードでスポンジ/圧縮構成を構築し、回路で使用するものと同じフィールド、状態サイズ t、S ボックス次数 d、ラウンド数、MDS/対角行列、ラウンド定数を渡す必要があります。さらに、パディングとドメイン分離も一致させてください。パラメーターが一致しないと、回路のものとは異なるルートが気付かれないまま生成されます。また、場当たり的な構成は安全でない可能性があります。Protocol 25 より前の環境では、コントラクトが必要とするのはルートの比較のみとし、メンバーシップ証明は SNARK 内で検証するようにツリーを設計してください。
落とし穴
- 証明を検証しても、ステートメントを検証していない。 有効な証明が示すのは、何らかの証人が回路を満たすことだけです。コントラクトは公開入力の意味を検証する必要があります。この証明は誰のものか、どのマークルルートを対象としているか、どのアクションか、どの金額かを確認してください。
- リプレイ防止のバインドがない。 有効な証明はリプレイされる可能性があります。nonce/セッション/アクションを公開入力にバインドし、オンチェーンにリプレイ防止策(ヌリファイア集合)を永続化してください。
- 曲線と検証器の不一致。 証明の曲線は、検証コントラクトのホスト関数と一致している必要があります。標準的な検証器の例は BLS12-381 であるため、デフォルトの
bn128を使う Circom 証明はそれに対して失敗します(BN254 検証器に対しても同様に逆の不一致が起こります)。Protocol 25 以降はどちらの曲線も使えますが、回路と検証器で同じ曲線を使ってください。 - 証明がプライベート入力を隠すと思い込む。 プライベート入力を隠すのはゼロ知識証明だけです。オンチェーンの UltraHonk 検証器が受け入れるのは非 ZK 証明のみであるため、ZK 方式をサポートするまでは、プライバシーが重要な回路に使用しないでください。
- 信頼されたセットアップの衛生管理。 Groth16 には回路固有のフェーズ 2 セットアップが必要です。本番環境では、単独の貢献者による開発用セットアップではなく、実際のマルチパーティセレモニーを使用してください。
- プロトコルの前提をハードコードする。 機能の有無に基づいて制御し、ドラフト段階の CAP が対象ネットワークで有効だと決めつけないでください。
テスト
- ユニットテスト:入力ドメインの検証、リプレイ保護、イベントの正確性、不正な証明や改ざんされた証明の拒否(重要なのは異常系です)
- 統合テスト:ローカルネットワークに対する、証明生成 → 送信 → 検証 → 状態遷移の一連のフロー
- 運用テスト:シミュレーション(
--send=no)による、現実的なサイズの証明にかかるリソースコストの確認 — ペアリング検査は高コストなため、トランザクションごとの検証を実装する前に予算を確認してください
参考資料
- groth16_verifier の例 — 標準的な検証器と arkworks のテスト用フィクスチャ
- soroban-examples
- Stellar の ZK 証明 · Stellar のプライバシー — 公式ドキュメント
- rs-soroban-ultrahonk — UltraHonk/BN254 検証コントラクト
- BLS12-381 SDK ドキュメント · BN254 SDK ドキュメント
- Circom ドキュメント · snarkjs · Noir ドキュメント · RISC Zero ドキュメント