Rand Protocol
ドキュメント
基本概念

Mempool

ノードがトランザクションを受信してからブロックに入れるまでに行う処理について説明します。受け入れの順序、nonce に代わる競合の追跡、4 つの検証ワーカー、ピアメーターと判定キャッシュ、プロポーザーによるブロックの選び方、そして二重支払いに敗れたトランザクションがひとりでに消える理由を扱います。

rand_sendTransaction がトランザクションを受理してからブロックに載るまでの間に何が起きるのかを、このページで説明します。拒否された場合はその理由、失敗せずにプールから消えることがある理由、ブロックが埋められる順序、不正な証明が大量に送られたときにバリデータに起こせること・起こせないことを扱います。メンプールは crates/randprotocol-node/src/mempool.rs にあり、その前段のガードは admission.rs にあります。

送信者がいないので、ナンスもない

アカウント型チェーンでは、メンプールは送信者ごとにトランザクションをナンスの順に並べます。シールドチェーンには送信者がいません。バンドルにはアドレスも署名もなく、2 つのバンドルが関係するのは同じノートに触れるときだけです。したがって、送信者ごとの順序付けは残っていません。ナンス管理の代わりになるのが競合追跡です。プールは保留中のすべてのトランザクションのナリファイアとコミットメントを索引化し、そのいずれかを主張する新規トランザクションを拒否します。台帳が 1 つの値につきちょうど 1 件のトランザクションしか受け付けない、いくつかのカウンタも同様です。バリデータの register nonce(Unbond または Withdraw)、トークンの mint_nonce(TokenMint と SetAuthority が共有)、RegisterToken が対象とするレジストリのインデックス、ブリッジの pause_nonce と list_nonce がそれにあたります。これらはそれぞれ、プール内のトランザクションが高々 1 件しか保持できないスロットです。こうした 2 件のトランザクションのうちブロックに含められるのは多くても 1 件であり、もう一方を保持すると、プロポーザーはブロック容量を消費し、全ノートはゴシップの 1 巡ごとに証明検証を 1 回余計に行うことになります。

プールが保持できるのは最大 10,000 件のトランザクション(node.rs の Mempool::new(10_000))で、v0.6.8 以降は最大ブロック 8 個分のバイト数でもあります。MAX_POOL_BLOCKS = 8 にチェーンの max_block_bytes を掛けた値で、chain 20 では 160 MiB です(audit v6、CH-7)。それ以前は件数の上限しかありませんでしたが、証明を含むトランザクション 1 件は約 1.3 MB から 12 MB 超に及びます。各トランザクションは、最後にコミットされたブロックではなくコンセンサスツリーの先端、つまりプロポーザーが実際に積み上げる状態の台帳に対して検証されます。

アドミッション:軽い検査から重い検査へ

