mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
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.
510 lines
15 KiB
Go
510 lines
15 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package state
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
validators "github.com/luxfi/validators"
|
|
|
|
safemath "github.com/luxfi/math"
|
|
)
|
|
|
|
func (s *state) ApplyValidatorWeightDiffs(
|
|
ctx context.Context,
|
|
validators map[ids.NodeID]*validators.GetValidatorOutput,
|
|
startHeight uint64,
|
|
endHeight uint64,
|
|
netID ids.ID,
|
|
) error {
|
|
diffIter := s.validatorWeightDiffsDB.NewIteratorWithStartAndPrefix(
|
|
marshalStartDiffKey(netID, startHeight),
|
|
netID[:],
|
|
)
|
|
defer diffIter.Release()
|
|
|
|
prevHeight := startHeight + 1
|
|
for diffIter.Next() {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, parsedHeight, nodeID, err := unmarshalDiffKey(diffIter.Key())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if parsedHeight > prevHeight {
|
|
log.Error("unexpected parsed height",
|
|
log.Stringer("netID", netID),
|
|
log.Uint64("parsedHeight", parsedHeight),
|
|
log.Stringer("nodeID", nodeID),
|
|
log.Uint64("prevHeight", prevHeight),
|
|
log.Uint64("startHeight", startHeight),
|
|
log.Uint64("endHeight", endHeight),
|
|
)
|
|
}
|
|
|
|
// If the parsedHeight is less than our target endHeight, then we have
|
|
// fully processed the diffs from startHeight through endHeight.
|
|
if parsedHeight < endHeight {
|
|
return diffIter.Error()
|
|
}
|
|
|
|
prevHeight = parsedHeight
|
|
|
|
weightDiff, err := unmarshalWeightDiff(diffIter.Value())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := applyWeightDiff(validators, nodeID, weightDiff); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return diffIter.Error()
|
|
}
|
|
|
|
func applyWeightDiff(
|
|
vdrs map[ids.NodeID]*validators.GetValidatorOutput,
|
|
nodeID ids.NodeID,
|
|
weightDiff *ValidatorWeightDiff,
|
|
) error {
|
|
vdr, ok := vdrs[nodeID]
|
|
if !ok {
|
|
// This node isn't in the current validator set.
|
|
vdr = &validators.GetValidatorOutput{
|
|
NodeID: nodeID,
|
|
}
|
|
vdrs[nodeID] = vdr
|
|
}
|
|
|
|
// Preserve TxID from ValidationID if this is an L1 validator diff.
|
|
// The ValidationID field in the diff always contains the original TxID
|
|
// (set during diff creation in calculateValidatorDiffs).
|
|
// When applying diffs backward, this restores the correct TxID even when
|
|
// ValidationID changes.
|
|
if weightDiff.ValidationID != ids.Empty {
|
|
vdr.TxID = weightDiff.ValidationID
|
|
}
|
|
|
|
// The weight of this node changed at this block.
|
|
var err error
|
|
if weightDiff.Decrease {
|
|
// The validator's weight was decreased at this block, so in the
|
|
// prior block it was higher.
|
|
vdr.Weight, err = safemath.Add(vdr.Weight, weightDiff.Amount)
|
|
} else {
|
|
// The validator's weight was increased at this block, so in the
|
|
// prior block it was lower.
|
|
vdr.Weight, err = safemath.Sub(vdr.Weight, weightDiff.Amount)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Keep Light in sync with Weight (Light is an alias for Weight)
|
|
vdr.Light = vdr.Weight
|
|
|
|
if vdr.Weight == 0 {
|
|
// The validator's weight was 0 before this block so they weren't in the
|
|
// validator set.
|
|
delete(vdrs, nodeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) ApplyValidatorPublicKeyDiffs(
|
|
ctx context.Context,
|
|
validators map[ids.NodeID]*validators.GetValidatorOutput,
|
|
startHeight uint64,
|
|
endHeight uint64,
|
|
chainID ids.ID,
|
|
) error {
|
|
diffIter := s.validatorPublicKeyDiffsDB.NewIteratorWithStartAndPrefix(
|
|
marshalStartDiffKey(chainID, startHeight),
|
|
chainID[:],
|
|
)
|
|
defer diffIter.Release()
|
|
|
|
for diffIter.Next() {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, parsedHeight, nodeID, err := unmarshalDiffKey(diffIter.Key())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// If the parsedHeight is less than our target endHeight, then we have
|
|
// fully processed the diffs from startHeight through endHeight.
|
|
if parsedHeight < endHeight {
|
|
break
|
|
}
|
|
|
|
vdr, ok := validators[nodeID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
pkBytes := diffIter.Value()
|
|
if len(pkBytes) == 0 {
|
|
vdr.PublicKey = nil
|
|
continue
|
|
}
|
|
|
|
vdr.PublicKey = pkBytes
|
|
}
|
|
|
|
// Note: this does not fallback to the linkeddb index because the linkeddb
|
|
// index does not contain entries for when to remove the public key.
|
|
//
|
|
// Nodes may see inconsistent public keys for heights before the new public
|
|
// key index was populated.
|
|
return diffIter.Error()
|
|
}
|
|
|
|
// getInheritedPublicKey returns the primary network validator's public key.
|
|
//
|
|
// Note: This function may return a nil public key and no error if the primary
|
|
// network validator does not have a public key.
|
|
func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) {
|
|
if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil {
|
|
// The primary network validator is present.
|
|
return vdr.validator.PublicKey, nil
|
|
}
|
|
if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil {
|
|
// The primary network validator is being modified.
|
|
return vdr.validator.PublicKey, nil
|
|
}
|
|
return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID)
|
|
}
|
|
|
|
// updateValidatorManager updates the validator manager with the pending
|
|
// validator set changes. L1s with zero active weight remain cached to
|
|
// avoid repeated database lookups during chain reorganization.
|
|
//
|
|
// This function must be called prior to writeCurrentStakers and
|
|
// writeL1Validators.
|
|
func (s *state) updateValidatorManager(updateValidators bool) error {
|
|
if !updateValidators {
|
|
return nil
|
|
}
|
|
|
|
for chainID, validatorDiffs := range s.currentStakers.validatorDiffs {
|
|
// Record the change in weight and/or public key for each validator.
|
|
for nodeID, diff := range validatorDiffs {
|
|
weightDiff, err := diff.WeightDiff()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if weightDiff.Amount == 0 {
|
|
continue // No weight change; go to the next validator.
|
|
}
|
|
|
|
if weightDiff.Decrease {
|
|
if err := s.validators.RemoveWeight(chainID, nodeID, weightDiff.Amount); err != nil {
|
|
return fmt.Errorf("failed to reduce validator weight: %w", err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if diff.validatorStatus != added {
|
|
if err := s.validators.AddWeight(chainID, nodeID, weightDiff.Amount); err != nil {
|
|
return fmt.Errorf("failed to increase validator weight: %w", err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
pk, err := s.getInheritedPublicKey(nodeID)
|
|
if err != nil {
|
|
// This should never happen as there should always be a primary
|
|
// network validator corresponding to a chain validator.
|
|
return err
|
|
}
|
|
|
|
err = s.validators.AddStaker(
|
|
chainID,
|
|
nodeID,
|
|
bls.PublicKeyToUncompressedBytes(pk),
|
|
diff.validator.TxID,
|
|
weightDiff.Amount,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add validator: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove all deleted L1 validators. This must be done before adding new
|
|
// L1 validators to support the case where a validator is removed and then
|
|
// immediately re-added with a different validationID.
|
|
//
|
|
// Sort validators by ValidationID for deterministic processing order.
|
|
// This is important when multiple inactive validators share the same
|
|
// effectiveNodeID (ids.EmptyNodeID), as the first one processed sets
|
|
// the TxID in the validators manager.
|
|
sortedValidationIDs := slices.Collect(maps.Keys(s.l1ValidatorsDiff.modified))
|
|
slices.SortFunc(sortedValidationIDs, func(a, b ids.ID) int {
|
|
return a.Compare(b)
|
|
})
|
|
for _, validationID := range sortedValidationIDs {
|
|
l1Validator := s.l1ValidatorsDiff.modified[validationID]
|
|
if !l1Validator.isDeleted() {
|
|
continue
|
|
}
|
|
|
|
priorL1Validator, err := s.getPersistedL1Validator(validationID)
|
|
if err == database.ErrNotFound {
|
|
// Deleting a non-existent validator is a noop. This can happen if
|
|
// the validator was added and then immediately removed.
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.validators.RemoveWeight(priorL1Validator.ChainID, priorL1Validator.effectiveNodeID(), priorL1Validator.Weight); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Now that the removed L1 validators have been deleted, perform additions
|
|
// and modifications.
|
|
for _, validationID := range sortedValidationIDs {
|
|
l1Validator := s.l1ValidatorsDiff.modified[validationID]
|
|
if l1Validator.isDeleted() {
|
|
continue
|
|
}
|
|
|
|
priorL1Validator, err := s.getPersistedL1Validator(validationID)
|
|
switch err {
|
|
case nil:
|
|
// Modifying an existing validator
|
|
if priorL1Validator.IsActive() == l1Validator.IsActive() {
|
|
// This validator's active status isn't changing. This means
|
|
// the effectiveNodeIDs are equal.
|
|
nodeID := l1Validator.effectiveNodeID()
|
|
if priorL1Validator.Weight < l1Validator.Weight {
|
|
err = s.validators.AddWeight(l1Validator.ChainID, nodeID, l1Validator.Weight-priorL1Validator.Weight)
|
|
} else if priorL1Validator.Weight > l1Validator.Weight {
|
|
err = s.validators.RemoveWeight(l1Validator.ChainID, nodeID, priorL1Validator.Weight-l1Validator.Weight)
|
|
}
|
|
} else {
|
|
// This validator's active status is changing.
|
|
err = errors.Join(
|
|
s.validators.RemoveWeight(l1Validator.ChainID, priorL1Validator.effectiveNodeID(), priorL1Validator.Weight),
|
|
addL1ValidatorToValidatorManager(s.validators, l1Validator),
|
|
)
|
|
}
|
|
case database.ErrNotFound:
|
|
// Adding a new validator
|
|
err = addL1ValidatorToValidatorManager(s.validators, l1Validator)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update the stake metrics
|
|
totalWeight, err := s.validators.TotalWeight(constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get total weight of primary network: %w", err)
|
|
}
|
|
|
|
s.metrics.SetLocalStake(s.validators.GetWeight(constants.PrimaryNetworkID, s.rt.NodeID))
|
|
s.metrics.SetTotalStake(totalWeight)
|
|
return nil
|
|
}
|
|
|
|
type validatorDiff struct {
|
|
weightDiff ValidatorWeightDiff
|
|
prevPublicKey []byte
|
|
newPublicKey []byte
|
|
hadRemoval bool // True if pass 1 processed a removal for this (netID, nodeID)
|
|
}
|
|
|
|
// calculateValidatorDiffs calculates the validator set diff contained by the
|
|
// pending validator set changes.
|
|
//
|
|
// This function must be called prior to writeCurrentStakers.
|
|
func (s *state) calculateValidatorDiffs() (map[chainIDNodeID]*validatorDiff, error) {
|
|
changes := make(map[chainIDNodeID]*validatorDiff)
|
|
|
|
// Calculate the changes to the pre-LP-77 validator set
|
|
for chainID, chainDiffs := range s.currentStakers.validatorDiffs {
|
|
for nodeID, diff := range chainDiffs {
|
|
weightDiff, err := diff.WeightDiff()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For legacy validators, set ValidationID to the validator's TxID
|
|
// to ensure TxID is preserved during historical reconstruction.
|
|
if diff.validator != nil && weightDiff.Amount != 0 {
|
|
weightDiff.ValidationID = diff.validator.TxID
|
|
}
|
|
|
|
pk, err := s.getInheritedPublicKey(nodeID)
|
|
if err != nil {
|
|
// This should never happen as there should always be a primary
|
|
// network validator corresponding to a chain validator.
|
|
return nil, err
|
|
}
|
|
|
|
change := &validatorDiff{
|
|
weightDiff: weightDiff,
|
|
}
|
|
if pk != nil {
|
|
pkBytes := bls.PublicKeyToUncompressedBytes(pk)
|
|
if diff.validatorStatus != added {
|
|
change.prevPublicKey = pkBytes
|
|
}
|
|
if diff.validatorStatus != deleted {
|
|
change.newPublicKey = pkBytes
|
|
}
|
|
}
|
|
|
|
chainIDNodeID := chainIDNodeID{
|
|
chainID: chainID,
|
|
nodeID: nodeID,
|
|
}
|
|
changes[chainIDNodeID] = change
|
|
}
|
|
}
|
|
|
|
// Calculate the changes to the LP-77 validator set
|
|
//
|
|
// Process in two passes to ensure TxID preservation during ValidationID changes:
|
|
// Pass 1: Process removals (weight decreases) to capture original TxIDs
|
|
// Pass 2: Process additions (weight increases) without overwriting TxIDs from pass 1
|
|
|
|
// Collect entries into two slices to ensure deterministic processing order
|
|
type validatorEntry struct {
|
|
validationID ids.ID
|
|
validator L1Validator
|
|
}
|
|
var removals, additions []validatorEntry
|
|
|
|
for validationID, l1Validator := range s.l1ValidatorsDiff.modified {
|
|
_, err := s.getPersistedL1Validator(validationID)
|
|
if err == nil {
|
|
// This validator existed before, so we're removing it
|
|
removals = append(removals, validatorEntry{validationID, l1Validator})
|
|
} else if err != database.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
|
|
// If not deleted, we're also adding it (possibly with a different ValidationID)
|
|
if !l1Validator.isDeleted() {
|
|
additions = append(additions, validatorEntry{validationID, l1Validator})
|
|
}
|
|
}
|
|
|
|
// Pass 1: Process all removals first
|
|
for _, entry := range removals {
|
|
priorL1Validator, err := s.getPersistedL1Validator(entry.validationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainIDNodeID := chainIDNodeID{
|
|
chainID: priorL1Validator.ChainID,
|
|
nodeID: priorL1Validator.effectiveNodeID(),
|
|
}
|
|
diff := getOrSetDefault(changes, chainIDNodeID)
|
|
// For removals, always set ValidationID to the original TxID.
|
|
// This ensures TxID preservation when ValidationID changes.
|
|
diff.weightDiff.ValidationID = entry.validationID
|
|
diff.hadRemoval = true // Mark that this diff includes a removal
|
|
if err := diff.weightDiff.Sub(priorL1Validator.Weight); err != nil {
|
|
return nil, err
|
|
}
|
|
diff.prevPublicKey = priorL1Validator.effectivePublicKeyBytes()
|
|
}
|
|
|
|
// Pass 2: Process all additions
|
|
for _, entry := range additions {
|
|
chainIDNodeID := chainIDNodeID{
|
|
chainID: entry.validator.ChainID,
|
|
nodeID: entry.validator.effectiveNodeID(),
|
|
}
|
|
diff := getOrSetDefault(changes, chainIDNodeID)
|
|
// Only set ValidationID if not already set by pass 1 (removal).
|
|
// This preserves the original TxID when a ValidationID changes.
|
|
if diff.weightDiff.ValidationID == ids.Empty {
|
|
diff.weightDiff.ValidationID = entry.validationID
|
|
}
|
|
if err := diff.weightDiff.Add(entry.validator.Weight); err != nil {
|
|
return nil, err
|
|
}
|
|
diff.newPublicKey = entry.validator.effectivePublicKeyBytes()
|
|
}
|
|
|
|
return changes, nil
|
|
}
|
|
|
|
// writeValidatorDiffs writes the validator set diff contained by the pending
|
|
// validator set changes to disk.
|
|
//
|
|
// This function must be called prior to writeCurrentStakers.
|
|
func (s *state) writeValidatorDiffs(height uint64) error {
|
|
changes, err := s.calculateValidatorDiffs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write the changes to the database
|
|
for chainIDNodeID, diff := range changes {
|
|
diffKey := marshalDiffKey(chainIDNodeID.chainID, height, chainIDNodeID.nodeID)
|
|
// Write weight diff if:
|
|
// 1. Amount changed, OR
|
|
// 2. Amount didn't change but ValidationID changed (hadRemoval indicates removal+addition)
|
|
// This ensures TxID tracking across ValidationID changes even when net weight is unchanged.
|
|
shouldWrite := diff.weightDiff.Amount != 0 || (diff.hadRemoval && diff.weightDiff.ValidationID != ids.Empty)
|
|
if shouldWrite {
|
|
err := s.validatorWeightDiffsDB.Put(
|
|
diffKey,
|
|
marshalWeightDiff(&diff.weightDiff),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !bytes.Equal(diff.prevPublicKey, diff.newPublicKey) {
|
|
err := s.validatorPublicKeyDiffsDB.Put(
|
|
diffKey,
|
|
diff.prevPublicKey,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getOrSetDefault returns the value at k in m if it exists. If it doesn't
|
|
// exist, it sets m[k] to a new value and returns that value.
|
|
func getOrSetDefault[K comparable, V any](m map[K]*V, k K) *V {
|
|
if v, ok := m[k]; ok {
|
|
return v
|
|
}
|
|
|
|
v := new(V)
|
|
m[k] = v
|
|
return v
|
|
}
|