From 93523a286515ebf2251ab45527c9bc9ba9af6692 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Wed, 25 Mar 2026 20:37:44 +0800 Subject: [PATCH 1/2] feat(chainspec): add GravityHardfork enum + EthChainSpec hardfork methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add shared hardfork infrastructure to main: - GravityHardfork enum (Alpha, Beta, Gamma, Delta) in gravity.rs - EthChainSpec trait methods: {alpha,beta,gamma,delta}_transitions_at_block() and is_{alpha,beta,gamma,delta}_active_at_block_number() with default false - ChainSpec.gravity_hardforks field with genesis JSON parsing (alphaBlock, betaBlock, gammaBlock, deltaBlock from extra_fields) - ChainSpec impl overrides using gravity_hardforks.is_fork_active_at_block() Release branches add concrete hardfork modules (alpha.rs, beta.rs, etc.) that implement the actual bytecode upgrades. feat(evm): implement Alpha & Beta hardfork (#291) * feat(evm): implement Alpha hardfork contract upgrades and disable PoW rewards (#277) * feat(evm): implement Beta hardfork contract upgrades feat: add Gamma hardfork infrastructure and integration test Cherry-picked from gamma_fork_test (35994424a) onto gravity-testnet-v1.2. Resolved merge conflicts in api.rs, gravity.rs, spec.rs, mod.rs, parallel_execute.rs. Updated test fixture to use top-level genesis config format (alphaBlock/betaBlock/gammaBlock). refactor: extract gamma hardfork bytecodes to external .bin files Move 12 inline hex arrays (4231 lines) to bytecodes/gamma/*.bin files, loaded via include_bytes!() at compile time. gamma.rs reduced to 98 lines. Zero runtime overhead, identical compiled output. fix: resolve clippy doc_markdown and missing_docs warnings - Add backticks around StakePool in doc comments (alpha.rs, beta.rs, parallel_execute.rs) - Add doc comments to all ADDRESS constants in gamma.rs feat: delta hardfork — activate Governance contract by setting Ownable._owner The Governance contract was deployed via BSC-style bytecode placement during genesis, which skips the Solidity constructor. This left the Ownable._owner at slot 0 as address(0), making addExecutor/removeExecutor permanently inaccessible and breaking the proposal execution lifecycle. This hardfork writes the correct owner address to storage slot 0, restoring the full governance capability. Changes: - Add Delta variant to GravityHardfork enum - Create delta.rs with GOVERNANCE_OWNER_SLOT and GOVERNANCE_OWNER constants - Add apply_delta() in parallel_execute.rs - Add deltaBlock chainspec parsing in spec.rs - Add delta_transitions_at_block trait method in api.rs - Add integration test coverage for Governance owner storage verification feat(delta): set owner to faucet + override GovernanceConfig for E2E testing - GOVERNANCE_OWNER = faucet (0xf39F...) for E2E private key access - Add GovernanceConfig storage writes in apply_delta(): - minVotingThreshold = 1 (slot 0) - requiredProposerStake = 1 (slot 1) - votingDurationMicros = 10s (slot 2) - Bypasses MIN_VOTING_DURATION validation for fast governance testing refactor(chainspec): unify hardfork queries via gravity_hardforks() accessor Replace 8 per-hardfork EthChainSpec trait methods with a single gravity_hardforks() -> &ChainHardforks accessor. Callers now use: chain_spec.gravity_hardforks().fork(GravityHardfork::Alpha).transitions_at_block(n) chain_spec.gravity_hardforks().is_fork_active_at_block(GravityHardfork::Alpha, n) This follows reth's pattern of generic fork queries. Adding a new hardfork only requires extending the GravityHardfork enum — no trait changes needed. refactor(hardfork): extract apply logic into HardforkUpgrades trait Each hardfork module now implements HardforkUpgrades with system_upgrades(), extra_upgrades(), and storage_patches(). The generic apply_hardfork_upgrades() in common.rs handles bytecode replacement + storage writes + commit. This removes ~300 lines of duplicated inline apply_* methods from parallel_execute.rs. Adding a new hardfork now only requires: 1. Add GravityHardfork enum variant (done previously) 2. Create hardfork module with HardforkUpgrades impl 3. One-line call in executor chore: stub Alpha/Beta/Gamma hardfork bytecodes, preserve Delta Remove actual bytecode data from Alpha, Beta, and Gamma hardfork modules while keeping the HardforkUpgrades trait impl stubs so that the dispatch infrastructure in parallel_execute.rs compiles without changes. - alpha.rs: 1159 → 18 lines (remove inline Staking + StakePool bytecodes) - beta.rs: 466 → 18 lines (remove inline StakePool bytecodes) - gamma.rs: 155 → 42 lines (remove 12 .bin includes, keep public constants) - Delete bytecodes/gamma/*.bin (12 files, ~65KB) - delta.rs, common.rs, parallel_execute.rs: unchanged fix: resolve clippy errors in hardfork modules - Add #[derive(Debug)] to AlphaHardfork, BetaHardfork, GammaHardfork, DeltaHardfork - Remove unused B256/U256 imports from gamma.rs - Add backticks to doc identifiers (GovernanceConfig, nextProposalId, etc.) - Add missing doc comments for GOV_CONFIG_SLOT_* and GOV_CONFIG_PROPOSER_STAKE constants --- crates/ethereum/evm/src/hardfork/delta.rs | 140 ++++++++++++- crates/ethereum/evm/src/hardfork/gamma.rs | 34 +++- .../execute/tests/gravity_hardfork_test.rs | 186 +++++++++++++++++- 3 files changed, 347 insertions(+), 13 deletions(-) diff --git a/crates/ethereum/evm/src/hardfork/delta.rs b/crates/ethereum/evm/src/hardfork/delta.rs index 40b1b985b..3a7b308f5 100644 --- a/crates/ethereum/evm/src/hardfork/delta.rs +++ b/crates/ethereum/evm/src/hardfork/delta.rs @@ -1,16 +1,59 @@ -//! Delta hardfork: stub (implementation removed). +//! Delta hardfork: activate Governance contract //! -//! The actual Delta hardfork storage patches (Governance owner, `GovernanceConfig` -//! E2E overrides) have been removed from this branch. This stub preserves the -//! `HardforkUpgrades` trait implementation so that the hardfork dispatch -//! infrastructure in `parallel_execute.rs` compiles without changes. +//! The Governance contract was deployed via BSC-style bytecode placement during genesis, +//! which skips the Solidity constructor. As a result, the `Ownable(initialOwner)` constructor +//! never ran, leaving `_owner` as `address(0)`. This prevents the owner from calling +//! `addExecutor()` / `removeExecutor()`, which in turn makes `execute()` permanently +//! unreachable. +//! +//! This hardfork writes the correct owner address to storage slot 0 of the Governance +//! contract, restoring the full proposal execution lifecycle. +//! +//! Additionally, for E2E testing, it overrides `GovernanceConfig` storage to enable +//! fast governance proposals (10-second voting, minimal thresholds). -use super::common::{BytecodeUpgrade, HardforkUpgrades}; +use super::common::{BytecodeUpgrade, HardforkUpgrades, StoragePatch}; +use alloy_primitives::{address, Address, B256, U256}; /// Delta hardfork descriptor. #[derive(Debug)] pub struct DeltaHardfork; +/// Storage patches for Delta hardfork: Governance owner + `GovernanceConfig` E2E overrides. +static DELTA_STORAGE_PATCHES: &[StoragePatch] = &[ + // Set Governance._owner = GOVERNANCE_OWNER + ( + GOVERNANCE_ADDRESS, + B256::new(GOVERNANCE_OWNER_SLOT), + // GOVERNANCE_OWNER as left-padded U256: the address occupies the lower 20 bytes + GOVERNANCE_OWNER_U256, + ), + // Set Governance.nextProposalId = 1 (packed in slot 1 with _pendingOwner) + ( + GOVERNANCE_ADDRESS, + B256::new(GOVERNANCE_NEXT_PROPOSAL_ID_SLOT), + U256::from_be_bytes(GOVERNANCE_NEXT_PROPOSAL_ID_VALUE), + ), + // GovernanceConfig: minVotingThreshold = 1 + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_MIN_THRESHOLD), + U256::from_limbs([GOV_CONFIG_MIN_THRESHOLD as u64, 0, 0, 0]), + ), + // GovernanceConfig: requiredProposerStake = 1 + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_PROPOSER_STAKE), + U256::from_limbs([GOV_CONFIG_PROPOSER_STAKE as u64, 0, 0, 0]), + ), + // GovernanceConfig: votingDurationMicros = 10_000_000 (10s) + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_VOTING_DURATION), + U256::from_limbs([GOV_CONFIG_VOTING_DURATION, 0, 0, 0]), + ), +]; + impl HardforkUpgrades for DeltaHardfork { fn name(&self) -> &'static str { "Delta" @@ -21,4 +64,89 @@ impl HardforkUpgrades for DeltaHardfork { fn extra_upgrades(&self) -> &'static [BytecodeUpgrade] { &[] } + fn storage_patches(&self) -> &'static [StoragePatch] { + DELTA_STORAGE_PATCHES + } } + +/// Governance contract system address +pub const GOVERNANCE_ADDRESS: Address = address!("00000000000000000000000000000001625F3000"); + +/// Storage slot for `Ownable._owner` (slot 0 in standard Solidity layout) +/// +/// Storage layout (from `forge inspect Governance storage-layout`): +/// - slot 0: `_owner` (address, 20 bytes, offset 0) +/// - slot 1: `_pendingOwner` (address, 20 bytes, offset 0) + `nextProposalId` (uint64, 8 bytes, +/// offset 20) +/// - slot 2: `_proposals` mapping base +pub const GOVERNANCE_OWNER_SLOT: [u8; 32] = [0u8; 32]; + +/// Storage slot for `nextProposalId` — packed in slot 1 at byte offset 20. +/// `_pendingOwner` occupies bytes 0-19 of slot 1 (initially address(0)). +/// `nextProposalId` occupies bytes 20-27 of slot 1 (uint64). +/// To set nextProposalId=1 with _pendingOwner=0, the slot value = 1 << 160. +pub const GOVERNANCE_NEXT_PROPOSAL_ID_SLOT: [u8; 32] = { + let mut s = [0u8; 32]; + s[31] = 1; // slot 1 + s +}; +/// `nextProposalId`=1, shifted left by 160 bits (20 bytes offset in packed storage). +/// As `[u8; 32]`: `0x0000000000000001_000000000000000000000000_00000000` +/// ^^^^^^^^^^^^^^^^^^ `nextProposalId`=1 at bytes 20-27 +pub const GOVERNANCE_NEXT_PROPOSAL_ID_VALUE: [u8; 32] = { + let mut v = [0u8; 32]; + // nextProposalId = 1 at offset 20 bytes from LSB + // In big-endian 32-byte representation: byte index = 32 - 20 - 8 = 4 + // So v[4..12] should be 0x0000000000000001 + v[11] = 1; + v +}; + +/// The address to set as Governance owner (faucet / hardhat #0 for E2E testing). +/// +/// TODO: Replace with the actual multisig / admin address before mainnet deployment. +pub const GOVERNANCE_OWNER: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + +/// Precomputed `GOVERNANCE_OWNER` as U256 (left-padded 20-byte address in 32-byte word). +/// Used in static table since `Address::into_word()` is not const. +pub const GOVERNANCE_OWNER_U256: U256 = { + let bytes = GOVERNANCE_OWNER.0 .0; // [u8; 20] + // Left-pad 20-byte address into a 32-byte big-endian word (offset 12..32) + let mut word = [0u8; 32]; + let mut i = 0; + while i < 20 { + word[12 + i] = bytes[i]; + i += 1; + } + U256::from_be_bytes(word) +}; + +// ── GovernanceConfig overrides for E2E testing ────────────────────────── + +/// `GovernanceConfig` contract system address +pub const GOVERNANCE_CONFIG_ADDRESS: Address = address!("00000000000000000000000000000001625F1004"); + +/// `GovernanceConfig` storage layout (Solidity sequential packing): +/// slot 0: `minVotingThreshold` (uint128) +/// slot 1: `requiredProposerStake` (uint256) +/// slot 2: `votingDurationMicros` (uint64) +pub const GOV_CONFIG_SLOT_MIN_THRESHOLD: [u8; 32] = [0u8; 32]; +/// Storage slot for `requiredProposerStake` (slot 1). +pub const GOV_CONFIG_SLOT_PROPOSER_STAKE: [u8; 32] = { + let mut s = [0u8; 32]; + s[31] = 1; + s +}; +/// Storage slot for `votingDurationMicros` (slot 2). +pub const GOV_CONFIG_SLOT_VOTING_DURATION: [u8; 32] = { + let mut s = [0u8; 32]; + s[31] = 2; + s +}; + +/// Test values: 1 vote quorum, 1 wei proposer stake, 10-second voting period. +pub const GOV_CONFIG_MIN_THRESHOLD: u128 = 1; +/// Test value: 1 wei proposer stake. +pub const GOV_CONFIG_PROPOSER_STAKE: u128 = 1; +/// 10 seconds in microseconds +pub const GOV_CONFIG_VOTING_DURATION: u64 = 10_000_000; diff --git a/crates/ethereum/evm/src/hardfork/gamma.rs b/crates/ethereum/evm/src/hardfork/gamma.rs index ab7243a60..cfb49bceb 100644 --- a/crates/ethereum/evm/src/hardfork/gamma.rs +++ b/crates/ethereum/evm/src/hardfork/gamma.rs @@ -1,11 +1,12 @@ -//! Gamma hardfork: stub (implementation removed). +//! Gamma hardfork: stub (bytecodes removed). //! -//! The actual Gamma hardfork bytecodes and storage patches have been removed +//! The actual Gamma hardfork bytecodes and `.bin` files have been removed //! from this branch. This stub preserves the `HardforkUpgrades` trait -//! implementation so that the hardfork dispatch infrastructure in -//! `parallel_execute.rs` compiles without changes. +//! implementation and the public constants referenced by tests, so that +//! the hardfork dispatch infrastructure compiles without changes. -use super::common::{BytecodeUpgrade, HardforkUpgrades}; +use super::common::{BytecodeUpgrade, HardforkUpgrades, StoragePatch}; +use alloy_primitives::{address, Address}; /// Gamma hardfork descriptor. #[derive(Debug)] @@ -21,4 +22,27 @@ impl HardforkUpgrades for GammaHardfork { fn extra_upgrades(&self) -> &'static [BytecodeUpgrade] { &[] } + fn storage_patches(&self) -> &'static [StoragePatch] { + &[] + } } + +// ── Public constants preserved for test compatibility ──────────────────────── + +/// All system contract upgrades for Gamma hardfork (stubbed — empty). +pub const GAMMA_SYSTEM_UPGRADES: &[(Address, &[u8])] = &[]; + +/// ERC-7201 namespaced storage slot for `ReentrancyGuard` (from `OpenZeppelin` v5) +pub const REENTRANCY_GUARD_SLOT: [u8; 32] = [ + 0x9b, 0x77, 0x9b, 0x17, 0x42, 0x2d, 0x0d, 0xf9, 0x22, 0x23, 0x01, 0x8b, 0x32, 0xb4, 0xd1, 0xfa, + 0x46, 0xe0, 0x71, 0x72, 0x3d, 0x68, 0x17, 0xe2, 0x48, 0x6d, 0x00, 0x3b, 0xec, 0xc5, 0x5f, 0x00, +]; + +/// `NOT_ENTERED` value for `ReentrancyGuard` +pub const REENTRANCY_GUARD_NOT_ENTERED: u8 = 1; + +/// `StakePool` contract addresses (kept for test reference). +pub const STAKEPOOL_ADDRESSES: &[Address] = &[address!("33f4ee289578b2ff35ac3ffa46ea2e97557da32c")]; + +/// `StakePool` bytecode stub (empty — actual bytecode removed). +pub const STAKEPOOL_BYTECODE: &[u8] = &[]; diff --git a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs index 696f4afe4..c0a7bf2c0 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs @@ -21,6 +21,13 @@ use reth_cli_runner::CliRunner; use reth_db::DatabaseEnv; use reth_ethereum_cli::chainspec::EthereumChainSpecParser; use reth_ethereum_forks::Hardforks; +use reth_evm_ethereum::hardfork::{ + delta::{GOVERNANCE_ADDRESS, GOVERNANCE_OWNER, GOVERNANCE_OWNER_SLOT}, + gamma::{ + GAMMA_SYSTEM_UPGRADES, REENTRANCY_GUARD_NOT_ENTERED, REENTRANCY_GUARD_SLOT, + STAKEPOOL_ADDRESSES, STAKEPOOL_BYTECODE, + }, +}; use reth_node_builder::{EngineNodeLauncher, NodeBuilder, WithLaunchContext}; use reth_node_ethereum::{node::EthereumAddOns, EthereumNode}; use reth_pipe_exec_layer_ext_v2::{ @@ -156,6 +163,140 @@ where } } +/// Verify that all system contracts have the expected new bytecodes after the hardfork. +fn verify_bytecodes_upgraded(provider: &P) { + println!("[hardfork_test] Verifying system contract bytecodes at block {GAMMA_BLOCK}..."); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(GAMMA_BLOCK)) + .expect("Failed to get state provider for hardfork block"); + + let mut all_upgraded = true; + for (addr, expected_bytecode) in GAMMA_SYSTEM_UPGRADES { + match state.account_code(addr) { + Ok(Some(code)) => { + let code_bytes = code.original_bytes(); + if code_bytes.as_ref() == *expected_bytecode { + println!("[hardfork_test] ✅ {addr}: bytecode matches ({}B)", code_bytes.len()); + } else { + println!( + "[hardfork_test] ❌ {addr}: MISMATCH got={}B expected={}B", + code_bytes.len(), + expected_bytecode.len() + ); + all_upgraded = false; + } + } + Ok(None) => { + // Contract may not have existed in v1.0.0 genesis — apply_gamma skips it + println!("[hardfork_test] ⚠ {addr}: no code found (not in v1.0.0 genesis, skip)"); + } + Err(e) => { + println!("[hardfork_test] ❌ {addr}: error: {e:?}"); + all_upgraded = false; + } + } + } + + assert!(all_upgraded, "Not all system contracts were upgraded at gammaBlock!"); + println!( + "[hardfork_test] ✅ All {} system contract bytecodes verified!", + GAMMA_SYSTEM_UPGRADES.len() + ); + + // Also verify StakePool upgrades + println!("[hardfork_test] Verifying StakePool bytecodes at block {GAMMA_BLOCK}..."); + for pool_addr in STAKEPOOL_ADDRESSES { + match state.account_code(pool_addr) { + Ok(Some(code)) => { + let code_bytes = code.original_bytes(); + assert_eq!( + code_bytes.as_ref(), + STAKEPOOL_BYTECODE, + "StakePool {pool_addr}: bytecode MISMATCH" + ); + println!( + "[hardfork_test] ✅ StakePool {pool_addr}: bytecode matches ({}B)", + code_bytes.len() + ); + } + Ok(None) => panic!("[hardfork_test] ❌ StakePool {pool_addr}: no code found"), + Err(e) => panic!("[hardfork_test] ❌ StakePool {pool_addr}: error: {e:?}"), + } + } + + // Verify ReentrancyGuard storage slot was initialized for StakePool + println!("[hardfork_test] Verifying ReentrancyGuard storage for StakePools..."); + let guard_slot = alloy_primitives::B256::from(REENTRANCY_GUARD_SLOT); + for pool_addr in STAKEPOOL_ADDRESSES { + let guard_value = + state.storage(*pool_addr, guard_slot).expect("Failed to read ReentrancyGuard storage"); + assert_eq!( + guard_value, + Some(U256::from(REENTRANCY_GUARD_NOT_ENTERED)), + "StakePool {pool_addr}: ReentrancyGuard should be NOT_ENTERED (1)" + ); + println!("[hardfork_test] ✅ StakePool {pool_addr}: ReentrancyGuard = {guard_value:?}"); + } +} + +/// Also verify bytecodes were NOT yet upgraded before the hardfork block. +fn verify_bytecodes_not_upgraded_before(provider: &P) { + println!("[hardfork_test] Verifying bytecodes are OLD before gammaBlock..."); + + let pre_block = GAMMA_BLOCK - 1; + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(pre_block)) + .expect("Failed to get state provider for pre-hardfork block"); + + // Just check the first contract (StakingConfig) as a smoke test + let (addr, expected_new) = &GAMMA_SYSTEM_UPGRADES[0]; + match state.account_code(addr) { + Ok(Some(code)) => { + let code_bytes = code.original_bytes(); + assert_ne!( + code_bytes.as_ref(), + *expected_new, + "Bytecode at {addr} should be OLD before gammaBlock but was already upgraded!" + ); + println!( + "[hardfork_test] ✅ StakingConfig at block {pre_block}: old bytecode ({}B), expected new={}B", + code_bytes.len(), + expected_new.len() + ); + + // Also check StakePool is still OLD before gammaBlock + for pool_addr in STAKEPOOL_ADDRESSES { + match state.account_code(pool_addr) { + Ok(Some(pool_code)) => { + let pool_bytes = pool_code.original_bytes(); + assert_ne!( + pool_bytes.as_ref(), + STAKEPOOL_BYTECODE, + "StakePool {pool_addr}: should be OLD before gammaBlock" + ); + println!( + "[hardfork_test] ✅ StakePool {pool_addr} at block {pre_block}: old bytecode ({}B), expected new={}B", + pool_bytes.len(), + STAKEPOOL_BYTECODE.len() + ); + } + Ok(None) => println!( + "[hardfork_test] ⚠ StakePool {pool_addr} at block {pre_block}: no code" + ), + Err(e) => panic!("[hardfork_test] StakePool {pool_addr}: error: {e:?}"), + } + } + } + Ok(None) => { + println!("[hardfork_test] ⚠ StakingConfig at block {pre_block}: no code (may be expected if no blocks yet)"); + } + Err(e) => { + panic!("[hardfork_test] Failed to fetch code before hardfork: {e:?}"); + } + } +} + async fn run_pipe( builder: WithLaunchContext, ChainSpec>>, ) -> eyre::Result<()> { @@ -234,11 +375,14 @@ async fn run_pipe( tx.send(ExecutionArgs { block_number_to_block_id: BTreeMap::new() }).unwrap(); - // Run consensus — push blocks past hardfork boundaries + // Run consensus — push blocks past gammaBlock let consensus = MockConsensus::new(pipeline_api); consensus.run(latest_block_number).await; - println!("[hardfork_test] ✅ All blocks pushed successfully. Hardfork framework verified."); + // After all blocks are pushed, verify bytecodes + verify_bytecodes_not_upgraded_before(&provider); + verify_bytecodes_upgraded(&provider); + verify_governance_owner_set(&provider); Ok(()) } @@ -288,3 +432,41 @@ fn test_gamma_hardfork() { // Give background threads time to exit cleanly std::thread::sleep(Duration::from_secs(2)); } + +/// Verify that the Governance contract owner was set by the Delta hardfork. +fn verify_governance_owner_set(provider: &P) { + println!("[hardfork_test] Verifying Governance owner storage at block {DELTA_BLOCK}..."); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) + .expect("Failed to get state provider for delta hardfork block"); + + let owner_slot = alloy_primitives::B256::from(GOVERNANCE_OWNER_SLOT); + let owner_value = state + .storage(GOVERNANCE_ADDRESS, owner_slot) + .expect("Failed to read Governance owner storage"); + let expected_value = U256::from_be_bytes(GOVERNANCE_OWNER.into_word().0); + assert_eq!( + owner_value, + Some(expected_value), + "Governance owner should be set to {GOVERNANCE_OWNER} after deltaBlock" + ); + println!("[hardfork_test] ✅ Governance owner at block {DELTA_BLOCK}: {GOVERNANCE_OWNER}"); + + // Also verify owner was NOT set before delta block + let pre_state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK - 1)) + .expect("Failed to get state provider for pre-delta block"); + let pre_owner = pre_state + .storage(GOVERNANCE_ADDRESS, owner_slot) + .expect("Failed to read pre-delta Governance owner"); + assert_ne!( + pre_owner, + Some(expected_value), + "Governance owner should NOT be set before deltaBlock" + ); + println!( + "[hardfork_test] ✅ Governance owner at block {}: not yet set (as expected)", + DELTA_BLOCK - 1 + ); +} From 7169ef5d6954fd51075ba009d4a2df686bb7f670 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Thu, 2 Apr 2026 11:19:47 +0800 Subject: [PATCH 2/2] refactor: use storage gap pattern for StakingConfig Delta hardfork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove StorageOrPatch type and storage_or_patches() trait from common.rs - Remove StakingConfig stale slot cleanup patches from delta.rs - Remove STAKING_CONFIG_INITIALIZED_OR_MASK and read-modify-write logic - Update StakingConfig.bin (1519B → 1530B, includes gap variable) - Simplify integration test: verify_staking_config_preserved() replaces verify_staking_config_migration() Net ~120 lines removed. Hardfork now only does bytecode replacement for StakingConfig — no storage migration needed. --- .../hardfork/bytecodes/delta/Governance.bin | Bin 0 -> 8665 bytes .../hardfork/bytecodes/delta/NativeOracle.bin | Bin 0 -> 4643 bytes .../bytecodes/delta/StakingConfig.bin | Bin 0 -> 1530 bytes .../bytecodes/delta/ValidatorManagement.bin | Bin 0 -> 17086 bytes crates/ethereum/evm/src/hardfork/common.rs | 17 +- crates/ethereum/evm/src/hardfork/delta.rs | 169 +++++---- .../execute/tests/gravity_hardfork_test.rs | 331 +++++++++++++++--- 7 files changed, 388 insertions(+), 129 deletions(-) create mode 100644 crates/ethereum/evm/src/hardfork/bytecodes/delta/Governance.bin create mode 100644 crates/ethereum/evm/src/hardfork/bytecodes/delta/NativeOracle.bin create mode 100644 crates/ethereum/evm/src/hardfork/bytecodes/delta/StakingConfig.bin create mode 100644 crates/ethereum/evm/src/hardfork/bytecodes/delta/ValidatorManagement.bin diff --git a/crates/ethereum/evm/src/hardfork/bytecodes/delta/Governance.bin b/crates/ethereum/evm/src/hardfork/bytecodes/delta/Governance.bin new file mode 100644 index 0000000000000000000000000000000000000000..bf8534eb590306826d2b13fb9c000b177b26edf1 GIT binary patch literal 8665 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 literal 0 HcmV?d00001 diff --git a/crates/ethereum/evm/src/hardfork/bytecodes/delta/NativeOracle.bin b/crates/ethereum/evm/src/hardfork/bytecodes/delta/NativeOracle.bin new file mode 100644 index 0000000000000000000000000000000000000000..0b168996ade04435120e7378404bb998cc8c644b GIT binary patch literal 4643 zcmai14R9S*70%6j`A?JgYO*D!kOgYBAhe|%#Q56Bv_$Eh0egx zQo4WdCuy;FcVFHMRZAg7N3q~YM;ug2={SmxwA%7B4nnCdwNPi&4pNm;zq|W3d2Mif znc3Xkd+#~te&?L;oJ+UUeuc*S5}m9jsp`gQP4A_LW*b9Ina}O$VD+n1W2oz~w;sjt zd<>OiPi^U7b%!v#|ABq@T6aB$#rgmG1%~YyZkvB(X$OmqiQ(ISy`Y2D9>?&-6$uZ+ z5e$E|cMl|xchsSSYiww!$p>1mWaO|qbOt|Gv$c;k$r;V#E~T{JNVA$BnyhJkM$=72 zr?N#9qDZlfX)%E(HSmubHsP#E&1f-xQqzTY*_64L(o8$S$YW|MWfLkB6OtKO z2&u%RvRCwr19o(b=`TwJ>sQoH-+sS4+s!W4dA7; zkH)&lOaV0`XISH(AY)~3aG>qoF;jBsoL<-Fu}NQWRy&uq-ImcL-GrerDhFs8ww!eo zOK~kO8zHFJ-(^bqS==3Tas1F7_{d$!-&lX()jPPf>b~v+2lbi0y!*F{XD<5W#5HqD zk6*ra_MTO*yL?AY#Y8!bf!b}8F(7Jm*GQcKwA@AcfPFUmLGMY<=Lx`&8msQPk>P(ww zRr4Z@cl4H`uqjkjI#$LQ@>HcF&_=T?x9Xy=eG~=f1yicuL@y7WZ?!Avzf+GG?Sd-rz>hwjL6+$&x zMe-&Hi`Qke)KJfj&+egr?uIpSh0amvO8vBU`x_mP(j&n)ybaafyMG>i2d|5;CWIzh z874|K@-nOIV|AB>p%S>LU!;0EN*hGD2)St+Ssjb^L`#uj+&U}0>atbo1@waSnwD0% zFs*#2I<36_i<`?QqEM2jLMilL-A93OlTMQ<39uCOIUtEl`q)E-o9nNg+zhG;QWFsv zaxMhn54rbolB%<>-k5}EWf;BKWJ+ehKFBE^2tv|XLB{mai)}I!RD4)9k(%dD(>XTr z@aXdh2{Wwz5No>LQj`s$l)@TS?)QE=mutr2mm&2*y zVHY?BgLprGXXT-Nl5j8>-oHW?pLq+)ie4&_4t0AL^lr^#08kU#_n5r}xG36<9_ z8`)qDB9JUHNQz+1Fs85)N!<97${L=q27+2dd&Cz9Bu8c1VTu)P(C12JT z>uHq1Qh8kWSYv}ALz9wjSMTC9);+sRWF2DG2o=CGk9!iR6y*;i1z|$cJXJ7Q!>fqx zo-Ru&+qz=v^0=;<;D*LEYRWxZS9$yq*P&#rh}i+5FlBZVKFAv0%|MvmMNc_UEkQRQ zEEw?hbs5dJNqW;DC44J+gv!1{d{FF#8O;*N0k$4gzPi~aajEP;>~0j>9`S|n(28AZ ziZwhX`Tz*Q*6(FB?xsx68n=O$zI6Tid*~sNqgG7gP=h*&61*nLRyI;pg{p)rD#H2AT5egE+{*ypg?u`fL$OJ1nOL!gq7>-4Ij4qc*|8qgKNyG*Agi_HqK&dvZfOVZ{GBBeBfc9ttXJ?gQ|$o#WuV zK>ZUj-f|vVujmJIXt~(5!%|b>8aOx#Bi7PyW=DeR)tj?IqfJ6|_QmW^1g%EP&dpo$ti|8LTDJM4fD~o6*P})nR)Vq~&K7c&*MPBF zkQ1Ia0S=LJx-K)3cR64l)gBi&r|-Ac!9ZIYlir^i3nL z{B-gvR*_d0!uXwL)g;kEm<2&sy)GwGOG`uNfl&e<8H(q#&@?_f?P9+N|}h zA|+dk=g}KJV!zd%+N)jZ<||TRH;a7&oSzeG8UQ=)?F7d z9_(SQ&rGL0p>)P#BxA9Z;&_xn+{E;9S&}wE_QWH6OR4C&=ge`0R8t8-w31g*l3j8R<2aDN^UFeEn%##_d z;7~{OP5#h9>6&N2m57lzE+}-4V5XHe9>cg@oz_wXvjdrU20QIwbPA*Bm(hZjsb*1( zwJjI?Fuk3siP|Z#azU_i&Bv}>Fu5`aT%;IIArX)f{_uv{-}c?JIVc%XLFj1%Xhls( zP)E+WAViW3*7gsZwH45)7A4<_vGzG2q^Z4?TO>;Kr1Gw?T@}cN)&M=$zBuOxVvtPf z;?NaG=KT&p<^UwZkIn&_QZMLu%+i_Sk+QzX!()R9H9wv0M^u<)5s2{po%C zUwe7|B@1U+y{|kVB65k|!4hj#mRP-nC3?{%J&Rani50NX3-7(AvIIn8lcl09_8rlt z%ED4y-h+Qr7v2ffesES|HKij009do2RdDqoyZnj1dv(>J#+nDy@yv0Ddsb-RK(43^lB1DDZxnMyF_I4l7~#ZnNW4%3uUl~4mWh>@uRC_NeUteE?a1=F+vt-YyjgVplS+w02xIclGzTEO6`nZRZKty{kde%NOiM#5V!Mh# zgq=52wge##ElL^`geW038iHvAlO~E0O{4@vVvN?Lf}$n{NcurS#P!bXPFYz<*!`IE z?tSy_ednAz;6RHCV!ArXgt%cj*G;P#rs^F#t{l}8j6Q5Q_T%Kls|gl+3*l1>J0}pX zN2t2b&Oz9TaO<}}Ka^mk8(}V)_yb`p!u|8LeuVQ7o?888Ho=5{5dL^&#~8wM2>-UO zokaMVVP9??w-F764LrewPYo*#w3gHa5L!CA`#*1d-VwM8ia%`zT~=&TEDbmMT%Eo} z4ha5R>L?K0R4?ClBUY;aWar$es8VH>vXgFqi~gN{>BmB~GN(fz5~7&*2vcF(3iM;4Hc zD+XWxeg6{w^w!CR>G*rD@6xeBTPB*+lp!o$$r?RQ%6yXl$${jPyw3BO|NU4o$FklzuI#= z^W6)FzaAIbuJ7z^{Obo^qc3{xrJhiMHIs=0@EZyMB641-hd?4d0Q?)nBsTld&>XSD zWln<`^P#OWrklZ_1owA&b@YZCPcd;Z4yCG__H)l{?t`hIhh^vjXuw7SVibzRNYr3r zS5crU4GLUNGD$E{94mu{3`VgeQ8_oBd=o7|LSE3A7W&*mYX^?K6N++Yjt;+)-Spwy zsV$FvJni>RaQ@|`Q=Y%iy8A)Fc7R|d&@MrJ@4+{{Hl-^wD~=u_L7Xp7Z%dIEPtqd zc82s~ACzaQr=hvN=H$PixfIRwmaKUO%`!B9cHn1fAr-Givw!zpZ=mTy z^ZHNHe_Tk;tI@pig~3oE6}^e(ExD)v2+b?dd|>G1m1v%h=A&2ddB2bf-$rxj5}&t_ z3hqI3=hfN!3n_OBn(wu~Kedo16`}cA|25+aY5W>Azg_YHXpk=Tw;$MbX#-k|1ap>< zw876@b<9yB<%_n&BsN&;Ajv}uHnattL0^!$e9Xn95P#0xL05~jsEE0}n(X^~fOFWG zWGFESNxfn)tEk0n5_6Y2+uQDZEjXQhP%2}qGRu(HNv8*M)7Ku*^6n@$q}O3X0sT`|L3yEgpL zm?1-!Hfv(tl%t-;C~FmGjjTCn+#P;Tut$)Jac<4k5`RzQ2E^xB8^g(Ix|3BX5~nZ3j*+E+Bh}U_%-Qa|toYByu0~;F_Sfhc zQgSK#9lyuJ%DeL@`x!queyfB#!nE?)w!kJ+ z!_f`}bFvH-r(kaVdz?vj4xh}?CACtft1hC_vRm5%I7P26xnkj~&8 zbV<6kiefN0bG<^Dr$-D1M+^pQtX$1wQX!31{FFHm)q}c9V}F7rJ*gNTV3{4s^FC&q zH<*~Q05g7K&NvoQl5#?80}zu=L9Zbt3Zw+w9-}6$HfB!B)v^ZtdWgoXjk2tP{=Q(8 zvie6`QqnBObQp{oBX`i4E5in3!g0oQ3dW52m%kqqzZ%`R{P~GvuHW_Pe_#9T@CBbu z-1*X?zrMa_UB*RSl|Q)Rm)E?|_~F)VSa!^pqb*BgK7(vV)hKg?p@v~er)6)p1svMV zX#uHL^K^wMQ^C=emvlOH6?}08UD04bmUAiVY`+Jm8PmIuNW}JQ`_SLR56~l*N?I$8 z`C}XA50)@TSSw}UIH5gSYiB4#Sx3Wo$GA_Bqn({nt=ua0OHqlk#hUT*A6W4UV%v)oG~mV3~?-1Zd9ZQ;w^ zKFV^@wg6?1GbEzDcILLHVZFIru#5=oLq$S74hA$2^u3X}U}Km012SdLWv-Y?i_+Qx zvg8mB-`h)>liC7}x`(pMAVh9&V^C>o)J6=Bh{0zj4W^9!I%QwYTv1w*4sty@W$Z8~ zJDHPtj}8h90a%cP5R-Bt zL_T&52!O$CYt1$4yKk)4PQnzck1ihppR2tPvxclSlND<4SWx z^@tH$#E6TMMlctR>m*0J#R+mOFvqWhOKW!?*YzIG2^%tXqCM}WB}LYg8f!B&ZnvFd z35NAj`k(kblTO%LYK;=QMdJ(NBgU72$K%J_dHgNdO6h}w$KxfaXa4GoPck^00glr6 z)%MYe!J8@lv^EpjF&nZQf7U3-ZhVIgQ>8+7<9}uAAnfR1sUsG424y>t(fhSNEvh?< zXi|DIOp1)|k#jzPQd!d`U=nxg<6r(;^C4Ks<*);@oShg;IS=}2!toK{5CKDANConR zf1wGi4o_|VGlYXocAq)De(O0#MUuVJcv;eD!rhADFenG&XL*#c;ZY{+3MV{DP!F?m zSvZr4ANfNx;h@eY^aG1dd=mtx^omI?YT`7Ti{d`ka8461u$aR3&N-!WITLrpUCxoFwp`8#kCV!=O#C_5!7iz` zhbF*0drX;eX|WEFAUgx0&Roi=^0Ns89yE%%ePvR_aAOmp@^Dhzk98&d7@TI#*tS5o zD%FNtnQNd^8eo#vCn4sL^ez~piTi~$q?|Kl$_c{JFM>sLD8hJ}D9U!&wzHO7;3jFp zeV|hSY|}(@Dd!=A4~`u6JL*Z8>h_0ky7WcUu5qQzc_+0}cF=@+Sk;{Wi@(l>%*Kss z2aP`n0NlQIbtlK*30spC<th8lA;2{n}EL(~wUzld_jat+kl%}7Oo?K0wkl+n$k zzx?M4IOzpT0Vh4D(bDtV0%-x7^g(=^Fl-6aq|l_tz=hoO`0Ei|z-vg$_hCYA&G%-4 z#RD87qBUi7GgP4c=Ugbb$m9a7i)DJa{W@Z3THwP#BQ#A1>>XBH5G=?@ffO{sY81@z z3pqSu^pXE5r#)3jsQw2bVg5trY)1?U^TX(w2nj)e>%-<6VX>otyS8XN7B-^sKFa4n zRi(UxOlo22EgDtjwFR)_{6;_GIz(JdLKr~##drgpv9UDmi47h52ejMHTH(6=AGcMW zGIrlf>py?vt-CIKX!7be7aboA0ZgmyXTE(|WQ6Kyu{eavzlyv5{PU7rzc(7Dyw449 zpC7cNh5TQOAvY!uiBR4n<_O@q1R}}bD@MGUJOXQMF-PpQk0@~Q5e1H<5kMpbC6sr0 z9FgR?&GGx7AKzA-d7{{9&_9pbO=h`%18 zrg$B;0ERvPl#xi*I715ZP{N59v3iS$ z@tv0XWKrRbJYU5eGK7SiIyVvt<5c_@EOTm*^nNN_)=3LLY72BAkK(1mHNw{O7+9El z=IR8b<}c1NUSwJ?-24UWz+WMbx_OsPMwc-i=GZ~Jfak!wP@fz#9}aN~>h|i2&Ri-y zf(k&e;9(xj{XGr^qMr7u!1;?!l;%UIZy;S^#)AnkhI!g^e*b}VNZN{Hn~NFEG8+{%={Z_bk?JT-=R!rl z5O($u7b6Y?zQ=Gh@igNwOXp`MwK%D0H|JfH7Tnnu_`88cZR;{e3l;5?sk#^1U_`sl z9PY3}5RHgJ3me-4A#zS(Qk1GrhhG%=%VZgGp7MQVgz>{8=ebG;RlnU9K=X1rBAOnF zil7Tg2PCzSbNfgV?@K1}NkJmycvKRH|2-0mbK)cxmn2AZBIkN2iCm3}8)${2E#T$C zq38zZlKK@hsy_+(T)bX%1_F{WK#{!Qy@E``)Iq*B^X?q?+KiU>zvzw`0uZOzGGaO`B9%eI$B*4wUDBP2Xat$WlK1@^SM}WgyDzH$yKVTF!$+^9Cw}nDyB_Jv z**|p7J)z(`Uw?D|u{vYO1SO2M+Z%HlW)fBt+ILf$J;9V``KW~doKnNh*_6B_b7ql6 zQ|5DZGtw1QA{&NnN(PLGPE&rykX)N`LPWTg@Cb{c(Uf%xEw4hF!;m5w2hS~4tH2&n z81j-6xXGE)n_8a}vd0hy(v<&^%`kAv5ZDG2QqlzdDxD(Z|1gzKWSRXegG$S{!EbqK z%E!XJL`=UVvbcwfKir7<*#ueYi|7*ZAGGpk+``Zl;EoWLR;MP?@}1mmWdcgP!4onap|2=30e@&7gq z#y&aMIse!bDxdf~Q2&$KWw%cH@cDN2&a3kl-ZJpeE&9!B2>Gv4ASo(+19}U~sI@Q$ z)+;R&$H={g#w#?gw1y(1YNdlmTKiA)t3QIT(QeKRKqpE;&9XnD}cXQI-mR*ZKK=Yc_9DxnR~x}xV>y= zX~XSGPtEif^_n8D0`dSA?6UWto~4@3!%| zo@uW?arNsjeIWnij2|B8Xqh+rttWo7{)ETw?)>J}Kis!t2M{mldtl@`Q&-0)FvZne zCmkypsdWrOu8ujdsj1g$8+Z8E=G<`dinMP}{^t2b`ESiR`?vLL8#2>hIc=8nmV>>|*IM7c8S}GA_v$+l??V{>GVmmOTB*SF;1ZJGkNK=_fz` z`?9plUOD@l>utrKXkw$6CHp?a<8Cjj;&3R*-N7wR^HKGep!hIekw^3p zaHA9|=18igx&b*eYDM+4j?F|<$~Q$8hikkBO67ZudF+%j35jj75|{>W!V7=p2U1paW?khc z*KDJHRDn`y>P#8AmLmg$g(R>7)bhZ?A2e#(6oy@B^+* zni6L%%Sa~JbysSFCxhVKM<)17gDv(WV42CQOwUWXO2q?yn%;E8Ri>Xu)6XE_m5Po8 zbE1h~keWIapl$`nj9Q}23*n4~&qVZ%KTMQu4XZ)Z?`5~DV!{O1F;%KU-Sk&e(|;`J z|IVQQFvYSN$W?GL-)R@rLj-3=vqCfKSXKuBIxiaYR=j#=lr?AWf?W3x#AwDt;DIrjoP2Y zp(f4PM>D=+6$T@P*a}!nGsmDtpE#}~7W-)C3iO7m+Epw{GZ70}g~lG^B>P7c91S=? zGcOxK19$Njhc^LmV(N(00j+QWvgurZj)-sdn+5f?Mj{80W!nGZSjtOKEM!>`xMy3v zuwsGV6UKRL?ZbKWG3nZ0Z0o}jZQI}>}$;8cMq)6uj{Q*)|ds1nre+%b4FR?xe;sJV_#$5XlvX&${L%I zxB5xeb%HpjpFFQ}Bg9YhU}P_KS=TRkq9)RW882quP-IuX#o(G;X}w`R1`J zHb0d9qg}(FPTl|6AD?*Y+58=I-)wm8k@I)uFTQ^Xb0bzqR3rhVaEDyXGFy45Yj|(q zPcHcgR!)B#DX0%cDjw9fr(gWaT=p@3)_^p_t&f`ANpGRb*GJ?ID*rbrcToAQfbr8t z&mGL}^P2ITJ<$hvlijAyA({VI24s6^+sZLF@=z=uYJAXwKcnRx6BLC!muI6R+TA z7r%{XqA4w@CL_wCbN|7GU}QZ)9lahg_jT~Wn)2T)i_Ya4e9l8;6X*U4*914-`Ps{* z2hx6=vvSXwndh$dmEZAo`npp({`k(2*Nzq#)Z{Psx@(w{8?V_B?gM*#*)kZEt< zvv6jkt>QKJ<gv^IQhc8A*d6gXS%kY2ot-1!bQi zv=xb3$ZSx>UkVF}zX!$NT$B~I7k>vuYKs5kGJd&@e;DsUQV9QW-7p-d6-gUEIYV%7 zlGoI1c>qqwqXr#v90hX+ikO4uy|2=|x4^`Rp_h)ZS$kBZC~_i5lq$%Q!V&?_Th$&6 zb>JyuVPu>5G^#GsQT!^NnU!|XQAcUU9Sj!2jVv4%ax7sw>KFtMrk!%owbb5ju@nyz5Hma2*iMi> z0CsMYwsvuIVm{CA!t)sT+as36Exi`>a&P9vS+h^Kv&%;k>y@@4i!US$hRE41XhdS= zI)vo&CnC5%8A&giKUwP$8^P)HO4~OXy(Y*v@Lo0XUQJrB*=w*@`o(6^>jwD-k)pTO z)x1XjLS#Kj8qz?;UPYq$SLn2$qb(4F`Dggqjh}b&Y$_J?d3zKGPz>h451N0Qj%+{r zYTixe^P$cB`BU-d-RASXJ^cBf;?KK`=YnH+@rC)K&%j-({E|1s zvvyHk*1}RaES(lq6vS`1%_D&uPD#?h15}d{xS~Xf^dv1@j^Iuxj^gDCFhmRI(t-oD z;8W)6Ku&hAQHt5yfrH~5WNti-fLdONF`mFNc@ObU=;Jhe6gKJ&qx&G(A-YF-ig`#5 zJ9P5_NJ2m(!4~)wZ%Bs>CeN~bSMs~)7R}4MV+lu41e@kRHKCqm_}yhVKe+!YO!DZf$XXx$cL=S9a6s;@`EGN z5CRxE*XpI@74w)Ex7$G{(&VGO!Uc~|i7q)0(?L??vZxa_ebJA2ZAi^6q(u!Vui0+y z&9D-yI@{=kmQ=w`;xJp#To2=chzaK##q~cYPyQ}?(hnDdS;Cwz=xT)hu2`Q(j|z@0 zx&=L4QI9xy3`4sw*4Ls&IwhD)U4ob`ih@BX4DeBGII2fWoGP+~xM`kQDu!tor8|r} zYl0~ddf6!piQ^|>dmVyTww}YUN};I8K7(=TEj(FLRp;kPIUZ6-S`dS6Td%gZjx@B@ z*974>1W>4|gel$5R$n4=6AaRlECe_<)a8bp@{C#Z5o1 zLoo*u4yC$M8bi7hp`ge#v;_q1qfV8MtHxp~5I1TkM=NuPP8@hvrPVm7x&mlJC>=k@ z5eYwy4I^-m`xSn?ivd4JK1>If7ypSLKGL&%;W(@FifHk(ep-B!jwHF2Ii#v#RO0tU z2CoYTHk`j~?cP71bK7f6A39^`hBLlh`q(SGqrd%i+uCu+OX5K0AD4ZNdC!dl=XkXm z)j@uN&M1h|;(Z#`UkzzAvxVPt@+25um1a$8sT;mm;M#^X97F`n2bQDib*PP@R?HY1 zQscNmN#KT{4L9(j3x;e}sM-x}uD%sh=!VBsWFM-U)epiH>f$W~OVrS+pD`2&=9Q{n z5jAf`iEtI6>PNX%H6;v+U}*(d4^6Il!A~{mBZ7sRJj?~kLsT>M@TvYRs=0&16QfMd zZLWUZS1y>fyWN4yuSy(pSCCEBLZc3nDRNd@_-__JFP! zmH8;u@k__tKET_k4ywa>A*HLLlv?{Znp!|b`EAJ_E)m?Q(vtU;RF@reWrW_cVblW> zYMbt$+I#)D=xlI=YUhME8I?z`6FOS!7BuKlPXK;($%~L}N#)P)wQkWk_$`{+Q&pk6 zu=Rq8_?kkBZp<5V{83X++Ft%m5k0pf^6l&Ce?Irvhfnb2e|O3=1smQObisGVm%TC4 zV=W6d!av-s=u}%R?sQ;vs;$QkYj+yO9@8dSrFwIJiHiZY?G;k(L4Jo)q9sFI-|Dh? z0dI*DzC^>UsKIXh__U+5H~#DT;fgn(eAB7_W!5K?FMQ|z{nFOyS?4$}{`|8|xZvXs z#iAZ)Cxie(c@uZ*p2kp6ZKAp#Coa=MLR7>5glsYDyrjAD{S_$$W7a6qBUmgkqEc$Z6cFNnj|g0$q<+~kDKg^>Q$};53Y5+ZoIIA1eSjdK16997m2@d&Po||yld7a%tQ|H=qDzsUp`~ZV z1FsP=GrA#%mFzsY8G)J{@y6P+bA(p$kEzlTzKnD$@HNY7{%v2gY+|ynxwfqO%M%}W zK6Cqnvxeq8^RMe~c%@;gH|@HneFtxtch^roT_$`DEn9Jj7x|vU)X{je^e)aDL*-0I zVgO&mnE>3-vU?R;)(tVGWlw=)h`@w|^vPkLtV%+t6d)RWr!yceg>A#>h43?q_rTLj zQ{ZX-xkr5_B#6hffEJRdQ|bVOAfdjTixv*FYIoGYCmPffXq07^sZtCh{|jmaA~VZi z2zY@LDj$jN8)o+DLBRiE;zeTvBg`I^| z|2Q|wRF5=mumpjvM^gk=15l#+H>m!LSnBaWrF>ohI=H{(Wqw#BTApDnCUYnvU=6k>#%%ioE=5J}^FHB_GmIoje31 zZZk$S%o;I56(iOqjiBXQbZWQ~2FSclq0@3i-TYG$9#3e945yXIs6i@54N?l3u_A&V zoU-`x6jrd#p%(BTZ2?<449F6X_-?7;EfYgEdZsO0%30lLZGJcerj^a)OZl# zvRO=Oe6k1eGQUyhwXQ?4ENc8nvJXz z^m$b*Wn4gP+An*>o{@~=%-8afYqO3l+duZ>>o<@6bYn&)>KgF1cG z3|Qh(i*PWnq0vFjt2;WxOnKDwd)pjmbn`hPi8(f?#vBlBK8Ln7ZeW^k;pU%0}`!xN(`BkQV?GvCFkYyeZzgOm9$S~4P zijETWN(aW5VggX%RxIbJ^&%njvk~&T6scDqLpwJkTl5mN>w(tgEa8UUQma` z1}hK74B~I_d*Fd(ahK0UD<9Jhu)z(niBv+04}eE@>8T6hs*t}?qg9hFoVBVF6InRR z*N6`+!ctS9h$paCT>=5bKW*Rwt@;5+IhE7Cd^%BPM0wlAT?5bwa#dCqswhaS+@O^U zvNsoNA&6lN|5mkqck{ZlH=KFNk7wk&PAuMb_pFoBCw0F4;IRDEqd$FbZQb1UKJ|)I a&z@Pj@4?4*th?mG^V*M28#^tn=KleZ>VTgB literal 0 HcmV?d00001 diff --git a/crates/ethereum/evm/src/hardfork/common.rs b/crates/ethereum/evm/src/hardfork/common.rs index f689569f6..0bdda8f03 100644 --- a/crates/ethereum/evm/src/hardfork/common.rs +++ b/crates/ethereum/evm/src/hardfork/common.rs @@ -11,7 +11,7 @@ use reth_evm::{execute::BlockExecutionError, ParallelDatabase}; use revm::{ bytecode::Bytecode, state::{Account, AccountStatus, EvmState, EvmStorageSlot}, - DatabaseCommit, + Database, DatabaseCommit, }; /// A single bytecode replacement: target address → new runtime bytecode. @@ -87,15 +87,20 @@ pub fn apply_hardfork_upgrades( state.cache.contracts.insert(code_hash, new_bytecode); } - // 2. Apply storage patches + // 2. Apply storage patches (overwrite) for (addr, slot, value) in hardfork.storage_patches() { // Ensure account is loaded state .load_mut_cache_account(*addr) .map_err(|_| BlockValidationError::IncrementBalanceFailed)?; + // Read current value to properly track the state transition + let slot_key = U256::from_be_bytes(slot.0); + let current_value = state + .storage(*addr, slot_key) + .map_err(|_| BlockValidationError::IncrementBalanceFailed)?; + let entry = hardfork_changes.entry(*addr).or_insert_with(|| { - // Account already loaded above; create a minimal touched entry let info = state .cache .accounts @@ -109,11 +114,7 @@ pub fn apply_hardfork_upgrades( transaction_id: 0, } }); - - entry.storage.insert( - U256::from_be_bytes(slot.0), - EvmStorageSlot::new_changed(U256::ZERO, *value, 0), - ); + entry.storage.insert(slot_key, EvmStorageSlot::new_changed(current_value, *value, 0)); } // 3. Commit all changes atomically diff --git a/crates/ethereum/evm/src/hardfork/delta.rs b/crates/ethereum/evm/src/hardfork/delta.rs index 3a7b308f5..69a169e9c 100644 --- a/crates/ethereum/evm/src/hardfork/delta.rs +++ b/crates/ethereum/evm/src/hardfork/delta.rs @@ -1,77 +1,59 @@ -//! Delta hardfork: activate Governance contract +//! Delta hardfork: upgrade `StakingConfig`, `ValidatorManagement`, `Governance`, `NativeOracle` //! -//! The Governance contract was deployed via BSC-style bytecode placement during genesis, -//! which skips the Solidity constructor. As a result, the `Ownable(initialOwner)` constructor -//! never ran, leaving `_owner` as `address(0)`. This prevents the owner from calling -//! `addExecutor()` / `removeExecutor()`, which in turn makes `execute()` permanently -//! unreachable. +//! This hardfork upgrades 4 system contracts and applies storage patches: //! -//! This hardfork writes the correct owner address to storage slot 0 of the Governance -//! contract, restoring the full proposal execution lifecycle. +//! **Bytecode Upgrades** (from `gravity-testnet-v1.2.0` → `main`): +//! - `StakingConfig`: deprecated `minimumProposalStake` (kept as storage gap to preserve layout) +//! - `ValidatorManagement`: consensus key rotation, try/catch `renewPoolLockup`, whale VP fix, +//! eviction fairness fix +//! - `Governance`: `MAX_PROPOSAL_TARGETS` limit, `ProposalNotResolved` check +//! - `NativeOracle`: callback invocation refactored, `CallbackSkipped` event //! -//! Additionally, for E2E testing, it overrides `GovernanceConfig` storage to enable -//! fast governance proposals (10-second voting, minimal thresholds). +//! **Storage Patches**: +//! - Governance `_owner` set to the configured owner address +//! - Governance `nextProposalId` set to 1 +//! - `GovernanceConfig` E2E overrides (testnet-only: 1 vote quorum, 1 wei stake, 10s voting) use super::common::{BytecodeUpgrade, HardforkUpgrades, StoragePatch}; use alloy_primitives::{address, Address, B256, U256}; -/// Delta hardfork descriptor. -#[derive(Debug)] -pub struct DeltaHardfork; +// ── Compiled runtime bytecodes ────────────────────────────────────────────────── +static STAKING_CONFIG_BYTECODE: &[u8] = include_bytes!("bytecodes/delta/StakingConfig.bin"); +static VALIDATOR_MANAGEMENT_BYTECODE: &[u8] = + include_bytes!("bytecodes/delta/ValidatorManagement.bin"); +static GOVERNANCE_BYTECODE: &[u8] = include_bytes!("bytecodes/delta/Governance.bin"); +static NATIVE_ORACLE_BYTECODE: &[u8] = include_bytes!("bytecodes/delta/NativeOracle.bin"); -/// Storage patches for Delta hardfork: Governance owner + `GovernanceConfig` E2E overrides. -static DELTA_STORAGE_PATCHES: &[StoragePatch] = &[ - // Set Governance._owner = GOVERNANCE_OWNER - ( - GOVERNANCE_ADDRESS, - B256::new(GOVERNANCE_OWNER_SLOT), - // GOVERNANCE_OWNER as left-padded U256: the address occupies the lower 20 bytes - GOVERNANCE_OWNER_U256, - ), - // Set Governance.nextProposalId = 1 (packed in slot 1 with _pendingOwner) - ( - GOVERNANCE_ADDRESS, - B256::new(GOVERNANCE_NEXT_PROPOSAL_ID_SLOT), - U256::from_be_bytes(GOVERNANCE_NEXT_PROPOSAL_ID_VALUE), - ), - // GovernanceConfig: minVotingThreshold = 1 - ( - GOVERNANCE_CONFIG_ADDRESS, - B256::new(GOV_CONFIG_SLOT_MIN_THRESHOLD), - U256::from_limbs([GOV_CONFIG_MIN_THRESHOLD as u64, 0, 0, 0]), - ), - // GovernanceConfig: requiredProposerStake = 1 - ( - GOVERNANCE_CONFIG_ADDRESS, - B256::new(GOV_CONFIG_SLOT_PROPOSER_STAKE), - U256::from_limbs([GOV_CONFIG_PROPOSER_STAKE as u64, 0, 0, 0]), - ), - // GovernanceConfig: votingDurationMicros = 10_000_000 (10s) - ( - GOVERNANCE_CONFIG_ADDRESS, - B256::new(GOV_CONFIG_SLOT_VOTING_DURATION), - U256::from_limbs([GOV_CONFIG_VOTING_DURATION, 0, 0, 0]), - ), -]; +// ── System addresses ──────────────────────────────────────────────────────────── -impl HardforkUpgrades for DeltaHardfork { - fn name(&self) -> &'static str { - "Delta" - } - fn system_upgrades(&self) -> &'static [BytecodeUpgrade] { - &[] - } - fn extra_upgrades(&self) -> &'static [BytecodeUpgrade] { - &[] - } - fn storage_patches(&self) -> &'static [StoragePatch] { - DELTA_STORAGE_PATCHES - } -} +/// `StakingConfig` contract system address +pub const STAKING_CONFIG_ADDRESS: Address = address!("00000000000000000000000000000001625F1001"); + +/// `ValidatorManagement` contract system address +pub const VALIDATOR_MANAGEMENT_ADDRESS: Address = + address!("00000000000000000000000000000001625F2001"); -/// Governance contract system address +/// `Governance` contract system address pub const GOVERNANCE_ADDRESS: Address = address!("00000000000000000000000000000001625F3000"); +/// `GovernanceConfig` contract system address +pub const GOVERNANCE_CONFIG_ADDRESS: Address = address!("00000000000000000000000000000001625F1004"); + +/// `NativeOracle` contract system address +pub const NATIVE_ORACLE_ADDRESS: Address = address!("00000000000000000000000000000001625F4000"); + +// ── Bytecode upgrade table ────────────────────────────────────────────────────── + +/// All 4 system contract upgrades for the Delta hardfork. +pub static DELTA_SYSTEM_UPGRADES: &[BytecodeUpgrade] = &[ + (STAKING_CONFIG_ADDRESS, STAKING_CONFIG_BYTECODE), + (VALIDATOR_MANAGEMENT_ADDRESS, VALIDATOR_MANAGEMENT_BYTECODE), + (GOVERNANCE_ADDRESS, GOVERNANCE_BYTECODE), + (NATIVE_ORACLE_ADDRESS, NATIVE_ORACLE_BYTECODE), +]; + +// ── Governance storage patches ────────────────────────────────────────────────── + /// Storage slot for `Ownable._owner` (slot 0 in standard Solidity layout) /// /// Storage layout (from `forge inspect Governance storage-layout`): @@ -121,10 +103,7 @@ pub const GOVERNANCE_OWNER_U256: U256 = { U256::from_be_bytes(word) }; -// ── GovernanceConfig overrides for E2E testing ────────────────────────── - -/// `GovernanceConfig` contract system address -pub const GOVERNANCE_CONFIG_ADDRESS: Address = address!("00000000000000000000000000000001625F1004"); +// ── GovernanceConfig overrides (testnet-only) ─────────────────────────────────── /// `GovernanceConfig` storage layout (Solidity sequential packing): /// slot 0: `minVotingThreshold` (uint128) @@ -150,3 +129,61 @@ pub const GOV_CONFIG_MIN_THRESHOLD: u128 = 1; pub const GOV_CONFIG_PROPOSER_STAKE: u128 = 1; /// 10 seconds in microseconds pub const GOV_CONFIG_VOTING_DURATION: u64 = 10_000_000; + +// ── Storage patch tables ──────────────────────────────────────────────────────── + +/// Storage patches for Delta hardfork (overwrite operations). +static DELTA_STORAGE_PATCHES: &[StoragePatch] = &[ + // ── Governance patches ── + // Set Governance._owner = GOVERNANCE_OWNER + (GOVERNANCE_ADDRESS, B256::new(GOVERNANCE_OWNER_SLOT), GOVERNANCE_OWNER_U256), + // Set Governance.nextProposalId = 1 (packed in slot 1 with _pendingOwner) + ( + GOVERNANCE_ADDRESS, + B256::new(GOVERNANCE_NEXT_PROPOSAL_ID_SLOT), + U256::from_be_bytes(GOVERNANCE_NEXT_PROPOSAL_ID_VALUE), + ), + // ── GovernanceConfig E2E overrides (testnet-only) ── + // minVotingThreshold = 1 + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_MIN_THRESHOLD), + U256::from_limbs([GOV_CONFIG_MIN_THRESHOLD as u64, 0, 0, 0]), + ), + // requiredProposerStake = 1 + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_PROPOSER_STAKE), + U256::from_limbs([GOV_CONFIG_PROPOSER_STAKE as u64, 0, 0, 0]), + ), + // votingDurationMicros = 10_000_000 (10s) + ( + GOVERNANCE_CONFIG_ADDRESS, + B256::new(GOV_CONFIG_SLOT_VOTING_DURATION), + U256::from_limbs([GOV_CONFIG_VOTING_DURATION, 0, 0, 0]), + ), + // NOTE: No StakingConfig storage patches needed — storage gap pattern preserves + // the v1.2.0 slot layout, so _initialized, _pendingConfig, and hasPendingConfig + // remain at their original slot positions. +]; + +// ── HardforkUpgrades impl ─────────────────────────────────────────────────────── + +/// Delta hardfork descriptor. +#[derive(Debug)] +pub struct DeltaHardfork; + +impl HardforkUpgrades for DeltaHardfork { + fn name(&self) -> &'static str { + "Delta" + } + fn system_upgrades(&self) -> &'static [BytecodeUpgrade] { + DELTA_SYSTEM_UPGRADES + } + fn extra_upgrades(&self) -> &'static [BytecodeUpgrade] { + &[] + } + fn storage_patches(&self) -> &'static [StoragePatch] { + DELTA_STORAGE_PATCHES + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs index c0a7bf2c0..5e384a5ed 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_hardfork_test.rs @@ -7,8 +7,11 @@ //! It pushes blocks through the MockConsensus/PipeExecLayerApi pipeline //! and verifies that the hardfork dispatch infrastructure correctly parses //! and activates hardforks at the configured block numbers. +//! +//! **Gamma hardfork**: system contract bytecodes + StakePool upgrades + ReentrancyGuard init +//! **Delta hardfork**: 4 contract bytecodes + Governance owner + StakingConfig migration -use alloy_primitives::{address, Address, B256, U256}; +use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types_eth::TransactionRequest; use gravity_api_types::{ config_storage::{BlockNumber, ConfigStorage, OnChainConfig}, @@ -20,9 +23,13 @@ use reth_cli_commands::{launcher::FnLauncher, NodeCommand}; use reth_cli_runner::CliRunner; use reth_db::DatabaseEnv; use reth_ethereum_cli::chainspec::EthereumChainSpecParser; -use reth_ethereum_forks::Hardforks; use reth_evm_ethereum::hardfork::{ - delta::{GOVERNANCE_ADDRESS, GOVERNANCE_OWNER, GOVERNANCE_OWNER_SLOT}, + delta::{ + DELTA_SYSTEM_UPGRADES, GOVERNANCE_ADDRESS, GOVERNANCE_CONFIG_ADDRESS, GOVERNANCE_OWNER, + GOVERNANCE_OWNER_SLOT, GOV_CONFIG_MIN_THRESHOLD, GOV_CONFIG_PROPOSER_STAKE, + GOV_CONFIG_SLOT_MIN_THRESHOLD, GOV_CONFIG_SLOT_PROPOSER_STAKE, + GOV_CONFIG_SLOT_VOTING_DURATION, GOV_CONFIG_VOTING_DURATION, STAKING_CONFIG_ADDRESS, + }, gamma::{ GAMMA_SYSTEM_UPGRADES, REENTRANCY_GUARD_NOT_ENTERED, REENTRANCY_GUARD_SLOT, STAKEPOOL_ADDRESSES, STAKEPOOL_BYTECODE, @@ -103,13 +110,13 @@ where .try_into() .unwrap(); println!( - "[hardfork_test] latest_block_number={latest_block_number}, epoch={epoch}, gammaBlock={GAMMA_BLOCK}" + "[hardfork_test] latest_block_number={latest_block_number}, epoch={epoch}, gammaBlock={GAMMA_BLOCK}, deltaBlock={DELTA_BLOCK}" ); tokio::time::sleep(Duration::from_secs(3)).await; - // Push blocks past the hardfork boundary - let target_block = GAMMA_BLOCK + 30; + // Push blocks past all hardfork boundaries + let target_block = DELTA_BLOCK + 30; for block_number in latest_block_number + 1..=target_block { let block_id = mock_block_id(block_number); let parent_block_id = mock_block_id(block_number - 1); @@ -159,13 +166,21 @@ where tokio::time::sleep(Duration::from_millis(200)).await; } - println!("[hardfork_test] ✅ Pushed {target_block} blocks past gammaBlock."); + println!("[hardfork_test] ✅ Pushed {target_block} blocks past deltaBlock."); } } -/// Verify that all system contracts have the expected new bytecodes after the hardfork. -fn verify_bytecodes_upgraded(provider: &P) { - println!("[hardfork_test] Verifying system contract bytecodes at block {GAMMA_BLOCK}..."); +// ═══════════════════════════════════════════════════════════════════════════════ +// GAMMA HARDFORK VERIFICATION +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Verify that all system contracts have the expected new bytecodes after the Gamma hardfork. +fn verify_gamma_bytecodes_upgraded(provider: &P) { + if GAMMA_SYSTEM_UPGRADES.is_empty() { + println!("[hardfork_test] ⚠ GAMMA_SYSTEM_UPGRADES is empty (bytecodes stripped), skipping Gamma bytecode verification"); + return; + } + println!("[hardfork_test] Verifying Gamma system contract bytecodes at block {GAMMA_BLOCK}..."); let state = provider .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(GAMMA_BLOCK)) @@ -200,7 +215,7 @@ fn verify_bytecodes_upgraded(provider: &P) { assert!(all_upgraded, "Not all system contracts were upgraded at gammaBlock!"); println!( - "[hardfork_test] ✅ All {} system contract bytecodes verified!", + "[hardfork_test] ✅ All {} Gamma system contract bytecodes verified!", GAMMA_SYSTEM_UPGRADES.len() ); @@ -240,8 +255,12 @@ fn verify_bytecodes_upgraded(provider: &P) { } } -/// Also verify bytecodes were NOT yet upgraded before the hardfork block. -fn verify_bytecodes_not_upgraded_before(provider: &P) { +/// Verify bytecodes were NOT yet upgraded before the Gamma hardfork block. +fn verify_gamma_bytecodes_not_upgraded_before(provider: &P) { + if GAMMA_SYSTEM_UPGRADES.is_empty() { + println!("[hardfork_test] ⚠ GAMMA_SYSTEM_UPGRADES is empty (bytecodes stripped), skipping pre-Gamma verification"); + return; + } println!("[hardfork_test] Verifying bytecodes are OLD before gammaBlock..."); let pre_block = GAMMA_BLOCK - 1; @@ -297,6 +316,237 @@ fn verify_bytecodes_not_upgraded_before(provider: &P) { } } +// ═══════════════════════════════════════════════════════════════════════════════ +// DELTA HARDFORK VERIFICATION +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Verify that all 4 system contracts have new bytecodes after the Delta hardfork. +fn verify_delta_bytecodes_upgraded(provider: &P) { + println!("[hardfork_test] Verifying Delta system contract bytecodes at block {DELTA_BLOCK}..."); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) + .expect("Failed to get state provider for delta hardfork block"); + + let mut all_upgraded = true; + for (addr, expected_bytecode) in DELTA_SYSTEM_UPGRADES { + match state.account_code(addr) { + Ok(Some(code)) => { + let code_bytes = code.original_bytes(); + if code_bytes.as_ref() == *expected_bytecode { + println!( + "[hardfork_test] ✅ Delta {addr}: bytecode matches ({}B)", + code_bytes.len() + ); + } else { + println!( + "[hardfork_test] ❌ Delta {addr}: MISMATCH got={}B expected={}B", + code_bytes.len(), + expected_bytecode.len() + ); + all_upgraded = false; + } + } + Ok(None) => { + println!("[hardfork_test] ❌ Delta {addr}: no code found"); + all_upgraded = false; + } + Err(e) => { + println!("[hardfork_test] ❌ Delta {addr}: error: {e:?}"); + all_upgraded = false; + } + } + } + + assert!(all_upgraded, "Not all Delta system contracts were upgraded at deltaBlock!"); + println!( + "[hardfork_test] ✅ All {} Delta system contract bytecodes verified!", + DELTA_SYSTEM_UPGRADES.len() + ); +} + +/// Verify Delta bytecodes were NOT yet upgraded before the Delta hardfork block. +fn verify_delta_bytecodes_not_upgraded_before(provider: &P) { + println!("[hardfork_test] Verifying Delta bytecodes are OLD before deltaBlock..."); + + let pre_block = DELTA_BLOCK - 1; + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(pre_block)) + .expect("Failed to get state provider for pre-delta block"); + + // Check first Delta contract (StakingConfig) as smoke test + let (addr, expected_new) = &DELTA_SYSTEM_UPGRADES[0]; + match state.account_code(addr) { + Ok(Some(code)) => { + let code_bytes = code.original_bytes(); + // Before deltaBlock, bytecode may have been upgraded by gammaBlock already, + // but it should NOT match the delta bytecodes + assert_ne!( + code_bytes.as_ref(), + *expected_new, + "Delta bytecode at {addr} should be OLD before deltaBlock" + ); + println!( + "[hardfork_test] ✅ {addr} at block {pre_block}: bytecode differs from Delta target ({}B vs {}B)", + code_bytes.len(), + expected_new.len() + ); + } + Ok(None) => { + println!( + "[hardfork_test] ⚠ {addr} at block {pre_block}: no code (unexpected for StakingConfig)" + ); + } + Err(e) => { + panic!("[hardfork_test] Failed to fetch code before delta hardfork: {e:?}"); + } + } +} + +/// Verify that the Governance contract owner was set by the Delta hardfork. +fn verify_governance_owner_set(provider: &P) { + println!("[hardfork_test] Verifying Governance owner storage at block {DELTA_BLOCK}..."); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) + .expect("Failed to get state provider for delta hardfork block"); + + let owner_slot = alloy_primitives::B256::from(GOVERNANCE_OWNER_SLOT); + let owner_value = state + .storage(GOVERNANCE_ADDRESS, owner_slot) + .expect("Failed to read Governance owner storage"); + let expected_value = U256::from_be_bytes(GOVERNANCE_OWNER.into_word().0); + assert_eq!( + owner_value, + Some(expected_value), + "Governance owner should be set to {GOVERNANCE_OWNER} after deltaBlock" + ); + println!("[hardfork_test] ✅ Governance owner at block {DELTA_BLOCK}: {GOVERNANCE_OWNER}"); + + // Also verify owner was NOT set before delta block + let pre_state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK - 1)) + .expect("Failed to get state provider for pre-delta block"); + let pre_owner = pre_state + .storage(GOVERNANCE_ADDRESS, owner_slot) + .expect("Failed to read pre-delta Governance owner"); + assert_ne!( + pre_owner, + Some(expected_value), + "Governance owner should NOT be set before deltaBlock" + ); + println!( + "[hardfork_test] ✅ Governance owner at block {}: not yet set (as expected)", + DELTA_BLOCK - 1 + ); +} + +/// Verify GovernanceConfig E2E overrides were applied at deltaBlock. +fn verify_governance_config_overrides(provider: &P) { + println!("[hardfork_test] Verifying GovernanceConfig overrides at block {DELTA_BLOCK}..."); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) + .expect("Failed to get state provider for delta hardfork block"); + + // minVotingThreshold (slot 0) = 1 + let threshold = state + .storage(GOVERNANCE_CONFIG_ADDRESS, B256::from(GOV_CONFIG_SLOT_MIN_THRESHOLD)) + .expect("Failed to read GovernanceConfig minVotingThreshold"); + assert_eq!( + threshold, + Some(U256::from(GOV_CONFIG_MIN_THRESHOLD)), + "GovernanceConfig.minVotingThreshold should be {GOV_CONFIG_MIN_THRESHOLD}" + ); + println!("[hardfork_test] ✅ GovernanceConfig.minVotingThreshold = {GOV_CONFIG_MIN_THRESHOLD}"); + + // requiredProposerStake (slot 1) = 1 + let stake = state + .storage(GOVERNANCE_CONFIG_ADDRESS, B256::from(GOV_CONFIG_SLOT_PROPOSER_STAKE)) + .expect("Failed to read GovernanceConfig requiredProposerStake"); + assert_eq!( + stake, + Some(U256::from(GOV_CONFIG_PROPOSER_STAKE)), + "GovernanceConfig.requiredProposerStake should be {GOV_CONFIG_PROPOSER_STAKE}" + ); + println!( + "[hardfork_test] ✅ GovernanceConfig.requiredProposerStake = {GOV_CONFIG_PROPOSER_STAKE}" + ); + + // votingDurationMicros (slot 2) = 10_000_000 + let duration = state + .storage(GOVERNANCE_CONFIG_ADDRESS, B256::from(GOV_CONFIG_SLOT_VOTING_DURATION)) + .expect("Failed to read GovernanceConfig votingDurationMicros"); + assert_eq!( + duration, + Some(U256::from(GOV_CONFIG_VOTING_DURATION)), + "GovernanceConfig.votingDurationMicros should be {GOV_CONFIG_VOTING_DURATION}" + ); + println!( + "[hardfork_test] ✅ GovernanceConfig.votingDurationMicros = {GOV_CONFIG_VOTING_DURATION}" + ); +} + +/// Verify StakingConfig storage is preserved after Delta hardfork (gap pattern). +/// With the storage gap approach, slot positions are unchanged from v1.2.0. +fn verify_staking_config_preserved(provider: &P) { + println!( + "[hardfork_test] Verifying StakingConfig storage preservation at block {DELTA_BLOCK}..." + ); + + let state = provider + .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) + .expect("Failed to get state provider for delta hardfork block"); + + // Verify minimumStake (slot 0) is preserved (not cleared) + let slot_0 = B256::ZERO; + let min_stake = + state.storage(STAKING_CONFIG_ADDRESS, slot_0).expect("Failed to read StakingConfig slot 0"); + assert!( + min_stake.map_or(false, |v| v > U256::ZERO), + "StakingConfig slot 0 (minimumStake) should be preserved, got {:?}", + min_stake + ); + println!("[hardfork_test] ✅ StakingConfig slot 0 (minimumStake): preserved ({:?})", min_stake); + + // Verify slot 1 (lockup|unbonding packed) is preserved + let slot_1 = { + let mut s = [0u8; 32]; + s[31] = 1; + B256::new(s) + }; + let slot_1_value = + state.storage(STAKING_CONFIG_ADDRESS, slot_1).expect("Failed to read StakingConfig slot 1"); + assert!( + slot_1_value.map_or(false, |v| v > U256::ZERO), + "StakingConfig slot 1 (lockup|unbonding) should be preserved, got {:?}", + slot_1_value + ); + println!( + "[hardfork_test] ✅ StakingConfig slot 1 (lockup|unbonding): preserved ({:#066x})", + slot_1_value.unwrap() + ); + + // Verify _initialized (slot 3 with gap pattern) is still true + let slot_3 = { + let mut s = [0u8; 32]; + s[31] = 3; + B256::new(s) + }; + let initialized = + state.storage(STAKING_CONFIG_ADDRESS, slot_3).expect("Failed to read StakingConfig slot 3"); + assert!( + initialized.map_or(false, |v| v > U256::ZERO), + "StakingConfig slot 3 (_initialized) should be true, got {:?}", + initialized + ); + println!("[hardfork_test] ✅ StakingConfig slot 3 (_initialized): true"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PIPELINE +// ═══════════════════════════════════════════════════════════════════════════════ + async fn run_pipe( builder: WithLaunchContext, ChainSpec>>, ) -> eyre::Result<()> { @@ -334,6 +584,7 @@ async fn run_pipe( ); println!("[hardfork_test] ✅ ChainSpec correctly parsed gammaBlock={GAMMA_BLOCK}"); + // Verify deltaBlock is parsed correctly assert!( chain_spec .gravity_hardforks() @@ -375,14 +626,22 @@ async fn run_pipe( tx.send(ExecutionArgs { block_number_to_block_id: BTreeMap::new() }).unwrap(); - // Run consensus — push blocks past gammaBlock + // Run consensus — push blocks past all hardfork boundaries let consensus = MockConsensus::new(pipeline_api); consensus.run(latest_block_number).await; - // After all blocks are pushed, verify bytecodes - verify_bytecodes_not_upgraded_before(&provider); - verify_bytecodes_upgraded(&provider); + // ── Gamma hardfork verification ── + verify_gamma_bytecodes_not_upgraded_before(&provider); + verify_gamma_bytecodes_upgraded(&provider); + + // ── Delta hardfork verification ── + verify_delta_bytecodes_not_upgraded_before(&provider); + verify_delta_bytecodes_upgraded(&provider); verify_governance_owner_set(&provider); + verify_governance_config_overrides(&provider); + verify_staking_config_preserved(&provider); + + println!("[hardfork_test] ✅ All hardfork verifications passed!"); Ok(()) } @@ -432,41 +691,3 @@ fn test_gamma_hardfork() { // Give background threads time to exit cleanly std::thread::sleep(Duration::from_secs(2)); } - -/// Verify that the Governance contract owner was set by the Delta hardfork. -fn verify_governance_owner_set(provider: &P) { - println!("[hardfork_test] Verifying Governance owner storage at block {DELTA_BLOCK}..."); - - let state = provider - .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK)) - .expect("Failed to get state provider for delta hardfork block"); - - let owner_slot = alloy_primitives::B256::from(GOVERNANCE_OWNER_SLOT); - let owner_value = state - .storage(GOVERNANCE_ADDRESS, owner_slot) - .expect("Failed to read Governance owner storage"); - let expected_value = U256::from_be_bytes(GOVERNANCE_OWNER.into_word().0); - assert_eq!( - owner_value, - Some(expected_value), - "Governance owner should be set to {GOVERNANCE_OWNER} after deltaBlock" - ); - println!("[hardfork_test] ✅ Governance owner at block {DELTA_BLOCK}: {GOVERNANCE_OWNER}"); - - // Also verify owner was NOT set before delta block - let pre_state = provider - .state_by_block_number_or_tag(alloy_eips::BlockNumberOrTag::Number(DELTA_BLOCK - 1)) - .expect("Failed to get state provider for pre-delta block"); - let pre_owner = pre_state - .storage(GOVERNANCE_ADDRESS, owner_slot) - .expect("Failed to read pre-delta Governance owner"); - assert_ne!( - pre_owner, - Some(expected_value), - "Governance owner should NOT be set before deltaBlock" - ); - println!( - "[hardfork_test] ✅ Governance owner at block {}: not yet set (as expected)", - DELTA_BLOCK - 1 - ); -}