RPC またはゴシップで届いたトランザクションは次の手順を順に通り、最初に拒否された時点のエラーが表示されます。

  1. 重複。 ハッシュがすでにプールにあります:already in mempool。
  2. 競合。 ナリファイア、コミットメント、または上記のナンスとインデックスのスロットのいずれかを、保留中のトランザクションがすでに主張しています:conflicts with a pending transaction over <hex>。トークンのスロットでは、hex は表示用のキーです。タグ語(mint_nonce なら tokn、登録インデックスなら tidx)に、インデックスとナンスが続きます。2 つのリレイヤーが競合させたブリッジのアテステーションは、そのダイジェストで区別されます:conflicts with a pending transaction over bridge attestation <hash>。
  3. 満杯。 mempool full。容量チェックは 3 つのうち最後です。バイト数の上限に達している場合、新規トランザクションは、自分より厳密に順位が低いプール内のエントリに取って代わることができます。順位は、各エントリの現在の KiB あたりの下限を超える手数料、次に手数料そのもので決まり、収まるまで最も安いものから追い出します。それでも足りない場合、または同順位にとどまる場合は、拒否されるのは新規トランザクションのほうです。ガバナンスアクションは両方の上限の対象外で、追い出されることはありません。対象は、ブリッジのガバナンスアクション 4 種(PauseMints、UnpauseMints、RegisterBridgedToken、ListBacking)と、v0.6.8 以降の RotatePqGuardians、RotatePauseKey、それらの …V2 形式、CancelRotation、AdmitValidator、SlashEquivocation です。これらのバイト数とエントリ数は、引き続き上限に算入されます。PauseMints はガーディアン鍵が漏えいした場合の歯止めです。バンドルも手数料も持たず、攻撃者がミントしている間に必ずブロックへ到達しなければならない唯一のトランザクションなので、プールが満杯でも締め出してはなりません。この免除は競合ステップによって制限されています。pause_nonce と list_nonce はそれぞれ一度に 1 件のトランザクションしかプールに入れられないため、4 種のブリッジアクションが上限を超えて追加できるのは最大 2 エントリです。それ以外はそれぞれ独自のナンスまたはクォーラムを消費します。
  4. Ledger::validate の軽い前半。 アドミッションから包含までの間に古くなり得るもののうち、証明を必要としないものすべてです。アンカーが直近 1,024 個のルートのいずれかであること(chain 20 では genesis の proof_window_blocks、これがない場合は 256)、time がウィンドウ内にあること、ナリファイアが未使用であること、コミットメントが存在しないこと、アテステーションのダイジェストが未主張であること、ステーキングアクションの register nonce が現在のものであること、トークンアクションの mint_nonce または登録インデックスがレジストリの現在位置と一致すること(チェーンがそれを過ぎている場合は wrong mint nonce、wrong token index)、停止またはリスティングのナンスがブリッジの現在のものであることを確認します。トランザクションが作成するコミットメントもここで計算されます。これには、Withdraw、BridgeAttest、アグリゲータへの支払いのために台帳自身が導出するノートも含まれ、これはワイヤ上には現れませんが、やはり衝突してはなりません。サイズの上限はデコードより前に確認されます。チェーンの max_proof_bytes(デフォルトでは 2 MiB、chain 20 では 4 MiB)を超える証明、16 KiB を超えるアテステーション、ブロックより大きいトランザクション(max_block_bytes:デフォルトでは 4 MiB、chain 20 では 20 MiB)は即座に拒否されるため、サイズの大きいデータを送っても計算コストを負わせることはできません。封印(プルーニング済み)マーカー形式のバンドル証明は、封印形式同期の外ではこの同じステップで PrunedFormOutsideSync として拒否されます。その後、まだ何も検証しないまま、プールは証明のヘッダを読みます。まず 3 つのポリシーが適用されます。入力、keccak、sha256 のいずれかのテーブルが 2^7 行未満のコールはプールされません(CallRevealsPrivateInputs)。どのコールも保持できないデプロイも同様です(ProgramUncallable)。バンドル証明またはコール証明に、誠実な証明者なら書かないヘッダフィールドがあるトランザクションも同様です(NonCanonicalProof)。後ろの 2 つは、chain 20 では hardening_v6 のもとで台帳のルールにもなっています。v0.6.8 以降、宣言された次数が最も高いテーブルの上限を超えるヘッダは、チェックが走査する前に NonCanonicalProof として拒否されます(audit v6、ZKV-4)。また、この事前スクリーニングのどこかでパニックが起きた場合は、ノードが落ちるのではなく、そのトランザクションの拒否として扱われます(VerifierPanicked)。次に手数料の下限です。chain 20 では、プールはコールまたは invoke の証明が宣言するガスリミットをデコードし、先端の価格で BUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉ に invoke のセル手数料を加えた額を下回る手数料を FeeTooLow として拒否します(手数料)。これはブロック自身のルールなので、プールはブロックが要求するものと全く同じものを要求します。
  5. 重い後半。コンセンサスループの外で実行。 トランザクションは、4 つの検証ワーカーのいずれかに向けて、最大 64 件までのキューに入れられます。ワーカーはブロッキングタスク上で Ledger::validate の残りを実行します。バンドルのダイジェスト、バンドル証明が宣言するガスリミット(chain 20 ではちょうど 20,479 でなければならず、そうでなければ BundleGasLimit)、バンドル証明、chain 17 以降はトランザクションのバインディングに対するその認可証明、そしてコールの場合はその証明自身と手数料の下限です。検証は先端のスナップショットに対して行われ、このスナップショットは遅延してクローンされます。クローンは先端が変わるたびに多くても 1 回、何かが待機している間だけ行われます。ゴシップで届いたトランザクションがキューの満杯に遭遇した場合は、投票を行うループをブロックするのではなく、検証も転送もされずに破棄されます。ピアの次のゴシップの巡回で再び届きます。
  6. 再チェックとプール。 insert_verified は、証明の検証中に先端が動いた可能性があるため、トランザクションが実際にプールされる先端に対してステップ 4 をもう一度実行し、そのうえでトランザクションを受け入れ、ゴシップ経由のトラフィックであれば転送します。

