Skip to content
Merged
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
58 changes: 48 additions & 10 deletions crates/engine-api/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@ pub struct RealMorphL2EngineApi<Provider> {
metrics: MorphEngineApiMetrics,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct CanonicalHead {
number: u64,
hash: B256,
timestamp: u64,
}

/// Tracks the L1-derived finalized block hash from `set_block_tags` so that FCU
/// calls can forward it to the engine tree.
///
Expand Down Expand Up @@ -89,6 +82,35 @@ impl BlockTagTracker {
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct CanonicalHead {
number: u64,
hash: B256,
timestamp: u64,
}

/// Whether a payload build may draw transactions from the local pool.
///
/// A named policy rather than a bare `bool`, because the build entry points already take
/// several positional `Option` arguments and picking the wrong one here silently forks the
/// chain instead of failing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TxPoolPolicy {
/// Sequencer assembly: execute the supplied L1 messages, then pack the best pool
/// transactions.
Include,
/// Derivation import: execute exactly the supplied transactions and nothing else, so the
/// rebuilt block is byte-identical to the one the sequencer committed to L1.
Exclude,
}

impl TxPoolPolicy {
/// Maps to the `no_tx_pool` flag carried by [`morph_payload_types::MorphPayloadAttributes`].
const fn no_tx_pool(self) -> bool {
matches!(self, Self::Exclude)
}
}

impl<Provider> RealMorphL2EngineApi<Provider> {
/// Creates a new [`RealMorphL2EngineApi`].
pub fn new(
Expand Down Expand Up @@ -152,7 +174,9 @@ where
params: AssembleL2BlockParams,
) -> EngineApiResult<ExecutableL2Data> {
let started = Instant::now();
let result = self.build_l2_payload(params, None, None, None).await;
let result = self
.build_l2_payload(params, None, None, None, TxPoolPolicy::Include)
.await;
self.metrics
.assemble_l2_block_duration_seconds
.record(started.elapsed());
Expand Down Expand Up @@ -198,7 +222,13 @@ where
};

let result = self
.build_l2_payload(assemble_params, None, None, Some(parent_hash))
.build_l2_payload(
assemble_params,
None,
None,
Some(parent_hash),
TxPoolPolicy::Include,
)
.await;
self.metrics
.assemble_l2_block_duration_seconds
Expand Down Expand Up @@ -491,7 +521,12 @@ where
// resolved parent, so callers that pin a non-head parent reorg correctly.
let parent_override = data.parent_hash;

// Assemble the block from SafeL2Data inputs.
// Reconstruct the block from SafeL2Data inputs. `SafeL2Data.transactions` is the
// complete ordered transaction list of the L1-committed block, so the build must be
// deterministic: TxPoolPolicy::Exclude keeps the local pool out. Without it a
// follower absorbs gossiped transactions that the sequencer committed to later
// blocks, forking off the sequencer chain (issue #179). SafeL2Data carries no
// expected block hash, so nothing downstream would catch such a divergence.
let assemble_params = AssembleL2BlockParams {
number: data.number,
// Move transactions out of data to avoid cloning the full Vec<Bytes>.
Expand All @@ -505,6 +540,7 @@ where
Some(data.gas_limit),
data.base_fee_per_gas,
parent_override,
TxPoolPolicy::Exclude,
)
.await
.inspect_err(|_| {
Expand Down Expand Up @@ -643,6 +679,7 @@ impl<Provider> RealMorphL2EngineApi<Provider> {
gas_limit_override: Option<u64>,
base_fee_override: Option<u128>,
parent_override: Option<B256>,
tx_pool_policy: TxPoolPolicy,
) -> EngineApiResult<MorphBuiltPayload>
where
Provider: HeaderProvider<Header = MorphHeader>
Expand Down Expand Up @@ -728,6 +765,7 @@ impl<Provider> RealMorphL2EngineApi<Provider> {
target_gas_limit: None,
},
transactions: Some(params.transactions),
no_tx_pool: tx_pool_policy.no_tx_pool(),
gas_limit: gas_limit_override,
base_fee_per_gas: base_fee_override,
};
Expand Down
25 changes: 23 additions & 2 deletions crates/evm/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ pub struct MorphBlockExecutor<DB: Database, I> {
receipts: Vec<MorphReceipt>,
/// Total gas used by executed transactions
gas_used: u64,
/// Gas unavailable to later transactions in this block.
///
/// Unlike receipt gas, L1 messages reserve their full gas limit because Morph geth does not
/// return their unused gas to the block gas pool.
gas_pool_used: u64,
/// Cached hardfork for this block (constant across all transactions).
/// Set in `apply_pre_execution_changes`, reused in `commit_transaction`.
hardfork: MorphHardfork,
Expand Down Expand Up @@ -119,6 +124,7 @@ where
receipt_builder,
receipts: Vec::new(),
gas_used: 0,
gas_pool_used: 0,
hardfork: MorphHardfork::default(),
}
}
Expand Down Expand Up @@ -227,8 +233,14 @@ where
) -> Result<Self::Result, BlockExecutionError> {
let (tx_env, recovered) = tx.into_parts();

// Validate gas limit fits in remaining block gas.
let block_available_gas = self.evm.block().gas_limit() - self.gas_used;
// Validate gas limit against the geth-compatible block gas pool. Receipt gas cannot be
// used here: L1 messages report their actual execution cost in receipts but reserve their
// full gas limit for block packing.
let block_available_gas = self
.evm
.block()
.gas_limit()
.saturating_sub(self.gas_pool_used);
if recovered.tx().gas_limit() > block_available_gas {
return Err(BlockExecutionError::msg(format!(
"transaction gas limit {} exceeds block available gas {}",
Expand Down Expand Up @@ -275,6 +287,15 @@ where
let gas_used = result.gas().tx_gas_used();
self.gas_used += gas_used;

// Morph geth's L1-message path deducts the full transaction gas limit from GasPool and
// deliberately skips the refund path. Regular transactions return unused gas, so their
// net gas-pool charge is the actual gas used.
self.gas_pool_used += if recovered.tx().is_l1_msg() {
recovered.tx().gas_limit()
} else {
gas_used
};

// Get MorphTx-specific fields using the recovered transaction. Errors here
// are tracing-only — the trait API no longer permits us to surface errors
// from `commit_transaction`.
Expand Down
2 changes: 2 additions & 0 deletions crates/node/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ impl PayloadAttributesBuilder<MorphPayloadAttributes, MorphHeader>
},
// No L1 transactions in local mining mode
transactions: None,
// Local mining exists to produce blocks from the pool.
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
}
Expand Down
2 changes: 2 additions & 0 deletions crates/node/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ pub async fn advance_empty_block(node: &mut MorphTestNode) -> eyre::Result<Morph
target_gas_limit: None,
},
transactions: Some(vec![]),
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
};
Expand Down Expand Up @@ -688,6 +689,7 @@ pub fn morph_payload_attributes(timestamp: u64) -> morph_payload_types::MorphPay
target_gas_limit: None,
},
transactions: None,
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/node/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,7 @@ mod tests {
target_gas_limit: None,
},
transactions: None,
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
};
Expand All @@ -1050,6 +1051,7 @@ mod tests {
target_gas_limit: None,
},
transactions: None,
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
};
Expand All @@ -1071,6 +1073,7 @@ mod tests {
target_gas_limit: None,
},
transactions: None,
no_tx_pool: false,
gas_limit: None,
base_fee_per_gas: None,
};
Expand Down
Loading