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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ 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/).

## reth-v2.7.0-fh3.1-2

### Fixed

- Staged sync no longer rejects Amsterdam blocks with `BlockAccessListHashMissing`. The Firehose
block executor now rebuilds each block's access list on both its traced and untraced paths and
hands it to the execution stage for validation.

### Changed

- `reth_firehose::run_wrapped_block` leaves the rebuilt block access list in the state. Call the
new `reth_firehose::take_traced_block_access_list` afterwards to check its hash and record its
RLP on the traced block; `FirehoseBlockExecutor` already does.

## reth-v2.7.0-fh3.1-1

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions crates/firehose-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ path = "tests/prestate.rs"
[[test]]
name = "batch_executor"
path = "tests/batch_executor.rs"

[[test]]
name = "staged_sync_bal"
path = "tests/staged_sync_bal.rs"
5 changes: 3 additions & 2 deletions crates/firehose-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
pub mod prestate;

pub use prestate::{
assert_block_equals_golden, build_account_info, decode_hex, parse_fire_block_for, run_prestate,
seed_cache_db, Prestate, RunOutcome, TraceContext,
assert_block_equals_golden, build_account_info, decode_hex, load_prestate,
parse_fire_block_for, run_prestate, seed_cache_db, LoadedPrestate, Prestate, RunOutcome,
TraceContext,
};
85 changes: 56 additions & 29 deletions crates/firehose-tests/src/prestate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ use prost::Message;
use reth_chainspec::ChainSpec;
use reth_ethereum_primitives::{Block, BlockBody, EthPrimitives, TransactionSigned};
use reth_evm_ethereum::EthEvmConfig;
use reth_firehose::{run_wrapped_block, FirehoseBlockTracer, NoPostTxExtras, NoPreTxAdjust};
use reth_firehose::{
run_wrapped_block, take_traced_block_access_list, FirehoseBlockTracer, NoPostTxExtras,
NoPreTxAdjust,
};
use reth_primitives_traits::{Block as _, RecoveredBlock};
use reth_revm::State;
use revm::{
Expand Down Expand Up @@ -90,40 +93,27 @@ pub struct TraceContext {
pub block_access_list_hash: Option<B256>,
}

/// A test case's `prestate.json`, turned into what executing its block needs.
#[derive(Debug)]
pub struct LoadedPrestate {
/// The parsed fixture.
pub prestate: Prestate,
/// Chain spec built from the fixture's genesis.
pub chain_spec: Arc<ChainSpec>,
/// The block to execute, carrying the fixture's single transaction.
pub block: RecoveredBlock<Block>,
/// State database seeded with the genesis allocation.
pub db: CacheDB<EmptyDB>,
}

/// Run the prestate-driven Firehose harness against `case_folder` and return the captured Block.
///
/// `case_folder` must contain `prestate.json` (Geth-style genesis + block context + RLP-encoded
/// signed transaction). The captured `Block` is the protobuf parsed from the single
/// `FIRE BLOCK` line emitted for the executed block; assert against a `.binpb` golden via
/// [`assert_block_equals_golden`].
pub fn run_prestate(case_folder: &Path) -> eyre::Result<RunOutcome> {
let prestate_path = case_folder.join("prestate.json");
let prestate: Prestate = serde_json::from_slice(&std::fs::read(&prestate_path)?)
.with_context(|| format!("reading prestate.json at {}", prestate_path.display()))?;

let chain_spec = Arc::new(ChainSpec::from(prestate.genesis.clone()));
let parent_hash = chain_spec.genesis_hash();

let tx_bytes = decode_hex(&prestate.input).context("decoding prestate.input hex")?;
let signed_tx = TransactionSigned::network_decode(&mut tx_bytes.as_slice())
.context("RLP-decoding prestate.input as a signed transaction")?;

let transactions = vec![signed_tx];
let header = build_header(&prestate.context, parent_hash, &transactions);

// Match Geth's `extblock` RLP shape on Shanghai+ chains: 4-element list
// [header, txs, uncles, withdrawals] where the withdrawals slot is always present (empty
// list when the block has no withdrawals). Reth's encoder elides the slot entirely when
// `withdrawals: None`, producing a 3-element list and a 1-byte-shorter `block.size`.
let block = Block {
header,
body: BlockBody { transactions, ommers: vec![], withdrawals: Some(Withdrawals::default()) },
};
let recovered: RecoveredBlock<Block> =
block.try_into_recovered().map_err(|_| eyre::eyre!("recovering tx senders"))?;

let mut db = CacheDB::new(EmptyDB::default());
seed_cache_db(&mut db, &prestate.genesis)?;
let LoadedPrestate { prestate, chain_spec, block: recovered, db } = load_prestate(case_folder)?;
let mut state = State::builder().with_database(db).with_bundle_update().build();

let evm_config = EthEvmConfig::new(chain_spec.clone());
Expand Down Expand Up @@ -164,7 +154,11 @@ pub fn run_prestate(case_folder: &Path) -> eyre::Result<RunOutcome> {
&mut block_tracer,
NoPreTxAdjust,
NoPostTxExtras,
);
)
.and_then(|result| {
take_traced_block_access_list(&mut state, &recovered, &mut block_tracer)?;
Ok(result)
});

match exec_result {
Ok(_) => block_tracer.mark_verified(),
Expand All @@ -182,6 +176,39 @@ pub fn run_prestate(case_folder: &Path) -> eyre::Result<RunOutcome> {
Ok(RunOutcome { block, raw })
}

/// Loads `case_folder`'s `prestate.json` into the block it describes and a state database seeded
/// with its genesis allocation.
pub fn load_prestate(case_folder: &Path) -> eyre::Result<LoadedPrestate> {
let prestate_path = case_folder.join("prestate.json");
let prestate: Prestate = serde_json::from_slice(&std::fs::read(&prestate_path)?)
.with_context(|| format!("reading prestate.json at {}", prestate_path.display()))?;

let chain_spec = Arc::new(ChainSpec::from(prestate.genesis.clone()));
let parent_hash = chain_spec.genesis_hash();

let tx_bytes = decode_hex(&prestate.input).context("decoding prestate.input hex")?;
let signed_tx = TransactionSigned::network_decode(&mut tx_bytes.as_slice())
.context("RLP-decoding prestate.input as a signed transaction")?;

let transactions = vec![signed_tx];
let header = build_header(&prestate.context, parent_hash, &transactions);

// Match Geth's `extblock` RLP shape on Shanghai+ chains: 4-element list
// [header, txs, uncles, withdrawals] where the withdrawals slot is always present (empty
// list when the block has no withdrawals). Reth's encoder elides the slot entirely when
// `withdrawals: None`, producing a 3-element list and a 1-byte-shorter `block.size`.
let block = Block {
header,
body: BlockBody { transactions, ommers: vec![], withdrawals: Some(Withdrawals::default()) },
};
let recovered: RecoveredBlock<Block> =
block.try_into_recovered().map_err(|_| eyre::eyre!("recovering tx senders"))?;

let mut db = CacheDB::new(EmptyDB::default());
seed_cache_db(&mut db, &prestate.genesis)?;
Ok(LoadedPrestate { prestate, chain_spec, block: recovered, db })
}

/// Compare a captured Firehose `Block` against the binary protobuf golden at `golden_path`.
///
/// On mismatch, returns an error containing pretty-printed `{:#?}` debug dumps of both the
Expand Down
5 changes: 3 additions & 2 deletions crates/firehose-tests/tests/prestate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ fn amsterdam_slot_number() {
}

/// EIP-7928: `run_wrapped_block` (this repo's pipeline/backfill path) has no payload sidecar to
/// source the block access list from, so it must reconstruct it via re-execution and only surface
/// it once the reconstructed hash matches the header's declared `block_access_list_hash`.
/// source the block access list from, so it must reconstruct it via re-execution, and
/// `take_traced_block_access_list` only surfaces it once the reconstructed hash matches the
/// header's declared `block_access_list_hash`.
///
/// Because of that check the fixture's `context.blockAccessListHash` is not free-standing: it has
/// to be regenerated from the reconstructed value whenever anything about the block changes,
Expand Down
56 changes: 56 additions & 0 deletions crates/firehose-tests/tests/staged_sync_bal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Regression test: staged sync gets a block access list for Amsterdam blocks.
//!
//! The execution stage hashes the access list it takes from the executor with
//! [`Executor::take_bal`] and rejects an Amsterdam block when there is none
//! (`BlockAccessListHashMissing`). [`FirehoseBlockExecutor`] must therefore rebuild it on both its
//! traced and untraced paths, and record it on the traced Firehose block.
//!
//! It lives in its own integration-test binary because it installs the process-wide tracer.

use std::path::PathBuf;

use alloy_consensus::BlockHeader;
use alloy_primitives::keccak256;
use reth_evm::execute::Executor;
use reth_evm_ethereum::EthEvmConfig;
use reth_firehose::FirehoseBlockExecutor;
use reth_firehose_tests::{load_prestate, parse_fire_block_for, LoadedPrestate};

#[test]
fn staged_sync_rebuilds_the_block_access_list() {
let case = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("cases")
.join("amsterdam_block_access_list");
let LoadedPrestate { prestate, chain_spec, block, db } =
load_prestate(&case).expect("loading the amsterdam_block_access_list prestate");
let expected_hash = block
.header()
.block_access_list_hash()
.expect("the Amsterdam fixture declares a block access list hash");
let evm_config = EthEvmConfig::new(chain_spec);

let mut executor = FirehoseBlockExecutor::new(evm_config.clone(), db.clone());
executor.execute_one(&block).expect("execute_one succeeds");
let bal = executor.take_bal().expect("execute_one rebuilds the block access list");
assert_eq!(keccak256(alloy_rlp::encode(&bal)), expected_hash);

let config = &prestate.genesis.config;
let buffer = reth_firehose::init_tracer_with_buffer(
config.chain_id,
config.shanghai_time,
config.cancun_time,
config.prague_time,
);
let mut executor = FirehoseBlockExecutor::new(evm_config, db);
executor.execute_and_trace_one(&block).expect("execute_and_trace_one succeeds");
let bal = executor.take_bal().expect("execute_and_trace_one rebuilds the block access list");
assert_eq!(keccak256(alloy_rlp::encode(&bal)), expected_hash);

// Reaching `into_state` confirms the last block passed validation, which flushes it.
let _ = executor.into_state();
let fire_block = parse_fire_block_for(&buffer.get_bytes(), block.header().number())
.expect("the traced block is emitted");
let header = fire_block.header.expect("the Firehose block has a header");
assert_eq!(header.block_access_list_rlp, Some(alloy_rlp::encode(&bal)));
}
Loading
Loading