Files
node/vms/platformvm/state/l1_validator.go
T
zeekayandHanzo Dev c44cc2289f platformvm builds GREEN off the codec: executor fold + full consumer flip
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.

Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
  native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
  capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
  manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
  ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.

All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.

KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:25:08 -07:00

438 lines
13 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"bytes"
"errors"
"fmt"
"github.com/google/btree"
validators "github.com/luxfi/validators"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/node/cache"
"github.com/luxfi/utils"
"github.com/luxfi/container/iterator"
"github.com/luxfi/math"
"github.com/luxfi/container/maybe"
)
var (
_ btree.LessFunc[L1Validator] = L1Validator.Less
_ utils.Sortable[L1Validator] = L1Validator{}
ErrMutatedL1Validator = errors.New("L1 validator contains mutated constant fields")
ErrConflictingL1Validator = errors.New("L1 validator contains conflicting chainID + nodeID pair")
ErrDuplicateL1Validator = errors.New("L1 validator contains duplicate chainID + nodeID pair")
)
type L1Validators interface {
// GetActiveL1ValidatorsIterator returns an iterator of all the active L1
// validators in increasing order of EndAccumulatedFee.
//
// It is the caller's responsibility to call [Release] on the iterator after
// use.
//
// It is not guaranteed to be safe to modify the state while using the
// iterator. After releasing the iterator, the state may be safely modified.
GetActiveL1ValidatorsIterator() (iterator.Iterator[L1Validator], error)
// NumActiveL1Validators returns the number of currently active L1
// validators.
NumActiveL1Validators() int
// WeightOfL1Validators returns the total active and inactive weight of L1
// validators on [chainID].
WeightOfL1Validators(chainID ids.ID) (uint64, error)
// GetL1Validator returns the validator with [validationID] if it exists. If
// the validator does not exist, [err] will equal [database.ErrNotFound].
GetL1Validator(validationID ids.ID) (L1Validator, error)
// HasL1Validator returns the validator with [validationID] if it exists.
HasL1Validator(chainID ids.ID, nodeID ids.NodeID) (bool, error)
// PutL1Validator inserts [l1Validator] as a validator. If the weight of the
// validator is 0, the validator is removed.
//
// If inserting this validator attempts to modify any of the constant fields
// of the L1 validator struct, an error will be returned.
//
// If inserting this validator would cause the total weight of L1 validators
// on a chain to overflow MaxUint64, an error will be returned.
//
// If inserting this validator would cause there to be multiple validators
// with the same chainID and nodeID pair to exist at the same time, an
// error will be returned.
//
// If an L1 validator is added with the same validationID as a previously
// removed L1 validator, the behavior is undefined.
PutL1Validator(l1Validator L1Validator) error
}
// L1Validator defines an LP-77 validator. For a given ValidationID, it is
// expected for ChainID, NodeID, PublicKey, RemainingBalanceOwner,
// DeactivationOwner, and StartTime to be constant.
type L1Validator struct {
// ValidationID is not serialized because it is used as the key in the
// database, so it doesn't need to be stored in the value.
ValidationID ids.ID
ChainID ids.ID `serialize:"true"`
NodeID ids.NodeID `serialize:"true"`
// PublicKey is the uncompressed BLS public key of the validator. It is
// guaranteed to be populated.
PublicKey []byte `serialize:"true"`
// RemainingBalanceOwner is the owner that will be used when returning the
// balance of the validator after removing accrued fees.
RemainingBalanceOwner []byte `serialize:"true"`
// DeactivationOwner is the owner that can manually deactivate the
// validator.
DeactivationOwner []byte `serialize:"true"`
// StartTime is the unix timestamp, in seconds, when this validator was
// added to the set.
StartTime uint64 `serialize:"true"`
// Weight of this validator. It can be updated when the MinNonce is
// increased. If the weight is being set to 0, the validator is being
// removed.
Weight uint64 `serialize:"true"`
// MinNonce is the smallest nonce that can be used to modify this
// validator's weight. It is initially set to 0 and is set to one higher
// than the last nonce used. It is not valid to use nonce MaxUint64 unless
// the weight is being set to 0, which removes the validator from the set.
MinNonce uint64 `serialize:"true"`
// EndAccumulatedFee is the amount of accumulated fees per validator that
// can accrue before this validator must be deactivated. It is equal to the
// amount of fees this validator is willing to pay plus the total amount of
// upgrade until this validator was registered. Note that this relies on the
// fact that every validator is charged the same fee for each unit of time.
//
// If this value is 0, the validator is inactive.
EndAccumulatedFee uint64 `serialize:"true"`
}
func (v L1Validator) Less(o L1Validator) bool {
return v.Compare(o) == -1
}
// Compare determines a canonical ordering of L1 validators based on their
// EndAccumulatedFees and ValidationIDs. Lower EndAccumulatedFees result in an
// earlier ordering.
func (v L1Validator) Compare(o L1Validator) int {
switch {
case v.EndAccumulatedFee < o.EndAccumulatedFee:
return -1
case o.EndAccumulatedFee < v.EndAccumulatedFee:
return 1
default:
return v.ValidationID.Compare(o.ValidationID)
}
}
// immutableFieldsAreUnmodified returns true if two versions of the same
// validator are valid. Either because the validationID has changed or because
// no unexpected fields have been modified.
func (v L1Validator) immutableFieldsAreUnmodified(o L1Validator) bool {
if v.ValidationID != o.ValidationID {
return true
}
return v.ChainID == o.ChainID &&
v.NodeID == o.NodeID &&
bytes.Equal(v.PublicKey, o.PublicKey) &&
bytes.Equal(v.RemainingBalanceOwner, o.RemainingBalanceOwner) &&
bytes.Equal(v.DeactivationOwner, o.DeactivationOwner) &&
v.StartTime == o.StartTime
}
func (v L1Validator) isDeleted() bool {
return v.Weight == 0
}
func (v L1Validator) IsActive() bool {
return v.Weight != 0 && v.EndAccumulatedFee != 0
}
func (v L1Validator) effectiveValidationID() ids.ID {
if v.IsActive() {
return v.ValidationID
}
// For inactive validators, return the ValidationID (not Empty).
// This ensures inactive validators are tracked with their ValidationID
// as the TxID in the validator set.
return v.ValidationID
}
func (v L1Validator) effectiveNodeID() ids.NodeID {
if v.IsActive() {
return v.NodeID
}
return ids.EmptyNodeID
}
func (v L1Validator) effectivePublicKey() *bls.PublicKey {
if v.IsActive() {
return bls.PublicKeyFromValidUncompressedBytes(v.PublicKey)
}
return nil
}
func (v L1Validator) effectivePublicKeyBytes() []byte {
if v.IsActive() {
return v.PublicKey
}
return nil
}
func getL1Validator(
cache cache.Cacher[ids.ID, maybe.Maybe[L1Validator]],
db database.KeyValueReader,
validationID ids.ID,
) (L1Validator, error) {
if maybeL1Validator, ok := cache.Get(validationID); ok {
if maybeL1Validator.IsNothing() {
return L1Validator{}, database.ErrNotFound
}
return maybeL1Validator.Value(), nil
}
bytes, err := db.Get(validationID[:])
if err == database.ErrNotFound {
cache.Put(validationID, maybe.Nothing[L1Validator]())
return L1Validator{}, database.ErrNotFound
}
if err != nil {
return L1Validator{}, err
}
l1Validator := L1Validator{
ValidationID: validationID,
}
if err := parseL1Validator(bytes, &l1Validator); err != nil {
return L1Validator{}, fmt.Errorf("failed to unmarshal L1 validator: %w", err)
}
cache.Put(validationID, maybe.Some(l1Validator))
return l1Validator, nil
}
func putL1Validator(
db database.KeyValueWriter,
cache cache.Cacher[ids.ID, maybe.Maybe[L1Validator]],
l1Validator L1Validator,
) error {
bytes, err := marshalL1Validator(l1Validator)
if err != nil {
return fmt.Errorf("failed to marshal L1 validator: %w", err)
}
if err := db.Put(l1Validator.ValidationID[:], bytes); err != nil {
return err
}
cache.Put(l1Validator.ValidationID, maybe.Some(l1Validator))
return nil
}
func deleteL1Validator(
db database.KeyValueDeleter,
cache cache.Cacher[ids.ID, maybe.Maybe[L1Validator]],
validationID ids.ID,
) error {
if err := db.Delete(validationID[:]); err != nil {
return err
}
cache.Put(validationID, maybe.Nothing[L1Validator]())
return nil
}
type l1ValidatorsDiff struct {
netAddedActive int // May be negative
modifiedTotalWeight map[ids.ID]uint64 // chainID -> totalWeight
modified map[ids.ID]L1Validator
modifiedHasNodeIDs map[chainIDNodeID]bool
active *btree.BTreeG[L1Validator]
}
func newL1ValidatorsDiff() *l1ValidatorsDiff {
return &l1ValidatorsDiff{
modifiedTotalWeight: make(map[ids.ID]uint64),
modified: make(map[ids.ID]L1Validator),
modifiedHasNodeIDs: make(map[chainIDNodeID]bool),
active: btree.NewG(defaultTreeDegree, L1Validator.Less),
}
}
// getActiveL1ValidatorsIterator takes in the parent iterator, removes all
// modified validators, and then adds all modified active validators.
func (d *l1ValidatorsDiff) getActiveL1ValidatorsIterator(parentIterator iterator.Iterator[L1Validator]) iterator.Iterator[L1Validator] {
return iterator.Merge(
L1Validator.Less,
iterator.Filter(parentIterator, func(l1Validator L1Validator) bool {
_, ok := d.modified[l1Validator.ValidationID]
return ok
}),
iterator.FromTree(d.active),
)
}
func (d *l1ValidatorsDiff) hasL1Validator(chainID ids.ID, nodeID ids.NodeID) (bool, bool) {
chainIDNodeID := chainIDNodeID{
chainID: chainID,
nodeID: nodeID,
}
has, modified := d.modifiedHasNodeIDs[chainIDNodeID]
return has, modified
}
func (d *l1ValidatorsDiff) putL1Validator(state Chain, l1Validator L1Validator) error {
var (
prevWeight uint64
prevActive bool
newActive = l1Validator.IsActive()
)
switch priorL1Validator, err := state.GetL1Validator(l1Validator.ValidationID); err {
case nil:
if !priorL1Validator.immutableFieldsAreUnmodified(l1Validator) {
return ErrMutatedL1Validator
}
prevWeight = priorL1Validator.Weight
prevActive = priorL1Validator.IsActive()
case database.ErrNotFound:
// Verify that there is not a legacy chain validator with the same
// chainID+nodeID as this L1 validator.
_, err := state.GetCurrentValidator(l1Validator.ChainID, l1Validator.NodeID)
if err == nil {
return ErrConflictingL1Validator
}
if err != database.ErrNotFound {
return err
}
has, err := state.HasL1Validator(l1Validator.ChainID, l1Validator.NodeID)
if err != nil {
return err
}
if has {
return ErrDuplicateL1Validator
}
default:
return err
}
if prevWeight != l1Validator.Weight {
weight, err := state.WeightOfL1Validators(l1Validator.ChainID)
if err != nil {
return err
}
weight, err = math.Sub(weight, prevWeight)
if err != nil {
return err
}
weight, err = math.Add(weight, l1Validator.Weight)
if err != nil {
return err
}
d.modifiedTotalWeight[l1Validator.ChainID] = weight
}
switch {
case prevActive && !newActive:
d.netAddedActive--
case !prevActive && newActive:
d.netAddedActive++
}
if prevL1Validator, ok := d.modified[l1Validator.ValidationID]; ok {
d.active.Delete(prevL1Validator)
}
d.modified[l1Validator.ValidationID] = l1Validator
chainIDNodeID := chainIDNodeID{
chainID: l1Validator.ChainID,
nodeID: l1Validator.NodeID,
}
d.modifiedHasNodeIDs[chainIDNodeID] = !l1Validator.isDeleted()
if l1Validator.IsActive() {
d.active.ReplaceOrInsert(l1Validator)
}
return nil
}
type activeL1Validators struct {
lookup map[ids.ID]L1Validator
tree *btree.BTreeG[L1Validator]
}
func newActiveL1Validators() *activeL1Validators {
return &activeL1Validators{
lookup: make(map[ids.ID]L1Validator),
tree: btree.NewG(defaultTreeDegree, L1Validator.Less),
}
}
func (a *activeL1Validators) get(validationID ids.ID) (L1Validator, bool) {
l1Validator, ok := a.lookup[validationID]
return l1Validator, ok
}
func (a *activeL1Validators) put(l1Validator L1Validator) {
a.lookup[l1Validator.ValidationID] = l1Validator
a.tree.ReplaceOrInsert(l1Validator)
}
func (a *activeL1Validators) delete(validationID ids.ID) bool {
l1Validator, ok := a.lookup[validationID]
if !ok {
return false
}
delete(a.lookup, validationID)
a.tree.Delete(l1Validator)
return true
}
func (a *activeL1Validators) len() int {
return len(a.lookup)
}
func (a *activeL1Validators) newIterator() iterator.Iterator[L1Validator] {
return iterator.FromTree(a.tree)
}
func (a *activeL1Validators) addStakersToValidatorManager(vdrs validators.Manager) error {
for validationID, l1Validator := range a.lookup {
if err := vdrs.AddStaker(l1Validator.ChainID, l1Validator.NodeID, l1Validator.PublicKey, validationID, l1Validator.Weight); err != nil {
return err
}
}
return nil
}
func addL1ValidatorToValidatorManager(vdrs validators.Manager, l1Validator L1Validator) error {
nodeID := l1Validator.effectiveNodeID()
if vdrs.GetWeight(l1Validator.ChainID, nodeID) != 0 {
return vdrs.AddWeight(l1Validator.ChainID, nodeID, l1Validator.Weight)
}
return vdrs.AddStaker(
l1Validator.ChainID,
nodeID,
l1Validator.effectivePublicKeyBytes(),
l1Validator.effectiveValidationID(),
l1Validator.Weight,
)
}