From 49b71b49320cd5f1ae05406acb423fc1033632c6 Mon Sep 17 00:00:00 2001 From: Alberto Benegiamo Date: Thu, 18 May 2023 20:32:21 +0200 Subject: [PATCH] P-chain validator management refactor (#1284) Co-authored-by: Stephen Buttolph --- snow/validators/manager.go | 14 + vms/platformvm/blocks/builder/helpers_test.go | 21 +- vms/platformvm/blocks/executor/acceptor.go | 11 +- .../blocks/executor/acceptor_test.go | 39 +- .../blocks/executor/helpers_test.go | 21 +- vms/platformvm/blocks/executor/manager.go | 12 +- vms/platformvm/txs/executor/helpers_test.go | 14 +- vms/platformvm/validators/manager.go | 286 ++++++++++++ vms/platformvm/validators/manager_test.go | 439 +++++++++++++++++ vms/platformvm/validators/test_manager.go | 33 ++ vms/platformvm/vm.go | 266 +---------- vms/platformvm/vm_test.go | 442 ------------------ 12 files changed, 829 insertions(+), 769 deletions(-) create mode 100644 vms/platformvm/validators/manager.go create mode 100644 vms/platformvm/validators/manager_test.go create mode 100644 vms/platformvm/validators/test_manager.go diff --git a/snow/validators/manager.go b/snow/validators/manager.go index 0d0bc56372e..9dd81abffc7 100644 --- a/snow/validators/manager.go +++ b/snow/validators/manager.go @@ -145,3 +145,17 @@ func Contains(m Manager, subnetID ids.ID, nodeID ids.NodeID) bool { } return vdrs.Contains(nodeID) } + +func NodeIDs(m Manager, subnetID ids.ID) ([]ids.NodeID, error) { + vdrs, exist := m.Get(subnetID) + if !exist { + return nil, fmt.Errorf("%w: %s", errMissingValidators, subnetID) + } + + vdrsList := vdrs.List() + nodeIDs := make([]ids.NodeID, len(vdrsList)) + for i, vdr := range vdrsList { + nodeIDs[i] = vdr.NodeID + } + return nodeIDs, nil +} diff --git a/vms/platformvm/blocks/builder/helpers_test.go b/vms/platformvm/blocks/builder/helpers_test.go index 22ec49b8e77..cea4a297a26 100644 --- a/vms/platformvm/blocks/builder/helpers_test.go +++ b/vms/platformvm/blocks/builder/helpers_test.go @@ -34,7 +34,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -53,13 +52,10 @@ import ( blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/blocks/executor" txbuilder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) -const ( - defaultWeight = 10000 - maxRecentlyAcceptedWindowSize = 256 - recentlyAcceptedWindowTTL = 5 * time.Minute -) +const defaultWeight = 10000 var ( defaultMinStakingDuration = 24 * time.Hour @@ -154,13 +150,6 @@ func newEnvironment(t *testing.T) *environment { } registerer := prometheus.NewRegistry() - window := window.New[ids.ID]( - window.Config{ - Clock: res.clk, - MaxSize: maxRecentlyAcceptedWindowSize, - TTL: recentlyAcceptedWindowTTL, - }, - ) res.sender = &common.SenderTest{T: t} metrics, err := metrics.New("", registerer, res.config.TrackedSubnets) @@ -177,7 +166,7 @@ func newEnvironment(t *testing.T) *environment { metrics, res.state, &res.backend, - window, + pvalidators.TestManager, ) res.Builder = New( @@ -331,9 +320,9 @@ func defaultConfig() *config.Config { func defaultClock() *mockable.Clock { // set time after Banff fork (and before default nextStakerTime) - clk := mockable.Clock{} + clk := &mockable.Clock{} clk.Set(defaultGenesisTime) - return &clk + return clk } type fxVMInt struct { diff --git a/vms/platformvm/blocks/executor/acceptor.go b/vms/platformvm/blocks/executor/acceptor.go index 5eb98e19e59..54adc602545 100644 --- a/vms/platformvm/blocks/executor/acceptor.go +++ b/vms/platformvm/blocks/executor/acceptor.go @@ -9,13 +9,12 @@ import ( "go.uber.org/zap" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) var ( @@ -29,9 +28,9 @@ var ( // being shutdown. type acceptor struct { *backend - metrics metrics.Metrics - recentlyAccepted window.Window[ids.ID] - bootstrapped *utils.Atomic[bool] + metrics metrics.Metrics + validators validators.Manager + bootstrapped *utils.Atomic[bool] } func (a *acceptor) BanffAbortBlock(b *blocks.BanffAbortBlock) error { @@ -316,6 +315,6 @@ func (a *acceptor) commonAccept(b blocks.Block) error { a.state.SetLastAccepted(blkID) a.state.SetHeight(b.Height()) a.state.AddStatelessBlock(b, choices.Accepted) - a.recentlyAccepted.Add(blkID) + a.validators.OnAcceptedBlockID(blkID) return nil } diff --git a/vms/platformvm/blocks/executor/acceptor_test.go b/vms/platformvm/blocks/executor/acceptor_test.go index 3f382d248a2..6a3de730ec5 100644 --- a/vms/platformvm/blocks/executor/acceptor_test.go +++ b/vms/platformvm/blocks/executor/acceptor_test.go @@ -5,7 +5,6 @@ package executor import ( "testing" - "time" "github.com/golang/mock/gomock" @@ -19,12 +18,12 @@ import ( "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/validators" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -61,8 +60,8 @@ func TestAcceptorVisitProposalBlock(t *testing.T) { }, state: s, }, - metrics: metrics.Noop, - recentlyAccepted: nil, + metrics: metrics.Noop, + validators: validators.TestManager, } err = acceptor.ApricotProposalBlock(blk) @@ -98,12 +97,8 @@ func TestAcceptorVisitAtomicBlock(t *testing.T) { SharedMemory: sharedMemory, }, }, - metrics: metrics.Noop, - recentlyAccepted: window.New[ids.ID](window.Config{ - Clock: &mockable.Clock{}, - MaxSize: 1, - TTL: time.Hour, - }), + metrics: metrics.Noop, + validators: validators.TestManager, } blk, err := blocks.NewApricotAtomicBlock( @@ -183,12 +178,8 @@ func TestAcceptorVisitStandardBlock(t *testing.T) { SharedMemory: sharedMemory, }, }, - metrics: metrics.Noop, - recentlyAccepted: window.New[ids.ID](window.Config{ - Clock: clk, - MaxSize: 1, - TTL: time.Hour, - }), + metrics: metrics.Noop, + validators: validators.TestManager, } blk, err := blocks.NewBanffStandardBlock( @@ -278,12 +269,8 @@ func TestAcceptorVisitCommitBlock(t *testing.T) { SharedMemory: sharedMemory, }, }, - metrics: metrics.Noop, - recentlyAccepted: window.New[ids.ID](window.Config{ - Clock: &mockable.Clock{}, - MaxSize: 1, - TTL: time.Hour, - }), + metrics: metrics.Noop, + validators: validators.TestManager, bootstrapped: &utils.Atomic[bool]{}, } @@ -374,12 +361,8 @@ func TestAcceptorVisitAbortBlock(t *testing.T) { SharedMemory: sharedMemory, }, }, - metrics: metrics.Noop, - recentlyAccepted: window.New[ids.ID](window.Config{ - Clock: &mockable.Clock{}, - MaxSize: 1, - TTL: time.Hour, - }), + metrics: metrics.Noop, + validators: validators.TestManager, bootstrapped: &utils.Atomic[bool]{}, } diff --git a/vms/platformvm/blocks/executor/helpers_test.go b/vms/platformvm/blocks/executor/helpers_test.go index cf97e68b479..329aa9cc8a5 100644 --- a/vms/platformvm/blocks/executor/helpers_test.go +++ b/vms/platformvm/blocks/executor/helpers_test.go @@ -35,7 +35,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -54,15 +53,14 @@ import ( db_manager "github.com/ava-labs/avalanchego/database/manager" p_tx_builder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" + pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) const ( pending stakerStatus = iota current - defaultWeight = 10000 - maxRecentlyAcceptedWindowSize = 256 - recentlyAcceptedWindowTTL = 5 * time.Minute + defaultWeight = 10000 ) var ( @@ -189,13 +187,6 @@ func newEnvironment(t *testing.T, ctrl *gomock.Controller) *environment { } registerer := prometheus.NewRegistry() - window := window.New[ids.ID]( - window.Config{ - Clock: res.clk, - MaxSize: maxRecentlyAcceptedWindowSize, - TTL: recentlyAcceptedWindowTTL, - }, - ) res.sender = &common.SenderTest{T: t} metrics := metrics.Noop @@ -212,7 +203,7 @@ func newEnvironment(t *testing.T, ctrl *gomock.Controller) *environment { metrics, res.state, res.backend, - window, + pvalidators.TestManager, ) addSubnet(res) } else { @@ -221,7 +212,7 @@ func newEnvironment(t *testing.T, ctrl *gomock.Controller) *environment { metrics, res.mockedState, res.backend, - window, + pvalidators.TestManager, ) // we do not add any subnet to state, since we can mock // whatever we need @@ -362,9 +353,9 @@ func defaultConfig() *config.Config { } func defaultClock() *mockable.Clock { - clk := mockable.Clock{} + clk := &mockable.Clock{} clk.Set(defaultGenesisTime) - return &clk + return clk } type fxVMInt struct { diff --git a/vms/platformvm/blocks/executor/manager.go b/vms/platformvm/blocks/executor/manager.go index b552bbc1290..6343217e45d 100644 --- a/vms/platformvm/blocks/executor/manager.go +++ b/vms/platformvm/blocks/executor/manager.go @@ -6,12 +6,12 @@ package executor import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" + "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) var _ Manager = (*manager)(nil) @@ -31,7 +31,7 @@ func NewManager( metrics metrics.Metrics, s state.State, txExecutorBackend *executor.Backend, - recentlyAccepted window.Window[ids.ID], + validatorManager validators.Manager, ) Manager { backend := &backend{ Mempool: mempool, @@ -48,10 +48,10 @@ func NewManager( txExecutorBackend: txExecutorBackend, }, acceptor: &acceptor{ - backend: backend, - metrics: metrics, - recentlyAccepted: recentlyAccepted, - bootstrapped: txExecutorBackend.Bootstrapped, + backend: backend, + metrics: metrics, + validators: validatorManager, + bootstrapped: txExecutorBackend.Bootstrapped, }, rejector: &rejector{backend: backend}, } diff --git a/vms/platformvm/txs/executor/helpers_test.go b/vms/platformvm/txs/executor/helpers_test.go index f0916d58913..16c2ebeaa41 100644 --- a/vms/platformvm/txs/executor/helpers_test.go +++ b/vms/platformvm/txs/executor/helpers_test.go @@ -120,19 +120,19 @@ func newEnvironment(postBanff, postCortina bool) *environment { baseDB := versiondb.New(baseDBManager.Current().Database) ctx, msm := defaultCtx(baseDB) - fx := defaultFx(&clk, ctx.Log, isBootstrapped.Get()) + fx := defaultFx(clk, ctx.Log, isBootstrapped.Get()) rewards := reward.NewCalculator(config.RewardConfig) baseState := defaultState(&config, ctx, baseDB, rewards) atomicUTXOs := avax.NewAtomicUTXOManager(ctx.SharedMemory, txs.Codec) uptimes := uptime.NewManager(baseState) - utxoHandler := utxo.NewHandler(ctx, &clk, fx) + utxoHandler := utxo.NewHandler(ctx, clk, fx) txBuilder := builder.New( ctx, &config, - &clk, + clk, fx, baseState, atomicUTXOs, @@ -142,7 +142,7 @@ func newEnvironment(postBanff, postCortina bool) *environment { backend := Backend{ Config: &config, Ctx: ctx, - Clk: &clk, + Clk: clk, Bootstrapped: &isBootstrapped, Fx: fx, FlowChecker: utxoHandler, @@ -153,7 +153,7 @@ func newEnvironment(postBanff, postCortina bool) *environment { env := &environment{ isBootstrapped: &isBootstrapped, config: &config, - clk: &clk, + clk: clk, baseDB: baseDB, ctx: ctx, msm: msm, @@ -318,13 +318,13 @@ func defaultConfig(postBanff, postCortina bool) config.Config { } } -func defaultClock(postFork bool) mockable.Clock { +func defaultClock(postFork bool) *mockable.Clock { now := defaultGenesisTime if postFork { // 1 second after Banff fork now = defaultValidateEndTime.Add(-2 * time.Second) } - clk := mockable.Clock{} + clk := &mockable.Clock{} clk.Set(now) return clk } diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go new file mode 100644 index 00000000000..7ded28c7c4e --- /dev/null +++ b/vms/platformvm/validators/manager.go @@ -0,0 +1,286 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/window" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/metrics" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +const ( + validatorSetsCacheSize = 64 + maxRecentlyAcceptedWindowSize = 256 + recentlyAcceptedWindowTTL = 5 * time.Minute +) + +var ( + _ validators.State = (*manager)(nil) + + ErrMissingValidator = errors.New("missing validator") +) + +// Manager adds the ability to introduce newly acceted blocks IDs to the State +// interface. +type Manager interface { + validators.State + + // OnAcceptedBlockID registers the ID of the latest accepted block. + // It is used to update the [recentlyAccepted] sliding window. + OnAcceptedBlockID(blkID ids.ID) +} + +func NewManager( + cfg config.Config, + state state.State, + metrics metrics.Metrics, + clk *mockable.Clock, +) Manager { + return &manager{ + cfg: cfg, + state: state, + metrics: metrics, + clk: clk, + caches: make(map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput]), + recentlyAccepted: window.New[ids.ID]( + window.Config{ + Clock: clk, + MaxSize: maxRecentlyAcceptedWindowSize, + TTL: recentlyAcceptedWindowTTL, + }, + ), + } +} + +type manager struct { + cfg config.Config + state state.State + metrics metrics.Metrics + clk *mockable.Clock + + // Maps caches for each subnet that is currently tracked. + // Key: Subnet ID + // Value: cache mapping height -> validator set map + caches map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] + + // sliding window of blocks that were recently accepted + recentlyAccepted window.Window[ids.ID] +} + +// GetMinimumHeight returns the height of the most recent block beyond the +// horizon of our recentlyAccepted window. +// +// Because the time between blocks is arbitrary, we're only guaranteed that +// the window's configured TTL amount of time has passed once an element +// expires from the window. +// +// To try to always return a block older than the window's TTL, we return the +// parent of the oldest element in the window (as an expired element is always +// guaranteed to be sufficiently stale). If we haven't expired an element yet +// in the case of a process restart, we default to the lastAccepted block's +// height which is likely (but not guaranteed) to also be older than the +// window's configured TTL. +// +// If [UseCurrentHeight] is true, we override the block selection policy +// described above and we will always return the last accepted block height +// as the minimum. +func (m *manager) GetMinimumHeight(ctx context.Context) (uint64, error) { + if m.cfg.UseCurrentHeight { + return m.GetCurrentHeight(ctx) + } + + oldest, ok := m.recentlyAccepted.Oldest() + if !ok { + return m.GetCurrentHeight(ctx) + } + + blk, _, err := m.state.GetStatelessBlock(oldest) + if err != nil { + return 0, err + } + + // We subtract 1 from the height of [oldest] because we want the height of + // the last block accepted before the [recentlyAccepted] window. + // + // There is guaranteed to be a block accepted before this window because the + // first block added to [recentlyAccepted] window is >= height 1. + return blk.Height() - 1, nil +} + +func (m *manager) GetCurrentHeight(context.Context) (uint64, error) { + lastAcceptedID := m.state.GetLastAccepted() + lastAccepted, _, err := m.state.GetStatelessBlock(lastAcceptedID) + if err != nil { + return 0, err + } + return lastAccepted.Height(), nil +} + +func (m *manager) GetValidatorSet(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + validatorSetsCache, exists := m.caches[subnetID] + if !exists { + validatorSetsCache = &cache.LRU[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{Size: validatorSetsCacheSize} + // Only cache tracked subnets + if subnetID == constants.PrimaryNetworkID || m.cfg.TrackedSubnets.Contains(subnetID) { + m.caches[subnetID] = validatorSetsCache + } + } + + if validatorSet, ok := validatorSetsCache.Get(height); ok { + m.metrics.IncValidatorSetsCached() + return validatorSet, nil + } + + lastAcceptedHeight, err := m.GetCurrentHeight(ctx) + if err != nil { + return nil, err + } + if lastAcceptedHeight < height { + return nil, database.ErrNotFound + } + + // get the start time to track metrics + startTime := m.clk.Time() + + currentSubnetValidators, ok := m.cfg.Validators.Get(subnetID) + if !ok { + currentSubnetValidators = validators.NewSet() + if err := m.state.ValidatorSet(subnetID, currentSubnetValidators); err != nil { + return nil, err + } + } + currentPrimaryNetworkValidators, ok := m.cfg.Validators.Get(constants.PrimaryNetworkID) + if !ok { + // This should never happen + return nil, ErrMissingValidator + } + + currentSubnetValidatorList := currentSubnetValidators.List() + vdrSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(currentSubnetValidatorList)) + for _, vdr := range currentSubnetValidatorList { + primaryVdr, ok := currentPrimaryNetworkValidators.Get(vdr.NodeID) + if !ok { + // This should never happen + return nil, fmt.Errorf("%w: %s", ErrMissingValidator, vdr.NodeID) + } + vdrSet[vdr.NodeID] = &validators.GetValidatorOutput{ + NodeID: vdr.NodeID, + PublicKey: primaryVdr.PublicKey, + Weight: vdr.Weight, + } + } + + for diffHeight := lastAcceptedHeight; diffHeight > height; diffHeight-- { + err := m.applyValidatorDiffs(vdrSet, subnetID, diffHeight) + if err != nil { + return nil, err + } + } + + // cache the validator set + validatorSetsCache.Put(height, vdrSet) + + endTime := m.clk.Time() + m.metrics.IncValidatorSetsCreated() + m.metrics.AddValidatorSetsDuration(endTime.Sub(startTime)) + m.metrics.AddValidatorSetsHeightDiff(lastAcceptedHeight - height) + return vdrSet, nil +} + +func (m *manager) applyValidatorDiffs( + vdrSet map[ids.NodeID]*validators.GetValidatorOutput, + subnetID ids.ID, + height uint64, +) error { + weightDiffs, err := m.state.GetValidatorWeightDiffs(height, subnetID) + if err != nil { + return err + } + + for nodeID, weightDiff := range weightDiffs { + vdr, ok := vdrSet[nodeID] + if !ok { + // This node isn't in the current validator set. + vdr = &validators.GetValidatorOutput{ + NodeID: nodeID, + } + vdrSet[nodeID] = vdr + } + + // The weight of this node changed at this block. + if weightDiff.Decrease { + // The validator's weight was decreased at this block, so in the + // prior block it was higher. + vdr.Weight, err = math.Add64(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 = math.Sub(vdr.Weight, weightDiff.Amount) + } + if err != nil { + return err + } + + if vdr.Weight == 0 { + // The validator's weight was 0 before this block so + // they weren't in the validator set. + delete(vdrSet, nodeID) + } + } + + pkDiffs, err := m.state.GetValidatorPublicKeyDiffs(height) + if err != nil { + return err + } + + for nodeID, pk := range pkDiffs { + // pkDiffs includes all primary network key diffs, if we are + // fetching a subnet's validator set, we should ignore non-subnet + // validators. + if vdr, ok := vdrSet[nodeID]; ok { + // The validator's public key was removed at this block, so it + // was in the validator set before. + vdr.PublicKey = pk + } + } + return nil +} + +func (m *manager) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) { + if chainID == constants.PlatformChainID { + return constants.PrimaryNetworkID, nil + } + + chainTx, _, err := m.state.GetTx(chainID) + if err != nil { + return ids.Empty, fmt.Errorf( + "problem retrieving blockchain %q: %w", + chainID, + err, + ) + } + chain, ok := chainTx.Unsigned.(*txs.CreateChainTx) + if !ok { + return ids.Empty, fmt.Errorf("%q is not a blockchain", chainID) + } + return chain.SubnetID, nil +} + +func (m *manager) OnAcceptedBlockID(blkID ids.ID) { + m.recentlyAccepted.Add(blkID) +} diff --git a/vms/platformvm/validators/manager_test.go b/vms/platformvm/validators/manager_test.go new file mode 100644 index 00000000000..7f5d8975f42 --- /dev/null +++ b/vms/platformvm/validators/manager_test.go @@ -0,0 +1,439 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/chains" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/blocks" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/metrics" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/state" +) + +// AVAX asset ID in tests +var defaultRewardConfig = reward.Config{ + MaxConsumptionRate: .12 * reward.PercentDenominator, + MinConsumptionRate: .10 * reward.PercentDenominator, + MintingPeriod: 365 * 24 * time.Hour, + SupplyCap: 720 * units.MegaAvax, +} + +func TestVM_GetValidatorSet(t *testing.T) { + // Populate the validator set to use below + var ( + numVdrs = 4 + vdrBaseWeight = uint64(1_000) + vdrs []*validators.Validator + ) + + for i := 0; i < numVdrs; i++ { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + vdrs = append(vdrs, &validators.Validator{ + NodeID: ids.GenerateTestNodeID(), + PublicKey: bls.PublicFromSecretKey(sk), + Weight: vdrBaseWeight + uint64(i), + }) + } + + type test struct { + name string + // Height we're getting the diff at + height uint64 + lastAcceptedHeight uint64 + subnetID ids.ID + // Validator sets at tip + currentPrimaryNetworkValidators []*validators.Validator + currentSubnetValidators []*validators.Validator + // Diff at tip, block before tip, etc. + // This must have [lastAcceptedHeight] - [height] elements + weightDiffs []map[ids.NodeID]*state.ValidatorWeightDiff + // Diff at tip, block before tip, etc. + // This must have [lastAcceptedHeight] - [height] elements + pkDiffs []map[ids.NodeID]*bls.PublicKey + expectedVdrSet map[ids.NodeID]*validators.GetValidatorOutput + expectedErr error + } + + tests := []test{ + { + name: "after tip", + height: 1, + lastAcceptedHeight: 0, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{}, + expectedErr: database.ErrNotFound, + }, + { + name: "at tip", + height: 1, + lastAcceptedHeight: 1, + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + }, + currentSubnetValidators: []*validators.Validator{ + copySubnetValidator(vdrs[0]), + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight, + }, + }, + expectedErr: nil, + }, + { + name: "1 before tip", + height: 2, + lastAcceptedHeight: 3, + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + copyPrimaryValidator(vdrs[1]), + }, + currentSubnetValidators: []*validators.Validator{ + // At tip we have these 2 validators + copySubnetValidator(vdrs[0]), + copySubnetValidator(vdrs[1]), + }, + weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ + { + // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, + // and vdrs[2] left + vdrs[0].NodeID: { + Decrease: true, + Amount: 1, + }, + vdrs[1].NodeID: { + Decrease: false, + Amount: 1, + }, + vdrs[2].NodeID: { + Decrease: true, + Amount: vdrs[2].Weight, + }, + }, + }, + pkDiffs: []map[ids.NodeID]*bls.PublicKey{ + { + vdrs[2].NodeID: vdrs[2].PublicKey, + }, + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight + 1, + }, + vdrs[1].NodeID: { + NodeID: vdrs[1].NodeID, + PublicKey: vdrs[1].PublicKey, + Weight: vdrs[1].Weight - 1, + }, + vdrs[2].NodeID: { + NodeID: vdrs[2].NodeID, + PublicKey: vdrs[2].PublicKey, + Weight: vdrs[2].Weight, + }, + }, + expectedErr: nil, + }, + { + name: "2 before tip", + height: 3, + lastAcceptedHeight: 5, + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + copyPrimaryValidator(vdrs[1]), + }, + currentSubnetValidators: []*validators.Validator{ + // At tip we have these 2 validators + copySubnetValidator(vdrs[0]), + copySubnetValidator(vdrs[1]), + }, + weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ + { + // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, + // and vdrs[2] left + vdrs[0].NodeID: { + Decrease: true, + Amount: 1, + }, + vdrs[1].NodeID: { + Decrease: false, + Amount: 1, + }, + vdrs[2].NodeID: { + Decrease: true, + Amount: vdrs[2].Weight, + }, + }, + { + // At the block before tip vdrs[0] lost weight, vdrs[1] gained weight, + // vdrs[2] joined + vdrs[0].NodeID: { + Decrease: true, + Amount: 1, + }, + vdrs[1].NodeID: { + Decrease: false, + Amount: 1, + }, + vdrs[2].NodeID: { + Decrease: false, + Amount: vdrs[2].Weight, + }, + }, + }, + pkDiffs: []map[ids.NodeID]*bls.PublicKey{ + { + vdrs[2].NodeID: vdrs[2].PublicKey, + }, + {}, + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight + 2, + }, + vdrs[1].NodeID: { + NodeID: vdrs[1].NodeID, + PublicKey: vdrs[1].PublicKey, + Weight: vdrs[1].Weight - 2, + }, + }, + expectedErr: nil, + }, + { + name: "1 before tip; nil public key", + height: 4, + lastAcceptedHeight: 5, + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + copyPrimaryValidator(vdrs[1]), + }, + currentSubnetValidators: []*validators.Validator{ + // At tip we have these 2 validators + copySubnetValidator(vdrs[0]), + copySubnetValidator(vdrs[1]), + }, + weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ + { + // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, + // and vdrs[2] left + vdrs[0].NodeID: { + Decrease: true, + Amount: 1, + }, + vdrs[1].NodeID: { + Decrease: false, + Amount: 1, + }, + vdrs[2].NodeID: { + Decrease: true, + Amount: vdrs[2].Weight, + }, + }, + }, + pkDiffs: []map[ids.NodeID]*bls.PublicKey{ + {}, + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight + 1, + }, + vdrs[1].NodeID: { + NodeID: vdrs[1].NodeID, + PublicKey: vdrs[1].PublicKey, + Weight: vdrs[1].Weight - 1, + }, + vdrs[2].NodeID: { + NodeID: vdrs[2].NodeID, + Weight: vdrs[2].Weight, + }, + }, + expectedErr: nil, + }, + { + name: "1 before tip; subnet", + height: 5, + lastAcceptedHeight: 6, + subnetID: ids.GenerateTestID(), + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + copyPrimaryValidator(vdrs[1]), + copyPrimaryValidator(vdrs[3]), + }, + currentSubnetValidators: []*validators.Validator{ + // At tip we have these 2 validators + copySubnetValidator(vdrs[0]), + copySubnetValidator(vdrs[1]), + }, + weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ + { + // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, + // and vdrs[2] left + vdrs[0].NodeID: { + Decrease: true, + Amount: 1, + }, + vdrs[1].NodeID: { + Decrease: false, + Amount: 1, + }, + vdrs[2].NodeID: { + Decrease: true, + Amount: vdrs[2].Weight, + }, + }, + }, + pkDiffs: []map[ids.NodeID]*bls.PublicKey{ + {}, + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight + 1, + }, + vdrs[1].NodeID: { + NodeID: vdrs[1].NodeID, + PublicKey: vdrs[1].PublicKey, + Weight: vdrs[1].Weight - 1, + }, + vdrs[2].NodeID: { + NodeID: vdrs[2].NodeID, + Weight: vdrs[2].Weight, + }, + }, + expectedErr: nil, + }, + { + name: "unrelated primary network key removal on subnet lookup", + height: 4, + lastAcceptedHeight: 5, + subnetID: ids.GenerateTestID(), + currentPrimaryNetworkValidators: []*validators.Validator{ + copyPrimaryValidator(vdrs[0]), + }, + currentSubnetValidators: []*validators.Validator{ + copySubnetValidator(vdrs[0]), + }, + weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ + {}, + }, + pkDiffs: []map[ids.NodeID]*bls.PublicKey{ + { + vdrs[1].NodeID: vdrs[1].PublicKey, + }, + }, + expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ + vdrs[0].NodeID: { + NodeID: vdrs[0].NodeID, + PublicKey: vdrs[0].PublicKey, + Weight: vdrs[0].Weight, + }, + }, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // setup validators set + vdrs := validators.NewMockManager(ctrl) + cfg := config.Config{ + Chains: chains.TestManager, + UptimePercentage: .2, + RewardConfig: defaultRewardConfig, + Validators: vdrs, + UptimeLockedCalculator: uptime.NewLockedCalculator(), + BanffTime: mockable.MaxTime, + } + mockState := state.NewMockState(ctrl) + + metrics, err := metrics.New("", prometheus.NewRegistry(), cfg.TrackedSubnets) + r.NoError(err) + + clk := &mockable.Clock{} + validatorssSet := NewManager(cfg, mockState, metrics, clk) + + // Mock the VM's validators + mockSubnetVdrSet := validators.NewMockSet(ctrl) + mockSubnetVdrSet.EXPECT().List().Return(tt.currentSubnetValidators).AnyTimes() + vdrs.EXPECT().Get(tt.subnetID).Return(mockSubnetVdrSet, true).AnyTimes() + + mockPrimaryVdrSet := mockSubnetVdrSet + if tt.subnetID != constants.PrimaryNetworkID { + mockPrimaryVdrSet = validators.NewMockSet(ctrl) + vdrs.EXPECT().Get(constants.PrimaryNetworkID).Return(mockPrimaryVdrSet, true).AnyTimes() + } + for _, vdr := range tt.currentPrimaryNetworkValidators { + mockPrimaryVdrSet.EXPECT().Get(vdr.NodeID).Return(vdr, true).AnyTimes() + } + + // Tell state what diffs to report + for _, weightDiff := range tt.weightDiffs { + mockState.EXPECT().GetValidatorWeightDiffs(gomock.Any(), gomock.Any()).Return(weightDiff, nil) + } + + for _, pkDiff := range tt.pkDiffs { + mockState.EXPECT().GetValidatorPublicKeyDiffs(gomock.Any()).Return(pkDiff, nil) + } + + // Tell state last accepted block to report + mockTip := blocks.NewMockBlock(ctrl) + mockTip.EXPECT().Height().Return(tt.lastAcceptedHeight) + mockTipID := ids.GenerateTestID() + mockState.EXPECT().GetLastAccepted().Return(mockTipID) + mockState.EXPECT().GetStatelessBlock(mockTipID).Return(mockTip, choices.Accepted, nil) + + // Compute validator set at previous height + gotVdrSet, err := validatorssSet.GetValidatorSet(context.Background(), tt.height, tt.subnetID) + r.ErrorIs(err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + r.Equal(tt.expectedVdrSet, gotVdrSet) + }) + } +} + +func copyPrimaryValidator(vdr *validators.Validator) *validators.Validator { + newVdr := *vdr + return &newVdr +} + +func copySubnetValidator(vdr *validators.Validator) *validators.Validator { + newVdr := *vdr + newVdr.PublicKey = nil + return &newVdr +} diff --git a/vms/platformvm/validators/test_manager.go b/vms/platformvm/validators/test_manager.go new file mode 100644 index 00000000000..d7ffe993248 --- /dev/null +++ b/vms/platformvm/validators/test_manager.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" +) + +var TestManager Manager = testManager{} + +type testManager struct{} + +func (testManager) GetMinimumHeight(context.Context) (uint64, error) { + return 0, nil +} + +func (testManager) GetCurrentHeight(context.Context) (uint64, error) { + return 0, nil +} + +func (testManager) GetSubnetID(context.Context, ids.ID) (ids.ID, error) { + return ids.Empty, nil +} + +func (testManager) GetValidatorSet(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + return nil, nil +} + +func (testManager) OnAcceptedBlockID(ids.ID) {} diff --git a/vms/platformvm/vm.go b/vms/platformvm/vm.go index faaf7f5c376..a55b8a16de6 100644 --- a/vms/platformvm/vm.go +++ b/vms/platformvm/vm.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "time" "github.com/gorilla/rpc/v2" @@ -18,7 +17,6 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" - "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/manager" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" @@ -33,7 +31,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/utils/window" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -53,12 +50,7 @@ import ( blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/blocks/executor" txbuilder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" -) - -const ( - validatorSetsCacheSize = 512 - maxRecentlyAcceptedWindowSize = 256 - recentlyAcceptedWindowTTL = 5 * time.Minute + pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) var ( @@ -68,12 +60,12 @@ var ( _ validators.SubnetConnector = (*VM)(nil) errMissingValidatorSet = errors.New("missing validator set") - errMissingValidator = errors.New("missing validator") ) type VM struct { config.Config blockbuilder.Builder + validators.State metrics metrics.Metrics atomicUtxosManager avax.AtomicUTXOManager @@ -95,14 +87,6 @@ type VM struct { // Bootstrapped remembers if this chain has finished bootstrapping or not bootstrapped utils.Atomic[bool] - // Maps caches for each subnet that is currently tracked. - // Key: Subnet ID - // Value: cache mapping height -> validator set map - validatorSetCaches map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] - - // sliding window of blocks that were recently accepted - recentlyAccepted window.Window[ids.ID] - txBuilder txbuilder.Builder manager blockexecutor.Manager } @@ -143,15 +127,6 @@ func (vm *VM) Initialize( return err } - vm.validatorSetCaches = make(map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput]) - vm.recentlyAccepted = window.New[ids.ID]( - window.Config{ - Clock: &vm.clock, - MaxSize: maxRecentlyAcceptedWindowSize, - TTL: recentlyAcceptedWindowTTL, - }, - ) - rewards := reward.NewCalculator(vm.RewardConfig) vm.state, err = state.New( vm.dbManager.Current().Database, @@ -167,6 +142,8 @@ func (vm *VM) Initialize( return err } + validatorManager := pvalidators.NewManager(vm.Config, vm.state, vm.metrics, &vm.clock) + vm.State = validatorManager vm.atomicUtxosManager = avax.NewAtomicUTXOManager(chainCtx.SharedMemory, txs.Codec) utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) vm.uptimeManager = uptime.NewManager(vm.state) @@ -205,7 +182,7 @@ func (vm *VM) Initialize( vm.metrics, vm.state, txExecutorBackend, - vm.recentlyAccepted, + validatorManager, ) vm.Builder = blockbuilder.New( mempool, @@ -290,18 +267,18 @@ func (vm *VM) onNormalOperationsStarted() error { return err } - primaryVdrIDs, exists := vm.getValidatorIDs(constants.PrimaryNetworkID) - if !exists { - return errMissingValidatorSet + primaryVdrIDs, err := validators.NodeIDs(vm.Validators, constants.PrimaryNetworkID) + if err != nil { + return err } if err := vm.uptimeManager.StartTracking(primaryVdrIDs, constants.PrimaryNetworkID); err != nil { return err } for subnetID := range vm.TrackedSubnets { - vdrIDs, exists := vm.getValidatorIDs(subnetID) - if !exists { - return errMissingValidatorSet + vdrIDs, err := validators.NodeIDs(vm.Validators, subnetID) + if err != nil { + return err } if err := vm.uptimeManager.StartTracking(vdrIDs, subnetID); err != nil { return err @@ -337,18 +314,18 @@ func (vm *VM) Shutdown(context.Context) error { vm.Builder.Shutdown() if vm.bootstrapped.Get() { - primaryVdrIDs, exists := vm.getValidatorIDs(constants.PrimaryNetworkID) - if !exists { - return errMissingValidatorSet + primaryVdrIDs, err := validators.NodeIDs(vm.Validators, constants.PrimaryNetworkID) + if err != nil { + return err } if err := vm.uptimeManager.StopTracking(primaryVdrIDs, constants.PrimaryNetworkID); err != nil { return err } for subnetID := range vm.TrackedSubnets { - vdrIDs, exists := vm.getValidatorIDs(subnetID) - if !exists { - return errMissingValidatorSet + vdrIDs, err := validators.NodeIDs(vm.Validators, subnetID) + if err != nil { + return err } if err := vm.uptimeManager.StopTracking(vdrIDs, subnetID); err != nil { return err @@ -368,21 +345,6 @@ func (vm *VM) Shutdown(context.Context) error { return errs.Err } -func (vm *VM) getValidatorIDs(subnetID ids.ID) ([]ids.NodeID, bool) { - validatorSet, exist := vm.Validators.Get(subnetID) - if !exist { - return nil, false - } - validators := validatorSet.List() - - validatorIDs := make([]ids.NodeID, len(validators)) - for i, vdr := range validators { - validatorIDs[i] = vdr.NodeID - } - - return validatorIDs, true -} - func (vm *VM) ParseBlock(_ context.Context, b []byte) (snowman.Block, error) { // Note: blocks to be parsed are not verified, so we must used blocks.Codec // rather than blocks.GenesisCodec @@ -475,200 +437,6 @@ func (vm *VM) Disconnected(_ context.Context, nodeID ids.NodeID) error { return vm.state.Commit() } -// GetValidatorSet returns the validator set at the specified height for the -// provided subnetID. -func (vm *VM) GetValidatorSet(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { - validatorSetsCache, exists := vm.validatorSetCaches[subnetID] - if !exists { - validatorSetsCache = &cache.LRU[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{Size: validatorSetsCacheSize} - // Only cache tracked subnets - if subnetID == constants.PrimaryNetworkID || vm.TrackedSubnets.Contains(subnetID) { - vm.validatorSetCaches[subnetID] = validatorSetsCache - } - } - - if validatorSet, ok := validatorSetsCache.Get(height); ok { - vm.metrics.IncValidatorSetsCached() - return validatorSet, nil - } - - lastAcceptedHeight, err := vm.GetCurrentHeight(ctx) - if err != nil { - return nil, err - } - if lastAcceptedHeight < height { - return nil, database.ErrNotFound - } - - // get the start time to track metrics - startTime := vm.Clock().Time() - - currentSubnetValidators, ok := vm.Validators.Get(subnetID) - if !ok { - currentSubnetValidators = validators.NewSet() - if err := vm.state.ValidatorSet(subnetID, currentSubnetValidators); err != nil { - return nil, err - } - } - currentPrimaryNetworkValidators, ok := vm.Validators.Get(constants.PrimaryNetworkID) - if !ok { - // This should never happen - return nil, errMissingValidatorSet - } - - currentSubnetValidatorList := currentSubnetValidators.List() - vdrSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(currentSubnetValidatorList)) - for _, vdr := range currentSubnetValidatorList { - primaryVdr, ok := currentPrimaryNetworkValidators.Get(vdr.NodeID) - if !ok { - // This should never happen - return nil, fmt.Errorf("%w: %s", errMissingValidator, vdr.NodeID) - } - vdrSet[vdr.NodeID] = &validators.GetValidatorOutput{ - NodeID: vdr.NodeID, - PublicKey: primaryVdr.PublicKey, - Weight: vdr.Weight, - } - } - - for i := lastAcceptedHeight; i > height; i-- { - weightDiffs, err := vm.state.GetValidatorWeightDiffs(i, subnetID) - if err != nil { - return nil, err - } - - for nodeID, weightDiff := range weightDiffs { - vdr, ok := vdrSet[nodeID] - if !ok { - // This node isn't in the current validator set. - vdr = &validators.GetValidatorOutput{ - NodeID: nodeID, - } - vdrSet[nodeID] = vdr - } - - // The weight of this node changed at this block. - var op func(uint64, uint64) (uint64, error) - if weightDiff.Decrease { - // The validator's weight was decreased at this block, so in the - // prior block it was higher. - op = math.Add64 - } else { - // The validator's weight was increased at this block, so in the - // prior block it was lower. - op = math.Sub[uint64] - } - - // Apply the weight change. - vdr.Weight, err = op(vdr.Weight, weightDiff.Amount) - if err != nil { - return nil, err - } - - if vdr.Weight == 0 { - // The validator's weight was 0 before this block so - // they weren't in the validator set. - delete(vdrSet, nodeID) - } - } - - pkDiffs, err := vm.state.GetValidatorPublicKeyDiffs(i) - if err != nil { - return nil, err - } - - for nodeID, pk := range pkDiffs { - // pkDiffs includes all primary network key diffs, if we are - // fetching a subnet's validator set, we should ignore non-subnet - // validators. - if vdr, ok := vdrSet[nodeID]; ok { - // The validator's public key was removed at this block, so it - // was in the validator set before. - vdr.PublicKey = pk - } - } - } - - // cache the validator set - validatorSetsCache.Put(height, vdrSet) - - endTime := vm.Clock().Time() - vm.metrics.IncValidatorSetsCreated() - vm.metrics.AddValidatorSetsDuration(endTime.Sub(startTime)) - vm.metrics.AddValidatorSetsHeightDiff(lastAcceptedHeight - height) - return vdrSet, nil -} - -// GetCurrentHeight returns the height of the last accepted block -func (vm *VM) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) { - if chainID == constants.PlatformChainID { - return constants.PrimaryNetworkID, nil - } - - chainTx, _, err := vm.state.GetTx(chainID) - if err != nil { - return ids.Empty, fmt.Errorf( - "problem retrieving blockchain %q: %w", - chainID, - err, - ) - } - chain, ok := chainTx.Unsigned.(*txs.CreateChainTx) - if !ok { - return ids.Empty, fmt.Errorf("%q is not a blockchain", chainID) - } - return chain.SubnetID, nil -} - -// GetMinimumHeight returns the height of the most recent block beyond the -// horizon of our recentlyAccepted window. -// -// Because the time between blocks is arbitrary, we're only guaranteed that -// the window's configured TTL amount of time has passed once an element -// expires from the window. -// -// To try to always return a block older than the window's TTL, we return the -// parent of the oldest element in the window (as an expired element is always -// guaranteed to be sufficiently stale). If we haven't expired an element yet -// in the case of a process restart, we default to the lastAccepted block's -// height which is likely (but not guaranteed) to also be older than the -// window's configured TTL. -// -// If [UseCurrentHeight] is true, we will always return the last accepted block -// height as the minimum. This is used to trigger the proposervm on recently -// created subnets before [recentlyAcceptedWindowTTL]. -func (vm *VM) GetMinimumHeight(ctx context.Context) (uint64, error) { - if vm.Config.UseCurrentHeight { - return vm.GetCurrentHeight(ctx) - } - - oldest, ok := vm.recentlyAccepted.Oldest() - if !ok { - return vm.GetCurrentHeight(ctx) - } - - blk, err := vm.manager.GetBlock(oldest) - if err != nil { - return 0, err - } - - // We subtract 1 from the height of [oldest] because we want the height of - // the last block accepted before the [recentlyAccepted] window. - // - // There is guaranteed to be a block accepted before this window because the - // first block added to [recentlyAccepted] window is >= height 1. - return blk.Height() - 1, nil -} - -// GetCurrentHeight returns the height of the last accepted block -func (vm *VM) GetCurrentHeight(context.Context) (uint64, error) { - lastAccepted, err := vm.manager.GetBlock(vm.state.GetLastAccepted()) - if err != nil { - return 0, err - } - return lastAccepted.Height(), nil -} - func (vm *VM) CodecRegistry() codec.Registry { return vm.codecRegistry } diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 28ce2f31c08..29f18ab076c 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -11,8 +11,6 @@ import ( "testing" "time" - "github.com/golang/mock/gomock" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" @@ -41,7 +39,6 @@ import ( "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/subnets" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/formatting/address" @@ -51,7 +48,6 @@ import ( "github.com/ava-labs/avalanchego/utils/resource" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer" - "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/version" @@ -60,7 +56,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/reward" - "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -2358,443 +2353,6 @@ func TestUptimeDisallowedAfterNeverConnecting(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) } -func TestVM_GetValidatorSet(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Setup VM - _, genesisBytes := defaultGenesis() - db := manager.NewMemDB(version.Semantic1_0_0) - - vdrManager := validators.NewManager() - primaryVdrs := validators.NewSet() - _ = vdrManager.Add(constants.PrimaryNetworkID, primaryVdrs) - - vm := &VM{Config: config.Config{ - Chains: chains.TestManager, - UptimePercentage: .2, - RewardConfig: defaultRewardConfig, - Validators: vdrManager, - UptimeLockedCalculator: uptime.NewLockedCalculator(), - BanffTime: mockable.MaxTime, - }} - - ctx := defaultContext() - ctx.Lock.Lock() - - msgChan := make(chan common.Message, 1) - appSender := &common.SenderTest{T: t} - err := vm.Initialize(context.Background(), ctx, db, genesisBytes, nil, nil, msgChan, nil, appSender) - require.NoError(t, err) - defer func() { - require.NoError(t, vm.Shutdown(context.Background())) - ctx.Lock.Unlock() - }() - - vm.clock.Set(defaultGenesisTime) - vm.uptimeManager.(uptime.TestManager).SetTime(defaultGenesisTime) - - require.NoError(t, vm.SetState(context.Background(), snow.Bootstrapping)) - require.NoError(t, vm.SetState(context.Background(), snow.NormalOp)) - - var ( - oldVdrs = vm.Validators - oldState = vm.state - numVdrs = 4 - vdrBaseWeight = uint64(1_000) - vdrs []*validators.Validator - ) - // Populate the validator set to use below - for i := 0; i < numVdrs; i++ { - sk, err := bls.NewSecretKey() - require.NoError(t, err) - - vdrs = append(vdrs, &validators.Validator{ - NodeID: ids.GenerateTestNodeID(), - PublicKey: bls.PublicFromSecretKey(sk), - Weight: vdrBaseWeight + uint64(i), - }) - } - - type test struct { - name string - // Height we're getting the diff at - height uint64 - lastAcceptedHeight uint64 - subnetID ids.ID - // Validator sets at tip - currentPrimaryNetworkValidators []*validators.Validator - currentSubnetValidators []*validators.Validator - // Diff at tip, block before tip, etc. - // This must have [height] - [lastAcceptedHeight] elements - weightDiffs []map[ids.NodeID]*state.ValidatorWeightDiff - // Diff at tip, block before tip, etc. - // This must have [height] - [lastAcceptedHeight] elements - pkDiffs []map[ids.NodeID]*bls.PublicKey - expectedVdrSet map[ids.NodeID]*validators.GetValidatorOutput - expectedErr error - } - - tests := []test{ - { - name: "after tip", - height: 1, - lastAcceptedHeight: 0, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{}, - expectedErr: database.ErrNotFound, - }, - { - name: "at tip", - height: 1, - lastAcceptedHeight: 1, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - }, - currentSubnetValidators: []*validators.Validator{ - copySubnetValidator(vdrs[0]), - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight, - }, - }, - expectedErr: nil, - }, - { - name: "1 before tip", - height: 2, - lastAcceptedHeight: 3, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), - }, - currentSubnetValidators: []*validators.Validator{ - // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), - }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { - // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, - // and vdrs[2] left - vdrs[0].NodeID: { - Decrease: true, - Amount: 1, - }, - vdrs[1].NodeID: { - Decrease: false, - Amount: 1, - }, - vdrs[2].NodeID: { - Decrease: true, - Amount: vdrs[2].Weight, - }, - }, - }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[2].NodeID: vdrs[2].PublicKey, - }, - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, - }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, - }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - PublicKey: vdrs[2].PublicKey, - Weight: vdrs[2].Weight, - }, - }, - expectedErr: nil, - }, - { - name: "2 before tip", - height: 3, - lastAcceptedHeight: 5, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), - }, - currentSubnetValidators: []*validators.Validator{ - // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), - }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { - // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, - // and vdrs[2] left - vdrs[0].NodeID: { - Decrease: true, - Amount: 1, - }, - vdrs[1].NodeID: { - Decrease: false, - Amount: 1, - }, - vdrs[2].NodeID: { - Decrease: true, - Amount: vdrs[2].Weight, - }, - }, - { - // At the block before tip vdrs[0] lost weight, vdrs[1] gained weight, - // vdrs[2] joined - vdrs[0].NodeID: { - Decrease: true, - Amount: 1, - }, - vdrs[1].NodeID: { - Decrease: false, - Amount: 1, - }, - vdrs[2].NodeID: { - Decrease: false, - Amount: vdrs[2].Weight, - }, - }, - }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[2].NodeID: vdrs[2].PublicKey, - }, - {}, - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 2, - }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 2, - }, - }, - expectedErr: nil, - }, - { - name: "1 before tip; nil public key", - height: 4, - lastAcceptedHeight: 5, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), - }, - currentSubnetValidators: []*validators.Validator{ - // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), - }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { - // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, - // and vdrs[2] left - vdrs[0].NodeID: { - Decrease: true, - Amount: 1, - }, - vdrs[1].NodeID: { - Decrease: false, - Amount: 1, - }, - vdrs[2].NodeID: { - Decrease: true, - Amount: vdrs[2].Weight, - }, - }, - }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - {}, - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, - }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, - }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - Weight: vdrs[2].Weight, - }, - }, - expectedErr: nil, - }, - { - name: "1 before tip; subnet", - height: 5, - lastAcceptedHeight: 6, - subnetID: ids.GenerateTestID(), - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), - copyPrimaryValidator(vdrs[3]), - }, - currentSubnetValidators: []*validators.Validator{ - // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), - }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { - // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, - // and vdrs[2] left - vdrs[0].NodeID: { - Decrease: true, - Amount: 1, - }, - vdrs[1].NodeID: { - Decrease: false, - Amount: 1, - }, - vdrs[2].NodeID: { - Decrease: true, - Amount: vdrs[2].Weight, - }, - }, - }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - {}, - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, - }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, - }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - Weight: vdrs[2].Weight, - }, - }, - expectedErr: nil, - }, - { - name: "unrelated primary network key removal on subnet lookup", - height: 4, - lastAcceptedHeight: 5, - subnetID: ids.GenerateTestID(), - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - }, - currentSubnetValidators: []*validators.Validator{ - copySubnetValidator(vdrs[0]), - }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - {}, - }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[1].NodeID: vdrs[1].PublicKey, - }, - }, - expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight, - }, - }, - expectedErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - - // Mock the VM's validators - vdrs := validators.NewMockManager(ctrl) - vm.Validators = vdrs - mockSubnetVdrSet := validators.NewMockSet(ctrl) - mockSubnetVdrSet.EXPECT().List().Return(tt.currentSubnetValidators).AnyTimes() - vdrs.EXPECT().Get(tt.subnetID).Return(mockSubnetVdrSet, true).AnyTimes() - - mockPrimaryVdrSet := mockSubnetVdrSet - if tt.subnetID != constants.PrimaryNetworkID { - mockPrimaryVdrSet = validators.NewMockSet(ctrl) - vdrs.EXPECT().Get(constants.PrimaryNetworkID).Return(mockPrimaryVdrSet, true).AnyTimes() - } - for _, vdr := range tt.currentPrimaryNetworkValidators { - mockPrimaryVdrSet.EXPECT().Get(vdr.NodeID).Return(vdr, true).AnyTimes() - } - - // Mock the block manager - mockManager := blockexecutor.NewMockManager(ctrl) - vm.manager = mockManager - - // Mock the VM's state - mockState := state.NewMockState(ctrl) - vm.state = mockState - - // Tell state what diffs to report - for _, weightDiff := range tt.weightDiffs { - mockState.EXPECT().GetValidatorWeightDiffs(gomock.Any(), gomock.Any()).Return(weightDiff, nil) - } - - for _, pkDiff := range tt.pkDiffs { - mockState.EXPECT().GetValidatorPublicKeyDiffs(gomock.Any()).Return(pkDiff, nil) - } - - // Tell state last accepted block to report - mockTip := smcon.NewMockBlock(ctrl) - mockTip.EXPECT().Height().Return(tt.lastAcceptedHeight) - mockTipID := ids.GenerateTestID() - mockState.EXPECT().GetLastAccepted().Return(mockTipID) - mockManager.EXPECT().GetBlock(mockTipID).Return(mockTip, nil) - - // Compute validator set at previous height - gotVdrSet, err := vm.GetValidatorSet(context.Background(), tt.height, tt.subnetID) - require.ErrorIs(err, tt.expectedErr) - if tt.expectedErr != nil { - return - } - require.Len(gotVdrSet, len(tt.expectedVdrSet)) - for nodeID, vdr := range tt.expectedVdrSet { - otherVdr, ok := gotVdrSet[nodeID] - require.True(ok) - require.Equal(vdr, otherVdr) - } - }) - } - - // Put these back so we don't need to mock calls made on Shutdown - vm.Validators = oldVdrs - vm.state = oldState -} - -func copyPrimaryValidator(vdr *validators.Validator) *validators.Validator { - newVdr := *vdr - return &newVdr -} - -func copySubnetValidator(vdr *validators.Validator) *validators.Validator { - newVdr := *vdr - newVdr.PublicKey = nil - return &newVdr -} - func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { require := require.New(t)