diff --git a/CHANGELOG.md b/CHANGELOG.md index e2405b3..3b53f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed + - Add missing payload attribute extraction in `EvolvePayloadBuilder` to properly handle transactions submitted via Engine API ([#33](https://github.com/evstack/ev-reth/pull/33)) - Remove unused configuration parameters to clean up codebase ([#32](https://github.com/evstack/ev-reth/pull/32)) +- Ensure `stateRoot` follows Ethereum post-state semantics to avoid false fork reports caused by height-1 root mismatches ### Changed + - Use `best_transactions` instead of `pending_transactions` queue for improved transaction selection logic ([#29](https://github.com/evstack/ev-reth/pull/29)) diff --git a/README.md b/README.md index 583be4b..13a019b 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ All standard Reth configuration options are supported. Key options for Evolve in ### Project Structure -``` +```tree ev-reth/ ├── bin/ │ └── ev-reth/ # Main binary diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 23acc2c..34c0222 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -177,7 +177,12 @@ where } } - // Finish building the block - this calculates the proper state root + // Finish building the block. This computes the *current block's* post-state root. + // + // Ethereum clients expect `header.state_root` to equal the post-state after executing the + // transactions in this block on top of the parent state. Accidentally using the parent + // (height-1) state root here can make downstream execution clients think each block is on + // a different fork, even when the chain is otherwise canonical. let BlockBuilderOutcome { execution_result: _, hashed_state: _, diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index e8bb18b..f3029a1 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -475,4 +475,5 @@ mod tests { DEFAULT_CONTRACT_SIZE_LIMIT ); } + } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 5b1e285..47e9a53 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -16,12 +16,14 @@ use reth_ethereum::{ }, }; use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; -use reth_primitives_traits::{Block as _, RecoveredBlock}; +use reth_primitives_traits::RecoveredBlock; use tracing::info; use crate::{attributes::EvolveEnginePayloadAttributes, node::EvolveEngineTypes}; /// Evolve engine validator that handles custom payload validation. +/// +/// This validator delegates to the standard Ethereum payload validation. #[derive(Debug, Clone)] pub struct EvolveEngineValidator { inner: EthereumExecutionPayloadValidator, @@ -51,33 +53,13 @@ impl PayloadValidator for EvolveEngineValidator { ) -> Result, NewPayloadError> { info!("Evolve engine validator: validating payload"); - // Use inner validator but with custom evolve handling. - match self.inner.ensure_well_formed_payload(payload.clone()) { - Ok(sealed_block) => { - info!("Evolve engine validator: payload validation succeeded"); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) - } - Err(err) => { - // Log the error for debugging. - tracing::debug!("Evolve payload validation error: {:?}", err); - - // Check if this is a block hash mismatch error - bypass it for evolve. - if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { - info!("Evolve engine validator: bypassing block hash mismatch for ev-reth"); - // For evolve, we trust the payload builder - just parse the block without hash validation. - let ExecutionData { payload, sidecar } = payload; - let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow(); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) - } else { - // For other errors, re-throw them. - Err(NewPayloadError::Eth(err)) - } - } - } + // Directly delegate to the inner Ethereum validator without any bypass logic. + // This will fail if block hashes don't match, allowing us to see if the error actually occurs. + let sealed_block = self.inner.ensure_well_formed_payload(payload)?; + info!("Evolve engine validator: payload validation succeeded"); + sealed_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) } fn validate_payload_attributes_against_header( @@ -145,3 +127,40 @@ where Ok(EvolveEngineValidator::new(ctx.config.chain.clone())) } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use reth_chainspec::ChainSpecBuilder; + use reth_primitives::{Block, SealedBlock}; + + fn create_validator() -> EvolveEngineValidator { + let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build()); + EvolveEngineValidator::new(chain_spec) + } + + fn mismatched_payload() -> ExecutionData { + let sealed_block: SealedBlock = SealedBlock::default(); + let block_hash = sealed_block.hash(); + let block = sealed_block.into_block(); + let mut data = ExecutionData::from_block_unchecked(block_hash, &block); + data.payload.as_v1_mut().block_hash = B256::repeat_byte(0x42); + data + } + + #[test] + fn test_hash_mismatch_is_rejected() { + // Hash mismatches should be rejected + let validator = create_validator(); + let payload = mismatched_payload(); + + let result = validator.ensure_well_formed_payload(payload); + assert!(matches!( + result, + Err(NewPayloadError::Eth( + alloy_rpc_types::engine::PayloadError::BlockHash { .. } + )) + )); + } +}