Rand Protocol
ドキュメント
RISC-V プログラム

Rust プログラムをデプロイする

no_std の Rust クレートからデプロイ済みのコンフィデンシャルプログラムまでを、順を追って説明します。固定されたツールチェーンのインストール、guest の作成、rand-guest によるビルドと検査、エミュレータでの実行、デプロイ、そして呼び出しまでを扱います。

このページでは、小さな Rust プログラムをソースから Rand 上のデプロイ済みプログラムにするまでを扱います。以下の rand-guest コマンドはすべて実際に実行したもので、出力は要約せずそのまま貼り付けています。実行日は 2026-09-19、circuits のコミットは 7ef3220 です。現在のテストネットは chain 20 で、そのツールチェーンは chain 18 および 19 と同じく aeacf31 でビルドされています(fullnode v0.6.8 は v0.6.7 と同じコミットを固定しています)。ただし、出力はそちらでは再実行していません。マシン、システムコール、ファイル形式については RISC-V ISA で説明しています。

1. ツールチェーンのインストール

hc は正確なコンパイラに依存するため、ツールチェーンは固定されています。

ツールバージョン理由
Rust1.98.1ゲストと rand-guest 自体のコンパイルに使用
riscv32im-unknown-none-elf ターゲット1.98.1 用ゲストのターゲット
circuits のチェックアウトchain 18 から 20 では randprotocol/zkp-circuits の aeacf31rand-guest、guest-sdk、リンカスクリプトを含む
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

rand-guest には build、check、pack、run、info の 5 つのサブコマンドがあります。

コミットが重要なのは、hc がソースとツールチェーンの関数だからです。別のコミットの guest-sdk に対してゲストをビルドし直すと、hc が変わることがあります。7ef3220 から aeacf31 までの間に、rand-guest ディレクトリは変わっておらず、guest-sdk/src/lib.rs は変わっています(ステップ 2)。

2. ゲストを書く

ゲストは guest-sdk に依存する no_std、no_main のバイナリクレートです。ゲストはcircuits のチェックアウトの内側に置く必要があります。rand-guest build はゲストのディレクトリより上位に guest-sdk/ があるかどうかを調べてチェックアウトを見つけ、チェックアウトの外にあるゲストは受け付けません。

このゲストは、しきい値、件数、そしてその件数分の非公開の残高を読み込みます。残高の合計がしきい値を満たしていれば、1 と超過分を公開します。残高そのものは公開しません。

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    threshold/
      Cargo.toml
      src/main.rs

my-guests/threshold/Cargo.toml:

[package]
name = "threshold"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

空の [workspace] は、コミット済みの fib ゲストと同様に、このクレートを親のワークスペースに含めないためのものです。

my-guests/threshold/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, read_input, write_output};

/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let threshold = read_input(0);
    let n = read_input(1);
    let mut sum: u32 = 0;
    for i in 0..n {
        sum = sum.wrapping_add(read_input(2 + i));
    }
    if sum >= threshold {
        write_output(0, 1);
        write_output(1, sum - threshold);
    }
    halt();
}

guest-sdk は read_input、read_public、write_output、poseidon2、keccak、keccak256、sha256_compress、sha256、halt をエクスポートし、スタックポインタを設定して main を呼び出す _start を提供します。アロケータはなく、浮動小数点もありません。

aeacf31 では、これらのうち 3 つのシグネチャが 7ef3220 から変わり、新たに 1 つのエクスポートが加わっています。

関数7ef3220aeacf31
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lenなしfn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8]、システムコール 7

poseidon2_len はメッセージ長をスポンジに束縛するもので、安全な呼び出しです。aeacf31 の rand-guest check はいまだにシステムコール 0 から 6 しか列挙しないため、これを呼び出すゲストは、build が ecall with a7 = 7, which is no syscall the machine implements として拒否するイメージにビルドされます(zkp-circuits の issue #1)。これが修正されるまでは、ゲストは固定長でタグ付きのメッセージに対して unsafe { poseidon2(ptr, n) } でハッシュを計算します。このページのゲストは、この 4 つのどれも使っていません。

ゲストのディレクトリに関するルール:

  • ゲストのディレクトリ、その親、$CARGO_HOME のいずれにも .cargo/config や .cargo/config.toml を置かないでください。cargo がビルドにマージしてしまうため、build は該当するファイルをすべて挙げて拒否します。
  • 独自のコンパイラフラグは指定しないでください。 build がすべて設定します。リンカスクリプト、-unaligned-scalar-mem、そしてチェックアウトの場所にイメージが依存しないようにするパスの再マップです。
  • .ld ファイルは多くても 1 つまでです。 build はゲストのディレクトリにある 1 つの .ld を使い、なければ guest-sdk/guest.ld を使います。複数ある場合は --ld で指定してください。

3. ビルドとチェック

$ rand-guest build my-guests/threshold --out threshold.bin
   Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
  |
  = note: this feature is not stably supported; its behavior can change in the future

warning: `guest-sdk` (lib) generated 1 warning
   Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
    Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)

