github.com/MetalBlockchain/metalgo@v1.11.9/vms/proposervm/block.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  	"go.uber.org/zap"
    13  
    14  	"github.com/MetalBlockchain/metalgo/ids"
    15  	"github.com/MetalBlockchain/metalgo/snow"
    16  	"github.com/MetalBlockchain/metalgo/snow/choices"
    17  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman"
    18  	"github.com/MetalBlockchain/metalgo/vms/proposervm/block"
    19  	"github.com/MetalBlockchain/metalgo/vms/proposervm/proposer"
    20  
    21  	smblock "github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    22  )
    23  
    24  const (
    25  	// allowable block issuance in the future
    26  	maxSkew = 10 * time.Second
    27  )
    28  
    29  var (
    30  	errUnsignedChild            = errors.New("expected child to be signed")
    31  	errUnexpectedBlockType      = errors.New("unexpected proposer block type")
    32  	errInnerParentMismatch      = errors.New("inner parentID didn't match expected parent")
    33  	errTimeNotMonotonic         = errors.New("time must monotonically increase")
    34  	errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height")
    35  	errPChainHeightNotReached   = errors.New("block P-chain height larger than current P-chain height")
    36  	errTimeTooAdvanced          = errors.New("time is too far advanced")
    37  	errProposerWindowNotStarted = errors.New("proposer window hasn't started")
    38  	errUnexpectedProposer       = errors.New("unexpected proposer for current window")
    39  	errProposerMismatch         = errors.New("proposer mismatch")
    40  	errProposersNotActivated    = errors.New("proposers haven't been activated yet")
    41  	errPChainHeightTooLow       = errors.New("block P-chain height is too low")
    42  )
    43  
    44  type Block interface {
    45  	snowman.Block
    46  
    47  	getInnerBlk() snowman.Block
    48  
    49  	// After a state sync, we may need to update last accepted block data
    50  	// without propagating any changes to the innerVM.
    51  	// acceptOuterBlk and acceptInnerBlk allow controlling acceptance of outer
    52  	// and inner blocks.
    53  	acceptOuterBlk() error
    54  	acceptInnerBlk(context.Context) error
    55  
    56  	verifyPreForkChild(ctx context.Context, child *preForkBlock) error
    57  	verifyPostForkChild(ctx context.Context, child *postForkBlock) error
    58  	verifyPostForkOption(ctx context.Context, child *postForkOption) error
    59  
    60  	buildChild(context.Context) (Block, error)
    61  
    62  	pChainHeight(context.Context) (uint64, error)
    63  }
    64  
    65  type PostForkBlock interface {
    66  	Block
    67  
    68  	setStatus(choices.Status)
    69  	getStatelessBlk() block.Block
    70  	setInnerBlk(snowman.Block)
    71  }
    72  
    73  // field of postForkBlock and postForkOption
    74  type postForkCommonComponents struct {
    75  	vm       *VM
    76  	innerBlk snowman.Block
    77  	status   choices.Status
    78  }
    79  
    80  // Return the inner block's height
    81  func (p *postForkCommonComponents) Height() uint64 {
    82  	return p.innerBlk.Height()
    83  }
    84  
    85  // Verify returns nil if:
    86  // 1) [p]'s inner block is not an oracle block
    87  // 2) [child]'s P-Chain height >= [parentPChainHeight]
    88  // 3) [p]'s inner block is the parent of [c]'s inner block
    89  // 4) [child]'s timestamp isn't before [p]'s timestamp
    90  // 5) [child]'s timestamp is within the skew bound
    91  // 6) [childPChainHeight] <= the current P-Chain height
    92  // 7) [child]'s timestamp is within its proposer's window
    93  // 8) [child] has a valid signature from its proposer
    94  // 9) [child]'s inner block is valid
    95  func (p *postForkCommonComponents) Verify(
    96  	ctx context.Context,
    97  	parentTimestamp time.Time,
    98  	parentPChainHeight uint64,
    99  	child *postForkBlock,
   100  ) error {
   101  	if err := verifyIsNotOracleBlock(ctx, p.innerBlk); err != nil {
   102  		return err
   103  	}
   104  
   105  	childPChainHeight := child.PChainHeight()
   106  	if childPChainHeight < parentPChainHeight {
   107  		return errPChainHeightNotMonotonic
   108  	}
   109  
   110  	expectedInnerParentID := p.innerBlk.ID()
   111  	innerParentID := child.innerBlk.Parent()
   112  	if innerParentID != expectedInnerParentID {
   113  		return errInnerParentMismatch
   114  	}
   115  
   116  	childTimestamp := child.Timestamp()
   117  	if childTimestamp.Before(parentTimestamp) {
   118  		return errTimeNotMonotonic
   119  	}
   120  
   121  	maxTimestamp := p.vm.Time().Add(maxSkew)
   122  	if childTimestamp.After(maxTimestamp) {
   123  		return errTimeTooAdvanced
   124  	}
   125  
   126  	// If the node is currently syncing - we don't assume that the P-chain has
   127  	// been synced up to this point yet.
   128  	if p.vm.consensusState == snow.NormalOp {
   129  		currentPChainHeight, err := p.vm.ctx.ValidatorState.GetCurrentHeight(ctx)
   130  		if err != nil {
   131  			p.vm.ctx.Log.Error("block verification failed",
   132  				zap.String("reason", "failed to get current P-Chain height"),
   133  				zap.Stringer("blkID", child.ID()),
   134  				zap.Error(err),
   135  			)
   136  			return err
   137  		}
   138  		if childPChainHeight > currentPChainHeight {
   139  			return fmt.Errorf("%w: %d > %d",
   140  				errPChainHeightNotReached,
   141  				childPChainHeight,
   142  				currentPChainHeight,
   143  			)
   144  		}
   145  
   146  		var shouldHaveProposer bool
   147  		if p.vm.IsDurangoActivated(parentTimestamp) {
   148  			shouldHaveProposer, err = p.verifyPostDurangoBlockDelay(ctx, parentTimestamp, parentPChainHeight, child)
   149  		} else {
   150  			shouldHaveProposer, err = p.verifyPreDurangoBlockDelay(ctx, parentTimestamp, parentPChainHeight, child)
   151  		}
   152  		if err != nil {
   153  			return err
   154  		}
   155  
   156  		hasProposer := child.SignedBlock.Proposer() != ids.EmptyNodeID
   157  		if shouldHaveProposer != hasProposer {
   158  			return fmt.Errorf("%w: shouldHaveProposer (%v) != hasProposer (%v)", errProposerMismatch, shouldHaveProposer, hasProposer)
   159  		}
   160  
   161  		p.vm.ctx.Log.Debug("verified post-fork block",
   162  			zap.Stringer("blkID", child.ID()),
   163  			zap.Time("parentTimestamp", parentTimestamp),
   164  			zap.Time("blockTimestamp", childTimestamp),
   165  		)
   166  	}
   167  
   168  	return p.vm.verifyAndRecordInnerBlk(
   169  		ctx,
   170  		&smblock.Context{
   171  			PChainHeight: parentPChainHeight,
   172  		},
   173  		child,
   174  	)
   175  }
   176  
   177  // Return the child (a *postForkBlock) of this block
   178  func (p *postForkCommonComponents) buildChild(
   179  	ctx context.Context,
   180  	parentID ids.ID,
   181  	parentTimestamp time.Time,
   182  	parentPChainHeight uint64,
   183  ) (Block, error) {
   184  	// Child's timestamp is the later of now and this block's timestamp
   185  	newTimestamp := p.vm.Time().Truncate(time.Second)
   186  	if newTimestamp.Before(parentTimestamp) {
   187  		newTimestamp = parentTimestamp
   188  	}
   189  
   190  	// The child's P-Chain height is proposed as the optimal P-Chain height that
   191  	// is at least the parent's P-Chain height
   192  	pChainHeight, err := p.vm.optimalPChainHeight(ctx, parentPChainHeight)
   193  	if err != nil {
   194  		p.vm.ctx.Log.Error("unexpected build block failure",
   195  			zap.String("reason", "failed to calculate optimal P-chain height"),
   196  			zap.Stringer("parentID", parentID),
   197  			zap.Error(err),
   198  		)
   199  		return nil, err
   200  	}
   201  
   202  	var shouldBuildSignedBlock bool
   203  	if p.vm.IsDurangoActivated(parentTimestamp) {
   204  		shouldBuildSignedBlock, err = p.shouldBuildSignedBlockPostDurango(
   205  			ctx,
   206  			parentID,
   207  			parentTimestamp,
   208  			parentPChainHeight,
   209  			newTimestamp,
   210  		)
   211  	} else {
   212  		shouldBuildSignedBlock, err = p.shouldBuildSignedBlockPreDurango(
   213  			ctx,
   214  			parentID,
   215  			parentTimestamp,
   216  			parentPChainHeight,
   217  			newTimestamp,
   218  		)
   219  	}
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	var innerBlock snowman.Block
   225  	if p.vm.blockBuilderVM != nil {
   226  		innerBlock, err = p.vm.blockBuilderVM.BuildBlockWithContext(ctx, &smblock.Context{
   227  			PChainHeight: parentPChainHeight,
   228  		})
   229  	} else {
   230  		innerBlock, err = p.vm.ChainVM.BuildBlock(ctx)
   231  	}
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	// Build the child
   237  	var statelessChild block.SignedBlock
   238  	if shouldBuildSignedBlock {
   239  		statelessChild, err = block.Build(
   240  			parentID,
   241  			newTimestamp,
   242  			pChainHeight,
   243  			p.vm.StakingCertLeaf,
   244  			innerBlock.Bytes(),
   245  			p.vm.ctx.ChainID,
   246  			p.vm.StakingLeafSigner,
   247  		)
   248  	} else {
   249  		statelessChild, err = block.BuildUnsigned(
   250  			parentID,
   251  			newTimestamp,
   252  			pChainHeight,
   253  			innerBlock.Bytes(),
   254  		)
   255  	}
   256  	if err != nil {
   257  		p.vm.ctx.Log.Error("unexpected build block failure",
   258  			zap.String("reason", "failed to generate proposervm block header"),
   259  			zap.Stringer("parentID", parentID),
   260  			zap.Stringer("blkID", innerBlock.ID()),
   261  			zap.Error(err),
   262  		)
   263  		return nil, err
   264  	}
   265  
   266  	child := &postForkBlock{
   267  		SignedBlock: statelessChild,
   268  		postForkCommonComponents: postForkCommonComponents{
   269  			vm:       p.vm,
   270  			innerBlk: innerBlock,
   271  			status:   choices.Processing,
   272  		},
   273  	}
   274  
   275  	p.vm.ctx.Log.Info("built block",
   276  		zap.Stringer("blkID", child.ID()),
   277  		zap.Stringer("innerBlkID", innerBlock.ID()),
   278  		zap.Uint64("height", child.Height()),
   279  		zap.Uint64("pChainHeight", pChainHeight),
   280  		zap.Time("parentTimestamp", parentTimestamp),
   281  		zap.Time("blockTimestamp", newTimestamp),
   282  	)
   283  	return child, nil
   284  }
   285  
   286  func (p *postForkCommonComponents) getInnerBlk() snowman.Block {
   287  	return p.innerBlk
   288  }
   289  
   290  func (p *postForkCommonComponents) setInnerBlk(innerBlk snowman.Block) {
   291  	p.innerBlk = innerBlk
   292  }
   293  
   294  func verifyIsOracleBlock(ctx context.Context, b snowman.Block) error {
   295  	oracle, ok := b.(snowman.OracleBlock)
   296  	if !ok {
   297  		return fmt.Errorf(
   298  			"%w: expected block %s to be a snowman.OracleBlock but it's a %T",
   299  			errUnexpectedBlockType, b.ID(), b,
   300  		)
   301  	}
   302  	_, err := oracle.Options(ctx)
   303  	return err
   304  }
   305  
   306  func verifyIsNotOracleBlock(ctx context.Context, b snowman.Block) error {
   307  	oracle, ok := b.(snowman.OracleBlock)
   308  	if !ok {
   309  		return nil
   310  	}
   311  	_, err := oracle.Options(ctx)
   312  	switch err {
   313  	case nil:
   314  		return fmt.Errorf(
   315  			"%w: expected block %s not to be an oracle block but it's a %T",
   316  			errUnexpectedBlockType, b.ID(), b,
   317  		)
   318  	case snowman.ErrNotOracle:
   319  		return nil
   320  	default:
   321  		return err
   322  	}
   323  }
   324  
   325  func (p *postForkCommonComponents) verifyPreDurangoBlockDelay(
   326  	ctx context.Context,
   327  	parentTimestamp time.Time,
   328  	parentPChainHeight uint64,
   329  	blk *postForkBlock,
   330  ) (bool, error) {
   331  	var (
   332  		blkTimestamp = blk.Timestamp()
   333  		childHeight  = blk.Height()
   334  		proposerID   = blk.Proposer()
   335  	)
   336  	minDelay, err := p.vm.Windower.Delay(
   337  		ctx,
   338  		childHeight,
   339  		parentPChainHeight,
   340  		proposerID,
   341  		proposer.MaxVerifyWindows,
   342  	)
   343  	if err != nil {
   344  		p.vm.ctx.Log.Error("unexpected block verification failure",
   345  			zap.String("reason", "failed to calculate required timestamp delay"),
   346  			zap.Stringer("blkID", blk.ID()),
   347  			zap.Error(err),
   348  		)
   349  		return false, err
   350  	}
   351  
   352  	delay := blkTimestamp.Sub(parentTimestamp)
   353  	if delay < minDelay {
   354  		return false, fmt.Errorf("%w: delay %s < minDelay %s", errProposerWindowNotStarted, delay, minDelay)
   355  	}
   356  
   357  	return delay < proposer.MaxVerifyDelay, nil
   358  }
   359  
   360  func (p *postForkCommonComponents) verifyPostDurangoBlockDelay(
   361  	ctx context.Context,
   362  	parentTimestamp time.Time,
   363  	parentPChainHeight uint64,
   364  	blk *postForkBlock,
   365  ) (bool, error) {
   366  	var (
   367  		blkTimestamp = blk.Timestamp()
   368  		blkHeight    = blk.Height()
   369  		currentSlot  = proposer.TimeToSlot(parentTimestamp, blkTimestamp)
   370  		proposerID   = blk.Proposer()
   371  	)
   372  	// populate the slot for the block.
   373  	blk.slot = &currentSlot
   374  
   375  	// find the expected proposer
   376  	expectedProposerID, err := p.vm.Windower.ExpectedProposer(
   377  		ctx,
   378  		blkHeight,
   379  		parentPChainHeight,
   380  		currentSlot,
   381  	)
   382  	switch {
   383  	case errors.Is(err, proposer.ErrAnyoneCanPropose):
   384  		return false, nil // block should be unsigned
   385  	case err != nil:
   386  		p.vm.ctx.Log.Error("unexpected block verification failure",
   387  			zap.String("reason", "failed to calculate expected proposer"),
   388  			zap.Stringer("blkID", blk.ID()),
   389  			zap.Error(err),
   390  		)
   391  		return false, err
   392  	case expectedProposerID == proposerID:
   393  		return true, nil // block should be signed
   394  	default:
   395  		return false, fmt.Errorf("%w: slot %d expects %s", errUnexpectedProposer, currentSlot, expectedProposerID)
   396  	}
   397  }
   398  
   399  func (p *postForkCommonComponents) shouldBuildSignedBlockPostDurango(
   400  	ctx context.Context,
   401  	parentID ids.ID,
   402  	parentTimestamp time.Time,
   403  	parentPChainHeight uint64,
   404  	newTimestamp time.Time,
   405  ) (bool, error) {
   406  	parentHeight := p.innerBlk.Height()
   407  	currentSlot := proposer.TimeToSlot(parentTimestamp, newTimestamp)
   408  	expectedProposerID, err := p.vm.Windower.ExpectedProposer(
   409  		ctx,
   410  		parentHeight+1,
   411  		parentPChainHeight,
   412  		currentSlot,
   413  	)
   414  	switch {
   415  	case errors.Is(err, proposer.ErrAnyoneCanPropose):
   416  		return false, nil // build an unsigned block
   417  	case err != nil:
   418  		p.vm.ctx.Log.Error("unexpected build block failure",
   419  			zap.String("reason", "failed to calculate expected proposer"),
   420  			zap.Stringer("parentID", parentID),
   421  			zap.Error(err),
   422  		)
   423  		return false, err
   424  	case expectedProposerID == p.vm.ctx.NodeID:
   425  		return true, nil // build a signed block
   426  	}
   427  
   428  	// It's not our turn to propose a block yet. This is likely caused by having
   429  	// previously notified the consensus engine to attempt to build a block on
   430  	// top of a block that is no longer the preferred block.
   431  	p.vm.ctx.Log.Debug("build block dropped",
   432  		zap.Time("parentTimestamp", parentTimestamp),
   433  		zap.Time("blockTimestamp", newTimestamp),
   434  		zap.Uint64("slot", currentSlot),
   435  		zap.Stringer("expectedProposer", expectedProposerID),
   436  	)
   437  
   438  	// We need to reschedule the block builder to the next time we can try to
   439  	// build a block.
   440  	//
   441  	// TODO: After Durango activates, restructure this logic to separate
   442  	// updating the scheduler from verifying the proposerID.
   443  	nextStartTime, err := p.vm.getPostDurangoSlotTime(
   444  		ctx,
   445  		parentHeight+1,
   446  		parentPChainHeight,
   447  		currentSlot+1, // We know we aren't the proposer for the current slot
   448  		parentTimestamp,
   449  	)
   450  	if err != nil {
   451  		p.vm.ctx.Log.Error("failed to reset block builder scheduler",
   452  			zap.String("reason", "failed to calculate expected proposer"),
   453  			zap.Stringer("parentID", parentID),
   454  			zap.Error(err),
   455  		)
   456  		return false, err
   457  	}
   458  
   459  	// report the build slot to the metrics.
   460  	p.vm.proposerBuildSlotGauge.Set(float64(proposer.TimeToSlot(parentTimestamp, nextStartTime)))
   461  
   462  	// set the scheduler to let us know when the next block need to be built.
   463  	p.vm.Scheduler.SetBuildBlockTime(nextStartTime)
   464  
   465  	// In case the inner VM only issued one pendingTxs message, we should
   466  	// attempt to re-handle that once it is our turn to build the block.
   467  	p.vm.notifyInnerBlockReady()
   468  	return false, fmt.Errorf("%w: slot %d expects %s", errUnexpectedProposer, currentSlot, expectedProposerID)
   469  }
   470  
   471  func (p *postForkCommonComponents) shouldBuildSignedBlockPreDurango(
   472  	ctx context.Context,
   473  	parentID ids.ID,
   474  	parentTimestamp time.Time,
   475  	parentPChainHeight uint64,
   476  	newTimestamp time.Time,
   477  ) (bool, error) {
   478  	delay := newTimestamp.Sub(parentTimestamp)
   479  	if delay >= proposer.MaxBuildDelay {
   480  		return false, nil // time for any node to build an unsigned block
   481  	}
   482  
   483  	parentHeight := p.innerBlk.Height()
   484  	proposerID := p.vm.ctx.NodeID
   485  	minDelay, err := p.vm.Windower.Delay(ctx, parentHeight+1, parentPChainHeight, proposerID, proposer.MaxBuildWindows)
   486  	if err != nil {
   487  		p.vm.ctx.Log.Error("unexpected build block failure",
   488  			zap.String("reason", "failed to calculate required timestamp delay"),
   489  			zap.Stringer("parentID", parentID),
   490  			zap.Error(err),
   491  		)
   492  		return false, err
   493  	}
   494  
   495  	if delay >= minDelay {
   496  		// it's time for this node to propose a block. It'll be signed or
   497  		// unsigned depending on the delay
   498  		return delay < proposer.MaxVerifyDelay, nil
   499  	}
   500  
   501  	// It's not our turn to propose a block yet. This is likely caused by having
   502  	// previously notified the consensus engine to attempt to build a block on
   503  	// top of a block that is no longer the preferred block.
   504  	p.vm.ctx.Log.Debug("build block dropped",
   505  		zap.Time("parentTimestamp", parentTimestamp),
   506  		zap.Duration("minDelay", minDelay),
   507  		zap.Time("blockTimestamp", newTimestamp),
   508  	)
   509  
   510  	// In case the inner VM only issued one pendingTxs message, we should
   511  	// attempt to re-handle that once it is our turn to build the block.
   512  	p.vm.notifyInnerBlockReady()
   513  	return false, fmt.Errorf("%w: delay %s < minDelay %s", errProposerWindowNotStarted, delay, minDelay)
   514  }