Skip to content

Commit 39b977c

Browse files
committed
use the correct hash
1 parent 4c1b64e commit 39b977c

File tree

5 files changed

+184
-5
lines changed

5 files changed

+184
-5
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,24 @@ This project builds upon the excellent work of:
456456

457457
- [Reth](https://github.com/paradigmxyz/reth) - The Rust Ethereum client
458458
- [Evolve](https://ev.xyz/) - The modular rollup framework
459+
460+
### Canonical Block Hash Activation
461+
462+
Legacy deployments allowed application-level hashes to flow through the Engine API, which meant
463+
Reth had to ignore block-hash mismatches and upstream tooling flagged every block as a fork. Newer
464+
networks can opt-in to canonical keccak block hashes by setting `hashRewireActivationHeight` inside
465+
the `evolve` section of the chainspec:
466+
467+
```json
468+
"config": {
469+
...,
470+
"evolve": {
471+
"hashRewireActivationHeight": 0
472+
}
473+
}
474+
```
475+
476+
Set the activation height to the first block where canonical hashes should be enforced (use `0` for
477+
fresh networks). Before the activation height the node continues to bypass hash mismatches so
478+
existing chains keep working; after activation, the node rejects malformed payloads and the reported
479+
block hash always matches the standard Engine API expectations.

crates/node/src/builder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,13 @@ where
187187
.finish(&state_provider)
188188
.map_err(PayloadBuilderError::other)?;
189189

190-
let sealed_block = block.sealed_block().clone();
190+
let mut sealed_block = block.sealed_block().clone();
191+
192+
if !self.config.is_hash_rewire_active_for_block(block_number) {
193+
let legacy_hash = sealed_block.header().state_root;
194+
let legacy_block = sealed_block.clone_block();
195+
sealed_block = SealedBlock::new_unchecked(legacy_block, legacy_hash);
196+
}
191197

192198
tracing::info!(
193199
block_number = sealed_block.number,

crates/node/src/config.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ 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>,
2427
}
2528

2629
/// Configuration for the Evolve payload builder
@@ -44,6 +47,9 @@ pub struct EvolvePayloadBuilderConfig {
4447
/// Block height at which the custom contract size limit activates.
4548
#[serde(default)]
4649
pub contract_size_limit_activation_height: Option<u64>,
50+
/// Block height at which canonical hash rewiring activates.
51+
#[serde(default)]
52+
pub hash_rewire_activation_height: Option<u64>,
4753
}
4854

4955
impl EvolvePayloadBuilderConfig {
@@ -56,6 +62,7 @@ impl EvolvePayloadBuilderConfig {
5662
mint_precompile_activation_height: None,
5763
contract_size_limit: None,
5864
contract_size_limit_activation_height: None,
65+
hash_rewire_activation_height: None,
5966
}
6067
}
6168

@@ -90,6 +97,7 @@ impl EvolvePayloadBuilderConfig {
9097
config.contract_size_limit = extras.contract_size_limit;
9198
config.contract_size_limit_activation_height =
9299
extras.contract_size_limit_activation_height;
100+
config.hash_rewire_activation_height = extras.hash_rewire_activation_height;
93101
}
94102
Ok(config)
95103
}
@@ -143,6 +151,16 @@ impl EvolvePayloadBuilderConfig {
143151
self.base_fee_redirect_settings()
144152
.and_then(|(sink, activation)| (block_number >= activation).then_some(sink))
145153
}
154+
155+
/// Returns the configured hash rewire activation height if present.
156+
pub const fn hash_rewire_settings(&self) -> Option<u64> {
157+
self.hash_rewire_activation_height
158+
}
159+
160+
/// Returns true if the canonical hash rewiring should be active for the provided block.
161+
pub const fn is_hash_rewire_active_for_block(&self, block_number: u64) -> bool {
162+
matches!(self.hash_rewire_activation_height, Some(activation) if block_number >= activation)
163+
}
146164
}
147165

