From 39b977cc8d7f43fab0b591056df8aeafb17a1019 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 12 Dec 2025 14:05:57 +0100 Subject: [PATCH 1/5] use the correct hash --- README.md | 21 +++++++++++ crates/node/src/builder.rs | 8 ++++- crates/node/src/config.rs | 44 ++++++++++++++++++++++- crates/node/src/validator.rs | 69 ++++++++++++++++++++++++++++++++++-- docs/canonical-hash-plan.md | 47 ++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 docs/canonical-hash-plan.md diff --git a/README.md b/README.md index 583be4b..373e528 100644 --- a/README.md +++ b/README.md @@ -456,3 +456,24 @@ This project builds upon the excellent work of: - [Reth](https://github.com/paradigmxyz/reth) - The Rust Ethereum client - [Evolve](https://ev.xyz/) - The modular rollup framework + +### Canonical Block Hash Activation + +Legacy deployments allowed application-level hashes to flow through the Engine API, which meant +Reth had to ignore block-hash mismatches and upstream tooling flagged every block as a fork. Newer +networks can opt-in to canonical keccak block hashes by setting `hashRewireActivationHeight` inside +the `evolve` section of the chainspec: + +```json +"config": { + ..., + "evolve": { + "hashRewireActivationHeight": 0 + } +} +``` + +Set the activation height to the first block where canonical hashes should be enforced (use `0` for +fresh networks). Before the activation height the node continues to bypass hash mismatches so +existing chains keep working; after activation, the node rejects malformed payloads and the reported +block hash always matches the standard Engine API expectations. diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 23acc2c..50d564f 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -187,7 +187,13 @@ where .finish(&state_provider) .map_err(PayloadBuilderError::other)?; - let sealed_block = block.sealed_block().clone(); + let mut sealed_block = block.sealed_block().clone(); + + if !self.config.is_hash_rewire_active_for_block(block_number) { + let legacy_hash = sealed_block.header().state_root; + let legacy_block = sealed_block.clone_block(); + sealed_block = SealedBlock::new_unchecked(legacy_block, legacy_hash); + } tracing::info!( block_number = sealed_block.number, diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index e8bb18b..da4f1de 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -21,6 +21,9 @@ struct ChainspecEvolveConfig { /// Block height at which the custom contract size limit activates. #[serde(default, rename = "contractSizeLimitActivationHeight")] pub contract_size_limit_activation_height: Option, + /// Block height at which canonical hash rewiring activates. + #[serde(default, rename = "hashRewireActivationHeight")] + pub hash_rewire_activation_height: Option, } /// Configuration for the Evolve payload builder @@ -44,6 +47,9 @@ pub struct EvolvePayloadBuilderConfig { /// Block height at which the custom contract size limit activates. #[serde(default)] pub contract_size_limit_activation_height: Option, + /// Block height at which canonical hash rewiring activates. + #[serde(default)] + pub hash_rewire_activation_height: Option, } impl EvolvePayloadBuilderConfig { @@ -56,6 +62,7 @@ impl EvolvePayloadBuilderConfig { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, + hash_rewire_activation_height: None, } } @@ -90,6 +97,7 @@ impl EvolvePayloadBuilderConfig { config.contract_size_limit = extras.contract_size_limit; config.contract_size_limit_activation_height = extras.contract_size_limit_activation_height; + config.hash_rewire_activation_height = extras.hash_rewire_activation_height; } Ok(config) } @@ -143,6 +151,16 @@ impl EvolvePayloadBuilderConfig { self.base_fee_redirect_settings() .and_then(|(sink, activation)| (block_number >= activation).then_some(sink)) } + + /// Returns the configured hash rewire activation height if present. + pub const fn hash_rewire_settings(&self) -> Option { + self.hash_rewire_activation_height + } + + /// Returns true if the canonical hash rewiring should be active for the provided block. + pub const fn is_hash_rewire_active_for_block(&self, block_number: u64) -> bool { + matches!(self.hash_rewire_activation_height, Some(activation) if block_number >= activation) + } } /// Errors that can occur during configuration validation @@ -221,7 +239,8 @@ mod tests { "baseFeeSink": sink, "baseFeeRedirectActivationHeight": 42, "mintAdmin": admin, - "mintPrecompileActivationHeight": 64 + "mintPrecompileActivationHeight": 64, + "hashRewireActivationHeight": 128 }); let chainspec = create_test_chainspec_with_extras(Some(extras)); @@ -231,6 +250,7 @@ mod tests { assert_eq!(config.base_fee_redirect_activation_height, Some(42)); assert_eq!(config.mint_admin, Some(admin)); assert_eq!(config.mint_precompile_activation_height, Some(64)); + assert_eq!(config.hash_rewire_activation_height, Some(128)); } #[test] @@ -256,6 +276,7 @@ mod tests { assert_eq!(config.base_fee_sink, None); assert_eq!(config.base_fee_redirect_activation_height, None); + assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -268,6 +289,7 @@ mod tests { assert_eq!(config.mint_admin, None); assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); + assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -306,6 +328,7 @@ mod tests { assert_eq!(config.mint_admin, None); assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); + assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -317,6 +340,7 @@ mod tests { assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); assert_eq!(config.contract_size_limit, None); + assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -332,6 +356,7 @@ mod tests { mint_precompile_activation_height: Some(0), contract_size_limit: None, contract_size_limit_activation_height: None, + hash_rewire_activation_height: None, }; assert!(config_with_sink.validate().is_ok()); } @@ -346,6 +371,7 @@ mod tests { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, + hash_rewire_activation_height: None, }; assert_eq!(config.base_fee_sink_for_block(4), None); @@ -373,11 +399,13 @@ mod tests { config.mint_admin, Some(address!("00000000000000000000000000000000000000aa")) ); + assert_eq!(config.hash_rewire_activation_height, None); let json_without_sink = json!({}); let config: ChainspecEvolveConfig = serde_json::from_value(json_without_sink).unwrap(); assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, None); + assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -475,4 +503,18 @@ mod tests { DEFAULT_CONTRACT_SIZE_LIMIT ); } + + #[test] + fn test_hash_rewire_activation_height_parsed() { + let extras = json!({ + "hashRewireActivationHeight": 500 + }); + + let chainspec = create_test_chainspec_with_extras(Some(extras)); + let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); + + assert_eq!(config.hash_rewire_activation_height, Some(500)); + assert!(config.is_hash_rewire_active_for_block(600)); + assert!(!config.is_hash_rewire_active_for_block(400)); + } } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 5b1e285..b678c63 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -19,19 +19,24 @@ use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; use reth_primitives_traits::{Block as _, RecoveredBlock}; use tracing::info; -use crate::{attributes::EvolveEnginePayloadAttributes, node::EvolveEngineTypes}; +use crate::{ + attributes::EvolveEnginePayloadAttributes, config::EvolvePayloadBuilderConfig, + node::EvolveEngineTypes, +}; /// Evolve engine validator that handles custom payload validation. #[derive(Debug, Clone)] pub struct EvolveEngineValidator { inner: EthereumExecutionPayloadValidator, + config: EvolvePayloadBuilderConfig, } impl EvolveEngineValidator { /// Instantiates a new validator. - pub const fn new(chain_spec: Arc) -> Self { + pub const fn new(chain_spec: Arc, config: EvolvePayloadBuilderConfig) -> Self { Self { inner: EthereumExecutionPayloadValidator::new(chain_spec), + config, } } @@ -65,6 +70,15 @@ impl PayloadValidator for EvolveEngineValidator { // Check if this is a block hash mismatch error - bypass it for evolve. if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { + let block_number = payload.payload.block_number(); + if self.config.is_hash_rewire_active_for_block(block_number) { + tracing::warn!( + block_number, + "canonical hash rewiring active; rejecting mismatched block hash" + ); + return Err(NewPayloadError::Eth(err)); + } + 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; @@ -142,6 +156,55 @@ where type Validator = EvolveEngineValidator; async fn build(self, ctx: &AddOnsContext<'_, N>) -> eyre::Result { - Ok(EvolveEngineValidator::new(ctx.config.chain.clone())) + let config = EvolvePayloadBuilderConfig::from_chain_spec(ctx.config.chain.as_ref())?; + Ok(EvolveEngineValidator::new(ctx.config.chain.clone(), config)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use reth_chainspec::ChainSpecBuilder; + use reth_primitives::{Block, SealedBlock}; + + fn validator_with_activation(height: Option) -> EvolveEngineValidator { + let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build()); + let mut config = EvolvePayloadBuilderConfig::new(); + config.hash_rewire_activation_height = height; + EvolveEngineValidator::new(chain_spec, config) + } + + 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 legacy_bypass_allows_mismatch_before_activation() { + let validator = validator_with_activation(None); + let payload = mismatched_payload(); + + validator + .ensure_well_formed_payload(payload) + .expect("hash mismatch should be bypassed before activation"); + } + + #[test] + fn canonical_mode_rejects_mismatch_after_activation() { + let validator = validator_with_activation(Some(0)); + let payload = mismatched_payload(); + + let result = validator.ensure_well_formed_payload(payload); + assert!(matches!( + result, + Err(NewPayloadError::Eth( + alloy_rpc_types::engine::PayloadError::BlockHash { .. } + )) + )); } } diff --git a/docs/canonical-hash-plan.md b/docs/canonical-hash-plan.md new file mode 100644 index 0000000..9274ad9 --- /dev/null +++ b/docs/canonical-hash-plan.md @@ -0,0 +1,47 @@ +# Plan: Restore Canonical Block Hashes While Preserving Rollkit Apphash + +## Goals + +- Emit keccak-based canonical hashes inside every header so upstream Reth tooling no longer reports continuous forks. +- Preserve the Rollkit `apphash` in a discoverable location so DA clients continue to verify block linkage. +- Gate the change behind a chainspec activation height for deterministic rollouts. + +## 1. Payload Builder & Types + +- Update `EvolvePayloadBuilder` (`crates/node/src/builder.rs`) to compute the keccak hash after `builder.finish` and assign it to `header.hash`/`parent_hash` before sealing. +- Extend `EvolveEnginePayloadAttributes` and related types (`crates/evolve/src/types.rs`, `crates/node/src/attributes.rs`) to carry the incoming `apphash` separately from the canonical hash. +- Persist the `apphash` to a new header field (candidate: encode in `extra_data` or define a dedicated header extension struct shared across payload serialization). +- Introduce activation-aware logic: pre-activation blocks keep legacy behavior; post-activation blocks always write canonical hashes while still storing the `apphash` in the new location. + +## 2. Validator & Consensus + +- Modify `EvolveEngineValidator` (`crates/node/src/validator.rs`) to stop bypassing `PayloadError::BlockHashMismatch` after the activation height. Retain the bypass for legacy blocks. +- Ensure `EvolveConsensus` (`crates/evolve/src/consensus.rs`) now validates hash/parentHash linkages post-activation while keeping the relaxed timestamp rule. +- Audit any code paths that rely on the `apphash` (e.g., Rollkit-specific checks) and point them to the relocated field. + +## 3. RPC & Serialization + +- Adjust the conversion helpers that produce `ExecutionPayload` / `ExecutionPayloadEnvelope` values so RPC consumers see the canonical hash in the standard field and the `apphash` via either `extraData` or a new optional field. +- Clearly document the new field semantics so explorers/light clients know where to read the Rollkit hash. +- Maintain backward compatibility in request parsing: continue accepting payload attributes that include the `apphash`, even though it is no longer stored in `header.hash`. + +## 4. Chainspec & Configuration + +- Add a new evolve config flag, e.g. `hashRewireActivationHeight`, parsed in `crates/evolve/src/config.rs` and surfaced through `EvolvePayloadBuilderConfig`. +- Validate the flag alongside existing evolve settings; log or reject invalid configurations. +- Update sample configs (`etc/ev-reth-genesis.json`) and the README/upgrade docs with instructions for setting the activation height. + +## 5. Testing & Tooling + +- Extend e2e tests (`crates/tests/src/e2e_tests.rs`, `test_evolve_engine_api.rs`) to cover both pre- and post-activation behavior, asserting that: + - Legacy mode still bypasses hash mismatches. + - Post-activation blocks produce canonical parent links and expose the `apphash` in the new field. +- Add unit tests around the serialization helpers to ensure RPC payloads echo both hashes correctly. +- Verify Rollkit integration tests continue to pass once they read the `apphash` from the new location. + +## 6. Rollout Steps + +1. Implement the code changes behind the activation height flag and land them with comprehensive tests. +2. Publish an upgrade note describing the new flag, how to set the activation block, and how to verify the behavior via RPC. +3. Coordinate with testnet/mainnet operators to schedule the activation block and ensure explorers/monitoring tools understand the relocated `apphash` field. +4. After activation, monitor forkchoice health and Rollkit ingestion to confirm both canonical and DA workflows function correctly. From d9d9f1898fbd30c7a81b71a03ed46289038ac90b Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 16 Dec 2025 10:13:57 -0800 Subject: [PATCH 2/5] more docs --- CHANGELOG.md | 1 + README.md | 6 +++++ crates/node/src/builder.rs | 10 +++++++- crates/node/src/config.rs | 8 +++++++ docs/canonical-hash-plan.md | 47 ------------------------------------- 5 files changed, 24 insertions(+), 48 deletions(-) delete mode 100644 docs/canonical-hash-plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e2405b3..acacd63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 373e528..d41a570 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,12 @@ Reth had to ignore block-hash mismatches and upstream tooling flagged every bloc networks can opt-in to canonical keccak block hashes by setting `hashRewireActivationHeight` inside the `evolve` section of the chainspec: +Separately, Ethereum clients expect `stateRoot` to be the current block's post-state root (after +executing this block's transactions on top of the parent). If an integration accidentally supplies +the parent (height-1) root instead, execution clients can report persistent "forks" that are really +just state-root mismatches. ev-reth computes and exposes the canonical post-state root so state-root +validation behaves like standard Ethereum. + ```json "config": { ..., diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 50d564f..8c1e58b 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: _, @@ -189,6 +194,9 @@ where let mut sealed_block = block.sealed_block().clone(); + // Legacy mode: preserve historical behavior where the Engine API block hash did not match + // the canonical keccak header hash. We intentionally re-seal with an alternate hash while + // keeping the Ethereum `state_root` intact for normal state-root validation. if !self.config.is_hash_rewire_active_for_block(block_number) { let legacy_hash = sealed_block.header().state_root; let legacy_block = sealed_block.clone_block(); diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index da4f1de..97f7dd3 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -48,6 +48,14 @@ pub struct EvolvePayloadBuilderConfig { #[serde(default)] pub contract_size_limit_activation_height: Option, /// Block height at which canonical hash rewiring activates. + /// + /// Before activation, ev-reth operates in "legacy" mode to support deployments that flowed an + /// application-level hash through Engine API payloads. In that mode the node may bypass block + /// hash mismatches and re-seal blocks with an alternate hash for compatibility. + /// + /// After activation, the node enforces canonical keccak block hashes as expected by standard + /// Ethereum tooling. Note that `header.state_root` always follows Ethereum semantics (the + /// current block's post-state root), independent of this setting. #[serde(default)] pub hash_rewire_activation_height: Option, } diff --git a/docs/canonical-hash-plan.md b/docs/canonical-hash-plan.md deleted file mode 100644 index 9274ad9..0000000 --- a/docs/canonical-hash-plan.md +++ /dev/null @@ -1,47 +0,0 @@ -# Plan: Restore Canonical Block Hashes While Preserving Rollkit Apphash - -## Goals - -- Emit keccak-based canonical hashes inside every header so upstream Reth tooling no longer reports continuous forks. -- Preserve the Rollkit `apphash` in a discoverable location so DA clients continue to verify block linkage. -- Gate the change behind a chainspec activation height for deterministic rollouts. - -## 1. Payload Builder & Types - -- Update `EvolvePayloadBuilder` (`crates/node/src/builder.rs`) to compute the keccak hash after `builder.finish` and assign it to `header.hash`/`parent_hash` before sealing. -- Extend `EvolveEnginePayloadAttributes` and related types (`crates/evolve/src/types.rs`, `crates/node/src/attributes.rs`) to carry the incoming `apphash` separately from the canonical hash. -- Persist the `apphash` to a new header field (candidate: encode in `extra_data` or define a dedicated header extension struct shared across payload serialization). -- Introduce activation-aware logic: pre-activation blocks keep legacy behavior; post-activation blocks always write canonical hashes while still storing the `apphash` in the new location. - -## 2. Validator & Consensus - -- Modify `EvolveEngineValidator` (`crates/node/src/validator.rs`) to stop bypassing `PayloadError::BlockHashMismatch` after the activation height. Retain the bypass for legacy blocks. -- Ensure `EvolveConsensus` (`crates/evolve/src/consensus.rs`) now validates hash/parentHash linkages post-activation while keeping the relaxed timestamp rule. -- Audit any code paths that rely on the `apphash` (e.g., Rollkit-specific checks) and point them to the relocated field. - -## 3. RPC & Serialization - -- Adjust the conversion helpers that produce `ExecutionPayload` / `ExecutionPayloadEnvelope` values so RPC consumers see the canonical hash in the standard field and the `apphash` via either `extraData` or a new optional field. -- Clearly document the new field semantics so explorers/light clients know where to read the Rollkit hash. -- Maintain backward compatibility in request parsing: continue accepting payload attributes that include the `apphash`, even though it is no longer stored in `header.hash`. - -## 4. Chainspec & Configuration - -- Add a new evolve config flag, e.g. `hashRewireActivationHeight`, parsed in `crates/evolve/src/config.rs` and surfaced through `EvolvePayloadBuilderConfig`. -- Validate the flag alongside existing evolve settings; log or reject invalid configurations. -- Update sample configs (`etc/ev-reth-genesis.json`) and the README/upgrade docs with instructions for setting the activation height. - -## 5. Testing & Tooling - -- Extend e2e tests (`crates/tests/src/e2e_tests.rs`, `test_evolve_engine_api.rs`) to cover both pre- and post-activation behavior, asserting that: - - Legacy mode still bypasses hash mismatches. - - Post-activation blocks produce canonical parent links and expose the `apphash` in the new field. -- Add unit tests around the serialization helpers to ensure RPC payloads echo both hashes correctly. -- Verify Rollkit integration tests continue to pass once they read the `apphash` from the new location. - -## 6. Rollout Steps - -1. Implement the code changes behind the activation height flag and land them with comprehensive tests. -2. Publish an upgrade note describing the new flag, how to set the activation block, and how to verify the behavior via RPC. -3. Coordinate with testnet/mainnet operators to schedule the activation block and ensure explorers/monitoring tools understand the relocated `apphash` field. -4. After activation, monitor forkchoice health and Rollkit ingestion to confirm both canonical and DA workflows function correctly. From a13eb05adbb6b04eec40cc8963d16459f69e4aed Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 16 Dec 2025 10:19:24 -0800 Subject: [PATCH 3/5] markdownlint --- CHANGELOG.md | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acacd63..3b53f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +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 d41a570..28d9429 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 From ddd20dc221aae45b8932a8b1cfc9f4a33f60a615 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 16 Dec 2025 20:36:32 -0800 Subject: [PATCH 4/5] add update --- crates/node/src/builder.rs | 11 +-- crates/node/src/config.rs | 129 +++++++++++++++++++++++++---------- crates/node/src/validator.rs | 70 +++++++++++++++---- 3 files changed, 149 insertions(+), 61 deletions(-) diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 8c1e58b..34c0222 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -192,16 +192,7 @@ where .finish(&state_provider) .map_err(PayloadBuilderError::other)?; - let mut sealed_block = block.sealed_block().clone(); - - // Legacy mode: preserve historical behavior where the Engine API block hash did not match - // the canonical keccak header hash. We intentionally re-seal with an alternate hash while - // keeping the Ethereum `state_root` intact for normal state-root validation. - if !self.config.is_hash_rewire_active_for_block(block_number) { - let legacy_hash = sealed_block.header().state_root; - let legacy_block = sealed_block.clone_block(); - sealed_block = SealedBlock::new_unchecked(legacy_block, legacy_hash); - } + let sealed_block = block.sealed_block().clone(); tracing::info!( block_number = sealed_block.number, diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index 97f7dd3..5cb47e5 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -21,9 +21,32 @@ struct ChainspecEvolveConfig { /// Block height at which the custom contract size limit activates. #[serde(default, rename = "contractSizeLimitActivationHeight")] pub contract_size_limit_activation_height: Option, - /// Block height at which canonical hash rewiring activates. - #[serde(default, rename = "hashRewireActivationHeight")] - pub hash_rewire_activation_height: Option, + /// Block height at which canonical block hash enforcement activates. + /// + /// # Background + /// + /// Early versions of ev-node passed block hashes from height H-1 instead of H + /// when communicating with ev-reth via the Engine API. This caused block hashes + /// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header), + /// resulting in block explorers like Blockscout incorrectly displaying every block + /// as a fork due to parent hash mismatches. + /// + /// # Migration Strategy + /// + /// For existing networks with historical blocks containing non-canonical hashes: + /// - Set this to a future block height where the fix will activate + /// - Before the activation height: hash mismatches are bypassed (legacy mode) + /// - At and after the activation height: canonical hash validation is enforced + /// + /// This allows nodes to sync from genesis on networks with historical hash + /// mismatches while ensuring new blocks use correct canonical hashes. + /// + /// # Default Behavior + /// + /// If not set, canonical hash validation is enforced from genesis (block 0). + /// This is the correct setting for new networks without legacy data. + #[serde(default, rename = "canonicalHashActivationHeight")] + pub canonical_hash_activation_height: Option, } /// Configuration for the Evolve payload builder @@ -47,17 +70,32 @@ pub struct EvolvePayloadBuilderConfig { /// Block height at which the custom contract size limit activates. #[serde(default)] pub contract_size_limit_activation_height: Option, - /// Block height at which canonical hash rewiring activates. + /// Block height at which canonical block hash enforcement activates. + /// + /// # Background + /// + /// Early versions of ev-node passed block hashes from height H-1 instead of H + /// when communicating with ev-reth via the Engine API. This caused block hashes + /// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header), + /// resulting in block explorers like Blockscout incorrectly displaying every block + /// as a fork due to parent hash mismatches. + /// + /// # Migration Strategy + /// + /// For existing networks with historical blocks containing non-canonical hashes: + /// - Set this to a future block height where the fix will activate + /// - Before the activation height: hash mismatches are bypassed (legacy mode) + /// - At and after the activation height: canonical hash validation is enforced + /// + /// This allows nodes to sync from genesis on networks with historical hash + /// mismatches while ensuring new blocks use correct canonical hashes. /// - /// Before activation, ev-reth operates in "legacy" mode to support deployments that flowed an - /// application-level hash through Engine API payloads. In that mode the node may bypass block - /// hash mismatches and re-seal blocks with an alternate hash for compatibility. + /// # Default Behavior /// - /// After activation, the node enforces canonical keccak block hashes as expected by standard - /// Ethereum tooling. Note that `header.state_root` always follows Ethereum semantics (the - /// current block's post-state root), independent of this setting. + /// If not set, canonical hash validation is enforced from genesis (block 0). + /// This is the correct setting for new networks without legacy data. #[serde(default)] - pub hash_rewire_activation_height: Option, + pub canonical_hash_activation_height: Option, } impl EvolvePayloadBuilderConfig { @@ -70,7 +108,7 @@ impl EvolvePayloadBuilderConfig { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, - hash_rewire_activation_height: None, + canonical_hash_activation_height: None, } } @@ -105,7 +143,7 @@ impl EvolvePayloadBuilderConfig { config.contract_size_limit = extras.contract_size_limit; config.contract_size_limit_activation_height = extras.contract_size_limit_activation_height; - config.hash_rewire_activation_height = extras.hash_rewire_activation_height; + config.canonical_hash_activation_height = extras.canonical_hash_activation_height; } Ok(config) } @@ -160,14 +198,27 @@ impl EvolvePayloadBuilderConfig { .and_then(|(sink, activation)| (block_number >= activation).then_some(sink)) } - /// Returns the configured hash rewire activation height if present. - pub const fn hash_rewire_settings(&self) -> Option { - self.hash_rewire_activation_height - } - - /// Returns true if the canonical hash rewiring should be active for the provided block. - pub const fn is_hash_rewire_active_for_block(&self, block_number: u64) -> bool { - matches!(self.hash_rewire_activation_height, Some(activation) if block_number >= activation) + /// Returns true if canonical block hash validation should be enforced for the given block. + /// + /// This method controls whether the validator should reject blocks with hash mismatches + /// or bypass the check for legacy compatibility. + /// + /// # Returns + /// + /// - `true`: Enforce canonical hash validation (reject mismatches) + /// - `false`: Bypass hash validation (legacy mode for historical blocks) + /// + /// # Logic + /// + /// - If `canonical_hash_activation_height` is `None`: Always enforce (new networks) + /// - If `canonical_hash_activation_height` is `Some(N)`: + /// - `block_number < N`: Don't enforce (legacy mode) + /// - `block_number >= N`: Enforce (canonical mode) + pub const fn is_canonical_hash_enforced(&self, block_number: u64) -> bool { + match self.canonical_hash_activation_height { + Some(activation) => block_number >= activation, + None => true, // Default: enforce canonical hashes from genesis + } } } @@ -247,8 +298,7 @@ mod tests { "baseFeeSink": sink, "baseFeeRedirectActivationHeight": 42, "mintAdmin": admin, - "mintPrecompileActivationHeight": 64, - "hashRewireActivationHeight": 128 + "mintPrecompileActivationHeight": 64 }); let chainspec = create_test_chainspec_with_extras(Some(extras)); @@ -258,7 +308,6 @@ mod tests { assert_eq!(config.base_fee_redirect_activation_height, Some(42)); assert_eq!(config.mint_admin, Some(admin)); assert_eq!(config.mint_precompile_activation_height, Some(64)); - assert_eq!(config.hash_rewire_activation_height, Some(128)); } #[test] @@ -284,7 +333,6 @@ mod tests { assert_eq!(config.base_fee_sink, None); assert_eq!(config.base_fee_redirect_activation_height, None); - assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -297,7 +345,6 @@ mod tests { assert_eq!(config.mint_admin, None); assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); - assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -336,7 +383,6 @@ mod tests { assert_eq!(config.mint_admin, None); assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); - assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -348,7 +394,6 @@ mod tests { assert_eq!(config.base_fee_redirect_activation_height, None); assert_eq!(config.mint_precompile_activation_height, None); assert_eq!(config.contract_size_limit, None); - assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -364,7 +409,7 @@ mod tests { mint_precompile_activation_height: Some(0), contract_size_limit: None, contract_size_limit_activation_height: None, - hash_rewire_activation_height: None, + canonical_hash_activation_height: None, }; assert!(config_with_sink.validate().is_ok()); } @@ -379,7 +424,7 @@ mod tests { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, - hash_rewire_activation_height: None, + canonical_hash_activation_height: None, }; assert_eq!(config.base_fee_sink_for_block(4), None); @@ -407,13 +452,11 @@ mod tests { config.mint_admin, Some(address!("00000000000000000000000000000000000000aa")) ); - assert_eq!(config.hash_rewire_activation_height, None); let json_without_sink = json!({}); let config: ChainspecEvolveConfig = serde_json::from_value(json_without_sink).unwrap(); assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, None); - assert_eq!(config.hash_rewire_activation_height, None); } #[test] @@ -513,16 +556,28 @@ mod tests { } #[test] - fn test_hash_rewire_activation_height_parsed() { + fn test_canonical_hash_activation_from_chainspec() { let extras = json!({ - "hashRewireActivationHeight": 500 + "canonicalHashActivationHeight": 500 }); let chainspec = create_test_chainspec_with_extras(Some(extras)); let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); - assert_eq!(config.hash_rewire_activation_height, Some(500)); - assert!(config.is_hash_rewire_active_for_block(600)); - assert!(!config.is_hash_rewire_active_for_block(400)); + assert_eq!(config.canonical_hash_activation_height, Some(500)); + // Before activation: legacy mode (not enforced) + assert!(!config.is_canonical_hash_enforced(499)); + // At and after activation: canonical mode (enforced) + assert!(config.is_canonical_hash_enforced(500)); + assert!(config.is_canonical_hash_enforced(600)); + } + + #[test] + fn test_canonical_hash_default_enforces_from_genesis() { + // When not configured, canonical hashes should be enforced from genesis + let config = EvolvePayloadBuilderConfig::new(); + assert_eq!(config.canonical_hash_activation_height, None); + assert!(config.is_canonical_hash_enforced(0)); + assert!(config.is_canonical_hash_enforced(1000)); } } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index b678c63..ca4e328 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -25,6 +25,17 @@ use crate::{ }; /// Evolve engine validator that handles custom payload validation. +/// +/// This validator extends the standard Ethereum payload validation with support for +/// legacy block hash compatibility. See [`EvolvePayloadBuilderConfig::canonical_hash_activation_height`] +/// for details on the migration strategy. +/// +/// # Block Hash Validation +/// +/// Early versions of ev-node passed block hashes from height H-1 instead of H, +/// causing block explorers (e.g., Blockscout) to show all blocks as forks. This +/// validator can bypass hash mismatch errors for historical blocks while enforcing +/// canonical validation for new blocks, controlled by `canonical_hash_activation_height`. #[derive(Debug, Clone)] pub struct EvolveEngineValidator { inner: EthereumExecutionPayloadValidator, @@ -56,7 +67,6 @@ 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"); @@ -65,29 +75,44 @@ impl PayloadValidator for EvolveEngineValidator { .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. + // Handle block hash mismatch errors specially for legacy compatibility. + // + // Background: Early versions of ev-node passed block hashes from height H-1 + // instead of H when communicating with ev-reth via the Engine API. This caused + // block hashes to not match the canonical Ethereum block hash (keccak256 of + // RLP-encoded header), resulting in block explorers like Blockscout incorrectly + // displaying every block as a fork due to parent hash mismatches. + // + // For existing networks with historical blocks containing these non-canonical + // hashes, we need to bypass this validation to allow nodes to sync from genesis. + // The `canonical_hash_activation_height` config controls when to start enforcing + // canonical hashes for new blocks. if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { let block_number = payload.payload.block_number(); - if self.config.is_hash_rewire_active_for_block(block_number) { + + // If canonical hash enforcement is active for this block, reject the mismatch + if self.config.is_canonical_hash_enforced(block_number) { tracing::warn!( block_number, - "canonical hash rewiring active; rejecting mismatched block hash" + "canonical hash enforcement active; rejecting mismatched block hash" ); return Err(NewPayloadError::Eth(err)); } - 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. + // Legacy mode: bypass hash mismatch to allow syncing historical blocks. + // Re-seal the block with the correct canonical hash (keccak256 of header). + info!( + block_number, + "bypassing block hash mismatch (legacy mode before activation height)" + ); 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)) } } @@ -168,10 +193,10 @@ mod tests { use reth_chainspec::ChainSpecBuilder; use reth_primitives::{Block, SealedBlock}; - fn validator_with_activation(height: Option) -> EvolveEngineValidator { + fn validator_with_activation(activation_height: Option) -> EvolveEngineValidator { let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build()); let mut config = EvolvePayloadBuilderConfig::new(); - config.hash_rewire_activation_height = height; + config.canonical_hash_activation_height = activation_height; EvolveEngineValidator::new(chain_spec, config) } @@ -185,17 +210,19 @@ mod tests { } #[test] - fn legacy_bypass_allows_mismatch_before_activation() { - let validator = validator_with_activation(None); + fn test_legacy_mode_bypasses_hash_mismatch() { + // When activation height is set in the future, legacy mode should bypass hash mismatches + let validator = validator_with_activation(Some(1000)); let payload = mismatched_payload(); validator .ensure_well_formed_payload(payload) - .expect("hash mismatch should be bypassed before activation"); + .expect("hash mismatch should be bypassed in legacy mode"); } #[test] - fn canonical_mode_rejects_mismatch_after_activation() { + fn test_canonical_mode_rejects_hash_mismatch() { + // When activation height is 0 (or in the past), canonical mode should reject mismatches let validator = validator_with_activation(Some(0)); let payload = mismatched_payload(); @@ -207,4 +234,19 @@ mod tests { )) )); } + + #[test] + fn test_default_enforces_canonical_hash() { + // When no activation height is set, canonical validation should be enforced (default) + let validator = validator_with_activation(None); + let payload = mismatched_payload(); + + let result = validator.ensure_well_formed_payload(payload); + assert!(matches!( + result, + Err(NewPayloadError::Eth( + alloy_rpc_types::engine::PayloadError::BlockHash { .. } + )) + )); + } } From 79f1c814246508949b49c7aaa2b957fce7822375 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 16 Dec 2025 21:02:29 -0800 Subject: [PATCH 5/5] testgpu --- README.md | 27 -------- crates/node/src/config.rs | 104 ------------------------------ crates/node/src/validator.rs | 120 +++++------------------------------ 3 files changed, 17 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index 28d9429..13a019b 100644 --- a/README.md +++ b/README.md @@ -456,30 +456,3 @@ This project builds upon the excellent work of: - [Reth](https://github.com/paradigmxyz/reth) - The Rust Ethereum client - [Evolve](https://ev.xyz/) - The modular rollup framework - -### Canonical Block Hash Activation - -Legacy deployments allowed application-level hashes to flow through the Engine API, which meant -Reth had to ignore block-hash mismatches and upstream tooling flagged every block as a fork. Newer -networks can opt-in to canonical keccak block hashes by setting `hashRewireActivationHeight` inside -the `evolve` section of the chainspec: - -Separately, Ethereum clients expect `stateRoot` to be the current block's post-state root (after -executing this block's transactions on top of the parent). If an integration accidentally supplies -the parent (height-1) root instead, execution clients can report persistent "forks" that are really -just state-root mismatches. ev-reth computes and exposes the canonical post-state root so state-root -validation behaves like standard Ethereum. - -```json -"config": { - ..., - "evolve": { - "hashRewireActivationHeight": 0 - } -} -``` - -Set the activation height to the first block where canonical hashes should be enforced (use `0` for -fresh networks). Before the activation height the node continues to bypass hash mismatches so -existing chains keep working; after activation, the node rejects malformed payloads and the reported -block hash always matches the standard Engine API expectations. diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index 5cb47e5..f3029a1 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -21,32 +21,6 @@ struct ChainspecEvolveConfig { /// Block height at which the custom contract size limit activates. #[serde(default, rename = "contractSizeLimitActivationHeight")] pub contract_size_limit_activation_height: Option, - /// Block height at which canonical block hash enforcement activates. - /// - /// # Background - /// - /// Early versions of ev-node passed block hashes from height H-1 instead of H - /// when communicating with ev-reth via the Engine API. This caused block hashes - /// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header), - /// resulting in block explorers like Blockscout incorrectly displaying every block - /// as a fork due to parent hash mismatches. - /// - /// # Migration Strategy - /// - /// For existing networks with historical blocks containing non-canonical hashes: - /// - Set this to a future block height where the fix will activate - /// - Before the activation height: hash mismatches are bypassed (legacy mode) - /// - At and after the activation height: canonical hash validation is enforced - /// - /// This allows nodes to sync from genesis on networks with historical hash - /// mismatches while ensuring new blocks use correct canonical hashes. - /// - /// # Default Behavior - /// - /// If not set, canonical hash validation is enforced from genesis (block 0). - /// This is the correct setting for new networks without legacy data. - #[serde(default, rename = "canonicalHashActivationHeight")] - pub canonical_hash_activation_height: Option, } /// Configuration for the Evolve payload builder @@ -70,32 +44,6 @@ pub struct EvolvePayloadBuilderConfig { /// Block height at which the custom contract size limit activates. #[serde(default)] pub contract_size_limit_activation_height: Option, - /// Block height at which canonical block hash enforcement activates. - /// - /// # Background - /// - /// Early versions of ev-node passed block hashes from height H-1 instead of H - /// when communicating with ev-reth via the Engine API. This caused block hashes - /// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header), - /// resulting in block explorers like Blockscout incorrectly displaying every block - /// as a fork due to parent hash mismatches. - /// - /// # Migration Strategy - /// - /// For existing networks with historical blocks containing non-canonical hashes: - /// - Set this to a future block height where the fix will activate - /// - Before the activation height: hash mismatches are bypassed (legacy mode) - /// - At and after the activation height: canonical hash validation is enforced - /// - /// This allows nodes to sync from genesis on networks with historical hash - /// mismatches while ensuring new blocks use correct canonical hashes. - /// - /// # Default Behavior - /// - /// If not set, canonical hash validation is enforced from genesis (block 0). - /// This is the correct setting for new networks without legacy data. - #[serde(default)] - pub canonical_hash_activation_height: Option, } impl EvolvePayloadBuilderConfig { @@ -108,7 +56,6 @@ impl EvolvePayloadBuilderConfig { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, - canonical_hash_activation_height: None, } } @@ -143,7 +90,6 @@ impl EvolvePayloadBuilderConfig { config.contract_size_limit = extras.contract_size_limit; config.contract_size_limit_activation_height = extras.contract_size_limit_activation_height; - config.canonical_hash_activation_height = extras.canonical_hash_activation_height; } Ok(config) } @@ -197,29 +143,6 @@ impl EvolvePayloadBuilderConfig { self.base_fee_redirect_settings() .and_then(|(sink, activation)| (block_number >= activation).then_some(sink)) } - - /// Returns true if canonical block hash validation should be enforced for the given block. - /// - /// This method controls whether the validator should reject blocks with hash mismatches - /// or bypass the check for legacy compatibility. - /// - /// # Returns - /// - /// - `true`: Enforce canonical hash validation (reject mismatches) - /// - `false`: Bypass hash validation (legacy mode for historical blocks) - /// - /// # Logic - /// - /// - If `canonical_hash_activation_height` is `None`: Always enforce (new networks) - /// - If `canonical_hash_activation_height` is `Some(N)`: - /// - `block_number < N`: Don't enforce (legacy mode) - /// - `block_number >= N`: Enforce (canonical mode) - pub const fn is_canonical_hash_enforced(&self, block_number: u64) -> bool { - match self.canonical_hash_activation_height { - Some(activation) => block_number >= activation, - None => true, // Default: enforce canonical hashes from genesis - } - } } /// Errors that can occur during configuration validation @@ -409,7 +332,6 @@ mod tests { mint_precompile_activation_height: Some(0), contract_size_limit: None, contract_size_limit_activation_height: None, - canonical_hash_activation_height: None, }; assert!(config_with_sink.validate().is_ok()); } @@ -424,7 +346,6 @@ mod tests { mint_precompile_activation_height: None, contract_size_limit: None, contract_size_limit_activation_height: None, - canonical_hash_activation_height: None, }; assert_eq!(config.base_fee_sink_for_block(4), None); @@ -555,29 +476,4 @@ mod tests { ); } - #[test] - fn test_canonical_hash_activation_from_chainspec() { - let extras = json!({ - "canonicalHashActivationHeight": 500 - }); - - let chainspec = create_test_chainspec_with_extras(Some(extras)); - let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); - - assert_eq!(config.canonical_hash_activation_height, Some(500)); - // Before activation: legacy mode (not enforced) - assert!(!config.is_canonical_hash_enforced(499)); - // At and after activation: canonical mode (enforced) - assert!(config.is_canonical_hash_enforced(500)); - assert!(config.is_canonical_hash_enforced(600)); - } - - #[test] - fn test_canonical_hash_default_enforces_from_genesis() { - // When not configured, canonical hashes should be enforced from genesis - let config = EvolvePayloadBuilderConfig::new(); - assert_eq!(config.canonical_hash_activation_height, None); - assert!(config.is_canonical_hash_enforced(0)); - assert!(config.is_canonical_hash_enforced(1000)); - } } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index ca4e328..47e9a53 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -16,38 +16,24 @@ 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, config::EvolvePayloadBuilderConfig, - node::EvolveEngineTypes, -}; +use crate::{attributes::EvolveEnginePayloadAttributes, node::EvolveEngineTypes}; /// Evolve engine validator that handles custom payload validation. /// -/// This validator extends the standard Ethereum payload validation with support for -/// legacy block hash compatibility. See [`EvolvePayloadBuilderConfig::canonical_hash_activation_height`] -/// for details on the migration strategy. -/// -/// # Block Hash Validation -/// -/// Early versions of ev-node passed block hashes from height H-1 instead of H, -/// causing block explorers (e.g., Blockscout) to show all blocks as forks. This -/// validator can bypass hash mismatch errors for historical blocks while enforcing -/// canonical validation for new blocks, controlled by `canonical_hash_activation_height`. +/// This validator delegates to the standard Ethereum payload validation. #[derive(Debug, Clone)] pub struct EvolveEngineValidator { inner: EthereumExecutionPayloadValidator, - config: EvolvePayloadBuilderConfig, } impl EvolveEngineValidator { /// Instantiates a new validator. - pub const fn new(chain_spec: Arc, config: EvolvePayloadBuilderConfig) -> Self { + pub const fn new(chain_spec: Arc) -> Self { Self { inner: EthereumExecutionPayloadValidator::new(chain_spec), - config, } } @@ -67,56 +53,13 @@ impl PayloadValidator for EvolveEngineValidator { ) -> Result, NewPayloadError> { info!("Evolve engine validator: validating payload"); - 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) => { - tracing::debug!("Evolve payload validation error: {:?}", err); - - // Handle block hash mismatch errors specially for legacy compatibility. - // - // Background: Early versions of ev-node passed block hashes from height H-1 - // instead of H when communicating with ev-reth via the Engine API. This caused - // block hashes to not match the canonical Ethereum block hash (keccak256 of - // RLP-encoded header), resulting in block explorers like Blockscout incorrectly - // displaying every block as a fork due to parent hash mismatches. - // - // For existing networks with historical blocks containing these non-canonical - // hashes, we need to bypass this validation to allow nodes to sync from genesis. - // The `canonical_hash_activation_height` config controls when to start enforcing - // canonical hashes for new blocks. - if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { - let block_number = payload.payload.block_number(); - - // If canonical hash enforcement is active for this block, reject the mismatch - if self.config.is_canonical_hash_enforced(block_number) { - tracing::warn!( - block_number, - "canonical hash enforcement active; rejecting mismatched block hash" - ); - return Err(NewPayloadError::Eth(err)); - } - - // Legacy mode: bypass hash mismatch to allow syncing historical blocks. - // Re-seal the block with the correct canonical hash (keccak256 of header). - info!( - block_number, - "bypassing block hash mismatch (legacy mode before activation height)" - ); - 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 { - 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( @@ -181,8 +124,7 @@ where type Validator = EvolveEngineValidator; async fn build(self, ctx: &AddOnsContext<'_, N>) -> eyre::Result { - let config = EvolvePayloadBuilderConfig::from_chain_spec(ctx.config.chain.as_ref())?; - Ok(EvolveEngineValidator::new(ctx.config.chain.clone(), config)) + Ok(EvolveEngineValidator::new(ctx.config.chain.clone())) } } @@ -193,11 +135,9 @@ mod tests { use reth_chainspec::ChainSpecBuilder; use reth_primitives::{Block, SealedBlock}; - fn validator_with_activation(activation_height: Option) -> EvolveEngineValidator { + fn create_validator() -> EvolveEngineValidator { let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build()); - let mut config = EvolvePayloadBuilderConfig::new(); - config.canonical_hash_activation_height = activation_height; - EvolveEngineValidator::new(chain_spec, config) + EvolveEngineValidator::new(chain_spec) } fn mismatched_payload() -> ExecutionData { @@ -210,35 +150,9 @@ mod tests { } #[test] - fn test_legacy_mode_bypasses_hash_mismatch() { - // When activation height is set in the future, legacy mode should bypass hash mismatches - let validator = validator_with_activation(Some(1000)); - let payload = mismatched_payload(); - - validator - .ensure_well_formed_payload(payload) - .expect("hash mismatch should be bypassed in legacy mode"); - } - - #[test] - fn test_canonical_mode_rejects_hash_mismatch() { - // When activation height is 0 (or in the past), canonical mode should reject mismatches - let validator = validator_with_activation(Some(0)); - let payload = mismatched_payload(); - - let result = validator.ensure_well_formed_payload(payload); - assert!(matches!( - result, - Err(NewPayloadError::Eth( - alloy_rpc_types::engine::PayloadError::BlockHash { .. } - )) - )); - } - - #[test] - fn test_default_enforces_canonical_hash() { - // When no activation height is set, canonical validation should be enforced (default) - let validator = validator_with_activation(None); + 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);