Skip to content

Commit ddd20dc

Browse files
committed
add update
1 parent a13eb05 commit ddd20dc

File tree

3 files changed

+149
-61
lines changed

3 files changed

+149
-61
lines changed

crates/node/src/builder.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,7 @@ where
192192
.finish(&state_provider)
193193
.map_err(PayloadBuilderError::other)?;
194194

195-
let mut sealed_block = block.sealed_block().clone();
196-
197-
// Legacy mode: preserve historical behavior where the Engine API block hash did not match
198-
// the canonical keccak header hash. We intentionally re-seal with an alternate hash while
199-
// keeping the Ethereum `state_root` intact for normal state-root validation.
200-
if !self.config.is_hash_rewire_active_for_block(block_number) {
201-
let legacy_hash = sealed_block.header().state_root;
202-
let legacy_block = sealed_block.clone_block();
203-
sealed_block = SealedBlock::new_unchecked(legacy_block, legacy_hash);
204-
}
195+
let sealed_block = block.sealed_block().clone();
205196

206197
tracing::info!(
207198
block_number = sealed_block.number,

crates/node/src/config.rs

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,32 @@ struct ChainspecEvolveConfig {
2121
/// Block height at which the custom contract size limit activates.
2222
#[serde(default, rename = "contractSizeLimitActivationHeight")]
2323
pub contract_size_limit_activation_height: Option<u64>,
24-
/// Block height at which canonical hash rewiring activates.
25-
#[serde(default, rename = "hashRewireActivationHeight")]
26-
pub hash_rewire_activation_height: Option<u64>,
24+
/// Block height at which canonical block hash enforcement activates.
25+
///
26+
/// # Background
27+
///
28+
/// Early versions of ev-node passed block hashes from height H-1 instead of H
29+
/// when communicating with ev-reth via the Engine API. This caused block hashes
30+
/// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header),
31+
/// resulting in block explorers like Blockscout incorrectly displaying every block
32+
/// as a fork due to parent hash mismatches.
33+
///
34+
/// # Migration Strategy
35+
///
36+
/// For existing networks with historical blocks containing non-canonical hashes:
37+
/// - Set this to a future block height where the fix will activate
38+
/// - Before the activation height: hash mismatches are bypassed (legacy mode)
39+
/// - At and after the activation height: canonical hash validation is enforced
40+
///
41+
/// This allows nodes to sync from genesis on networks with historical hash
42+
/// mismatches while ensuring new blocks use correct canonical hashes.
43+
///
44+
/// # Default Behavior
45+
///
46+
/// If not set, canonical hash validation is enforced from genesis (block 0).
47+
/// This is the correct setting for new networks without legacy data.
48+
#[serde(default, rename = "canonicalHashActivationHeight")]
49+
pub canonical_hash_activation_height: Option<u64>,
2750
}
2851

2952
/// Configuration for the Evolve payload builder
@@ -47,17 +70,32 @@ pub struct EvolvePayloadBuilderConfig {
4770
/// Block height at which the custom contract size limit activates.
4871
#[serde(default)]
4972
pub contract_size_limit_activation_height: Option<u64>,
50-
/// Block height at which canonical hash rewiring activates.
73+
/// Block height at which canonical block hash enforcement activates.
74+
///
75+
/// # Background
76+
///
77+
/// Early versions of ev-node passed block hashes from height H-1 instead of H
78+
/// when communicating with ev-reth via the Engine API. This caused block hashes
79+
/// to not match the canonical Ethereum block hash (keccak256 of RLP-encoded header),
80+
/// resulting in block explorers like Blockscout incorrectly displaying every block
81+
/// as a fork due to parent hash mismatches.
82+
///
83+
/// # Migration Strategy
84+
///
85+
/// For existing networks with historical blocks containing non-canonical hashes:
86+
/// - Set this to a future block height where the fix will activate
87+
/// - Before the activation height: hash mismatches are bypassed (legacy mode)
88+
/// - At and after the activation height: canonical hash validation is enforced
89+
///
90+
/// This allows nodes to sync from genesis on networks with historical hash
91+
/// mismatches while ensuring new blocks use correct canonical hashes.
5192
///
52-
/// Before activation, ev-reth operates in "legacy" mode to support deployments that flowed an
53-
/// application-level hash through Engine API payloads. In that mode the node may bypass block
54-
/// hash mismatches and re-seal blocks with an alternate hash for compatibility.
93+
/// # Default Behavior
5594
///
56-
/// After activation, the node enforces canonical keccak block hashes as expected by standard
57-
/// Ethereum tooling. Note that `header.state_root` always follows Ethereum semantics (the
58-
/// current block's post-state root), independent of this setting.
95+
/// If not set, canonical hash validation is enforced from genesis (block 0).
96+
/// This is the correct setting for new networks without legacy data.
5997
#[serde(default)]
60-
pub hash_rewire_activation_height: Option<u64>,
98+
pub canonical_hash_activation_height: Option<u64>,
6199
}
62100

63101
impl EvolvePayloadBuilderConfig {
@@ -70,7 +108,7 @@ impl EvolvePayloadBuilderConfig {
70108
mint_precompile_activation_height: None,
71109
contract_size_limit: None,
72110
contract_size_limit_activation_height: None,
73-
hash_rewire_activation_height: None,
111+
canonical_hash_activation_height: None,
74112
}
75113
}
76114

@@ -105,7 +143,7 @@ impl EvolvePayloadBuilderConfig {
105143
config.contract_size_limit = extras.contract_size_limit;
106144
config.contract_size_limit_activation_height =
107145
extras.contract_size_limit_activation_height;
108-
config.hash_rewire_activation_height = extras.hash_rewire_activation_height;
146+
config.canonical_hash_activation_height = extras.canonical_hash_activation_height;
109147
}
110148
Ok(config)
111149
}
@@ -160,14 +198,27 @@ impl EvolvePayloadBuilderConfig {
160198
.and_then(|(sink, activation)| (block_number >= activation).then_some(sink))
161199
}
162200

