このページでは、独自のプログラムを Rand に載せるために必要なことをすべて扱います。プログラムとして何を書けるか、何を読み取り何を公開するかをどう設計するか、そして Rust と C で一度ずつ書いた完全な例です。そのあと、テスト、証明の予算、デプロイ、呼び出し、そして誰でも結果を検証できる仕組みを説明します。以下の rand-guest の出力はすべて実際の実行結果で、表示されたとおりに貼り付けています。実行は 2026-09-25、circuits のコミット 7ef3220 で行いました。testnet は現在チェーン20で、そのツールチェーンはチェーン18・19と同じく aeacf31(ブランチ feat/v067-circuits。fullnode v0.6.8 も同じコミットに固定されています)でビルドされており、出力はそこでは再実行していません。SDK やチェーンで挙動が異なる箇所は、本文中に明記します。
短いチュートリアルが2本あり、近道になります。Rust プログラムのデプロイと RISC-V プログラムのデプロイです。このページは、Rust チュートリアルのステップ1を済ませていることを前提とします。つまり、aeacf31 の randprotocol/zkp-circuits のチェックアウト、riscv32im-unknown-none-elf ターゲットを備えた Rust 1.98.1、rand-guest バイナリ、そして C の場合は clang 23.1.1 と llvm-tools です。マシン自体のリファレンスは RISC-V ISA です。サンプルプログラムでは、ここから先の規模の大きいプログラムと、チェーン18のガス計測で変わった点を扱います。このページが扱うのは、Call がステートレスに実行する RPL-1 プログラムです。チェーン20には、Invoke が変更する RPL-2 のプログラム状態、セル、ボールトもあり(プログラム)、完全な例は randprotocol/example-programs にあります。
1. プログラムにできること
プログラムとは、次の形に収まる任意の計算です。
- 入力として 32ビットワード を読み取ります。
- 出力として 最大8個の32ビットワード を公開します。
- 決定的です。同じ入力からは常に同じ出力が得られます。
- そのティアの予算内で停止します(セクション7)。
この制限の中であれば、何でも書けます。ループ、再帰、構造体、配列、64ビット演算、クレート、そしてアロケータを持ち込めばヒープも使えます。ただしマシンは素の RV32IM で、その下にオペレーティングシステムはありません。
| 使えるもの | 使えないもの |
|---|---|
MUL/DIV/REM を含む RV32IM の整数命令 | 浮動小数点ハードウェア(Rust のソフトウェア浮動小数点は動作しますが、低速です) |
デフォルトのリンカスクリプトで 0x1000 から最大 1 MiB の RAM、64 KiB のスタック | ヒープ(アロケータを用意した場合を除く。セクション6) |
| システムコールとしての Poseidon2、Keccak-f、SHA-256 | ファイル、ネットワーク、時計、乱数、スレッド、std、libc |
no_std の Rust クレートと、自己完結した C | 呼び出しをまたいで残る状態 |
| 8個の公開出力ワード | プログラムの内部から RAND やトークンを移動する手段 |
プログラムを設計するとき、最も重要なのは最後の行です。出力はデータであり、作用ではありません。 呼び出しが証明するのは、特定のダイジェストを持つプログラムが、ある秘密の入力に対して実行され、8個のワードを公開した、ということです。チェーンは証明を検証し、その8個のワードをレシートに保存します。残高は変更されず、何も起動されません。ワードが何を意味するかは、プログラム、呼び出し元、そしてレシートを読む人の間の取り決めで決まります。チェーンがまだサポートしていないことは、プログラムに一覧があります。
2. 最初にインターフェースを設計する
プログラムには3つのチャネルがあります。コードを書く前に、それぞれに何を載せるかを決めてください。
| チャネル | 設定されるとき | 読み取り方法 | 見えるのは誰か | チェーン20での上限 |
|---|---|---|---|---|
| プライベート入力 | 呼び出しのたび | read_input(i) / rand_read_input(i) | 呼び出し元と、その呼び出しの鍵を渡された人のみ | 16,071ワード(封印された入力エンベロープの上限) |
| 公開セグメント | デプロイ時に一度だけ | read_public(i) / rand_read_public(i) | 全員。プログラム ID がこれを束縛する | 32,768ワード |
| 出力 | 実行ごと | write_output(slot, w) / rand_write_output | レシートを通じて全員 | 8ワード |
プライベート入力は、それだけでは世界について何も証明しない
プライベート入力は、ソルト付きコミットメント H_IN によって証明に束縛されるため、証明後に変更することはできません。しかしプログラムには、その入力が真であることを知る手段がありません。呼び出し元は任意の数を入力できます。プライベート入力に意味を持たせるには、プログラム内でそれを公開されている何かに結び付けます。
- 先に公開されたコミットメント(公開セグメント内、または検証者に帯域外で渡したもの)で、プライベート入力がそれを開示(オープン)しなければならないもの。後述のオークションがこの例です。
- プライベートなリーフがその下で証明されなければならない Merkle ルート(これに使うシステムコールが Poseidon2 です)。
- 検証者がすでに持っているダイジェストで、プログラムがそれを再計算して出力として公開するもの。
これは、機密プログラムを設計するうえでの中心的な技術です。証明が検証者に伝えるのは、何が計算されたかです。公開データへの束縛が伝えるのは、何に対して計算されたかです。
データをワードとして符号化する
すべては u32 として運ばれます。
| データ | 符号化 |
|---|---|
| バイト列 | 長さのワードに続けて、バイトを1ワードあたり4つ、リトルエンディアンで並べ、最後のワードはゼロで埋めます(ウォレットが ELF の公開入力に使う形式) |
u64 または金額 | 2ワード。下位、上位の順 |
| 符号付き整数 | 1ワードの2の補数 |
| 大きな結果 | その Poseidon2 ダイジェスト(8ワードで、出力すべてを使い切る)。または、いくつかの要約ワードと、より短いコミットメントを公開する |
コマンドラインツールは、入力ワードを10進数で受け取ります。rand-guest run --input と rand call --input は 0x… を受け付けません。rand program deploy --public に渡す公開セグメントのファイルは、10進数と 0x 付き16進数の両方を受け付けます。
出力の形を整える
- スロット0はステータスのワードにします。 失敗も含め、結果ごとに番号を割り当ててください。たとえば、オープンの不一致、項目数の超過、入力の範囲外などです。チェックに失敗した場合でも、ステータスを出して正常に停止させるべきです。そうすれば、呼び出し元は何がうまくいかなかったかを示す証明を得られます。
- 各スロットへの書き込みは最大1回です。 同じスロットへの2回目の書き込みはトラップです(エミュレータでは
DoubleWrite(0))。トラップした実行は証明できません。一度も書き込まなかったスロットは、ゼロとして読まれます。 - Rust では、スロット7を空けておきます。 パニックした Rust ゲストは、スロット7に
0xdeadbeef(3,735,928,559)を書き込んで停止します。パニック前に書き込んだ出力はそのまま残るため、読み手は他のスロットを信用する前にスロット7を確認しなければなりません(セクション6)。 - 与えられた入力の範囲を超えて読むとトラップです(エミュレータでは
InputIndex(5))。先に個数を読み取り、確認してください。
3. 実践例: 封印入札オークション
売り手が、落札できなかった入札額を公開せずに、第二価格オークションを行いたいとします。
- 各入札者は、入札額と128ビットのランダムなソルトを選び、売り手にコミットメントを渡します。これは
("BID1", 入札者のインデックス, 入札額, ソルト)に対する Poseidon2 です。 - 売り手は、最低落札価格とすべてのコミットメントを公開セグメントとして、オークションプログラムをデプロイします。これでプログラム ID は、このオークションだけを指すことになります。
- 各入札者は、入札額とソルトを売り手に秘密裏に送ります。
- 売り手は、すべての入札額とソルトをプライベート入力として、プログラムを呼び出します。プログラムは各オープンをコミットメントと照合し、落札者を決めて、
[status, winner, price, bidder count]を公開します。
チェーンが知るのは、誰がいくらで落札したかだけです。落札できなかった入札額は、一切見ません。売り手が入札額を1つでも改ざんすると、結果ではなく BAD_OPENING が返ります。コミットメントがプログラム ID に固定されているためです。
すべては circuits のチェックアウトの内側に置きます。rand-guest build は、ゲストのディレクトリから上方向にたどって guest-sdk を探すためです。
circuits/
guest-sdk/
rand-guest/
my-guests/
commit/ a Rust guest that computes one commitment
auction-core/ the auction rule as a plain no_std library, tested on the host
auction/ the Rust guest
auction-c/ the same guest in C
auction-c-test/ a host test for the C rule, outside the guest's directory
ハッシュ計算機としてのエミュレータ
入札者には、マシンが計算するのとまったく同じ、Goldilocks 体上の Poseidon2 が必要です。最も簡単で正確な道具は、それを計算する小さなゲストを作り、エミュレータで実行することです。ダイジェストは8ワードで、これは出力スロットの数とちょうど一致します。
my-guests/commit/Cargo.toml:
[package]
name = "commit"
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]
my-guests/commit/src/main.rs:
#![no_std]
#![no_main]
use guest_sdk::{halt, poseidon2, read_input, write_output};
/// Inputs: a bidder's index, bid and four salt words.
/// Outputs: the 8-word commitment Poseidon2("BID1", index, bid, salt[0..4]).
#[no_mangle]
pub extern "C" fn main() -> ! {
let mut buf = [0u32; 8];
buf[0] = 0x4249_4431; // "BID1"
for i in 0..6 {
buf[1 + i] = read_input(i as u32);
}
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
for (slot, word) in buf.iter().enumerate() {
write_output(slot as u32, *word);
}
halt();
}
poseidon2(ptr, n) は n ワードをその場でハッシュし、8ワードのダイジェストを ptr..ptr+8 に上書きします。そのため、ハッシュするのは7ワードだけですが、バッファは少なくとも8ワード分必要です。"BID1" タグは、これらのダイジェストを Poseidon2 の他の用途から区別します。
aeacf31 では poseidon2 は unsafe fn です。生ポインタを受け取るため、呼び出しは unsafe ブロック内に置きます。以下の出力を生成した 7ef3220 では安全な関数だったため、同じ行をブロックなしで書いていました。スポンジはパディングしないので、[a] と [a, 0] は同じハッシュになります。ここでは、すべてのメッセージが7ワードでタグから始まるため、問題ありません。長さが変わりうるメッセージについては、aeacf31 の SDK に長さを束縛する poseidon2_len(buf, n)(システムコール7)がありますが、そのコミットの rand-guest check はシステムコール7を拒否するため、そのようなゲストはまだビルドできません(zkp-circuits の issue #1)。代わりに、長さをメッセージの中に入れてください。
$ rand-guest build my-guests/commit --out commit.bin
76 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote commit.bin and its .sha256 (76 words, hc 41007891b5c393296fdd76539c93d0b06678bc8a0bd52cfc3b6886dcf0381343, program id 11dabdf740989d43599b296c43342bf3ce890e980f5cc558d15ba8ca9e8e9438)
$ rand-guest run commit.bin --input 0 --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
out[0] = 3502385358
out[1] = 1187922325
out[2] = 2500412136
out[3] = 2452821097
out[4] = 2045657999
out[5] = 1866863173
out[6] = 678782893
out[7] = 3524569252
cycles 77
tier 10
ここでも以降でも、ビルド出力からは cargo の行を省いています。cargo が出力する unaligned-scalar-mem の警告は想定どおりです(Rust プログラムのデプロイ)。
4人の入札者の入札額とソルトは、次のとおりです。
| 入札者 | 入札額 | ソルト(4ワード) |
|---|---|---|
| 0 | 400 | 2654435769 2135587861 4087136352 1559071514 |
| 1 | 250 | 461845907 3432918353 3864292196 2246822507 |
| 2 | 900 | 3266489909 668265263 374761393 3550635116 |
| 3 | 300 | 4252018373 3042594569 1759714724 625341585 |
入札者ごとに commit.bin を一度ずつ実行し、リザーブ価格(100)、件数(4)、4つのダイジェストを順に書き出すと、公開セグメント auction-public.txt が得られます。これは34ワードです。
100 4 3502385358 1187922325 2500412136 2452821097 2045657999 1866863173 678782893 3524569252 3413403069 1515911705 3633767467 1218237979 474331206 896634170 2889783686 541804293 3111308722 99534202 1917186958 1017842926 3082732585 4215265588 467906960 2929273893 588931287 1295911076 647129560 1384102451 625140897 4060943258 2970944086 698558155
実際のオークションでは、各入札者が自分のマシンで commit.bin を実行し、ソルトは本物の乱数源から取得します。
4. Rust でのプログラム
ルールをテスト可能なライブラリとして書く
syscall を必要としないロジックは、専用の no_std クレートに分けておきます。guest-sdk は riscv32 以外のターゲットではビルドを拒否するため、guest クレートでは手元のノートPCで cargo test を実行できません。guest-sdk に依存しないライブラリクレートであれば実行できます。
my-guests/auction-core/Cargo.toml:
[package]
name = "auction-core"
version = "0.1.0"
edition = "2021"
[workspace]
my-guests/auction-core/src/lib.rs:
//! The auction's rule, with no syscalls, so it builds and tests on the host.
#![no_std]
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
/// The highest bidder wins and pays the second-highest bid, or the reserve if higher.
Sold { winner: u32, price: u32 },
/// No bid reached the reserve.
NoSale,
}
/// A second-price (Vickrey) auction. Ties go to the lower index.
pub fn settle(reserve: u32, bids: &[u32]) -> Outcome {
let mut best: Option<(u32, u32)> = None; // (index, bid)
let mut second = 0u32;
for (i, &bid) in bids.iter().enumerate() {
match best {
Some((_, top)) if bid <= top => second = second.max(bid),
Some((_, top)) => {
second = top;
best = Some((i as u32, bid));
}
None => best = Some((i as u32, bid)),
}
}
match best {
Some((winner, top)) if top >= reserve => Outcome::Sold { winner, price: second.max(reserve) },
_ => Outcome::NoSale,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highest_bid_pays_the_second() {
assert_eq!(settle(100, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 400 });
}
#[test]
fn the_reserve_sets_a_floor() {
assert_eq!(settle(500, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 500 });
assert_eq!(settle(1000, &[400, 250, 900, 300]), Outcome::NoSale);
}
#[test]
fn a_tie_goes_to_the_lower_index() {
assert_eq!(settle(0, &[700, 700]), Outcome::Sold { winner: 0, price: 700 });
}
#[test]
fn no_bids_is_no_sale() {
assert_eq!(settle(0, &[]), Outcome::NoSale);
}
}
$ cd my-guests/auction-core && cargo +1.98.1 test
running 4 tests
test tests::a_tie_goes_to_the_lower_index ... ok
test tests::highest_bid_pays_the_second ... ok
test tests::no_bids_is_no_sale ... ok
test tests::the_reserve_sets_a_floor ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
guest
my-guests/auction/Cargo.toml は、両方のクレートにパスで依存します。
[package]
name = "auction"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
auction-core = { path = "../auction-core" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
my-guests/auction/src/main.rs:
#![no_std]
#![no_main]
use auction_core::{settle, Outcome};
use guest_sdk::{halt, poseidon2, read_input, read_public, write_output};
const MAX_BIDDERS: usize = 64;
const TAG: u32 = 0x4249_4431; // "BID1", the same tag the commit guest uses
// Output slot 0: what happened.
const SOLD: u32 = 1;
const NO_SALE: u32 = 2;
const BAD_OPENING: u32 = 3;
const TOO_MANY: u32 = 4;
/// Public (fixed at deploy): reserve, n, then n 8-word commitments.
/// Private (per call): for each bidder, the bid and four salt words.
/// Outputs: [status, winner, price, n, 0, 0, 0, 0]; a panic sets slot 7 to 0xdeadbeef.
#[no_mangle]
pub extern "C" fn main() -> ! {
let reserve = read_public(0);
let n = read_public(1) as usize;
if n > MAX_BIDDERS {
finish(TOO_MANY, 0, 0, n as u32);
}
let mut bids = [0u32; MAX_BIDDERS];
for i in 0..n {
let base = (5 * i) as u32;
let bid = read_input(base);
let mut buf = [TAG, i as u32, bid, read_input(base + 1), read_input(base + 2),
read_input(base + 3), read_input(base + 4), 0];
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
let committed = 2 + 8 * i as u32;
for w in 0..8 {
if buf[w] != read_public(committed + w as u32) {
finish(BAD_OPENING, i as u32, 0, n as u32);
}
}
bids[i] = bid;
}
match settle(reserve, &bids[..n]) {
Outcome::Sold { winner, price } => finish(SOLD, winner, price, n as u32),
Outcome::NoSale => finish(NO_SALE, 0, 0, n as u32),
}
}
fn finish(status: u32, winner: u32, price: u32, n: u32) -> ! {
write_output(0, status);
write_output(1, winner);
write_output(2, price);
write_output(3, n);
halt();
}
すべての終了経路は finish を通ります。finish は各スロットに一度だけ書き込み、その後で停止します。ループの上限は公開セグメントから決まり、これはデプロイ時に固定されます。したがって、呼び出し側がコミット済みの入札数より少ない件数しか読ませないようにすることはできません。
ビルドと実行
$ rand-guest build my-guests/auction --out auction.bin
258 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction.bin and its .sha256 (258 words, hc aa04e4c451e986f086e9fcff53b213eb52e593317c32a979d5e24991164f050d, program id 148ab0c4b9ad5115caab4a2ab433e3e13b5e8fbecbd9d8a9d6ba20c64f942c73)
rand-guest run は、公開セグメントを繰り返し指定する --public フラグとして受け取ります。シェルでは次のようになります。
PUB=$(for w in $(cat auction-public.txt); do printf -- "--public %s " "$w"; done)
BIDS="--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585"
rand-guest run auction.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 641
tier 10
tier 10: fits (cycles 721 of 1023, Poseidon2 permutations 88 of 128)
入札者2が、2番目に高い入札額である400で落札しました(1 は売却成立)。カウントされた入札者は4人です。失敗するはずの実行が2つあります。
| 変更内容 | 出力 0–3 | 意味 |
|---|---|---|
| 入札者1の入札額250を260に置き換え | 3 1 0 0 | 入札者1で BAD_OPENING。それ以外は何も公開されません |
| リザーブ価格を100ではなく1,000にする(公開セグメントが異なるため、プログラム ID も異なる) | 2 0 0 4 | NO_SALE: リザーブ価格に達した入札がありませんでした |
5. C での同じプログラム
C の guest ディレクトリには、.c ファイルと独自のヘッダを置きます。rand-guest build --lang c が、guest.h、_start、小さなランタイムを提供します(Deploy a RISC-V program)。ディレクトリ内のすべての .c ファイルがコンパイルされるため、ホスト用のテストファイルは別の場所に置いてください。
my-guests/auction-c/auction.h:
#ifndef AUCTION_H
#define AUCTION_H
#include <stdint.h>
/* Outcome of a second-price auction. sold is 0 when no bid reached the reserve. */
struct outcome {
uint32_t sold;
uint32_t winner;
uint32_t price;
};
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n);
#endif
my-guests/auction-c/settle.c:
#include "auction.h"
/* A second-price (Vickrey) auction. Ties go to the lower index. */
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n) {
struct outcome o = {0, 0, 0};
if (n == 0) {
return o;
}
uint32_t winner = 0, top = bids[0], second = 0;
for (uint32_t i = 1; i < n; i++) {
if (bids[i] > top) {
second = top;
top = bids[i];
winner = i;
} else if (bids[i] > second) {
second = bids[i];
}
}
if (top >= reserve) {
o.sold = 1;
o.winner = winner;
o.price = second > reserve ? second : reserve;
}
return o;
}
my-guests/auction-c/main.c:
#include "guest.h"
#include "auction.h"
#define MAX_BIDDERS 64
#define TAG 0x42494431u /* "BID1", the same tag the commit guest uses */
enum { SOLD = 1, NO_SALE = 2, BAD_OPENING = 3, TOO_MANY = 4 };
static uint32_t bids[MAX_BIDDERS];
static void finish(uint32_t status, uint32_t winner, uint32_t price, uint32_t n) {
rand_write_output(0, status);
rand_write_output(1, winner);
rand_write_output(2, price);
rand_write_output(3, n);
rand_halt();
}
/* Public (fixed at deploy): reserve, n, then n 8-word commitments.
* Private (per call): for each bidder, the bid and four salt words.
* Outputs: [status, winner, price, n, 0, 0, 0, 0]. */
void main(void) {
uint32_t reserve = rand_read_public(0);
uint32_t n = rand_read_public(1);
if (n > MAX_BIDDERS) {
finish(TOO_MANY, 0, 0, n);
}
for (uint32_t i = 0; i < n; i++) {
uint32_t base = 5 * i;
uint32_t buf[8] = {TAG, i, rand_read_input(base), rand_read_input(base + 1),
rand_read_input(base + 2), rand_read_input(base + 3),
rand_read_input(base + 4), 0};
bids[i] = buf[2];
rand_poseidon2(buf, 7);
for (uint32_t w = 0; w < 8; w++) {
if (buf[w] != rand_read_public(2 + 8 * i + w)) {
finish(BAD_OPENING, i, 0, n);
}
}
}
struct outcome o = settle(reserve, bids, n);
if (o.sold) {
finish(SOLD, o.winner, o.price, n);
}
finish(NO_SALE, 0, 0, n);
}
bids はゼロ初期化された static なので、.bss に置かれます。RAM は最初からゼロとして読み出されるため、イメージのワードは消費しません。
ホストでルールをテストする
settle.c は syscall を使わないので、通常のコンパイラでビルドできます。my-guests/auction-c-test/test_settle.c:
#include <assert.h>
#include <stdio.h>
#include "../auction-c/auction.h"
int main(void) {
uint32_t bids[] = {400, 250, 900, 300};
struct outcome o = settle(100, bids, 4);
assert(o.sold && o.winner == 2 && o.price == 400);
o = settle(500, bids, 4);
assert(o.sold && o.price == 500);
o = settle(1000, bids, 4);
assert(!o.sold);
uint32_t tie[] = {700, 700};
o = settle(0, tie, 2);
assert(o.winner == 0 && o.price == 700);
puts("settle: 4 checks passed");
return 0;
}
$ cc -Wall -o test_settle my-guests/auction-c-test/test_settle.c my-guests/auction-c/settle.c && ./test_settle
settle: 4 checks passed
ビルドと実行
$ rand-guest build --lang c my-guests/auction-c --out auction-c.bin
200 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction-c.bin and its .sha256 (200 words, hc 68605aa8d28a863a4a52a25de07ba159327a0fd0145a9dcd5e5aa6e79ab6bea0, program id 5667c343b8848805a908c9009d426164f39d2f927b2c780529a054f0d66aafc1)
$ rand-guest run auction-c.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 576
tier 10
tier 10: fits (cycles 641 of 1023, Poseidon2 permutations 73 of 128)
出力は Rust 版 guest と一致します。入札者が4人の場合、C のイメージは58ワード小さく、65サイクル速くなります。セクション7では、64人の入札者でこの優劣が逆転することを示します。両者は別のプログラムであり、hc もプログラム ID も異なります。ソースコードを公開するほうをデプロイしてください。
6. 言語ごとの注意点
Rust
クレート。 #![no_std]、#![no_main]、そして halt() で終わる #[no_mangle] pub extern "C" fn main() -> ! を使います。リリースプロファイルでは panic = "abort" を設定します。空の [workspace] テーブルを置くと、そのクレートは親のワークスペースに含まれません。.cargo/config.toml と独自のコンパイラフラグは使わないでください。rand-guest build は前者を拒否し、後者はすべて自身で設定します(Deploy a Rust program)。
パニック。 guest-sdk のパニックハンドラは、スロット7に 0xdeadbeef を書き込んで停止します。この実行は、有効で証明可能な実行のままです。次のプログラムは、スロット0と1に書き込んだあと、要素数6の Vec のインデックス999を読み出します。
$ rand-guest run heap.bin --input 6 --input 1 --input 2 --input 3 --input 4 --input 5 --input 6
out[0] = 4
out[1] = 1080033280
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 3735928559
cycles 1326
tier 12
スロット0と1には実際の値が入り、スロット7は実行がパニックしたことを示します。このような実行を送信した呼び出し側は、チェーン上でレシートを得ます。レシートを読む側は、スロット7 = 3,735,928,559 を拒否しなければなりません。さもなければ、プログラムがパニックしないようにしなければなりません。どの経路でも、get()、検査付き算術、明示的なステータスコードを使ってください。
ヒープ。 デフォルトではアロケータはありません。アロケータを用意すれば alloc が使えます。一度実行して終了するプログラムには、固定アリーナ上のバンプアロケータが適しています。
extern crate alloc;
use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
/// A bump allocator over a fixed arena: allocation moves a pointer forward, free does nothing.
/// One run is one short-lived process, so nothing needs to be reclaimed.
struct Bump {
arena: UnsafeCell<[u8; ARENA]>,
next: UnsafeCell<usize>,
}
const ARENA: usize = 256 * 1024;
unsafe impl Sync for Bump {}
unsafe impl GlobalAlloc for Bump {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let next = &mut *self.next.get();
let start = (*next + layout.align() - 1) & !(layout.align() - 1);
let end = start + layout.size();
if end > ARENA {
return core::ptr::null_mut();
}
*next = end;
(self.arena.get() as *mut u8).add(start)
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
#[global_allocator]
static HEAP: Bump = Bump { arena: UnsafeCell::new([0; ARENA]), next: UnsafeCell::new(0) };
アリーナはゼロ初期化されるため .bss に置かれ、イメージのワードは増えません。デフォルトのリンカスクリプトが与える RAM は 1 MiB で、アリーナ、静的変数、64 KiB のスタックがその中に収まる必要があります。このアロケータを使えば、Vec、collect()、iter().sum() はいずれもビルドでき、check も通ります。ただし alloc とその依存関係はコードを増やします。このテストプログラムは1,341ワードで、オークションのプログラムは258ワードです。プログラムのサイズが証明の予算を消費する理由は、セクション7で説明します。
浮動小数点。 このターゲットには浮動小数点命令がなく、Rust は f32/f64 をソフトウェアルーチンに変換します。動作はします。9、2、7、4、5の平均を f64 で計算した結果は、5.4f32 のビット列として返りました。ただし、1回ごとに多くのサイクルを消費します。可能なところでは、整数か固定小数点を使ってください。
クレート。 crates.io の no_std クレートは、通常の依存関係と同じようにビルドできます。crc クレートによる hello, rand の CRC-32 をエミュレータで実行すると 2124296908 が出力され、これは Python の zlib.crc32 が返す値と同じです。std を必要とするクレートは、このターゲットではビルドできません。rand-guest build は --locked を渡さないので、Cargo.lock をコミットしてください。コミットしないと、数か月後に再ビルドしたときに新しいクレートが解決され、異なる hc になることがあります。
unimp の罠: ライブラリコードが引き起こす Csr の指摘。 core の一部のコードは、LLVM が到達不能とみなす経路で unimp ワード(0xc0001073、csrrw x0, cycle, x0)にコンパイルされます。このマシンには CSR がないため、check はそのイメージを拒否します。
0x00002250 0xc0001073 Csr: CSR instruction: the machine has no CSRs
2613 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
wrote heap.bin (the loader refuses it: no hc)
ここで原因となったのは、長さが実行時にしか分からない Vec に対する slice::sort_unstable でした。安定ソートの sort() でも同じことが起き、固定長16要素の配列に対する sort_unstable は通りました。このような指摘の発生源を探すには、guest の ELF を逆アセンブルし、check が表示したアドレス周辺の関数を調べます。
llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp
そのうえで、その呼び出しを自前のコードに置き換えてください(このプログラムでは、短い挿入ソート)。guest-sdk 自身の halt() が noreturn の約束ではなく loop {} で終わるのも、同じ理由によります。
C
- エントリポイントは
void main(void)で、rand_halt()で終わります。付属の_startがスタックを設定し、これを呼び出します。 - libc はありません。
printf、malloc、<string.h>は使えません。ランタイムはmemcpy、memmove、memset、memcmp(clang は構造体のコピーや大きな初期化子でこれらの呼び出しを生成します)と、4つの64ビット除算ヘルパーを提供します。いずれもバイト単位のループなので、頻繁に実行されるコードではワード単位でコピーと除算を行ってください。ヒープが必要な場合は、Rust と同様にstatic配列上のバンプアロケータを書いてください。 - 未定義動作は検出されません。 パニックハンドラはありません。範囲外への書き込みはメモリを黙って壊し、アラインされていないロードやストアはトラップするため、その実行は証明できなくなります。
main.cがnをMAX_BIDDERSと照合しているように、インデックスは自分で定めた上限と照合してください。 - ディレクトリのルール。 独自の
guest.hやstart.Sを追加しないでください。build --lang cはどちらも拒否します。テストファイルは guest ディレクトリの外に置いてください。ディレクトリ内のすべての.cファイルがイメージにコンパイルされるためです。 - 固定された clang。
rand-guestは 23.1.1 以外の clang を拒否します。RAND_GUEST_CLANG_UNPINNED=1を設定すると上書きできますが、hcが他の人のビルドと一致しないという警告が出ます。
7. 証明できるか。予算
prover は、各実行を tier t までパディングします。tier には独立した2つの予算があり、rand-guest run --tier t は両方を報告します。
- 行数: 最大
2ᵗ − 1。実行したサイクル数とダイジェスト行を合わせて数えます。 - Poseidon2 置換: 最大
2ᵗ⁻³。
| tier | 行数 | Poseidon2 置換 | ハッシュテーブルなしのガス上限 | chain 20 での呼び出し |
|---|---|---|---|---|
| 10 | 1,023 | 128 | 1,279 | 受理 |
| 12 | 4,095 | 512 | 5,119 | 受理 |
| 14 | 16,383 | 2,048 | 20,479 | 受理 |
| 16 | 65,535 | 8,192 | 81,919 | v0.5.6 以降は拒否 |
| 18, 20 | 2¹⁸ − 1, 2²⁰ − 1 | 2¹⁵, 2¹⁷ | 327,679, 1,310,719 | v0.5.6 以降は拒否 |
手数料は tier ではなく、ガスとバイト数に従います。 chain 15 では、呼び出しごとに固定の 0.001 + call_fee(tier) を支払っていました。tier 10 では 0.002 RAND、tier 12 では 0.0021、tier 14 では 0.0022 です。chain 18 以降は、すべての実行が計量されます。通常の行は 1 ガス、Poseidon2 の absorb 行は 3、Keccak の置換は 192、SHA-256 の圧縮は 64 です。proof は宣言ガス上限を公開し、回路は実行のガスがそれ以下であることを強制し、正確なガスは非公開のままです。ハッシュテーブルなしで tier が宣言できる上限は (2ᵗ − 1) + 2ᵗ⁻² で、上の表の列がそれにあたります。手数料の下限は次のとおりです。
floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉
BUNDLE_BASE は 0.001 RAND、バイト数は呼び出しの proof と入力エンベロープの合計です。単価は、1ガスあたり100ユニット、1 KiB あたり800ユニット(1ユニットは 10⁻⁹ RAND)から始まり、ブロックごとに少しずつ動きます。chain 20 では、それぞれ 10,000 と 80,000 ユニットを超えて上昇することはなく、バイト単価を動かすのは呼び出しの proof とエンベロープのバイトだけです。chain 18 と 19 には上限がありませんでした。セクション9では、ウォレットがデフォルトで支払う額を示します。また、Example programs には chain 18 での実測した呼び出しが4件あり、1件あたり 0.0057〜0.0075 RAND です。
ティア 14 は、呼び出しが使える最上位のティアです。 フルノード v0.5.6(2026-09-25)以降、ノードはティア 14 を超える呼び出しプルーフを、検証鍵を構築する前に CallTierTooHigh で拒否します。同じ規則により、そのような呼び出しを含むブロックは無効になります。この制限は、メモリが 2〜4 GB のバリデータを守るためのものです。ティア 20 の検証鍵は、構築に数分と数 GB を要します。rand call は、プルーフを生成する前にそのような呼び出しを拒否しませんが、chain 20 では先にティアを表示します。呼び出す前に rand-guest run … --tier 14 を実行してください。 実行が収まらない場合は、プログラムを再構成します。デプロイの前に rand program deploy --input <word> …(入力のない呼び出しなら --check-call)を実行すると、想定している呼び出しがエミュレータで実行され、ティア 14 以下に収まらない限りデプロイは拒否されます。
予算を消費するもの
意外なのは、ダイジェストの行とパーミュテーションです。コードが実行される前に、マシンはプログラム、プライベート入力、公開セグメントをハッシュして、hc、H_IN、H_PUB を作ります。それぞれのコストは、4 ワードにつき約 1 パーミュテーションと 1 行です。これに加えて、POSEIDON2 を呼び出すたびに、ハッシュされる 4 ワードにつき約 1 パーミュテーションかかります。ティア 10 のオークションでは次のとおりです。
| 項目 | ワード数 | パーミュテーション数 |
|---|---|---|
プログラム(hc) | 258 | 約 65 |
プライベート入力(H_IN) | 20 | 約 5 |
公開セグメント(H_PUB) | 34 | 約 9 |
| コミットメントの検査 4 回、各 7 ワード | 4 × 7 | 8 |
合計(run の報告値) | 128 のうち 88 |
行数:641 サイクルにダイジェストの行を加えて、1,023 のうち 721 になりました。
chain 20 でも chain 19 と同様に、ウォレット自身の集計は rand-guest run のものよりわずかに高くなります。そこでは、呼び出しプルーフがそのトランザクションに束縛されます(chain 20 では、束縛にジェネシスハッシュも含まれますが、ワード数は同じ 8 ワードです)。この束縛は、さらに 8 ワードの公開セグメントとして運ばれます。公開セグメントを持つプログラムでは、ダイジェストの行とパーミュテーションがそれぞれ 2 増え、持たないプログラムではそれぞれ 1 増えます。
このため、プログラムのサイズも予算の一部になります。ヒープのテストプログラムは 1,183 サイクルしか実行しませんでしたが、1,341 ワードで 340 パーミュテーションを要し、ティア 12 に押し上げられました。ティア 14 の 2,048 パーミュテーションでは、約 8,000 ワードを大きく超えるプログラムは、何をするものであっても呼び出せません。chain 20 での正確な上限は、公開セグメントのないプログラムで入力のない呼び出しの場合に 8,180 ワードで、どちらかがあればそれより小さくなります。chain 20 のプログラム上限は chain 15 と同じ 65,535 ワードですが、chain 19 と chain 20 はこの上限を超えるプログラムのデプロイを拒否します(ProgramUncallable)。chain 15 では、誰も呼び出せないプログラムのデプロイ手数料を受け取っていました。chain 13 で変換された ERC-20(11,686 ワード、ティア 16 で呼び出し)が chain 20 でデプロイも呼び出しもできないのは、このためです。プログラムは小さく保ってください。Rust では opt-level = "s"、C では -Os(設定済み)を使い、依存関係は少なくし、必要でない限り alloc は使いません。
オークションは線形に増加します。入札者が 1 人増えるごとに、入力が 5 ワード、公開が 8 ワード、検査のパーミュテーションが 2、サイクルが約 100 増えます。入札者が 64 人(ランダムな入札額、入力 320 ワード、公開 514 ワード)の場合、どちらのビルドも同じ勝者と価格を選び、どちらもティア 14 を必要とします。
auction.bin tier 14: fits (cycles 6557 of 16383, Poseidon2 permutations 403 of 2048)
auction-c.bin tier 14: fits (cycles 8571 of 16383, Poseidon2 permutations 388 of 2048)
パーミュテーションはティア 12 の 512 に収まりますが、行数が 4,095 に収まりません。ここでティアを決めているのは、ハッシュではなくサイクルです。入札者が 64 人の場合は、Rust のビルドのほうが高速です。ループが支配的になると、その opt-level = 3 が C ビルドの固定された -Os を上回ります。
Keccak と SHA-256
keccak と sha256_compress は、それぞれ 1 行のコストですが、どちらかを使うプルーフには追加のテーブルが加わります。aeacf31 では、どちらも配列への参照を取ります。keccak(&mut state) は [u32; 50] に、sha256_compress(&mut buf) は [u32; 24] に対して使い、7ef3220 では生ポインタを取っていました。v0.5.6 以降、1 回の呼び出しで使えるのは、最大 128 回の Keccak-f パーミュテーションと最大 128 回の SHA-256 圧縮です。chain 20 では、Keccak パーミュテーションは 192 ガス、SHA-256 圧縮は 64 ガスのコストもかかります。
テーブルによってプルーフが大きくなり、chain 20 は呼び出しプルーフを 4,194,304 バイト(4 MiB。chain 17〜19 と同じで、chain 13〜16 では 8 MiB)に制限しています。chain 18 では、Keccak テーブルを使うティア 12 の呼び出しが 3.35 MB と上限の大部分に達し、SHA-256 テーブルを使うティア 14 の呼び出しは 1.91 MB でした。どちらも使わない場合は 1.50 MB です(Example programs)。1 KiB ごとにバイト単価が課金されます。chain 15 では、無料枠の 2 MiB + 18,432 を超えたバイトだけが 1 KiB あたり 1,000 単位で課金されました(Fees)。外部のハッシュと一致させる必要がない限り、Poseidon2 を使ってください。
メモリと証明時間
証明の時間とメモリは、ティアとともに急激に増えます。ティア 10 の呼び出しは、ノート PC で数秒で証明できます。chain 20 では、すべてのデプロイと呼び出しが、バンドル側の 2 つのプルーフも伴います。ティア 10 の支払い承認(数秒)と、手数料を支払うティア 14 のバンドル(ノート PC で約 2 分)です。chain 18 で計測した 4 回の呼び出しは、それぞれ 3 分から 5 分半かかり、ピークは 6.1〜6.6 GB でした(Example programs)。chain 15 では、バンドルは約 100 秒、約 5.7 GB のティア 14 のプルーフ 1 つでした。計測値は Validator and aggregator hardware にあります。
8. デプロイ
testnet は chain 20 です。デプロイには、フルノード v0.6.8 以降の rand ウォレット、鍵、そして少量の RAND が必要です(Introduction。chain 20 のファウセットは chain 19 と同様に、ジェネシスの許可リストにある 16 個のウォレットにのみミントします)。v0.6.7 のウォレットは chain 20 では送信できません。chain 20 のジェネシスは binding_domain: 1 を設定しているため、すべてのプルーフの束縛と署名付きメッセージにジェネシスハッシュが含まれますが、v0.6.7 のウォレットはそれを入れないからです。ウォレットの同期、デプロイ、呼び出しには、公開エンドポイント https://rpc.randprotocol.org で十分です(setup)。オークションは公開セグメントを付けてデプロイします。
rand fee deploy 258 --public-words 34 # 0.001 + 0.0001 × (258 + 34) = 0.0302 RAND
rand program deploy auction.bin --public auction-public.txt
program deploy は、プログラム ID、hc、ワード数、公開入力のワード数とダイジェストを表示します。プルーフを生成する前に、プログラムをチェーンの上限と照合し、公開入力を max_program_public_words(chain 20 では 32,768)と照合します。続いて、手数料を支払うゼロ値の自己送金バンドルを証明し、送信して、コミットを待ちます。chain 20 では、chain 17 以降と同様に、これは 2 つのプルーフであり、それぞれが独自の出力行を持ちます。ティア 10 の支払い承認(数秒、約 1.37 MB)と、ティア 14 のバンドル(約 2 分、約 1.5 MB)です。chain 15 では、約 100 秒、約 1.42 MB のティア 14 のプルーフ 1 つでした。デプロイ手数料にガス項はありません。オークションは chain 18〜20 ではデプロイされていません。An anonymous vote に、chain 18 で公開セグメント付きのデプロイをウォレットが表示したとおりに示しています。
プログラム ID は --public によって変わります。 rand-guest は、コードだけの場合に 148ab0c4…2c73 を表示しました。公開セグメントを付けたデプロイは blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public) のもとに保存され、これには準備金と 4 つのコミットメントも束縛されます。program deploy が表示する ID を使ってください。コードが同じでコミットメントが異なる 2 つ目のオークションは、別のプログラム ID になります。
rand program show <auction id> # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
code_hash は、rand-guest が表示した hc を、表記を変えたものです。rand-guest はダイジェストの 8 ワードをそれぞれビッグエンディアンで表示し、ウォレット、rand program show、rand_getProgram は各ワードのバイトをリトルエンディアンで表示します。このため、8 桁の 16 進数のグループごとにバイト順が逆になります。上記のビルドでは、aa04e4c4…050d(Rust)は c4e404aa…4f16 として、68605aa8…bea0(C)は a85a6068…b69a として表示されます。
9. 呼び出し
出品者は、20 個のプライベートワードをすべて指定して呼び出します。
rand call <auction id> --expect-public auction-public.txt \
--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514 \
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507 \
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116 \
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585
ウォレットはプログラムのコードと公開セグメントを取得し、それらが ID にハッシュされることを確認します。--expect-public は、公開セグメントがそのファイルのワードでない場合にも、プルーフを生成する前に拒否します。続いてウォレットはエミュレータで呼び出しを実行し、正確なガス、宣言する上限、ティア(実行が収まる最小のもの。ここではティア 10)、手数料を表示します。
gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)
ウォレットは 20 個の入力ワードを、呼び出し元だけが開けるエンベロープに封入し、実行をローカルで証明し、バンドルを通じて手数料を支払い、送信し、待ってからレシートを表示します。chain 20 では、これは 3 つのプルーフです。呼び出し、ティア 10 の支払い承認、ティア 14 のバンドルです。
呼び出しのコスト。 chain 15 では、この呼び出しは 0.001 + call_fee(10) = 0.002 RAND を支払いました。chain 20 では、ウォレットは第 7 節の下限を、プルーフが存在する前に算出して支払い、3 つのデフォルトがあります。
- 宣言する上限は、正確なガスを
2^(tier − 2)の倍数に切り上げた値です。tier 10 では 256、tier 12 では 1,024、tier 14 では 4,096 になります。--gas-limit maxはその tier の上限を宣言し、--gas-limit Nは正確な値を宣言します。 - 証明は実際のサイズではなく、チェーンの上限である 4,194,304 バイトで価格が決まります。chain 20 の call の証明はそのトランザクションに結び付けられており、手数料を先に確定させる必要があるためです。
- 価格の段階にして 2 段階分の余裕を見込みます(1.125² 倍)。call は価格が決まってから 2〜3 ブロック後に取り込まれるためです。
--fee を指定するとデフォルトを上書きでき、rand fee call <tier> --gas <limit> --bytes <B> を使うとノードに最低手数料そのものを問い合わせられます。オークションの call は chain 18 から 20 では行っていないため、そのガスの行と手数料はここには示しません。匿名投票には、ウォレットが出力したとおりの完全な call が chain 18 にあり、手数料は 0.0062 RAND です。
レシートには、tier、H_IN、H_PUB、出力 [1, 2, 400, 4, 0, 0, 0, 0] が含まれます。エミュレータが出力したものと同じワードです。証明は同一の実行に対するものだからです。4 件の入札とソルトはオンチェーンにありません。--auditor <rand1…> を指定すると、入力を指定した監査人向けにも封印します。--no-envelope を指定するとトランスクリプトをまったく公開せず、この操作は取り消せません(Programs)。
10. 結果を確認する
入札者を含め誰でも、売り手を信頼することなくオークションを検証できます。
- コード。 公開されているソースを、同じ circuits コミット、Rust 1.98.1(または clang 23.1.1)で
rand-guest buildにより再ビルドします。hcを、rand program showまたはrand_getProgramが返すcode_hashと比較します。バイト順が逆の表記になる点に注意してください(セクション 8)。ビルドは再現可能なので、どのマシンでも両者は一致します。 - 公開セグメント。
rand_getProgramPublicで取得するか、rand call --expect-publicを実行します。自分のコミットメントが自分のインデックスの位置にあることを確認します。 - レシート。
rand receipt <tx hash>またはrand_getReceiptを使います。status が1、勝者と価格が正しく、スロット 7 が0xdeadbeefではなく、programがオークションの id と等しいことを確認します。 - 入力(開示を受ける権利がある場合)。 売り手の
rand open-call <tx hash>(または監査人の--as-auditor)は、封印された入力を開き、H_INと照合し、プログラムを再実行して、不一致があれば非ゼロで終了します。v0.6.8 のウォレットを使ってください。v0.6.8 以降、再実行はプログラムの公開セグメントと call のバインディングに対して行われるため、オークションでも動作します(fullnode issue #116)。v0.6.7 では、公開セグメントを持つ状態でデプロイされたプログラムすべてについて、open-callがre-running the program on these inputs failed: PublicIndex(0)で停止していました。
証明が保証するのは、コミットされた入札のオープニングに対してルールが適用されたことです。資金は移動しません。勝者は通常の rand send で売り手に支払い、双方が拠り所とするのはレシートです。
トラブルシューティング
| 症状 | 原因と対処 |
|---|---|
古いコミット向けに書かれた poseidon2(…)、keccak(…)、sha256_compress(…) の呼び出しでビルドが失敗する | aeacf31 では poseidon2 は unsafe fn で、他の 2 つはそれぞれ &mut [u32; 50] と &mut [u32; 24] を取ります(セクション 3 と 7) |
check が a7 = 7 の Syscall を報告する | guest が poseidon2_len を呼んでいます。aeacf31 の rand-guest check はシステムコール 7 を一覧に含めていません(zkp-circuits issue #1) |
check が 0xc0001073 で Csr を報告する | ライブラリのコードが unimp にコンパイルされています。llvm-objdump で該当の関数を探し、その呼び出しを置き換えてください(セクション 6) |
run が、実行がどの tier にも収まらない、または tier 14 を超えると報告する | 行数または Poseidon2 順列が多すぎます。プログラムを小さくする、ハッシュを減らす、または作業を複数の call に分け、その間でダイジェストを引き継いでください |
| slot 7 = 3735928559 | Rust の guest がパニックしました。それ以前の出力は信頼できません |
trap: DoubleWrite(n) | 出力スロット n に 2 回書き込んでいます。すべての終了経路を 1 つの関数にまとめてください |
trap: InputIndex(i) | プログラムが、呼び出し側の与えていない非公開入力を読み取りました。先に個数を読み取って確認してください |
ロードまたはストアでの trap: … | アラインメント違反、または範囲外のアドレスです。C では、配列の境界を超えた添字が原因です |
invalid value '0x…' for '--input' | コマンドラインでは、入力ワードは 10 進数で指定します |
CallTierTooHigh で call が拒否される | 証明が tier 14 を超えています。call の前に rand-guest run --tier 14 で確認してください |
再ビルドした hc がデプロイ済みのものと異なる | circuits コミット、ツールチェーン、依存関係のバージョンのいずれかが異なります。3 つすべてを固定し、Cargo.lock をコミットしてください。表記だけが異なる場合は、8 桁の 16 進数ごとにバイト順が逆になっています(セクション 8) |
rand open-call が PublicIndex(0) で停止する | プログラムは公開セグメントを持ち、ウォレットは v0.6.7 です。その再実行は何も渡しません(fullnode issue #116)。v0.6.8 を使ってください |
… is not inside a circuits checkout | guest ディレクトリを checkout の内側に移動してください(Deploy a Rust program) |