この章では、稼働中のチェーンでバリデータセットに加わる方法、離脱する方法、登録簿を読み戻す方法を説明します。コンセンサス上の重みは公開されている必要があります。そのため、バリデータ登録簿はこのチェーンで唯一、金額が平文で記録されるテーブルです。それでもステークは shielded な note から引き出され、再び note に戻ります。
登録簿
一度でもボンドしたバリデータごとに1行があり、base58 アドレスがキーです。
ValidatorEntry {
public_key, // Dilithium2: signs blocks, and this validator's staking actions
stake: u64, // bonded: the weight an epoch's set gives it
pending: Vec<(u64, u64)>, // unbonding, as (release_epoch, amount), oldest first
rewards: u64, // fees earned as a block proposer, unpaid
payout: ShieldedAddress, // rand1… — where a withdraw pays
nonce: u64, // replay protection for its signed actions
activation_epoch: u64, // under a `staking` section: the first epoch this row may be in a set
}
すべてのフィールドは公開され、登録簿は各ブロックのステートルートにハッシュされます。行が削除されることはありません。すべてをアンボンドしたバリデータにも、stake = 0 の行が残ります。
| 定数 | 値 | 意味 |
|---|---|---|
MIN_STAKE | 1,000 RAND | エポックのセットに入るために行が必要とするステーク |
MAX_VALIDATORS | 100 | エポックが持てるセットの最大数 |
UNBONDING_EPOCHS | 2 | アンボンドされた金額が引き出し可能になるまで待つエポック数 |
epoch_blocks | chain 20 では 1,000 | 1エポックあたりのブロック数。genesis パラメータ |
エポック
epoch(h) = h / epoch_blocks です。エポック e ≥ 1 のセットは、エポック e − 1 の最後のブロック時点の登録簿から導出します。stake ≥ MIN_STAKE のすべての行のうち、(stake の降順, address の昇順) で上位 MAX_VALIDATORS 件を選び、アドレス順に並べます。ビューのリーダー、ブロックの投票者、証明書の検証はすべて、そのブロックが属するエポックのセットを使います。これには2つの帰結があります。
- ボンドは、次以降の境界までは重みになりません。 ボンドは登録簿には即座に反映されますが、セットが変わるのは境界でのみです。
stakingセクションのないチェーンでは次の境界、chain 20 ではそれより後になります(後述)。 - アンボンドは、次の境界で重みを失わせます。 一方、その金額が引き出し可能になるまでには2エポックかかります。バリデータは、引き出せるようになるずっと前に提案をやめます。
導出結果が空の場合は、前のセットを引き継ぎます。--validator 付きで起動したノードは、その鍵がどの現行セットにも含まれていなければ、あるエポックが受け入れるまで観察役にとどまります。したがって、genesis 後にボンドして加わるバリデータは再起動を必要としません。rand_status は is_validator(鍵がここにある)と active_validator(現行セットに含まれている)を別々に報告します。
staking セクション
genesis には staking セクションを含めることができます(fullnode v0.5.4、audit v4 STAKE-2)。このセクションがなければ、上記のルールがすべてで、chain 14 がそれにあたります。chain 15 以降のすべてのチェーンがこのセクションを持っています。chain 20 で有効なスイッチは次のとおりです。
"staking": {
"faucet_budget_per_epoch": "10000000000000", "bond_activation_epochs": 2,
"max_weight_bps": 3333, "max_stake_entry_bps_per_epoch": 2500,
"admission_by_vote": true, "registration_v2": true,
"faucet_recipients": ["rand1…", "…16 wallets…"],
"faucet_minters": ["<validator address>", "…18 validators…"]
}
| フィールド | chain 20 | ルール |
|---|---|---|
bond_activation_epochs | 2 | エポック e で適用されたボンドは、エポック e + 1 + 2 に向けてキューに入ります。新規登録も追加ボンドも同じです。その金額は直ちに stake に含まれますが、その時点までは重みにならず、キュー中のステークはアンボンドできません |
max_stake_entry_bps_per_epoch | 2,500(25 %) | 1回の境界で重みになれるステークの上限(新規登録と追加ボンドの合計)。その時点で既に有効な重みに対する割合で、⌊active · bps / 10,000⌋ です。上限を超えた期日到来分の行は、ボンド順に次の境界まで待ちます。chain 20 の有効ステーク 26,000 RAND では、1境界あたり 6,500 RAND になります。新規参入者のシェアは、k 回の境界を経た時点で、どの規模でも高々 1 − 1.25^−k で、2回後には約3分の1です |
admission_by_vote | true | 新しい鍵を登録するボンドは、投票重みの3分の2を超えるバリデータがその鍵に賛成票を投じるまで拒否されます(NotAdmitted)(後述)。追加ボンドに投票は不要です |
max_weight_bps | 3,333 | どのバリデータの重みも、そのセットの総重みの 33.33 % を超えません。超過分はセット内で切り詰められ、登録簿の stake は変わりません |
registration_v2 | true | 登録は、genesis ハッシュ、チェーン ID、バリデータのアドレス、ペイアウトに対して rand-register-2 で署名します。v1 の登録は拒否されます |
faucet_budget_per_epoch | 10,000 RAND | faucet がエポックごとにミントできる上限です(超過分は FaucetBudgetExhausted。カウンターは次のエポックで再開します) |
faucet_recipients | 16 ウォレット | faucet の Mint が支払えるのはこれらのウォレットだけです(FaucetRecipientNotAllowed)。このリストがあるから、faucet をブリッジと並べて置けます |
faucet_minters | 18 バリデータ | faucet の Mint に署名できるのはこれらのバリデータ鍵だけです(MinterNotAllowed)。別の鍵がどれだけ長くボンドされていても、受け入れ時とブロック適用時に検査されます。chain 20 では、26 の genesis バリデータのうち 18 がこれにあたります。このリストは chain 16 以降のすべての genesis に含まれています |
slashing | なし | slashing セクションがないため、スラッシュされるバリデータも、ジェイルされるバリデータもありません(公開される情報) |
このセクションがある場合、faucet カウンターとボンドキューがステートルートの末尾に追加され、ドメインは rand-state-5、バリデータのリーフは rand-validator-leaf-4 になります。chain 20 にはさらに、動的なガス価格と RPL-2 のプログラム状態があり、これらがその後ろに追加されてドメインは rand-state-8 に移ります。さらに投票による承認が、そのルートを H("rand-state-admitted-1", root ‖ admitted_root) としてもう一度包みます。chain 15 には faucet_minters リストがありませんでした。そこでは、すべてのノードが faucet の Mint をプールして転送するのは、genesis バリデータが署名した場合のみ(ノードのポリシーとしての MinterNotAllowed)でした。genesis 後にボンドしたバリデータが、プールを通じて予算を使い果たせないようにするためです。リストがあれば、同じ拒否が有効性ルールになります。
経緯: chain 15 から 19 には admission_by_vote がなく、1,000 RAND を持つ鍵ならどれでも登録できました。また、参入予算は1境界あたり 10,000 RAND という固定の max_stake_entry_per_epoch で設定されていました。固定値は1つの genesis に合わせた大きさですが、割合であれば、ステーク総量が変わっても参入者のシェアを同じ水準に抑えられます。この2つのフィールドを同時に設定することはできません。
投票による承認
fullnode v0.6.8(audit v6、STAKE-2)で新たに導入され、chain 20 で有効です。1,000 RAND の鍵が26個あるとき、セットの3分の1のブロッキング少数派は、購入したステークで約 13,000 RAND にすぎず、しかもスラッシュはありません。このルールのもとでは、どの新しい鍵が登録できるかをセット自身が決めます。
- 投票はアクションです。
AdmitValidator { candidate, signatures }で、バンドルも手数料もありません。各署名は、バリデータによるblake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address)に対する Dilithium2 署名で、投票者のアドレスの昇順に並べます。投票者は、投票セットの重みの3分の2を厳密に超える分を保有していなければなりません。これは quorum certificate の算術と同じで、chain 20 では 26 バリデータのうち 18 です。投票は1つの genesis に紐づき、nonce は不要です。承認は、それが許可する登録によって消費されます。 - 投票セットは、現行エポックについての登録簿自身の導出結果です。キュー中のボンドはその中では重みにならず、アンボンドしたバリデータは直ちにそこから外れます。候補者が自分自身に賛成票を投じることはできません。
- 承認済みセットはコンセンサス状態で、登録待ちの鍵が最大 256 個です(超過すると
AdmissionSetFull)。rand_getAdmittedがこれを返します。チェーンが投票を必要とするかどうかは、rand_getLimits.admission_by_voteでわかります。
# each voting validator, offline
rand-node admit sign --candidate <registration hex or public key> --key node.key.json --genesis-hash <hash>
# whoever collects the lines
rand-node admit submit --candidate <…> --signature <line> --signature <line> … --rpc http://127.0.0.1:8545
その後、ボンドは以下のとおり進みます。admit sign に必要なハッシュは rand_getGenesisHash が返します。
4つのコマンド
どのバイナリがどのアクションを担うかは、どの鍵が署名するかで決まります。ボンドは note を使うのでウォレットのコマンドです。アンボンドと引き出しには、ノードだけが持つバリデータの Dilithium2 鍵で署名します。
| アクション | コマンド | 載せる先 | 支払い |
|---|---|---|---|
| register | rand-node register | なし(blob を出力) | なし |
Bond | rand bond | burn_r がステークとなるバンドル | ウォレットの note から 0.001 RAND |
Unbond | rand-node unbond | なし(bundle: null) | なし |
Withdraw | rand-node withdraw | なし(bundle: null) | 引き出す金額から 0.001 RAND |
登録
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2 [--rpc http://127.0.0.1:8545]
バリデータのアドレスと、16進の Registration(公開鍵、ペイアウトアドレス、署名)を出力します。chain 20 で必須の --v2 を付けると、署名はチェーンの genesis ハッシュ、チェーン ID、バリデータのアドレス、ペイアウトに対する rand-register-2 の下で行われます。付けない場合は v1 形式の blake3("rand-register" ‖ bincode(chain_id, payout)) となり、chain 20 はこれを拒否します(chain 20 のような binding_domain: 1 のチェーンでは、登録メッセージはいずれにせよ v2 のものです)。チェーン ID と、--v2 の場合は genesis ハッシュを RPC から読み取るため、あるチェーン向けに署名された登録は別のチェーンでは拒否されます。ペイアウトアドレスはウォレットアドレスであり、ノード鍵ではありません。これは、後の追加ボンドでも変更できない唯一のフィールドです。16進文字列は、ステークを保有する人に渡してください。
ボンド
rand bond <validator base58> <amount in RAND> [--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]
ごく普通のシールド取引です。ウォレットは、chain 14 以降すべての取引が備えている 4 入力 4 出力の hidden-asset バンドルを証明します。ステーク額は RAND のバーンである burn_r に入り、台帳は burn_r == amount のときに限ってボンドを受理します。chain 20 では、chain 17 以降のすべてのバンドルと同様に、バンドルは 2 つの証明を持ちます。tier-14 のバンドル証明と、スペンドキーに対する tier-10 の認可証明です。どちらも既定ではローカルで証明され、Apple Silicon のノート PC で約 2 分かかります(Proving memory)。ウォレットの --prover オプションを使うと、バンドル証明は連携した prover が作成し、認可証明はウォレットのマシン上に残ります。--registration は、バリデータがまだレジスタにいないときに限って必須で、すでにいるときは拒否されます。ウォレットは最初にレジスタを確認します。chain 20 では、セットがそのキーを投票で承認していない限り、新規登録のボンドは NotAdmitted で拒否されます(Admission by vote)。追加ボンドには投票は不要です。新規登録のボンドは 1,000 RAND 以上で、追加ボンドは 0 を超える任意の額にできます。チェーンが知るのは、このバリデータのステークがこの額だけ増えたことだけです。どのノートが支払ったか、それが誰のものだったかは、送金の場合と同様に秘匿されます。つまり、金額以外は何も明かさずに、誰でもバリデータへステークできます。
submitted bond 8c3f…e1
0 RAND out, 1000 RAND burned, 0.999 RAND change, fee 0.001 RAND, anchored at height 412
2nRdFC…: stake 1000 RAND, counting as consensus weight from epoch 69
ウォレットが表示するエポックは、次の境界のものです。アクティベーション遅延は数えられていません。chain 20 では、エポック 68 でコミットされたボンドが重みになるのは、早くてもエポック 71 からです。エントリー予算(境界ごとにアクティブな重みの 4 分の 1)を使い切っている場合は、さらに後になります。
Unbond
rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]
amount を stake から pending へ移し、release_epoch = epoch + 2 を設定して、nonce を 1 増やします。手数料は無料でバンドルもありません。署名の対象は、chain 14 から 19 では blake3("rand-unbond" ‖ bincode(chain_id, validator, amount, nonce))、chain 20 では blake3("rand-unbond-2" ‖ bincode(genesis_hash, chain_id, validator, amount, nonce)) です(genesis の binding_domain: 1)。コミットはブロック 1 つ分の時間で済みます。rand-node v0.6.8 は、接続先のチェーンに合った形式を選びます。ステークを超える額やゼロのアンボンドは拒否されます。同じエポック内の複数のアンボンドは、1 つの pending 行にまとめられます。
Withdraw
rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]
解放済みの価値、すなわち rewards と、release_epoch に達した pending の行(古いものから順)を、レジスタの payout アドレス宛ての入金ノートとして支払います。バリデータが署名し(chain 20 では rand-withdraw-2 のもと、アンボンドと同様に genesis ハッシュを先頭にして署名します)、バンドルはありませんが、無料ではありません。0.001 RAND の基本料金は、それを適用したブロックのプロポーザに、金額の中から支払われます。したがって 1,000 RAND の引き出しでは、価値 999.999 のノートが作られます。基本料金を賄えない額は拒否されます。ノートはチェーン自身が計算します。
cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)
time はコマンド実行時のヘッド高です(表示され、署名にもバインドされます)。バンドルウィンドウ内であればどこでも受理されます(chain 20 では 1,024 ブロックで、genesis の proof_window_blocks で設定されます。それ以前のチェーンでは 256 ブロックです)。r は公開されるため、チェーンがワイヤ経由でコミットメントを受け取ることはありません。その後、支払先ウォレットは、何も知らされなくても、スキャンによってノートを見つけます。
rand --key payout.key.json balance # balance: 999.999 RAND
参加と離脱
# on the joining validator's machine
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2
rand-node run --datadir ./data --key node.key.json --validator --bootstrap /ip4/…/p2p/…
# it syncs and observes; rand_status says is_validator true, active_validator false
# on chain 20: validators holding more than two thirds of the weight vote the key in
rand-node admit sign --candidate <hex from above> --key <their node.key.json> --genesis-hash <hash>
rand-node admit submit --candidate <hex from above> --signature <line> … --rpc http://127.0.0.1:8545
# on the machine holding the stake
rand bond <validator base58> 1000 --registration <hex>
rand validators # the new row, active false until it activates
# from its activation epoch on (on chain 20, three boundaries after the bond at the earliest)
rand-node status # active_validator: true
rand-node unbond 1000 --key node.key.json # out of the set at the next boundary
# wait two epochs
rand-node withdraw 1000 --key node.key.json # a note worth 999.999 RAND at the payout address
アンボンドする前に、クォーラムの計算を確認してください。n 個のセットでは、ステークの 2/3 を超える分がオンラインである必要があります。chain 20 では 26 のバリデータすべてがちょうど MIN_STAKE の水準にあるため、アンボンドをすると、そのノードは次のエポックのセットから外れます。そして 26 のうち 18 が稼働し続けなければなりません。稼働していても投票しないバリデータも、クォーラムの不足として数えられます。chain 14 以降、バリデータは自身の時計より 15 秒を超えて先のブロックには投票しません。そのため、時計が遅れているバリデータは、エラーなしに投票をやめてしまいます。NTP を動かしてください(Running a validator)。
genesis はレジスタを直接シードします。エントリは --validator <key or hex pubkey>,<stake>,<payout> で 1 件ずつ指定し、最小額を下回るステークは拒否されます。chain 20 の genesis は、1,000 RAND ずつ 26 行をシードし、キーは chain 19 と同じです。
v0.5.11 以降、ビルドには genesis ベスティングも含まれます。これはタイムロックされた割り当ての公開レジスタで、取り消し不能なエントリからは、ロックされた RAND をバリデータのステークとしてボンドし(rand-node vesting bond)、取り戻す(rand-node vesting unbond)ことができます。これには genesis に vesting セクションが必要です。chain 20 を含め、どのチェーンにもそれはなく、それがなければこれらの操作は受理の段階で拒否されます。(v0.6.8 では、取り消し可能なベスティングエントリを、固定のトレジャリーに対する閾値数の署名者だけが取り消せるようにもできますが、これも vesting セクションがある場合に限ります。)
公開される情報
| 公開 | 秘匿 | |
|---|---|---|
| レジスタ | すべての行:アドレス、ステーク、アンボンドのキュー、未払いの報酬、支払先アドレス、nonce | なし |
Bond | バリデータ、金額、登録が添付されたかどうか、バンドルの burn_r | どのノートが支払ったか、誰が所有しているか、お釣り |
AdmitValidator(chain 20) | 候補者と、すべての投票者およびその署名 | ほかには何も存在しません |
Unbond | バリデータ、金額、nonce | ほかには何も存在しません |
Withdraw | バリデータ、金額、nonce、ノートの time と r | ノートを開ける人、およびそのノートのその後のすべての支出 |
報酬は、バリデータがプロポーズしたブロックの手数料です。ブロック報酬もインフレーションもありません。また chain 20 では、バリデータのスラッシングもジェイルもありません。不正を働いたバリデータがステークを失うことはなく、対処はオペレーターに委ねられます。Fullnode v0.6.8 には、リーダーの二重署名に対するスラッシングルールが実装されています(audit v6、STAKE-1:1 つのキーが 1 つのビューに対して 2 つの署名済みブロックヘッダーを出すと、ステークの equivocation_bps が破棄され、そのキーはジェイルされます)。ただし、これは genesis の staking.slashing セクションがある場合に限られ、chain 20 にはそれがありません(rand_getLimits.slashing は null です)。fullnode は、ステークが複数のオペレーターに保有されるようになってからの有効化を推奨しています。(ブロック集約のコードには、同じ nonce で 2 つの異なる集約に署名したアグリゲーターに対する SlashAggregator アクションもありますが、chain 20 には集約セクションがないため、有効ではありません。)
読み戻す
rand validators # the whole register
rand status # is_validator / active_validator for the node asked
→ {"method":"rand_getValidators","params":[]}
← [{ "address": "2nRdFC…", "stake": "1000000000000",
"pending": [{ "release_epoch": 71, "amount": "1000000000000" }],
"rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true, "jailed_until": null }]
→ {"method":"rand_getAdmitted","params":[]}
← { "admission_by_vote": true, "admitted": [], "max": 256 }
→ {"method":"rand_getEpoch","params":[]}
← { "epoch": 69, "epoch_blocks": 1000, "next_set": ["2nRdFC…", "ByDkxs…"] }
金額は単位の 10 進数文字列です。active は、その行が現在のエポックを動かしているセットに入っているかどうかを示します。jailed_until は、スラッシングセクションがある場合を除いて null です。admitted は、投票で承認されたもののまだ登録されていないキーを一覧にします。next_set は予測値で、境界より前のボンドとアンボンドのたびに変わります。エクスプローラーの GET /validators は、share_percent、blocks_proposed、および最後にプロポーズした高さを追加します。