unaligned-scalar-mem の警告は想定どおりです。ツールチェーンは意図的にアラインされていないアクセスを無効にしています。

build は cargo +1.98.1 build --release --target riscv32im-unknown-none-elf でコンパイルし、その結果に対して check を実行して、イメージコンテナと .sha256 ピンを書き出します。check がプログラムを拒否した場合でも、build はイメージを書き出したうえで、ゼロ以外の終了コードで終了します。

フラグデフォルト意味
--out <path><dir>/image.binイメージの出力先
--max-words <n>4096check が基準にする上限。対象チェーンの上限を渡します(chain 20 では 65,535)
--ld <path>ディレクトリ内の唯一の .ld、なければ guest-sdk/guest.ldリンカスクリプト
--lang rust|crustコンパイラ

4. エミュレータで実行する

非公開入力は --input で 1 ワードずつ渡します。--tier t を付けると、その実行がティア t に収まるかどうかも報告されます。

$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)

残高の合計は 1,025 なので、プログラムは 1 と超過分 25 を公開します。残高が 400 と 250 の場合は合計が足りず、出力はすべてゼロになります。トラップが起きると trap: <error> と出力され、終了コード 2 で終了します。

5. イメージを調べる

$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits

hc は、呼び出しの証明が検証される際の基準です。プログラムIDは、デプロイと呼び出しに使うものです。

6. デプロイ

fullnode v0.3 以降の rand ウォレットを使ってください。それより古いウォレットはイメージコンテナを読み取れません。chain 14 以降は、すべてのバンドル証明がそのトランザクションのバインディングに対して検証されるため、chain 14 ビルド(b3c594c)より古いウォレットは、チェーンが拒否するバンドルを証明してしまいます。現行チェーンである chain 20 では、fullnode v0.6.8 以降の rand を使ってください(フリートは v0.6.8 を実行しており、Linux 向けにビルドした rand が GitHub のリリースに添付されています)。v0.6.7 のウォレットは chain 20 では送信できません。chain 20 のジェネシスは binding_domain: 1 を設定しており、すべての証明バインディングと署名済みメッセージにジェネシスハッシュが含まれますが、v0.6.7 のウォレットはチェーン ID だけをバインドするためです。ウォレットの同期、デプロイ、呼び出しには、公開エンドポイント https://rpc.randprotocol.org で足ります(セットアップ)。

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy は、プログラムID、ワード数、hc を出力します。続いて rand_estimateFee でノードにデプロイ手数料を問い合わせます。この呼び出しは、証明を行う前に、チェーンの上限を超えるプログラムを拒否します。その後、手数料を支払うゼロ価値の自己宛て転送バンドルを証明し、送信して、コミットを待ちます。このバンドルは、すべてのトランザクションが持つ 4 入力 4 出力の隠蔽資産バンドルです。chain 17 以降は 2 つの証明で構成され、ウォレットはそれぞれについて 1 行ずつ出力します。ティア 10 の支出承認(数秒、約 1.37 MB)と、ティア 14 のバンドル(ノート PC で約 2 分、約 1.5 MB)です。chain 15 では、約 100 秒、約 1.42 MB のティア 14 の証明が 1 つでした。匿名投票では、chain 18 でのデプロイがウォレットの出力どおりに示されています。

デプロイ手数料は 0.001 RAND + 0.0001 RAND × words で、chain 20 でもこれまでと同じです。ガスが課金されるのは呼び出しであり、デプロイではありません。

プログラムワード数デプロイ手数料
threshold400.005 RAND
デフォルトの上限のプログラム(chain 12)4,0960.4106 RAND
公開セグメントなしで chain 20 がデプロイできる最大のプログラム8,1800.819 RAND
65,535 ワード上限のプログラム(chain 13 から 15)65,5356.5545 RAND

threshold は 4,096 ワード未満なので、デフォルトの上限にも chain 20 の 65,535 にも収まります。4,096 ワードを超えるプログラムには、ジェネシスで max_program_words をそれ以上に設定しているチェーン(chain 20 など)が必要です(RISC-V ISA)。chain 20 には chain 19 と同様に、もう 1 つ下側の上限があります。ティア 14 の呼び出しでは保持できないプログラムのデプロイは拒否されます(ProgramUncallable)。その境界は、公開セグメントなしで 8,180 ワードを超えるプログラムで、公開セグメントがある場合はそれより小さくなります。rand program deploy --input <word> …(入力のない呼び出しなら --check-call)は、想定している呼び出しを先にエミュレータで実行し、ティア 14 以下に収まらなければデプロイを拒否します。

