diff --git a/crates/client/metering/src/meter.rs b/crates/client/metering/src/meter.rs index cf21ab04..279fa5cd 100644 --- a/crates/client/metering/src/meter.rs +++ b/crates/client/metering/src/meter.rs @@ -95,6 +95,7 @@ pub fn meter_bundle( chain_spec: Arc, bundle: ParsedBundle, header: &SealedHeader, + parent_beacon_block_root: Option, pending_state: Option, ) -> EyreResult where @@ -136,12 +137,15 @@ where // Set up next block attributes // Use bundle.min_timestamp if provided, otherwise use header timestamp + BLOCK_TIME let timestamp = bundle.min_timestamp.unwrap_or_else(|| header.timestamp() + BLOCK_TIME); + // Pending flashblock headers may omit parent_beacon_block_root; prefer the explicit value + // provided by the caller (e.g., flashblock base payload) to keep EIP-4788 happy. let attributes = OpNextBlockEnvAttributes { timestamp, suggested_fee_recipient: header.beneficiary(), prev_randao: header.mix_hash().unwrap_or(B256::random()), gas_limit: header.gas_limit(), - parent_beacon_block_root: header.parent_beacon_block_root(), + parent_beacon_block_root: parent_beacon_block_root + .or_else(|| header.parent_beacon_block_root()), extra_data: header.extra_data().clone(), }; @@ -268,8 +272,14 @@ mod tests { let parsed_bundle = create_parsed_bundle(Vec::new())?; - let output = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header, None)?; + let output = meter_bundle( + state_provider, + harness.chain_spec(), + parsed_bundle, + &header, + header.parent_beacon_block_root(), + None, + )?; assert!(output.results.is_empty()); assert_eq!(output.total_gas_used, 0); @@ -312,8 +322,14 @@ mod tests { let parsed_bundle = create_parsed_bundle(vec![tx])?; - let output = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header, None)?; + let output = meter_bundle( + state_provider, + harness.chain_spec(), + parsed_bundle, + &header, + header.parent_beacon_block_root(), + None, + )?; assert_eq!(output.results.len(), 1); let result = &output.results[0]; @@ -339,6 +355,58 @@ mod tests { Ok(()) } + #[tokio::test] + async fn meter_bundle_requires_parent_beacon_block_root() -> eyre::Result<()> { + let harness = TestHarness::new().await?; + let latest = harness.latest_block(); + let header = latest.sealed_header().clone(); + + let parsed_bundle = create_parsed_bundle(Vec::new())?; + + let state_provider = harness + .blockchain_provider() + .state_by_block_hash(latest.hash()) + .context("getting state provider")?; + + // Mimic a pending flashblock header that lacks the parent beacon block root. + let mut header_without_root = header.clone_header(); + header_without_root.parent_beacon_block_root = None; + let sealed_without_root = SealedHeader::new(header_without_root, header.hash()); + + let err = meter_bundle( + state_provider, + harness.chain_spec(), + parsed_bundle.clone(), + &sealed_without_root, + None, + None, + ) + .expect_err("missing parent beacon block root should fail"); + assert!( + err.to_string().to_lowercase().contains("parent beacon block root"), + "expected missing parent beacon block root error, got {err:?}" + ); + + let state_provider2 = harness + .blockchain_provider() + .state_by_block_hash(latest.hash()) + .context("getting state provider")?; + + let output = meter_bundle( + state_provider2, + harness.chain_spec(), + parsed_bundle, + &sealed_without_root, + Some(header.parent_beacon_block_root().unwrap_or(B256::ZERO)), + None, + )?; + + assert!(output.total_time_us > 0); + assert!(output.state_root_time_us > 0); + + Ok(()) + } + #[tokio::test] async fn meter_bundle_multiple_transactions() -> eyre::Result<()> { let harness = TestHarness::new().await?; @@ -390,8 +458,14 @@ mod tests { let parsed_bundle = create_parsed_bundle(vec![tx_1, tx_2])?; - let output = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header, None)?; + let output = meter_bundle( + state_provider, + harness.chain_spec(), + parsed_bundle, + &header, + header.parent_beacon_block_root(), + None, + )?; assert_eq!(output.results.len(), 2); assert!(output.total_time_us > 0); @@ -463,8 +537,14 @@ mod tests { let parsed_bundle = create_parsed_bundle(vec![tx])?; - let output = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header, None)?; + let output = meter_bundle( + state_provider, + harness.chain_spec(), + parsed_bundle, + &header, + header.parent_beacon_block_root(), + None, + )?; // Verify invariant: total time must include state root time assert!( @@ -519,6 +599,7 @@ mod tests { harness.chain_spec(), parsed_bundle.clone(), &header, + header.parent_beacon_block_root(), None, // No pending state ); @@ -563,6 +644,7 @@ mod tests { harness.chain_spec(), parsed_bundle, &header, + header.parent_beacon_block_root(), Some(pending_state), ); diff --git a/crates/client/metering/src/rpc.rs b/crates/client/metering/src/rpc.rs index 1c706693..8d351333 100644 --- a/crates/client/metering/src/rpc.rs +++ b/crates/client/metering/src/rpc.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use alloy_consensus::{Header, Sealed}; +use alloy_consensus::{BlockHeader, Header, Sealed}; use alloy_eips::BlockNumberOrTag; use alloy_primitives::{B256, U256}; use base_bundles::{Bundle, MeterBundleResponse, ParsedBundle}; @@ -167,12 +167,23 @@ where None }; + // Pending flashblock headers can omit parent_beacon_block_root; prefer the CL-provided + // value from the flashblock base payload when available, otherwise fall back to the header. + let parent_beacon_block_root = header.parent_beacon_block_root().or_else(|| { + pending_blocks.as_ref().and_then(|pb| { + pb.get_flashblocks() + .first() + .and_then(|fb| fb.base.as_ref().map(|base| base.parent_beacon_block_root)) + }) + }); + // Meter bundle using utility function let output = meter_bundle( state_provider, self.provider.chain_spec(), parsed_bundle, &header, + parent_beacon_block_root, pending_state, ) .map_err(|e| {