このページは、プールを読むあらゆるものを構築・監査するために使います。たとえば、自分のノートをスキャンするウォレット、コミットメントとヌリファイアを追うインデクサ、トランザクションが公開する内容を検査するツールです。このチェーン上の価値はノートの集合であり、チェーンが保持するのはそのコミットメントだけで、中身は保持しません。
ノート
ノートは { pk, from, amount, asset, time, r } で、32 ビットのワード 28 個、112 バイトです。レイアウトは pk(8) from(8) amount_lo amount_hi asset time r(8) です。pk は所有者のノート公開値(viewing key から導出)、from は送信者のノート公開値、amount は単位で表した u64(1 RAND = 10⁹ 単位)、asset は u32 です(RAND は 0、それ以外はブリッジ経由かネイティブかを問わず、RPL トークンに対するトークンレジストリの連番インデックスです。RPL tokens を参照)。time は送信者が指定したブロック高、r は 8 ワードのランダムなブラインディングです。
平文がチェーン上に現れることはありません。現れるのは次のものです。
- コミットメント
cm = H(CM, words)。ドメインタグ 4 の下で 28 ワードに対して取る Poseidon2 ハッシュで、ツリーの葉として追加されます。 - エンベロープ。平文を所有者のアドレス宛に封印したもので、所有者か、送信者(outgoing viewing key 経由)だけが読めます。chain 18 以降(chain 20 を含む)は、エンベロープに暗号化されたメモも含まれます。公開フィールドからチェーンが計算する一部のノートには、エンベロープがまったくありません(後述)。
ノートを使用すると、その ヌリファイア H(NF, nk, cm) が公開されます。これは、所有者の viewing key とコミットメントに対してドメインタグ 3 の下で取る Poseidon2 ハッシュです。nk がなければ、ヌリファイアをそのコミットメントに結び付けることはできません。二重支払いを防ぐのは、このヌリファイア集合です。
エンベロープ
Envelope { kem_ct, to_receiver, to_sender, body }
| 部分 | バイト数 | 内容 |
|---|---|---|
kem_ct | 1,088 | 受信者のアドレス宛の ML-KEM-768 カプセル化 |
to_receiver | 60 | カプセル化された秘密の下でラップした、トランザクションごとの鍵 TxKey |
to_sender | 60 | 送信者の outgoing viewing key ovk の下でラップした同じ TxKey |
body | 652 | TxKey の下の、112 バイトのノートと 512 バイトのメモフィールド(chain 17 まではノートのみの 140 バイト) |
どの部分も ChaCha20-Poly1305 で、先頭にランダムな 12 バイトのナンスが付き、16 バイトのタグが付きます。関連データは、タグ(rand-envelope-receiver、rand-envelope-sender、rand-envelope-body)にノートのコミットメントを続けたものです。そのため、鍵は自分が封印していないエンベロープを開けられず、開いたノートは、その下にある葉に束縛されます。暗号文は合計 1,860 バイトで、bincode の長さプレフィックスによりワイヤ上では 1,892 バイトになります。chain 20 のジェネシスは envelope_bytes: 1860 を設定しており、その台帳は他の長さのノートエンベロープを拒否します(envelope is 1348 bytes, this chain requires exactly 1860)。エンベロープについて検査するのはこれだけです。chain 17 まではエンベロープにメモフィールドがなく、1,348 バイト(ワイヤ上では 1,380)で、台帳は 2,048 バイトの上限を課すだけでした。ジェネシスに envelope_bytes がないチェーンでは、現在もこの規則が適用されます。
メモフィールドは len (u16 LE) ‖ UTF-8 text ‖ zero padding の 512 バイトで、テキストは最大 510 バイトです。chain 20 のノートエンベロープにはすべてこれがあり、メモの有無は問いません。おつりの出力、ダミーの出力、メモなしの支払いも同じようにパディングされ、チェーン上ではメモの存在、その長さ、不在のいずれも区別できません。メモはノートにも、コミットメントにも、どの証明にも含まれません。メモを読めるのは、すでにそのノートを読める者、つまり所有者、ovk を持つ送信者、その出力の TxKey を渡された者だけです。不正なメモフィールド(長さの誤り、無効な UTF-8、パディングが 0 でない)は、メモなしとして開かれ、所有者がノートを失うことはありません。公開アドレスにはだれでもダスト額のノートを支払えるため、メモは任意の人のテキストです。rand ウォレットと同様に、制御文字と Unicode のフォーマット文字を置き換えたうえで、信頼できないものとして表示してください。rand send --memo は支払いの出力にメモを封印し、rand notes --memo と rand history --memo はそれを表示します。
ツリー、アンカー、ウィンドウ
コミットメントは、深さ 32 の Poseidon2 ツリーに古い順に追加され、削除されることはありません。葉 i は、チェーンがこれまでに作成した i 番目のノートです。ジェネシスのデポジットノートが最初の葉になります。バンドルは、何を動かすかにかかわらず、ダミーも含めて 4 つの葉と 4 つのヌリファイアを追加します。空の葉はすべてゼロで、ノードは H(NODE, left, right)(ドメインタグ 7)です。ノードはフロンティアだけを保持するため、コンセンサス状態が葉を保持することはありません。rand_getWitness は、保存された葉からオンデマンドでツリー全体を再構築します。ノードが提供する読み取りのうち最も高コストなのはこのためです。
バンドルは、入力がアンカー(ツリーのルート)の下に含まれることを証明します。アンカーの正当性は次の 2 つの規則で保たれます。
| 定数 | 値 | 規則 |
|---|---|---|
ANCHOR_WINDOW | 256 ブロック。chain 20 では 1,024 | アンカーは直近 window 個のブロック終了時のルートのいずれかでなければならない。ブロックの途中でツリーが通過するルートがアンカーになることはない |
TIME_WINDOW | 256 ブロック。chain 20 では 1,024 | バンドルの time は [height − window, height] の範囲内でなければならない |
ジェネシスが proof_window_blocks(v0.6.8、256 から 4,096)を設定しない限り、どちらのウィンドウも 256 ブロックです。この設定は 2 つのウィンドウをまとめて置き換えます。chain 20 は 1,024 を設定しています。chain 14 から 19 にはこのフィールドがありませんでした。
ウィンドウがあるのは、証明に実際に時間がかかるためです。tier-14 のバンドル証明はノート PC で約 100 秒かかります(chain 17 以降、ウォレットは tier-10 の認可証明も 1 つ作成し、これに約 7 秒かかります)。256 ブロックは、chain 18 の 1.17 秒ブロックで約 5 分でした。低速な prover への委任証明が 294 秒で拒否されたこともあります。chain 20 の 1,024 ブロックは、エクスプローラが報告した 1.34 秒のブロックで約 23 分です。競争に負けたウォレットには anchor is not one of the last 1024 roots と通知され、証明をやり直します。パラメータなしの rand_getAnchor は先頭ブロックのブロック終了時ルートを返し、これが prover が基準にすべき唯一のアンカーです。
バンドル
価値を動かすトランザクションはすべて、固定の 4 入力 4 出力の hidden-asset バンドルをちょうど 1 つ持ちます(chain 14 以降。chain 13 までは 2 入力 2 出力でした)。このバンドルは、ダイジェスト hc_bundle がジェネシスファイルに固定された hidden-asset zkVM ゲストによって証明されます(chain 17 から 20 では 60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce、バンドルゲスト v3。chain 14 と 15 では 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8、ゲスト v1 でした)。
Bundle { anchor, nullifiers[4], commitments[4], fee, burn_a, burn_r, burn_asset, time, envelopes[4], proof,
auth_commit, auth_proof } // the last two since chain 17
入力・出力ともスロット 0〜1 はプライベートアセット A を運び、スロット 2〜3 は常に RAND を運びます。RAND のみの支払いでは、スロット 0〜1 はダミーとなり、価値と手数料はスロット 2〜3 を通って動きます。トークン送金では、トークンはスロット 0〜1 を通って動き、RAND の手数料は同じ証明のスロット 2〜3 から支払われます。A は証明のウィットネスワードであり、どの公開フィールドにも現れません。そのため、あらゆる RPL トークンの送金は burn_a = burn_r = burn_asset = 0 の Action::None となり、フィールド単位で RAND の支払いと同じ形になります。
証明が述べるのは、すべての実入力が anchor の下の葉であってそのスロットのアセットを持つこと、4 つのヌリファイアが公開されたものであること、各グループの収支が合うこと(プライベートアセットでは in0 + in1 = out0 + out1 + burn_a、RAND では in2 + in3 = out2 + out3 + fee + burn_r)、そして支払者が鍵を保持していることです。どの葉か、どの金額か、どのアセットか、だれかは明かされません。3 つの burn フィールドは、プールから出ていく価値についてバンドルが公開する唯一の記述です。burn_a と burn_asset は TokenBurn または BridgeBurn の場合にのみ非ゼロで、アクションの金額およびアセットと等しくなります。burn_r はバーンされた RAND で、Bond(ステーク)と RegisterAggregator(ボンド)の場合にのみ非ゼロです。手数料は常に RAND です。
chain 17 以降、支払い権限は 2 つの証明に分割されています(分割認可)。バンドルゲスト v3 は、以前のゲストが spend key を受け取っていたところで viewing key nk とソルトを受け取り、pk_self = H(PK, nk) を導出して、すべての入力がそれの所有であることを要求し、c = H(AUTH, nk ‖ salt) を、公開するダイジェストに組み込みます。もう 1 つのゲストである認可ゲスト(ジェネシスの hc_auth、1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1)は、spend key sk と同じソルトを受け取り、nk = H(NK, sk) を導出して同じ c を公開します。バンドルは c を auth_commit として、認可ゲストの tier-10 証明を auth_proof として運びます。台帳は、両方の証明が同じトランザクションバインディングに対して検証され、2 つの c の値が一致することを要求します。したがって、nk とソルトを持つ prover はバンドル証明を作れても認可証明は作れず、委任証明はこの性質に基づいています。ソルトはバンドルごとに新たに生成する 256 ビットのランダム値なので、auth_commit は公開されますが、何も結び付けません。自分で証明するウォレットは両方の証明を自分で作るため、自己証明のトランザクションと委任証明のトランザクションはチェーン上で同じに見えます。
ここでは固定された形のため、スロットはダミーであることがよくあります。ダミー入力は支払者が所有する金額ゼロのノートで(そのナリファイアは公開されます)、ダミー出力は金額ゼロのノートです。お釣りの出力は、お釣りがゼロの場合でも存在します。お釣りがないときだけ形が変わるトランザクションでは、お釣りがなかったことが漏れてしまうからです。ウォレットにとっての帰結として、バンドルの2つのグループはそれぞれ最大2つのノートしか消費しません。RAND の支払い、ボンド、デプロイ、コールは最大2つの RAND ノートを消費します。トークンの転送とバーンは、そのトークンのノートを最大2つ、手数料用の RAND ノートを最大2つ消費します。1つのグループで3つ以上のノートに分散した価値は、先に自分のアドレスへの送金でまとめる必要があります。
チェーン14以降、証明はそのトランザクションにも束縛されています。証明は、証明部分を空にしたトランザクション全体のハッシュに対して作成され、そのハッシュに対して検証されます。そのため、別のアクション、別のエンベロープ、別のチェーンにコピーすることはできません。チェーン20ではこのハッシュにジェネシスハッシュも含まれるため、同じチェーン ID を共有するチェーンにもコピーできません(トランザクション)。
何が公開され、何が隠されるか
| チェーン上で公開されるもの | 隠されるもの | |
|---|---|---|
| RAND または任意の RPL トークンの転送 | アンカー、4つすべてのナリファイア、4つすべてのコミットメント、手数料、burn_a = burn_r = burn_asset = 0、time、4つのエンベロープ暗号文、証明、auth_commit と認可証明 | 送信者、受取人、金額、メモ、どの資産が動いたか、お釣り、どのリーフが消費されたか、どのスロットがダミーだったか |
| トークンのバーンまたはブリッジのバーン | 転送が公開するすべて。加えて、burn_asset と burn_a はアクションの資産と金額に等しい(ブリッジのバーンでは、リレーヤー手数料、裏付けトークン、宛先も) | どのノートがバーンされたか、誰がバーンしたか |
| フォーセットのミント | コミットメント、エンベロープ、金額、ミントしたバリデータ、そしてチェーン14以降はノートのオープニング(pk、time、r)。受け入れ時にこれからコミットメントが再計算されます | そのノートのその後のすべての消費 |
| ジェネシスのデポジット | ジェネシスファイル内の { cm, envelope, amount, opening: { pk, time, r } }。リスト済みトークンのノートでは、オープニングに asset が含まれます(チェーン20のジェネシスには2つの zUSD ノートがあります)。所有者の pk、金額、資産は設計上公開されます | 後にいつ、何に対して消費されるか |
| ウィズドロー、ブリッジのデポジット、トークンのミント、登録時の初期ミント | 金額、受取人、ノートの time とブラインディング r(コミットメントはチェーンが自ら計算します) | そのノートのその後のすべての消費 |
| invoke のペイアウトまたはプログラムのミント(チェーン20) | 金額、資産、受取人、r。ノートの time はバンドルのものです | そのノートのその後のすべての消費 |
| ブリッジの手数料ノート(チェーン20) | 金額、資産、time、導出されたブラインディング、所有者としてのジェネシスのトレジャリー。エンベロープはありません | そのノートのその後のすべての消費 |
価値は公開された金額でプールに入ります。フォーセットのミント、ジェネシスのデポジット、バリデータのウィズドロー、ブリッジのアテステーションとその手数料ノート、トークンのミント、プログラムのヴォルトのペイアウトがそれにあたります。そして、公開された手数料またはバーンとしてプールを出ます。その間にあるものはすべてノートです。
この表はチェーン20の証明が隠すものを示していますが、以前から常にそうだったわけではありません。チェーン14と15では、バンドル証明そのものがより多くを開示していました(2026-09-27 の zkVM レビュー、INT-2 / GV-1)。各テーブルの LogUp の累積和がブラインドされないまま公開されていたうえ、v1 ゲストはダミー入力に対して Merkle チェックと資産チェックを省略し、Merkle パスのすべてのビットで分岐していました。そのため、バンドル証明を読むだけで、4つの入力スロットのうちどれが実在のものか、したがってトークンが動いたのか RAND だけなのか、そして消費された各リーフのインデックスに含まれる1のビットの数を読み取ることができました。これを2つの変更で塞ぎました。チェーン16では、すべての LogUp 終端をブラインドし、宣言されたすべてのテーブルを 2^7 行以上に引き上げる制約セット7へ移行し、分岐のないバンドルゲスト v2 に切り替えました。v2 では、ダミーか実在かを問わずすべてのスロットが、同じ命令列で完全なメンバーシップチェックと資産チェックを実行します。チェーン17から20が動かすゲスト v3 は、花押の代わりに nk とソルトを用いる点を除いて v2 と同じ関係式であり、やはり分岐がありません。チェーン14と15でコミットされたバンドルは、漏れた情報を残したままです。チェーン20で証明がなお示すのはそのヘッダーです。バンドルについては固定された1つの形(ティア14、すべてのバンドルで宣言されたガスリミットは 20,479)であり、Call や Invoke については、ティア、宣言されたガスリミット、そして呼び出しが keccak または sha256 プリコンパイルを使ったかどうか、使った場合はおおよその回数です(証明)。
エンベロープのないノート
ほとんどのノートは、そのエンベロープを開くことで見つかります。一部のノートは公開フィールドからチェーンが計算するもので、それらではエンベロープは存在しないか、信頼できません。
- ブリッジの手数料ノート(チェーン20)。
bridge.feesの下で、チェーンはすべてのデポジットとすべてのバーンのたびに、ジェネシスのトレジャリー宛ての zUSD ノートを追加します(RPL トークン)。このノートにはエンベロープがありません。ストレージは空のエンベロープ(誰にも封印されていないもの)でインデックスし、rand_getCompactBlocksとrand_getCommitmentsもそのように表示します。デポジットの場合はデポジットノートの後に、バーンの場合はバンドルの4つのノートの後に現れます。ノートのすべてのワードが公開されています。金額はトランザクションとジェネシスのレートから、資産とtimeはトランザクションから、from = 0、ブラインディングはデポジットならblake3("rand-bridge-fee-r-1" ‖ mu)、バーンならblake3("rand-bridge-burn-fee-r-1" ‖ txid)、所有者はジェネシスのrecipientです。 - エンベロープが何も束縛しないノート。 ブリッジのデポジット、トークンのミント、登録時の初期ミント、そしてチェーン20では invoke のペイアウトとプログラムのミントは、トランザクションを提出した者が封印したエンベロープを持ちます。しかしチェーンはアクションの公開フィールドからノートを計算し、エンベロープをそれと照合することはありません。不注意な、あるいは悪意のある提出者が無意味なデータを封印する可能性があるため、受取人はそれに依存してはなりません。
そのため rand ウォレットは、これらのノートを自ら再構築します(wallet::rebuilt_notes_with)。試行復号と並んで行う公開パスで、自分の pk を名指しするそのようなトランザクションすべてについて、またアドレスがトレジャリーである場合は手数料ノートについて、ノートを計算し、そのコミットメントをハッシュした結果と一致するリーフに記録します。再構築されたノートはあくまで候補にすぎません。トランザクション、手数料レート、ハッシュについて嘘をつくノードは、ノートを見つからなくすることはできても、存在しないノートを計上させることはできません。v0.6.8 以降、rand_getBlocks が返すヘッダーは、デポジット、ミント、登録のトランザクションを public_notes に含めます。ブリッジ手数料のあるチェーンではバーンも含め、証明は取り除かれ、本来の ID が付いています。そのためこのパスはヘッダーのページだけを読みます。v0.6.8 では invoke は public_notes に含まれないため、v0.6.8 のノードに対しては、ウォレットが invoke のペイアウトを見つけられるのはそのエンベロープ経由のみです。しかしそのエンベロープは invoke を行った側が封印するもので、何も検査されません。フルノードの main では、d0bb73fb 以降、invoke もそこに含まれます(証明は取り除かれます)。これはノード側のみの変更で、チェーン20のフリートにはまだ展開されておらず、ウォレット側の変更は不要です。
スキャン:ウォレットが自分のノートを見つける方法
ノードが答えるのは「ツリー全体はこれです」であり、「あなたの残高はこれです」ではありません。ウォレットは次の手順を踏みます。
rand_getCommitments(from_index, limit)(1回の呼び出しにつき最大1,000件)、またはブロックごとのストリームrand_getCompactBlocks(from_height, to_height)(1回の呼び出しにつき最大128ブロック)でリーフをページ送りします。バンドルを含むトランザクションはすべて、4つのノートと4つのナリファイアをここに列挙し、ダミーのスロットと実在のスロットを見分けられる読み手はいません。- すべてのエンベロープを試行復号します。受信者としては ML-KEM の脱カプセル化鍵で、送信者としては
ovkで行います。pkが自分のものであるノートを残し、公開金額を持つノートはそれらのトランザクションから再構築します(上記)。 - 残した各ノートのナリファイアを計算し、
rand_getNullifiers(from_height, limit)に現れるものに印を付けます。 - 残りを合計します。
rand_getTreeInfo は、スキャンすべき範囲を示します。{ "next_index", "root", "nullifiers" } です。消費のためには、ウォレットは実在する各入力の32レベルの Merkle パスを必要とします。rand ウォレットは、同じ rand_getCommitments のページからスキャン中に構築した独自のコミットメントツリーのコピーを保持し、すべてのパスを自分で計算します。そのため、送金がノードに尋ねるツリーに関する問い合わせは、リーフを名指ししない rand_getAnchor だけです。rand_getWitness(index) は、他のクライアントのためにノードに残されています。これは、呼び出し側がどのリーフを消費しようとしているかをノードに伝えてしまいます。ローカルのツリーが登場する前は、これがプールで最大の漏洩でした。これを使わざるを得ないクライアントは、自分のノードを動かすか、必要なウィットネスと並べて不要なウィットネスも要求すべきです。
rand sync # scanned 41 leaves and 37 blocks; 1 notes, 1 unspent
rand notes # index, asset, amount, height, spent, pending, memo
rand history # notes this wallet created for others, opened through ovk
ノートは、rand send --no-wait から、そのノートのナリファイアを検出するか time + window を超えたブロックを検出する次の sync までの間、pending となります(chain 20 では 1,024 で、rand_getLimits.proof_window_blocks から読み取ります。これがない場合は 256 です)。その後はバンドルが承認されることはなく、ノートは再び使用可能になります。