ステップ 1〜4 が Mempool::precheck で、STARK を検証するのはステップ 5 だけです。そのコストはワーカー上で支払われるため、不正な証明が大量に送られてもスレッドを 4 本占有するだけで、コンセンサスループには影響しません。トランザクションとアクションには、台帳自身のアドミッション順序が載っており、ステップ 4 と 5 を合わせたものがそれを実行します。

ゴシップ向けの 2 つのガードとキャッシュ

アドミッションの前段には 2 つの仕組みがあり、ピアから届くトランザクションにだけ適用されます。RPC での送信はこれらを通りません。そのポートはオペレーター自身のもので、リクエストボディの上限ですでに制限されているためです。RPC 経路で唯一計量されているのは、後述のフォーセットです。

  • ピアごとのバイトメーター。 v0.6.8 以降、すべてのゴシップフレームは、デコードされる前に転送元ピアのバイト予算へ課金されます。バーストは8フレーム、補充は1秒あたり最大送信サイズのフレーム2つ分です。予算を超えたフレームはデコードされずに破棄されます(audit v6、GOSSIP-1)。証明全体をハッシュするトランザクション ID は、転送元の許容量の範囲内でのみ計算されます。
  • ピアごとのトランザクションメーター。 転送ピアごとのトークンバケットで、バースト16、補充は毎秒4です(PEER_TX_BURST、PEER_TX_PER_SEC)。計量の対象はトランザクションを転送したピアであり、作成者ではありません。シールドチェーンでは作成者を特定できないためです。
  • 判定キャッシュ。 このノードが、バイト列のみで決まる理由(不正な証明、不正な署名、サイズ超過のパートなど)で一度拒否したトランザクションは、再度検証されることなく、8,192エントリの FIFO キャッシュ(REFUSED_CACHE_ENTRIES)から拒否されます。このノードの状態に依存する判定(未知のアンカー、使用済みのナリファイアなど)は、決してキャッシュされません。キャッシュすると、1ブロック遅れているノードが、まもなく有効になるトランザクションを自ら拒否し続けてしまうためです。この一覧は許可リストであり、新しいエラーは、誰かが別途決定するまで状態として扱われます。次の例は、その境界線を示しています。
    • BadPauseSignature(the pause signature does not verify)は、v0.6.8 以降は永続的ではありません(audit v6、BRG-18)。この検査はチェーンの現在の pause キーに対する署名を確認しますが、そのキーは bridge rules v2 でローテーションできるため、これは状態です。ローテーションに1ブロック遅れているノードは、そうでなければ再起動するまで、新しい保有者の PauseMints を拒否してしまいます。範囲外の PQ 共同署名インデックスも、同じ理由でキャッシュ対象から外れました。v0.6.7 までのチェーン18と19では、どちらも永続的としてキャッシュされていました。不正な pause が繰り返されると、配信ごとに Dilithium2 の検証が1回かかりますが、その回数はピアごとのメーターで制限されます。
    • PrunedFormOutsideSync は、意図的に永続的ではありません。封印されたバンドルのマーカー形式は、元のトランザクション自身の ID にハッシュされ、キャッシュはその ID をキーにしています。そのため拒否をキャッシュすると、任意のゴシップピアが、正当なトランザクションをネットワーク全体で検閲できてしまいます。
    • 証明が宣言するガスリミットは、証明自身のバイト列とジェネシスの定数との比較で決まるため、BundleGasLimit と AuthGasLimit は永続的です。FeeTooLow は永続的ではありません。チェーン20では、下限が価格とともに動くためです。ステップ4の3つのヘッダーポリシーも決してキャッシュされません。それぞれがこのビルドの知識であり、後のビルドで変わりうるためです。
    • faucet 許可リストを持つチェーン(チェーン20)では、FaucetRecipientNotAllowed は永続的です。mint 自身の受取人をジェネシスの定数と比較するだけだからです。FaucetBudgetExhausted は永続的ではありません。これは状態であり、同じ mint は次のエポックで再び受理されます。
    • トークンアクションの BadSignature も、ステーキングアクションのものとは異なり、永続的ではありません。トークンの mint 権限は状態であり、SetAuthority によって別のキーに渡すことができます。同じバイト列が、そのローテーションがコミットされる前には拒否され、後には受理されうるのです。バイトレベルのトークン拒否(不正な名前やシンボル、小数桁数の超過、ゼロの金額、トークンインデックス0)はキャッシュされます。アクションが設定してはならない burn フィールド、金額に対してオープンできないコミットメントを持つ mint、ブラインディングが導出されたものではないブリッジ入金も同様です。