レコードを確認します。

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

その code_hash は、rand-guest が出力した hc を別の表記にしたものです。rand-guest はダイジェストの 8 ワードをそれぞれビッグエンディアンで出力しますが、ウォレット、rand program show、rand_getProgram は各ワードのバイトをリトルエンディアンで出力します。そのため a491df99…3665 は 99df91a4…707c と表示されます。16 進数 8 桁ずつの各グループが、バイト単位で逆順になります。プログラムIDの表記は、どちらでも同じです。

7. 呼び出す

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

ウォレットはコードを取得し、エミュレータで呼び出しを実行して、証明を始める前に gas: <exact> (declaring <limit>, tier <t>) と手数料を出力します。その後、入力トランスクリプトを封印し、実行をローカルで証明し、バンドルを通じて手数料を支払い、レシートを出力します。chain 20 では、これは 3 つの証明になります。呼び出し、ティア 10 の支出承認、ティア 14 のバンドルです。

chain 20 の手数料は、chain 15 でこの呼び出しが 0.002 RAND だった 0.001 + call_fee(tier) ではなくなりました。下限は BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉ です。すなわち、0.001 RAND に、宣言したガス上限にガス価格を掛けた額(ジェネシス時点で 100 ユニット、1 ユニットは 10⁻⁹ RAND)と、証明とエンベロープのバイト数にバイト価格を掛けた額(ジェネシス時点で 1 KiB あたり 800 ユニット)を加えたものです。chain 20 では、この 2 つの価格は 10,000 ユニットと 80,000 ユニットを超えて上がることはありません。正確なガス量は非公開のままです。既定では、ウォレットはガス量を 2^(tier − 2) の倍数に切り上げて宣言します。手数料は証明が存在する前に確定するため、証明はチェーンの上限である 4,194,304 バイトとして価格を計算します。さらに、価格 2 段階分の余裕を上乗せします。chain 18 で測定した 4 件の呼び出しは、1 件あたり 0.0057〜0.0075 RAND でした(サンプルプログラム)。この呼び出しは chain 18〜20 では行っていないため、数値は示していません。

チェーンが目にするのは、tier、宣言されたガス上限、H_IN、そして 8 個の出力ワード [1, 25, 0, 0, 0, 0, 0, 0] です。残高は一切目にしません。

プログラムでは、レシート、入力エンベロープ、rand open-call を扱っています。このような Call はステートレスです。chain 20 では、プログラムは Invoke を通じてセルとボールトを保持することもできます(プログラムの状態(RPL-2))。

他の人が hc を検証できるようにする

ビルドは再現可能です。build はコンパイラの環境(RUSTFLAGS、CARGO_* による上書き、RUSTC ラッパーなど)を除去し、cargo の設定ファイルを拒否し、チェックアウトのパスを /rand-circuits に置き換えます。同じソースを Rust 1.98.1 でビルドすれば、どのマシンでも同じ hc になります。デプロイした内容を他の人が確認できるようにするには、ゲストのソースと circuits のコミット(chain 18〜20 向けにビルドしたゲストでは aeacf31)を公開してください。誰でも再ビルドして、hc を rand_getProgram の code_hash と比較できます。チェーンが保存するのはワードのみで、ソースは保存しません。

トラブルシューティング

メッセージ原因
… is not inside a circuits checkout (no guest-sdk/ above it)ゲストのディレクトリをチェックアウトの内側に移してください
build が .cargo/config.toml を指摘する削除するか移動してください。除去することはできません
Cap を伴う REJECTEDプログラムが --max-words を超えています。対象チェーンの上限まで引き上げるか、プログラムを小さくしてください(opt-level = "s")
Syscall と a7 = 7 を伴う REJECTEDゲストが poseidon2_len を呼び出しています。aeacf31 の rand-guest check はまだ syscall 7 を認識しません(zkp-circuits issue #1)
Fence、Csr、Ebreak を伴う REJECTEDビルドが、このマシンにない命令を出力しました。拒否されるものを参照してください
run で trap: …アドレスのアラインメント違反や未実装のシステムコール番号など、実行時の障害です
run が、どの tier にも収まらないと表示する実行が 2²⁰ 行、または Poseidon2 の予算を超えています。証明できません

fullnode のコミット c0878c3f(チェーン 21)に対して 2026-10-12 に最終確認しました。また、circuits `7ef3220` (`rand-guest/README.md`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; nothing on this page was deployed or called on chain 21。 コードとこのページが食い違う場合は、コードが正です。お知らせください。