github.com/ethereum-optimism/optimism@v1.7.2/op-node/rollup/derive/batch_queue.go (about)

     1  package derive
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  
     9  	"github.com/ethereum/go-ethereum/log"
    10  
    11  	"github.com/ethereum-optimism/optimism/op-node/rollup"
    12  	"github.com/ethereum-optimism/optimism/op-service/eth"
    13  )
    14  
    15  // The batch queue is responsible for ordering unordered batches & generating empty batches
    16  // when the sequence window has passed. This is a very stateful stage.
    17  //
    18  // It receives batches that are tagged with the L1 Inclusion block of the batch. It only considers
    19  // batches that are inside the sequencing window of a specific L1 Origin.
    20  // It tries to eagerly pull batches based on the current L2 safe head.
    21  // Otherwise it filters/creates an entire epoch's worth of batches at once.
    22  //
    23  // This stage tracks a range of L1 blocks with the assumption that all batches with an L1 inclusion
    24  // block inside that range have been added to the stage by the time that it attempts to advance a
    25  // full epoch.
    26  //
    27  // It is internally responsible for making sure that batches with L1 inclusions block outside it's
    28  // working range are not considered or pruned.
    29  
    30  type NextBatchProvider interface {
    31  	Origin() eth.L1BlockRef
    32  	NextBatch(ctx context.Context) (Batch, error)
    33  }
    34  
    35  type SafeBlockFetcher interface {
    36  	L2BlockRefByNumber(context.Context, uint64) (eth.L2BlockRef, error)
    37  	PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayloadEnvelope, error)
    38  }
    39  
    40  // BatchQueue contains a set of batches for every L1 block.
    41  // L1 blocks are contiguous and this does not support reorgs.
    42  type BatchQueue struct {
    43  	log    log.Logger
    44  	config *rollup.Config
    45  	prev   NextBatchProvider
    46  	origin eth.L1BlockRef
    47  
    48  	// l1Blocks contains consecutive eth.L1BlockRef sorted by time.
    49  	// Every L1 origin of unsafe L2 blocks must be eventually included in l1Blocks.
    50  	// Batch queue's job is to ensure below two rules:
    51  	//  If every L2 block corresponding to single L1 block becomes safe, it will be popped from l1Blocks.
    52  	//  If new L2 block's L1 origin is not included in l1Blocks, fetch and push to l1Blocks.
    53  	// length of l1Blocks never exceeds SequencerWindowSize
    54  	l1Blocks []eth.L1BlockRef
    55  
    56  	// batches in order of when we've first seen them
    57  	batches []*BatchWithL1InclusionBlock
    58  
    59  	// nextSpan is cached SingularBatches derived from SpanBatch
    60  	nextSpan []*SingularBatch
    61  
    62  	l2 SafeBlockFetcher
    63  }
    64  
    65  // NewBatchQueue creates a BatchQueue, which should be Reset(origin) before use.
    66  func NewBatchQueue(log log.Logger, cfg *rollup.Config, prev NextBatchProvider, l2 SafeBlockFetcher) *BatchQueue {
    67  	return &BatchQueue{
    68  		log:    log,
    69  		config: cfg,
    70  		prev:   prev,
    71  		l2:     l2,
    72  	}
    73  }
    74  
    75  func (bq *BatchQueue) Origin() eth.L1BlockRef {
    76  	return bq.prev.Origin()
    77  }
    78  
    79  // popNextBatch pops the next batch from the current queued up span-batch nextSpan.
    80  // The queue must be non-empty, or the function will panic.
    81  func (bq *BatchQueue) popNextBatch(parent eth.L2BlockRef) *SingularBatch {
    82  	if len(bq.nextSpan) == 0 {
    83  		panic("popping non-existent span-batch, invalid state")
    84  	}
    85  	nextBatch := bq.nextSpan[0]
    86  	bq.nextSpan = bq.nextSpan[1:]
    87  	// Must set ParentHash before return. we can use parent because the parentCheck is verified in CheckBatch().
    88  	nextBatch.ParentHash = parent.Hash
    89  	bq.log.Debug("pop next batch from the cached span batch")
    90  	return nextBatch
    91  }
    92  
    93  // NextBatch return next valid batch upon the given safe head.
    94  // It also returns the boolean that indicates if the batch is the last block in the batch.
    95  func (bq *BatchQueue) NextBatch(ctx context.Context, parent eth.L2BlockRef) (*SingularBatch, bool, error) {
    96  	if len(bq.nextSpan) > 0 {
    97  		// There are cached singular batches derived from the span batch.
    98  		// Check if the next cached batch matches the given parent block.
    99  		if bq.nextSpan[0].Timestamp == parent.Time+bq.config.BlockTime {
   100  			// Pop first one and return.
   101  			nextBatch := bq.popNextBatch(parent)
   102  			// len(bq.nextSpan) == 0 means it's the last batch of the span.
   103  			return nextBatch, len(bq.nextSpan) == 0, nil
   104  		} else {
   105  			// Given parent block does not match the next batch. It means the previously returned batch is invalid.
   106  			// Drop cached batches and find another batch.
   107  			bq.log.Warn("parent block does not match the next batch. dropped cached batches", "parent", parent.ID(), "nextBatchTime", bq.nextSpan[0].GetTimestamp())
   108  			bq.nextSpan = bq.nextSpan[:0]
   109  		}
   110  	}
   111  
   112  	// If the epoch is advanced, update bq.l1Blocks
   113  	// Advancing epoch must be done after the pipeline successfully apply the entire span batch to the chain.
   114  	// Because the span batch can be reverted during processing the batch, then we must preserve existing l1Blocks
   115  	// to verify the epochs of the next candidate batch.
   116  	if len(bq.l1Blocks) > 0 && parent.L1Origin.Number > bq.l1Blocks[0].Number {
   117  		for i, l1Block := range bq.l1Blocks {
   118  			if parent.L1Origin.Number == l1Block.Number {
   119  				bq.l1Blocks = bq.l1Blocks[i:]
   120  				bq.log.Debug("Advancing internal L1 blocks", "next_epoch", bq.l1Blocks[0].ID(), "next_epoch_time", bq.l1Blocks[0].Time)
   121  				break
   122  			}
   123  		}
   124  		// If we can't find the origin of parent block, we have to advance bq.origin.
   125  	}
   126  
   127  	// Note: We use the origin that we will have to determine if it's behind. This is important
   128  	// because it's the future origin that gets saved into the l1Blocks array.
   129  	// We always update the origin of this stage if it is not the same so after the update code
   130  	// runs, this is consistent.
   131  	originBehind := bq.prev.Origin().Number < parent.L1Origin.Number
   132  
   133  	// Advance origin if needed
   134  	// Note: The entire pipeline has the same origin
   135  	// We just don't accept batches prior to the L1 origin of the L2 safe head
   136  	if bq.origin != bq.prev.Origin() {
   137  		bq.origin = bq.prev.Origin()
   138  		if !originBehind {
   139  			bq.l1Blocks = append(bq.l1Blocks, bq.origin)
   140  		} else {
   141  			// This is to handle the special case of startup. At startup we call Reset & include
   142  			// the L1 origin. That is the only time where immediately after `Reset` is called
   143  			// originBehind is false.
   144  			bq.l1Blocks = bq.l1Blocks[:0]
   145  		}
   146  		bq.log.Info("Advancing bq origin", "origin", bq.origin, "originBehind", originBehind)
   147  	}
   148  
   149  	// Load more data into the batch queue
   150  	outOfData := false
   151  	if batch, err := bq.prev.NextBatch(ctx); err == io.EOF {
   152  		outOfData = true
   153  	} else if err != nil {
   154  		return nil, false, err
   155  	} else if !originBehind {
   156  		bq.AddBatch(ctx, batch, parent)
   157  	}
   158  
   159  	// Skip adding data unless we are up to date with the origin, but do fully
   160  	// empty the previous stages
   161  	if originBehind {
   162  		if outOfData {
   163  			return nil, false, io.EOF
   164  		} else {
   165  			return nil, false, NotEnoughData
   166  		}
   167  	}
   168  
   169  	// Finally attempt to derive more batches
   170  	batch, err := bq.deriveNextBatch(ctx, outOfData, parent)
   171  	if err == io.EOF && outOfData {
   172  		return nil, false, io.EOF
   173  	} else if err == io.EOF {
   174  		return nil, false, NotEnoughData
   175  	} else if err != nil {
   176  		return nil, false, err
   177  	}
   178  
   179  	var nextBatch *SingularBatch
   180  	switch batch.GetBatchType() {
   181  	case SingularBatchType:
   182  		singularBatch, ok := batch.(*SingularBatch)
   183  		if !ok {
   184  			return nil, false, NewCriticalError(errors.New("failed type assertion to SingularBatch"))
   185  		}
   186  		nextBatch = singularBatch
   187  	case SpanBatchType:
   188  		spanBatch, ok := batch.(*SpanBatch)
   189  		if !ok {
   190  			return nil, false, NewCriticalError(errors.New("failed type assertion to SpanBatch"))
   191  		}
   192  		// If next batch is SpanBatch, convert it to SingularBatches.
   193  		singularBatches, err := spanBatch.GetSingularBatches(bq.l1Blocks, parent)
   194  		if err != nil {
   195  			return nil, false, NewCriticalError(err)
   196  		}
   197  		bq.nextSpan = singularBatches
   198  		// span-batches are non-empty, so the below pop is safe.
   199  		nextBatch = bq.popNextBatch(parent)
   200  	default:
   201  		return nil, false, NewCriticalError(fmt.Errorf("unrecognized batch type: %d", batch.GetBatchType()))
   202  	}
   203  
   204  	// If the nextBatch is derived from the span batch, len(bq.nextSpan) == 0 means it's the last batch of the span.
   205  	// For singular batches, len(bq.nextSpan) == 0 is always true.
   206  	return nextBatch, len(bq.nextSpan) == 0, nil
   207  }
   208  
   209  func (bq *BatchQueue) Reset(ctx context.Context, base eth.L1BlockRef, _ eth.SystemConfig) error {
   210  	// Copy over the Origin from the next stage
   211  	// It is set in the engine queue (two stages away) such that the L2 Safe Head origin is the progress
   212  	bq.origin = base
   213  	bq.batches = []*BatchWithL1InclusionBlock{}
   214  	// Include the new origin as an origin to build on
   215  	// Note: This is only for the initialization case. During normal resets we will later
   216  	// throw out this block.
   217  	bq.l1Blocks = bq.l1Blocks[:0]
   218  	bq.l1Blocks = append(bq.l1Blocks, base)
   219  	bq.nextSpan = bq.nextSpan[:0]
   220  	return io.EOF
   221  }
   222  
   223  func (bq *BatchQueue) AddBatch(ctx context.Context, batch Batch, parent eth.L2BlockRef) {
   224  	if len(bq.l1Blocks) == 0 {
   225  		panic(fmt.Errorf("cannot add batch with timestamp %d, no origin was prepared", batch.GetTimestamp()))
   226  	}
   227  	data := BatchWithL1InclusionBlock{
   228  		L1InclusionBlock: bq.origin,
   229  		Batch:            batch,
   230  	}
   231  	validity := CheckBatch(ctx, bq.config, bq.log, bq.l1Blocks, parent, &data, bq.l2)
   232  	if validity == BatchDrop {
   233  		return // if we do drop the batch, CheckBatch will log the drop reason with WARN level.
   234  	}
   235  	batch.LogContext(bq.log).Debug("Adding batch")
   236  	bq.batches = append(bq.batches, &data)
   237  }
   238  
   239  // deriveNextBatch derives the next batch to apply on top of the current L2 safe head,
   240  // following the validity rules imposed on consecutive batches,
   241  // based on currently available buffered batch and L1 origin information.
   242  // If no batch can be derived yet, then (nil, io.EOF) is returned.
   243  func (bq *BatchQueue) deriveNextBatch(ctx context.Context, outOfData bool, parent eth.L2BlockRef) (Batch, error) {
   244  	if len(bq.l1Blocks) == 0 {
   245  		return nil, NewCriticalError(errors.New("cannot derive next batch, no origin was prepared"))
   246  	}
   247  	epoch := bq.l1Blocks[0]
   248  	bq.log.Trace("Deriving the next batch", "epoch", epoch, "parent", parent, "outOfData", outOfData)
   249  
   250  	// Note: epoch origin can now be one block ahead of the L2 Safe Head
   251  	// This is in the case where we auto generate all batches in an epoch & advance the epoch
   252  	// but don't advance the L2 Safe Head's epoch
   253  	if parent.L1Origin != epoch.ID() && parent.L1Origin.Number != epoch.Number-1 {
   254  		return nil, NewResetError(fmt.Errorf("buffered L1 chain epoch %s in batch queue does not match safe head origin %s", epoch, parent.L1Origin))
   255  	}
   256  
   257  	// Find the first-seen batch that matches all validity conditions.
   258  	// We may not have sufficient information to proceed filtering, and then we stop.
   259  	// There may be none: in that case we force-create an empty batch
   260  	nextTimestamp := parent.Time + bq.config.BlockTime
   261  	var nextBatch *BatchWithL1InclusionBlock
   262  
   263  	// Go over all batches, in order of inclusion, and find the first batch we can accept.
   264  	// We filter in-place by only remembering the batches that may be processed in the future, or those we are undecided on.
   265  	var remaining []*BatchWithL1InclusionBlock
   266  batchLoop:
   267  	for i, batch := range bq.batches {
   268  		validity := CheckBatch(ctx, bq.config, bq.log.New("batch_index", i), bq.l1Blocks, parent, batch, bq.l2)
   269  		switch validity {
   270  		case BatchFuture:
   271  			remaining = append(remaining, batch)
   272  			continue
   273  		case BatchDrop:
   274  			batch.Batch.LogContext(bq.log).Warn("Dropping batch",
   275  				"parent", parent.ID(),
   276  				"parent_time", parent.Time,
   277  			)
   278  			continue
   279  		case BatchAccept:
   280  			nextBatch = batch
   281  			// don't keep the current batch in the remaining items since we are processing it now,
   282  			// but retain every batch we didn't get to yet.
   283  			remaining = append(remaining, bq.batches[i+1:]...)
   284  			break batchLoop
   285  		case BatchUndecided:
   286  			remaining = append(remaining, bq.batches[i:]...)
   287  			bq.batches = remaining
   288  			return nil, io.EOF
   289  		default:
   290  			return nil, NewCriticalError(fmt.Errorf("unknown batch validity type: %d", validity))
   291  		}
   292  	}
   293  	bq.batches = remaining
   294  
   295  	if nextBatch != nil {
   296  		nextBatch.Batch.LogContext(bq.log).Info("Found next batch")
   297  		return nextBatch.Batch, nil
   298  	}
   299  
   300  	// If the current epoch is too old compared to the L1 block we are at,
   301  	// i.e. if the sequence window expired, we create empty batches for the current epoch
   302  	expiryEpoch := epoch.Number + bq.config.SeqWindowSize
   303  	forceEmptyBatches := (expiryEpoch == bq.origin.Number && outOfData) || expiryEpoch < bq.origin.Number
   304  	firstOfEpoch := epoch.Number == parent.L1Origin.Number+1
   305  
   306  	bq.log.Trace("Potentially generating an empty batch",
   307  		"expiryEpoch", expiryEpoch, "forceEmptyBatches", forceEmptyBatches, "nextTimestamp", nextTimestamp,
   308  		"epoch_time", epoch.Time, "len_l1_blocks", len(bq.l1Blocks), "firstOfEpoch", firstOfEpoch)
   309  
   310  	if !forceEmptyBatches {
   311  		// sequence window did not expire yet, still room to receive batches for the current epoch,
   312  		// no need to force-create empty batch(es) towards the next epoch yet.
   313  		return nil, io.EOF
   314  	}
   315  	if len(bq.l1Blocks) < 2 {
   316  		// need next L1 block to proceed towards
   317  		return nil, io.EOF
   318  	}
   319  
   320  	nextEpoch := bq.l1Blocks[1]
   321  	// Fill with empty L2 blocks of the same epoch until we meet the time of the next L1 origin,
   322  	// to preserve that L2 time >= L1 time. If this is the first block of the epoch, always generate a
   323  	// batch to ensure that we at least have one batch per epoch.
   324  	if nextTimestamp < nextEpoch.Time || firstOfEpoch {
   325  		bq.log.Info("Generating next batch", "epoch", epoch, "timestamp", nextTimestamp)
   326  		return &SingularBatch{
   327  			ParentHash:   parent.Hash,
   328  			EpochNum:     rollup.Epoch(epoch.Number),
   329  			EpochHash:    epoch.Hash,
   330  			Timestamp:    nextTimestamp,
   331  			Transactions: nil,
   332  		}, nil
   333  	}
   334  
   335  	// At this point we have auto generated every batch for the current epoch
   336  	// that we can, so we can advance to the next epoch.
   337  	bq.log.Trace("Advancing internal L1 blocks", "next_timestamp", nextTimestamp, "next_epoch_time", nextEpoch.Time)
   338  	bq.l1Blocks = bq.l1Blocks[1:]
   339  	return nil, io.EOF
   340  }