faucet の制限

rand_mint は手数料がかからず、1回の呼び出しにつきプール内のトランザクションを1つ消費します。チェーン20では、faucet が有効であり、その背後に価値を保有するブリッジがあります。ジェネシスはその動作範囲を制限しています。mint が支払うのは許可リストに載った16のウォレットのみで、1,000ブロックのエポックあたり最大10,000 RAND までです(Staking)。また、ジェネシスの staking.faucet_minters リストにある18のキーのうち、署名できるのは1つだけです(MinterNotAllowed。キャッシュされません。チェーン16以降は台帳のルールで、リストがジェネシスのバリデーターだったチェーン15ではノードのポリシーでした)。これに加えて、各ノードは自身の faucet を計量します。連続8回の mint、補充は毎秒1回です(FAUCET_MINT_BURST、FAUCET_MINT_PER_SEC)。これを超えると、呼び出しは faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly と応答します。この制限は呼び出し元ごとではなくノードプロセスごとです。RPC ポートには計量すべきピアの識別情報がなく、保護する対象がこのノードのプールだからです。上記の容量の免除と合わせて、faucet に大量のリクエストが来ても、PauseMints より先にプールが埋まることはありません。

Gossipsub はアプリケーションレベルの検証で動作するため、トランザクションは、ここで検証を通過して初めて他のピアへ転送されます。無効なトランザクションを大量に送りつけるピアは、メーターによって絞られ、キャッシュから応答され、コンセンサスループには何のコストも負わせません。

ブロックの構築

バリデーターがビューをリードするとき、プールに候補を要求します。candidates_within は、構築中の台帳に今も適用できるトランザクションだけを残します。そのために、受理時の軽い側の検査をやり直します。アンカーがまだウィンドウ内にあること、time がまだウィンドウ内にあること、ナリファイアが未使用であること、コミットメントがまだ存在しないことです。また、チェーンが mint_nonce や登録インデックスをすでに通り過ぎたトークンアクション、古いノンスの pause や listing も除外します。v0.6.8 以降は、構築対象のチップで有効な価格で、受理時に記憶したガスリミットから、すべての call と invoke を再計算し、手数料が現在の下限を下回るものは除外します。除外されたものはプールに残り、価格が下がれば再び候補になります(audit v6、CH-9)。チェーン20では、残りをガバナンス優先、次に現在の下限を超える手数料のトランザクション1 KiB あたりの額、次に手数料の高い順、同額の場合はハッシュで決定という順で並べます。これにより、同じプールをもとに構築する正直な提案者は、すべて同じブロックを選びます。そして、2,000トランザクションとチェーンのブロック上限(MAX_BLOCK_TXS。max_block_bytes はデフォルトで4 MiB、チェーン20では20 MiB)という予算まで埋めます。ブロックに不足するのはバイトであり、そのため超過分は KiB あたりで評価されます。gas セクションのないチェーンや、ガスポリシーのないノードは、そのキーを飛ばし、チェーン15と同様にガバナンス、手数料、ハッシュの順で並べます。ガバナンス優先は、pause の保証のもう一方の半分です。PauseMints は設計上手数料を支払いません。RAND を持たないウォレットからでも動作しなければならないためです。単純な手数料順では、緊急ブレーキが、手数料を支払うすべてのトランザクションの後ろに回ってしまいます。

call または invoke が準備できている間は、もう1つのルールが適用されます(audit v6、POOL-2。すべてのチェーンでのノードポリシーであり、有効性のルールではありません)。バイト単価を支払うのはそれらだけで、基本の定額をわずかに上回る額を支払う転送は、1 KiB あたりでそれらより上位になります。そのため、転送が続くと、準備できている call がどのブロックにも入れなくなる可能性があります。そこで、定額手数料のトランザクションは、最初のパスではブロックのバイト数の4分の3までしか取れません。call が配置された後、残った余地は、保留されていた転送で、その順序のまま埋められます。ブロック空間は失われず、準備できた call がなければ何も変わりません(Fees)。