163-
/// Returns the configured hash rewire activation height if present.
164-
pub const fn hash_rewire_settings(&self) -> Option<u64> {
165-
self.hash_rewire_activation_height
166-
}
167-
168-
/// Returns true if the canonical hash rewiring should be active for the provided block.
169-
pub const fn is_hash_rewire_active_for_block(&self, block_number: u64) -> bool {
170-
matches!(self.hash_rewire_activation_height, Some(activation) if block_number >= activation)
201+
/// Returns true if canonical block hash validation should be enforced for the given block.
202+
///
203+
/// This method controls whether the validator should reject blocks with hash mismatches
204+
/// or bypass the check for legacy compatibility.
205+
///
206+
/// # Returns
207+
///
208+
/// - `true`: Enforce canonical hash validation (reject mismatches)
209+
/// - `false`: Bypass hash validation (legacy mode for historical blocks)
210+
///
211+
/// # Logic
212+
///
213+
/// - If `canonical_hash_activation_height` is `None`: Always enforce (new networks)
214+
/// - If `canonical_hash_activation_height` is `Some(N)`:
215+
/// - `block_number < N`: Don't enforce (legacy mode)
216+
/// - `block_number >= N`: Enforce (canonical mode)
217+
pub const fn is_canonical_hash_enforced(&self, block_number: u64) -> bool {
218+
match self.canonical_hash_activation_height {
219+
Some(activation) => block_number >= activation,
220+
None => true, // Default: enforce canonical hashes from genesis
221+
}
171222
}
172223
}
173224

@@ -247,8 +298,7 @@ mod tests {
247298
"baseFeeSink": sink,
248299
"baseFeeRedirectActivationHeight": 42,
249300
"mintAdmin": admin,
250-
"mintPrecompileActivationHeight": 64,
251-
"hashRewireActivationHeight": 128
301+
"mintPrecompileActivationHeight": 64
252302
});
253303

254304
let chainspec = create_test_chainspec_with_extras(Some(extras));
@@ -258,7 +308,6 @@ mod tests {
258308
assert_eq!(config.base_fee_redirect_activation_height, Some(42));
259309
assert_eq!(config.mint_admin, Some(admin));
260310
assert_eq!(config.mint_precompile_activation_height, Some(64));
261-
assert_eq!(config.hash_rewire_activation_height, Some(128));
262311
}
263312

264313
#[test]
@@ -284,7 +333,6 @@ mod tests {
284333

285334
assert_eq!(config.base_fee_sink, None);
286335
assert_eq!(config.base_fee_redirect_activation_height, None);
287-
assert_eq!(config.hash_rewire_activation_height, None);
288336
}
289337

290338
#[test]
@@ -297,7 +345,6 @@ mod tests {
297345
assert_eq!(config.mint_admin, None);
298346
assert_eq!(config.base_fee_redirect_activation_height, None);
299347
assert_eq!(config.mint_precompile_activation_height, None);
300-
assert_eq!(config.hash_rewire_activation_height, None);
301348
}
302349

303350
#[test]
@@ -336,7 +383,6 @@ mod tests {
336383
assert_eq!(config.mint_admin, None);
337384
assert_eq!(config.base_fee_redirect_activation_height, None);
338385
assert_eq!(config.mint_precompile_activation_height, None);
339-
assert_eq!(config.hash_rewire_activation_height, None);
340386
}
341387

