From 594c35a1191e420964ad9ba5d036e33100e95291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Duchesneau?= Date: Tue, 6 Oct 2026 10:30:39 -0400 Subject: [PATCH 1/2] fix: skip parallel BAL path while Firehose traces In BAL mode the payload processor streams transactions out of block order, which the sequential Firehose executor ran as-is, rejecting a sender's later nonces as too high. --- .../engine/tree/src/tree/payload_validator.rs | 7 ++++- .../tests/firehose_live_tracing_amsterdam.rs | 31 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/engine/tree/src/tree/payload_validator.rs b/crates/engine/tree/src/tree/payload_validator.rs index 886e3693cd6..862318046c3 100644 --- a/crates/engine/tree/src/tree/payload_validator.rs +++ b/crates/engine/tree/src/tree/payload_validator.rs @@ -612,7 +612,10 @@ where // Get an iterator over the transactions in the payload let txs = self.tx_iterator_for(&input)?; - let parallel_bal_execution = ensure_ok!(self.bal_path_eligible(env.decoded_bal.as_deref())); + // Firehose executes sequentially, and in BAL mode the payload processor streams + // transactions in conversion-completion order instead of block order. + let parallel_bal_execution = ensure_ok!(self.bal_path_eligible(env.decoded_bal.as_deref())) && + !reth_firehose::is_tracer_initialized(); // Prepare the state-root job before execution so it can provide streaming hooks. let mut state_root_job = @@ -762,6 +765,8 @@ where // // BAL parallel execution is incompatible with sequential Firehose tracing, so a block // carrying a BAL is executed sequentially here, building its BAL like `execute_block`. + // `parallel_bal_execution` is forced off while tracing so the transactions arrive in + // block order. match make_state_provider(false) { Ok(state_provider) => self.execute_and_trace_block( state_provider, diff --git a/crates/engine/tree/tests/firehose_live_tracing_amsterdam.rs b/crates/engine/tree/tests/firehose_live_tracing_amsterdam.rs index e357a1eaeb3..e9e7f0b1e76 100644 --- a/crates/engine/tree/tests/firehose_live_tracing_amsterdam.rs +++ b/crates/engine/tree/tests/firehose_live_tracing_amsterdam.rs @@ -11,6 +11,10 @@ //! call the same way the pipeline path does (see the `amsterdam_create_runtime_out_of_gas` //! prestate case in `reth-firehose-tests`). //! +//! The last block carries enough transactions from a single sender for the payload processor to +//! convert them in parallel. The traced execution is sequential, so it must still receive them in +//! block order or the sender's later nonces are rejected as too high. +//! //! Like `firehose_live_tracing`, it lives in its own integration-test binary because it installs //! a process-wide tracer. @@ -19,7 +23,11 @@ use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; use eyre::Result; use firehose_tracer::pb::sf::ethereum::r#type::v2::{CallType, TransactionTraceStatus}; use reth_chainspec::{EthChainSpec, EthereumHardfork}; -use reth_e2e_test_utils::{transaction::TransactionTestContext, wallet::Wallet, E2ETestSetupExt}; +use reth_e2e_test_utils::{ + transaction::TransactionTestContext, + wallet::{test_signer, Wallet}, + E2ETestSetupExt, +}; use reth_node_ethereum::EthereumNode; use reth_provider::BalProvider; @@ -32,6 +40,12 @@ const FRAMELESS_BLOCK: u64 = 2; /// Above Amsterdam's intrinsic cost for the init code below, far below its creation state gas. const FRAMELESS_GAS_LIMIT: u64 = 30_000; +/// Block that carries the same-sender batch. +const BATCH_BLOCK: u64 = 3; + +/// Above the payload processor's sequential conversion threshold of 30 transactions. +const BATCH_TX_COUNT: u64 = 40; + #[tokio::test] async fn live_payload_validation_traces_amsterdam_blocks() -> Result<()> { reth_tracing::init_test_tracing(); @@ -66,6 +80,17 @@ async fn live_payload_validation_traces_amsterdam_blocks() -> Result<()> { TransactionTestContext::sign_tx_bytes(Wallet::default().inner, deploy).await; node.rpc.inject_tx(deploy).await?; } + if number == BATCH_BLOCK { + for nonce in 0..BATCH_TX_COUNT { + let transfer = TransactionTestContext::transfer_tx_bytes_with_nonce( + chain_id, + test_signer(1), + nonce, + ) + .await; + node.rpc.inject_tx(transfer).await?; + } + } let payload = node.advance_block().await?; hashes.push(payload.block().hash()); } @@ -100,6 +125,10 @@ async fn live_payload_validation_traces_amsterdam_blocks() -> Result<()> { assert_eq!(root.call_type, CallType::Create as i32); assert!(root.status_failed); + let block = reth_firehose_tests::parse_fire_block_for(&buffer.get_bytes(), BATCH_BLOCK)?; + let nonces = block.transaction_traces.iter().map(|trx| trx.nonce).collect::>(); + assert_eq!(nonces, (0..BATCH_TX_COUNT).collect::>()); + // The BAL built by the traced execution is what the node stores for the block. for hash in hashes { let bal = node.inner.provider.get_bal_by_hash(hash)?; From c87123a30dd383e3b6b5038242c3b4760d05dd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Duchesneau?= Date: Tue, 6 Oct 2026 10:44:55 -0400 Subject: [PATCH 2/2] bump in changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37f1cd12b1..b86f4793dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ This changelog covers Firehose-specific changes only. For upstream reth changes, The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased +## reth-v2.7.0-fh3.1-1 + +### Fixed + +- Fixed glamsterdam hard fork crash when BAL executes in parallel (when a block has more than 30 transactions) ### Changed