残りの空間に収まらないほど大きなトランザクションはスキップされ、ブロックはそこで終わりません。その後ろにある小さなものは、引き続き含められます。チェーン18の転送は約2.85 MB(1.49 MB のバンドル証明と1.36 MB の認可証明)なので、20 MiB のブロックには約7件、デフォルトの4 MiB のブロックには1件が収まります。チェーン15では、転送1件が約1.43 MB で、同じ20 MiB に14件が収まりました。負荷がかかったときは、どれが入るかを手数料が決めます(Fees)。

プルーニング

コミットのたびに、prune は、candidates_within がスキップするものとちょうど同じものをプールから取り除きます。対象は、ノートが他者によって使用されたトランザクション、コミットメントがすでに存在するトランザクション、ノンスや登録インデックスをチェーンが通り過ぎたトランザクション、アンカーまたは time がウィンドウ(チェーン20では1,024ブロック、チェーン19までは256ブロック)から外れたトランザクションです。したがって、二重支払いの競争に負けたバンドルは、誰にもエラーを返すことなく、自然に消えます。rand_sendTransaction による受理はコミットではありません。rand_getTransaction がブロックを返すまでポーリングし、ブロックが得られないままアンカーウィンドウが過ぎた場合は、新しいアンカーでバンドルを作り直して再送信してください。

監視

rand_status は、mempool_size、verify_queue(ワーカーを待っているトランザクション)、refused_cache(判定キャッシュのエントリ数)を報告します。verify_queue が64に張り付いている場合は、証明の到着が、4つのワーカーによる検証(キーがウォームな状態で1件あたり約16 ms)より速いことを意味します。ブロックが満杯のまま mempool_size が増え続ける場合は、受理の順序ではなく手数料が、含まれるかどうかを決めていることを意味します。

まだ実装されていないもの

  • 置換は不可。 Chain 20 では、ガス価格とバイト価格がブロックごとに上限まで動き、それに応じて call の下限手数料も変動します。transfer の下限手数料は固定です。同じ note を対象とする、より高い手数料のトランザクションでプール内のトランザクションを置き換えること(replacement)はできません。競合ルールがこれを拒否します。価格に届かなくなった call は、価格が下がるか anchor の期間が過ぎるまで、提案されないままプールで待機します。v0.6.7(chain 18 と 19)では、プール内の call は受理された時点の下限手数料を保持したままで、価格が上昇した後も提案対象となり、proposer の試行適用で FeeTooLow として失敗していました。fullnode のブロックスペースに関する文書では、手数料順のメモリプールはより大きなブロックに先立つ段階とされ、ブロック単位のアグリゲーションがブロックごとの上限を引き上げるものとされています。
  • 経過時間による失効なし。 プール内のトランザクションがプールを離れるのは、ブロックへの取り込み、バイト上限での追い出し、または anchor と時間の期間のいずれかによる場合のみです。Chain 20 では 1,024 ブロック、explorer が同チェーンで報告した 1 ブロックあたり 1.34 秒で約 23 分です。Chain 19 までは、この期間は 256 ブロック、約 5 分でした。
  • メモリプール RPC なし。 保留中のトランザクションを一覧する method はありません。確認できるのは、rand_status の mempool_size と、rand_getMempoolInfo の件数、バイト数、最古の経過時間のみで、explorer が索引化するのはコミット済みのブロックだけです。

fullnode のコミット c0878c3f(チェーン 21)に対して 2026-10-12 に最終確認しました。また、fullnode v0.6.8 `c9c9bd3c` (chain 20): `crates/randprotocol-node/src/mempool.rs` (`MAX_POOL_BLOCKS`, `make_room`, `is_governance`, the call reservation in `candidates_within`), the commits for CH-7, CH-9, POOL-2, BRG-18, GOSSIP-1 and ZKV-4, and `rand_getLimits` on chain 20, fullnode v0.6.7 `86941a1`: `crates/randprotocol-node/src/mempool.rs`, `admission.rs`, `crates/randprotocol-core/src/ledger/mod.rs` (the gas floor), fullnode `90e7716`: `docs/architecture.md` §8, `docs/rpc.md` (changelog, the 2026-09-28 gas entries), fullnode `ed88241`: `docs/confidential.md` (transaction binding)。 コードとこのページが食い違う場合は、コードが正です。お知らせください。