github.com/MetalBlockchain/metalgo@v1.11.9/vms/proposervm/vm.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  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	"go.uber.org/zap"
    14  
    15  	"github.com/MetalBlockchain/metalgo/cache"
    16  	"github.com/MetalBlockchain/metalgo/cache/metercacher"
    17  	"github.com/MetalBlockchain/metalgo/database"
    18  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    19  	"github.com/MetalBlockchain/metalgo/database/versiondb"
    20  	"github.com/MetalBlockchain/metalgo/ids"
    21  	"github.com/MetalBlockchain/metalgo/snow"
    22  	"github.com/MetalBlockchain/metalgo/snow/choices"
    23  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman"
    24  	"github.com/MetalBlockchain/metalgo/snow/engine/common"
    25  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    26  	"github.com/MetalBlockchain/metalgo/utils/constants"
    27  	"github.com/MetalBlockchain/metalgo/utils/math"
    28  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    29  	"github.com/MetalBlockchain/metalgo/utils/units"
    30  	"github.com/MetalBlockchain/metalgo/vms/proposervm/proposer"
    31  	"github.com/MetalBlockchain/metalgo/vms/proposervm/scheduler"
    32  	"github.com/MetalBlockchain/metalgo/vms/proposervm/state"
    33  	"github.com/MetalBlockchain/metalgo/vms/proposervm/tree"
    34  
    35  	statelessblock "github.com/MetalBlockchain/metalgo/vms/proposervm/block"
    36  )
    37  
    38  const (
    39  	// DefaultMinBlockDelay should be kept as whole seconds because block
    40  	// timestamps are only specific to the second.
    41  	DefaultMinBlockDelay = time.Second
    42  	// DefaultNumHistoricalBlocks as 0 results in never deleting any historical
    43  	// blocks.
    44  	DefaultNumHistoricalBlocks uint64 = 0
    45  
    46  	checkIndexedFrequency = 10 * time.Second
    47  	innerBlkCacheSize     = 64 * units.MiB
    48  )
    49  
    50  var (
    51  	_ block.ChainVM         = (*VM)(nil)
    52  	_ block.BatchedChainVM  = (*VM)(nil)
    53  	_ block.StateSyncableVM = (*VM)(nil)
    54  
    55  	dbPrefix = []byte("proposervm")
    56  )
    57  
    58  func cachedBlockSize(_ ids.ID, blk snowman.Block) int {
    59  	return ids.IDLen + len(blk.Bytes()) + constants.PointerOverhead
    60  }
    61  
    62  type VM struct {
    63  	block.ChainVM
    64  	Config
    65  	blockBuilderVM block.BuildBlockWithContextChainVM
    66  	batchedVM      block.BatchedChainVM
    67  	ssVM           block.StateSyncableVM
    68  
    69  	state.State
    70  
    71  	proposer.Windower
    72  	tree.Tree
    73  	scheduler.Scheduler
    74  	mockable.Clock
    75  
    76  	ctx         *snow.Context
    77  	db          *versiondb.Database
    78  	toScheduler chan<- common.Message
    79  
    80  	// Block ID --> Block
    81  	// Each element is a block that passed verification but
    82  	// hasn't yet been accepted/rejected
    83  	verifiedBlocks map[ids.ID]PostForkBlock
    84  	// Stateless block ID --> inner block.
    85  	// Only contains post-fork blocks near the tip so that the cache doesn't get
    86  	// filled with random blocks every time this node parses blocks while
    87  	// processing a GetAncestors message from a bootstrapping node.
    88  	innerBlkCache  cache.Cacher[ids.ID, snowman.Block]
    89  	preferred      ids.ID
    90  	consensusState snow.State
    91  	context        context.Context
    92  	onShutdown     func()
    93  
    94  	// lastAcceptedTime is set to the last accepted PostForkBlock's timestamp
    95  	// if the last accepted block has been a PostForkOption block since having
    96  	// initialized the VM.
    97  	lastAcceptedTime time.Time
    98  
    99  	// lastAcceptedHeight is set to the last accepted PostForkBlock's height.
   100  	lastAcceptedHeight uint64
   101  
   102  	// proposerBuildSlotGauge reports the slot index when this node may attempt
   103  	// to build a block.
   104  	proposerBuildSlotGauge prometheus.Gauge
   105  
   106  	// acceptedBlocksSlotHistogram reports the slots that accepted blocks were
   107  	// proposed in.
   108  	acceptedBlocksSlotHistogram prometheus.Histogram
   109  }
   110  
   111  // New performs best when [minBlkDelay] is whole seconds. This is because block
   112  // timestamps are only specific to the second.
   113  func New(
   114  	vm block.ChainVM,
   115  	config Config,
   116  ) *VM {
   117  	blockBuilderVM, _ := vm.(block.BuildBlockWithContextChainVM)
   118  	batchedVM, _ := vm.(block.BatchedChainVM)
   119  	ssVM, _ := vm.(block.StateSyncableVM)
   120  	return &VM{
   121  		ChainVM:        vm,
   122  		Config:         config,
   123  		blockBuilderVM: blockBuilderVM,
   124  		batchedVM:      batchedVM,
   125  		ssVM:           ssVM,
   126  	}
   127  }
   128  
   129  func (vm *VM) Initialize(
   130  	ctx context.Context,
   131  	chainCtx *snow.Context,
   132  	db database.Database,
   133  	genesisBytes []byte,
   134  	upgradeBytes []byte,
   135  	configBytes []byte,
   136  	toEngine chan<- common.Message,
   137  	fxs []*common.Fx,
   138  	appSender common.AppSender,
   139  ) error {
   140  	vm.ctx = chainCtx
   141  	vm.db = versiondb.New(prefixdb.New(dbPrefix, db))
   142  	baseState, err := state.NewMetered(vm.db, "state", vm.Config.Registerer)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	vm.State = baseState
   147  	vm.Windower = proposer.New(chainCtx.ValidatorState, chainCtx.SubnetID, chainCtx.ChainID)
   148  	vm.Tree = tree.New()
   149  	innerBlkCache, err := metercacher.New(
   150  		"inner_block_cache",
   151  		vm.Config.Registerer,
   152  		cache.NewSizedLRU(
   153  			innerBlkCacheSize,
   154  			cachedBlockSize,
   155  		),
   156  	)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	vm.innerBlkCache = innerBlkCache
   161  
   162  	scheduler, vmToEngine := scheduler.New(vm.ctx.Log, toEngine)
   163  	vm.Scheduler = scheduler
   164  	vm.toScheduler = vmToEngine
   165  
   166  	go chainCtx.Log.RecoverAndPanic(func() {
   167  		scheduler.Dispatch(time.Now())
   168  	})
   169  
   170  	vm.verifiedBlocks = make(map[ids.ID]PostForkBlock)
   171  	detachedCtx := context.WithoutCancel(ctx)
   172  	context, cancel := context.WithCancel(detachedCtx)
   173  	vm.context = context
   174  	vm.onShutdown = cancel
   175  
   176  	err = vm.ChainVM.Initialize(
   177  		ctx,
   178  		chainCtx,
   179  		db,
   180  		genesisBytes,
   181  		upgradeBytes,
   182  		configBytes,
   183  		vmToEngine,
   184  		fxs,
   185  		appSender,
   186  	)
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
   192  		return err
   193  	}
   194  
   195  	if err := vm.setLastAcceptedMetadata(ctx); err != nil {
   196  		return err
   197  	}
   198  
   199  	if err := vm.pruneOldBlocks(); err != nil {
   200  		return err
   201  	}
   202  
   203  	forkHeight, err := vm.GetForkHeight()
   204  	switch err {
   205  	case nil:
   206  		chainCtx.Log.Info("initialized proposervm",
   207  			zap.String("state", "after fork"),
   208  			zap.Uint64("forkHeight", forkHeight),
   209  			zap.Uint64("lastAcceptedHeight", vm.lastAcceptedHeight),
   210  		)
   211  	case database.ErrNotFound:
   212  		chainCtx.Log.Info("initialized proposervm",
   213  			zap.String("state", "before fork"),
   214  		)
   215  	default:
   216  		return err
   217  	}
   218  
   219  	vm.proposerBuildSlotGauge = prometheus.NewGauge(prometheus.GaugeOpts{
   220  		Name: "block_building_slot",
   221  		Help: "the slot that this node may attempt to build a block",
   222  	})
   223  	vm.acceptedBlocksSlotHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
   224  		Name: "accepted_blocks_slot",
   225  		Help: "the slot accepted blocks were proposed in",
   226  		// define the following ranges:
   227  		// (-inf, 0]
   228  		// (0, 1]
   229  		// (1, 2]
   230  		// (2, inf)
   231  		// the usage of ".5" before was to ensure we work around the limitation
   232  		// of comparing floating point of the same numerical value.
   233  		Buckets: []float64{0.5, 1.5, 2.5},
   234  	})
   235  
   236  	return errors.Join(
   237  		vm.Config.Registerer.Register(vm.proposerBuildSlotGauge),
   238  		vm.Config.Registerer.Register(vm.acceptedBlocksSlotHistogram),
   239  	)
   240  }
   241  
   242  // shutdown ops then propagate shutdown to innerVM
   243  func (vm *VM) Shutdown(ctx context.Context) error {
   244  	vm.onShutdown()
   245  
   246  	vm.Scheduler.Close()
   247  
   248  	if err := vm.db.Commit(); err != nil {
   249  		return err
   250  	}
   251  	return vm.ChainVM.Shutdown(ctx)
   252  }
   253  
   254  func (vm *VM) SetState(ctx context.Context, newState snow.State) error {
   255  	if err := vm.ChainVM.SetState(ctx, newState); err != nil {
   256  		return err
   257  	}
   258  
   259  	oldState := vm.consensusState
   260  	vm.consensusState = newState
   261  	if oldState != snow.StateSyncing {
   262  		return nil
   263  	}
   264  
   265  	// When finishing StateSyncing, if state sync has failed or was skipped,
   266  	// repairAcceptedChainByHeight rolls back the chain to the previously last
   267  	// accepted block. If state sync has completed successfully, this call is a
   268  	// no-op.
   269  	if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
   270  		return err
   271  	}
   272  	return vm.setLastAcceptedMetadata(ctx)
   273  }
   274  
   275  func (vm *VM) BuildBlock(ctx context.Context) (snowman.Block, error) {
   276  	preferredBlock, err := vm.getBlock(ctx, vm.preferred)
   277  	if err != nil {
   278  		vm.ctx.Log.Error("unexpected build block failure",
   279  			zap.String("reason", "failed to fetch preferred block"),
   280  			zap.Stringer("parentID", vm.preferred),
   281  			zap.Error(err),
   282  		)
   283  		return nil, err
   284  	}
   285  
   286  	return preferredBlock.buildChild(ctx)
   287  }
   288  
   289  func (vm *VM) ParseBlock(ctx context.Context, b []byte) (snowman.Block, error) {
   290  	if blk, err := vm.parsePostForkBlock(ctx, b); err == nil {
   291  		return blk, nil
   292  	}
   293  	return vm.parsePreForkBlock(ctx, b)
   294  }
   295  
   296  func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (snowman.Block, error) {
   297  	return vm.getBlock(ctx, id)
   298  }
   299  
   300  func (vm *VM) SetPreference(ctx context.Context, preferred ids.ID) error {
   301  	if vm.preferred == preferred {
   302  		return nil
   303  	}
   304  	vm.preferred = preferred
   305  
   306  	blk, err := vm.getPostForkBlock(ctx, preferred)
   307  	if err != nil {
   308  		return vm.ChainVM.SetPreference(ctx, preferred)
   309  	}
   310  
   311  	if err := vm.ChainVM.SetPreference(ctx, blk.getInnerBlk().ID()); err != nil {
   312  		return err
   313  	}
   314  
   315  	pChainHeight, err := blk.pChainHeight(ctx)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	var (
   321  		childBlockHeight = blk.Height() + 1
   322  		parentTimestamp  = blk.Timestamp()
   323  		nextStartTime    time.Time
   324  	)
   325  	if vm.IsDurangoActivated(parentTimestamp) {
   326  		currentTime := vm.Clock.Time().Truncate(time.Second)
   327  		if nextStartTime, err = vm.getPostDurangoSlotTime(
   328  			ctx,
   329  			childBlockHeight,
   330  			pChainHeight,
   331  			proposer.TimeToSlot(parentTimestamp, currentTime),
   332  			parentTimestamp,
   333  		); err == nil {
   334  			vm.proposerBuildSlotGauge.Set(float64(proposer.TimeToSlot(parentTimestamp, nextStartTime)))
   335  		}
   336  	} else {
   337  		nextStartTime, err = vm.getPreDurangoSlotTime(
   338  			ctx,
   339  			childBlockHeight,
   340  			pChainHeight,
   341  			parentTimestamp,
   342  		)
   343  	}
   344  	if err != nil {
   345  		vm.ctx.Log.Debug("failed to fetch the expected delay",
   346  			zap.Error(err),
   347  		)
   348  
   349  		// A nil error is returned here because it is possible that
   350  		// bootstrapping caused the last accepted block to move past the latest
   351  		// P-chain height. This will cause building blocks to return an error
   352  		// until the P-chain's height has advanced.
   353  		return nil
   354  	}
   355  	vm.Scheduler.SetBuildBlockTime(nextStartTime)
   356  
   357  	vm.ctx.Log.Debug("set preference",
   358  		zap.Stringer("blkID", blk.ID()),
   359  		zap.Time("blockTimestamp", parentTimestamp),
   360  		zap.Time("nextStartTime", nextStartTime),
   361  	)
   362  	return nil
   363  }
   364  
   365  func (vm *VM) getPreDurangoSlotTime(
   366  	ctx context.Context,
   367  	blkHeight,
   368  	pChainHeight uint64,
   369  	parentTimestamp time.Time,
   370  ) (time.Time, error) {
   371  	delay, err := vm.Windower.Delay(ctx, blkHeight, pChainHeight, vm.ctx.NodeID, proposer.MaxBuildWindows)
   372  	if err != nil {
   373  		return time.Time{}, err
   374  	}
   375  
   376  	// Note: The P-chain does not currently try to target any block time. It
   377  	// notifies the consensus engine as soon as a new block may be built. To
   378  	// avoid fast runs of blocks there is an additional minimum delay that
   379  	// validators can specify. This delay may be an issue for high performance,
   380  	// custom VMs. Until the P-chain is modified to target a specific block
   381  	// time, ProposerMinBlockDelay can be configured in the subnet config.
   382  	delay = max(delay, vm.MinBlkDelay)
   383  	return parentTimestamp.Add(delay), nil
   384  }
   385  
   386  func (vm *VM) getPostDurangoSlotTime(
   387  	ctx context.Context,
   388  	blkHeight,
   389  	pChainHeight,
   390  	slot uint64,
   391  	parentTimestamp time.Time,
   392  ) (time.Time, error) {
   393  	delay, err := vm.Windower.MinDelayForProposer(
   394  		ctx,
   395  		blkHeight,
   396  		pChainHeight,
   397  		vm.ctx.NodeID,
   398  		slot,
   399  	)
   400  	// Note: The P-chain does not currently try to target any block time. It
   401  	// notifies the consensus engine as soon as a new block may be built. To
   402  	// avoid fast runs of blocks there is an additional minimum delay that
   403  	// validators can specify. This delay may be an issue for high performance,
   404  	// custom VMs. Until the P-chain is modified to target a specific block
   405  	// time, ProposerMinBlockDelay can be configured in the subnet config.
   406  	switch {
   407  	case err == nil:
   408  		delay = max(delay, vm.MinBlkDelay)
   409  		return parentTimestamp.Add(delay), err
   410  	case errors.Is(err, proposer.ErrAnyoneCanPropose):
   411  		return parentTimestamp.Add(vm.MinBlkDelay), err
   412  	default:
   413  		return time.Time{}, err
   414  	}
   415  }
   416  
   417  func (vm *VM) LastAccepted(ctx context.Context) (ids.ID, error) {
   418  	lastAccepted, err := vm.State.GetLastAccepted()
   419  	if err == database.ErrNotFound {
   420  		return vm.ChainVM.LastAccepted(ctx)
   421  	}
   422  	return lastAccepted, err
   423  }
   424  
   425  func (vm *VM) repairAcceptedChainByHeight(ctx context.Context) error {
   426  	innerLastAcceptedID, err := vm.ChainVM.LastAccepted(ctx)
   427  	if err != nil {
   428  		return err
   429  	}
   430  	innerLastAccepted, err := vm.ChainVM.GetBlock(ctx, innerLastAcceptedID)
   431  	if err != nil {
   432  		return err
   433  	}
   434  	proLastAcceptedID, err := vm.State.GetLastAccepted()
   435  	if err == database.ErrNotFound {
   436  		// If the last accepted block isn't indexed yet, then the underlying
   437  		// chain is the only chain and there is nothing to repair.
   438  		return nil
   439  	}
   440  	if err != nil {
   441  		return err
   442  	}
   443  	proLastAccepted, err := vm.getPostForkBlock(ctx, proLastAcceptedID)
   444  	if err != nil {
   445  		return err
   446  	}
   447  
   448  	proLastAcceptedHeight := proLastAccepted.Height()
   449  	innerLastAcceptedHeight := innerLastAccepted.Height()
   450  	if proLastAcceptedHeight < innerLastAcceptedHeight {
   451  		return fmt.Errorf("proposervm height index (%d) should never be lower than the inner height index (%d)", proLastAcceptedHeight, innerLastAcceptedHeight)
   452  	}
   453  	if proLastAcceptedHeight == innerLastAcceptedHeight {
   454  		// There is nothing to repair - as the heights match
   455  		return nil
   456  	}
   457  
   458  	vm.ctx.Log.Info("repairing accepted chain by height",
   459  		zap.Uint64("outerHeight", proLastAcceptedHeight),
   460  		zap.Uint64("innerHeight", innerLastAcceptedHeight),
   461  	)
   462  
   463  	// The inner vm must be behind the proposer vm, so we must roll the
   464  	// proposervm back.
   465  	forkHeight, err := vm.State.GetForkHeight()
   466  	if err != nil {
   467  		return err
   468  	}
   469  
   470  	if forkHeight > innerLastAcceptedHeight {
   471  		// We are rolling back past the fork, so we should just forget about all
   472  		// of our proposervm indices.
   473  		if err := vm.State.DeleteLastAccepted(); err != nil {
   474  			return err
   475  		}
   476  		return vm.db.Commit()
   477  	}
   478  
   479  	newProLastAcceptedID, err := vm.State.GetBlockIDAtHeight(innerLastAcceptedHeight)
   480  	if err != nil {
   481  		// This fatal error can happen if NumHistoricalBlocks is set too
   482  		// aggressively and the inner vm rolled back before the oldest
   483  		// proposervm block.
   484  		return fmt.Errorf("proposervm failed to rollback last accepted block to height (%d): %w", innerLastAcceptedHeight, err)
   485  	}
   486  
   487  	if err := vm.State.SetLastAccepted(newProLastAcceptedID); err != nil {
   488  		return err
   489  	}
   490  	return vm.db.Commit()
   491  }
   492  
   493  func (vm *VM) setLastAcceptedMetadata(ctx context.Context) error {
   494  	lastAcceptedID, err := vm.GetLastAccepted()
   495  	if err == database.ErrNotFound {
   496  		// If the last accepted block wasn't a PostFork block, then we don't
   497  		// initialize the metadata.
   498  		vm.lastAcceptedHeight = 0
   499  		vm.lastAcceptedTime = time.Time{}
   500  		return nil
   501  	}
   502  	if err != nil {
   503  		return err
   504  	}
   505  
   506  	lastAccepted, err := vm.getPostForkBlock(ctx, lastAcceptedID)
   507  	if err != nil {
   508  		return err
   509  	}
   510  
   511  	// Set the last accepted height
   512  	vm.lastAcceptedHeight = lastAccepted.Height()
   513  
   514  	if _, ok := lastAccepted.getStatelessBlk().(statelessblock.SignedBlock); ok {
   515  		// If the last accepted block wasn't a PostForkOption, then we don't
   516  		// initialize the time.
   517  		return nil
   518  	}
   519  
   520  	acceptedParent, err := vm.getPostForkBlock(ctx, lastAccepted.Parent())
   521  	if err != nil {
   522  		return err
   523  	}
   524  	vm.lastAcceptedTime = acceptedParent.Timestamp()
   525  	return nil
   526  }
   527  
   528  func (vm *VM) parsePostForkBlock(ctx context.Context, b []byte) (PostForkBlock, error) {
   529  	statelessBlock, err := statelessblock.Parse(b, vm.ctx.ChainID)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  
   534  	// if the block already exists, then make sure the status is set correctly
   535  	blkID := statelessBlock.ID()
   536  	blk, err := vm.getPostForkBlock(ctx, blkID)
   537  	if err == nil {
   538  		return blk, nil
   539  	}
   540  	if err != database.ErrNotFound {
   541  		return nil, err
   542  	}
   543  
   544  	innerBlkBytes := statelessBlock.Block()
   545  	innerBlk, err := vm.parseInnerBlock(ctx, blkID, innerBlkBytes)
   546  	if err != nil {
   547  		return nil, err
   548  	}
   549  
   550  	if statelessSignedBlock, ok := statelessBlock.(statelessblock.SignedBlock); ok {
   551  		blk = &postForkBlock{
   552  			SignedBlock: statelessSignedBlock,
   553  			postForkCommonComponents: postForkCommonComponents{
   554  				vm:       vm,
   555  				innerBlk: innerBlk,
   556  				status:   choices.Processing,
   557  			},
   558  		}
   559  	} else {
   560  		blk = &postForkOption{
   561  			Block: statelessBlock,
   562  			postForkCommonComponents: postForkCommonComponents{
   563  				vm:       vm,
   564  				innerBlk: innerBlk,
   565  				status:   choices.Processing,
   566  			},
   567  		}
   568  	}
   569  	return blk, nil
   570  }
   571  
   572  func (vm *VM) parsePreForkBlock(ctx context.Context, b []byte) (*preForkBlock, error) {
   573  	blk, err := vm.ChainVM.ParseBlock(ctx, b)
   574  	return &preForkBlock{
   575  		Block: blk,
   576  		vm:    vm,
   577  	}, err
   578  }
   579  
   580  func (vm *VM) getBlock(ctx context.Context, id ids.ID) (Block, error) {
   581  	if blk, err := vm.getPostForkBlock(ctx, id); err == nil {
   582  		return blk, nil
   583  	}
   584  	return vm.getPreForkBlock(ctx, id)
   585  }
   586  
   587  func (vm *VM) getPostForkBlock(ctx context.Context, blkID ids.ID) (PostForkBlock, error) {
   588  	block, exists := vm.verifiedBlocks[blkID]
   589  	if exists {
   590  		return block, nil
   591  	}
   592  
   593  	statelessBlock, status, err := vm.State.GetBlock(blkID)
   594  	if err != nil {
   595  		return nil, err
   596  	}
   597  
   598  	innerBlkBytes := statelessBlock.Block()
   599  	innerBlk, err := vm.parseInnerBlock(ctx, blkID, innerBlkBytes)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  
   604  	if statelessSignedBlock, ok := statelessBlock.(statelessblock.SignedBlock); ok {
   605  		return &postForkBlock{
   606  			SignedBlock: statelessSignedBlock,
   607  			postForkCommonComponents: postForkCommonComponents{
   608  				vm:       vm,
   609  				innerBlk: innerBlk,
   610  				status:   status,
   611  			},
   612  		}, nil
   613  	}
   614  	return &postForkOption{
   615  		Block: statelessBlock,
   616  		postForkCommonComponents: postForkCommonComponents{
   617  			vm:       vm,
   618  			innerBlk: innerBlk,
   619  			status:   status,
   620  		},
   621  	}, nil
   622  }
   623  
   624  func (vm *VM) getPreForkBlock(ctx context.Context, blkID ids.ID) (*preForkBlock, error) {
   625  	blk, err := vm.ChainVM.GetBlock(ctx, blkID)
   626  	return &preForkBlock{
   627  		Block: blk,
   628  		vm:    vm,
   629  	}, err
   630  }
   631  
   632  func (vm *VM) acceptPostForkBlock(blk PostForkBlock) error {
   633  	height := blk.Height()
   634  	blkID := blk.ID()
   635  
   636  	vm.lastAcceptedHeight = height
   637  	delete(vm.verifiedBlocks, blkID)
   638  
   639  	// Persist this block, its height index, and its status
   640  	if err := vm.State.SetLastAccepted(blkID); err != nil {
   641  		return err
   642  	}
   643  	if err := vm.State.PutBlock(blk.getStatelessBlk(), choices.Accepted); err != nil {
   644  		return err
   645  	}
   646  	if err := vm.updateHeightIndex(height, blkID); err != nil {
   647  		return err
   648  	}
   649  	return vm.db.Commit()
   650  }
   651  
   652  func (vm *VM) verifyAndRecordInnerBlk(ctx context.Context, blockCtx *block.Context, postFork PostForkBlock) error {
   653  	innerBlk := postFork.getInnerBlk()
   654  	postForkID := postFork.ID()
   655  	originalInnerBlock, previouslyVerified := vm.Tree.Get(innerBlk)
   656  	if previouslyVerified {
   657  		innerBlk = originalInnerBlock
   658  		// We must update all of the mappings from postFork -> innerBlock to
   659  		// now point to originalInnerBlock.
   660  		postFork.setInnerBlk(originalInnerBlock)
   661  		vm.innerBlkCache.Put(postForkID, originalInnerBlock)
   662  	}
   663  
   664  	var (
   665  		shouldVerifyWithCtx = blockCtx != nil
   666  		blkWithCtx          block.WithVerifyContext
   667  		err                 error
   668  	)
   669  	if shouldVerifyWithCtx {
   670  		blkWithCtx, shouldVerifyWithCtx = innerBlk.(block.WithVerifyContext)
   671  		if shouldVerifyWithCtx {
   672  			shouldVerifyWithCtx, err = blkWithCtx.ShouldVerifyWithContext(ctx)
   673  			if err != nil {
   674  				return err
   675  			}
   676  		}
   677  	}
   678  
   679  	// Invariant: If either [Verify] or [VerifyWithContext] returns nil, this
   680  	//            function must return nil. This maintains the inner block's
   681  	//            invariant that successful verification will eventually result
   682  	//            in accepted or rejected being called.
   683  	if shouldVerifyWithCtx {
   684  		// This block needs to know the P-Chain height during verification.
   685  		// Note that [VerifyWithContext] with context may be called multiple
   686  		// times with multiple contexts.
   687  		err = blkWithCtx.VerifyWithContext(ctx, blockCtx)
   688  	} else if !previouslyVerified {
   689  		// This isn't a [block.WithVerifyContext] so we only call [Verify] once.
   690  		err = innerBlk.Verify(ctx)
   691  	}
   692  	if err != nil {
   693  		return err
   694  	}
   695  
   696  	// Since verification passed, we should ensure the inner block tree is
   697  	// populated.
   698  	if !previouslyVerified {
   699  		vm.Tree.Add(innerBlk)
   700  	}
   701  	vm.verifiedBlocks[postForkID] = postFork
   702  	return nil
   703  }
   704  
   705  // notifyInnerBlockReady tells the scheduler that the inner VM is ready to build
   706  // a new block
   707  func (vm *VM) notifyInnerBlockReady() {
   708  	select {
   709  	case vm.toScheduler <- common.PendingTxs:
   710  	default:
   711  		vm.ctx.Log.Debug("dropping message to consensus engine")
   712  	}
   713  }
   714  
   715  func (vm *VM) optimalPChainHeight(ctx context.Context, minPChainHeight uint64) (uint64, error) {
   716  	minimumHeight, err := vm.ctx.ValidatorState.GetMinimumHeight(ctx)
   717  	if err != nil {
   718  		return 0, err
   719  	}
   720  
   721  	return max(minimumHeight, minPChainHeight), nil
   722  }
   723  
   724  // parseInnerBlock attempts to parse the provided bytes as an inner block. If
   725  // the inner block happens to be cached, then the inner block will not be
   726  // parsed.
   727  func (vm *VM) parseInnerBlock(ctx context.Context, outerBlkID ids.ID, innerBlkBytes []byte) (snowman.Block, error) {
   728  	if innerBlk, ok := vm.innerBlkCache.Get(outerBlkID); ok {
   729  		return innerBlk, nil
   730  	}
   731  
   732  	innerBlk, err := vm.ChainVM.ParseBlock(ctx, innerBlkBytes)
   733  	if err != nil {
   734  		return nil, err
   735  	}
   736  	vm.cacheInnerBlock(outerBlkID, innerBlk)
   737  	return innerBlk, nil
   738  }
   739  
   740  // Caches proposervm block ID --> inner block if the inner block's height
   741  // is within [innerBlkCacheSize] of the last accepted block's height.
   742  func (vm *VM) cacheInnerBlock(outerBlkID ids.ID, innerBlk snowman.Block) {
   743  	diff := math.AbsDiff(innerBlk.Height(), vm.lastAcceptedHeight)
   744  	if diff < innerBlkCacheSize {
   745  		vm.innerBlkCache.Put(outerBlkID, innerBlk)
   746  	}
   747  }