github.com/MetalBlockchain/metalgo@v1.11.9/vms/platformvm/block/builder/builder.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package builder
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"sync"
    11  	"time"
    12  
    13  	"go.uber.org/zap"
    14  
    15  	"github.com/MetalBlockchain/metalgo/ids"
    16  	"github.com/MetalBlockchain/metalgo/snow"
    17  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman"
    18  	"github.com/MetalBlockchain/metalgo/utils/set"
    19  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    20  	"github.com/MetalBlockchain/metalgo/utils/units"
    21  	"github.com/MetalBlockchain/metalgo/vms/platformvm/block"
    22  	"github.com/MetalBlockchain/metalgo/vms/platformvm/state"
    23  	"github.com/MetalBlockchain/metalgo/vms/platformvm/status"
    24  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs"
    25  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs/mempool"
    26  
    27  	blockexecutor "github.com/MetalBlockchain/metalgo/vms/platformvm/block/executor"
    28  	txexecutor "github.com/MetalBlockchain/metalgo/vms/platformvm/txs/executor"
    29  )
    30  
    31  // targetBlockSize is maximum number of transaction bytes to place into a
    32  // StandardBlock
    33  const targetBlockSize = 128 * units.KiB
    34  
    35  var (
    36  	_ Builder = (*builder)(nil)
    37  
    38  	ErrEndOfTime                 = errors.New("program time is suspiciously far in the future")
    39  	ErrNoPendingBlocks           = errors.New("no pending blocks")
    40  	errMissingPreferredState     = errors.New("missing preferred block state")
    41  	errCalculatingNextStakerTime = errors.New("failed calculating next staker time")
    42  )
    43  
    44  type Builder interface {
    45  	mempool.Mempool
    46  
    47  	// StartBlockTimer starts to issue block creation requests to advance the
    48  	// chain timestamp.
    49  	StartBlockTimer()
    50  
    51  	// ResetBlockTimer forces the block timer to recalculate when it should
    52  	// advance the chain timestamp.
    53  	ResetBlockTimer()
    54  
    55  	// ShutdownBlockTimer stops block creation requests to advance the chain
    56  	// timestamp.
    57  	//
    58  	// Invariant: Assumes the context lock is held when calling.
    59  	ShutdownBlockTimer()
    60  
    61  	// BuildBlock can be called to attempt to create a new block
    62  	BuildBlock(context.Context) (snowman.Block, error)
    63  
    64  	// PackBlockTxs returns an array of txs that can fit into a valid block of
    65  	// size [targetBlockSize]. The returned txs are all verified against the
    66  	// preferred state.
    67  	//
    68  	// Note: This function does not call the consensus engine.
    69  	PackBlockTxs(targetBlockSize int) ([]*txs.Tx, error)
    70  }
    71  
    72  // builder implements a simple builder to convert txs into valid blocks
    73  type builder struct {
    74  	mempool.Mempool
    75  
    76  	txExecutorBackend *txexecutor.Backend
    77  	blkManager        blockexecutor.Manager
    78  
    79  	// resetTimer is used to signal that the block builder timer should update
    80  	// when it will trigger building of a block.
    81  	resetTimer chan struct{}
    82  	closed     chan struct{}
    83  	closeOnce  sync.Once
    84  }
    85  
    86  func New(
    87  	mempool mempool.Mempool,
    88  	txExecutorBackend *txexecutor.Backend,
    89  	blkManager blockexecutor.Manager,
    90  ) Builder {
    91  	return &builder{
    92  		Mempool:           mempool,
    93  		txExecutorBackend: txExecutorBackend,
    94  		blkManager:        blkManager,
    95  		resetTimer:        make(chan struct{}, 1),
    96  		closed:            make(chan struct{}),
    97  	}
    98  }
    99  
   100  func (b *builder) StartBlockTimer() {
   101  	go func() {
   102  		timer := time.NewTimer(0)
   103  		defer timer.Stop()
   104  
   105  		for {
   106  			// Invariant: The [timer] is not stopped.
   107  			select {
   108  			case <-timer.C:
   109  			case <-b.resetTimer:
   110  				if !timer.Stop() {
   111  					<-timer.C
   112  				}
   113  			case <-b.closed:
   114  				return
   115  			}
   116  
   117  			// Note: Because the context lock is not held here, it is possible
   118  			// that [ShutdownBlockTimer] is called concurrently with this
   119  			// execution.
   120  			for {
   121  				duration, err := b.durationToSleep()
   122  				if err != nil {
   123  					b.txExecutorBackend.Ctx.Log.Error("block builder encountered a fatal error",
   124  						zap.Error(err),
   125  					)
   126  					return
   127  				}
   128  
   129  				if duration > 0 {
   130  					timer.Reset(duration)
   131  					break
   132  				}
   133  
   134  				// Block needs to be issued to advance time.
   135  				b.Mempool.RequestBuildBlock(true /*=emptyBlockPermitted*/)
   136  
   137  				// Invariant: ResetBlockTimer is guaranteed to be called after
   138  				// [durationToSleep] returns a value <= 0. This is because we
   139  				// are guaranteed to attempt to build block. After building a
   140  				// valid block, the chain will have its preference updated which
   141  				// may change the duration to sleep and trigger a timer reset.
   142  				select {
   143  				case <-b.resetTimer:
   144  				case <-b.closed:
   145  					return
   146  				}
   147  			}
   148  		}
   149  	}()
   150  }
   151  
   152  func (b *builder) durationToSleep() (time.Duration, error) {
   153  	// Grabbing the lock here enforces that this function is not called mid-way
   154  	// through modifying of the state.
   155  	b.txExecutorBackend.Ctx.Lock.Lock()
   156  	defer b.txExecutorBackend.Ctx.Lock.Unlock()
   157  
   158  	// If [ShutdownBlockTimer] was called, we want to exit the block timer
   159  	// goroutine. We check this with the context lock held because
   160  	// [ShutdownBlockTimer] is expected to only be called with the context lock
   161  	// held.
   162  	select {
   163  	case <-b.closed:
   164  		return 0, nil
   165  	default:
   166  	}
   167  
   168  	preferredID := b.blkManager.Preferred()
   169  	preferredState, ok := b.blkManager.GetState(preferredID)
   170  	if !ok {
   171  		return 0, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID)
   172  	}
   173  
   174  	nextStakerChangeTime, err := state.GetNextStakerChangeTime(preferredState)
   175  	if err != nil {
   176  		return 0, fmt.Errorf("%w of %s: %w", errCalculatingNextStakerTime, preferredID, err)
   177  	}
   178  
   179  	now := b.txExecutorBackend.Clk.Time()
   180  	return nextStakerChangeTime.Sub(now), nil
   181  }
   182  
   183  func (b *builder) ResetBlockTimer() {
   184  	// Ensure that the timer will be reset at least once.
   185  	select {
   186  	case b.resetTimer <- struct{}{}:
   187  	default:
   188  	}
   189  }
   190  
   191  func (b *builder) ShutdownBlockTimer() {
   192  	b.closeOnce.Do(func() {
   193  		close(b.closed)
   194  	})
   195  }
   196  
   197  // BuildBlock builds a block to be added to consensus.
   198  // This method removes the transactions from the returned
   199  // blocks from the mempool.
   200  func (b *builder) BuildBlock(context.Context) (snowman.Block, error) {
   201  	// If there are still transactions in the mempool, then we need to
   202  	// re-trigger block building.
   203  	defer b.Mempool.RequestBuildBlock(false /*=emptyBlockPermitted*/)
   204  
   205  	b.txExecutorBackend.Ctx.Log.Debug("starting to attempt to build a block")
   206  
   207  	// Get the block to build on top of and retrieve the new block's context.
   208  	preferredID := b.blkManager.Preferred()
   209  	preferred, err := b.blkManager.GetBlock(preferredID)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	nextHeight := preferred.Height() + 1
   214  	preferredState, ok := b.blkManager.GetState(preferredID)
   215  	if !ok {
   216  		return nil, fmt.Errorf("%w: %s", state.ErrMissingParentState, preferredID)
   217  	}
   218  
   219  	timestamp, timeWasCapped, err := state.NextBlockTime(preferredState, b.txExecutorBackend.Clk)
   220  	if err != nil {
   221  		return nil, fmt.Errorf("could not calculate next staker change time: %w", err)
   222  	}
   223  
   224  	statelessBlk, err := buildBlock(
   225  		b,
   226  		preferredID,
   227  		nextHeight,
   228  		timestamp,
   229  		timeWasCapped,
   230  		preferredState,
   231  	)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	return b.blkManager.NewBlock(statelessBlk), nil
   237  }
   238  
   239  func (b *builder) PackBlockTxs(targetBlockSize int) ([]*txs.Tx, error) {
   240  	preferredID := b.blkManager.Preferred()
   241  	preferredState, ok := b.blkManager.GetState(preferredID)
   242  	if !ok {
   243  		return nil, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID)
   244  	}
   245  
   246  	return packBlockTxs(
   247  		preferredID,
   248  		preferredState,
   249  		b.Mempool,
   250  		b.txExecutorBackend,
   251  		b.blkManager,
   252  		b.txExecutorBackend.Clk.Time(),
   253  		targetBlockSize,
   254  	)
   255  }
   256  
   257  // [timestamp] is min(max(now, parent timestamp), next staker change time)
   258  func buildBlock(
   259  	builder *builder,
   260  	parentID ids.ID,
   261  	height uint64,
   262  	timestamp time.Time,
   263  	forceAdvanceTime bool,
   264  	parentState state.Chain,
   265  ) (block.Block, error) {
   266  	blockTxs, err := packBlockTxs(
   267  		parentID,
   268  		parentState,
   269  		builder.Mempool,
   270  		builder.txExecutorBackend,
   271  		builder.blkManager,
   272  		timestamp,
   273  		targetBlockSize,
   274  	)
   275  	if err != nil {
   276  		return nil, fmt.Errorf("failed to pack block txs: %w", err)
   277  	}
   278  
   279  	// Try rewarding stakers whose staking period ends at the new chain time.
   280  	// This is done first to prioritize advancing the timestamp as quickly as
   281  	// possible.
   282  	stakerTxID, shouldReward, err := getNextStakerToReward(timestamp, parentState)
   283  	if err != nil {
   284  		return nil, fmt.Errorf("could not find next staker to reward: %w", err)
   285  	}
   286  	if shouldReward {
   287  		rewardValidatorTx, err := NewRewardValidatorTx(builder.txExecutorBackend.Ctx, stakerTxID)
   288  		if err != nil {
   289  			return nil, fmt.Errorf("could not build tx to reward staker: %w", err)
   290  		}
   291  
   292  		return block.NewBanffProposalBlock(
   293  			timestamp,
   294  			parentID,
   295  			height,
   296  			rewardValidatorTx,
   297  			blockTxs,
   298  		)
   299  	}
   300  
   301  	// If there is no reason to build a block, don't.
   302  	if len(blockTxs) == 0 && !forceAdvanceTime {
   303  		builder.txExecutorBackend.Ctx.Log.Debug("no pending txs to issue into a block")
   304  		return nil, ErrNoPendingBlocks
   305  	}
   306  
   307  	// Issue a block with as many transactions as possible.
   308  	return block.NewBanffStandardBlock(
   309  		timestamp,
   310  		parentID,
   311  		height,
   312  		blockTxs,
   313  	)
   314  }
   315  
   316  func packBlockTxs(
   317  	parentID ids.ID,
   318  	parentState state.Chain,
   319  	mempool mempool.Mempool,
   320  	backend *txexecutor.Backend,
   321  	manager blockexecutor.Manager,
   322  	timestamp time.Time,
   323  	remainingSize int,
   324  ) ([]*txs.Tx, error) {
   325  	stateDiff, err := state.NewDiffOn(parentState)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	if _, err := txexecutor.AdvanceTimeTo(backend, stateDiff, timestamp); err != nil {
   331  		return nil, err
   332  	}
   333  
   334  	var (
   335  		blockTxs []*txs.Tx
   336  		inputs   set.Set[ids.ID]
   337  	)
   338  
   339  	for {
   340  		tx, exists := mempool.Peek()
   341  		if !exists {
   342  			break
   343  		}
   344  		txSize := len(tx.Bytes())
   345  		if txSize > remainingSize {
   346  			break
   347  		}
   348  		mempool.Remove(tx)
   349  
   350  		// Invariant: [tx] has already been syntactically verified.
   351  
   352  		txDiff, err := state.NewDiffOn(stateDiff)
   353  		if err != nil {
   354  			return nil, err
   355  		}
   356  
   357  		executor := &txexecutor.StandardTxExecutor{
   358  			Backend: backend,
   359  			State:   txDiff,
   360  			Tx:      tx,
   361  		}
   362  
   363  		err = tx.Unsigned.Visit(executor)
   364  		if err != nil {
   365  			txID := tx.ID()
   366  			mempool.MarkDropped(txID, err)
   367  			continue
   368  		}
   369  
   370  		if inputs.Overlaps(executor.Inputs) {
   371  			txID := tx.ID()
   372  			mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs)
   373  			continue
   374  		}
   375  		err = manager.VerifyUniqueInputs(parentID, executor.Inputs)
   376  		if err != nil {
   377  			txID := tx.ID()
   378  			mempool.MarkDropped(txID, err)
   379  			continue
   380  		}
   381  		inputs.Union(executor.Inputs)
   382  
   383  		txDiff.AddTx(tx, status.Committed)
   384  		err = txDiff.Apply(stateDiff)
   385  		if err != nil {
   386  			return nil, err
   387  		}
   388  
   389  		remainingSize -= txSize
   390  		blockTxs = append(blockTxs, tx)
   391  	}
   392  
   393  	return blockTxs, nil
   394  }
   395  
   396  // getNextStakerToReward returns the next staker txID to remove from the staking
   397  // set with a RewardValidatorTx rather than an AdvanceTimeTx. [chainTimestamp]
   398  // is the timestamp of the chain at the time this validator would be getting
   399  // removed and is used to calculate [shouldReward].
   400  // Returns:
   401  // - [txID] of the next staker to reward
   402  // - [shouldReward] if the txID exists and is ready to be rewarded
   403  // - [err] if something bad happened
   404  func getNextStakerToReward(
   405  	chainTimestamp time.Time,
   406  	preferredState state.Chain,
   407  ) (ids.ID, bool, error) {
   408  	if !chainTimestamp.Before(mockable.MaxTime) {
   409  		return ids.Empty, false, ErrEndOfTime
   410  	}
   411  
   412  	currentStakerIterator, err := preferredState.GetCurrentStakerIterator()
   413  	if err != nil {
   414  		return ids.Empty, false, err
   415  	}
   416  	defer currentStakerIterator.Release()
   417  
   418  	for currentStakerIterator.Next() {
   419  		currentStaker := currentStakerIterator.Value()
   420  		priority := currentStaker.Priority
   421  		// If the staker is a permissionless staker (not a permissioned subnet
   422  		// validator), it's the next staker we will want to remove with a
   423  		// RewardValidatorTx rather than an AdvanceTimeTx.
   424  		if priority != txs.SubnetPermissionedValidatorCurrentPriority {
   425  			return currentStaker.TxID, chainTimestamp.Equal(currentStaker.EndTime), nil
   426  		}
   427  	}
   428  	return ids.Empty, false, nil
   429  }
   430  
   431  func NewRewardValidatorTx(ctx *snow.Context, txID ids.ID) (*txs.Tx, error) {
   432  	utx := &txs.RewardValidatorTx{TxID: txID}
   433  	tx, err := txs.NewSigned(utx, txs.Codec, nil)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  	return tx, tx.SyntacticVerify(ctx)
   438  }