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