Skip to content

fix(evm): map Constantinople chains to Petersburg spec#26362

Draft
0xalpharush wants to merge 2 commits into
paradigmxyz:codex/excise-revmfrom
0xalpharush:codex/fix-constantinople-spec-map
Draft

fix(evm): map Constantinople chains to Petersburg spec#26362
0xalpharush wants to merge 2 commits into
paradigmxyz:codex/excise-revmfrom
0xalpharush:codex/fix-constantinople-spec-map

Conversation

@0xalpharush

Copy link
Copy Markdown
Contributor

Summary

Map Constantinople-active chains to SpecId::PETERSBURG in spec_id_by_timestamp_and_block_number.

evm2 does not expose a standalone Constantinople execution spec. Its own CLI and EEST paths already treat Constantinople / ConstantinopleFix as Petersburg, so Reth's evm2 fork mapping should do the same instead of falling through to Byzantium.

Why

This was found by differential fuzzing against fork-boundary state tests. The fuzzer can generate a chain where Constantinople is active but Petersburg is not yet active. Byzantium is still active in that case because it is an earlier fork.

On PR #25002, that case falls through to SpecId::BYZANTIUM:

} else if chain_spec.is_istanbul_active_at_block(block_number) {
SpecId::ISTANBUL
} else if chain_spec.is_petersburg_active_at_block(block_number) {
SpecId::PETERSBURG
} else if chain_spec.is_byzantium_active_at_block(block_number) {
SpecId::BYZANTIUM

The executor then passes that spec_id into post-block reward calculation:

let spec_id = evm.spec_id();
let block = *evm.block_env();
let block_number = block.number.to::<u64>();
let block_beneficiary = block.beneficiary;
Self {
evm,
spec_id,

post_block_balance_state_changes(
&mut self.evm,
&mut self.block_state,
self.hashed_state_mode.stream(),
&mut |state| emit_hashed_state(&mut self.hashed_state_update_hook, state),
self.spec_id,
self.block_number,

base_block_reward(SpecId::BYZANTIUM) then takes the Byzantium reward branch and credits 3 ETH:

const fn base_block_reward(spec_id: SpecId) -> Option<u128> {
if spec_id.enables(SpecId::MERGE) {
None
} else if spec_id.enables(SpecId::PETERSBURG) {
Some(ETH_TO_WEI * 2)
} else if spec_id.enables(SpecId::BYZANTIUM) {
Some(ETH_TO_WEI * 3)
} else {

The reth-main/revm path delegates block reward calculation through alloy-evm:

pub use alloy_evm::{
spec as revm_spec,
spec_by_timestamp_and_block_number as revm_spec_by_timestamp_and_block_number,
};

alloy-evm treats Constantinople as the 2 ETH reward era:

https://github.com/alloy-rs/evm/blob/6e56ca0ae11090d9575008583b4c4a3a469d7428/crates/evm/src/block/calc.rs#L37-L44

Observed fuzzing effect: receipts and sender state match, but the beneficiary balance differs by exactly 1 ETH, causing a state-root divergence. Main credits 2 ETH + fees; evm2 credits 3 ETH + fees.

Alternative Considered

An alternative fix was to keep the EVM spec mapped to Byzantium and use the alloy-evm block reward helper at the reward site.

That would fix the observed reward mismatch, but it would leave the broader fork mapping inconsistent with evm2's own Constantinople / ConstantinopleFix behavior and would add reward-specific plumbing. Mapping Constantinople to Petersburg fixes the mismatch at the spec-selection boundary instead.

@github-project-automation github-project-automation Bot moved this to Backlog in Reth Tracker Jul 10, 2026
@0xalpharush 0xalpharush marked this pull request as draft July 10, 2026 19:10
@0xalpharush

Copy link
Copy Markdown
Contributor Author

But this results in gas differences...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant