diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 236bdf6a35a5..f9eb09ad611a 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -439,12 +439,11 @@ func (d *diff) Apply(baseState State) { } for _, subnetValidatorDiffs := range d.currentStakerDiffs.validatorDiffs { for _, validatorDiff := range subnetValidatorDiffs { - if validatorDiff.validatorModified { - if validatorDiff.validatorDeleted { - baseState.DeleteCurrentValidator(validatorDiff.validator) - } else { - baseState.PutCurrentValidator(validatorDiff.validator) - } + if validatorDiff.validatorAdded { + baseState.PutCurrentValidator(validatorDiff.validator) + } + if validatorDiff.validatorDeleted { + baseState.DeleteCurrentValidator(validatorDiff.validator) } addedDelegatorIterator := NewTreeIterator(validatorDiff.addedDelegators) @@ -460,12 +459,11 @@ func (d *diff) Apply(baseState State) { } for _, subnetValidatorDiffs := range d.pendingStakerDiffs.validatorDiffs { for _, validatorDiff := range subnetValidatorDiffs { - if validatorDiff.validatorModified { - if validatorDiff.validatorDeleted { - baseState.DeletePendingValidator(validatorDiff.validator) - } else { - baseState.PutPendingValidator(validatorDiff.validator) - } + if validatorDiff.validatorAdded { + baseState.PutPendingValidator(validatorDiff.validator) + } + if validatorDiff.validatorDeleted { + baseState.DeletePendingValidator(validatorDiff.validator) } addedDelegatorIterator := NewTreeIterator(validatorDiff.addedDelegators) diff --git a/vms/platformvm/state/diff_test.go b/vms/platformvm/state/diff_test.go index e4a74c9881c2..9d026668cb7a 100644 --- a/vms/platformvm/state/diff_test.go +++ b/vms/platformvm/state/diff_test.go @@ -109,6 +109,7 @@ func TestDiffCurrentValidator(t *testing.T) { d.DeleteCurrentValidator(currentValidator) // Make sure the deletion worked + state.EXPECT().GetCurrentValidator(currentValidator.SubnetID, currentValidator.NodeID).Return(nil, database.ErrNotFound).Times(1) _, err = d.GetCurrentValidator(currentValidator.SubnetID, currentValidator.NodeID) require.ErrorIs(err, database.ErrNotFound) } @@ -146,6 +147,7 @@ func TestDiffPendingValidator(t *testing.T) { d.DeletePendingValidator(pendingValidator) // Make sure the deletion worked + state.EXPECT().GetPendingValidator(pendingValidator.SubnetID, pendingValidator.NodeID).Return(nil, database.ErrNotFound).Times(1) _, err = d.GetPendingValidator(pendingValidator.SubnetID, pendingValidator.NodeID) require.ErrorIs(err, database.ErrNotFound) } diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index b3deae019741..8b65afd468ce 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -128,8 +128,7 @@ func (v *baseStakers) PutValidator(staker *Staker) { validator.validator = staker validatorDiff := v.getOrCreateValidatorDiff(staker.SubnetID, staker.NodeID) - validatorDiff.validatorModified = true - validatorDiff.validatorDeleted = false + validatorDiff.validatorAdded = true validatorDiff.validator = staker v.stakers.ReplaceOrInsert(staker) @@ -141,7 +140,6 @@ func (v *baseStakers) DeleteValidator(staker *Staker) { v.pruneValidator(staker.SubnetID, staker.NodeID) validatorDiff := v.getOrCreateValidatorDiff(staker.SubnetID, staker.NodeID) - validatorDiff.validatorModified = true validatorDiff.validatorDeleted = true validatorDiff.validator = staker @@ -249,8 +247,9 @@ type diffStakers struct { } type diffValidator struct { - validatorModified bool - // [validatorDeleted] implies [validatorModified] + // Invariant: [validatorAdded] and [validatorDeleted] will not be set at the + // same time. + validatorAdded bool validatorDeleted bool validator *Staker @@ -266,6 +265,8 @@ type diffValidator struct { // 2. If the validator was removed in this diff, [nil, true] will be returned. // 3. If the validator was not modified by this diff, [nil, false] will be // returned. +// +// Invariant: Assumes that the validator will never be removed and then added. func (s *diffStakers) GetValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, bool) { subnetValidatorDiffs, ok := s.validatorDiffs[subnetID] if !ok { @@ -277,20 +278,19 @@ func (s *diffStakers) GetValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, return nil, false } - if !validatorDiff.validatorModified { - return nil, false - } - - if validatorDiff.validatorDeleted { + switch { + case validatorDiff.validatorAdded: + return validatorDiff.validator, true + case validatorDiff.validatorDeleted: return nil, true + default: + return nil, false } - return validatorDiff.validator, true } func (s *diffStakers) PutValidator(staker *Staker) { validatorDiff := s.getOrCreateDiff(staker.SubnetID, staker.NodeID) - validatorDiff.validatorModified = true - validatorDiff.validatorDeleted = false + validatorDiff.validatorAdded = true validatorDiff.validator = staker if s.addedStakers == nil { @@ -301,14 +301,18 @@ func (s *diffStakers) PutValidator(staker *Staker) { func (s *diffStakers) DeleteValidator(staker *Staker) { validatorDiff := s.getOrCreateDiff(staker.SubnetID, staker.NodeID) - validatorDiff.validatorModified = true - validatorDiff.validatorDeleted = true - validatorDiff.validator = staker - - if s.deletedStakers == nil { - s.deletedStakers = make(map[ids.ID]*Staker) + if validatorDiff.validatorAdded { + validatorDiff.validatorAdded = false + s.addedStakers.Delete(validatorDiff.validator) + validatorDiff.validator = nil + } else { + validatorDiff.validatorDeleted = true + validatorDiff.validator = staker + if s.deletedStakers == nil { + s.deletedStakers = make(map[ids.ID]*Staker) + } + s.deletedStakers[staker.TxID] = staker } - s.deletedStakers[staker.TxID] = staker } func (s *diffStakers) GetDelegatorIterator( diff --git a/vms/platformvm/state/stakers_test.go b/vms/platformvm/state/stakers_test.go index d022d9cd1e75..d5650971eedd 100644 --- a/vms/platformvm/state/stakers_test.go +++ b/vms/platformvm/state/stakers_test.go @@ -166,14 +166,30 @@ func TestDiffStakersValidator(t *testing.T) { v.DeleteValidator(staker) - returnedStaker, ok = v.GetValidator(staker.SubnetID, staker.NodeID) - require.True(ok) - require.Nil(returnedStaker) + _, ok = v.GetValidator(staker.SubnetID, staker.NodeID) + require.False(ok) stakerIterator = v.GetStakerIterator(EmptyIterator) assertIteratorsEqual(t, NewSliceIterator(delegator), stakerIterator) } +func TestDiffStakersDeleteValidator(t *testing.T) { + require := require.New(t) + staker := newTestStaker() + delegator := newTestStaker() + + v := diffStakers{} + + _, ok := v.GetValidator(ids.GenerateTestID(), delegator.NodeID) + require.False(ok) + + v.DeleteValidator(staker) + + returnedStaker, ok := v.GetValidator(staker.SubnetID, staker.NodeID) + require.True(ok) + require.Nil(returnedStaker) +} + func TestDiffStakersDelegator(t *testing.T) { staker := newTestStaker() delegator := newTestStaker() diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index f959212c6e4b..dfa51a5eed05 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1592,58 +1592,55 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64) error // Copy [nodeID] so it doesn't get overwritten next iteration. nodeID := nodeID - var ( - weightDiff = &ValidatorWeightDiff{} - isNewValidator bool - ) - if validatorDiff.validatorModified { - // This validator is being added or removed. + weightDiff := &ValidatorWeightDiff{ + Decrease: validatorDiff.validatorDeleted, + } + switch { + case validatorDiff.validatorAdded: staker := validatorDiff.validator - - weightDiff.Decrease = validatorDiff.validatorDeleted weightDiff.Amount = staker.Weight - if validatorDiff.validatorDeleted { - // Invariant: Only the Primary Network contains non-nil - // public keys. - if staker.PublicKey != nil { - // Record the public key of the validator being removed. - pkDiffs[nodeID] = staker.PublicKey - - pkBytes := bls.PublicKeyToBytes(staker.PublicKey) - if err := pkDiffDB.Put(nodeID[:], pkBytes); err != nil { - return err - } - } + // The validator is being added. + vdr := &uptimeAndReward{ + txID: staker.TxID, + lastUpdated: staker.StartTime, - if err := validatorDB.Delete(staker.TxID[:]); err != nil { - return fmt.Errorf("failed to delete current staker: %w", err) - } + UpDuration: 0, + LastUpdated: uint64(staker.StartTime.Unix()), + PotentialReward: staker.PotentialReward, + } - s.validatorUptimes.DeleteUptime(nodeID, subnetID) - } else { - // The validator is being added. - vdr := &uptimeAndReward{ - txID: staker.TxID, - lastUpdated: staker.StartTime, - - UpDuration: 0, - LastUpdated: uint64(staker.StartTime.Unix()), - PotentialReward: staker.PotentialReward, - } + vdrBytes, err := blocks.GenesisCodec.Marshal(blocks.Version, vdr) + if err != nil { + return fmt.Errorf("failed to serialize current validator: %w", err) + } - vdrBytes, err := blocks.GenesisCodec.Marshal(blocks.Version, vdr) - if err != nil { - return fmt.Errorf("failed to serialize current validator: %w", err) - } + if err = validatorDB.Put(staker.TxID[:], vdrBytes); err != nil { + return fmt.Errorf("failed to write current validator to list: %w", err) + } - if err = validatorDB.Put(staker.TxID[:], vdrBytes); err != nil { - return fmt.Errorf("failed to write current validator to list: %w", err) + s.validatorUptimes.LoadUptime(nodeID, subnetID, vdr) + case validatorDiff.validatorDeleted: + staker := validatorDiff.validator + weightDiff.Amount = staker.Weight + + // Invariant: Only the Primary Network contains non-nil + // public keys. + if staker.PublicKey != nil { + // Record the public key of the validator being removed. + pkDiffs[nodeID] = staker.PublicKey + + pkBytes := bls.PublicKeyToBytes(staker.PublicKey) + if err := pkDiffDB.Put(nodeID[:], pkBytes); err != nil { + return err } + } - s.validatorUptimes.LoadUptime(nodeID, subnetID, vdr) - isNewValidator = true + if err := validatorDB.Delete(staker.TxID[:]); err != nil { + return fmt.Errorf("failed to delete current staker: %w", err) } + + s.validatorUptimes.DeleteUptime(nodeID, subnetID) } err := writeCurrentDelegatorDiff( @@ -1683,7 +1680,7 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64) error if weightDiff.Decrease { err = validators.RemoveWeight(s.cfg.Validators, subnetID, nodeID, weightDiff.Amount) } else { - if isNewValidator { + if validatorDiff.validatorAdded { staker := validatorDiff.validator err = validators.Add( s.cfg.Validators, @@ -1781,17 +1778,16 @@ func writePendingDiff( pendingDelegatorList linkeddb.LinkedDB, validatorDiff *diffValidator, ) error { - if validatorDiff.validatorModified { - staker := validatorDiff.validator - - var err error - if validatorDiff.validatorDeleted { - err = pendingValidatorList.Delete(staker.TxID[:]) - } else { - err = pendingValidatorList.Put(staker.TxID[:], nil) + if validatorDiff.validatorAdded { + err := pendingValidatorList.Put(validatorDiff.validator.TxID[:], nil) + if err != nil { + return fmt.Errorf("failed to add pending validator: %w", err) } + } + if validatorDiff.validatorDeleted { + err := pendingValidatorList.Delete(validatorDiff.validator.TxID[:]) if err != nil { - return fmt.Errorf("failed to update pending validator: %w", err) + return fmt.Errorf("failed to delete pending validator: %w", err) } } diff --git a/vms/platformvm/vm_regression_test.go b/vms/platformvm/vm_regression_test.go index ac4c5a8279e8..88c60a430d10 100644 --- a/vms/platformvm/vm_regression_test.go +++ b/vms/platformvm/vm_regression_test.go @@ -1252,3 +1252,239 @@ func TestAddDelegatorTxAddBeforeRemove(t *testing.T) { // total stake weight would go over the limit. require.Error(vm.Builder.AddUnverifiedTx(addSecondDelegatorTx)) } + +func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t *testing.T) { + require := require.New(t) + + validatorStartTime := banffForkTime.Add(txexecutor.SyncBound).Add(1 * time.Second) + validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) + + vm, _, _ := defaultVM() + + vm.ctx.Lock.Lock() + defer func() { + err := vm.Shutdown(context.Background()) + require.NoError(err) + + vm.ctx.Lock.Unlock() + }() + + key, err := testKeyFactory.NewPrivateKey() + require.NoError(err) + + id := key.PublicKey().Address() + changeAddr := keys[0].PublicKey().Address() + + addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + id, + reward.PercentDenominator, + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(addValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + addValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(addValidatorBlock.Verify(context.Background())) + require.NoError(addValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( + 1, + []ids.ShortID{changeAddr}, + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(createSubnetTx) + require.NoError(err) + + // trigger block creation for the subnet tx + createSubnetBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(createSubnetBlock.Verify(context.Background())) + require.NoError(createSubnetBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(addSubnetValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + addSubnetValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(addSubnetValidatorBlock.Verify(context.Background())) + require.NoError(addSubnetValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + emptyValidatorSet, err := vm.GetValidatorSet( + context.Background(), + addSubnetValidatorBlock.Height(), + createSubnetTx.ID(), + ) + require.NoError(err) + require.Empty(emptyValidatorSet) + + removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + // Set the clock so that the validator will be moved from the pending + // validator set into the current validator set. + vm.clock.Set(validatorStartTime) + + err = vm.Builder.AddUnverifiedTx(removeSubnetValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + removeSubnetValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(removeSubnetValidatorBlock.Verify(context.Background())) + require.NoError(removeSubnetValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + emptyValidatorSet, err = vm.GetValidatorSet( + context.Background(), + addSubnetValidatorBlock.Height(), + createSubnetTx.ID(), + ) + require.NoError(err) + require.Empty(emptyValidatorSet) +} + +func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *testing.T) { + require := require.New(t) + + validatorStartTime := banffForkTime.Add(txexecutor.SyncBound).Add(1 * time.Second) + validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) + + vm, _, _ := defaultVM() + + vm.ctx.Lock.Lock() + defer func() { + err := vm.Shutdown(context.Background()) + require.NoError(err) + + vm.ctx.Lock.Unlock() + }() + + key, err := testKeyFactory.NewPrivateKey() + require.NoError(err) + + id := key.PublicKey().Address() + changeAddr := keys[0].PublicKey().Address() + + addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + id, + reward.PercentDenominator, + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(addValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + addValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(addValidatorBlock.Verify(context.Background())) + require.NoError(addValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( + 1, + []ids.ShortID{changeAddr}, + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(createSubnetTx) + require.NoError(err) + + // trigger block creation for the subnet tx + createSubnetBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(createSubnetBlock.Verify(context.Background())) + require.NoError(createSubnetBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + vm.TrackedSubnets.Add(createSubnetTx.ID()) + subnetValidators := validators.NewSet() + err = vm.state.ValidatorSet(createSubnetTx.ID(), subnetValidators) + require.NoError(err) + + added := vm.Validators.Add(createSubnetTx.ID(), subnetValidators) + require.True(added) + + addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(addSubnetValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + addSubnetValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(addSubnetValidatorBlock.Verify(context.Background())) + require.NoError(addSubnetValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{keys[0], keys[1]}, + changeAddr, + ) + require.NoError(err) + + // Set the clock so that the validator will be moved from the pending + // validator set into the current validator set. + vm.clock.Set(validatorStartTime) + + err = vm.Builder.AddUnverifiedTx(removeSubnetValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + removeSubnetValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(removeSubnetValidatorBlock.Verify(context.Background())) + require.NoError(removeSubnetValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) +} diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 8d305a597e58..75a908077b05 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -2826,3 +2826,106 @@ func copySubnetValidator(vdr *validators.Validator) *validators.Validator { newVdr.PublicKey = nil return &newVdr } + +func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { + require := require.New(t) + + validatorStartTime := banffForkTime.Add(txexecutor.SyncBound).Add(1 * time.Second) + validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) + + vm, _, _ := defaultVM() + + vm.ctx.Lock.Lock() + defer func() { + err := vm.Shutdown(context.Background()) + require.NoError(err) + + vm.ctx.Lock.Unlock() + }() + + keyIntf, err := testKeyFactory.NewPrivateKey() + require.NoError(err) + key := keyIntf.(*crypto.PrivateKeySECP256K1R) + + id := key.PublicKey().Address() + + addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + id, + reward.PercentDenominator, + []*crypto.PrivateKeySECP256K1R{keys[0]}, + keys[0].Address(), + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(addValidatorTx) + require.NoError(err) + + // trigger block creation for the validator tx + addValidatorBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(addValidatorBlock.Verify(context.Background())) + require.NoError(addValidatorBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( + 1, + []ids.ShortID{id}, + []*crypto.PrivateKeySECP256K1R{keys[0]}, + keys[0].Address(), + ) + require.NoError(err) + + err = vm.Builder.AddUnverifiedTx(createSubnetTx) + require.NoError(err) + + // trigger block creation for the subnet tx + createSubnetBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + require.NoError(createSubnetBlock.Verify(context.Background())) + require.NoError(createSubnetBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + defaultMaxValidatorStake, + uint64(validatorStartTime.Unix()), + uint64(validatorEndTime.Unix()), + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{key, keys[1]}, + keys[1].Address(), + ) + require.NoError(err) + + removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + ids.NodeID(id), + createSubnetTx.ID(), + []*crypto.PrivateKeySECP256K1R{key, keys[2]}, + keys[2].Address(), + ) + require.NoError(err) + + statelessBlock, err := blocks.NewBanffStandardBlock( + vm.state.GetTimestamp(), + createSubnetBlock.ID(), + createSubnetBlock.Height()+1, + []*txs.Tx{ + addSubnetValidatorTx, + removeSubnetValidatorTx, + }, + ) + require.NoError(err) + + blockBytes := statelessBlock.Bytes() + block, err := vm.ParseBlock(context.Background(), blockBytes) + require.NoError(err) + require.NoError(block.Verify(context.Background())) + require.NoError(block.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + _, err = vm.state.GetPendingValidator(createSubnetTx.ID(), ids.NodeID(id)) + require.ErrorIs(err, database.ErrNotFound) +}