このページでは、トランザクションがいつファイナルになるか、何台のバリデータが稼働している必要があるか、RPC やエクスプローラーに表示されるブロックのフィールドが何を意味するかを説明します。コンセンサスはステーク加重のクォーラムを用いる連鎖型 HotStuff BFT で、chain 20 では 26 台のバリデータのうち 18 台がクォーラムです。
ノードが実行するルール
| ルール | 実装 |
|---|---|
| クォーラム | クォーラム証明書(QC)は、総ステークの2/3 を厳密に超えるステークを保有するバリデータによる Dilithium2 の投票の集合です |
| リーダーのスケジュール | 純粋なラウンドロビンです。ビュー v のリーダーは validators[v mod n] で、集合はアドレス順に並べます。ステークが重み付けするのは投票であり、リーダーの選出ではありません |
| 提案 | ビュー v のリーダーは、v − 1 の QC、または v に対する NewView メッセージのクォーラムを保持した時点で提案します。無効なトランザクションを適用することになる提案では、ブロックではなくそのトランザクションが除外されます |
| 投票とセーフティ | バリデータはビューごとに一度だけ、ロックに対して安全なブロックにのみ投票します(そのブロックの justify QC がロック済みの QC より新しいか、ブロックがロック済みのブロックを延長している場合)。セーフティの状態は、投票がノードから送出される前に RocksDB へ fsync されます |
| 署名ドメイン | consensus_domain: 1 を持つジェネシス(chain 20、および chain 15 以降のすべてのチェーン)では、すべての投票、new-view、提案の署名がジェネシスハッシュ(rand-vote-2、rand-newview-2、rand-block-2)を対象とするため、あるチェーン向けの署名は他のどのチェーンでも検証を通りません。chain 14 以前はこれなしで署名していました。chain 20 の binding_domain: 1 は、同じルールをトランザクションの証明と署名済みアクションに拡張します(Transactions) |
| 兄弟提案 | レプリカは (ビュー, リーダー) ごとに 1 つのブロックを保持します。同じリーダーが同じビューに対して別のブロックを送ってきた場合は、equivocation(二重提案)として拒否されます。レプリカ自身のビューより 8 ビューを超えて先の提案(PROPOSAL_VIEW_WINDOW)は、署名を検証する前に拒否されます。投機的ツリーが満杯の場合は、何かを拒否する前にブランチ外のブロックを追い出します |
| ロックの解除 | ロックは自動的には下げられません(CON-4、v0.6.8)。ロックを解くのは、justify QC がそれを上回る提案か、ロック済みのブロックの到着だけです。署名済みの NotHeld 応答のクォーラムはオペレーター向けにログへ記録されるだけで、何も解除しません。また、バリデータは自分が投票したブロックについて NotHeld に署名しないよう、投票を記録しています。ヘッドより上の認証済みブロックはすべて永続化され、再起動時に復元されるため、フリート全体を再起動しても、誰も保持していないブロックに QC が取り残されることはありません。ロック済みのブロックが完全に失われたバリデータは、オフラインで手動により解除します: rand-node safety release-lock |
| QC の組み立て | すべてのバリデータが、ゴシップされた投票から QC をローカルで組み立てます。そのため、次のリーダーがオフラインでもチェーンはコミットを続けます |
| コミット | 連鎖型の 3 QC ルールです。ブロックは、その上に 3 つの証明書が連続するビュー v, v+1, v+2 で連鎖し、その連鎖がコミット済みのヘッドまでつながっている場合にコミットされます |
| タイムアウトとビュー変更 | 基本の --view-timeout-ms は 3,000 ms で、連続するタイムアウトごとに倍になり、進捗があればリセットされます。タイムアウト証明書によるペースメーカー(CH-1、v0.6.8): タイマーが発火したレプリカは自分のビューにとどまり、次のビューを求める NewView に署名します。レプリカがビューに入るのは、直前のビューの QC を得たとき、または 2/3 を超えるステークを持つバリデータがそのビューかそれ以降のビューを求めたときです。自分のビューより先のビューを 1/3 超が求めた場合は、自らも加わります(自分の要求に署名します)。単独のバリデータの NewView では、誰も動きません。ビューには上限があります(MAX_VIEW_AHEAD = 10⁶) |
| ペース配分 | 保留中のものが何もないとき、リーダーは、いずれかの提案者から最後に見たブロックから少なくとも --block-interval-ms(デフォルト 1,000 ms)後に空のブロックを提案します |
| 投票とゴシップの上限 | 8 ビューを超えて先の投票は無視され、(ビュー, 投票者) ごとに 1 票だけが数えられます。コンセンサスのゴシップは転送元のピアごとに制限されます(バースト 256、64/s)。親が不明な提案がオーファンとして保持されるのは、その提案者が現在のエポックの集合または次のエポックの集合でそのビューのリーダーであり、コミット済みヘッドからツリーの上限内にあり、サイズとトランザクションルートが妥当な場合に限られます(オーファンプールの上限は 64 MiB で、v0.6.1 以降は提案者ごとに 16 オーファンまで)。v0.6.8 以降、提案がリレーされ、オーファンが保持されるのは、そのトランザクションリストのハッシュがヘッダーのコミットするルートと一致し、トランザクションの重複がない場合に限られます(GOSSIP-2) |
| ブロックの上限 | チェーンの max_block_bytes 分のトランザクションバイト(デフォルトは 4 MiB、chain 20 のジェネシスでは 20 MiB)と 2,000 トランザクションで、提案者が強制し、すべてのレプリカが Ledger::apply_block で再度強制します |
| ブロック時刻 | bridge セクションを持つチェーンの場合: ブロックの timestamp_ms は親より前であってはならず(TimestampRewind)、親より 60 秒を超えて進んでいてもなりません(TimestampLeap)。また、バリデータは自身の時計より 15 秒を超えて先のブロックには投票しません。詳しくは後述します |
連続する 3 ビューのコミットルールは、全体を支える要です。これを緩めると、フルノードに回帰テストのある、競合するファイナリティのバグが再発します。クライアントが RPC を通じて目にするものはすべてコミット済みです。提供されるのはコミット済みのブロックだけで、WebSocket の newHeads ストリームは、コミットされたブロックごとに 1 件の通知を順番に送ります。
この表は、chain 20 が稼働させているリリースである v0.6.8 のものです。そのうち 3 つのルールは v0.6.8(audit v6)で新たに加わったもので、chain 20 が稼働を開始した 2026-10-01 から有効です。タイムアウト証明書によるペースメーカー(CH-1)、ロックの自動解除の廃止(CON-4)、提案のトランザクションリスト検査(GOSSIP-2)です。chain 18 と chain 19(v0.6.7)では、単独のバリデータが署名した NewView だけですべてのレプリカがそのビューへ移動し、ロックはステークのクォーラムによる署名済み NotHeld 応答で下げられていました。ペースメーカーとロックのルールは、バリデータが何に、いつ署名するかを変えるため、v0.6.8 は chain 20 のカットとともに全停止・全起動でロールアウトされ、ノードを 1 台ずつ更新することはありませんでした。
testnet の数値
chain 20 のバリデータは 26 台で、それぞれがちょうど 1,000 RAND をステークしており、26 台すべてが、chain 17 から chain 19 が使っていたキーとピア ID でジェネシスに含まれています。chain 15 ではジェネシスに 18 台が含まれ(chain 14 のキー)、さらに 8 台が v2 登録で 2026-09-27 にボンドし、エポック 43 から有効になりました。ステークは均一なので、クォーラムは頭数で決まります。何かをコミットするには、26 台のうち 18 台がオンラインである必要があります。 17 台が稼働している場合、ビューは進みますが、何もファイナライズされません。同額のバリデータが n 台の集合では、ステークの 2/3 超が必要です: 2 台中 2 台、4 台中 3 台、6 台中 5 台、18 台中 13 台、26 台中 18 台。ジェネシスの重み上限(バリデータあたり 3,333 bps)は、同額ステークの集合には作用しません。
ブロックはデフォルトの 1,000 ms でペース配分されます。chain 15 では実際には約 1.2 秒ごとに到着しました(最初の 29 時間の平均は 1.18 秒)。chain 20 では、エクスプローラーが高さ 3,270(2026-10-01、avg_block_time_ms)で平均 1.34 秒と報告しました。エクスプローラーが報告するのは、観測した平均ブロック時間です(GET /stats の avg_block_time_ms)。ビューがタイムアウトするたびに、バリデータのビューは高さより先に進みます。rand_getHead が両方を報告するのはそのためです。
エポック
バリデータ集合は、epoch_blocks = 1,000 ブロック(ジェネシスのパラメータ)ごとにレジスターから再導出されます。epoch(h) = h / 1000 で、エポック e ≥ 1 の集合は、エポック e − 1 の最後のブロック時点のレジスターから導出されます。対象は stake ≥ 1,000 RAND のすべての行のうちステーク上位 100 で、アドレス順に並べます。staking セクションのないチェーンでは、ボンドは次の境界で重みになります。chain 20 ではさらに 2 エポック待たされ、さらにエポックごとの参入枠として、境界時点のアクティブなボンド済みの重みの 2,500 bps(集合が 26,000 を保有している間は 6,500 RAND)が適用されます。また、新しいバリデータを登録するボンドは、集合が先に AdmitValidator でそのキーを投票により受け入れていない限り拒否されます(staking.admission_by_vote)。chain 19 の枠はエポックあたり固定の 10,000 RAND で、受け入れ投票はありませんでした。アンボンドは次の境界で重みを減らしますが、その額が引き出し可能になるまでには 2 エポック待ちます。rand_getEpoch は、エポック、エポックの長さ、次の集合の予測を返します。Staking を参照してください。
ブロックが運ぶもの
{
"hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
"proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
"tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
"tx_count": 0, "transactions": []
}
view はブロックが提案されたビューで、justify_view はブロックが親のために運ぶ QC のビューです。timestamp_ms は提案者が設定し、次のセクションの範囲内に収まります。state_root はブロック適用後のレジャーにコミットします。chain 20 では次のとおりです。
inner = blake3("rand-state-8" || tree_root || nullifier_root || validators_root || programs_root
|| bridge_root || tokens_root
|| faucet_epoch || faucet_minted_in_epoch || bond_queue_root
|| gas_price || byte_price
|| program_state_root)
state_root = blake3("rand-state-admitted-1" || inner || admitted_root)
chain 19 は、program_state_root なし、ラッパーなしで、rand-state-7 のもとに同じフィールドをコミットしていました。
コンポーネントはジェネシスによって決まります。オプションのセクションを一つも持たないチェーンは、最初の四つを rand-state-2 の下でコミットします。bridge セクションがあると bridge_root が追加されます。tokens セクションがあるとその後ろに tokens_root が追加され、ハッシュ全体のドメインが rand-state-4 に切り替わります。bridge セクションを持つジェネシスは tokens セクションも持たなければならないため、chain 20 には両方があります。aggregation セクションがあると、次にアグリゲータのルートが追加されます(トークンのないチェーンでは rand-state-3 の下)。chain 20 にはありません。staking セクションがあると、最後にファウセットの二つのエポックカウンタとボンドキューのルート(rand-bond-queue-1)が追加され、ほかの構成にかかわらずハッシュ全体のドメインが rand-state-5 に切り替わります。chain 14 にはこのセクションがなく rand-state-4 でハッシュしており、chain 15 から 17 は rand-state-5 でハッシュしていました。vesting セクションがあると、その後ろにベスティングレジスタのルートが追加され、ハッシュのドメインが rand-state-6 に切り替わります。chain 20 にはありません。dynamic コントローラを持つ gas セクションがあると、ほかのすべての後ろに現在の gas_price と byte_price(それぞれビッグエンディアンの u64)が追加され、ハッシュのドメインが rand-state-7 に切り替わります。これが chain 19 のドメインでした。chain 20 も価格をルートに含めており、価格はブロックごとに一度、ブロックのトランザクションの後、ルートの前に更新されます(Fees)。価格が固定の gas セクションはルートに含まれません。program_state セクション(RPL-2)があると、価格の後ろにすべてのプログラムのセルとボールトのルートが追加され、ハッシュのドメインが rand-state-8 に切り替わります。これが chain 20 のものです。Audit v6 のステーキング状態は、この内側ではなく外側に折り込まれます。staking.admission_by_vote(chain 20)の下では、ルートは blake3("rand-state-admitted-1" || inner || admitted_root) となり、staking.slashing(まだ採用しているチェーンはありません)の下では、二つ目のラッパーが jail 済みの集合を加えます。tokens_root は、blake3("rand-token-leaf-1" || bincode(TokenInfo)) のリーフをインデックス順に並べた Merkle ルートです。そのため、すべてのトークンのメタデータ、権限、mint_nonce、供給量、各バッキングの locked の額がコンセンサス状態になります。あるトークンの行について二つのノードの見解が食い違えば、チェーンについても見解が食い違います。chain 20 では、トークンルートにレジストリ拡張(max_tokens の上限、焼却された登録手数料、ノート価値の上限、ブリッジのローリングなミント上限ウィンドウ)も rand-token-registry-3 の下で折り込まれます。ブリッジルートは、エミッタ、ガーディアンセット、使用済みのアテステーションダイジェスト、バーンシーケンス、PQ ガーディアンキー、一時停止キー、一時停止フラグ、そして二つのガバナンスノンスをカバーします。アセットレジストリはそこから tokens_root に移されました。chain 20 のブリッジは rules_v2 を備えているため、ローテーションノンスとグローバル上限のパラメータが追加され、ドメインは rand-bridge-state-5 になります(chain 14 は rand-bridge-state-4 でした)。このルートはさらにインバウンドのリプレイフロア(rand-bridge-replay-floor-1)でラップされ、chain 20 ではローテーションルールと保留中のローテーション(rand-bridge-rotation-1)、および手数料グループ(rand-bridge-fees-1)でもラップされます。バリデータレジスタは validators_root にハッシュされ(リーフのドメインは chain 20 では各行の activation_epoch を含む rand-validator-leaf-4、staking セクションのないチェーンでは rand-validator-leaf-2)、レジスタの一行について見解が食い違うノードは、チェーンについても見解が食い違います。
ブロック時刻には上限と下限がある(B2)
ブリッジされたチェーンでは、ブロックのタイムスタンプがガーディアンセットの失効と、バッキングの日次ミント上限がどの日に計上されるかを決めます。そのため、時計を先に進められるリーダーは、ローテーションされたセットの猶予期間を早く失効させたり、ある日の上限を飛び越えたりできてしまいます。chain 20 には bridge セクションがあるので、次の三つのルールが適用されます。いずれもこのセクションを条件としています。
| ルール | 境界 | 種別 |
|---|---|---|
| 巻き戻し禁止 | timestamp_ms ≥ parent.timestamp_ms(等しい場合は許可)。満たさなければ BlockError::TimestampRewind。プロポーザは max(now, parent.timestamp_ms) を出力する | 有効性ルール。リプレイと同期でも検査される |
| 飛躍禁止 | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS(60,000 ms)。満たさなければ BlockError::TimestampLeap | 有効性ルール。リプレイと同期でも検査される |
| クロックドリフト | バリデータは、自身の時計より MAX_CLOCK_DRIFT_MS(15,000 ms)を超えて先のブロックへの投票を拒否する | 投票ルールのみ。ブロックは投機的ツリーに残り、時計が一致するクォーラムであれば認証でき、コミット済みの履歴のリプレイでは適用されない |
ブリッジされたチェーンのバリデータはすべて NTP が必須です。 時計が 15 秒を超えてずれているバリデータは、障害のあるリーダー(そのプロポーザルが票を得られないことがある)であり、同時に障害のある投票者(正直なリーダーの時刻どおりのブロックを拒否することがある)でもあります。正直な過半数の時計が一致している限り、どちらもチェーンを停止させませんが、同期していないフリートは理由もなくライブネスの余裕を失います。
ジェネシスの timestamp_ms は、実際のローンチ時刻に近くなければなりません。過去に寄りすぎると、ステップの上限によって追いつけるようになるまで、ブロック時刻が実時間より遅れます。追いつけるのは 1 ブロックあたり最大 60 秒です。未来に 15 秒を超えて寄りすぎると、ブロック 1 はまったくコミットされません。リーダー自身のものを含むすべての投票が、実時間がその時刻に達するまで保留されるためです。長時間の停止の後には、同じ追いつきによってミント上限の日が実際の一日より速く繰り上がることがありますが、その上限は、蓄積した遅れの丸一日につき、追加で一日分の上限までです。
クライアントにとってのファイナリティ
rand_getTransaction(hash)がブロックを返した時点で、トランザクションはファイナルです。rand_sendTransactionによる受理は、メモリプールへの受け入れにすぎません。- RPC が返すブロック、またはエクスプローラがインデックスしたブロックは、コミット済みです。 待つべきファイナリティウィンドウはありません。エクスプローラのガイドも同じことを述べています。
rand_subscribeの["newHeads"]から届くヘッドは、ノードが失うことのないヘッドです。 ブロックがストレージにコミットされるまで、何も送信されません。- 遅れたノードはすべてを再検証します。 同期されたブロックを受け入れる前に、すべての証明書、すべてのバンドルと呼び出しの証明、すべてのレシートを検証します。ファイナリティの署名が証明の代わりに信頼されることはありません。
復旧のためのチートシート(フルノード自身のものより):peer_count が低いまま高さが進まない場合は、ネットワークの問題です。ピアは正常なのに高さが進まない場合は、オンラインのステークが 2/3 に満たないことを意味します。起動時の CORRUPT CHAIN は、ノードが破損した末尾を切り詰め、再同期していることを意味します。