342388
#[test]
@@ -348,7 +394,6 @@ mod tests {
348394
assert_eq!(config.base_fee_redirect_activation_height, None);
349395
assert_eq!(config.mint_precompile_activation_height, None);
350396
assert_eq!(config.contract_size_limit, None);
351-
assert_eq!(config.hash_rewire_activation_height, None);
352397
}
353398

354399
#[test]
@@ -364,7 +409,7 @@ mod tests {
364409
mint_precompile_activation_height: Some(0),
365410
contract_size_limit: None,
366411
contract_size_limit_activation_height: None,
367-
hash_rewire_activation_height: None,
412+
canonical_hash_activation_height: None,
368413
};
369414
assert!(config_with_sink.validate().is_ok());
370415
}
@@ -379,7 +424,7 @@ mod tests {
379424
mint_precompile_activation_height: None,
380425
contract_size_limit: None,
381426
contract_size_limit_activation_height: None,
382-
hash_rewire_activation_height: None,
427+
canonical_hash_activation_height: None,
383428
};
384429

385430
assert_eq!(config.base_fee_sink_for_block(4), None);
@@ -407,13 +452,11 @@ mod tests {
407452
config.mint_admin,
408453
Some(address!("00000000000000000000000000000000000000aa"))
409454
);
410-
assert_eq!(config.hash_rewire_activation_height, None);
411455

412456
let json_without_sink = json!({});
413457
let config: ChainspecEvolveConfig = serde_json::from_value(json_without_sink).unwrap();
414458
assert_eq!(config.base_fee_sink, None);
415459
assert_eq!(config.mint_admin, None);
416-
assert_eq!(config.hash_rewire_activation_height, None);
417460
}
418461

419462
#[test]
@@ -513,16 +556,28 @@ mod tests {
513556
}
514557

515558
#[test]
516-
fn test_hash_rewire_activation_height_parsed() {
559+
fn test_canonical_hash_activation_from_chainspec() {
517560
let extras = json!({
518-
"hashRewireActivationHeight": 500
561+
"canonicalHashActivationHeight": 500
519562
});
520563

521564
let chainspec = create_test_chainspec_with_extras(Some(extras));
522565
let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap();
523566

524-
assert_eq!(config.hash_rewire_activation_height, Some(500));
525-
assert!(config.is_hash_rewire_active_for_block(600));
526-
assert!(!config.is_hash_rewire_active_for_block(400));
567+
assert_eq!(config.canonical_hash_activation_height, Some(500));
568+
// Before activation: legacy mode (not enforced)
569+
assert!(!config.is_canonical_hash_enforced(499));
570+
// At and after activation: canonical mode (enforced)
571+
assert!(config.is_canonical_hash_enforced(500));
572+
assert!(config.is_canonical_hash_enforced(600));
573+
}
574+
575+
#[test]
576+
fn test_canonical_hash_default_enforces_from_genesis() {
577+
// When not configured, canonical hashes should be enforced from genesis
578+
let config = EvolvePayloadBuilderConfig::new();
579+
assert_eq!(config.canonical_hash_activation_height, None);
580+
assert!(config.is_canonical_hash_enforced(0));
581+
assert!(config.is_canonical_hash_enforced(1000));
527582
}
528583
}

crates/node/src/validator.rs

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ use crate::{
2525
};
2626