148166
/// Errors that can occur during configuration validation
@@ -221,7 +239,8 @@ mod tests {
221239
"baseFeeSink": sink,
222240
"baseFeeRedirectActivationHeight": 42,
223241
"mintAdmin": admin,
224-
"mintPrecompileActivationHeight": 64
242+
"mintPrecompileActivationHeight": 64,
243+
"hashRewireActivationHeight": 128
225244
});
226245

227246
let chainspec = create_test_chainspec_with_extras(Some(extras));
@@ -231,6 +250,7 @@ mod tests {
231250
assert_eq!(config.base_fee_redirect_activation_height, Some(42));
232251
assert_eq!(config.mint_admin, Some(admin));
233252
assert_eq!(config.mint_precompile_activation_height, Some(64));
253+
assert_eq!(config.hash_rewire_activation_height, Some(128));
234254
}
235255

236256
#[test]
@@ -256,6 +276,7 @@ mod tests {
256276

257277
assert_eq!(config.base_fee_sink, None);
258278
assert_eq!(config.base_fee_redirect_activation_height, None);
279+
assert_eq!(config.hash_rewire_activation_height, None);
259280
}
260281

261282
#[test]
@@ -268,6 +289,7 @@ mod tests {
268289
assert_eq!(config.mint_admin, None);
269290
assert_eq!(config.base_fee_redirect_activation_height, None);
270291
assert_eq!(config.mint_precompile_activation_height, None);
292+
assert_eq!(config.hash_rewire_activation_height, None);
271293
}
272294

273295
#[test]
@@ -306,6 +328,7 @@ mod tests {
306328
assert_eq!(config.mint_admin, None);
307329
assert_eq!(config.base_fee_redirect_activation_height, None);
308330
assert_eq!(config.mint_precompile_activation_height, None);
331+
assert_eq!(config.hash_rewire_activation_height, None);
309332
}
310333

311334
#[test]
@@ -317,6 +340,7 @@ mod tests {
317340
assert_eq!(config.base_fee_redirect_activation_height, None);
318341
assert_eq!(config.mint_precompile_activation_height, None);
319342
assert_eq!(config.contract_size_limit, None);
343+
assert_eq!(config.hash_rewire_activation_height, None);
320344
}
321345

322346
#[test]
@@ -332,6 +356,7 @@ mod tests {
332356
mint_precompile_activation_height: Some(0),
333357
contract_size_limit: None,
334358
contract_size_limit_activation_height: None,
359+
hash_rewire_activation_height: None,
335360
};
336361
assert!(config_with_sink.validate().is_ok());
337362
}
@@ -346,6 +371,7 @@ mod tests {
346371
mint_precompile_activation_height: None,
347372
contract_size_limit: None,
348373
contract_size_limit_activation_height: None,
374+
hash_rewire_activation_height: None,
349375
};
350376

351377
assert_eq!(config.base_fee_sink_for_block(4), None);
@@ -373,11 +399,13 @@ mod tests {
373399
config.mint_admin,
374400
Some(address!("00000000000000000000000000000000000000aa"))
375401
);
402+
assert_eq!(config.hash_rewire_activation_height, None);
376403

377404
let json_without_sink = json!({});
378405
let config: ChainspecEvolveConfig = serde_json::from_value(json_without_sink).unwrap();
379406
assert_eq!(config.base_fee_sink, None);
380407
assert_eq!(config.mint_admin, None);
408+
assert_eq!(config.hash_rewire_activation_height, None);
381409
}
382410

383411
#[test]
@@ -475,4 +503,18 @@ mod tests {
475503
DEFAULT_CONTRACT_SIZE_LIMIT
476504
);
477505
}
506+
507+
#[test]
508+
fn test_hash_rewire_activation_height_parsed() {
509+
let extras = json!({
510+
"hashRewireActivationHeight": 500
511+
});
512+
513+
let chainspec = create_test_chainspec_with_extras(Some(extras));
514+
let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap();
515+
516+
assert_eq!(config.hash_rewire_activation_height, Some(500));
517+
assert!(config.is_hash_rewire_active_for_block(600));
518+
assert!(!config.is_hash_rewire_active_for_block(400));
519+
}
478520
}

