From fad48ec3f0d9ad088b4b3bf0c68e7187dc494222 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Fri, 15 Nov 2024 22:01:56 +0300 Subject: [PATCH] Refactor uptime tracking (#1388) Co-authored-by: Darioush Jalali Co-authored-by: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> --- plugin/evm/service.go | 8 +- .../evm/validators/interfaces/interfaces.go | 30 ++ plugin/evm/validators/manager.go | 161 ++++++++ plugin/evm/validators/manager_test.go | 221 +++++++++++ plugin/evm/validators/{ => state}/codec.go | 2 +- .../{ => state}/interfaces/mock_listener.go | 4 +- .../interfaces/state.go} | 22 +- plugin/evm/validators/{ => state}/state.go | 4 +- .../evm/validators/{ => state}/state_test.go | 7 +- .../uptime/interfaces/interface.go | 4 +- .../uptime/pausable_manager.go | 2 +- .../uptime/pausable_manager_test.go | 2 +- .../validators/validatorstest/noop_state.go | 64 --- plugin/evm/vm.go | 134 +------ plugin/evm/vm_validators_state_test.go | 363 ------------------ plugin/evm/vm_validators_test.go | 161 ++++++++ scripts/mocks.mockgen.txt | 2 +- warp/backend.go | 14 +- warp/backend_test.go | 13 +- warp/handlers/signature_request_test.go | 8 +- warp/verifier_backend.go | 19 +- warp/verifier_backend_test.go | 24 +- warp/warptest/noop_validator_reader.go | 21 + 23 files changed, 665 insertions(+), 625 deletions(-) create mode 100644 plugin/evm/validators/interfaces/interfaces.go create mode 100644 plugin/evm/validators/manager.go create mode 100644 plugin/evm/validators/manager_test.go rename plugin/evm/validators/{ => state}/codec.go (96%) rename plugin/evm/validators/{ => state}/interfaces/mock_listener.go (94%) rename plugin/evm/validators/{interfaces/interface.go => state/interfaces/state.go} (94%) rename plugin/evm/validators/{ => state}/state.go (99%) rename plugin/evm/validators/{ => state}/state_test.go (97%) rename plugin/evm/{ => validators}/uptime/interfaces/interface.go (67%) rename plugin/evm/{ => validators}/uptime/pausable_manager.go (98%) rename plugin/evm/{ => validators}/uptime/pausable_manager_test.go (99%) delete mode 100644 plugin/evm/validators/validatorstest/noop_state.go delete mode 100644 plugin/evm/vm_validators_state_test.go create mode 100644 plugin/evm/vm_validators_test.go create mode 100644 warp/warptest/noop_validator_reader.go diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 1a7b285bfe..625d169514 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -33,19 +33,19 @@ func (api *ValidatorsAPI) GetCurrentValidators(_ *http.Request, _ *struct{}, rep api.vm.ctx.Lock.RLock() defer api.vm.ctx.Lock.RUnlock() - vIDs := api.vm.validatorState.GetValidationIDs() + vIDs := api.vm.validatorsManager.GetValidationIDs() reply.Validators = make([]CurrentValidator, 0, vIDs.Len()) for _, vID := range vIDs.List() { - validator, err := api.vm.validatorState.GetValidator(vID) + validator, err := api.vm.validatorsManager.GetValidator(vID) if err != nil { return err } - isConnected := api.vm.uptimeManager.IsConnected(validator.NodeID) + isConnected := api.vm.validatorsManager.IsConnected(validator.NodeID) - uptime, _, err := api.vm.uptimeManager.CalculateUptime(validator.NodeID) + uptime, _, err := api.vm.validatorsManager.CalculateUptime(validator.NodeID) if err != nil { return err } diff --git a/plugin/evm/validators/interfaces/interfaces.go b/plugin/evm/validators/interfaces/interfaces.go new file mode 100644 index 0000000000..d8b88b3626 --- /dev/null +++ b/plugin/evm/validators/interfaces/interfaces.go @@ -0,0 +1,30 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interfaces + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + avalancheuptime "github.com/ava-labs/avalanchego/snow/uptime" + stateinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" +) + +type ValidatorReader interface { + // GetValidatorAndUptime returns the uptime of the validator specified by validationID + GetValidatorAndUptime(validationID ids.ID) (stateinterfaces.Validator, time.Duration, time.Time, error) +} + +type Manager interface { + stateinterfaces.State + avalancheuptime.Manager + ValidatorReader + + // Sync updates the validator set managed + // by the manager + Sync(ctx context.Context) error + // DispatchSync starts the sync process + DispatchSync(ctx context.Context) +} diff --git a/plugin/evm/validators/manager.go b/plugin/evm/validators/manager.go new file mode 100644 index 0000000000..e679be220c --- /dev/null +++ b/plugin/evm/validators/manager.go @@ -0,0 +1,161 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + avalancheuptime "github.com/ava-labs/avalanchego/snow/uptime" + avalanchevalidators "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + validators "github.com/ava-labs/subnet-evm/plugin/evm/validators/state" + stateinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/uptime" + uptimeinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/uptime/interfaces" + + "github.com/ethereum/go-ethereum/log" +) + +const ( + SyncFrequency = 1 * time.Minute +) + +type manager struct { + chainCtx *snow.Context + stateinterfaces.State + uptimeinterfaces.PausableManager +} + +// NewManager returns a new validator manager +// that manages the validator state and the uptime manager. +func NewManager( + ctx *snow.Context, + db database.Database, + clock *mockable.Clock, +) (interfaces.Manager, error) { + validatorState, err := validators.NewState(db) + if err != nil { + return nil, fmt.Errorf("failed to initialize validator state: %w", err) + } + + // Initialize uptime manager + uptimeManager := uptime.NewPausableManager(avalancheuptime.NewManager(validatorState, clock)) + validatorState.RegisterListener(uptimeManager) + + return &manager{ + chainCtx: ctx, + State: validatorState, + PausableManager: uptimeManager, + }, nil +} + +// GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID +// and the last updated time. +// GetValidatorAndUptime holds the chain context lock while performing the operation and can be called concurrently. +func (m *manager) GetValidatorAndUptime(validationID ids.ID) (stateinterfaces.Validator, time.Duration, time.Time, error) { + // lock the state + m.chainCtx.Lock.RLock() + defer m.chainCtx.Lock.RUnlock() + + // Get validator first + vdr, err := m.GetValidator(validationID) + if err != nil { + return stateinterfaces.Validator{}, 0, time.Time{}, fmt.Errorf("failed to get validator: %w", err) + } + + uptime, lastUpdated, err := m.CalculateUptime(vdr.NodeID) + if err != nil { + return stateinterfaces.Validator{}, 0, time.Time{}, fmt.Errorf("failed to get uptime: %w", err) + } + + return vdr, uptime, lastUpdated, nil +} + +// DispatchSync starts the sync process +// DispatchSync holds the chain context lock while performing the sync. +func (m *manager) DispatchSync(ctx context.Context) { + ticker := time.NewTicker(SyncFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.chainCtx.Lock.Lock() + if err := m.Sync(ctx); err != nil { + log.Error("failed to sync validators", "error", err) + } + m.chainCtx.Lock.Unlock() + case <-ctx.Done(): + return + } + } +} + +// Sync synchronizes the validator state with the current validator set +// and writes the state to the database. +// Sync is not safe to call concurrently and should be called with the chain context locked. +func (m *manager) Sync(ctx context.Context) error { + now := time.Now() + log.Debug("performing validator sync") + // get current validator set + currentValidatorSet, _, err := m.chainCtx.ValidatorState.GetCurrentValidatorSet(ctx, m.chainCtx.SubnetID) + if err != nil { + return fmt.Errorf("failed to get current validator set: %w", err) + } + + // load the current validator set into the validator state + if err := loadValidators(m.State, currentValidatorSet); err != nil { + return fmt.Errorf("failed to load current validators: %w", err) + } + + // write validators to the database + if err := m.State.WriteState(); err != nil { + return fmt.Errorf("failed to write validator state: %w", err) + } + + // TODO: add metrics + log.Debug("validator sync complete", "duration", time.Since(now)) + return nil +} + +// loadValidators loads the [validators] into the validator state [validatorState] +func loadValidators(validatorState stateinterfaces.State, newValidators map[ids.ID]*avalanchevalidators.GetCurrentValidatorOutput) error { + currentValidationIDs := validatorState.GetValidationIDs() + // first check if we need to delete any existing validators + for vID := range currentValidationIDs { + // if the validator is not in the new set of validators + // delete the validator + if _, exists := newValidators[vID]; !exists { + validatorState.DeleteValidator(vID) + } + } + + // then load the new validators + for newVID, newVdr := range newValidators { + currentVdr := stateinterfaces.Validator{ + ValidationID: newVID, + NodeID: newVdr.NodeID, + Weight: newVdr.Weight, + StartTimestamp: newVdr.StartTime, + IsActive: newVdr.IsActive, + IsSoV: newVdr.IsSoV, + } + if currentValidationIDs.Contains(newVID) { + if err := validatorState.UpdateValidator(currentVdr); err != nil { + return err + } + } else { + if err := validatorState.AddValidator(currentVdr); err != nil { + return err + } + } + } + return nil +} diff --git a/plugin/evm/validators/manager_test.go b/plugin/evm/validators/manager_test.go new file mode 100644 index 0000000000..282488f68f --- /dev/null +++ b/plugin/evm/validators/manager_test.go @@ -0,0 +1,221 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "testing" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/state" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + avagovalidators "github.com/ava-labs/avalanchego/snow/validators" +) + +func TestLoadNewValidators(t *testing.T) { + testNodeIDs := []ids.NodeID{ + ids.GenerateTestNodeID(), + ids.GenerateTestNodeID(), + ids.GenerateTestNodeID(), + } + testValidationIDs := []ids.ID{ + ids.GenerateTestID(), + ids.GenerateTestID(), + ids.GenerateTestID(), + } + tests := []struct { + name string + initialValidators map[ids.ID]*avagovalidators.GetCurrentValidatorOutput + newValidators map[ids.ID]*avagovalidators.GetCurrentValidatorOutput + registerMockListenerCalls func(*interfaces.MockStateCallbackListener) + expectedLoadErr error + }{ + { + name: "before empty/after empty", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{}, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{}, + registerMockListenerCalls: func(*interfaces.MockStateCallbackListener) {}, + }, + { + name: "before empty/after one", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{}, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + }, + }, + { + name: "before one/after empty", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{}, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + // initial validator will trigger first + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + // then it will be removed + mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1) + }, + }, + { + name: "no change", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + }, + }, + { + name: "status and weight change and new one", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + Weight: 1, + }, + }, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: false, + StartTime: 0, + Weight: 2, + }, + testValidationIDs[1]: { + NodeID: testNodeIDs[1], + IsActive: true, + StartTime: 0, + }, + }, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + // initial validator will trigger first + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + // then it will be updated + mock.EXPECT().OnValidatorStatusUpdated(testValidationIDs[0], testNodeIDs[0], false).Times(1) + // new validator will be added + mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[1], uint64(0), true).Times(1) + }, + }, + { + name: "renew validation ID", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[1]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + // initial validator will trigger first + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + // then it will be removed + mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1) + // new validator will be added + mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[0], uint64(0), true).Times(1) + }, + }, + { + name: "renew node ID", + initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + IsActive: true, + StartTime: 0, + }, + }, + newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[1], + IsActive: true, + StartTime: 0, + }, + }, + expectedLoadErr: state.ErrImmutableField, + registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { + // initial validator will trigger first + mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) + // then it won't be called since we don't track the node ID changes + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + require := require.New(tt) + db := memdb.New() + validatorState, err := state.NewState(db) + require.NoError(err) + + // set initial validators + for vID, validator := range test.initialValidators { + err := validatorState.AddValidator(interfaces.Validator{ + ValidationID: vID, + NodeID: validator.NodeID, + Weight: validator.Weight, + StartTimestamp: validator.StartTime, + IsActive: validator.IsActive, + IsSoV: validator.IsSoV, + }) + require.NoError(err) + } + // enable mock listener + ctrl := gomock.NewController(tt) + mockListener := interfaces.NewMockStateCallbackListener(ctrl) + test.registerMockListenerCalls(mockListener) + + validatorState.RegisterListener(mockListener) + // load new validators + err = loadValidators(validatorState, test.newValidators) + if test.expectedLoadErr != nil { + require.Error(err) + return + } + require.NoError(err) + // check if the state is as expected + require.Equal(len(test.newValidators), validatorState.GetValidationIDs().Len()) + for vID, validator := range test.newValidators { + v, err := validatorState.GetValidator(vID) + require.NoError(err) + require.Equal(validator.NodeID, v.NodeID) + require.Equal(validator.Weight, v.Weight) + require.Equal(validator.StartTime, v.StartTimestamp) + require.Equal(validator.IsActive, v.IsActive) + require.Equal(validator.IsSoV, v.IsSoV) + } + }) + } +} diff --git a/plugin/evm/validators/codec.go b/plugin/evm/validators/state/codec.go similarity index 96% rename from plugin/evm/validators/codec.go rename to plugin/evm/validators/state/codec.go index dadba8b273..aeb1a683b2 100644 --- a/plugin/evm/validators/codec.go +++ b/plugin/evm/validators/state/codec.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package validators +package state import ( "math" diff --git a/plugin/evm/validators/interfaces/mock_listener.go b/plugin/evm/validators/state/interfaces/mock_listener.go similarity index 94% rename from plugin/evm/validators/interfaces/mock_listener.go rename to plugin/evm/validators/state/interfaces/mock_listener.go index 8cf1903729..053a371d16 100644 --- a/plugin/evm/validators/interfaces/mock_listener.go +++ b/plugin/evm/validators/state/interfaces/mock_listener.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces (interfaces: StateCallbackListener) +// Source: github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces (interfaces: StateCallbackListener) // // Generated by this command: // -// mockgen -package=interfaces -destination=plugin/evm/validators/interfaces/mock_listener.go github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces StateCallbackListener +// mockgen -package=interfaces -destination=plugin/evm/validators/state/interfaces/mock_listener.go github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces StateCallbackListener // // Package interfaces is a generated GoMock package. diff --git a/plugin/evm/validators/interfaces/interface.go b/plugin/evm/validators/state/interfaces/state.go similarity index 94% rename from plugin/evm/validators/interfaces/interface.go rename to plugin/evm/validators/state/interfaces/state.go index 39b6b8c9e9..ec7fac2416 100644 --- a/plugin/evm/validators/interfaces/interface.go +++ b/plugin/evm/validators/state/interfaces/state.go @@ -4,31 +4,31 @@ package interfaces import ( - "time" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/set" ) +type StateReader interface { + // GetValidator returns the validator data for the given validation ID + GetValidator(vID ids.ID) (Validator, error) + // GetValidationIDs returns the validation IDs in the state + GetValidationIDs() set.Set[ids.ID] + // GetNodeIDs returns the validator node IDs in the state + GetNodeIDs() set.Set[ids.NodeID] +} + type State interface { uptime.State + StateReader // AddValidator adds a new validator to the state AddValidator(vdr Validator) error // UpdateValidator updates the validator in the state UpdateValidator(vdr Validator) error - // GetValidator returns the validator data for the given validation ID - GetValidator(vID ids.ID) (Validator, error) // DeleteValidator deletes the validator from the state DeleteValidator(vID ids.ID) error // WriteState writes the validator state to the disk WriteState() error - - // GetValidationIDs returns the validation IDs in the state - GetValidationIDs() set.Set[ids.ID] - // GetNodeIDs returns the validator node IDs in the state - GetNodeIDs() set.Set[ids.NodeID] - // RegisterListener registers a listener to the state RegisterListener(StateCallbackListener) } @@ -51,5 +51,3 @@ type Validator struct { IsActive bool `json:"isActive"` IsSoV bool `json:"isSoV"` } - -func (v *Validator) StartTime() time.Time { return time.Unix(int64(v.StartTimestamp), 0) } diff --git a/plugin/evm/validators/state.go b/plugin/evm/validators/state/state.go similarity index 99% rename from plugin/evm/validators/state.go rename to plugin/evm/validators/state/state.go index a8769466e9..14c7912ea9 100644 --- a/plugin/evm/validators/state.go +++ b/plugin/evm/validators/state/state.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package validators +package state import ( "fmt" @@ -11,7 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" ) var _ uptime.State = &state{} diff --git a/plugin/evm/validators/state_test.go b/plugin/evm/validators/state/state_test.go similarity index 97% rename from plugin/evm/validators/state_test.go rename to plugin/evm/validators/state/state_test.go index 7ba6574785..b888de156d 100644 --- a/plugin/evm/validators/state_test.go +++ b/plugin/evm/validators/state/state_test.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package validators +package state import ( "testing" @@ -15,7 +15,7 @@ import ( "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/wrappers" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" ) func TestState(t *testing.T) { @@ -94,8 +94,7 @@ func TestState(t *testing.T) { require.ErrorIs(state.UpdateValidator(vdr), ErrImmutableField) // set a different start time should fail - newStartTime := vdr.StartTime().Add(time.Hour) - vdr.StartTimestamp = uint64(newStartTime.Unix()) + vdr.StartTimestamp = vdr.StartTimestamp + 100 require.ErrorIs(state.UpdateValidator(vdr), ErrImmutableField) // set SoV should fail diff --git a/plugin/evm/uptime/interfaces/interface.go b/plugin/evm/validators/uptime/interfaces/interface.go similarity index 67% rename from plugin/evm/uptime/interfaces/interface.go rename to plugin/evm/validators/uptime/interfaces/interface.go index 13e6b7abba..296daae314 100644 --- a/plugin/evm/uptime/interfaces/interface.go +++ b/plugin/evm/validators/uptime/interfaces/interface.go @@ -6,11 +6,11 @@ package interfaces import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" - validatorsinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + validatorsstateinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" ) type PausableManager interface { uptime.Manager - validatorsinterfaces.StateCallbackListener + validatorsstateinterfaces.StateCallbackListener IsPaused(nodeID ids.NodeID) bool } diff --git a/plugin/evm/uptime/pausable_manager.go b/plugin/evm/validators/uptime/pausable_manager.go similarity index 98% rename from plugin/evm/uptime/pausable_manager.go rename to plugin/evm/validators/uptime/pausable_manager.go index 6c437dd049..a715c54257 100644 --- a/plugin/evm/uptime/pausable_manager.go +++ b/plugin/evm/validators/uptime/pausable_manager.go @@ -6,7 +6,7 @@ package uptime import ( "errors" - "github.com/ava-labs/subnet-evm/plugin/evm/uptime/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/uptime/interfaces" "github.com/ethereum/go-ethereum/log" "github.com/ava-labs/avalanchego/ids" diff --git a/plugin/evm/uptime/pausable_manager_test.go b/plugin/evm/validators/uptime/pausable_manager_test.go similarity index 99% rename from plugin/evm/uptime/pausable_manager_test.go rename to plugin/evm/validators/uptime/pausable_manager_test.go index a910203773..8cdadba0fd 100644 --- a/plugin/evm/uptime/pausable_manager_test.go +++ b/plugin/evm/validators/uptime/pausable_manager_test.go @@ -10,7 +10,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/subnet-evm/plugin/evm/uptime/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/uptime/interfaces" "github.com/stretchr/testify/require" ) diff --git a/plugin/evm/validators/validatorstest/noop_state.go b/plugin/evm/validators/validatorstest/noop_state.go deleted file mode 100644 index 3594999574..0000000000 --- a/plugin/evm/validators/validatorstest/noop_state.go +++ /dev/null @@ -1,64 +0,0 @@ -package validatorstest - -import ( - "time" - - ids "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" -) - -var NoOpState interfaces.State = &noOpState{} - -type noOpState struct{} - -func (n *noOpState) GetStatus(vID ids.ID) (bool, error) { return false, nil } - -func (n *noOpState) GetValidationIDs() set.Set[ids.ID] { return set.NewSet[ids.ID](0) } - -func (n *noOpState) GetNodeIDs() set.Set[ids.NodeID] { return set.NewSet[ids.NodeID](0) } - -func (n *noOpState) GetValidator(vID ids.ID) (interfaces.Validator, error) { - return interfaces.Validator{}, nil -} - -func (n *noOpState) GetNodeID(vID ids.ID) (ids.NodeID, error) { return ids.NodeID{}, nil } - -func (n *noOpState) AddValidator(vdr interfaces.Validator) error { - return nil -} - -func (n *noOpState) UpdateValidator(vdr interfaces.Validator) error { - return nil -} - -func (n *noOpState) DeleteValidator(vID ids.ID) error { - return nil -} -func (n *noOpState) WriteState() error { return nil } - -func (n *noOpState) SetStatus(vID ids.ID, isActive bool) error { return nil } - -func (n *noOpState) SetWeight(vID ids.ID, newWeight uint64) error { return nil } - -func (n *noOpState) RegisterListener(interfaces.StateCallbackListener) {} - -func (n *noOpState) GetUptime( - nodeID ids.NodeID, -) (upDuration time.Duration, lastUpdated time.Time, err error) { - return 0, time.Time{}, nil -} - -func (n *noOpState) SetUptime( - nodeID ids.NodeID, - upDuration time.Duration, - lastUpdated time.Time, -) error { - return nil -} - -func (n *noOpState) GetStartTime( - nodeID ids.NodeID, -) (startTime time.Time, err error) { - return time.Time{}, nil -} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index b09aa99860..4966114884 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -39,10 +39,8 @@ import ( "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/peer" "github.com/ava-labs/subnet-evm/plugin/evm/message" - "github.com/ava-labs/subnet-evm/plugin/evm/uptime" - uptimeinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/uptime/interfaces" "github.com/ava-labs/subnet-evm/plugin/evm/validators" - validatorsinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" "github.com/ava-labs/subnet-evm/triedb" "github.com/ava-labs/subnet-evm/triedb/hashdb" @@ -77,8 +75,6 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - avalancheUptime "github.com/ava-labs/avalanchego/snow/uptime" - avalancheValidators "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/perms" "github.com/ava-labs/avalanchego/utils/profiler" "github.com/ava-labs/avalanchego/utils/timer/mockable" @@ -126,8 +122,6 @@ const ( txGossipThrottlingPeriod = 10 * time.Second txGossipThrottlingLimit = 2 txGossipPollSize = 1 - - loadValidatorsFrequency = 1 * time.Minute ) // Define the API endpoints for the VM @@ -237,7 +231,7 @@ type VM struct { client peer.NetworkClient networkCodec codec.Manager - validators *p2p.Validators + p2pValidators *p2p.Validators // Metrics sdkMetrics *prometheus.Registry @@ -259,8 +253,7 @@ type VM struct { ethTxPushGossiper avalancheUtils.Atomic[*gossip.PushGossiper[*GossipEthTx]] ethTxPullGossiper gossip.Gossiper - uptimeManager uptimeinterfaces.PausableManager - validatorState validatorsinterfaces.State + validatorsManager interfaces.Manager chainAlias string // RPC handlers (should be stopped before closing chaindb) @@ -489,20 +482,16 @@ func (vm *VM) Initialize( if err != nil { return fmt.Errorf("failed to initialize p2p network: %w", err) } - vm.validators = p2p.NewValidators(p2pNetwork.Peers, vm.ctx.Log, vm.ctx.SubnetID, vm.ctx.ValidatorState, maxValidatorSetStaleness) + vm.p2pValidators = p2p.NewValidators(p2pNetwork.Peers, vm.ctx.Log, vm.ctx.SubnetID, vm.ctx.ValidatorState, maxValidatorSetStaleness) vm.networkCodec = message.Codec vm.Network = peer.NewNetwork(p2pNetwork, appSender, vm.networkCodec, chainCtx.NodeID, vm.config.MaxOutboundActiveRequests) vm.client = peer.NewNetworkClient(vm.Network) - vm.validatorState, err = validators.NewState(vm.validatorsDB) + vm.validatorsManager, err = validators.NewManager(vm.ctx, vm.validatorsDB, &vm.clock) if err != nil { - return fmt.Errorf("failed to initialize validator state: %w", err) + return fmt.Errorf("failed to initialize validators manager: %w", err) } - // Initialize uptime manager - vm.uptimeManager = uptime.NewPausableManager(avalancheUptime.NewManager(vm.validatorState, &vm.clock)) - vm.validatorState.RegisterListener(vm.uptimeManager) - // Initialize warp backend offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) for i, hexMsg := range vm.config.WarpOffChainMessages { @@ -526,9 +515,7 @@ func (vm *VM) Initialize( vm.ctx.ChainID, vm.ctx.WarpSigner, vm, - vm.uptimeManager, - vm.validatorState, - vm.ctx.Lock.RLocker(), + vm.validatorsManager, vm.warpDB, meteredCache, offchainWarpMessages, @@ -727,28 +714,28 @@ func (vm *VM) onNormalOperationsStarted() error { ctx, cancel := context.WithCancel(context.TODO()) vm.cancel = cancel - // update validators first - if err := vm.performValidatorUpdate(ctx); err != nil { + // sync validators first + if err := vm.validatorsManager.Sync(ctx); err != nil { return fmt.Errorf("failed to update validators: %w", err) } - vdrIDs := vm.validatorState.GetNodeIDs().List() + vdrIDs := vm.validatorsManager.GetNodeIDs().List() // Then start tracking with updated validators // StartTracking initializes the uptime tracking with the known validators // and update their uptime to account for the time we were being offline. - if err := vm.uptimeManager.StartTracking(vdrIDs); err != nil { + if err := vm.validatorsManager.StartTracking(vdrIDs); err != nil { return fmt.Errorf("failed to start tracking uptime: %w", err) } // dispatch validator set update vm.shutdownWg.Add(1) go func() { - vm.dispatchUpdateValidators(ctx) + vm.validatorsManager.DispatchSync(ctx) vm.shutdownWg.Done() }() // Initialize goroutines related to block building // once we enter normal operation as there is no need to handle mempool gossip before this point. ethTxGossipMarshaller := GossipEthTxMarshaller{} - ethTxGossipClient := vm.Network.NewClient(p2p.TxGossipHandlerID, p2p.WithValidatorSampling(vm.validators)) + ethTxGossipClient := vm.Network.NewClient(p2p.TxGossipHandlerID, p2p.WithValidatorSampling(vm.p2pValidators)) ethTxGossipMetrics, err := gossip.NewMetrics(vm.sdkMetrics, ethTxGossipNamespace) if err != nil { return fmt.Errorf("failed to initialize eth tx gossip metrics: %w", err) @@ -778,7 +765,7 @@ func (vm *VM) onNormalOperationsStarted() error { ethTxPushGossiper, err = gossip.NewPushGossiper[*GossipEthTx]( ethTxGossipMarshaller, ethTxPool, - vm.validators, + vm.p2pValidators, ethTxGossipClient, ethTxGossipMetrics, pushGossipParams, @@ -808,7 +795,7 @@ func (vm *VM) onNormalOperationsStarted() error { txGossipTargetMessageSize, txGossipThrottlingPeriod, txGossipThrottlingLimit, - vm.validators, + vm.p2pValidators, ) } @@ -829,7 +816,7 @@ func (vm *VM) onNormalOperationsStarted() error { vm.ethTxPullGossiper = gossip.ValidatorGossiper{ Gossiper: ethTxPullGossiper, NodeID: vm.ctx.NodeID, - Validators: vm.validators, + Validators: vm.p2pValidators, } } @@ -874,11 +861,11 @@ func (vm *VM) Shutdown(context.Context) error { vm.cancel() } if vm.bootstrapped.Get() { - vdrIDs := vm.validatorState.GetNodeIDs().List() - if err := vm.uptimeManager.StopTracking(vdrIDs); err != nil { + vdrIDs := vm.validatorsManager.GetNodeIDs().List() + if err := vm.validatorsManager.StopTracking(vdrIDs); err != nil { return fmt.Errorf("failed to stop tracking uptime: %w", err) } - if err := vm.validatorState.WriteState(); err != nil { + if err := vm.validatorsManager.WriteState(); err != nil { return fmt.Errorf("failed to write validator: %w", err) } } @@ -1256,95 +1243,16 @@ func attachEthService(handler *rpc.Server, apis []rpc.API, names []string) error } func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, version *version.Application) error { - if err := vm.uptimeManager.Connect(nodeID); err != nil { + if err := vm.validatorsManager.Connect(nodeID); err != nil { return fmt.Errorf("uptime manager failed to connect node %s: %w", nodeID, err) } return vm.Network.Connected(ctx, nodeID, version) } func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { - if err := vm.uptimeManager.Disconnect(nodeID); err != nil { + if err := vm.validatorsManager.Disconnect(nodeID); err != nil { return fmt.Errorf("uptime manager failed to disconnect node %s: %w", nodeID, err) } return vm.Network.Disconnected(ctx, nodeID) } - -func (vm *VM) dispatchUpdateValidators(ctx context.Context) { - ticker := time.NewTicker(loadValidatorsFrequency) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - vm.ctx.Lock.Lock() - if err := vm.performValidatorUpdate(ctx); err != nil { - log.Error("failed to update validators", "error", err) - } - vm.ctx.Lock.Unlock() - case <-ctx.Done(): - return - } - } -} - -// performValidatorUpdate updates the validator state with the current validator set -// and writes the state to the database. -func (vm *VM) performValidatorUpdate(ctx context.Context) error { - now := time.Now() - log.Debug("performing validator update") - // get current validator set - currentValidatorSet, _, err := vm.ctx.ValidatorState.GetCurrentValidatorSet(ctx, vm.ctx.SubnetID) - if err != nil { - return fmt.Errorf("failed to get current validator set: %w", err) - } - - // load the current validator set into the validator state - if err := loadValidators(vm.validatorState, currentValidatorSet); err != nil { - return fmt.Errorf("failed to load current validators: %w", err) - } - - // write validators to the database - if err := vm.validatorState.WriteState(); err != nil { - return fmt.Errorf("failed to write validator state: %w", err) - } - - // TODO: add metrics - log.Debug("validator update complete", "duration", time.Since(now)) - return nil -} - -// loadValidators loads the [validators] into the validator state [validatorState] -func loadValidators(validatorState validatorsinterfaces.State, newValidators map[ids.ID]*avalancheValidators.GetCurrentValidatorOutput) error { - currentValidationIDs := validatorState.GetValidationIDs() - // first check if we need to delete any existing validators - for vID := range currentValidationIDs { - // if the validator is not in the new set of validators - // delete the validator - if _, exists := newValidators[vID]; !exists { - validatorState.DeleteValidator(vID) - } - } - - // then load the new validators - for newVID, newVdr := range newValidators { - currentVdr := validatorsinterfaces.Validator{ - ValidationID: newVID, - NodeID: newVdr.NodeID, - Weight: newVdr.Weight, - StartTimestamp: newVdr.StartTime, - IsActive: newVdr.IsActive, - IsSoV: newVdr.IsSoV, - } - if currentValidationIDs.Contains(newVID) { - if err := validatorState.UpdateValidator(currentVdr); err != nil { - return err - } - } else { - if err := validatorState.AddValidator(currentVdr); err != nil { - return err - } - } - } - return nil -} diff --git a/plugin/evm/vm_validators_state_test.go b/plugin/evm/vm_validators_state_test.go deleted file mode 100644 index 8bf8c3546f..0000000000 --- a/plugin/evm/vm_validators_state_test.go +++ /dev/null @@ -1,363 +0,0 @@ -package evm - -import ( - "context" - "testing" - "time" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" - commonEng "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/enginetest" - avagoValidators "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/snow/validators/validatorstest" - "github.com/ava-labs/subnet-evm/core" - "github.com/ava-labs/subnet-evm/plugin/evm/validators" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" - "github.com/ava-labs/subnet-evm/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestValidatorState(t *testing.T) { - require := require.New(t) - genesis := &core.Genesis{} - require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONLatest))) - genesisJSON, err := genesis.MarshalJSON() - require.NoError(err) - - vm := &VM{} - ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, string(genesisJSON)) - appSender := &enginetest.Sender{T: t} - appSender.CantSendAppGossip = true - testNodeIDs := []ids.NodeID{ - ids.GenerateTestNodeID(), - ids.GenerateTestNodeID(), - ids.GenerateTestNodeID(), - } - testValidationIDs := []ids.ID{ - ids.GenerateTestID(), - ids.GenerateTestID(), - ids.GenerateTestID(), - } - ctx.ValidatorState = &validatorstest.State{ - GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagoValidators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - PublicKey: nil, - Weight: 1, - }, - testValidationIDs[1]: { - NodeID: testNodeIDs[1], - PublicKey: nil, - Weight: 1, - }, - testValidationIDs[2]: { - NodeID: testNodeIDs[2], - PublicKey: nil, - Weight: 1, - }, - }, 0, nil - }, - } - appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil } - err = vm.Initialize( - context.Background(), - ctx, - dbManager, - genesisBytes, - []byte(""), - []byte(""), - issuer, - []*commonEng.Fx{}, - appSender, - ) - require.NoError(err, "error initializing GenesisVM") - - // Test case 1: state should not be populated until bootstrapped - require.NoError(vm.SetState(context.Background(), snow.Bootstrapping)) - require.Equal(0, vm.validatorState.GetValidationIDs().Len()) - _, _, err = vm.uptimeManager.CalculateUptime(testNodeIDs[0]) - require.ErrorIs(database.ErrNotFound, err) - require.False(vm.uptimeManager.StartedTracking()) - - // Test case 2: state should be populated after bootstrapped - require.NoError(vm.SetState(context.Background(), snow.NormalOp)) - require.Len(vm.validatorState.GetValidationIDs(), 3) - _, _, err = vm.uptimeManager.CalculateUptime(testNodeIDs[0]) - require.NoError(err) - require.True(vm.uptimeManager.StartedTracking()) - - // Test case 3: restarting VM should not lose state - vm.Shutdown(context.Background()) - // Shutdown should stop tracking - require.False(vm.uptimeManager.StartedTracking()) - - vm = &VM{} - err = vm.Initialize( - context.Background(), - utils.TestSnowContext(), // this context does not have validators state, making VM to source it from the database - dbManager, - genesisBytes, - []byte(""), - []byte(""), - issuer, - []*commonEng.Fx{}, - appSender, - ) - require.NoError(err, "error initializing GenesisVM") - require.Len(vm.validatorState.GetValidationIDs(), 3) - _, _, err = vm.uptimeManager.CalculateUptime(testNodeIDs[0]) - require.NoError(err) - require.False(vm.uptimeManager.StartedTracking()) - - // Test case 4: new validators should be added to the state - newValidationID := ids.GenerateTestID() - newNodeID := ids.GenerateTestNodeID() - testState := &validatorstest.State{ - GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagoValidators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - PublicKey: nil, - Weight: 1, - }, - testValidationIDs[1]: { - NodeID: testNodeIDs[1], - PublicKey: nil, - Weight: 1, - }, - testValidationIDs[2]: { - NodeID: testNodeIDs[2], - PublicKey: nil, - Weight: 1, - }, - newValidationID: { - NodeID: newNodeID, - PublicKey: nil, - Weight: 1, - }, - }, 0, nil - }, - } - vm.ctx.ValidatorState = testState - // set VM as bootstrapped - require.NoError(vm.SetState(context.Background(), snow.Bootstrapping)) - require.NoError(vm.SetState(context.Background(), snow.NormalOp)) - - // new validator should be added to the state eventually after validatorsLoadFrequency - require.EventuallyWithT(func(c *assert.CollectT) { - assert.Len(c, vm.validatorState.GetNodeIDs(), 4) - newValidator, err := vm.validatorState.GetValidator(newValidationID) - assert.NoError(c, err) - assert.Equal(c, newNodeID, newValidator.NodeID) - }, loadValidatorsFrequency*2, 5*time.Second) -} - -func TestLoadNewValidators(t *testing.T) { - testNodeIDs := []ids.NodeID{ - ids.GenerateTestNodeID(), - ids.GenerateTestNodeID(), - ids.GenerateTestNodeID(), - } - testValidationIDs := []ids.ID{ - ids.GenerateTestID(), - ids.GenerateTestID(), - ids.GenerateTestID(), - } - tests := []struct { - name string - initialValidators map[ids.ID]*avagoValidators.GetCurrentValidatorOutput - newValidators map[ids.ID]*avagoValidators.GetCurrentValidatorOutput - registerMockListenerCalls func(*interfaces.MockStateCallbackListener) - expectedLoadErr error - }{ - { - name: "before empty/after empty", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{}, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{}, - registerMockListenerCalls: func(*interfaces.MockStateCallbackListener) {}, - }, - { - name: "before empty/after one", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{}, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - }, - }, - { - name: "before one/after empty", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{}, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - // initial validator will trigger first - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - // then it will be removed - mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1) - }, - }, - { - name: "no change", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - }, - }, - { - name: "status and weight change and new one", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - Weight: 1, - }, - }, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: false, - StartTime: 0, - Weight: 2, - }, - testValidationIDs[1]: { - NodeID: testNodeIDs[1], - IsActive: true, - StartTime: 0, - }, - }, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - // initial validator will trigger first - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - // then it will be updated - mock.EXPECT().OnValidatorStatusUpdated(testValidationIDs[0], testNodeIDs[0], false).Times(1) - // new validator will be added - mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[1], uint64(0), true).Times(1) - }, - }, - { - name: "renew validation ID", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[1]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - // initial validator will trigger first - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - // then it will be removed - mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1) - // new validator will be added - mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[0], uint64(0), true).Times(1) - }, - }, - { - name: "renew node ID", - initialValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[0], - IsActive: true, - StartTime: 0, - }, - }, - newValidators: map[ids.ID]*avagoValidators.GetCurrentValidatorOutput{ - testValidationIDs[0]: { - NodeID: testNodeIDs[1], - IsActive: true, - StartTime: 0, - }, - }, - expectedLoadErr: validators.ErrImmutableField, - registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) { - // initial validator will trigger first - mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1) - // then it won't be called since we don't track the node ID changes - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - require := require.New(tt) - db := memdb.New() - validatorState, err := validators.NewState(db) - require.NoError(err) - - // set initial validators - for vID, validator := range test.initialValidators { - err := validatorState.AddValidator(interfaces.Validator{ - ValidationID: vID, - NodeID: validator.NodeID, - Weight: validator.Weight, - StartTimestamp: validator.StartTime, - IsActive: validator.IsActive, - IsSoV: validator.IsSoV, - }) - require.NoError(err) - } - // enable mock listener - ctrl := gomock.NewController(tt) - mockListener := interfaces.NewMockStateCallbackListener(ctrl) - test.registerMockListenerCalls(mockListener) - - validatorState.RegisterListener(mockListener) - // load new validators - err = loadValidators(validatorState, test.newValidators) - if test.expectedLoadErr != nil { - require.Error(err) - return - } - require.NoError(err) - // check if the state is as expected - require.Equal(len(test.newValidators), validatorState.GetValidationIDs().Len()) - for vID, validator := range test.newValidators { - v, err := validatorState.GetValidator(vID) - require.NoError(err) - require.Equal(validator.NodeID, v.NodeID) - require.Equal(validator.Weight, v.Weight) - require.Equal(validator.StartTime, v.StartTimestamp) - require.Equal(validator.IsActive, v.IsActive) - require.Equal(validator.IsSoV, v.IsSoV) - } - }) - } -} diff --git a/plugin/evm/vm_validators_test.go b/plugin/evm/vm_validators_test.go new file mode 100644 index 0000000000..d8d0719fde --- /dev/null +++ b/plugin/evm/vm_validators_test.go @@ -0,0 +1,161 @@ +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "testing" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + commonEng "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/enginetest" + avagovalidators "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/snow/validators/validatorstest" + "github.com/ava-labs/subnet-evm/core" + "github.com/ava-labs/subnet-evm/plugin/evm/validators" + "github.com/ava-labs/subnet-evm/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidatorState(t *testing.T) { + require := require.New(t) + genesis := &core.Genesis{} + require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONLatest))) + genesisJSON, err := genesis.MarshalJSON() + require.NoError(err) + + vm := &VM{} + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, string(genesisJSON)) + appSender := &enginetest.Sender{T: t} + appSender.CantSendAppGossip = true + testNodeIDs := []ids.NodeID{ + ids.GenerateTestNodeID(), + ids.GenerateTestNodeID(), + ids.GenerateTestNodeID(), + } + testValidationIDs := []ids.ID{ + ids.GenerateTestID(), + ids.GenerateTestID(), + ids.GenerateTestID(), + } + ctx.ValidatorState = &validatorstest.State{ + GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagovalidators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + PublicKey: nil, + Weight: 1, + }, + testValidationIDs[1]: { + NodeID: testNodeIDs[1], + PublicKey: nil, + Weight: 1, + }, + testValidationIDs[2]: { + NodeID: testNodeIDs[2], + PublicKey: nil, + Weight: 1, + }, + }, 0, nil + }, + } + appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil } + err = vm.Initialize( + context.Background(), + ctx, + dbManager, + genesisBytes, + []byte(""), + []byte(""), + issuer, + []*commonEng.Fx{}, + appSender, + ) + require.NoError(err, "error initializing GenesisVM") + + // Test case 1: state should not be populated until bootstrapped + require.NoError(vm.SetState(context.Background(), snow.Bootstrapping)) + require.Equal(0, vm.validatorsManager.GetValidationIDs().Len()) + _, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0]) + require.ErrorIs(database.ErrNotFound, err) + require.False(vm.validatorsManager.StartedTracking()) + + // Test case 2: state should be populated after bootstrapped + require.NoError(vm.SetState(context.Background(), snow.NormalOp)) + require.Len(vm.validatorsManager.GetValidationIDs(), 3) + _, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0]) + require.NoError(err) + require.True(vm.validatorsManager.StartedTracking()) + + // Test case 3: restarting VM should not lose state + vm.Shutdown(context.Background()) + // Shutdown should stop tracking + require.False(vm.validatorsManager.StartedTracking()) + + vm = &VM{} + err = vm.Initialize( + context.Background(), + utils.TestSnowContext(), // this context does not have validators state, making VM to source it from the database + dbManager, + genesisBytes, + []byte(""), + []byte(""), + issuer, + []*commonEng.Fx{}, + appSender, + ) + require.NoError(err, "error initializing GenesisVM") + require.Len(vm.validatorsManager.GetValidationIDs(), 3) + _, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0]) + require.NoError(err) + require.False(vm.validatorsManager.StartedTracking()) + + // Test case 4: new validators should be added to the state + newValidationID := ids.GenerateTestID() + newNodeID := ids.GenerateTestNodeID() + testState := &validatorstest.State{ + GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagovalidators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{ + testValidationIDs[0]: { + NodeID: testNodeIDs[0], + PublicKey: nil, + Weight: 1, + }, + testValidationIDs[1]: { + NodeID: testNodeIDs[1], + PublicKey: nil, + Weight: 1, + }, + testValidationIDs[2]: { + NodeID: testNodeIDs[2], + PublicKey: nil, + Weight: 1, + }, + newValidationID: { + NodeID: newNodeID, + PublicKey: nil, + Weight: 1, + }, + }, 0, nil + }, + } + // set VM as bootstrapped + require.NoError(vm.SetState(context.Background(), snow.Bootstrapping)) + require.NoError(vm.SetState(context.Background(), snow.NormalOp)) + + vm.ctx.ValidatorState = testState + + // new validator should be added to the state eventually after SyncFrequency + require.EventuallyWithT(func(c *assert.CollectT) { + vm.ctx.Lock.Lock() + defer vm.ctx.Lock.Unlock() + assert.Len(c, vm.validatorsManager.GetNodeIDs(), 4) + newValidator, err := vm.validatorsManager.GetValidator(newValidationID) + assert.NoError(c, err) + assert.Equal(c, newNodeID, newValidator.NodeID) + }, validators.SyncFrequency*2, 5*time.Second) +} diff --git a/scripts/mocks.mockgen.txt b/scripts/mocks.mockgen.txt index 43a9d60cad..aba87c80da 100644 --- a/scripts/mocks.mockgen.txt +++ b/scripts/mocks.mockgen.txt @@ -1,3 +1,3 @@ github.com/ava-labs/subnet-evm/precompile/precompileconfig=Predicater,Config,ChainConfig,Accepter=precompile/precompileconfig/mocks.go github.com/ava-labs/subnet-evm/precompile/contract=BlockContext,AccessibleState,StateDB=precompile/contract/mocks.go -github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces=StateCallbackListener=plugin/evm/validators/interfaces/mock_listener.go +github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces=StateCallbackListener=plugin/evm/validators/state/interfaces/mock_listener.go diff --git a/warp/backend.go b/warp/backend.go index eb38f1f410..84e62c7a57 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -7,14 +7,12 @@ import ( "context" "errors" "fmt" - "sync" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/uptime" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" @@ -59,9 +57,7 @@ type backend struct { db database.Database warpSigner avalancheWarp.Signer blockClient BlockClient - uptimeCalculator uptime.Calculator - validatorState interfaces.State - stateLock sync.Locker + validatorReader interfaces.ValidatorReader signatureCache cache.Cacher[ids.ID, []byte] messageCache *cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage @@ -74,9 +70,7 @@ func NewBackend( sourceChainID ids.ID, warpSigner avalancheWarp.Signer, blockClient BlockClient, - uptimeCalculator uptime.Calculator, - validatorsState interfaces.State, - stateLock sync.Locker, + validatorReader interfaces.ValidatorReader, db database.Database, signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, @@ -88,9 +82,7 @@ func NewBackend( warpSigner: warpSigner, blockClient: blockClient, signatureCache: signatureCache, - uptimeCalculator: uptimeCalculator, - validatorState: validatorsState, - stateLock: stateLock, + validatorReader: validatorReader, messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: messageCacheSize}, stats: newVerifierStats(), offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), diff --git a/warp/backend_test.go b/warp/backend_test.go index 331bd7c1ff..cb98a2351c 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -5,19 +5,16 @@ package warp import ( "context" - "sync" "testing" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" "github.com/ava-labs/subnet-evm/warp/warptest" "github.com/stretchr/testify/require" ) @@ -48,7 +45,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -71,7 +68,7 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) require.NoError(t, err) // Try getting a signature for a message that was not added. @@ -90,7 +87,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) @@ -117,7 +114,7 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -177,7 +174,7 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, test.offchainMessages) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { test.check(require, backend) diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index af664d5f16..aa265d9b33 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -10,12 +10,10 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/message" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" "github.com/ava-labs/subnet-evm/utils" "github.com/ava-labs/subnet-evm/warp" "github.com/ava-labs/subnet-evm/warp/warptest" @@ -35,7 +33,7 @@ func TestMessageSignatureHandler(t *testing.T) { require.NoError(t, err) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 100} - backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptime.NoOpCalculator, validatorstest.NoOpState, snowCtx.Lock.RLocker(), database, messageSignatureCache, [][]byte{offchainMessage.Bytes()}) + backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, warptest.NoOpValidatorReader{}, database, messageSignatureCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, []byte("test")) @@ -141,9 +139,7 @@ func TestBlockSignatureHandler(t *testing.T) { snowCtx.ChainID, warpSigner, blockClient, - uptime.NoOpCalculator, - validatorstest.NoOpState, - snowCtx.Lock.RLocker(), + warptest.NoOpValidatorReader{}, database, messageSignatureCache, nil, diff --git a/warp/verifier_backend.go b/warp/verifier_backend.go index 71a33356cc..255851d601 100644 --- a/warp/verifier_backend.go +++ b/warp/verifier_backend.go @@ -110,24 +110,11 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa } func (b *backend) verifyUptimeMessage(uptimeMsg *messages.ValidatorUptime) *common.AppError { - b.stateLock.Lock() - defer b.stateLock.Unlock() - // first get the validator's nodeID - vdr, err := b.validatorState.GetValidator(uptimeMsg.ValidationID) + vdr, currentUptime, _, err := b.validatorReader.GetValidatorAndUptime(uptimeMsg.ValidationID) if err != nil { return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get validator for validationID %s: %s", uptimeMsg.ValidationID, err.Error()), - } - } - nodeID := vdr.NodeID - - // then get the current uptime - currentUptime, _, err := b.uptimeCalculator.CalculateUptime(nodeID) - if err != nil { - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("failed to calculate uptime for nodeID %s: %s", nodeID, err.Error()), + Message: fmt.Sprintf("failed to get uptime for validationID %s: %s", uptimeMsg.ValidationID, err.Error()), } } @@ -136,7 +123,7 @@ func (b *backend) verifyUptimeMessage(uptimeMsg *messages.ValidatorUptime) *comm if currentUptimeSeconds < uptimeMsg.TotalUptime { return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, nodeID), + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, vdr.NodeID), } } diff --git a/warp/verifier_backend_test.go b/warp/verifier_backend_test.go index a3546f60c7..659c8b94b5 100644 --- a/warp/verifier_backend_test.go +++ b/warp/verifier_backend_test.go @@ -14,14 +14,12 @@ import ( "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/timer/mockable" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/validators" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" - "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" + stateinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" "github.com/ava-labs/subnet-evm/utils" "github.com/ava-labs/subnet-evm/warp/messages" "github.com/ava-labs/subnet-evm/warp/warptest" @@ -104,7 +102,7 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptime.NoOpCalculator, validatorstest.NoOpState, snowCtx.Lock.RLocker(), database, sigCache, [][]byte{offchainMessage.Bytes()}) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, warptest.NoOpValidatorReader{}, database, sigCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) @@ -219,9 +217,7 @@ func TestBlockSignatures(t *testing.T) { snowCtx.ChainID, warpSigner, blockClient, - uptime.NoOpCalculator, - validatorstest.NoOpState, - snowCtx.Lock.RLocker(), + warptest.NoOpValidatorReader{}, database, sigCache, nil, @@ -291,12 +287,12 @@ func TestUptimeSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - state, err := validators.NewState(memdb.New()) - require.NoError(t, err) + chainCtx := utils.TestSnowContext() clk := &mockable.Clock{} - uptimeManager := uptime.NewManager(state, clk) - uptimeManager.StartTracking([]ids.NodeID{}) - warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptimeManager, state, snowCtx.Lock.RLocker(), database, sigCache, nil) + validatorsManager, err := validators.NewManager(chainCtx, memdb.New(), clk) + require.NoError(t, err) + validatorsManager.StartTracking([]ids.NodeID{}) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, validatorsManager, database, sigCache, nil) require.NoError(t, err) handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) @@ -316,7 +312,7 @@ func TestUptimeSignatures(t *testing.T) { // uptime is less than requested (not connected) validationID := ids.GenerateTestID() nodeID := ids.GenerateTestNodeID() - require.NoError(t, state.AddValidator(interfaces.Validator{ + require.NoError(t, validatorsManager.AddValidator(stateinterfaces.Validator{ ValidationID: validationID, NodeID: nodeID, Weight: 1, @@ -330,7 +326,7 @@ func TestUptimeSignatures(t *testing.T) { require.Contains(t, appErr.Error(), "current uptime 0 is less than queried uptime 80") // uptime is less than requested (not enough) - require.NoError(t, uptimeManager.Connect(nodeID)) + require.NoError(t, validatorsManager.Connect(nodeID)) clk.Set(clk.Time().Add(40 * time.Second)) protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) diff --git a/warp/warptest/noop_validator_reader.go b/warp/warptest/noop_validator_reader.go new file mode 100644 index 0000000000..03171f84f5 --- /dev/null +++ b/warp/warptest/noop_validator_reader.go @@ -0,0 +1,21 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// warptest exposes common functionality for testing the warp package. +package warptest + +import ( + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + stateinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/state/interfaces" +) + +var _ interfaces.ValidatorReader = &NoOpValidatorReader{} + +type NoOpValidatorReader struct{} + +func (NoOpValidatorReader) GetValidatorAndUptime(ids.ID) (stateinterfaces.Validator, time.Duration, time.Time, error) { + return stateinterfaces.Validator{}, 0, time.Time{}, nil +}