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 }