Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion crates/node/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: _,
Expand Down
1 change: 1 addition & 0 deletions crates/node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,5 @@ mod tests {
DEFAULT_CONTRACT_SIZE_LIMIT
);
}

}
75 changes: 47 additions & 28 deletions crates/node/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChainSpec>,
Expand Down Expand Up @@ -51,33 +53,13 @@ impl PayloadValidator<EvolveEngineTypes> for EvolveEngineValidator {
) -> Result<RecoveredBlock<Self::Block>, 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(
Expand Down Expand Up @@ -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<Block> = 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 { .. }
))
));
}
}
Loading