2727
/// Evolve engine validator that handles custom payload validation.
28+
///
29+
/// This validator extends the standard Ethereum payload validation with support for
30+
/// legacy block hash compatibility. See [`EvolvePayloadBuilderConfig::canonical_hash_activation_height`]
31+
/// for details on the migration strategy.
32+
///
33+
/// # Block Hash Validation
34+
///
35+
/// Early versions of ev-node passed block hashes from height H-1 instead of H,
36+
/// causing block explorers (e.g., Blockscout) to show all blocks as forks. This
37+
/// validator can bypass hash mismatch errors for historical blocks while enforcing
38+
/// canonical validation for new blocks, controlled by `canonical_hash_activation_height`.
2839
#[derive(Debug, Clone)]
2940
pub struct EvolveEngineValidator {
3041
inner: EthereumExecutionPayloadValidator<ChainSpec>,
@@ -56,7 +67,6 @@ impl PayloadValidator<EvolveEngineTypes> for EvolveEngineValidator {
5667
) -> Result<RecoveredBlock<Self::Block>, NewPayloadError> {
5768
info!("Evolve engine validator: validating payload");
5869

59-
// Use inner validator but with custom evolve handling.
6070
match self.inner.ensure_well_formed_payload(payload.clone()) {
6171
Ok(sealed_block) => {
6272
info!("Evolve engine validator: payload validation succeeded");
@@ -65,29 +75,44 @@ impl PayloadValidator<EvolveEngineTypes> for EvolveEngineValidator {
6575
.map_err(|e| NewPayloadError::Other(e.into()))
6676
}
6777
Err(err) => {
68-
// Log the error for debugging.
6978
tracing::debug!("Evolve payload validation error: {:?}", err);
7079

71-
// Check if this is a block hash mismatch error - bypass it for evolve.
80+
// Handle block hash mismatch errors specially for legacy compatibility.
81+
//
82+
// Background: Early versions of ev-node passed block hashes from height H-1
83+
// instead of H when communicating with ev-reth via the Engine API. This caused
84+
// block hashes to not match the canonical Ethereum block hash (keccak256 of
85+
// RLP-encoded header), resulting in block explorers like Blockscout incorrectly
86+
// displaying every block as a fork due to parent hash mismatches.
87+
//
88+
// For existing networks with historical blocks containing these non-canonical
89+
// hashes, we need to bypass this validation to allow nodes to sync from genesis.
90+
// The `canonical_hash_activation_height` config controls when to start enforcing
91+
// canonical hashes for new blocks.
7292
if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) {
7393
let block_number = payload.payload.block_number();
74-
if self.config.is_hash_rewire_active_for_block(block_number) {
94+
95+
// If canonical hash enforcement is active for this block, reject the mismatch
96+
if self.config.is_canonical_hash_enforced(block_number) {
7597
tracing::warn!(
7698
block_number,
77-
"canonical hash rewiring active; rejecting mismatched block hash"
99+
"canonical hash enforcement active; rejecting mismatched block hash"
78100
);
79101
return Err(NewPayloadError::Eth(err));
80102
}
81103

82-
info!("Evolve engine validator: bypassing block hash mismatch for ev-reth");
83-
// For evolve, we trust the payload builder - just parse the block without hash validation.
104+
// Legacy mode: bypass hash mismatch to allow syncing historical blocks.
105+
// Re-seal the block with the correct canonical hash (keccak256 of header).
106+
info!(
107+
block_number,
108+
"bypassing block hash mismatch (legacy mode before activation height)"
109+
);
84110
let ExecutionData { payload, sidecar } = payload;
85111
let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow();
86112
sealed_block
87113
.try_recover()
88114
.map_err(|e| NewPayloadError::Other(e.into()))
89115
} else {
90-
// For other errors, re-throw them.
91116
Err(NewPayloadError::Eth(err))
92117
}
93118
}
@@ -168,10 +193,10 @@ mod tests {
168193
use reth_chainspec::ChainSpecBuilder;
169194
use reth_primitives::{Block, SealedBlock};
170195

171-
fn validator_with_activation(height: Option<u64>) -> EvolveEngineValidator {
196+
fn validator_with_activation(activation_height: Option<u64>) -> EvolveEngineValidator {
172197
let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build());
173198
let mut config = EvolvePayloadBuilderConfig::new();
174-
config.hash_rewire_activation_height = height;
199+
config.canonical_hash_activation_height = activation_height;
175200
EvolveEngineValidator::new(chain_spec, config)
176201
}
177202

@@ -185,17 +210,19 @@ mod tests {
185210
}
186211

187212
#[test]
188-
fn legacy_bypass_allows_mismatch_before_activation() {
189-
let validator = validator_with_activation(None);
213+
fn test_legacy_mode_bypasses_hash_mismatch() {
214+
// When activation height is set in the future, legacy mode should bypass hash mismatches
215+
let validator = validator_with_activation(Some(1000));
190216
let payload = mismatched_payload();
191217

192218
validator
193219
.ensure_well_formed_payload(payload)
194-
.expect("hash mismatch should be bypassed before activation");
220+
.expect("hash mismatch should be bypassed in legacy mode");
195221
}
196222

197223
#[test]
198-
fn canonical_mode_rejects_mismatch_after_activation() {
224+
fn test_canonical_mode_rejects_hash_mismatch() {
225+
// When activation height is 0 (or in the past), canonical mode should reject mismatches
199226
let validator = validator_with_activation(Some(0));
200227
let payload = mismatched_payload();
201228

@@ -207,4 +234,19 @@ mod tests {
207234
))
208235
));
209236
}
237+
238+
#[test]
239+
fn test_default_enforces_canonical_hash() {
240+
// When no activation height is set, canonical validation should be enforced (default)
241+
let validator = validator_with_activation(None);
242+
let payload = mismatched_payload();
243+
244+
let result = validator.ensure_well_formed_payload(payload);
245+
assert!(matches!(
246+
result,
247+
Err(NewPayloadError::Eth(
248+
alloy_rpc_types::engine::PayloadError::BlockHash { .. }
249+
))
250+
));
251+
}
210252
}

0 commit comments

Comments
 (0)