The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
9.9 KiB
Validators versioning
One of the main responsibilities of the P-chain is to register and expose the validator set of any Net at every height.
This information helps Nets to bootstrap securely, downloading information from active validators only; moreover it supports validated cross-chain communication via Warp.
In this brief document we dive into the technicalities of how platformVM tracks and versions the validator set of any Net.
The tracked content
The entry point to retrieve validator information at a given height is the GetValidatorSet method in the validators package. Here is its signature:
GetValidatorSet(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*GetValidatorOutput, error)
GetValidatorSet lets any VM specify a Net and a height and returns the data of all Net validators active at the requested height, and only those.
Validator data are collected in a struct named validators.GetValidatorOutput which holds for each active validator, its NodeID, its Weight and its BLS Public Key if it was registered.
Note that a validator Weight is not just its stake; it's the aggregate value of the validator's own stake and all of its delegators' stake. A validator's Weight gauges how relevant its preference should be in consensus or Warp operations.
We will see in the next section how the P-chain keeps track of this information over time as the validator set changes.
Validator diffs content
Every new block accepted by the P-chain can potentially alter the validator set of any Net, including the primary one. New validators may be added; some of them may have reached their end of life and are therefore removed. Moreover a validator can register itself again once its staking time is done, possibly with a Weight and a BLS Public key different from the previous staking period.
Whenever the block at height H adds or removes a validator, the P-chain does, among others, the following operations:
- it updates the current validator set to add the new validator or remove it if expired;
- it explicitly records the validator set diffs with respect to the validator set at height
H-1.
These diffs are key to rebuilding the validator set at a given past height. In this section we illustrate their content. In next ones, We'll see how the diffs are stored and used.
The validators diffs track changes in a validator's Weight and BLS Public key. Along with the NodeID this is the data exposed by the GetValidatorSet method.
Note that Weight and BLS Public key behave differently throughout the validator's lifetime:
BLS Public keycannot change through a validator's lifetime. It can only change when a validator is added/re-added and removed.Weightcan change throughout a validator's lifetime by the creation and removal of its delegators as well as by validator's own creation and removal.
Here is a scheme of what Weight and BLS Public key diff content we record upon relevant scenarios:
| Weight Diff (forward looking) | BLS Key Diff (backward looking) | |
|---|---|---|
| Validator creation | record golang state.ValidatorWeightDiff{ Decrease: false, Weight: validator.Weight, } |
record an empty byte slice if validator.BlsKey is specified; otherwise record nothing |
| Delegator creation | record golang state.ValidatorWeightDiff{ Decrease: false, Weight: validator.Weight, } |
No entry is recorded |
| Delegator removal | record golang state.ValidatorWeightDiff{ Decrease: true, Weight: validator.Weight, } |
No entry is recorded |
| Validator removal | record golang state.ValidatorWeightDiff{ Decrease: true, Weight: validator.Weight, } |
record validator.BlsKey if it is specified; otherwise record nothing |
Note that Weight diffs are encoded state.ValidatorWeightDiff and are forward-looking: a diff recorded at height H stores the change that transforms validator weight at height H-1 into validator weight at height H.
In contrast, BLS Public Key diffs are backward-looking: a diff recorded at height H stores the change that transforms validator BLS Public Key at height H into validator BLS Public key at height H-1.
Finally, if no changes are made to the validator set no diff entry is recorded. This implies that a validator Weight or BLS Public Key diff may not be stored for every height H.
Validator diffs layout
Validator diffs layout is optimized to support iteration. Validator sets are rebuilt by accumulating Weight and BLS Public Key diffs from the top-most height down to the requested height. So validator diffs are stored so that it's fast to iterate them in this order.
Weight diffs are stored as a contiguous block of key-value pairs as follows:
| Key | Value |
|---|---|
| ChainID + Reverse_Height + NodeID | serialized state.ValidatorWeightDiff |
Note that:
Weightdiffs related to a Net are stored contiguously.- Diff height is serialized as
Reverse_Height. It is stored with big endian format and has its bits flipped too. Big endianness ensures that heights are stored in order, bit flipping ensures that the top-most height is always the first. NodeIDis part of the key andstate.ValidatorWeightDiffis part of the value.
BLS Public diffs are stored as follows:
| Key | Value |
|---|---|
| ChainID + Reverse_Height + NodeID | validator.BlsKey bytes or nil |
Note that:
BLS Public Keydiffs have the same keys asWeightdiffs. This implies that the same ordering is guaranteed.- Value is either validator
BLS Public Keybytes or an empty byte slice, as illustrated in the previous section.
Validators diff usage in rebuilding validators state
Now let's see how diffs are used to rebuild the validator set at a given height. The procedure varies slightly between Primary Network and Net validator, so we'll describe them separately. We assume that the reader knows that every Net validator must also be a Primary Network validator.
Primary network validator set rebuild
If the P-Chain's current height is T and we want to retrieve the Primary Network validators at height H < T. We proceed as follows:
- We retrieve the Primary Network validator set at current height
T. This is the base state on top of which diffs will be applied. - We apply weight diffs first. Specifically:
Weightdiff iteration starts from the top-most height smaller or equal toT. Remember that entry heights do not need to be contiguous, so the iteration starts from the highest height smaller or equal toT, in caseTdoes not have a diff entry.- Since
Weightdiffs are forward-looking, each diff is applied in reverse. A validator's weight is decreased ifstate.ValidatorWeightDiff.Decreaseisfalseand it is increased if it istrue. - We take care of adding or removing a validator from the base set based on its weight. Whenever a validator weight, following diff application, becomes zero, we drop it; conversely whenever we encounter a diff increasing weight for a currently-non-existing validator, we add the validator to the base set.
- The iteration stops at the first height smaller or equal to
H+1. Note that aWeightdiff stored at heightKholds the content to turn validator state at heightK-1into validator state at heightK. So to get validator state at heightKwe must apply diff content at heightK+1.
- Once all
Weightdiffs have been applied, the resulting validator set will contain all Primary Network validators active at heightHand only those. We still need to compute the correctBLS Public Keysregistered at heightHfor these validators, as each validator may have restaked between heightHandT. They may have a different (or no)BLS Public Keyat either height. We solve this by applyingBLS Public Keydiffs to the validator set:- Once again we iterate
BLS Public Keydiffs from the top-most height smaller or equal toTtill the first height smaller or equal toH+1. - Since
BLS Public Keydiffs are backward-looking, we simply nil the BLS key when diff is nil and we restore the BLS Key when it is specified in the diff.
- Once again we iterate
Net validator set rebuild
Let's see first the reason why Net validators needs to be handled differently. We allow BLS Public Key registration only for Primary network validators. A given NodeID may be both a Primary Network validator and a Net validator, but it'll register its BLS Public Key only when it registers as Primary Network validator. Despite this, we want to provide a validator BLS Public Key when validators.GetValidatorOutput is called. So we need to fetch it from the Primary Network validator set.
Say P-chain current height is T and we want to retrieve Primary network validators at height H < T. We proceed as follows:
- We retrieve both Net and Primary Network validator set at current height
T, - We apply
Weightdiff on top of the Net validator set, exactly as described in the previous section, - Before applying
BLS Public Keydiffs, we retrieveBLS Public Keyfrom the current Primary Network validator set for each of the current Net validators. This ensures theBLS Public Keys are duly initialized before applying the diffs, - Finally we apply the
BLS Public Keydiffs exactly as described in the previous section.