crates/node/src/validator.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,24 @@ use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator;
1919
use reth_primitives_traits::{Block as _, RecoveredBlock};
2020
use tracing::info;
2121

22-
use crate::{attributes::EvolveEnginePayloadAttributes, node::EvolveEngineTypes};
22+
use crate::{
23+
attributes::EvolveEnginePayloadAttributes, config::EvolvePayloadBuilderConfig,
24+
node::EvolveEngineTypes,
25+
};
2326

2427
/// Evolve engine validator that handles custom payload validation.
2528
#[derive(Debug, Clone)]
2629
pub struct EvolveEngineValidator {
2730
inner: EthereumExecutionPayloadValidator<ChainSpec>,
31+
config: EvolvePayloadBuilderConfig,
2832
}
2933

3034
impl EvolveEngineValidator {
3135
/// Instantiates a new validator.
32-
pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
36+
pub const fn new(chain_spec: Arc<ChainSpec>, config: EvolvePayloadBuilderConfig) -> Self {
3337
Self {
3438
inner: EthereumExecutionPayloadValidator::new(chain_spec),
39+
config,
3540
}
3641
}
3742

@@ -65,6 +70,15 @@ impl PayloadValidator<EvolveEngineTypes> for EvolveEngineValidator {
6570

6671
// Check if this is a block hash mismatch error - bypass it for evolve.
6772
if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) {
73+
let block_number = payload.payload.block_number();
74+
if self.config.is_hash_rewire_active_for_block(block_number) {
75+
tracing::warn!(
76+
block_number,
77+
"canonical hash rewiring active; rejecting mismatched block hash"
78+
);
79+
return Err(NewPayloadError::Eth(err));
80+
}
81+
6882
info!("Evolve engine validator: bypassing block hash mismatch for ev-reth");
6983
// For evolve, we trust the payload builder - just parse the block without hash validation.
7084
let ExecutionData { payload, sidecar } = payload;
@@ -142,6 +156,55 @@ where
142156
type Validator = EvolveEngineValidator;
143157

144158
async fn build(self, ctx: &AddOnsContext<'_, N>) -> eyre::Result<Self::Validator> {
145-
Ok(EvolveEngineValidator::new(ctx.config.chain.clone()))
159+
let config = EvolvePayloadBuilderConfig::from_chain_spec(ctx.config.chain.as_ref())?;
160+
Ok(EvolveEngineValidator::new(ctx.config.chain.clone(), config))
161+
}
162+
}
163+
164+
#[cfg(test)]
165+
mod tests {
166+
use super::*;
167+
use alloy_primitives::B256;
168+
use reth_chainspec::ChainSpecBuilder;
169+
use reth_primitives::{Block, SealedBlock};
170+
171+
fn validator_with_activation(height: Option<u64>) -> EvolveEngineValidator {
172+
let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build());
173+
let mut config = EvolvePayloadBuilderConfig::new();
174+
config.hash_rewire_activation_height = height;
175+
EvolveEngineValidator::new(chain_spec, config)
176+
}
177+
178+
fn mismatched_payload() -> ExecutionData {
179+
let sealed_block: SealedBlock<Block> = SealedBlock::default();
180+
let block_hash = sealed_block.hash();
181+
let block = sealed_block.into_block();
182+
let mut data = ExecutionData::from_block_unchecked(block_hash, &block);
183+
data.payload.as_v1_mut().block_hash = B256::repeat_byte(0x42);
184+
data
185+
}
186+
187+
#[test]
188+
fn legacy_bypass_allows_mismatch_before_activation() {
189+
let validator = validator_with_activation(None);
190+
let payload = mismatched_payload();
191+
192+
validator
193+
.ensure_well_formed_payload(payload)
194+
.expect("hash mismatch should be bypassed before activation");
195+
}
196+
197+
#[test]
198+
fn canonical_mode_rejects_mismatch_after_activation() {
199+
let validator = validator_with_activation(Some(0));
200+
let payload = mismatched_payload();
201+
202+
let result = validator.ensure_well_formed_payload(payload);
203+
assert!(matches!(
204+
result,
205+
Err(NewPayloadError::Eth(
206+
alloy_rpc_types::engine::PayloadError::BlockHash { .. }
207+
))
208+
));
146209
}
147210
}

