diff --git a/snow/engine/snowman/block/block_context_vm.go b/snow/engine/snowman/block/block_context_vm.go index 4efc66ae16b1..e0d8436a7274 100644 --- a/snow/engine/snowman/block/block_context_vm.go +++ b/snow/engine/snowman/block/block_context_vm.go @@ -8,6 +8,7 @@ package block import ( "context" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" ) @@ -39,6 +40,12 @@ type BuildBlockWithContextChainVM interface { BuildBlockWithContext(ctx context.Context, blockCtx *Context) (snowman.Block, error) } +// SetPreferenceWithContextChainVM defines the interface a ChainVM can optionally +// implement to consider the P-Chain height when setting preference. +type SetPreferenceWithContextChainVM interface { + SetPreferenceWithContext(ctx context.Context, blkID ids.ID, blockCtx *Context) error +} + // WithVerifyContext defines the interface a Block can optionally implement to // consider the P-Chain height when verifying itself. // diff --git a/snow/engine/snowman/block/blocktest/set_preference_vm.go b/snow/engine/snowman/block/blocktest/set_preference_vm.go new file mode 100644 index 000000000000..0c2944a76428 --- /dev/null +++ b/snow/engine/snowman/block/blocktest/set_preference_vm.go @@ -0,0 +1,42 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package blocktest + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +var ( + errSetPreferenceWithContext = errors.New("unexpectedly called SetPreferenceWithContext") + + _ block.SetPreferenceWithContextChainVM = (*SetPreferenceVM)(nil) +) + +type SetPreferenceVM struct { + T *testing.T + + CantSetPreferenceWithContext bool + SetPreferenceWithContextF func(context.Context, ids.ID, *block.Context) error +} + +func (vm *SetPreferenceVM) Default(cant bool) { + vm.CantSetPreferenceWithContext = cant +} + +func (vm *SetPreferenceVM) SetPreferenceWithContext(ctx context.Context, id ids.ID, blockCtx *block.Context) error { + if vm.SetPreferenceWithContextF != nil { + return vm.SetPreferenceWithContextF(ctx, id, blockCtx) + } + if vm.CantSetPreferenceWithContext && vm.T != nil { + require.FailNow(vm.T, errSetPreferenceWithContext.Error()) + } + return errSetPreferenceWithContext +} diff --git a/vms/metervm/block_metrics.go b/vms/metervm/block_metrics.go index 0461035509cc..b1c1887274db 100644 --- a/vms/metervm/block_metrics.go +++ b/vms/metervm/block_metrics.go @@ -32,6 +32,8 @@ type blockMetrics struct { // Block building with context metrics buildBlockWithContext, buildBlockWithContextErr, + // Setting preference with context metrics + setPreferenceWithContext, // Batched metrics getAncestors, batchedParseBlock, @@ -47,6 +49,7 @@ type blockMetrics struct { func (m *blockMetrics) Initialize( supportsBlockBuildingWithContext bool, + supportsSettingPreferenceWithContext bool, supportsBatchedFetching bool, supportsStateSync bool, reg prometheus.Registerer, @@ -73,6 +76,9 @@ func (m *blockMetrics) Initialize( m.buildBlockWithContext = newAverager("build_block_with_context", reg, &errs) m.buildBlockWithContextErr = newAverager("build_block_with_context_err", reg, &errs) } + if supportsSettingPreferenceWithContext { + m.setPreferenceWithContext = newAverager("set_preference_with_context", reg, &errs) + } if supportsBatchedFetching { m.getAncestors = newAverager("get_ancestors", reg, &errs) m.batchedParseBlock = newAverager("batched_parse_block", reg, &errs) diff --git a/vms/metervm/block_vm.go b/vms/metervm/block_vm.go index f6672e994230..b15b0c1239a4 100644 --- a/vms/metervm/block_vm.go +++ b/vms/metervm/block_vm.go @@ -18,17 +18,19 @@ import ( ) var ( - _ block.ChainVM = (*blockVM)(nil) - _ block.BuildBlockWithContextChainVM = (*blockVM)(nil) - _ block.BatchedChainVM = (*blockVM)(nil) - _ block.StateSyncableVM = (*blockVM)(nil) + _ block.ChainVM = (*blockVM)(nil) + _ block.BuildBlockWithContextChainVM = (*blockVM)(nil) + _ block.SetPreferenceWithContextChainVM = (*blockVM)(nil) + _ block.BatchedChainVM = (*blockVM)(nil) + _ block.StateSyncableVM = (*blockVM)(nil) ) type blockVM struct { block.ChainVM - buildBlockVM block.BuildBlockWithContextChainVM - batchedVM block.BatchedChainVM - ssVM block.StateSyncableVM + buildBlockVM block.BuildBlockWithContextChainVM + setPreferenceVM block.SetPreferenceWithContextChainVM + batchedVM block.BatchedChainVM + ssVM block.StateSyncableVM blockMetrics registry prometheus.Registerer @@ -39,14 +41,16 @@ func NewBlockVM( reg prometheus.Registerer, ) block.ChainVM { buildBlockVM, _ := vm.(block.BuildBlockWithContextChainVM) + setPreferenceVM, _ := vm.(block.SetPreferenceWithContextChainVM) batchedVM, _ := vm.(block.BatchedChainVM) ssVM, _ := vm.(block.StateSyncableVM) return &blockVM{ - ChainVM: vm, - buildBlockVM: buildBlockVM, - batchedVM: batchedVM, - ssVM: ssVM, - registry: reg, + ChainVM: vm, + buildBlockVM: buildBlockVM, + setPreferenceVM: setPreferenceVM, + batchedVM: batchedVM, + ssVM: ssVM, + registry: reg, } } @@ -62,6 +66,7 @@ func (vm *blockVM) Initialize( ) error { err := vm.blockMetrics.Initialize( vm.buildBlockVM != nil, + vm.setPreferenceVM != nil, vm.batchedVM != nil, vm.ssVM != nil, vm.registry, diff --git a/vms/metervm/set_preference_with_context_vm.go b/vms/metervm/set_preference_with_context_vm.go new file mode 100644 index 000000000000..c39fffd06faa --- /dev/null +++ b/vms/metervm/set_preference_with_context_vm.go @@ -0,0 +1,23 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metervm + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +func (vm *blockVM) SetPreferenceWithContext(ctx context.Context, blkID ids.ID, blockCtx *block.Context) error { + if vm.setPreferenceVM == nil { + return vm.SetPreference(ctx, blkID) + } + + start := time.Now() + err := vm.setPreferenceVM.SetPreferenceWithContext(ctx, blkID, blockCtx) + vm.blockMetrics.setPreferenceWithContext.Observe(float64(time.Since(start))) + return err +} diff --git a/vms/platformvm/block/builder/builder_test.go b/vms/platformvm/block/builder/builder_test.go index ba99b8756d05..74f6b298e7d7 100644 --- a/vms/platformvm/block/builder/builder_test.go +++ b/vms/platformvm/block/builder/builder_test.go @@ -157,7 +157,7 @@ func TestBuildBlockShouldReward(t *testing.T) { require.Equal([]*txs.Tx{tx}, blk.(*blockexecutor.Block).Block.Txs()) require.NoError(blk.Verify(context.Background())) require.NoError(blk.Accept(context.Background())) - env.blkManager.SetPreference(blk.ID()) + env.blkManager.SetPreference(blk.ID(), nil) // Validator should now be current staker, err := env.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) @@ -196,7 +196,7 @@ func TestBuildBlockShouldReward(t *testing.T) { require.NoError(blk.Accept(context.Background())) require.NoError(commit.Verify(context.Background())) require.NoError(commit.Accept(context.Background())) - env.blkManager.SetPreference(commit.ID()) + env.blkManager.SetPreference(commit.ID(), nil) // Stop rewarding once our staker is rewarded if staker.TxID == txID { @@ -459,7 +459,7 @@ func TestNoErrorOnUnexpectedSetPreferenceDuringBootstrapping(t *testing.T) { defer env.ctx.Lock.Unlock() env.isBootstrapped.Set(false) - env.blkManager.SetPreference(ids.GenerateTestID()) // should not panic + env.blkManager.SetPreference(ids.GenerateTestID(), nil) // should not panic } func TestGetNextStakerToReward(t *testing.T) { diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index 39782e7d3ce1..b6d83a478e70 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -178,7 +178,7 @@ func newEnvironment(t *testing.T, f upgradetest.Fork) *environment { //nolint:un res.blkManager, ) - res.blkManager.SetPreference(genesisID) + res.blkManager.SetPreference(genesisID, nil) addSubnet(t, res) t.Cleanup(func() { diff --git a/vms/platformvm/block/executor/executormock/manager.go b/vms/platformvm/block/executor/executormock/manager.go index 589cd742078a..4e25b7a9f1e5 100644 --- a/vms/platformvm/block/executor/executormock/manager.go +++ b/vms/platformvm/block/executor/executormock/manager.go @@ -14,8 +14,9 @@ import ( ids "github.com/ava-labs/avalanchego/ids" snowman "github.com/ava-labs/avalanchego/snow/consensus/snowman" + block "github.com/ava-labs/avalanchego/snow/engine/snowman/block" set "github.com/ava-labs/avalanchego/utils/set" - block "github.com/ava-labs/avalanchego/vms/platformvm/block" + block0 "github.com/ava-labs/avalanchego/vms/platformvm/block" state "github.com/ava-labs/avalanchego/vms/platformvm/state" txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" gomock "go.uber.org/mock/gomock" @@ -76,10 +77,10 @@ func (mr *ManagerMockRecorder) GetState(blkID any) *gomock.Call { } // GetStatelessBlock mocks base method. -func (m *Manager) GetStatelessBlock(blkID ids.ID) (block.Block, error) { +func (m *Manager) GetStatelessBlock(blkID ids.ID) (block0.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStatelessBlock", blkID) - ret0, _ := ret[0].(block.Block) + ret0, _ := ret[0].(block0.Block) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -105,7 +106,7 @@ func (mr *ManagerMockRecorder) LastAccepted() *gomock.Call { } // NewBlock mocks base method. -func (m *Manager) NewBlock(arg0 block.Block) snowman.Block { +func (m *Manager) NewBlock(arg0 block0.Block) snowman.Block { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewBlock", arg0) ret0, _ := ret[0].(snowman.Block) @@ -133,15 +134,15 @@ func (mr *ManagerMockRecorder) Preferred() *gomock.Call { } // SetPreference mocks base method. -func (m *Manager) SetPreference(blkID ids.ID) { +func (m *Manager) SetPreference(blkID ids.ID, blockCtx *block.Context) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetPreference", blkID) + m.ctrl.Call(m, "SetPreference", blkID, blockCtx) } // SetPreference indicates an expected call of SetPreference. -func (mr *ManagerMockRecorder) SetPreference(blkID any) *gomock.Call { +func (mr *ManagerMockRecorder) SetPreference(blkID, blockCtx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPreference", reflect.TypeOf((*Manager)(nil).SetPreference), blkID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPreference", reflect.TypeOf((*Manager)(nil).SetPreference), blkID, blockCtx) } // VerifyTx mocks base method. diff --git a/vms/platformvm/block/executor/manager.go b/vms/platformvm/block/executor/manager.go index 0d56cae69c6b..572d9cf6eb76 100644 --- a/vms/platformvm/block/executor/manager.go +++ b/vms/platformvm/block/executor/manager.go @@ -19,6 +19,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/validators" "github.com/ava-labs/avalanchego/vms/txs/mempool" + + snowmanblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) var ( @@ -34,7 +36,7 @@ type Manager interface { // Returns the ID of the most recently accepted block. LastAccepted() ids.ID - SetPreference(blkID ids.ID) + SetPreference(blkID ids.ID, blockCtx *snowmanblock.Context) Preferred() ids.ID GetBlock(blkID ids.ID) (snowman.Block, error) @@ -88,6 +90,7 @@ type manager struct { rejector block.Visitor preferred ids.ID + preferredCtx *snowmanblock.Context txExecutorBackend *executor.Backend } @@ -110,8 +113,9 @@ func (m *manager) NewBlock(blk block.Block) snowman.Block { } } -func (m *manager) SetPreference(blkID ids.ID) { +func (m *manager) SetPreference(blkID ids.ID, blockCtx *snowmanblock.Context) { m.preferred = blkID + m.preferredCtx = blockCtx } func (m *manager) Preferred() ids.ID { @@ -132,9 +136,17 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { } } - recommendedPChainHeight, err := m.ctx.ValidatorState.GetMinimumHeight(context.TODO()) - if err != nil { - return fmt.Errorf("failed to fetch P-chain height: %w", err) + var ( + recommendedPChainHeight uint64 + err error + ) + if m.preferredCtx != nil { + recommendedPChainHeight = m.preferredCtx.PChainHeight + } else { + recommendedPChainHeight, err = m.ctx.ValidatorState.GetMinimumHeight(context.TODO()) + if err != nil { + return fmt.Errorf("failed to fetch P-chain height: %w", err) + } } err = executor.VerifyWarpMessages( context.TODO(), diff --git a/vms/platformvm/block/executor/manager_test.go b/vms/platformvm/block/executor/manager_test.go index b70be6db7414..eff32808a0c3 100644 --- a/vms/platformvm/block/executor/manager_test.go +++ b/vms/platformvm/block/executor/manager_test.go @@ -13,6 +13,8 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/state" + + snowmanblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) func TestGetBlock(t *testing.T) { @@ -82,6 +84,25 @@ func TestManagerSetPreference(t *testing.T) { require.Equal(initialPreference, manager.Preferred()) newPreference := ids.GenerateTestID() - manager.SetPreference(newPreference) + manager.SetPreference(newPreference, nil) + require.Equal(newPreference, manager.Preferred()) +} + +func TestManagerSetPreferenceWithContext(t *testing.T) { + require := require.New(t) + + initialPreference := ids.GenerateTestID() + manager := &manager{ + preferred: initialPreference, + } + require.Equal(initialPreference, manager.Preferred()) + require.Nil(manager.preferredCtx) + + newPreference := ids.GenerateTestID() + newContext := &snowmanblock.Context{ + PChainHeight: 100, + } + manager.SetPreference(newPreference, newContext) require.Equal(newPreference, manager.Preferred()) + require.Equal(newContext, manager.preferredCtx) } diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index 3eb05a804eca..0f307ced924e 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -1372,7 +1372,7 @@ func TestAddValidatorProposalBlock(t *testing.T) { blk := env.blkManager.NewBlock(statelessBlk) require.NoError(blk.Verify(context.Background())) require.NoError(blk.Accept(context.Background())) - env.blkManager.SetPreference(statelessBlk.ID()) + env.blkManager.SetPreference(statelessBlk.ID(), nil) // Should be current staker, err := env.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) @@ -1405,7 +1405,7 @@ func TestAddValidatorProposalBlock(t *testing.T) { blk = env.blkManager.NewBlock(statelessBlk) require.NoError(blk.Verify(context.Background())) require.NoError(blk.Accept(context.Background())) - env.blkManager.SetPreference(statelessBlk.ID()) + env.blkManager.SetPreference(statelessBlk.ID(), nil) } env.clk.Set(validatorEndTime) diff --git a/vms/platformvm/vm.go b/vms/platformvm/vm.go index a38d29746e55..828847397a2f 100644 --- a/vms/platformvm/vm.go +++ b/vms/platformvm/vm.go @@ -52,10 +52,11 @@ import ( ) var ( - _ snowmanblock.ChainVM = (*VM)(nil) - _ snowmanblock.BuildBlockWithContextChainVM = (*VM)(nil) - _ secp256k1fx.VM = (*VM)(nil) - _ validators.State = (*VM)(nil) + _ snowmanblock.ChainVM = (*VM)(nil) + _ snowmanblock.BuildBlockWithContextChainVM = (*VM)(nil) + _ snowmanblock.SetPreferenceWithContextChainVM = (*VM)(nil) + _ secp256k1fx.VM = (*VM)(nil) + _ validators.State = (*VM)(nil) ) type VM struct { @@ -425,7 +426,12 @@ func (vm *VM) LastAccepted(context.Context) (ids.ID, error) { // SetPreference sets the preferred block to be the one with ID [blkID] func (vm *VM) SetPreference(_ context.Context, blkID ids.ID) error { - vm.manager.SetPreference(blkID) + vm.manager.SetPreference(blkID, nil) + return nil +} + +func (vm *VM) SetPreferenceWithContext(_ context.Context, blkID ids.ID, blockCtx *snowmanblock.Context) error { + vm.manager.SetPreference(blkID, blockCtx) return nil } diff --git a/vms/proposervm/vm.go b/vms/proposervm/vm.go index eff3f3abeefc..c187409dd0ae 100644 --- a/vms/proposervm/vm.go +++ b/vms/proposervm/vm.go @@ -35,6 +35,7 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/tree" "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/proposervm/acp181" "github.com/ava-labs/avalanchego/vms/proposervm/proposer" "github.com/ava-labs/avalanchego/vms/proposervm/state" @@ -70,9 +71,10 @@ func cachedBlockSize(_ ids.ID, blk snowman.Block) int { type VM struct { block.ChainVM Config - blockBuilderVM block.BuildBlockWithContextChainVM - batchedVM block.BatchedChainVM - ssVM block.StateSyncableVM + blockBuilderVM block.BuildBlockWithContextChainVM + setPreferenceVM block.SetPreferenceWithContextChainVM + batchedVM block.BatchedChainVM + ssVM block.StateSyncableVM state.State @@ -123,14 +125,16 @@ func New( config Config, ) *VM { blockBuilderVM, _ := vm.(block.BuildBlockWithContextChainVM) + setPreferenceVM, _ := vm.(block.SetPreferenceWithContextChainVM) batchedVM, _ := vm.(block.BatchedChainVM) ssVM, _ := vm.(block.StateSyncableVM) return &VM{ - ChainVM: vm, - Config: config, - blockBuilderVM: blockBuilderVM, - batchedVM: batchedVM, - ssVM: ssVM, + ChainVM: vm, + Config: config, + blockBuilderVM: blockBuilderVM, + setPreferenceVM: setPreferenceVM, + batchedVM: batchedVM, + ssVM: ssVM, } } @@ -370,7 +374,51 @@ func (vm *VM) SetPreference(ctx context.Context, preferred ids.ID) error { return vm.ChainVM.SetPreference(ctx, preferred) } + preferredEpoch, err := blk.pChainEpoch(ctx) + if err != nil { + return err + } + innerBlkID := blk.getInnerBlk().ID() + + // If the inner VM implements SetPreferenceWithContext, use it to set the + // preference with the P-Chain height to be used to verify a child of the + // preferred block. + if vm.setPreferenceVM != nil && preferredEpoch != (statelessblock.Epoch{}) { + // The P-Chain height used to verify a child of the preferred block will + // potentially be different than the P-Chain height used to verify the + // preferred block if the preferred block seals the current epoch. + preferredPChainHeight, err := blk.pChainHeight(ctx) + if err != nil { + return err + } + + // The exact child timestamp doesn't matter here because we know Granite + // is already activated. + timestamp := blk.Timestamp() + nextEpoch := acp181.NewEpoch( + vm.Upgrades, + preferredPChainHeight, + preferredEpoch, + timestamp, + timestamp, + ) + nextPChainHeight := nextEpoch.PChainHeight + + if err := vm.setPreferenceVM.SetPreferenceWithContext(ctx, innerBlkID, &block.Context{ + PChainHeight: nextPChainHeight, + }); err != nil { + return err + } + + vm.ctx.Log.Debug("set preference with context", + zap.Stringer("blkID", preferred), + zap.Stringer("innerBlkID", innerBlkID), + zap.Uint64("pChainHeight", nextPChainHeight), + ) + return nil + } + if err := vm.ChainVM.SetPreference(ctx, innerBlkID); err != nil { return err } diff --git a/vms/proposervm/vm_test.go b/vms/proposervm/vm_test.go index 99c3e5e2199d..2dbcfb88ecfd 100644 --- a/vms/proposervm/vm_test.go +++ b/vms/proposervm/vm_test.go @@ -45,13 +45,15 @@ import ( ) var ( - _ block.ChainVM = (*fullVM)(nil) - _ block.StateSyncableVM = (*fullVM)(nil) + _ block.ChainVM = (*fullVM)(nil) + _ block.StateSyncableVM = (*fullVM)(nil) + _ block.SetPreferenceWithContextChainVM = (*fullVM)(nil) ) type fullVM struct { *blocktest.VM *blocktest.StateSyncableVM + *blocktest.SetPreferenceVM } var ( @@ -100,37 +102,40 @@ func initTestProposerVM( VM: &blocktest.VM{ VM: enginetest.VM{ T: t, + InitializeF: func(context.Context, *snow.Context, database.Database, []byte, []byte, []byte, []*common.Fx, common.AppSender) error { + return nil + }, + }, + LastAcceptedF: snowmantest.MakeLastAcceptedBlockF( + []*snowmantest.Block{snowmantest.Genesis}, + ), + GetBlockF: func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + switch blkID { + case snowmantest.GenesisID: + return snowmantest.Genesis, nil + default: + return nil, errUnknownBlock + } + }, + ParseBlockF: func(_ context.Context, b []byte) (snowman.Block, error) { + switch { + case bytes.Equal(b, snowmantest.GenesisBytes): + return snowmantest.Genesis, nil + default: + return nil, errUnknownBlock + } }, }, StateSyncableVM: &blocktest.StateSyncableVM{ T: t, }, + SetPreferenceVM: &blocktest.SetPreferenceVM{ + T: t, + }, } - - coreVM.InitializeF = func(context.Context, *snow.Context, database.Database, - []byte, []byte, []byte, - []*common.Fx, common.AppSender, - ) error { - return nil - } - coreVM.LastAcceptedF = snowmantest.MakeLastAcceptedBlockF( - []*snowmantest.Block{snowmantest.Genesis}, - ) - coreVM.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case snowmantest.GenesisID: - return snowmantest.Genesis, nil - default: - return nil, errUnknownBlock - } - } - coreVM.ParseBlockF = func(_ context.Context, b []byte) (snowman.Block, error) { - switch { - case bytes.Equal(b, snowmantest.GenesisBytes): - return snowmantest.Genesis, nil - default: - return nil, errUnknownBlock - } + // Default to routing SetPreferenceWithContext to SetPreference + coreVM.SetPreferenceWithContextF = func(ctx context.Context, blkID ids.ID, _ *block.Context) error { + return coreVM.SetPreference(ctx, blkID) } var upgrades upgrade.Config @@ -155,38 +160,38 @@ func initTestProposerVM( valState := &validatorstest.State{ T: t, - } - valState.GetMinimumHeightF = func(context.Context) (uint64, error) { - return snowmantest.GenesisHeight, nil - } - valState.GetCurrentHeightF = func(context.Context) (uint64, error) { - return defaultPChainHeight, nil - } - valState.GetValidatorSetF = func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { - var ( - thisNode = proVM.ctx.NodeID - nodeID1 = ids.BuildTestNodeID([]byte{1}) - nodeID2 = ids.BuildTestNodeID([]byte{2}) - nodeID3 = ids.BuildTestNodeID([]byte{3}) - ) - return map[ids.NodeID]*validators.GetValidatorOutput{ - thisNode: { - NodeID: thisNode, - Weight: 10, - }, - nodeID1: { - NodeID: nodeID1, - Weight: 5, - }, - nodeID2: { - NodeID: nodeID2, - Weight: 6, - }, - nodeID3: { - NodeID: nodeID3, - Weight: 7, - }, - }, nil + GetMinimumHeightF: func(context.Context) (uint64, error) { + return snowmantest.GenesisHeight, nil + }, + GetCurrentHeightF: func(context.Context) (uint64, error) { + return defaultPChainHeight, nil + }, + GetValidatorSetF: func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + var ( + thisNode = proVM.ctx.NodeID + nodeID1 = ids.BuildTestNodeID([]byte{1}) + nodeID2 = ids.BuildTestNodeID([]byte{2}) + nodeID3 = ids.BuildTestNodeID([]byte{3}) + ) + return map[ids.NodeID]*validators.GetValidatorOutput{ + thisNode: { + NodeID: thisNode, + Weight: 10, + }, + nodeID1: { + NodeID: nodeID1, + Weight: 5, + }, + nodeID2: { + NodeID: nodeID2, + Weight: 6, + }, + nodeID3: { + NodeID: nodeID3, + Weight: 7, + }, + }, nil + }, } ctx := snowtest.Context(t, ids.ID{1}) @@ -757,6 +762,138 @@ func TestPreFork_SetPreference(t *testing.T) { require.Equal(builtBlk.ID(), nextBlk.Parent()) } +// TestPostFork_SetPreference tests the SetPreference functionality after the fork +// when SetPreferenceWithContext may be called based on various conditions. +func TestPostFork_SetPreference(t *testing.T) { + // Helper to create a block with the given epoch, P-Chain height, and optional custom timestamp + createBlockWithEpoch := func(proVM *VM, epoch statelessblock.Epoch, blockPChainHeight uint64, customTimestamp ...time.Time) PostForkBlock { + coreBlk := snowmantest.BuildChild(snowmantest.Genesis) + + timestamp := coreBlk.Timestamp() + if len(customTimestamp) > 0 { + timestamp = customTimestamp[0] + } + + statelessBlk, err := statelessblock.BuildUnsigned( + snowmantest.GenesisID, + timestamp, + blockPChainHeight, + epoch, + coreBlk.Bytes(), + ) + require.NoError(t, err) + + return &postForkBlock{ + SignedBlock: statelessBlk, + postForkCommonComponents: postForkCommonComponents{ + vm: proVM, + innerBlk: coreBlk, + }, + } + } + + testErr := errors.New("test err") + + tests := []struct { + name string + hasSetPreferenceWithContext bool + epochPChainHeight *uint64 // nil = no epoch, otherwise epoch with this P-Chain height + sealEpoch bool // whether block timestamp should seal the epoch + expectSetPreferenceWithContext bool + expectedError error + }{ + { + name: "setPreferenceVM is nil - should call regular SetPreference", + hasSetPreferenceWithContext: false, + epochPChainHeight: &defaultPChainHeight, + expectSetPreferenceWithContext: false, + }, + { + name: "preferredEpoch is empty - should call regular SetPreference", + hasSetPreferenceWithContext: true, + epochPChainHeight: nil, // no epoch + expectSetPreferenceWithContext: false, + }, + { + name: "both conditions met - should call SetPreferenceWithContext", + hasSetPreferenceWithContext: true, + epochPChainHeight: &defaultPChainHeight, + expectSetPreferenceWithContext: true, + }, + { + name: "SetPreferenceWithContext returns error", + hasSetPreferenceWithContext: true, + epochPChainHeight: &defaultPChainHeight, + expectSetPreferenceWithContext: true, + expectedError: testErr, + }, + { + name: "epoch sealed - next epoch has different PChainHeight", + hasSetPreferenceWithContext: true, + epochPChainHeight: func() *uint64 { + h := defaultPChainHeight - 100 // older epoch height + return &h + }(), + sealEpoch: true, + expectSetPreferenceWithContext: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + coreVM, _, proVM, _ := initTestProposerVM(t, upgradetest.Latest, defaultPChainHeight) + defer func() { + require.NoError(proVM.Shutdown(context.Background())) + }() + + if test.hasSetPreferenceWithContext { + coreVM.SetPreferenceWithContextF = func(_ context.Context, _ ids.ID, blockContext *block.Context) error { + require.Equal(defaultPChainHeight, blockContext.PChainHeight) + return test.expectedError + } + } else { + proVM.setPreferenceVM = nil + coreVM.SetPreferenceF = func(context.Context, ids.ID) error { + return test.expectedError + } + } + + if test.expectSetPreferenceWithContext { + coreVM.CantSetPreference = true + coreVM.SetPreferenceF = nil + } else { + coreVM.CantSetPreferenceWithContext = true + coreVM.SetPreferenceWithContextF = nil + } + + // Create block based on test requirements + var epoch statelessblock.Epoch + if test.epochPChainHeight != nil { + epoch = statelessblock.Epoch{ + PChainHeight: *test.epochPChainHeight, + Number: 1, + StartTime: snowmantest.GenesisTimestamp.Unix(), + } + } + + var postForkBlk PostForkBlock + if test.sealEpoch { + // Create a block timestamp that seals the epoch + epochSealingTimestamp := snowmantest.GenesisTimestamp.Add(upgrade.Default.GraniteEpochDuration) + postForkBlk = createBlockWithEpoch(proVM, epoch, defaultPChainHeight, epochSealingTimestamp) + } else { + postForkBlk = createBlockWithEpoch(proVM, epoch, defaultPChainHeight) + } + + proVM.verifiedBlocks[postForkBlk.ID()] = postForkBlk + err := proVM.SetPreference(context.Background(), postForkBlk.ID()) + require.ErrorIs(err, test.expectedError) + }) + } +} + func TestExpiredBuildBlock(t *testing.T) { require := require.New(t) diff --git a/vms/tracedvm/block_vm.go b/vms/tracedvm/block_vm.go index f03f838a5653..6b959eb11330 100644 --- a/vms/tracedvm/block_vm.go +++ b/vms/tracedvm/block_vm.go @@ -20,17 +20,19 @@ import ( ) var ( - _ block.ChainVM = (*blockVM)(nil) - _ block.BuildBlockWithContextChainVM = (*blockVM)(nil) - _ block.BatchedChainVM = (*blockVM)(nil) - _ block.StateSyncableVM = (*blockVM)(nil) + _ block.ChainVM = (*blockVM)(nil) + _ block.BuildBlockWithContextChainVM = (*blockVM)(nil) + _ block.SetPreferenceWithContextChainVM = (*blockVM)(nil) + _ block.BatchedChainVM = (*blockVM)(nil) + _ block.StateSyncableVM = (*blockVM)(nil) ) type blockVM struct { block.ChainVM - buildBlockVM block.BuildBlockWithContextChainVM - batchedVM block.BatchedChainVM - ssVM block.StateSyncableVM + buildBlockVM block.BuildBlockWithContextChainVM + setPreferenceVM block.SetPreferenceWithContextChainVM + batchedVM block.BatchedChainVM + ssVM block.StateSyncableVM // ChainVM tags initializeTag string buildBlockTag string @@ -46,6 +48,8 @@ type blockVM struct { verifyWithContextTag string // BuildBlockWithContextChainVM tags buildBlockWithContextTag string + // SetPreferenceWithContextChainVM tags + setPreferenceWithContextTag string // BatchedChainVM tags getAncestorsTag string batchedParseBlockTag string @@ -62,11 +66,13 @@ type blockVM struct { func NewBlockVM(vm block.ChainVM, name string, tracer trace.Tracer) block.ChainVM { buildBlockVM, _ := vm.(block.BuildBlockWithContextChainVM) + setPreferenceVM, _ := vm.(block.SetPreferenceWithContextChainVM) batchedVM, _ := vm.(block.BatchedChainVM) ssVM, _ := vm.(block.StateSyncableVM) return &blockVM{ ChainVM: vm, buildBlockVM: buildBlockVM, + setPreferenceVM: setPreferenceVM, batchedVM: batchedVM, ssVM: ssVM, initializeTag: name + ".initialize", @@ -82,6 +88,7 @@ func NewBlockVM(vm block.ChainVM, name string, tracer trace.Tracer) block.ChainV shouldVerifyWithContextTag: name + ".shouldVerifyWithContext", verifyWithContextTag: name + ".verifyWithContext", buildBlockWithContextTag: name + ".buildBlockWithContext", + setPreferenceWithContextTag: name + ".setPreferenceWithContext", getAncestorsTag: name + ".getAncestors", batchedParseBlockTag: name + ".batchedParseBlock", getBlockIDAtHeightTag: name + ".getBlockIDAtHeight", diff --git a/vms/tracedvm/set_preference_with_context_vm.go b/vms/tracedvm/set_preference_with_context_vm.go new file mode 100644 index 000000000000..74a86f5ad6b1 --- /dev/null +++ b/vms/tracedvm/set_preference_with_context_vm.go @@ -0,0 +1,38 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tracedvm + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + + oteltrace "go.opentelemetry.io/otel/trace" +) + +func (vm *blockVM) SetPreferenceWithContext(ctx context.Context, blkID ids.ID, blockCtx *block.Context) error { + if vm.setPreferenceVM == nil { + return vm.SetPreference(ctx, blkID) + } + + var option oteltrace.SpanStartEventOption + if blockCtx == nil { + option = oteltrace.WithAttributes( + attribute.Stringer("blkID", blkID), + ) + } else { + option = oteltrace.WithAttributes( + attribute.Stringer("blkID", blkID), + attribute.Int64("pChainHeight", int64(blockCtx.PChainHeight)), + ) + } + + ctx, span := vm.tracer.Start(ctx, vm.setPreferenceWithContextTag, option) + defer span.End() + + return vm.setPreferenceVM.SetPreferenceWithContext(ctx, blkID, blockCtx) +}