| 549 | } |
| 550 | |
| 551 | fn step(&mut self) -> Result<StepResult, StepError> { |
| 552 | if let Some(error) = self.deferred_step_error.take() { |
| 553 | return Err(error); |
| 554 | } |
| 555 | |
| 556 | // Check for channel timeout first. |
| 557 | if self.check_channel_timeout()? { |
| 558 | return Ok(StepResult::ChannelClosed); |
| 559 | } |
| 560 | |
| 561 | // If there are no blocks to encode, we're idle. |
| 562 | if self.block_cursor >= self.blocks.len() { |
| 563 | if self.config.batch_type == BatchType::Span |
| 564 | && let Some(max_blocks_per_span_batch) = self.config.max_blocks_per_span_batch |
| 565 | && self.span_accumulator.len() >= max_blocks_per_span_batch |
| 566 | { |
| 567 | if !self.try_flush_span_or_close("size_full")? { |
| 568 | return Ok(StepResult::ChannelClosed); |
| 569 | } |
| 570 | return Ok(StepResult::SpanFlushed); |
| 571 | } |
| 572 | |
| 573 | return Ok(StepResult::Idle); |
| 574 | } |
| 575 | |
| 576 | // Get the block at the cursor. |
| 577 | let block = &self.blocks[self.block_cursor]; |
| 578 | let block_da_backlog_bytes = Self::block_da_backlog_bytes(block); |
| 579 | |
| 580 | // Convert block to a SingleBatch. Failure here is fatal: skipping the block |
| 581 | // would produce a gap in the L2 block sequence submitted to L1. |
| 582 | let (single_batch, l1_info) = BatchComposer::block_to_single_batch(block) |
| 583 | .map_err(|source| StepError::CompositionFailed { cursor: self.block_cursor, source })?; |
| 584 | |
| 585 | match self.config.batch_type { |
| 586 | BatchType::Span => { |
| 587 | // In Span mode blocks are accumulated in memory; the span batch is |
| 588 | // written to the channel only when a span-batch or channel boundary is reached. |
| 589 | let seq_num = l1_info.sequence_number(); |
| 590 | // Maintain a running byte counter so the size check below is O(1) per |
| 591 | // step rather than O(N·M) over the entire accumulator. |
| 592 | let block_raw_bytes = Self::SPAN_BATCH_PER_BLOCK_OVERHEAD |
| 593 | + single_batch.transactions.iter().map(|tx| tx.len()).sum::<usize>(); |
| 594 | self.span_raw_bytes += block_raw_bytes; |
| 595 | self.span_da_backlog_bytes += block_da_backlog_bytes; |
| 596 | self.span_accumulator.push((single_batch, seq_num)); |
| 597 | self.block_cursor += 1; |
| 598 | |
| 599 | // Track the L1 head at which the first block of this span was accumulated. |
| 600 | // `check_channel_timeout()` uses this to detect when the span has been open |
| 601 | // too long even though `current_channel` is None between flushes. |
| 602 | if self.span_opened_at_l1.is_none() { |
| 603 | self.span_opened_at_l1 = Some(self.l1_head); |
| 604 | } |
| 605 | |
| 606 | // Estimate the compressed size of the accumulated span batch and close |
| 607 | // the channel when it would exceed the configured size budget. This mirrors |
| 608 | // the reference batcher's `SpanChannelOut`, which triggers closure based on estimated |