github.com/MetalBlockchain/metalgo@v1.11.9/vms/proposervm/block_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package proposervm
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/ecdsa"
    10  	"crypto/elliptic"
    11  	"crypto/rand"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/stretchr/testify/require"
    17  	"go.uber.org/mock/gomock"
    18  
    19  	"github.com/MetalBlockchain/metalgo/ids"
    20  	"github.com/MetalBlockchain/metalgo/snow"
    21  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman"
    22  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman/snowmantest"
    23  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    24  	"github.com/MetalBlockchain/metalgo/snow/validators"
    25  	"github.com/MetalBlockchain/metalgo/staking"
    26  	"github.com/MetalBlockchain/metalgo/utils/logging"
    27  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    28  	"github.com/MetalBlockchain/metalgo/vms/proposervm/proposer"
    29  	"github.com/MetalBlockchain/metalgo/vms/proposervm/scheduler"
    30  )
    31  
    32  // Assert that when the underlying VM implements ChainVMWithBuildBlockContext
    33  // and the proposervm is activated, we call the VM's BuildBlockWithContext
    34  // method to build a block rather than BuildBlockWithContext. If the proposervm
    35  // isn't activated, we should call BuildBlock rather than BuildBlockWithContext.
    36  func TestPostForkCommonComponents_buildChild(t *testing.T) {
    37  	require := require.New(t)
    38  	ctrl := gomock.NewController(t)
    39  
    40  	var (
    41  		nodeID                 = ids.GenerateTestNodeID()
    42  		pChainHeight    uint64 = 1337
    43  		parentID               = ids.GenerateTestID()
    44  		parentTimestamp        = time.Now().Truncate(time.Second)
    45  		parentHeight    uint64 = 1234
    46  		blkID                  = ids.GenerateTestID()
    47  	)
    48  
    49  	innerBlk := snowmantest.NewMockBlock(ctrl)
    50  	innerBlk.EXPECT().ID().Return(blkID).AnyTimes()
    51  	innerBlk.EXPECT().Height().Return(parentHeight + 1).AnyTimes()
    52  
    53  	builtBlk := snowmantest.NewMockBlock(ctrl)
    54  	builtBlk.EXPECT().Bytes().Return([]byte{1, 2, 3}).AnyTimes()
    55  	builtBlk.EXPECT().ID().Return(ids.GenerateTestID()).AnyTimes()
    56  	builtBlk.EXPECT().Height().Return(pChainHeight).AnyTimes()
    57  
    58  	innerVM := block.NewMockChainVM(ctrl)
    59  	innerBlockBuilderVM := block.NewMockBuildBlockWithContextChainVM(ctrl)
    60  	innerBlockBuilderVM.EXPECT().BuildBlockWithContext(gomock.Any(), &block.Context{
    61  		PChainHeight: pChainHeight - 1,
    62  	}).Return(builtBlk, nil).AnyTimes()
    63  
    64  	vdrState := validators.NewMockState(ctrl)
    65  	vdrState.EXPECT().GetMinimumHeight(context.Background()).Return(pChainHeight, nil).AnyTimes()
    66  
    67  	windower := proposer.NewMockWindower(ctrl)
    68  	windower.EXPECT().ExpectedProposer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nodeID, nil).AnyTimes()
    69  
    70  	pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    71  	require.NoError(err)
    72  	vm := &VM{
    73  		Config: Config{
    74  			ActivationTime:    time.Unix(0, 0),
    75  			DurangoTime:       time.Unix(0, 0),
    76  			StakingCertLeaf:   &staking.Certificate{},
    77  			StakingLeafSigner: pk,
    78  			Registerer:        prometheus.NewRegistry(),
    79  		},
    80  		ChainVM:        innerVM,
    81  		blockBuilderVM: innerBlockBuilderVM,
    82  		ctx: &snow.Context{
    83  			NodeID:         nodeID,
    84  			ValidatorState: vdrState,
    85  			Log:            logging.NoLog{},
    86  		},
    87  		Windower: windower,
    88  	}
    89  
    90  	blk := &postForkCommonComponents{
    91  		innerBlk: innerBlk,
    92  		vm:       vm,
    93  	}
    94  
    95  	// Should call BuildBlockWithContext since proposervm is activated
    96  	gotChild, err := blk.buildChild(
    97  		context.Background(),
    98  		parentID,
    99  		parentTimestamp,
   100  		pChainHeight-1,
   101  	)
   102  	require.NoError(err)
   103  	require.Equal(builtBlk, gotChild.(*postForkBlock).innerBlk)
   104  }
   105  
   106  func TestPreDurangoValidatorNodeBlockBuiltDelaysTests(t *testing.T) {
   107  	require := require.New(t)
   108  	ctx := context.Background()
   109  
   110  	var (
   111  		activationTime = time.Unix(0, 0)
   112  		durangoTime    = mockable.MaxTime
   113  	)
   114  	coreVM, valState, proVM, _ := initTestProposerVM(t, activationTime, durangoTime, 0)
   115  	defer func() {
   116  		require.NoError(proVM.Shutdown(ctx))
   117  	}()
   118  
   119  	// Build a post fork block. It'll be the parent block in our test cases
   120  	parentTime := time.Now().Truncate(time.Second)
   121  	proVM.Set(parentTime)
   122  
   123  	coreParentBlk := snowmantest.BuildChild(snowmantest.Genesis)
   124  	coreVM.BuildBlockF = func(context.Context) (snowman.Block, error) {
   125  		return coreParentBlk, nil
   126  	}
   127  	coreVM.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) {
   128  		switch blkID {
   129  		case coreParentBlk.ID():
   130  			return coreParentBlk, nil
   131  		case snowmantest.GenesisID:
   132  			return snowmantest.Genesis, nil
   133  		default:
   134  			return nil, errUnknownBlock
   135  		}
   136  	}
   137  	coreVM.ParseBlockF = func(_ context.Context, b []byte) (snowman.Block, error) { // needed when setting preference
   138  		switch {
   139  		case bytes.Equal(b, coreParentBlk.Bytes()):
   140  			return coreParentBlk, nil
   141  		case bytes.Equal(b, snowmantest.GenesisBytes):
   142  			return snowmantest.Genesis, nil
   143  		default:
   144  			return nil, errUnknownBlock
   145  		}
   146  	}
   147  
   148  	parentBlk, err := proVM.BuildBlock(ctx)
   149  	require.NoError(err)
   150  	require.NoError(parentBlk.Verify(ctx))
   151  	require.NoError(parentBlk.Accept(ctx))
   152  
   153  	// Make sure preference is duly set
   154  	require.NoError(proVM.SetPreference(ctx, parentBlk.ID()))
   155  	require.Equal(proVM.preferred, parentBlk.ID())
   156  	_, err = proVM.getPostForkBlock(ctx, parentBlk.ID())
   157  	require.NoError(err)
   158  
   159  	// Force this node to be the only validator, so to guarantee
   160  	// it'd be picked if block build time was before MaxVerifyDelay
   161  	valState.GetValidatorSetF = func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
   162  		// a validator with a weight large enough to fully fill the proposers list
   163  		weight := uint64(proposer.MaxBuildWindows * 2)
   164  
   165  		return map[ids.NodeID]*validators.GetValidatorOutput{
   166  			proVM.ctx.NodeID: {
   167  				NodeID: proVM.ctx.NodeID,
   168  				Weight: weight,
   169  			},
   170  		}, nil
   171  	}
   172  
   173  	coreChildBlk := snowmantest.BuildChild(coreParentBlk)
   174  	coreVM.BuildBlockF = func(context.Context) (snowman.Block, error) {
   175  		return coreChildBlk, nil
   176  	}
   177  
   178  	{
   179  		// Set local clock before MaxVerifyDelay from parent timestamp.
   180  		// Check that child block is signed.
   181  		localTime := parentBlk.Timestamp().Add(proposer.MaxVerifyDelay - time.Second)
   182  		proVM.Set(localTime)
   183  
   184  		childBlkIntf, err := proVM.BuildBlock(ctx)
   185  		require.NoError(err)
   186  		require.IsType(&postForkBlock{}, childBlkIntf)
   187  
   188  		childBlk := childBlkIntf.(*postForkBlock)
   189  		require.Equal(proVM.ctx.NodeID, childBlk.Proposer()) // signed block
   190  	}
   191  
   192  	{
   193  		// Set local clock exactly MaxVerifyDelay from parent timestamp.
   194  		// Check that child block is unsigned.
   195  		localTime := parentBlk.Timestamp().Add(proposer.MaxVerifyDelay)
   196  		proVM.Set(localTime)
   197  
   198  		childBlkIntf, err := proVM.BuildBlock(ctx)
   199  		require.NoError(err)
   200  		require.IsType(&postForkBlock{}, childBlkIntf)
   201  
   202  		childBlk := childBlkIntf.(*postForkBlock)
   203  		require.Equal(ids.EmptyNodeID, childBlk.Proposer()) // unsigned block
   204  	}
   205  
   206  	{
   207  		// Set local clock between MaxVerifyDelay and MaxBuildDelay from parent
   208  		// timestamp.
   209  		// Check that child block is unsigned.
   210  		localTime := parentBlk.Timestamp().Add((proposer.MaxVerifyDelay + proposer.MaxBuildDelay) / 2)
   211  		proVM.Set(localTime)
   212  
   213  		childBlkIntf, err := proVM.BuildBlock(ctx)
   214  		require.NoError(err)
   215  		require.IsType(&postForkBlock{}, childBlkIntf)
   216  
   217  		childBlk := childBlkIntf.(*postForkBlock)
   218  		require.Equal(ids.EmptyNodeID, childBlk.Proposer()) // unsigned block
   219  	}
   220  
   221  	{
   222  		// Set local clock after MaxBuildDelay from parent timestamp.
   223  		// Check that child block is unsigned.
   224  		localTime := parentBlk.Timestamp().Add(proposer.MaxBuildDelay)
   225  		proVM.Set(localTime)
   226  
   227  		childBlkIntf, err := proVM.BuildBlock(ctx)
   228  		require.NoError(err)
   229  		require.IsType(&postForkBlock{}, childBlkIntf)
   230  
   231  		childBlk := childBlkIntf.(*postForkBlock)
   232  		require.Equal(ids.EmptyNodeID, childBlk.Proposer()) // unsigned block
   233  	}
   234  }
   235  
   236  func TestPreDurangoNonValidatorNodeBlockBuiltDelaysTests(t *testing.T) {
   237  	require := require.New(t)
   238  	ctx := context.Background()
   239  
   240  	var (
   241  		activationTime = time.Unix(0, 0)
   242  		durangoTime    = mockable.MaxTime
   243  	)
   244  	coreVM, valState, proVM, _ := initTestProposerVM(t, activationTime, durangoTime, 0)
   245  	defer func() {
   246  		require.NoError(proVM.Shutdown(ctx))
   247  	}()
   248  
   249  	// Build a post fork block. It'll be the parent block in our test cases
   250  	parentTime := time.Now().Truncate(time.Second)
   251  	proVM.Set(parentTime)
   252  
   253  	coreParentBlk := snowmantest.BuildChild(snowmantest.Genesis)
   254  	coreVM.BuildBlockF = func(context.Context) (snowman.Block, error) {
   255  		return coreParentBlk, nil
   256  	}
   257  	coreVM.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) {
   258  		switch blkID {
   259  		case coreParentBlk.ID():
   260  			return coreParentBlk, nil
   261  		case snowmantest.GenesisID:
   262  			return snowmantest.Genesis, nil
   263  		default:
   264  			return nil, errUnknownBlock
   265  		}
   266  	}
   267  	coreVM.ParseBlockF = func(_ context.Context, b []byte) (snowman.Block, error) { // needed when setting preference
   268  		switch {
   269  		case bytes.Equal(b, coreParentBlk.Bytes()):
   270  			return coreParentBlk, nil
   271  		case bytes.Equal(b, snowmantest.GenesisBytes):
   272  			return snowmantest.Genesis, nil
   273  		default:
   274  			return nil, errUnknownBlock
   275  		}
   276  	}
   277  
   278  	parentBlk, err := proVM.BuildBlock(ctx)
   279  	require.NoError(err)
   280  	require.NoError(parentBlk.Verify(ctx))
   281  	require.NoError(parentBlk.Accept(ctx))
   282  
   283  	// Make sure preference is duly set
   284  	require.NoError(proVM.SetPreference(ctx, parentBlk.ID()))
   285  	require.Equal(proVM.preferred, parentBlk.ID())
   286  	_, err = proVM.getPostForkBlock(ctx, parentBlk.ID())
   287  	require.NoError(err)
   288  
   289  	// Mark node as non validator
   290  	valState.GetValidatorSetF = func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
   291  		var (
   292  			aValidator = ids.GenerateTestNodeID()
   293  
   294  			// a validator with a weight large enough to fully fill the proposers list
   295  			weight = uint64(proposer.MaxBuildWindows * 2)
   296  		)
   297  		return map[ids.NodeID]*validators.GetValidatorOutput{
   298  			aValidator: {
   299  				NodeID: aValidator,
   300  				Weight: weight,
   301  			},
   302  		}, nil
   303  	}
   304  
   305  	coreChildBlk := snowmantest.BuildChild(coreParentBlk)
   306  	coreVM.BuildBlockF = func(context.Context) (snowman.Block, error) {
   307  		return coreChildBlk, nil
   308  	}
   309  
   310  	{
   311  		// Set local clock before MaxVerifyDelay from parent timestamp.
   312  		// Check that child block is not built.
   313  		localTime := parentBlk.Timestamp().Add(proposer.MaxVerifyDelay - time.Second)
   314  		proVM.Set(localTime)
   315  
   316  		_, err := proVM.BuildBlock(ctx)
   317  		require.ErrorIs(err, errProposerWindowNotStarted)
   318  	}
   319  
   320  	{
   321  		// Set local clock exactly MaxVerifyDelay from parent timestamp.
   322  		// Check that child block is not built.
   323  		localTime := parentBlk.Timestamp().Add(proposer.MaxVerifyDelay)
   324  		proVM.Set(localTime)
   325  
   326  		_, err := proVM.BuildBlock(ctx)
   327  		require.ErrorIs(err, errProposerWindowNotStarted)
   328  	}
   329  
   330  	{
   331  		// Set local clock among MaxVerifyDelay and MaxBuildDelay from parent timestamp
   332  		// Check that child block is not built.
   333  		localTime := parentBlk.Timestamp().Add((proposer.MaxVerifyDelay + proposer.MaxBuildDelay) / 2)
   334  		proVM.Set(localTime)
   335  
   336  		_, err := proVM.BuildBlock(ctx)
   337  		require.ErrorIs(err, errProposerWindowNotStarted)
   338  	}
   339  
   340  	{
   341  		// Set local clock after MaxBuildDelay from parent timestamp
   342  		// Check that child block is built and it is unsigned
   343  		localTime := parentBlk.Timestamp().Add(proposer.MaxBuildDelay)
   344  		proVM.Set(localTime)
   345  
   346  		childBlkIntf, err := proVM.BuildBlock(ctx)
   347  		require.NoError(err)
   348  		require.IsType(&postForkBlock{}, childBlkIntf)
   349  
   350  		childBlk := childBlkIntf.(*postForkBlock)
   351  		require.Equal(ids.EmptyNodeID, childBlk.Proposer()) // unsigned block
   352  	}
   353  }
   354  
   355  // We consider cases where this node is not current proposer (may be scheduled in the next future or not).
   356  // We check that scheduler is called nonetheless, to be able to process innerVM block requests
   357  func TestPostDurangoBuildChildResetScheduler(t *testing.T) {
   358  	require := require.New(t)
   359  	ctrl := gomock.NewController(t)
   360  
   361  	var (
   362  		thisNodeID              = ids.GenerateTestNodeID()
   363  		selectedProposer        = ids.GenerateTestNodeID()
   364  		pChainHeight     uint64 = 1337
   365  		parentID                = ids.GenerateTestID()
   366  		parentTimestamp         = time.Now().Truncate(time.Second)
   367  		now                     = parentTimestamp.Add(12 * time.Second)
   368  		parentHeight     uint64 = 1234
   369  	)
   370  
   371  	innerBlk := snowmantest.NewMockBlock(ctrl)
   372  	innerBlk.EXPECT().Height().Return(parentHeight + 1).AnyTimes()
   373  
   374  	vdrState := validators.NewMockState(ctrl)
   375  	vdrState.EXPECT().GetMinimumHeight(context.Background()).Return(pChainHeight, nil).AnyTimes()
   376  
   377  	windower := proposer.NewMockWindower(ctrl)
   378  	windower.EXPECT().ExpectedProposer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
   379  		Return(selectedProposer, nil).AnyTimes() // return a proposer different from thisNode, to check whether scheduler is reset
   380  
   381  	scheduler := scheduler.NewMockScheduler(ctrl)
   382  
   383  	pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   384  	require.NoError(err)
   385  	vm := &VM{
   386  		Config: Config{
   387  			ActivationTime:    time.Unix(0, 0),
   388  			DurangoTime:       time.Unix(0, 0),
   389  			StakingCertLeaf:   &staking.Certificate{},
   390  			StakingLeafSigner: pk,
   391  			Registerer:        prometheus.NewRegistry(),
   392  		},
   393  		ChainVM: block.NewMockChainVM(ctrl),
   394  		ctx: &snow.Context{
   395  			NodeID:         thisNodeID,
   396  			ValidatorState: vdrState,
   397  			Log:            logging.NoLog{},
   398  		},
   399  		Windower:               windower,
   400  		Scheduler:              scheduler,
   401  		proposerBuildSlotGauge: prometheus.NewGauge(prometheus.GaugeOpts{}),
   402  	}
   403  	vm.Clock.Set(now)
   404  
   405  	blk := &postForkCommonComponents{
   406  		innerBlk: innerBlk,
   407  		vm:       vm,
   408  	}
   409  
   410  	delays := []time.Duration{
   411  		proposer.MaxLookAheadWindow - time.Minute,
   412  		proposer.MaxLookAheadWindow,
   413  		proposer.MaxLookAheadWindow + time.Minute,
   414  	}
   415  
   416  	for _, delay := range delays {
   417  		windower.EXPECT().MinDelayForProposer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
   418  			Return(delay, nil).Times(1)
   419  
   420  		// we mock the scheduler setting the exact time we expect it to be reset
   421  		// to
   422  		expectedSchedulerTime := parentTimestamp.Add(delay)
   423  		scheduler.EXPECT().SetBuildBlockTime(expectedSchedulerTime).Times(1)
   424  
   425  		_, err = blk.buildChild(
   426  			context.Background(),
   427  			parentID,
   428  			parentTimestamp,
   429  			pChainHeight-1,
   430  		)
   431  		require.ErrorIs(err, errUnexpectedProposer)
   432  	}
   433  }