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 000000000..bf8534eb5 Binary files /dev/null and b/crates/ethereum/evm/src/hardfork/bytecodes/delta/Governance.bin differ 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 000000000..0b168996a Binary files /dev/null and b/crates/ethereum/evm/src/hardfork/bytecodes/delta/NativeOracle.bin differ diff --git a/crates/ethereum/evm/src/hardfork/bytecodes/delta/StakingConfig.bin b/crates/ethereum/evm/src/hardfork/bytecodes/delta/StakingConfig.bin new file mode 100644 index 000000000..ebb6721f2 Binary files /dev/null and b/crates/ethereum/evm/src/hardfork/bytecodes/delta/StakingConfig.bin differ diff --git a/crates/ethereum/evm/src/hardfork/bytecodes/delta/ValidatorManagement.bin b/crates/ethereum/evm/src/hardfork/bytecodes/delta/ValidatorManagement.bin new file mode 100644 index 000000000..70dda3b3d Binary files /dev/null and b/crates/ethereum/evm/src/hardfork/bytecodes/delta/ValidatorManagement.bin differ 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 40b1b985b..69a169e9c 100644 --- a/crates/ethereum/evm/src/hardfork/delta.rs +++ b/crates/ethereum/evm/src/hardfork/delta.rs @@ -1,11 +1,173 @@ -//! Delta hardfork: stub (implementation removed). +//! Delta hardfork: upgrade `StakingConfig`, `ValidatorManagement`, `Governance`, `NativeOracle` //! -//! 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. +//! This hardfork upgrades 4 system contracts and applies storage patches: +//! +//! **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 +//! +//! **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}; + +// ── 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"); + +// ── System addresses ──────────────────────────────────────────────────────────── + +/// `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 +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 ────────────────────────────────────────────────── -use super::common::{BytecodeUpgrade, HardforkUpgrades}; +/// 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 (testnet-only) ─────────────────────────────────── + +/// `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; + +// ── 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)] @@ -16,9 +178,12 @@ impl HardforkUpgrades for DeltaHardfork { "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/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..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,7 +23,18 @@ 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::{ + 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, + }, +}; use reth_node_builder::{EngineNodeLauncher, NodeBuilder, WithLaunchContext}; use reth_node_ethereum::{node::EthereumAddOns, EthereumNode}; use reth_pipe_exec_layer_ext_v2::{ @@ -96,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); @@ -152,10 +166,387 @@ 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."); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 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)) + .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 {} Gamma 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:?}"); + } +} + +/// 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; + 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:?}"); + } } } +// ═══════════════════════════════════════════════════════════════════════════════ +// 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<()> { @@ -193,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() @@ -234,11 +626,22 @@ 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 all hardfork boundaries let consensus = MockConsensus::new(pipeline_api); consensus.run(latest_block_number).await; - println!("[hardfork_test] ✅ All blocks pushed successfully. Hardfork framework verified."); + // ── 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(()) }