Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions crates/node/tests/it/l1_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,86 @@ async fn l1_message_gas_is_tracked() -> eyre::Result<()> {

Ok(())
}

/// When a later L1 message does not fit remaining block gas, assemble still
/// succeeds with the messages that already fit. The leftover is retried on
/// the next block via `next_l1_msg_index`.
#[tokio::test(flavor = "multi_thread")]
async fn l1_message_gas_overflow_seals_what_fits() -> eyre::Result<()> {
reth_tracing::init_test_tracing();

let block_gas_limit = 50_000u64;
let (mut nodes, _wallet) = TestNodeBuilder::new()
.with_gas_limit(block_gas_limit)
.build()
.await?;
let mut node = nodes.pop().unwrap();

let msg0 = L1MessageBuilder::new(0)
.with_target(Address::with_last_byte(0x01))
.with_gas_limit(21_000)
.build_encoded();
let msg1 = L1MessageBuilder::new(1)
.with_target(Address::with_last_byte(0x02))
.with_gas_limit(40_000)
.build_encoded();

let payload = advance_block_with_l1_messages(&mut node, vec![msg0, msg1]).await?;
let block = payload.block();

assert_eq!(
block.body().transactions.len(),
1,
"only the L1 message that fits remaining gas should be included"
);
let tx = block.body().transactions.first().unwrap();
assert!(tx.is_l1_msg());
assert_eq!(tx.queue_index(), Some(0));
assert_eq!(block.header().next_l1_msg_index, 1);

let leftover = L1MessageBuilder::new(1)
.with_target(Address::with_last_byte(0x02))
.with_gas_limit(40_000)
.build_encoded();
let payload2 = advance_block_with_l1_messages(&mut node, vec![leftover]).await?;
let block2 = payload2.block();

assert_eq!(block2.body().transactions.len(), 1);
assert_eq!(
block2.body().transactions.first().unwrap().queue_index(),
Some(1)
);
assert_eq!(block2.header().next_l1_msg_index, 2);

Ok(())
}

/// A single L1 message larger than the whole block gas limit must not abort
/// assemble. The message is left for a later block (`next_l1_msg_index` unchanged).
#[tokio::test(flavor = "multi_thread")]
async fn single_oversized_l1_message_does_not_abort_assemble() -> eyre::Result<()> {
reth_tracing::init_test_tracing();

let block_gas_limit = 50_000u64;
let (mut nodes, _wallet) = TestNodeBuilder::new()
.with_gas_limit(block_gas_limit)
.build()
.await?;
let mut node = nodes.pop().unwrap();

let oversized = L1MessageBuilder::new(0)
.with_target(Address::with_last_byte(0x01))
.with_gas_limit(100_000)
.build_encoded();

let payload = advance_block_with_l1_messages(&mut node, vec![oversized]).await?;
let block = payload.block();

assert!(
block.body().transactions.is_empty(),
"oversized L1 message must not be included"
);
assert_eq!(block.header().next_l1_msg_index, 0);

Ok(())
}
49 changes: 28 additions & 21 deletions crates/payload/builder/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,16 @@ impl MorphPayloadBuilderCtx {
BestTransactionsAttributes::new(base_fee, None)
}

/// Executes all L1 message transactions from payload attributes.
/// Executes L1 message transactions from payload attributes.
///
/// L1 messages are forced transactions from the L1 bridge that must be executed first.
/// They must have sequential queue indices and are never pulled from the transaction pool.
///
/// If the next L1 message does not fit in remaining block gas, packing stops and the
/// leftover messages are left for the next block via `next_l1_msg_index`. A single
/// message larger than the whole block gas limit is skipped for this height (not
/// included, index not advanced) so assemble still returns a block.
///
/// Returns the executed transaction bytes for inclusion in ExecutableL2Data.
fn execute_l1_messages(
&self,
Expand All @@ -342,8 +347,6 @@ impl MorphPayloadBuilderCtx {
let base_fee = builder.evm().block().basefee();
let l1_tx_count = self.attributes().transactions.len();
let mut executed_txs: Vec<Bytes> = Vec::with_capacity(l1_tx_count);
// Track gas spent by each transaction for error reporting
let mut gas_spent_by_transactions: Vec<u64> = Vec::with_capacity(l1_tx_count);

for (tx_idx, tx_with_encoded) in self.attributes().transactions.iter().enumerate() {
// The transaction is already recovered in `try_new` via `try_into_recovered()`.
Expand All @@ -361,23 +364,28 @@ impl MorphPayloadBuilderCtx {

let tx_gas = recovered_tx.gas_limit();

// Check if adding this transaction would exceed block gas limit
if info.cumulative_gas_used + tx_gas > block_gas_limit {
tracing::warn!(
target: "payload_builder",
tx_index = tx_idx,
tx_gas,
cumulative_gas_used = info.cumulative_gas_used,
block_gas_limit,
"L1 message transaction would exceed block gas limit; aborting build"
);
gas_spent_by_transactions.push(tx_gas);
return Err(PayloadBuilderError::other(
MorphPayloadBuilderError::BlockGasLimitExceededBySequencerTransactions {
gas_spent_by_tx: gas_spent_by_transactions,
gas: block_gas_limit,
},
));
// Match morph-geth: stop L1 packing when the next message does not fit
// remaining gas, and still seal the block with what already fits.
if info.cumulative_gas_used.saturating_add(tx_gas) > block_gas_limit {
if info.transaction_count == 0 {
tracing::warn!(
target: "payload_builder",
tx_index = tx_idx,
tx_gas,
block_gas_limit,
"Single L1 message gas limit exceeded for current block"
);
} else {
tracing::debug!(
target: "payload_builder",
tx_index = tx_idx,
tx_gas,
cumulative_gas_used = info.cumulative_gas_used,
block_gas_limit,
"L1 message would exceed remaining block gas; stopping L1 packing"
);
}
break;
}

// Execute the transaction and record EVM execution time.
Expand Down Expand Up @@ -463,7 +471,6 @@ impl MorphPayloadBuilderCtx {
};

info.cumulative_gas_used += gas_used;
gas_spent_by_transactions.push(gas_used);

// Increment transaction count
info.transaction_count += 1;
Expand Down
11 changes: 0 additions & 11 deletions crates/payload/builder/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,6 @@ pub enum MorphPayloadBuilderError {
#[error("failed to recover transaction signer")]
TransactionEcRecoverFailed,

/// Block gas limit exceeded by sequencer transactions.
#[error(
"block gas limit {gas} exceeded by sequencer transactions, gas spent by tx: {gas_spent_by_tx:?}"
)]
BlockGasLimitExceededBySequencerTransactions {
/// Gas spent by each transaction.
gas_spent_by_tx: Vec<u64>,
/// Block gas limit.
gas: u64,
},

/// Invalid sequencer transaction in forced transaction list.
#[error("invalid sequencer transaction: {error}")]
InvalidSequencerTransaction {
Expand Down
2 changes: 2 additions & 0 deletions crates/payload/builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
//! - Queue indices must be strictly sequential
//! - Gas is prepaid on L1, so no refunds for unused gas
//! - L1 messages are never in the transaction pool
//! - If the next L1 message does not fit remaining block gas, packing stops and
//! leftovers are retried on the next block via `next_l1_msg_index`

#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
Expand Down