mirror of
https://github.com/luxfi/proto.git
synced 2026-10-30 15:21:09 +00:00
proto required luxfi/node and luxfi/node required proto, with no replace directive on either side. The cycle was not load-bearing: most of it was proto reaching into the node for packages proto already owns — p/warp, p/txs, p/signer, p/stakeable, p/security, x/txs — which now name themselves. Two were real. The UTXO components moved here, where the chains that use them already live, and the node is the only other caller, so that dependency now runs one way. And p/config held the node's chain manager to queue a chain the P-Chain had accepted a CreateChainTx for; it calls one method, so it declares one method. A P-Chain that imports the node to describe QueueChainCreation is what made two modules require each other. Dropping the requirement dropped a registration with it: lux.ParseUTXO is a registry filled by an init, which the node used to link in on proto's behalf. The packages that call it now say so themselves. Co-authored-by: Hanzo Dev <[email protected]>
3457 lines
99 KiB
Go
3457 lines
99 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"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/btree"
|
|
"github.com/luxfi/metric"
|
|
|
|
"github.com/luxfi/cache"
|
|
"github.com/luxfi/cache/lru"
|
|
"github.com/luxfi/cache/metercacher"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/container/iterator"
|
|
"github.com/luxfi/container/maybe"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/linkeddb"
|
|
"github.com/luxfi/database/prefixdb"
|
|
"github.com/luxfi/database/versiondb"
|
|
"github.com/luxfi/ids"
|
|
log "github.com/luxfi/log"
|
|
"github.com/luxfi/proto/p/block"
|
|
"github.com/luxfi/proto/p/config"
|
|
"github.com/luxfi/proto/p/fx"
|
|
"github.com/luxfi/proto/p/genesis"
|
|
"github.com/luxfi/proto/p/metrics"
|
|
"github.com/luxfi/proto/p/reward"
|
|
"github.com/luxfi/proto/p/status"
|
|
"github.com/luxfi/proto/p/txs"
|
|
"github.com/luxfi/runtime"
|
|
"github.com/luxfi/timer"
|
|
"github.com/luxfi/upgrade"
|
|
lux "github.com/luxfi/utxo"
|
|
// lux.ParseUTXO is a registry that something has to fill. The
|
|
// components/lux init is what fills it, and this package is one of the
|
|
// callers, so it says so rather than inheriting it from whoever linked it.
|
|
_ "github.com/luxfi/proto/components/lux"
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/validators/uptime"
|
|
"github.com/luxfi/vm/components/gas"
|
|
|
|
safemath "github.com/luxfi/math"
|
|
)
|
|
|
|
const (
|
|
defaultTreeDegree = 2
|
|
indexIterationLimit = 4096
|
|
indexIterationSleepMultiplier = 5
|
|
indexIterationSleepCap = 10 * time.Second
|
|
indexLogFrequency = 30 * time.Second
|
|
)
|
|
|
|
var (
|
|
_ State = (*state)(nil)
|
|
|
|
errValidatorSetAlreadyPopulated = errors.New("validator set already populated")
|
|
errIsNotNet = errors.New("is not a chain")
|
|
errMissingPrimaryNetworkValidator = errors.New("missing primary network validator")
|
|
|
|
BlockIDPrefix = []byte("blockID")
|
|
BlockPrefix = []byte("block")
|
|
ValidatorsPrefix = []byte("validators")
|
|
CurrentPrefix = []byte("current")
|
|
PendingPrefix = []byte("pending")
|
|
ValidatorPrefix = []byte("validator")
|
|
DelegatorPrefix = []byte("delegator")
|
|
NetValidatorPrefix = []byte("chainValidator")
|
|
NetDelegatorPrefix = []byte("chainDelegator")
|
|
ValidatorWeightDiffsPrefix = []byte("flatValidatorDiffs")
|
|
ValidatorPublicKeyDiffsPrefix = []byte("flatPublicKeyDiffs")
|
|
TxPrefix = []byte("tx")
|
|
RewardUTXOsPrefix = []byte("rewardUTXOs")
|
|
UTXOPrefix = []byte("utxo")
|
|
NetPrefix = []byte("chain")
|
|
NetOwnerPrefix = []byte("chainOwner")
|
|
NetToL1ConversionPrefix = []byte("chainToL1Conversion")
|
|
TransformedNetPrefix = []byte("transformedNet")
|
|
SupplyPrefix = []byte("supply")
|
|
ChainPrefix = []byte("chain")
|
|
ChainNamePrefix = []byte("chainName") // maps lowercase chain name -> chainID
|
|
ExpiryReplayProtectionPrefix = []byte("expiryReplayProtection")
|
|
L1Prefix = []byte("l1")
|
|
WeightsPrefix = []byte("weights")
|
|
ChainIDNodeIDPrefix = []byte("chainIDNodeID")
|
|
ActivePrefix = []byte("active")
|
|
InactivePrefix = []byte("inactive")
|
|
SingletonPrefix = []byte("singleton")
|
|
|
|
TimestampKey = []byte("timestamp")
|
|
FeeStateKey = []byte("fee state")
|
|
L1ValidatorExcessKey = []byte("l1Validator excess")
|
|
AccruedFeesKey = []byte("accrued fees")
|
|
CurrentSupplyKey = []byte("current supply")
|
|
LastAcceptedKey = []byte("last accepted")
|
|
HeightsIndexedKey = []byte("heights indexed")
|
|
InitializedKey = []byte("initialized")
|
|
BlocksReindexedKey = []byte("blocks reindexed.3")
|
|
|
|
emptyL1ValidatorCache = &cache.Empty[ids.ID, maybe.Maybe[L1Validator]]{}
|
|
)
|
|
|
|
// Chain collects all methods to manage the state of the chain for block
|
|
// execution.
|
|
type Chain interface {
|
|
Expiry
|
|
L1Validators
|
|
Stakers
|
|
L1Validators
|
|
lux.UTXOAdder
|
|
lux.UTXOGetter
|
|
lux.UTXODeleter
|
|
|
|
GetTimestamp() time.Time
|
|
SetTimestamp(tm time.Time)
|
|
|
|
GetFeeState() gas.State
|
|
SetFeeState(f gas.State)
|
|
|
|
GetL1ValidatorExcess() gas.Gas
|
|
SetL1ValidatorExcess(e gas.Gas)
|
|
|
|
GetAccruedFees() uint64
|
|
SetAccruedFees(f uint64)
|
|
|
|
GetCurrentSupply(chainID ids.ID) (uint64, error)
|
|
SetCurrentSupply(chainID ids.ID, cs uint64)
|
|
|
|
AddRewardUTXO(txID ids.ID, utxo *lux.UTXO)
|
|
|
|
AddNet(netID ids.ID)
|
|
|
|
GetNetOwner(netID ids.ID) (fx.Owner, error)
|
|
SetNetOwner(netID ids.ID, owner fx.Owner)
|
|
|
|
GetNetToL1Conversion(chainID ids.ID) (NetToL1Conversion, error)
|
|
SetNetToL1Conversion(chainID ids.ID, c NetToL1Conversion)
|
|
|
|
GetNetTransformation(chainID ids.ID) (*txs.Tx, error)
|
|
AddNetTransformation(transformNetTx *txs.Tx)
|
|
|
|
AddChain(createChainTx *txs.Tx)
|
|
|
|
// Chain name uniqueness - for network-wide chain name resolution
|
|
GetChainIDByName(name string) (ids.ID, error)
|
|
IsChainNameTaken(name string) bool
|
|
|
|
GetTx(txID ids.ID) (*txs.Tx, status.Status, error)
|
|
AddTx(tx *txs.Tx, status status.Status)
|
|
|
|
// L1 Validator support - most methods inherited from L1Validators interface
|
|
// Only PutL1Validator is Chain-specific
|
|
PutL1Validator(validator L1Validator) error
|
|
}
|
|
|
|
type State interface {
|
|
Chain
|
|
uptime.State
|
|
lux.UTXOReader
|
|
|
|
GetLastAccepted() ids.ID
|
|
SetLastAccepted(blkID ids.ID)
|
|
|
|
GetStatelessBlock(blockID ids.ID) (block.Block, error)
|
|
|
|
// Invariant: [block] is an accepted block.
|
|
AddStatelessBlock(block block.Block)
|
|
|
|
GetBlockIDAtHeight(height uint64) (ids.ID, error)
|
|
|
|
GetRewardUTXOs(txID ids.ID) ([]*lux.UTXO, error)
|
|
GetChainIDs() ([]ids.ID, error)
|
|
GetChains(netID ids.ID) ([]*txs.Tx, error)
|
|
|
|
// ApplyValidatorWeightDiffs iterates from [startHeight] towards the genesis
|
|
// block until it has applied all of the diffs up to and including
|
|
// [endHeight]. Applying the diffs modifies [validators].
|
|
//
|
|
// Invariant: If attempting to generate the validator set for
|
|
// [endHeight - 1], [validators] must initially contain the validator
|
|
// weights for [startHeight].
|
|
//
|
|
// Note: Because this function iterates towards the genesis, [startHeight]
|
|
// will typically be greater than or equal to [endHeight]. If [startHeight]
|
|
// is less than [endHeight], no diffs will be applied.
|
|
ApplyValidatorWeightDiffs(
|
|
ctx context.Context,
|
|
validators map[ids.NodeID]*validators.GetValidatorOutput,
|
|
startHeight uint64,
|
|
endHeight uint64,
|
|
netID ids.ID,
|
|
) error
|
|
|
|
// ApplyValidatorPublicKeyDiffs iterates from [startHeight] towards the
|
|
// genesis block until it has applied all of the diffs up to and including
|
|
// [endHeight]. Applying the diffs modifies [validators].
|
|
//
|
|
// Invariant: If attempting to generate the validator set for
|
|
// [endHeight - 1], [validators] must initially contain the validator
|
|
// weights for [startHeight].
|
|
//
|
|
// Note: Because this function iterates towards the genesis, [startHeight]
|
|
// will typically be greater than or equal to [endHeight]. If [startHeight]
|
|
// is less than [endHeight], no diffs will be applied.
|
|
ApplyValidatorPublicKeyDiffs(
|
|
ctx context.Context,
|
|
validators map[ids.NodeID]*validators.GetValidatorOutput,
|
|
startHeight uint64,
|
|
endHeight uint64,
|
|
chainID ids.ID,
|
|
) error
|
|
|
|
SetHeight(height uint64)
|
|
|
|
// GetCurrentValidators returns chain and L1 validators for the given
|
|
// chainID along with the current P-chain height.
|
|
// This method works for both chains and L1s. Depending of the requested
|
|
// chain/L1 validator schema, the return values can include only chain
|
|
// validator, only L1 validators or both if there are initial stakers in the
|
|
// L1 conversion.
|
|
GetCurrentValidators(ctx context.Context, chainID ids.ID) ([]*Staker, []L1Validator, uint64, error)
|
|
|
|
// Discard uncommitted changes to the database.
|
|
Abort()
|
|
|
|
// ReindexBlocks converts any block indices using the legacy storage format
|
|
// to the new format. If this database has already updated the indices,
|
|
// this function will return immediately, without iterating over the
|
|
// database.
|
|
//
|
|
// TODO: Remove after v1.14.x is activated
|
|
ReindexBlocks(lock sync.Locker, log log.Logger) error
|
|
|
|
// Commit changes to the base database.
|
|
Commit() error
|
|
|
|
// Returns a batch of unwritten changes that, when written, will commit all
|
|
// pending changes to the base database.
|
|
CommitBatch() (database.Batch, error)
|
|
|
|
Checksum() ids.ID
|
|
|
|
Close() error
|
|
}
|
|
|
|
/*
|
|
* VMDB
|
|
* |-. validators
|
|
* | |-. current
|
|
* | | |-. validator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> uptime + potential reward + potential delegatee reward
|
|
* | | |-. delegator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> potential reward
|
|
* | | |-. chainValidator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> uptime + potential reward + potential delegatee reward
|
|
* | | '-. chainDelegator
|
|
* | | '-. list
|
|
* | | '-- txID -> potential reward
|
|
* | |-. pending
|
|
* | | |-. validator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> nil
|
|
* | | |-. delegator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> nil
|
|
* | | |-. chainValidator
|
|
* | | | '-. list
|
|
* | | | '-- txID -> nil
|
|
* | | '-. chainDelegator
|
|
* | | '-. list
|
|
* | | '-- txID -> nil
|
|
* | |-. l1
|
|
* | | |-. weights
|
|
* | | | '-- chainID -> weight
|
|
* | | |-. chainIDNodeID
|
|
* | | | '-- chainID+nodeID -> validationID
|
|
* | | |-. active
|
|
* | | | '-- validationID -> l1Validator
|
|
* | | '-. inactive
|
|
* | | '-- validationID -> l1Validator
|
|
* | |-. weight diffs
|
|
* | | '-- chain+height+nodeID -> weightChange
|
|
* | '-. pub key diffs
|
|
* | '-- chain+height+nodeID -> uncompressed public key or nil
|
|
* |-. blockIDs
|
|
* | '-- height -> blockID
|
|
* |-. blocks
|
|
* | '-- blockID -> block bytes
|
|
* |-. txs
|
|
* | '-- txID -> tx bytes + tx status
|
|
* |- rewardUTXOs
|
|
* | '-. txID
|
|
* | '-. list
|
|
* | '-- utxoID -> utxo bytes
|
|
* |- utxos
|
|
* | '-- utxoDB
|
|
* |-. chains
|
|
* | '-. list
|
|
* | '-- txID -> nil
|
|
* |-. chainOwners
|
|
* | '-- chainID -> owner
|
|
* |-. chainToL1Conversions
|
|
* | '-- chainID -> conversionID + chainID + addr
|
|
* |-. chains
|
|
* | '-. netID
|
|
* | '-. list
|
|
* | '-- txID -> nil
|
|
* |-. expiryReplayProtection
|
|
* | '-- timestamp + validationID -> nil
|
|
* '-. singletons
|
|
* |-- initializedKey -> nil
|
|
* |-- blocksReindexedKey -> nil
|
|
* |-- timestampKey -> timestamp
|
|
* |-- feeStateKey -> feeState
|
|
* |-- l1ValidatorExcessKey -> l1ValidatorExcess
|
|
* |-- accruedFeesKey -> accruedFees
|
|
* |-- currentSupplyKey -> currentSupply
|
|
* |-- lastAcceptedKey -> lastAccepted
|
|
* '-- heightsIndexKey -> startIndexHeight + endIndexHeight
|
|
*/
|
|
type state struct {
|
|
validatorState
|
|
|
|
validators validators.Manager
|
|
runtime *runtime.Runtime
|
|
upgrades upgrade.Config
|
|
metrics metrics.Metrics
|
|
rewards reward.Calculator
|
|
|
|
baseDB *versiondb.Database
|
|
|
|
expiry *btree.BTreeG[ExpiryEntry]
|
|
expiryDiff *expiryDiff
|
|
expiryDB database.Database
|
|
|
|
activeL1Validators *activeL1Validators
|
|
l1ValidatorsDiff *l1ValidatorsDiff
|
|
l1ValidatorsDiffLock sync.RWMutex // Protects concurrent access to l1ValidatorsDiff
|
|
l1ValidatorsDB database.Database
|
|
weightsCache cache.Cacher[ids.ID, uint64] // chainID -> total L1 validator weight
|
|
weightsDB database.Database
|
|
chainIDNodeIDCache cache.Cacher[chainIDNodeID, bool] // chainID+nodeID -> is validator
|
|
chainIDNodeIDDB database.Database
|
|
activeDB database.Database
|
|
inactiveCache cache.Cacher[ids.ID, maybe.Maybe[L1Validator]] // validationID -> L1Validator
|
|
inactiveDB database.Database
|
|
|
|
currentStakers *baseStakers
|
|
pendingStakers *baseStakers
|
|
|
|
currentHeight uint64
|
|
|
|
// L1 validators storage (temporary in-memory implementation)
|
|
l1Validators map[ids.ID]L1Validator
|
|
|
|
addedBlockIDs map[uint64]ids.ID // map of height -> blockID
|
|
blockIDCache cache.Cacher[uint64, ids.ID] // cache of height -> blockID; if the entry is ids.Empty, it is not in the database
|
|
blockIDDB database.Database
|
|
|
|
addedBlocks map[ids.ID]block.Block // map of blockID -> Block
|
|
blockCache cache.Cacher[ids.ID, block.Block] // cache of blockID -> Block; if the entry is nil, it is not in the database
|
|
blockDB database.Database
|
|
|
|
validatorsDB database.Database
|
|
currentValidatorsDB database.Database
|
|
currentValidatorBaseDB database.Database
|
|
currentValidatorList linkeddb.LinkedDB
|
|
currentDelegatorBaseDB database.Database
|
|
currentDelegatorList linkeddb.LinkedDB
|
|
currentNetValidatorBaseDB database.Database
|
|
currentNetValidatorList linkeddb.LinkedDB
|
|
currentNetDelegatorBaseDB database.Database
|
|
currentNetDelegatorList linkeddb.LinkedDB
|
|
pendingValidatorsDB database.Database
|
|
pendingValidatorBaseDB database.Database
|
|
pendingValidatorList linkeddb.LinkedDB
|
|
pendingDelegatorBaseDB database.Database
|
|
pendingDelegatorList linkeddb.LinkedDB
|
|
pendingNetValidatorBaseDB database.Database
|
|
pendingNetValidatorList linkeddb.LinkedDB
|
|
pendingNetDelegatorBaseDB database.Database
|
|
pendingNetDelegatorList linkeddb.LinkedDB
|
|
|
|
validatorWeightDiffsDB database.Database
|
|
validatorPublicKeyDiffsDB database.Database
|
|
|
|
addedTxs map[ids.ID]*txAndStatus // map of txID -> {*txs.Tx, Status}
|
|
txCache cache.Cacher[ids.ID, *txAndStatus] // txID -> {*txs.Tx, Status}; if the entry is nil, it is not in the database
|
|
txDB database.Database
|
|
|
|
addedRewardUTXOs map[ids.ID][]*lux.UTXO // map of txID -> []*UTXO
|
|
rewardUTXOsCache cache.Cacher[ids.ID, []*lux.UTXO] // txID -> []*UTXO
|
|
rewardUTXODB database.Database
|
|
|
|
modifiedUTXOs map[ids.ID]*lux.UTXO // map of modified UTXOID -> *UTXO; if the UTXO is nil, it has been removed
|
|
utxoDB database.Database
|
|
utxoState lux.UTXOState
|
|
|
|
cachedChainIDs []ids.ID // nil if the chains haven't been loaded
|
|
addedChainIDs []ids.ID
|
|
chainBaseDB database.Database
|
|
chainDB linkeddb.LinkedDB
|
|
|
|
chainOwners map[ids.ID]fx.Owner // map of netID -> owner
|
|
chainOwnerCache cache.Cacher[ids.ID, fxOwnerAndSize] // cache of netID -> owner; if the entry is nil, it is not in the database
|
|
chainOwnerDB database.Database
|
|
|
|
chainToL1Conversions map[ids.ID]NetToL1Conversion // map of chainID -> conversion of the chain
|
|
chainToL1ConversionCache cache.Cacher[ids.ID, NetToL1Conversion] // cache of chainID -> conversion
|
|
chainToL1ConversionDB database.Database
|
|
|
|
transformedNets map[ids.ID]*txs.Tx // map of chainID -> transformNetTx
|
|
transformedNetCache cache.Cacher[ids.ID, *txs.Tx] // cache of chainID -> transformNetTx; if the entry is nil, it is not in the database
|
|
transformedNetDB database.Database
|
|
|
|
modifiedSupplies map[ids.ID]uint64 // map of netID -> current supply
|
|
supplyCache cache.Cacher[ids.ID, *uint64] // cache of netID -> current supply; if the entry is nil, it is not in the database
|
|
supplyDB database.Database
|
|
|
|
addedChains map[ids.ID][]*txs.Tx // maps netID -> the newly added chains to the chain
|
|
chainCache cache.Cacher[ids.ID, []*txs.Tx] // cache of netID -> the chains after all local modifications []*txs.Tx
|
|
chainDBCache cache.Cacher[ids.ID, linkeddb.LinkedDB] // cache of netID -> linkedDB
|
|
chainTxDB database.Database
|
|
|
|
// Chain name uniqueness - maps lowercase chain name to chain ID
|
|
addedChainNames map[string]ids.ID // newly added chain names (lowercase) -> chainID
|
|
chainNameCache cache.Cacher[string, ids.ID] // cache of lowercase chain name -> chainID
|
|
chainNameDB database.Database
|
|
|
|
// The persisted fields represent the current database value
|
|
timestamp, persistedTimestamp time.Time
|
|
feeState, persistedFeeState gas.State
|
|
l1ValidatorExcess, persistedL1ValidatorExcess gas.Gas
|
|
accruedFees, persistedAccruedFees uint64
|
|
currentSupply, persistedCurrentSupply uint64
|
|
// [lastAccepted] is the most recently accepted block.
|
|
lastAccepted, persistedLastAccepted ids.ID
|
|
lastAcceptedLock sync.RWMutex // Protects concurrent access to lastAccepted
|
|
indexedHeights *heightRange
|
|
singletonDB database.Database
|
|
}
|
|
|
|
// heightRange is used to track which heights are safe to use the native DB
|
|
// iterator for querying validator diffs.
|
|
//
|
|
// new indexing mechanism.
|
|
type heightRange struct {
|
|
LowerBound uint64 `serialize:"true"`
|
|
UpperBound uint64 `serialize:"true"`
|
|
}
|
|
|
|
type ValidatorWeightDiff struct {
|
|
Decrease bool `serialize:"true"`
|
|
Amount uint64 `serialize:"true"`
|
|
ValidationID ids.ID `serialize:"true"` // Added to preserve TxID during diff application
|
|
}
|
|
|
|
func (v *ValidatorWeightDiff) Add(amount uint64) error {
|
|
return v.addOrSub(false, amount)
|
|
}
|
|
|
|
func (v *ValidatorWeightDiff) Sub(amount uint64) error {
|
|
return v.addOrSub(true, amount)
|
|
}
|
|
|
|
func (v *ValidatorWeightDiff) addOrSub(sub bool, amount uint64) error {
|
|
if v.Decrease == sub {
|
|
var err error
|
|
v.Amount, err = safemath.Add(v.Amount, amount)
|
|
return err
|
|
}
|
|
|
|
if v.Amount > amount {
|
|
v.Amount -= amount
|
|
} else {
|
|
v.Amount = safemath.AbsDiff(v.Amount, amount)
|
|
v.Decrease = sub
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type txBytesAndStatus struct {
|
|
Tx []byte `serialize:"true"`
|
|
Status status.Status `serialize:"true"`
|
|
}
|
|
|
|
type txAndStatus struct {
|
|
tx *txs.Tx
|
|
status status.Status
|
|
}
|
|
|
|
type fxOwnerAndSize struct {
|
|
owner fx.Owner
|
|
size int
|
|
}
|
|
|
|
type NetToL1Conversion struct {
|
|
ConversionID ids.ID `serialize:"true"`
|
|
ChainID ids.ID `serialize:"true"`
|
|
Addr []byte `serialize:"true"`
|
|
}
|
|
|
|
func txSize(_ ids.ID, tx *txs.Tx) int {
|
|
if tx == nil {
|
|
return ids.IDLen + constants.PointerOverhead
|
|
}
|
|
return ids.IDLen + len(tx.Bytes()) + constants.PointerOverhead
|
|
}
|
|
|
|
func txAndStatusSize(_ ids.ID, t *txAndStatus) int {
|
|
if t == nil {
|
|
return ids.IDLen + constants.PointerOverhead
|
|
}
|
|
return ids.IDLen + len(t.tx.Bytes()) + 4 + 2*constants.PointerOverhead
|
|
}
|
|
|
|
func blockSize(_ ids.ID, blk block.Block) int {
|
|
if blk == nil {
|
|
return ids.IDLen + constants.PointerOverhead
|
|
}
|
|
return ids.IDLen + len(blk.Bytes()) + constants.PointerOverhead
|
|
}
|
|
|
|
// New constructs a State backed by db. genesisCodec is the proto/p
|
|
// block-genesis codec (block.RegisterTypes bound through a
|
|
// codec.NewManager(math.MaxInt32) — typically built via
|
|
// proto/internal/pcodectest.NewPVMCodecs() in tests, or via the PVM's
|
|
// own codec wiring in production). txCodec is the runtime tx codec
|
|
// (codec.NewDefaultManager bound through the same RegisterTypes).
|
|
func New(
|
|
db database.Database,
|
|
genesisBytes []byte,
|
|
metricsReg metric.Registerer,
|
|
validators validators.Manager,
|
|
upgrades upgrade.Config,
|
|
execCfg *config.Config,
|
|
rt *runtime.Runtime,
|
|
metrics metrics.Metrics,
|
|
rewards reward.Calculator,
|
|
) (State, error) {
|
|
// Convert metric.Registerer to metric.Registry
|
|
// metricsReg implements Registerer, cast it to metric.Registry
|
|
var reg metric.Registry
|
|
if r, ok := metricsReg.(metric.Registry); ok {
|
|
reg = r
|
|
} else {
|
|
// If conversion fails, use nil (metercacher will handle it)
|
|
reg = nil
|
|
}
|
|
|
|
blockIDCache, err := metercacher.New[uint64, ids.ID](
|
|
"block_id_cache",
|
|
reg,
|
|
lru.NewCache[uint64, ids.ID](execCfg.BlockIDCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockCache, err := metercacher.New[ids.ID, block.Block](
|
|
"block_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.BlockCacheSize, blockSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseDB := versiondb.New(db)
|
|
|
|
validatorsDB := prefixdb.New(ValidatorsPrefix, baseDB)
|
|
|
|
currentValidatorsDB := prefixdb.New(CurrentPrefix, validatorsDB)
|
|
currentValidatorBaseDB := prefixdb.New(ValidatorPrefix, currentValidatorsDB)
|
|
currentDelegatorBaseDB := prefixdb.New(DelegatorPrefix, currentValidatorsDB)
|
|
currentNetValidatorBaseDB := prefixdb.New(NetValidatorPrefix, currentValidatorsDB)
|
|
currentNetDelegatorBaseDB := prefixdb.New(NetDelegatorPrefix, currentValidatorsDB)
|
|
|
|
pendingValidatorsDB := prefixdb.New(PendingPrefix, validatorsDB)
|
|
pendingValidatorBaseDB := prefixdb.New(ValidatorPrefix, pendingValidatorsDB)
|
|
pendingDelegatorBaseDB := prefixdb.New(DelegatorPrefix, pendingValidatorsDB)
|
|
pendingNetValidatorBaseDB := prefixdb.New(NetValidatorPrefix, pendingValidatorsDB)
|
|
pendingNetDelegatorBaseDB := prefixdb.New(NetDelegatorPrefix, pendingValidatorsDB)
|
|
|
|
l1ValidatorsDB := prefixdb.New(L1Prefix, validatorsDB)
|
|
|
|
validatorWeightDiffsDB := prefixdb.New(ValidatorWeightDiffsPrefix, validatorsDB)
|
|
validatorPublicKeyDiffsDB := prefixdb.New(ValidatorPublicKeyDiffsPrefix, validatorsDB)
|
|
|
|
weightsCache, err := metercacher.New(
|
|
"l1_validator_weights_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.L1WeightsCacheSize, func(ids.ID, uint64) int {
|
|
return ids.IDLen + 8
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inactiveL1ValidatorsCache, err := metercacher.New(
|
|
"l1_validator_inactive_cache",
|
|
reg,
|
|
lru.NewSizedCache(
|
|
execCfg.L1InactiveValidatorsCacheSize,
|
|
func(_ ids.ID, maybeL1Validator maybe.Maybe[L1Validator]) int {
|
|
const (
|
|
l1ValidatorOverhead = ids.IDLen + ids.NodeIDLen + 4*8 + 3*constants.PointerOverhead
|
|
maybeL1ValidatorOverhead = 1 + l1ValidatorOverhead
|
|
entryOverhead = ids.IDLen + maybeL1ValidatorOverhead
|
|
)
|
|
if maybeL1Validator.IsNothing() {
|
|
return entryOverhead
|
|
}
|
|
|
|
l1Validator := maybeL1Validator.Value()
|
|
return entryOverhead + len(l1Validator.PublicKey) + len(l1Validator.RemainingBalanceOwner) + len(l1Validator.DeactivationOwner)
|
|
},
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainIDNodeIDCache, err := metercacher.New(
|
|
"l1_validator_chain_id_node_id_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.L1NetIDNodeIDCacheSize, func(chainIDNodeID, bool) int {
|
|
return ids.IDLen + ids.NodeIDLen + 1
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txCache, err := metercacher.New(
|
|
"tx_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.TxCacheSize, txAndStatusSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rewardUTXODB := prefixdb.New(RewardUTXOsPrefix, baseDB)
|
|
rewardUTXOsCache, err := metercacher.New[ids.ID, []*lux.UTXO](
|
|
"reward_utxos_cache",
|
|
reg,
|
|
lru.NewCache[ids.ID, []*lux.UTXO](execCfg.RewardUTXOsCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utxoDB := prefixdb.New(UTXOPrefix, baseDB)
|
|
utxoState, err := lux.NewMeteredUTXOState(utxoDB, metricsReg, execCfg.ChecksumsEnabled)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainBaseDB := prefixdb.New(NetPrefix, baseDB)
|
|
|
|
chainOwnerDB := prefixdb.New(NetOwnerPrefix, baseDB)
|
|
chainOwnerCache, err := metercacher.New[ids.ID, fxOwnerAndSize](
|
|
"chain_owner_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.FxOwnerCacheSize, func(_ ids.ID, f fxOwnerAndSize) int {
|
|
return ids.IDLen + f.size
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainToL1ConversionDB := prefixdb.New(NetToL1ConversionPrefix, baseDB)
|
|
chainToL1ConversionCache, err := metercacher.New[ids.ID, NetToL1Conversion](
|
|
"chain_conversion_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.NetToL1ConversionCacheSize, func(_ ids.ID, c NetToL1Conversion) int {
|
|
return 3*ids.IDLen + len(c.Addr)
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transformedNetCache, err := metercacher.New(
|
|
"transformed_chain_cache",
|
|
reg,
|
|
lru.NewSizedCache(execCfg.TransformedNetTxCacheSize, txSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
supplyCache, err := metercacher.New[ids.ID, *uint64](
|
|
"supply_cache",
|
|
reg,
|
|
lru.NewCache[ids.ID, *uint64](execCfg.ChainCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainCache, err := metercacher.New[ids.ID, []*txs.Tx](
|
|
"chain_cache",
|
|
reg,
|
|
lru.NewCache[ids.ID, []*txs.Tx](execCfg.ChainCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainDBCache, err := metercacher.New[ids.ID, linkeddb.LinkedDB](
|
|
"chain_db_cache",
|
|
reg,
|
|
lru.NewCache[ids.ID, linkeddb.LinkedDB](execCfg.ChainDBCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainNameCache, err := metercacher.New[string, ids.ID](
|
|
"chain_name_cache",
|
|
reg,
|
|
lru.NewCache[string, ids.ID](execCfg.ChainCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &state{
|
|
validatorState: newValidatorState(),
|
|
|
|
validators: validators,
|
|
runtime: rt,
|
|
upgrades: upgrades,
|
|
metrics: metrics,
|
|
rewards: rewards,
|
|
baseDB: baseDB,
|
|
|
|
addedBlockIDs: make(map[uint64]ids.ID),
|
|
blockIDCache: blockIDCache,
|
|
blockIDDB: prefixdb.New(BlockIDPrefix, baseDB),
|
|
|
|
addedBlocks: make(map[ids.ID]block.Block),
|
|
blockCache: blockCache,
|
|
blockDB: prefixdb.New(BlockPrefix, baseDB),
|
|
|
|
expiry: btree.NewG(defaultTreeDegree, ExpiryEntry.Less),
|
|
expiryDiff: newExpiryDiff(),
|
|
expiryDB: prefixdb.New(ExpiryReplayProtectionPrefix, baseDB),
|
|
|
|
activeL1Validators: newActiveL1Validators(),
|
|
l1ValidatorsDiff: newL1ValidatorsDiff(),
|
|
l1ValidatorsDB: l1ValidatorsDB,
|
|
weightsCache: weightsCache,
|
|
weightsDB: prefixdb.New(WeightsPrefix, l1ValidatorsDB),
|
|
chainIDNodeIDCache: chainIDNodeIDCache,
|
|
chainIDNodeIDDB: prefixdb.New(ChainIDNodeIDPrefix, l1ValidatorsDB),
|
|
activeDB: prefixdb.New(ActivePrefix, l1ValidatorsDB),
|
|
inactiveCache: inactiveL1ValidatorsCache,
|
|
inactiveDB: prefixdb.New(InactivePrefix, l1ValidatorsDB),
|
|
|
|
currentStakers: newBaseStakers(),
|
|
pendingStakers: newBaseStakers(),
|
|
|
|
l1Validators: make(map[ids.ID]L1Validator),
|
|
|
|
validatorsDB: validatorsDB,
|
|
currentValidatorsDB: currentValidatorsDB,
|
|
currentValidatorBaseDB: currentValidatorBaseDB,
|
|
currentValidatorList: linkeddb.NewDefault(currentValidatorBaseDB),
|
|
currentDelegatorBaseDB: currentDelegatorBaseDB,
|
|
currentDelegatorList: linkeddb.NewDefault(currentDelegatorBaseDB),
|
|
currentNetValidatorBaseDB: currentNetValidatorBaseDB,
|
|
currentNetValidatorList: linkeddb.NewDefault(currentNetValidatorBaseDB),
|
|
currentNetDelegatorBaseDB: currentNetDelegatorBaseDB,
|
|
currentNetDelegatorList: linkeddb.NewDefault(currentNetDelegatorBaseDB),
|
|
pendingValidatorsDB: pendingValidatorsDB,
|
|
pendingValidatorBaseDB: pendingValidatorBaseDB,
|
|
pendingValidatorList: linkeddb.NewDefault(pendingValidatorBaseDB),
|
|
pendingDelegatorBaseDB: pendingDelegatorBaseDB,
|
|
pendingDelegatorList: linkeddb.NewDefault(pendingDelegatorBaseDB),
|
|
pendingNetValidatorBaseDB: pendingNetValidatorBaseDB,
|
|
pendingNetValidatorList: linkeddb.NewDefault(pendingNetValidatorBaseDB),
|
|
pendingNetDelegatorBaseDB: pendingNetDelegatorBaseDB,
|
|
pendingNetDelegatorList: linkeddb.NewDefault(pendingNetDelegatorBaseDB),
|
|
validatorWeightDiffsDB: validatorWeightDiffsDB,
|
|
validatorPublicKeyDiffsDB: validatorPublicKeyDiffsDB,
|
|
|
|
addedTxs: make(map[ids.ID]*txAndStatus),
|
|
txDB: prefixdb.New(TxPrefix, baseDB),
|
|
txCache: txCache,
|
|
|
|
addedRewardUTXOs: make(map[ids.ID][]*lux.UTXO),
|
|
rewardUTXODB: rewardUTXODB,
|
|
rewardUTXOsCache: rewardUTXOsCache,
|
|
|
|
modifiedUTXOs: make(map[ids.ID]*lux.UTXO),
|
|
utxoDB: utxoDB,
|
|
utxoState: utxoState,
|
|
|
|
chainBaseDB: chainBaseDB,
|
|
chainDB: linkeddb.NewDefault(chainBaseDB),
|
|
|
|
chainOwners: make(map[ids.ID]fx.Owner),
|
|
chainOwnerDB: chainOwnerDB,
|
|
chainOwnerCache: chainOwnerCache,
|
|
|
|
chainToL1Conversions: make(map[ids.ID]NetToL1Conversion),
|
|
chainToL1ConversionDB: chainToL1ConversionDB,
|
|
chainToL1ConversionCache: chainToL1ConversionCache,
|
|
|
|
transformedNets: make(map[ids.ID]*txs.Tx),
|
|
transformedNetCache: transformedNetCache,
|
|
transformedNetDB: prefixdb.New(TransformedNetPrefix, baseDB),
|
|
|
|
modifiedSupplies: make(map[ids.ID]uint64),
|
|
supplyCache: supplyCache,
|
|
supplyDB: prefixdb.New(SupplyPrefix, baseDB),
|
|
|
|
addedChains: make(map[ids.ID][]*txs.Tx),
|
|
chainTxDB: prefixdb.New(ChainPrefix, baseDB),
|
|
chainCache: chainCache,
|
|
chainDBCache: chainDBCache,
|
|
|
|
addedChainNames: make(map[string]ids.ID),
|
|
chainNameCache: chainNameCache,
|
|
chainNameDB: prefixdb.New(ChainNamePrefix, baseDB),
|
|
|
|
singletonDB: prefixdb.New(SingletonPrefix, baseDB),
|
|
}
|
|
|
|
if err := s.sync(genesisBytes); err != nil {
|
|
return nil, errors.Join(
|
|
err,
|
|
s.Close(),
|
|
)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func (s *state) GetExpiryIterator() (iterator.Iterator[ExpiryEntry], error) {
|
|
return s.expiryDiff.getExpiryIterator(
|
|
iterator.FromTree(s.expiry),
|
|
), nil
|
|
}
|
|
|
|
// HasExpiry allows for concurrent reads.
|
|
func (s *state) HasExpiry(entry ExpiryEntry) (bool, error) {
|
|
if has, modified := s.expiryDiff.modified[entry]; modified {
|
|
return has, nil
|
|
}
|
|
return s.expiry.Has(entry), nil
|
|
}
|
|
|
|
func (s *state) PutExpiry(entry ExpiryEntry) {
|
|
s.expiryDiff.PutExpiry(entry)
|
|
}
|
|
|
|
func (s *state) DeleteExpiry(entry ExpiryEntry) {
|
|
s.expiryDiff.DeleteExpiry(entry)
|
|
}
|
|
|
|
func (s *state) GetCurrentValidators(ctx context.Context, chainID ids.ID) ([]*Staker, []L1Validator, uint64, error) {
|
|
// First add the current validators (non-L1)
|
|
legacyBaseStakers := s.currentStakers.validators[chainID]
|
|
legacyStakers := make([]*Staker, 0, len(legacyBaseStakers))
|
|
for _, staker := range legacyBaseStakers {
|
|
legacyStakers = append(legacyStakers, staker.validator)
|
|
}
|
|
|
|
// Then iterate over chainIDNodeID DB and add the L1 validators
|
|
var l1Validators []L1Validator
|
|
validationIDIter := s.chainIDNodeIDDB.NewIteratorWithPrefix(
|
|
chainID[:],
|
|
)
|
|
defer validationIDIter.Release()
|
|
|
|
for validationIDIter.Next() {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
validationID, err := ids.ToID(validationIDIter.Value())
|
|
if err != nil {
|
|
return nil, nil, 0, fmt.Errorf("failed to parse validation ID: %w", err)
|
|
}
|
|
|
|
vdr, err := s.GetL1Validator(validationID)
|
|
if err != nil {
|
|
return nil, nil, 0, fmt.Errorf("failed to get validator: %w", err)
|
|
}
|
|
l1Validators = append(l1Validators, vdr)
|
|
}
|
|
|
|
return legacyStakers, l1Validators, s.currentHeight, nil
|
|
}
|
|
|
|
func (s *state) GetActiveL1ValidatorsIterator() (iterator.Iterator[L1Validator], error) {
|
|
s.l1ValidatorsDiffLock.RLock()
|
|
defer s.l1ValidatorsDiffLock.RUnlock()
|
|
return s.l1ValidatorsDiff.getActiveL1ValidatorsIterator(
|
|
s.activeL1Validators.newIterator(),
|
|
), nil
|
|
}
|
|
|
|
func (s *state) NumActiveL1Validators() int {
|
|
return s.activeL1Validators.len() + s.l1ValidatorsDiff.netAddedActive
|
|
}
|
|
|
|
func (s *state) WeightOfL1Validators(chainID ids.ID) (uint64, error) {
|
|
if weight, modified := s.l1ValidatorsDiff.modifiedTotalWeight[chainID]; modified {
|
|
return weight, nil
|
|
}
|
|
|
|
if weight, ok := s.weightsCache.Get(chainID); ok {
|
|
return weight, nil
|
|
}
|
|
|
|
weight, err := database.GetUInt64(s.weightsDB, chainID[:])
|
|
if err != nil {
|
|
if err == database.ErrNotFound {
|
|
weight = 0
|
|
} else {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
s.weightsCache.Put(chainID, weight)
|
|
return weight, nil
|
|
}
|
|
|
|
// GetL1Validator allows for concurrent reads.
|
|
func (s *state) GetL1Validator(validationID ids.ID) (L1Validator, error) {
|
|
if l1Validator, modified := s.l1ValidatorsDiff.modified[validationID]; modified {
|
|
if l1Validator.isDeleted() {
|
|
return L1Validator{}, database.ErrNotFound
|
|
}
|
|
return l1Validator, nil
|
|
}
|
|
|
|
return s.getPersistedL1Validator(validationID)
|
|
}
|
|
|
|
// getPersistedL1Validator returns the currently persisted
|
|
// L1Validator with the given validationID. It is guaranteed that any
|
|
// returned validator is either active or inactive (not deleted).
|
|
func (s *state) getPersistedL1Validator(validationID ids.ID) (L1Validator, error) {
|
|
if l1Validator, ok := s.activeL1Validators.get(validationID); ok {
|
|
return l1Validator, nil
|
|
}
|
|
|
|
return getL1Validator(s.inactiveCache, s.inactiveDB, validationID)
|
|
}
|
|
|
|
func (s *state) HasL1Validator(chainID ids.ID, nodeID ids.NodeID) (bool, error) {
|
|
if has, modified := s.l1ValidatorsDiff.hasL1Validator(chainID, nodeID); modified {
|
|
return has, nil
|
|
}
|
|
|
|
chainIDNodeID := chainIDNodeID{
|
|
chainID: chainID,
|
|
nodeID: nodeID,
|
|
}
|
|
if has, ok := s.chainIDNodeIDCache.Get(chainIDNodeID); ok {
|
|
return has, nil
|
|
}
|
|
|
|
key := chainIDNodeID.Marshal()
|
|
has, err := s.chainIDNodeIDDB.Has(key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
s.chainIDNodeIDCache.Put(chainIDNodeID, has)
|
|
return has, nil
|
|
}
|
|
|
|
func (s *state) PutL1Validator(l1Validator L1Validator) error {
|
|
return s.l1ValidatorsDiff.putL1Validator(s, l1Validator)
|
|
}
|
|
|
|
func (s *state) GetCurrentValidator(netID ids.ID, nodeID ids.NodeID) (*Staker, error) {
|
|
return s.currentStakers.GetValidator(netID, nodeID)
|
|
}
|
|
|
|
func (s *state) PutCurrentValidator(staker *Staker) error {
|
|
s.currentStakers.PutValidator(staker)
|
|
return nil
|
|
}
|
|
|
|
func (s *state) DeleteCurrentValidator(staker *Staker) {
|
|
s.currentStakers.DeleteValidator(staker)
|
|
}
|
|
|
|
func (s *state) GetCurrentDelegatorIterator(chainID ids.ID, nodeID ids.NodeID) (iterator.Iterator[*Staker], error) {
|
|
return s.currentStakers.GetDelegatorIterator(chainID, nodeID), nil
|
|
}
|
|
|
|
func (s *state) PutCurrentDelegator(staker *Staker) {
|
|
s.currentStakers.PutDelegator(staker)
|
|
}
|
|
|
|
func (s *state) DeleteCurrentDelegator(staker *Staker) {
|
|
s.currentStakers.DeleteDelegator(staker)
|
|
}
|
|
|
|
func (s *state) GetCurrentStakerIterator() (iterator.Iterator[*Staker], error) {
|
|
return s.currentStakers.GetStakerIterator(), nil
|
|
}
|
|
|
|
func (s *state) GetPendingValidator(netID ids.ID, nodeID ids.NodeID) (*Staker, error) {
|
|
return s.pendingStakers.GetValidator(netID, nodeID)
|
|
}
|
|
|
|
func (s *state) PutPendingValidator(staker *Staker) error {
|
|
s.pendingStakers.PutValidator(staker)
|
|
return nil
|
|
}
|
|
|
|
func (s *state) DeletePendingValidator(staker *Staker) {
|
|
s.pendingStakers.DeleteValidator(staker)
|
|
}
|
|
|
|
func (s *state) GetPendingDelegatorIterator(chainID ids.ID, nodeID ids.NodeID) (iterator.Iterator[*Staker], error) {
|
|
return s.pendingStakers.GetDelegatorIterator(chainID, nodeID), nil
|
|
}
|
|
|
|
func (s *state) PutPendingDelegator(staker *Staker) {
|
|
s.pendingStakers.PutDelegator(staker)
|
|
}
|
|
|
|
func (s *state) DeletePendingDelegator(staker *Staker) {
|
|
s.pendingStakers.DeleteDelegator(staker)
|
|
}
|
|
|
|
func (s *state) GetPendingStakerIterator() (iterator.Iterator[*Staker], error) {
|
|
return s.pendingStakers.GetStakerIterator(), nil
|
|
}
|
|
|
|
func (s *state) GetChainIDs() ([]ids.ID, error) {
|
|
if s.cachedChainIDs != nil {
|
|
return s.cachedChainIDs, nil
|
|
}
|
|
|
|
chainDBIt := s.chainDB.NewIterator()
|
|
defer chainDBIt.Release()
|
|
|
|
chainIDs := []ids.ID{}
|
|
for chainDBIt.Next() {
|
|
chainIDBytes := chainDBIt.Key()
|
|
chainID, err := ids.ToID(chainIDBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainIDs = append(chainIDs, chainID)
|
|
}
|
|
if err := chainDBIt.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
chainIDs = append(chainIDs, s.addedChainIDs...)
|
|
s.cachedChainIDs = chainIDs
|
|
return chainIDs, nil
|
|
}
|
|
|
|
func (s *state) AddNet(chainID ids.ID) {
|
|
s.addedChainIDs = append(s.addedChainIDs, chainID)
|
|
if s.cachedChainIDs != nil {
|
|
s.cachedChainIDs = append(s.cachedChainIDs, chainID)
|
|
}
|
|
}
|
|
|
|
func (s *state) GetNetOwner(netID ids.ID) (fx.Owner, error) {
|
|
if owner, exists := s.chainOwners[netID]; exists {
|
|
return owner, nil
|
|
}
|
|
|
|
if ownerAndSize, cached := s.chainOwnerCache.Get(netID); cached {
|
|
if ownerAndSize.owner == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return ownerAndSize.owner, nil
|
|
}
|
|
|
|
ownerBytes, err := s.chainOwnerDB.Get(netID[:])
|
|
if err == nil {
|
|
owner, err := parseOwner(ownerBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.chainOwnerCache.Put(netID, fxOwnerAndSize{
|
|
owner: owner,
|
|
size: len(ownerBytes),
|
|
})
|
|
return owner, nil
|
|
}
|
|
if err != database.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
|
|
chainIntf, _, err := s.GetTx(netID)
|
|
if err != nil {
|
|
if err == database.ErrNotFound {
|
|
s.chainOwnerCache.Put(netID, fxOwnerAndSize{})
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
network, ok := chainIntf.Unsigned.(*txs.CreateNetworkTx)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q %w", netID, errIsNotNet)
|
|
}
|
|
|
|
s.SetNetOwner(netID, network.Owner)
|
|
return network.Owner, nil
|
|
}
|
|
|
|
func (s *state) SetNetOwner(netID ids.ID, owner fx.Owner) {
|
|
s.chainOwners[netID] = owner
|
|
}
|
|
|
|
// GetNetToL1Conversion allows for concurrent reads.
|
|
func (s *state) GetNetToL1Conversion(chainID ids.ID) (NetToL1Conversion, error) {
|
|
if c, ok := s.chainToL1Conversions[chainID]; ok {
|
|
return c, nil
|
|
}
|
|
|
|
if c, ok := s.chainToL1ConversionCache.Get(chainID); ok {
|
|
return c, nil
|
|
}
|
|
|
|
bytes, err := s.chainToL1ConversionDB.Get(chainID[:])
|
|
if err != nil {
|
|
return NetToL1Conversion{}, err
|
|
}
|
|
|
|
c, err := parseConversion(bytes)
|
|
if err != nil {
|
|
return NetToL1Conversion{}, err
|
|
}
|
|
s.chainToL1ConversionCache.Put(chainID, c)
|
|
return c, nil
|
|
}
|
|
|
|
func (s *state) SetNetToL1Conversion(chainID ids.ID, c NetToL1Conversion) {
|
|
s.chainToL1Conversions[chainID] = c
|
|
}
|
|
|
|
func (s *state) GetNetTransformation(chainID ids.ID) (*txs.Tx, error) {
|
|
if tx, exists := s.transformedNets[chainID]; exists {
|
|
return tx, nil
|
|
}
|
|
|
|
if tx, cached := s.transformedNetCache.Get(chainID); cached {
|
|
if tx == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return tx, nil
|
|
}
|
|
|
|
transformNetTxID, err := database.GetID(s.transformedNetDB, chainID[:])
|
|
if err == database.ErrNotFound {
|
|
s.transformedNetCache.Put(chainID, nil)
|
|
return nil, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transformNetTx, _, err := s.GetTx(transformNetTxID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.transformedNetCache.Put(chainID, transformNetTx)
|
|
return transformNetTx, nil
|
|
}
|
|
|
|
func (s *state) AddNetTransformation(transformNetTxIntf *txs.Tx) {
|
|
transformNetTx := transformNetTxIntf.Unsigned.(*txs.TransformChainTx)
|
|
s.transformedNets[transformNetTx.Chain] = transformNetTxIntf
|
|
}
|
|
|
|
func (s *state) GetChains(netID ids.ID) ([]*txs.Tx, error) {
|
|
if chains, cached := s.chainCache.Get(netID); cached {
|
|
return chains, nil
|
|
}
|
|
chainDB := s.getChainDB(netID)
|
|
chainDBIt := chainDB.NewIterator()
|
|
defer chainDBIt.Release()
|
|
|
|
txs := []*txs.Tx(nil)
|
|
for chainDBIt.Next() {
|
|
chainIDBytes := chainDBIt.Key()
|
|
chainID, err := ids.ToID(chainIDBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainTx, _, err := s.GetTx(chainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txs = append(txs, chainTx)
|
|
}
|
|
if err := chainDBIt.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
txs = append(txs, s.addedChains[netID]...)
|
|
s.chainCache.Put(netID, txs)
|
|
return txs, nil
|
|
}
|
|
|
|
func (s *state) AddChain(createChainTxIntf *txs.Tx) {
|
|
createChainTx := createChainTxIntf.Unsigned.(*txs.CreateChainTx)
|
|
netID := createChainTx.ValidateNetworkID
|
|
s.addedChains[netID] = append(s.addedChains[netID], createChainTxIntf)
|
|
if chains, cached := s.chainCache.Get(netID); cached {
|
|
chains = append(chains, createChainTxIntf)
|
|
s.chainCache.Put(netID, chains)
|
|
}
|
|
|
|
// Register chain name for uniqueness tracking (case-insensitive)
|
|
if createChainTx.BlockchainName != "" {
|
|
nameLower := strings.ToLower(createChainTx.BlockchainName)
|
|
chainID := createChainTxIntf.ID()
|
|
s.addedChainNames[nameLower] = chainID
|
|
s.chainNameCache.Put(nameLower, chainID)
|
|
}
|
|
}
|
|
|
|
// GetChainIDByName returns the chain ID for the given chain name (case-insensitive).
|
|
// Returns database.ErrNotFound if no chain with the given name exists.
|
|
func (s *state) GetChainIDByName(name string) (ids.ID, error) {
|
|
nameLower := strings.ToLower(name)
|
|
|
|
// Check in-memory additions first
|
|
if chainID, exists := s.addedChainNames[nameLower]; exists {
|
|
return chainID, nil
|
|
}
|
|
|
|
// Check cache
|
|
if chainID, cached := s.chainNameCache.Get(nameLower); cached {
|
|
return chainID, nil
|
|
}
|
|
|
|
// Check database
|
|
chainIDBytes, err := s.chainNameDB.Get([]byte(nameLower))
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
chainID, err := ids.ToID(chainIDBytes)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
s.chainNameCache.Put(nameLower, chainID)
|
|
return chainID, nil
|
|
}
|
|
|
|
// IsChainNameTaken returns true if a chain with the given name already exists (case-insensitive).
|
|
func (s *state) IsChainNameTaken(name string) bool {
|
|
_, err := s.GetChainIDByName(name)
|
|
return err == nil
|
|
}
|
|
|
|
func (s *state) getChainDB(netID ids.ID) linkeddb.LinkedDB {
|
|
if chainDB, cached := s.chainDBCache.Get(netID); cached {
|
|
return chainDB
|
|
}
|
|
rawChainDB := prefixdb.New(netID[:], s.chainTxDB)
|
|
chainDB := linkeddb.NewDefault(rawChainDB)
|
|
s.chainDBCache.Put(netID, chainDB)
|
|
return chainDB
|
|
}
|
|
|
|
func (s *state) GetTx(txID ids.ID) (*txs.Tx, status.Status, error) {
|
|
if tx, exists := s.addedTxs[txID]; exists {
|
|
return tx.tx, tx.status, nil
|
|
}
|
|
if tx, cached := s.txCache.Get(txID); cached {
|
|
if tx == nil {
|
|
return nil, status.Unknown, database.ErrNotFound
|
|
}
|
|
return tx.tx, tx.status, nil
|
|
}
|
|
txBytes, err := s.txDB.Get(txID[:])
|
|
if err == database.ErrNotFound {
|
|
s.txCache.Put(txID, nil)
|
|
return nil, status.Unknown, database.ErrNotFound
|
|
} else if err != nil {
|
|
return nil, status.Unknown, err
|
|
}
|
|
|
|
stx, err := parseTxStatus(txBytes)
|
|
if err != nil {
|
|
return nil, status.Unknown, err
|
|
}
|
|
|
|
tx, err := txs.Parse(stx.Tx)
|
|
if err != nil {
|
|
return nil, status.Unknown, err
|
|
}
|
|
|
|
ptx := &txAndStatus{
|
|
tx: tx,
|
|
status: stx.Status,
|
|
}
|
|
|
|
s.txCache.Put(txID, ptx)
|
|
return ptx.tx, ptx.status, nil
|
|
}
|
|
|
|
func (s *state) AddTx(tx *txs.Tx, status status.Status) {
|
|
s.addedTxs[tx.ID()] = &txAndStatus{
|
|
tx: tx,
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
func (s *state) GetRewardUTXOs(txID ids.ID) ([]*lux.UTXO, error) {
|
|
if utxos, exists := s.addedRewardUTXOs[txID]; exists {
|
|
return utxos, nil
|
|
}
|
|
if utxos, exists := s.rewardUTXOsCache.Get(txID); exists {
|
|
return utxos, nil
|
|
}
|
|
|
|
rawTxDB := prefixdb.New(txID[:], s.rewardUTXODB)
|
|
txDB := linkeddb.NewDefault(rawTxDB)
|
|
it := txDB.NewIterator()
|
|
defer it.Release()
|
|
|
|
utxos := []*lux.UTXO(nil)
|
|
for it.Next() {
|
|
utxo, err := lux.ParseUTXO(it.Value())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
utxos = append(utxos, utxo)
|
|
}
|
|
if err := it.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.rewardUTXOsCache.Put(txID, utxos)
|
|
return utxos, nil
|
|
}
|
|
|
|
func (s *state) AddRewardUTXO(txID ids.ID, utxo *lux.UTXO) {
|
|
s.addedRewardUTXOs[txID] = append(s.addedRewardUTXOs[txID], utxo)
|
|
}
|
|
|
|
func (s *state) GetUTXO(utxoID ids.ID) (*lux.UTXO, error) {
|
|
if utxo, exists := s.modifiedUTXOs[utxoID]; exists {
|
|
if utxo == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return utxo, nil
|
|
}
|
|
return s.utxoState.GetUTXO(utxoID)
|
|
}
|
|
|
|
func (s *state) UTXOIDs(addr []byte, start ids.ID, limit int) ([]ids.ID, error) {
|
|
return s.utxoState.UTXOIDs(addr, start, limit)
|
|
}
|
|
|
|
func (s *state) AddUTXO(utxo *lux.UTXO) {
|
|
s.modifiedUTXOs[utxo.InputID()] = utxo
|
|
}
|
|
|
|
func (s *state) DeleteUTXO(utxoID ids.ID) {
|
|
s.modifiedUTXOs[utxoID] = nil
|
|
}
|
|
|
|
func (s *state) GetStartTime(nodeID ids.NodeID, netID ids.ID) (time.Time, error) {
|
|
staker, err := s.currentStakers.GetValidator(netID, nodeID)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
return staker.StartTime, nil
|
|
}
|
|
|
|
// GetTimestamp allows for concurrent reads.
|
|
func (s *state) GetTimestamp() time.Time {
|
|
return s.timestamp
|
|
}
|
|
|
|
func (s *state) SetTimestamp(tm time.Time) {
|
|
s.timestamp = tm
|
|
}
|
|
|
|
func (s *state) GetFeeState() gas.State {
|
|
return s.feeState
|
|
}
|
|
|
|
func (s *state) SetFeeState(feeState gas.State) {
|
|
s.feeState = feeState
|
|
}
|
|
|
|
func (s *state) GetL1ValidatorExcess() gas.Gas {
|
|
return s.l1ValidatorExcess
|
|
}
|
|
|
|
func (s *state) SetL1ValidatorExcess(e gas.Gas) {
|
|
s.l1ValidatorExcess = e
|
|
}
|
|
|
|
func (s *state) GetAccruedFees() uint64 {
|
|
return s.accruedFees
|
|
}
|
|
|
|
func (s *state) SetAccruedFees(accruedFees uint64) {
|
|
s.accruedFees = accruedFees
|
|
}
|
|
|
|
func (s *state) GetLastAccepted() ids.ID {
|
|
s.lastAcceptedLock.RLock()
|
|
defer s.lastAcceptedLock.RUnlock()
|
|
return s.lastAccepted
|
|
}
|
|
|
|
func (s *state) SetLastAccepted(lastAccepted ids.ID) {
|
|
s.lastAcceptedLock.Lock()
|
|
defer s.lastAcceptedLock.Unlock()
|
|
s.lastAccepted = lastAccepted
|
|
}
|
|
|
|
func (s *state) GetCurrentSupply(netID ids.ID) (uint64, error) {
|
|
if netID == constants.PrimaryNetworkID {
|
|
return s.currentSupply, nil
|
|
}
|
|
|
|
supply, ok := s.modifiedSupplies[netID]
|
|
if ok {
|
|
return supply, nil
|
|
}
|
|
|
|
cachedSupply, ok := s.supplyCache.Get(netID)
|
|
if ok {
|
|
if cachedSupply == nil {
|
|
return 0, database.ErrNotFound
|
|
}
|
|
return *cachedSupply, nil
|
|
}
|
|
|
|
supply, err := database.GetUInt64(s.supplyDB, netID[:])
|
|
if err == database.ErrNotFound {
|
|
s.supplyCache.Put(netID, nil)
|
|
return 0, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
s.supplyCache.Put(netID, &supply)
|
|
return supply, nil
|
|
}
|
|
|
|
func (s *state) SetCurrentSupply(netID ids.ID, cs uint64) {
|
|
if netID == constants.PrimaryNetworkID {
|
|
s.currentSupply = cs
|
|
} else {
|
|
s.modifiedSupplies[netID] = cs
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
func (s *state) syncGenesis(genesisBlk block.Block, genesis *genesis.Genesis) error {
|
|
genesisBlkID := genesisBlk.ID()
|
|
s.SetLastAccepted(genesisBlkID)
|
|
s.SetTimestamp(time.Unix(int64(genesis.Timestamp), 0))
|
|
s.SetCurrentSupply(constants.PrimaryNetworkID, genesis.InitialSupply)
|
|
s.AddStatelessBlock(genesisBlk)
|
|
|
|
// Initialize fee state with default values for genesis
|
|
// This is required because loadMetadata expects fee state to exist
|
|
// We must directly write it since both feeState and persistedFeeState
|
|
// start as zero values and won't trigger the write in writeMetadata
|
|
initialFeeState := gas.State{}
|
|
if err := putFeeState(s.singletonDB, initialFeeState); err != nil {
|
|
return fmt.Errorf("failed to write initial fee state: %w", err)
|
|
}
|
|
s.feeState = initialFeeState
|
|
s.persistedFeeState = initialFeeState
|
|
|
|
// Persist UTXOs that exist at genesis
|
|
for _, utxo := range genesis.UTXOs {
|
|
luxUTXO := utxo.UTXO
|
|
s.AddUTXO(&luxUTXO)
|
|
}
|
|
|
|
// Persist primary network validator set at genesis
|
|
for _, vdrTx := range genesis.Validators {
|
|
// We expect genesis validator txs to be either AddValidatorTx or
|
|
// AddPermissionlessValidatorTx.
|
|
//
|
|
validatorTx, ok := vdrTx.Unsigned.(txs.ScheduledStaker)
|
|
if !ok {
|
|
return fmt.Errorf("expected a scheduled staker but got %T", vdrTx.Unsigned)
|
|
}
|
|
|
|
stakeAmount := validatorTx.Weight()
|
|
// Note: We use [StartTime()] here because genesis transactions are
|
|
// guaranteed to be pre-Durango activation.
|
|
startTime := validatorTx.StartTime()
|
|
stakeDuration := validatorTx.EndTime().Sub(startTime)
|
|
currentSupply, err := s.GetCurrentSupply(constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
potentialReward := s.rewards.Calculate(
|
|
stakeDuration,
|
|
stakeAmount,
|
|
currentSupply,
|
|
)
|
|
newCurrentSupply, err := safemath.Add(currentSupply, potentialReward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
staker, err := NewCurrentStaker(vdrTx.ID(), validatorTx, startTime, potentialReward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.PutCurrentValidator(staker); err != nil {
|
|
return err
|
|
}
|
|
s.AddTx(vdrTx, status.Committed)
|
|
s.SetCurrentSupply(constants.PrimaryNetworkID, newCurrentSupply)
|
|
}
|
|
|
|
for _, chain := range genesis.Chains {
|
|
unsignedChain, ok := chain.Unsigned.(*txs.CreateChainTx)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type *txs.CreateChainTx but got %T", chain.Unsigned)
|
|
}
|
|
|
|
// Ensure all chains that the genesis bytes say to create have the right
|
|
// network ID
|
|
networkID := s.runtime.NetworkID
|
|
if false && unsignedChain.NetworkID != networkID { // Temporarily disabled for genesis compatibility
|
|
return lux.ErrWrongNetworkID
|
|
}
|
|
|
|
s.AddChain(chain)
|
|
s.AddTx(chain, status.Committed)
|
|
}
|
|
|
|
// updateValidators is set to false here to maintain the invariant that the
|
|
// primary network's validator set is empty before the validator sets are
|
|
// initialized.
|
|
if err := s.write(false /*=updateValidators*/, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark blocks as already reindexed since this is a fresh database with no
|
|
// legacy block indices to convert. This prevents the ReindexBlocks goroutine
|
|
// from running and racing with other state operations on fresh databases.
|
|
return s.singletonDB.Put(BlocksReindexedKey, nil)
|
|
}
|
|
|
|
// Load pulls data previously stored on disk that is expected to be in memory.
|
|
func (s *state) load() error {
|
|
return errors.Join(
|
|
s.loadMetadata(),
|
|
s.loadExpiry(),
|
|
s.loadActiveL1Validators(),
|
|
s.loadCurrentValidators(),
|
|
s.loadPendingValidators(),
|
|
s.initValidatorSets(),
|
|
)
|
|
}
|
|
|
|
func (s *state) loadMetadata() error {
|
|
timestamp, err := database.GetTimestamp(s.singletonDB, TimestampKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.persistedTimestamp = timestamp
|
|
s.SetTimestamp(timestamp)
|
|
|
|
feeState, err := getFeeState(s.singletonDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.persistedFeeState = feeState
|
|
s.SetFeeState(feeState)
|
|
|
|
l1ValidatorExcess, err := database.GetUInt64(s.singletonDB, L1ValidatorExcessKey)
|
|
if err != nil {
|
|
if err != database.ErrNotFound {
|
|
return err
|
|
}
|
|
l1ValidatorExcess = 0
|
|
}
|
|
s.persistedL1ValidatorExcess = gas.Gas(l1ValidatorExcess)
|
|
s.SetL1ValidatorExcess(gas.Gas(l1ValidatorExcess))
|
|
|
|
accruedFees, err := database.GetUInt64(s.singletonDB, AccruedFeesKey)
|
|
if err != nil {
|
|
if err != database.ErrNotFound {
|
|
return err
|
|
}
|
|
accruedFees = 0
|
|
}
|
|
s.persistedAccruedFees = accruedFees
|
|
s.SetAccruedFees(accruedFees)
|
|
|
|
currentSupply, err := database.GetUInt64(s.singletonDB, CurrentSupplyKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.persistedCurrentSupply = currentSupply
|
|
s.SetCurrentSupply(constants.PrimaryNetworkID, currentSupply)
|
|
|
|
lastAccepted, err := database.GetID(s.singletonDB, LastAcceptedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.persistedLastAccepted = lastAccepted
|
|
s.lastAcceptedLock.Lock()
|
|
s.lastAccepted = lastAccepted
|
|
s.lastAcceptedLock.Unlock()
|
|
|
|
// Lookup the most recently indexed range on disk. If we haven't started
|
|
// indexing the weights, then we keep the indexed heights as nil.
|
|
indexedHeightsBytes, err := s.singletonDB.Get(HeightsIndexedKey)
|
|
if err == database.ErrNotFound {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
indexedHeights := &heightRange{}
|
|
if err := parseHeightRange(indexedHeightsBytes, indexedHeights); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the indexed range is not up to date, then we will act as if the range
|
|
// doesn't exist.
|
|
lastAcceptedBlock, err := s.GetStatelessBlock(lastAccepted)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if indexedHeights.UpperBound != lastAcceptedBlock.Height() {
|
|
return nil
|
|
}
|
|
s.indexedHeights = indexedHeights
|
|
return nil
|
|
}
|
|
|
|
func (s *state) loadExpiry() error {
|
|
it := s.expiryDB.NewIterator()
|
|
defer it.Release()
|
|
|
|
for it.Next() {
|
|
key := it.Key()
|
|
|
|
var entry ExpiryEntry
|
|
if err := entry.Unmarshal(key); err != nil {
|
|
return fmt.Errorf("failed to unmarshal ExpiryEntry during load: %w", err)
|
|
}
|
|
s.expiry.ReplaceOrInsert(entry)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) loadActiveL1Validators() error {
|
|
it := s.activeDB.NewIterator()
|
|
defer it.Release()
|
|
for it.Next() {
|
|
key := it.Key()
|
|
validationID, err := ids.ToID(key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal ValidationID during load: %w", err)
|
|
}
|
|
|
|
var (
|
|
value = it.Value()
|
|
l1Validator = L1Validator{
|
|
ValidationID: validationID,
|
|
}
|
|
)
|
|
if err := parseL1Validator(value, &l1Validator); err != nil {
|
|
return fmt.Errorf("failed to unmarshal L1 validator: %w", err)
|
|
}
|
|
|
|
s.activeL1Validators.put(l1Validator)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) loadCurrentValidators() error {
|
|
s.currentStakers = newBaseStakers()
|
|
|
|
fmt.Println("[VALIDATOR DEBUG] loadCurrentValidators: STARTING")
|
|
log.Warn("loadCurrentValidators: starting", "dbPrefix", "currentValidatorList")
|
|
validatorCount := 0
|
|
validatorIt := s.currentValidatorList.NewIterator()
|
|
defer validatorIt.Release()
|
|
for validatorIt.Next() {
|
|
validatorCount++
|
|
txIDBytes := validatorIt.Key()
|
|
txID, err := ids.ToID(txIDBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, _, err := s.GetTx(txID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed loading validator transaction txID %s, %w", txID, err)
|
|
}
|
|
|
|
stakerTx, ok := tx.Unsigned.(txs.Staker)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type txs.Staker but got %T", tx.Unsigned)
|
|
}
|
|
|
|
metadataBytes := validatorIt.Value()
|
|
metadata := &validatorMetadata{
|
|
txID: txID,
|
|
}
|
|
if scheduledStakerTx, ok := tx.Unsigned.(txs.ScheduledStaker); ok {
|
|
// Populate [StakerStartTime] using the tx as a default in the event
|
|
// it was added pre-durango and is not stored in the database.
|
|
//
|
|
// Note: We do not populate [LastUpdated] since it is expected to
|
|
// always be present on disk.
|
|
metadata.StakerStartTime = uint64(scheduledStakerTx.StartTime().Unix())
|
|
}
|
|
if err := parseValidatorMetadata(metadataBytes, metadata); err != nil {
|
|
return err
|
|
}
|
|
|
|
staker, err := NewCurrentStaker(
|
|
txID,
|
|
stakerTx,
|
|
time.Unix(int64(metadata.StakerStartTime), 0),
|
|
metadata.PotentialReward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.currentStakers.LoadValidator(staker)
|
|
log.Warn("loadCurrentValidators: loaded validator",
|
|
"txID", staker.TxID,
|
|
"nodeID", staker.NodeID,
|
|
"chainID", staker.ChainID,
|
|
"weight", staker.Weight,
|
|
)
|
|
|
|
s.validatorState.LoadValidatorMetadata(staker.NodeID, staker.ChainID, metadata)
|
|
}
|
|
fmt.Printf("[VALIDATOR DEBUG] loadCurrentValidators: PRIMARY validators loaded: count=%d currentStakersLen=%d\n",
|
|
validatorCount, len(s.currentStakers.validators))
|
|
log.Warn("loadCurrentValidators: primary validators loaded",
|
|
"count", validatorCount,
|
|
"currentStakersValidatorsLen", len(s.currentStakers.validators),
|
|
)
|
|
|
|
chainValidatorIt := s.currentNetValidatorList.NewIterator()
|
|
defer chainValidatorIt.Release()
|
|
for chainValidatorIt.Next() {
|
|
txIDBytes := chainValidatorIt.Key()
|
|
txID, err := ids.ToID(txIDBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, _, err := s.GetTx(txID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakerTx, ok := tx.Unsigned.(txs.Staker)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type txs.Staker but got %T", tx.Unsigned)
|
|
}
|
|
|
|
metadataBytes := chainValidatorIt.Value()
|
|
metadata := &validatorMetadata{
|
|
txID: txID,
|
|
}
|
|
if scheduledStakerTx, ok := tx.Unsigned.(txs.ScheduledStaker); ok {
|
|
// Populate [StakerStartTime] and [LastUpdated] using the tx as a
|
|
// default in the event they are not stored in the database.
|
|
startTime := uint64(scheduledStakerTx.StartTime().Unix())
|
|
metadata.StakerStartTime = startTime
|
|
metadata.LastUpdated = startTime
|
|
}
|
|
if err := parseValidatorMetadata(metadataBytes, metadata); err != nil {
|
|
return err
|
|
}
|
|
|
|
staker, err := NewCurrentStaker(
|
|
txID,
|
|
stakerTx,
|
|
time.Unix(int64(metadata.StakerStartTime), 0),
|
|
metadata.PotentialReward,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.currentStakers.LoadValidator(staker)
|
|
|
|
s.validatorState.LoadValidatorMetadata(staker.NodeID, staker.ChainID, metadata)
|
|
}
|
|
|
|
delegatorIt := s.currentDelegatorList.NewIterator()
|
|
defer delegatorIt.Release()
|
|
|
|
chainDelegatorIt := s.currentNetDelegatorList.NewIterator()
|
|
defer chainDelegatorIt.Release()
|
|
|
|
for _, delegatorIt := range []database.Iterator{delegatorIt, chainDelegatorIt} {
|
|
for delegatorIt.Next() {
|
|
txIDBytes := delegatorIt.Key()
|
|
txID, err := ids.ToID(txIDBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, _, err := s.GetTx(txID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakerTx, ok := tx.Unsigned.(txs.Staker)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type txs.Staker but got %T", tx.Unsigned)
|
|
}
|
|
|
|
metadataBytes := delegatorIt.Value()
|
|
metadata := &delegatorMetadata{
|
|
txID: txID,
|
|
}
|
|
if scheduledStakerTx, ok := tx.Unsigned.(txs.ScheduledStaker); ok {
|
|
// Populate [StakerStartTime] using the tx as a default in the
|
|
// event it was added pre-durango and is not stored in the
|
|
// database.
|
|
metadata.StakerStartTime = uint64(scheduledStakerTx.StartTime().Unix())
|
|
}
|
|
err = parseDelegatorMetadata(metadataBytes, metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
staker, err := NewCurrentStaker(
|
|
txID,
|
|
stakerTx,
|
|
time.Unix(int64(metadata.StakerStartTime), 0),
|
|
metadata.PotentialReward,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.currentStakers.LoadDelegator(staker)
|
|
}
|
|
}
|
|
|
|
return errors.Join(
|
|
validatorIt.Error(),
|
|
chainValidatorIt.Error(),
|
|
delegatorIt.Error(),
|
|
chainDelegatorIt.Error(),
|
|
)
|
|
}
|
|
|
|
func (s *state) loadPendingValidators() error {
|
|
s.pendingStakers = newBaseStakers()
|
|
|
|
validatorIt := s.pendingValidatorList.NewIterator()
|
|
defer validatorIt.Release()
|
|
|
|
chainValidatorIt := s.pendingNetValidatorList.NewIterator()
|
|
defer chainValidatorIt.Release()
|
|
|
|
for _, validatorIt := range []database.Iterator{validatorIt, chainValidatorIt} {
|
|
for validatorIt.Next() {
|
|
txIDBytes := validatorIt.Key()
|
|
txID, err := ids.ToID(txIDBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, _, err := s.GetTx(txID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakerTx, ok := tx.Unsigned.(txs.ScheduledStaker)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type txs.Staker but got %T", tx.Unsigned)
|
|
}
|
|
|
|
staker, err := NewPendingStaker(txID, stakerTx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.pendingStakers.LoadValidator(staker)
|
|
}
|
|
}
|
|
|
|
delegatorIt := s.pendingDelegatorList.NewIterator()
|
|
defer delegatorIt.Release()
|
|
|
|
chainDelegatorIt := s.pendingNetDelegatorList.NewIterator()
|
|
defer chainDelegatorIt.Release()
|
|
|
|
for _, delegatorIt := range []database.Iterator{delegatorIt, chainDelegatorIt} {
|
|
for delegatorIt.Next() {
|
|
txIDBytes := delegatorIt.Key()
|
|
txID, err := ids.ToID(txIDBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, _, err := s.GetTx(txID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakerTx, ok := tx.Unsigned.(txs.ScheduledStaker)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type txs.Staker but got %T", tx.Unsigned)
|
|
}
|
|
|
|
staker, err := NewPendingStaker(txID, stakerTx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.pendingStakers.LoadDelegator(staker)
|
|
}
|
|
}
|
|
|
|
return errors.Join(
|
|
validatorIt.Error(),
|
|
chainValidatorIt.Error(),
|
|
delegatorIt.Error(),
|
|
chainDelegatorIt.Error(),
|
|
)
|
|
}
|
|
|
|
// Invariant: initValidatorSets requires loadActiveL1Validators and
|
|
// loadCurrentValidators to have already been called.
|
|
func (s *state) initValidatorSets() error {
|
|
fmt.Printf("[VALIDATOR DEBUG] initValidatorSets CALLED - validatorManagerPtr=%p\n", s.validators) // Direct print to bypass log filtering
|
|
log.Warn("[VALIDATOR DEBUG] initValidatorSets: STARTING NOW",
|
|
"validatorManagerPtr", fmt.Sprintf("%p", s.validators),
|
|
)
|
|
log.Info("initValidatorSets: starting",
|
|
"numNets", s.validators.NumNets(),
|
|
"currentStakersChains", len(s.currentStakers.validators),
|
|
)
|
|
|
|
if s.validators.NumNets() != 0 {
|
|
// The validators manager may already be populated by the network layer
|
|
// with genesis validators (for the samplePeers fix). In this case, we
|
|
// skip re-adding them here and just verify the state is consistent.
|
|
log.Warn("initValidatorSets: validator set already populated by network layer, skipping population",
|
|
"numNets", s.validators.NumNets(),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// Load active LP-77 validators
|
|
if err := s.activeL1Validators.addStakersToValidatorManager(s.validators); err != nil {
|
|
return err
|
|
}
|
|
log.Info("initValidatorSets: after L1 validators", "numNets", s.validators.NumNets())
|
|
|
|
// Load inactive LP-77 validators
|
|
//
|
|
// Inactive validators must be loaded individually with their ValidationID
|
|
// as TxID, not aggregated with ids.Empty.
|
|
inactiveIt := s.inactiveDB.NewIterator()
|
|
defer inactiveIt.Release()
|
|
|
|
for inactiveIt.Next() {
|
|
validationIDBytes := inactiveIt.Key()
|
|
validationID, err := ids.ToID(validationIDBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse validation ID: %w", err)
|
|
}
|
|
|
|
var l1Validator L1Validator
|
|
if err := parseL1Validator(inactiveIt.Value(), &l1Validator); err != nil {
|
|
return fmt.Errorf("failed to unmarshal inactive L1 validator: %w", err)
|
|
}
|
|
l1Validator.ValidationID = validationID
|
|
|
|
// Add inactive validator to validator manager using addL1ValidatorToValidatorManager
|
|
// which properly handles effectiveNodeID, effectivePublicKey, and effectiveValidationID
|
|
if err := addL1ValidatorToValidatorManager(s.validators, l1Validator); err != nil {
|
|
return fmt.Errorf("failed to add inactive L1 validator: %w", err)
|
|
}
|
|
}
|
|
|
|
// Load primary network and non-LP77 validators
|
|
primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID]
|
|
log.Info("initValidatorSets: loading primary network validators",
|
|
"primaryNetworkID", constants.PrimaryNetworkID,
|
|
"primaryNetworkValidatorCount", len(primaryNetworkValidators),
|
|
"totalChains", len(s.currentStakers.validators),
|
|
)
|
|
for chainID, chainValidators := range s.currentStakers.validators {
|
|
log.Info("initValidatorSets: processing chain",
|
|
"chainID", chainID,
|
|
"validatorCount", len(chainValidators),
|
|
)
|
|
for nodeID, chainValidator := range chainValidators {
|
|
// The chain validator's Public Key is inherited from the
|
|
// corresponding primary network validator.
|
|
primaryValidator, ok := primaryNetworkValidators[nodeID]
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID)
|
|
}
|
|
|
|
var (
|
|
primaryStaker = primaryValidator.validator
|
|
chainStaker = chainValidator.validator
|
|
)
|
|
if err := s.validators.AddStaker(chainID, nodeID, bls.PublicKeyToUncompressedBytes(primaryStaker.PublicKey), chainStaker.TxID, chainStaker.Weight); err != nil {
|
|
return err
|
|
}
|
|
log.Info("initValidatorSets: added validator",
|
|
"chainID", chainID,
|
|
"nodeID", nodeID,
|
|
"weight", chainStaker.Weight,
|
|
)
|
|
|
|
delegatorIterator := iterator.FromTree(chainValidator.delegators)
|
|
for delegatorIterator.Next() {
|
|
delegatorStaker := delegatorIterator.Value()
|
|
if err := s.validators.AddWeight(chainID, nodeID, delegatorStaker.Weight); err != nil {
|
|
delegatorIterator.Release()
|
|
return err
|
|
}
|
|
}
|
|
delegatorIterator.Release()
|
|
}
|
|
}
|
|
|
|
s.metrics.SetLocalStake(s.validators.GetWeight(constants.PrimaryNetworkID, s.runtime.NodeID))
|
|
totalWeight, err := s.validators.TotalWeight(constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get total weight of primary network validators: %w", err)
|
|
}
|
|
s.metrics.SetTotalStake(totalWeight)
|
|
|
|
// Log final state
|
|
numValidators := s.validators.NumValidators(constants.PrimaryNetworkID)
|
|
fmt.Printf("[VALIDATOR DEBUG] initValidatorSets COMPLETE: validatorManagerPtr=%p primaryNetworkID=%s numValidators=%d totalWeight=%d numNets=%d\n",
|
|
s.validators, constants.PrimaryNetworkID, numValidators, totalWeight, s.validators.NumNets())
|
|
log.Warn("[VALIDATOR DEBUG] initValidatorSets: COMPLETE",
|
|
"validatorManagerPtr", fmt.Sprintf("%p", s.validators),
|
|
"primaryNetworkID", constants.PrimaryNetworkID,
|
|
"numValidators", numValidators,
|
|
"totalWeight", totalWeight,
|
|
"numNets", s.validators.NumNets(),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) write(updateValidators bool, height uint64) error {
|
|
return errors.Join(
|
|
s.writeBlocks(),
|
|
s.writeExpiry(),
|
|
s.updateValidatorManager(updateValidators),
|
|
s.writeValidatorDiffs(height),
|
|
s.writeCurrentStakers(),
|
|
s.writePendingStakers(),
|
|
s.WriteValidatorMetadata(s.currentValidatorList, s.currentNetValidatorList), // Must be called after writeCurrentStakers
|
|
s.writeL1Validators(),
|
|
s.writeTXs(),
|
|
s.writeRewardUTXOs(),
|
|
s.writeUTXOs(),
|
|
s.writeNets(),
|
|
s.writeNetOwners(),
|
|
s.writeNetToL1Conversions(),
|
|
s.writeTransformedNets(),
|
|
s.writeNetSupplies(),
|
|
s.writeChains(),
|
|
s.writeMetadata(),
|
|
)
|
|
}
|
|
|
|
func (s *state) Close() error {
|
|
// Only close the base database. All other databases are prefixdb wrappers
|
|
// that don't need to be closed separately. Closing them would cause
|
|
// "closed" errors because prefixdb.Close() calls Close() on the underlying
|
|
// database, which would be closed multiple times.
|
|
// The baseDB field was not stored in the state struct, but we can close
|
|
// the underlying database through any of the prefixdb instances.
|
|
// We'll close singletonDB which is a direct prefixdb over baseDB.
|
|
return s.singletonDB.Close()
|
|
}
|
|
|
|
func (s *state) sync(genesis []byte) error {
|
|
wasInitialized, err := isInitialized(s.singletonDB)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to check if the database is initialized: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
// If the database wasn't previously initialized, create the platform chain
|
|
// anew using the provided genesis state.
|
|
if !wasInitialized {
|
|
if err := s.init(genesis); err != nil {
|
|
return fmt.Errorf(
|
|
"failed to initialize the database: %w",
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
if err := s.load(); err != nil {
|
|
return fmt.Errorf(
|
|
"failed to load the database state: %w",
|
|
err,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) init(genesisBytes []byte) error {
|
|
// Create the genesis block and save it as being accepted (We don't do
|
|
// genesisBlock.Accept() because then it'd look for genesisBlock's
|
|
// non-existent parent)
|
|
genesisID := hash.ComputeHash256Array(genesisBytes)
|
|
genesisBlock, err := block.NewCommitBlock(time.Unix(0, 0), genesisID, 0 /*height*/)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parsedGenesis, err := genesis.Parse(genesisBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.syncGenesis(genesisBlock, parsedGenesis); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Commit the genesis state to the underlying database
|
|
// This is necessary because baseDB is a versiondb which keeps changes
|
|
// in memory until CommitBatch() is called
|
|
if _, err := s.baseDB.CommitBatch(); err != nil {
|
|
return fmt.Errorf("failed to commit genesis state: %w", err)
|
|
}
|
|
|
|
if err := markInitialized(s.singletonDB); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.Commit()
|
|
}
|
|
|
|
func (s *state) AddStatelessBlock(block block.Block) {
|
|
blkID := block.ID()
|
|
s.addedBlockIDs[block.Height()] = blkID
|
|
s.addedBlocks[blkID] = block
|
|
}
|
|
|
|
func (s *state) SetHeight(height uint64) {
|
|
if s.indexedHeights == nil {
|
|
// If indexedHeights hasn't been created yet, then we are newly tracking
|
|
// the range. This means we should initialize the LowerBound to the
|
|
// current height.
|
|
s.indexedHeights = &heightRange{
|
|
LowerBound: height,
|
|
}
|
|
}
|
|
|
|
s.indexedHeights.UpperBound = height
|
|
s.currentHeight = height
|
|
}
|
|
|
|
func (s *state) Commit() error {
|
|
defer s.Abort()
|
|
batch, err := s.CommitBatch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return batch.Write()
|
|
}
|
|
|
|
func (s *state) Abort() {
|
|
s.baseDB.Abort()
|
|
}
|
|
|
|
func (s *state) Checksum() ids.ID {
|
|
return s.utxoState.Checksum()
|
|
}
|
|
|
|
func (s *state) CommitBatch() (database.Batch, error) {
|
|
// updateValidators is set to true here so that the validator manager is
|
|
// kept up to date with the last accepted state.
|
|
if err := s.write(true /*=updateValidators*/, s.currentHeight); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.baseDB.CommitBatch()
|
|
}
|
|
|
|
func (s *state) writeBlocks() error {
|
|
for blkID, blk := range s.addedBlocks {
|
|
blkBytes := blk.Bytes()
|
|
blkHeight := blk.Height()
|
|
heightKey := database.PackUInt64(blkHeight)
|
|
|
|
delete(s.addedBlockIDs, blkHeight)
|
|
s.blockIDCache.Put(blkHeight, blkID)
|
|
if err := database.PutID(s.blockIDDB, heightKey, blkID); err != nil {
|
|
return fmt.Errorf("failed to add blockID: %w", err)
|
|
}
|
|
|
|
delete(s.addedBlocks, blkID)
|
|
// Note: Evict is used rather than Put here because blk may end up
|
|
// referencing additional data (because of shared byte slices) that
|
|
// would not be properly accounted for in the cache sizing.
|
|
s.blockCache.Evict(blkID)
|
|
if err := s.blockDB.Put(blkID[:], blkBytes); err != nil {
|
|
return fmt.Errorf("failed to write block %s: %w", blkID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) GetStatelessBlock(blockID ids.ID) (block.Block, error) {
|
|
if blk, exists := s.addedBlocks[blockID]; exists {
|
|
return blk, nil
|
|
}
|
|
if blk, cached := s.blockCache.Get(blockID); cached {
|
|
if blk == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
|
|
return blk, nil
|
|
}
|
|
|
|
blkBytes, err := s.blockDB.Get(blockID[:])
|
|
if err == database.ErrNotFound {
|
|
s.blockCache.Put(blockID, nil)
|
|
return nil, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blk, err := parseStoredBlock(blkBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.blockCache.Put(blockID, blk)
|
|
return blk, nil
|
|
}
|
|
|
|
func (s *state) GetBlockIDAtHeight(height uint64) (ids.ID, error) {
|
|
if blkID, exists := s.addedBlockIDs[height]; exists {
|
|
return blkID, nil
|
|
}
|
|
if blkID, cached := s.blockIDCache.Get(height); cached {
|
|
if blkID == ids.Empty {
|
|
return ids.Empty, database.ErrNotFound
|
|
}
|
|
|
|
return blkID, nil
|
|
}
|
|
|
|
heightKey := database.PackUInt64(height)
|
|
|
|
blkID, err := database.GetID(s.blockIDDB, heightKey)
|
|
if err == database.ErrNotFound {
|
|
s.blockIDCache.Put(height, ids.Empty)
|
|
return ids.Empty, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
s.blockIDCache.Put(height, blkID)
|
|
return blkID, nil
|
|
}
|
|
|
|
func (s *state) writeExpiry() error {
|
|
for entry, isAdded := range s.expiryDiff.modified {
|
|
var (
|
|
key = entry.Marshal()
|
|
err error
|
|
)
|
|
if isAdded {
|
|
s.expiry.ReplaceOrInsert(entry)
|
|
err = s.expiryDB.Put(key, nil)
|
|
} else {
|
|
s.expiry.Delete(entry)
|
|
err = s.expiryDB.Delete(key)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
s.expiryDiff = newExpiryDiff()
|
|
return nil
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// This function must be called prior to writeCurrentStakers and
|
|
// writeL1Validators.
|
|
//
|
|
// TODO: L1s with no active weight should not be held in memory.
|
|
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.runtime.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
|
|
}
|
|
|
|
func (s *state) writeCurrentStakers() error {
|
|
for chainID, validatorDiffs := range s.currentStakers.validatorDiffs {
|
|
// Select db to write to
|
|
validatorDB := s.currentNetValidatorList
|
|
delegatorDB := s.currentNetDelegatorList
|
|
if chainID == constants.PrimaryNetworkID {
|
|
validatorDB = s.currentValidatorList
|
|
delegatorDB = s.currentDelegatorList
|
|
}
|
|
|
|
// Record the change in weight and/or public key for each validator.
|
|
for nodeID, validatorDiff := range validatorDiffs {
|
|
switch validatorDiff.validatorStatus {
|
|
case added:
|
|
staker := validatorDiff.validator
|
|
|
|
// The validator is being added.
|
|
//
|
|
// Invariant: It's impossible for a delegator to have been rewarded
|
|
// in the same block that the validator was added.
|
|
startTime := uint64(staker.StartTime.Unix())
|
|
metadata := &validatorMetadata{
|
|
txID: staker.TxID,
|
|
lastUpdated: staker.StartTime,
|
|
|
|
UpDuration: 0,
|
|
LastUpdated: startTime,
|
|
StakerStartTime: startTime,
|
|
PotentialReward: staker.PotentialReward,
|
|
PotentialDelegateeReward: 0,
|
|
}
|
|
|
|
metadataBytes, err := marshalValidatorMetadata(metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize current validator: %w", err)
|
|
}
|
|
|
|
if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil {
|
|
return fmt.Errorf("failed to write current validator to list: %w", err)
|
|
}
|
|
|
|
s.validatorState.LoadValidatorMetadata(nodeID, chainID, metadata)
|
|
case deleted:
|
|
if err := validatorDB.Delete(validatorDiff.validator.TxID[:]); err != nil {
|
|
return fmt.Errorf("failed to delete current staker: %w", err)
|
|
}
|
|
|
|
s.validatorState.DeleteValidatorMetadata(nodeID, chainID)
|
|
}
|
|
|
|
err := writeCurrentDelegatorDiff(
|
|
delegatorDB,
|
|
validatorDiff,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
clear(s.currentStakers.validatorDiffs)
|
|
return nil
|
|
}
|
|
|
|
func writeCurrentDelegatorDiff(
|
|
currentDelegatorList linkeddb.LinkedDB,
|
|
validatorDiff *diffValidator,
|
|
) error {
|
|
addedDelegatorIterator := iterator.FromTree(validatorDiff.addedDelegators)
|
|
defer addedDelegatorIterator.Release()
|
|
|
|
for addedDelegatorIterator.Next() {
|
|
staker := addedDelegatorIterator.Value()
|
|
|
|
metadata := &delegatorMetadata{
|
|
txID: staker.TxID,
|
|
PotentialReward: staker.PotentialReward,
|
|
StakerStartTime: uint64(staker.StartTime.Unix()),
|
|
}
|
|
if err := writeDelegatorMetadata(currentDelegatorList, metadata); err != nil {
|
|
return fmt.Errorf("failed to write current delegator to list: %w", err)
|
|
}
|
|
}
|
|
|
|
for _, staker := range validatorDiff.deletedDelegators {
|
|
if err := currentDelegatorList.Delete(staker.TxID[:]); err != nil {
|
|
return fmt.Errorf("failed to delete current staker: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writePendingStakers() error {
|
|
for netID, chainValidatorDiffs := range s.pendingStakers.validatorDiffs {
|
|
delete(s.pendingStakers.validatorDiffs, netID)
|
|
|
|
validatorDB := s.pendingNetValidatorList
|
|
delegatorDB := s.pendingNetDelegatorList
|
|
if netID == constants.PrimaryNetworkID {
|
|
validatorDB = s.pendingValidatorList
|
|
delegatorDB = s.pendingDelegatorList
|
|
}
|
|
|
|
for _, validatorDiff := range chainValidatorDiffs {
|
|
err := writePendingDiff(
|
|
validatorDB,
|
|
delegatorDB,
|
|
validatorDiff,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePendingDiff(
|
|
pendingValidatorList linkeddb.LinkedDB,
|
|
pendingDelegatorList linkeddb.LinkedDB,
|
|
validatorDiff *diffValidator,
|
|
) error {
|
|
switch validatorDiff.validatorStatus {
|
|
case added:
|
|
err := pendingValidatorList.Put(validatorDiff.validator.TxID[:], nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add pending validator: %w", err)
|
|
}
|
|
case deleted:
|
|
err := pendingValidatorList.Delete(validatorDiff.validator.TxID[:])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete pending validator: %w", err)
|
|
}
|
|
}
|
|
|
|
addedDelegatorIterator := iterator.FromTree(validatorDiff.addedDelegators)
|
|
defer addedDelegatorIterator.Release()
|
|
for addedDelegatorIterator.Next() {
|
|
staker := addedDelegatorIterator.Value()
|
|
|
|
if err := pendingDelegatorList.Put(staker.TxID[:], nil); err != nil {
|
|
return fmt.Errorf("failed to write pending delegator to list: %w", err)
|
|
}
|
|
}
|
|
|
|
for _, staker := range validatorDiff.deletedDelegators {
|
|
if err := pendingDelegatorList.Delete(staker.TxID[:]); err != nil {
|
|
return fmt.Errorf("failed to delete pending delegator: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeL1Validators() error {
|
|
// Write modified weights
|
|
for chainID, weight := range s.l1ValidatorsDiff.modifiedTotalWeight {
|
|
var err error
|
|
if weight == 0 {
|
|
err = s.weightsDB.Delete(chainID[:])
|
|
} else {
|
|
err = database.PutUInt64(s.weightsDB, chainID[:], weight)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.weightsCache.Put(chainID, weight)
|
|
}
|
|
|
|
// The L1 validator diff application is split into two loops to ensure that all
|
|
// deletions to the chainIDNodeIDDB happen prior to any additions.
|
|
// Otherwise replacing an L1 validator by deleting it and then re-adding it with a
|
|
// different validationID could result in an inconsistent state.
|
|
for validationID, l1Validator := range s.l1ValidatorsDiff.modified {
|
|
// Delete the prior validator if it exists
|
|
var err error
|
|
if s.activeL1Validators.delete(validationID) {
|
|
err = deleteL1Validator(s.activeDB, emptyL1ValidatorCache, validationID)
|
|
} else {
|
|
err = deleteL1Validator(s.inactiveDB, s.inactiveCache, validationID)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !l1Validator.isDeleted() {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
chainIDNodeID = chainIDNodeID{
|
|
chainID: l1Validator.ChainID,
|
|
nodeID: l1Validator.NodeID,
|
|
}
|
|
chainIDNodeIDKey = chainIDNodeID.Marshal()
|
|
)
|
|
if err := s.chainIDNodeIDDB.Delete(chainIDNodeIDKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.chainIDNodeIDCache.Put(chainIDNodeID, false)
|
|
}
|
|
|
|
for validationID, l1Validator := range s.l1ValidatorsDiff.modified {
|
|
if l1Validator.isDeleted() {
|
|
continue
|
|
}
|
|
|
|
// Update the chainIDNodeID mapping
|
|
var (
|
|
chainIDNodeID = chainIDNodeID{
|
|
chainID: l1Validator.ChainID,
|
|
nodeID: l1Validator.NodeID,
|
|
}
|
|
chainIDNodeIDKey = chainIDNodeID.Marshal()
|
|
)
|
|
if err := s.chainIDNodeIDDB.Put(chainIDNodeIDKey, validationID[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.chainIDNodeIDCache.Put(chainIDNodeID, true)
|
|
|
|
// Add the new validator
|
|
var err error
|
|
if l1Validator.IsActive() {
|
|
s.activeL1Validators.put(l1Validator)
|
|
err = putL1Validator(s.activeDB, emptyL1ValidatorCache, l1Validator)
|
|
} else {
|
|
err = putL1Validator(s.inactiveDB, s.inactiveCache, l1Validator)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
s.l1ValidatorsDiffLock.Lock()
|
|
s.l1ValidatorsDiff = newL1ValidatorsDiff()
|
|
s.l1ValidatorsDiffLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeTXs() error {
|
|
for txID, txStatus := range s.addedTxs {
|
|
stx := txBytesAndStatus{
|
|
Tx: txStatus.tx.Bytes(),
|
|
Status: txStatus.status,
|
|
}
|
|
|
|
// We serialize the [txBytesAndStatus] envelope natively (see
|
|
// statewire.go). The embedded tx keeps its own self-delimiting
|
|
// signed bytes so its TxID is preserved on re-parse.
|
|
txBytes, err := marshalTxStatus(stx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize tx: %w", err)
|
|
}
|
|
|
|
delete(s.addedTxs, txID)
|
|
// Note: Evict is used rather than Put here because stx may end up
|
|
// referencing additional data (because of shared byte slices) that
|
|
// would not be properly accounted for in the cache sizing.
|
|
s.txCache.Evict(txID)
|
|
if err := s.txDB.Put(txID[:], txBytes); err != nil {
|
|
return fmt.Errorf("failed to add tx: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeRewardUTXOs() error {
|
|
for txID, utxos := range s.addedRewardUTXOs {
|
|
delete(s.addedRewardUTXOs, txID)
|
|
s.rewardUTXOsCache.Put(txID, utxos)
|
|
rawTxDB := prefixdb.New(txID[:], s.rewardUTXODB)
|
|
txDB := linkeddb.NewDefault(rawTxDB)
|
|
|
|
for _, utxo := range utxos {
|
|
utxoBytes, err := utxo.WireBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize reward UTXO: %w", err)
|
|
}
|
|
utxoID := utxo.InputID()
|
|
if err := txDB.Put(utxoID[:], utxoBytes); err != nil {
|
|
return fmt.Errorf("failed to add reward UTXO: %w", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeUTXOs() error {
|
|
for utxoID, utxo := range s.modifiedUTXOs {
|
|
delete(s.modifiedUTXOs, utxoID)
|
|
|
|
if utxo == nil {
|
|
if err := s.utxoState.DeleteUTXO(utxoID); err != nil {
|
|
return fmt.Errorf("failed to delete UTXO: %w", err)
|
|
}
|
|
continue
|
|
}
|
|
if err := s.utxoState.PutUTXO(utxo); err != nil {
|
|
return fmt.Errorf("failed to add UTXO: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeNets() error {
|
|
for _, chainID := range s.addedChainIDs {
|
|
if err := s.chainDB.Put(chainID[:], nil); err != nil {
|
|
return fmt.Errorf("failed to write chain: %w", err)
|
|
}
|
|
}
|
|
s.addedChainIDs = nil
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeNetOwners() error {
|
|
for chainID, owner := range s.chainOwners {
|
|
delete(s.chainOwners, chainID)
|
|
|
|
ownerBytes, err := marshalOwner(owner)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal net owner: %w", err)
|
|
}
|
|
|
|
s.chainOwnerCache.Put(chainID, fxOwnerAndSize{
|
|
owner: owner,
|
|
size: len(ownerBytes),
|
|
})
|
|
|
|
if err := s.chainOwnerDB.Put(chainID[:], ownerBytes); err != nil {
|
|
return fmt.Errorf("failed to write net owner: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeNetToL1Conversions() error {
|
|
for chainID, c := range s.chainToL1Conversions {
|
|
delete(s.chainToL1Conversions, chainID)
|
|
|
|
bytes, err := marshalConversion(c)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal chain conversion: %w", err)
|
|
}
|
|
|
|
s.chainToL1ConversionCache.Put(chainID, c)
|
|
|
|
if err := s.chainToL1ConversionDB.Put(chainID[:], bytes); err != nil {
|
|
return fmt.Errorf("failed to write chain conversion: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeTransformedNets() error {
|
|
for netID, tx := range s.transformedNets {
|
|
txID := tx.ID()
|
|
|
|
delete(s.transformedNets, netID)
|
|
// Note: Evict is used rather than Put here because tx may end up
|
|
// referencing additional data (because of shared byte slices) that
|
|
// would not be properly accounted for in the cache sizing.
|
|
s.transformedNetCache.Evict(netID)
|
|
if err := database.PutID(s.transformedNetDB, netID[:], txID); err != nil {
|
|
return fmt.Errorf("failed to write transformed chain: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeNetSupplies() error {
|
|
for chainID, supply := range s.modifiedSupplies {
|
|
delete(s.modifiedSupplies, chainID)
|
|
s.supplyCache.Put(chainID, &supply)
|
|
if err := database.PutUInt64(s.supplyDB, chainID[:], supply); err != nil {
|
|
return fmt.Errorf("failed to write chain supply: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeChains() error {
|
|
for netID, chains := range s.addedChains {
|
|
for _, chain := range chains {
|
|
chainDB := s.getChainDB(netID)
|
|
|
|
chainID := chain.ID()
|
|
if err := chainDB.Put(chainID[:], nil); err != nil {
|
|
return fmt.Errorf("failed to write chain: %w", err)
|
|
}
|
|
}
|
|
delete(s.addedChains, netID)
|
|
}
|
|
|
|
// Persist chain names for network-wide uniqueness
|
|
for nameLower, chainID := range s.addedChainNames {
|
|
if err := s.chainNameDB.Put([]byte(nameLower), chainID[:]); err != nil {
|
|
return fmt.Errorf("failed to write chain name %s: %w", nameLower, err)
|
|
}
|
|
delete(s.addedChainNames, nameLower)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeMetadata() error {
|
|
if !s.persistedTimestamp.Equal(s.timestamp) {
|
|
if err := database.PutTimestamp(s.singletonDB, TimestampKey, s.timestamp); err != nil {
|
|
return fmt.Errorf("failed to write timestamp: %w", err)
|
|
}
|
|
s.persistedTimestamp = s.timestamp
|
|
}
|
|
if s.feeState != s.persistedFeeState {
|
|
if err := putFeeState(s.singletonDB, s.feeState); err != nil {
|
|
return fmt.Errorf("failed to write fee state: %w", err)
|
|
}
|
|
s.persistedFeeState = s.feeState
|
|
}
|
|
if s.l1ValidatorExcess != s.persistedL1ValidatorExcess {
|
|
if err := database.PutUInt64(s.singletonDB, L1ValidatorExcessKey, uint64(s.l1ValidatorExcess)); err != nil {
|
|
return fmt.Errorf("failed to write l1Validator excess: %w", err)
|
|
}
|
|
s.persistedL1ValidatorExcess = s.l1ValidatorExcess
|
|
}
|
|
if s.accruedFees != s.persistedAccruedFees {
|
|
if err := database.PutUInt64(s.singletonDB, AccruedFeesKey, s.accruedFees); err != nil {
|
|
return fmt.Errorf("failed to write accrued fees: %w", err)
|
|
}
|
|
s.persistedAccruedFees = s.accruedFees
|
|
}
|
|
if s.persistedCurrentSupply != s.currentSupply {
|
|
if err := database.PutUInt64(s.singletonDB, CurrentSupplyKey, s.currentSupply); err != nil {
|
|
return fmt.Errorf("failed to write current supply: %w", err)
|
|
}
|
|
s.persistedCurrentSupply = s.currentSupply
|
|
}
|
|
s.lastAcceptedLock.RLock()
|
|
lastAcceptedChanged := s.persistedLastAccepted != s.lastAccepted
|
|
currentLastAccepted := s.lastAccepted
|
|
s.lastAcceptedLock.RUnlock()
|
|
if lastAcceptedChanged {
|
|
if err := database.PutID(s.singletonDB, LastAcceptedKey, currentLastAccepted); err != nil {
|
|
return fmt.Errorf("failed to write last accepted: %w", err)
|
|
}
|
|
s.persistedLastAccepted = currentLastAccepted
|
|
}
|
|
if s.indexedHeights != nil {
|
|
indexedHeightsBytes, err := marshalHeightRange(s.indexedHeights)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.singletonDB.Put(HeightsIndexedKey, indexedHeightsBytes); err != nil {
|
|
return fmt.Errorf("failed to write indexed range: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseStoredBlock returns the block stored under a blockDB value. Blocks are
|
|
// stored as their native-ZAP wire bytes (struct-is-wire); block.Parse wraps
|
|
// them zero-copy.
|
|
func parseStoredBlock(blkBytes []byte) (block.Block, error) {
|
|
return block.Parse(blkBytes)
|
|
}
|
|
|
|
func (s *state) ReindexBlocks(lock sync.Locker, log log.Logger) error {
|
|
has, err := s.singletonDB.Has(BlocksReindexedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
log.Info("blocks already reindexed")
|
|
return nil
|
|
}
|
|
|
|
// It is possible that new blocks are added after grabbing this iterator.
|
|
// New blocks are guaranteed to be persisted in the new format, so we don't
|
|
// need to check them.
|
|
blockIterator := s.blockDB.NewIterator()
|
|
// Releasing is done using a closure to ensure that updating blockIterator
|
|
// will result in having the most recent iterator released when executing
|
|
// the deferred function.
|
|
defer func() {
|
|
blockIterator.Release()
|
|
}()
|
|
|
|
log.Info("starting block reindexing")
|
|
|
|
var (
|
|
startTime = time.Now()
|
|
lastCommit = startTime
|
|
nextUpdate = startTime.Add(indexLogFrequency)
|
|
numIndicesChecked = 0
|
|
numIndicesUpdated = 0
|
|
)
|
|
|
|
for blockIterator.Next() {
|
|
keyBytes := blockIterator.Key()
|
|
valueBytes := blockIterator.Value()
|
|
|
|
// Skip entries that are not 32 bytes (not block IDs)
|
|
if len(keyBytes) != ids.IDLen {
|
|
continue
|
|
}
|
|
|
|
blk, err := parseStoredBlock(valueBytes)
|
|
if err != nil {
|
|
// Skip entries that can't be parsed as blocks
|
|
// This could be metadata or other non-block data
|
|
continue
|
|
}
|
|
|
|
blkID := blk.ID()
|
|
|
|
numIndicesChecked++
|
|
|
|
now := time.Now()
|
|
if now.After(nextUpdate) {
|
|
nextUpdate = now.Add(indexLogFrequency)
|
|
|
|
progress := timer.ProgressFromHash(blkID[:])
|
|
eta := timer.EstimateETA(
|
|
startTime,
|
|
progress,
|
|
math.MaxUint64,
|
|
)
|
|
|
|
log.Info("reindexing blocks",
|
|
"numIndicesUpdated", numIndicesUpdated,
|
|
"numIndicesChecked", numIndicesChecked,
|
|
"eta", eta,
|
|
)
|
|
}
|
|
|
|
if numIndicesChecked%indexIterationLimit == 0 {
|
|
// We must hold the lock during committing to make sure we don't
|
|
// attempt to commit to disk while a block is concurrently being
|
|
// accepted.
|
|
lock.Lock()
|
|
err := errors.Join(
|
|
s.Commit(),
|
|
blockIterator.Error(),
|
|
)
|
|
lock.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We release the iterator here to allow the underlying database to
|
|
// clean up deleted state.
|
|
blockIterator.Release()
|
|
|
|
// We take the minimum here because it's possible that the node is
|
|
// currently bootstrapping. This would mean that grabbing the lock
|
|
// could take an extremely long period of time; which we should not
|
|
// delay processing for.
|
|
indexDuration := now.Sub(lastCommit)
|
|
sleepDuration := min(
|
|
indexIterationSleepMultiplier*indexDuration,
|
|
indexIterationSleepCap,
|
|
)
|
|
time.Sleep(sleepDuration)
|
|
|
|
// Make sure not to include the sleep duration into the next index
|
|
// duration.
|
|
lastCommit = time.Now()
|
|
|
|
blockIterator = s.blockDB.NewIteratorWithStart(blkID[:])
|
|
}
|
|
}
|
|
|
|
// Ensure we fully iterated over all blocks before writing that indexing has
|
|
// finished.
|
|
//
|
|
// Note: This is needed because a transient read error could cause the
|
|
// iterator to stop early.
|
|
if err := blockIterator.Error(); err != nil {
|
|
return fmt.Errorf("failed to iterate over historical blocks: %w", err)
|
|
}
|
|
|
|
if err := s.singletonDB.Put(BlocksReindexedKey, nil); err != nil {
|
|
return fmt.Errorf("failed to put marked blocks as reindexed: %w", err)
|
|
}
|
|
|
|
// We must hold the lock during committing to make sure we don't attempt to
|
|
// commit to disk while a block is concurrently being accepted.
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
log.Info("finished block reindexing",
|
|
"numIndicesUpdated", numIndicesUpdated,
|
|
"numIndicesChecked", numIndicesChecked,
|
|
"duration", time.Since(startTime),
|
|
)
|
|
|
|
return s.Commit()
|
|
}
|
|
|
|
func (s *state) GetUptime(vdrID ids.NodeID, netID ids.ID) (time.Duration, time.Duration, error) {
|
|
upDuration, lastUpdated, err := s.validatorState.GetUptime(vdrID, netID)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
// Convert lastUpdated time.Time to Duration since Unix epoch
|
|
lastUpdatedDuration := time.Duration(lastUpdated.Unix()) * time.Second
|
|
return upDuration, lastUpdatedDuration, nil
|
|
}
|
|
|
|
func (s *state) SetUptime(vdrID ids.NodeID, netID ids.ID, upDuration time.Duration, lastUpdated time.Time) error {
|
|
return s.validatorState.SetUptime(vdrID, netID, upDuration, lastUpdated)
|
|
}
|
|
|
|
func markInitialized(db database.KeyValueWriter) error {
|
|
return db.Put(InitializedKey, nil)
|
|
}
|
|
|
|
func isInitialized(db database.KeyValueReader) (bool, error) {
|
|
return db.Has(InitializedKey)
|
|
}
|
|
|
|
func putFeeState(db database.KeyValueWriter, feeState gas.State) error {
|
|
feeStateBytes, err := marshalFeeState(feeState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return db.Put(FeeStateKey, feeStateBytes)
|
|
}
|
|
|
|
func getFeeState(db database.KeyValueReader) (gas.State, error) {
|
|
feeStateBytes, err := db.Get(FeeStateKey)
|
|
if err == database.ErrNotFound {
|
|
return gas.State{}, nil
|
|
}
|
|
if err != nil {
|
|
return gas.State{}, err
|
|
}
|
|
|
|
return parseFeeState(feeStateBytes)
|
|
}
|