docs/canonical-hash-plan.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Plan: Restore Canonical Block Hashes While Preserving Rollkit Apphash
2+
3+
## Goals
4+
5+
- Emit keccak-based canonical hashes inside every header so upstream Reth tooling no longer reports continuous forks.
6+
- Preserve the Rollkit `apphash` in a discoverable location so DA clients continue to verify block linkage.
7+
- Gate the change behind a chainspec activation height for deterministic rollouts.
8+
9+
## 1. Payload Builder & Types
10+
11+
- 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.
12+
- 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.
13+
- Persist the `apphash` to a new header field (candidate: encode in `extra_data` or define a dedicated header extension struct shared across payload serialization).
14+
- 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.
15+
16+
## 2. Validator & Consensus
17+
18+
- Modify `EvolveEngineValidator` (`crates/node/src/validator.rs`) to stop bypassing `PayloadError::BlockHashMismatch` after the activation height. Retain the bypass for legacy blocks.
19+
- Ensure `EvolveConsensus` (`crates/evolve/src/consensus.rs`) now validates hash/parentHash linkages post-activation while keeping the relaxed timestamp rule.
20+
- Audit any code paths that rely on the `apphash` (e.g., Rollkit-specific checks) and point them to the relocated field.
21+
22+
## 3. RPC & Serialization
23+
24+
- 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.
25+
- Clearly document the new field semantics so explorers/light clients know where to read the Rollkit hash.
26+
- Maintain backward compatibility in request parsing: continue accepting payload attributes that include the `apphash`, even though it is no longer stored in `header.hash`.
27+
28+
## 4. Chainspec & Configuration
29+
30+
- Add a new evolve config flag, e.g. `hashRewireActivationHeight`, parsed in `crates/evolve/src/config.rs` and surfaced through `EvolvePayloadBuilderConfig`.
31+
- Validate the flag alongside existing evolve settings; log or reject invalid configurations.
32+
- Update sample configs (`etc/ev-reth-genesis.json`) and the README/upgrade docs with instructions for setting the activation height.
33+
34+
## 5. Testing & Tooling
35+
36+
- Extend e2e tests (`crates/tests/src/e2e_tests.rs`, `test_evolve_engine_api.rs`) to cover both pre- and post-activation behavior, asserting that:
37+
- Legacy mode still bypasses hash mismatches.
38+
- Post-activation blocks produce canonical parent links and expose the `apphash` in the new field.
39+
- Add unit tests around the serialization helpers to ensure RPC payloads echo both hashes correctly.
40+
- Verify Rollkit integration tests continue to pass once they read the `apphash` from the new location.
41+
42+
## 6. Rollout Steps
43+
44+
1. Implement the code changes behind the activation height flag and land them with comprehensive tests.
45+
2. Publish an upgrade note describing the new flag, how to set the activation block, and how to verify the behavior via RPC.
46+
3. Coordinate with testnet/mainnet operators to schedule the activation block and ensure explorers/monitoring tools understand the relocated `apphash` field.
47+
4. After activation, monitor forkchoice health and Rollkit ingestion to confirm both canonical and DA workflows function correctly.

0 commit comments

Comments
 (0)