From 20ed5bf32005e3f2b8978231ee15fd6b685fcab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Fri, 3 Jul 2026 15:25:00 +0200 Subject: [PATCH 1/2] Implement EIP-2935 Prague block-hash history support --- EVM.md | 11 +- executor/evm/tests/executor.rs | 111 +++++++++ .../components/contract_runtime/operations.rs | 155 ++++++++++-- storage/src/system/evm.rs | 228 +++++++++++------- .../genesis/account_contract_installer.rs | 6 +- .../src/system/genesis/entity_installer.rs | 6 +- storage/src/system/protocol_upgrade.rs | 6 +- types/src/evm.rs | 4 + types/src/evm/eip2935.rs | 47 ++++ 9 files changed, 457 insertions(+), 117 deletions(-) create mode 100644 types/src/evm/eip2935.rs diff --git a/EVM.md b/EVM.md index 1305414aeb..ec55702eae 100644 --- a/EVM.md +++ b/EVM.md @@ -63,6 +63,9 @@ Implemented in this workspace: lists passed through to `revm` for Prague execution. - [EIP-4788][eip-4788] beacon roots predeploy and pre-block system-call update. Casper stores the parent Casper block hash as the root value. +- [EIP-2935][eip-2935] block-hash history predeploy and pre-block + system-call update. Casper stores parent Casper block hashes in the + 8191-slot history ring buffer. Implemented in the sidecar workspace for validation: @@ -102,7 +105,7 @@ Ethereum JSON-RPC method names below refer to the Ethereum | --- | --- | --- | | `EvmSpec::Prague` / `revm::SpecId::PRAGUE` | Implemented. | Execution behavior is delegated to `revm`; Casper does not maintain its own EVM interpreter. | | [EIP-2537][eip-2537] BLS12-381 precompiles | Delegated to `revm`. | Expected at `0x0b` through `0x11`, but Casper-owned conformance tests are still needed for gas costs, malformed input, subgroup checks, and failure behavior. | -| [EIP-2935][eip-2935] block-hash history contract | Missing. | Current `BLOCKHASH` uses a recent Casper block-hash provider only. Full compatibility needs the history contract at `0x0000F90827F1C53a10cb7A02335B175320002935`, a pre-block system call, and the 8191-entry ring buffer. | +| [EIP-2935][eip-2935] block-hash history contract | Implemented. | Standard address, bytecode, interface, and system-call update path are present. Casper stores parent Casper block hashes; `BLOCKHASH` opcode semantics remain unchanged. | | [EIP-4788][eip-4788] beacon roots contract | Implemented. | Standard address, bytecode, interface, and system-call update path are present, but `parentBeaconBlockRoot := parent Casper block hash`, not an Ethereum beacon block root. | | [EIP-6110][eip-6110] validator deposit requests | Missing / decision needed. | Ethereum-specific deposit-log-to-request flow. Full support requires [EIP-7685][eip-7685] request construction and commitment. | | [EIP-7002][eip-7002] withdrawal request predeploy | Missing / decision needed. | Contract-visible predeploy at `0x00000961Ef480Eb55e80D19ad83579A64c007002` is absent. Full support requires queue/fee state, post-block extraction, and [EIP-7685][eip-7685] request output. | @@ -128,7 +131,7 @@ Ethereum JSON-RPC method names below refer to the Ethereum | EVM bytecode storage | Implemented. | Runtime bytecode is stored as `ByteCodeKind::EvmPrague`; future bytecode-affecting forks should add new bytecode kinds. | | EVM storage slots | Implemented. | Slots are Casper `U256` values under `Key::Evm(EvmAddr::Storage(..))`; zero writes prune state. | | Logs and receipts | Implemented. | Node stores EVM receipts/logs; sidecar computes Ethereum-style blooms and receipt roots from stored EVM receipts. Blob receipts are absent. | -| `BLOCKHASH` opcode | Partial. | Recent Casper block hashes are available through a node-supplied provider. This is not [EIP-2935][eip-2935] history-contract state. | +| `BLOCKHASH` opcode | Implemented with Casper semantics. | Recent Casper block hashes are available through a node-supplied provider. This remains separate from [EIP-2935][eip-2935] history-contract state. | | `NUMBER`, `TIMESTAMP`, `GASLIMIT`, `BASEFEE` | Implemented. | Timestamp is Casper block time in seconds. Base fee is chainspec-configured and wei-denominated through `wei_per_mote`, not Ethereum's dynamic base-fee adjustment. | | `COINBASE` | Implemented with Casper semantics. | The address is derived from the Casper block proposer public key. | | `CHAINID` | Implemented. | Transaction chain ID is enforced against chainspec `[evm].chain_id`. | @@ -187,8 +190,8 @@ This gives contracts a deterministic consensus-root oracle for Casper. It is not strict Ethereum beacon-chain semantics. The detailed, audited compatibility matrix is in [Current Status](#current-status). -The highest-priority smart-contract-visible gaps after EIP-4788 are -[EIP-2935][eip-2935], request predeploy decisions for [EIP-7002][eip-7002] and +The highest-priority smart-contract-visible gaps after EIP-4788 and +EIP-2935 are request predeploy decisions for [EIP-7002][eip-7002] and [EIP-7251][eip-7251], and explicit Prague conformance coverage for [EIP-2537][eip-2537], [EIP-7623][eip-7623], and [EIP-7702][eip-7702]. diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 5396b84f16..86b8eed20c 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -800,6 +800,117 @@ fn user_call_does_not_update_eip4788_beacon_roots() { assert_eq!(query.output, system_root); } +#[test] +fn system_call_updates_eip2935_block_hash_history() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let parent_hash = [0xab; evm::HASH_LENGTH]; + + seed_evm_code( + &mut tracking_copy, + evm::BLOCK_HASH_HISTORY_ADDRESS, + evm::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + let outcome = executor + .execute_system_call( + &mut tracking_copy, + SystemCallRequest { + block: block(), + target: evm::BLOCK_HASH_HISTORY_ADDRESS, + input: parent_hash.to_vec(), + }, + ) + .expect("EVM system call should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + let query = execute_call( + &executor, + &mut tracking_copy, + evm::Address::ZERO, + Some(evm::BLOCK_HASH_HISTORY_ADDRESS), + word(0).to_vec(), + ); + assert_eq!(query.output, parent_hash); +} + +#[test] +fn eip2935_current_or_future_block_reverts() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_code( + &mut tracking_copy, + evm::BLOCK_HASH_HISTORY_ADDRESS, + evm::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + let outcome = executor + .execute( + &mut tracking_copy, + call_request( + evm::Address::ZERO, + Some(evm::BLOCK_HASH_HISTORY_ADDRESS), + word(block().number).to_vec(), + CasperU256::zero(), + ), + ) + .expect("EVM call should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Revert); +} + +#[test] +fn eip2935_too_old_block_reverts() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_code( + &mut tracking_copy, + evm::BLOCK_HASH_HISTORY_ADDRESS, + evm::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + let mut request = call_request( + evm::Address::ZERO, + Some(evm::BLOCK_HASH_HISTORY_ADDRESS), + word(0).to_vec(), + CasperU256::zero(), + ); + request.block.number = 8_192; + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("EVM call should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Revert); +} + +#[test] +fn eip2935_invalid_calldata_length_reverts() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_code( + &mut tracking_copy, + evm::BLOCK_HASH_HISTORY_ADDRESS, + evm::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + let outcome = executor + .execute( + &mut tracking_copy, + call_request( + evm::Address::ZERO, + Some(evm::BLOCK_HASH_HISTORY_ADDRESS), + vec![0; evm::HASH_LENGTH - 1], + CasperU256::zero(), + ), + ) + .expect("EVM call should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Revert); +} + #[test] fn blockhash_uses_supplied_provider() { let executor = executor(EvmSpec::Prague); diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index abef7d36b6..986b564ea1 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -92,6 +92,12 @@ fn evm_block_context( } } +struct PragueEvmSystemCall { + target: EvmAddress, + input: Vec, + name: &'static str, +} + fn execute_eip4788_beacon_roots_update( scratch_state: &ScratchGlobalState, state_root_hash: Digest, @@ -99,6 +105,50 @@ fn execute_eip4788_beacon_roots_update( block_context: EvmBlockContext, parent_hash: BlockHash, evm_block_hash_provider: &dyn EvmBlockHashProvider, +) -> Result { + execute_prague_evm_system_call( + scratch_state, + state_root_hash, + chainspec, + block_context, + PragueEvmSystemCall { + target: casper_types::evm::BEACON_ROOTS_ADDRESS, + input: parent_hash.as_ref().to_vec(), + name: "EIP-4788 beacon roots", + }, + evm_block_hash_provider, + ) +} + +fn execute_eip2935_block_hash_history_update( + scratch_state: &ScratchGlobalState, + state_root_hash: Digest, + chainspec: &Chainspec, + block_context: EvmBlockContext, + parent_hash: BlockHash, + evm_block_hash_provider: &dyn EvmBlockHashProvider, +) -> Result { + execute_prague_evm_system_call( + scratch_state, + state_root_hash, + chainspec, + block_context, + PragueEvmSystemCall { + target: casper_types::evm::BLOCK_HASH_HISTORY_ADDRESS, + input: parent_hash.as_ref().to_vec(), + name: "EIP-2935 block hash history", + }, + evm_block_hash_provider, + ) +} + +fn execute_prague_evm_system_call( + scratch_state: &ScratchGlobalState, + state_root_hash: Digest, + chainspec: &Chainspec, + block_context: EvmBlockContext, + system_call: PragueEvmSystemCall, + evm_block_hash_provider: &dyn EvmBlockHashProvider, ) -> Result { if !chainspec.evm_config.enabled || chainspec.evm_config.spec < EvmSpec::Prague { return Ok(state_root_hash); @@ -113,8 +163,8 @@ fn execute_eip4788_beacon_roots_update( .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; let request = EvmSystemCallRequest { block: block_context, - target: casper_types::evm::BEACON_ROOTS_ADDRESS, - input: parent_hash.as_ref().to_vec(), + target: system_call.target, + input: system_call.input, }; let outcome = EvmExecutor::new(chainspec.evm_config) .execute_system_call_with_block_hash_provider( @@ -126,8 +176,8 @@ fn execute_eip4788_beacon_roots_update( if !matches!(outcome.status, EvmExecutionStatus::Success) { return Err(BlockExecutionError::TransactionConversion(format!( - "EIP-4788 beacon roots system call failed with status {:?}", - outcome.status + "{} system call failed with status {:?}", + system_call.name, outcome.status ))); } @@ -755,11 +805,20 @@ pub fn execute_finalized_block( } } + let evm_context = evm_block_context(chainspec, block_height, block_time, &proposer); state_root_hash = execute_eip4788_beacon_roots_update( &scratch_state, state_root_hash, chainspec, - evm_block_context(chainspec, block_height, block_time, &proposer), + evm_context.clone(), + parent_hash, + evm_block_hash_provider, + )?; + state_root_hash = execute_eip2935_block_hash_history_update( + &scratch_state, + state_root_hash, + chainspec, + evm_context, parent_hash, evm_block_hash_provider, )?; @@ -2405,6 +2464,20 @@ mod tests { bytes } + fn enabled_prague_evm_chainspec() -> Chainspec { + Chainspec { + evm_config: EvmConfig { + enabled: true, + chain_id: 7, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + wei_per_mote: DEFAULT_WEI_PER_MOTE, + }, + ..Default::default() + } + } + #[test] fn should_not_raise_evm_min_cost_above_converted_fee() { let gas_limit = Gas::new(21_000); @@ -2431,17 +2504,7 @@ mod tests { #[test] fn eip4788_hook_updates_beacon_roots_without_transactions() { - let chainspec = Chainspec { - evm_config: EvmConfig { - enabled: true, - chain_id: 7, - spec: EvmSpec::Prague, - block_gas_limit: 30_000_000, - base_fee: 0, - wei_per_mote: DEFAULT_WEI_PER_MOTE, - }, - ..Default::default() - }; + let chainspec = enabled_prague_evm_chainspec(); let (global_state, state_root_hash, _tempdir) = state::lmdb::make_temporary_global_state([ ( Key::Evm(EvmAddr::CodeHash(evm::BEACON_ROOTS_ADDRESS)), @@ -2498,4 +2561,64 @@ mod tests { assert_eq!(outcome.status, EvmExecutionStatus::Success); assert_eq!(outcome.output, parent_hash.as_ref()); } + + #[test] + fn eip2935_hook_updates_block_hash_history_without_transactions() { + let chainspec = enabled_prague_evm_chainspec(); + let (global_state, state_root_hash, _tempdir) = state::lmdb::make_temporary_global_state([ + ( + Key::Evm(EvmAddr::CodeHash(evm::BLOCK_HASH_HISTORY_ADDRESS)), + StoredValue::CLValue( + CLValue::from_t(evm::block_hash_history_code_hash()) + .expect("code hash should encode"), + ), + ), + ( + Key::Evm(EvmAddr::ByteCode(evm::block_hash_history_code_hash())), + StoredValue::ByteCode(ByteCode::new( + ByteCodeKind::EvmPrague, + evm::BLOCK_HASH_HISTORY_CODE.to_vec(), + )), + ), + ]); + let scratch_state = global_state.create_scratch(); + let block_time = BlockTime::new(2_000); + let block_context = evm_block_context(&chainspec, 1, block_time, &PublicKey::System); + let parent_hash = BlockHash::new(Digest::from_raw([0x55; 32])); + + let updated_state_root_hash = execute_eip2935_block_hash_history_update( + &scratch_state, + state_root_hash, + &chainspec, + block_context.clone(), + parent_hash, + &StaticEvmBlockHashProvider::default(), + ) + .expect("EIP-2935 hook should succeed"); + let mut tracking_copy = scratch_state + .tracking_copy(updated_state_root_hash) + .expect("tracking copy should not fail") + .expect("state root should exist"); + let outcome = EvmExecutor::new(chainspec.evm_config) + .execute( + &mut tracking_copy, + EvmExecuteRequest { + block: block_context, + kind: EvmExecuteKind::Call(EvmExecutorCallRequest { + from: EvmAddress::ZERO, + to: Some(evm::BLOCK_HASH_HISTORY_ADDRESS), + value: casper_types::U256::from(0u8), + input: evm_word(0), + gas_limit: 5_000_000, + gas_price: 0, + nonce: 0, + validation: EvmCallValidation::UncheckedSimulation, + }), + }, + ) + .expect("EVM call should execute"); + + assert_eq!(outcome.status, EvmExecutionStatus::Success); + assert_eq!(outcome.output, parent_hash.as_ref()); + } } diff --git a/storage/src/system/evm.rs b/storage/src/system/evm.rs index f7438d874e..ee3be12c82 100644 --- a/storage/src/system/evm.rs +++ b/storage/src/system/evm.rs @@ -52,66 +52,105 @@ impl From for EvmPredeployError { } } -/// Returns whether EIP-4788 should be installed for the supplied EVM config. -pub(crate) fn should_upsert_eip4788_predeploy(config: &EvmConfig) -> bool { - config.enabled && config.spec >= EvmSpec::Prague +#[derive(Clone, Copy)] +struct EvmPredeploy { + name: &'static str, + address: evm::Address, + code: &'static [u8], + code_hash: evm::Hash, } -/// Idempotently installs the EIP-4788 beacon roots predeploy. -pub(crate) fn upsert_eip4788_predeploy( - tracking_copy: &mut TrackingCopy, -) -> Result<(), EvmPredeployError> -where - R: StateReader, -{ - upsert_beacon_roots_code_hash(tracking_copy)?; - upsert_beacon_roots_byte_code(tracking_copy)?; - Ok(()) -} +impl EvmPredeploy { + fn eip4788() -> Self { + EvmPredeploy { + name: "EIP-4788", + address: evm::BEACON_ROOTS_ADDRESS, + code: evm::BEACON_ROOTS_CODE, + code_hash: evm::beacon_roots_code_hash(), + } + } + + fn eip2935() -> Self { + EvmPredeploy { + name: "EIP-2935", + address: evm::BLOCK_HASH_HISTORY_ADDRESS, + code: evm::BLOCK_HASH_HISTORY_CODE, + code_hash: evm::block_hash_history_code_hash(), + } + } + + fn code_hash_key(self) -> Key { + Key::Evm(EvmAddr::CodeHash(self.address)) + } -fn beacon_roots_code_hash_key() -> Key { - Key::Evm(EvmAddr::CodeHash(evm::BEACON_ROOTS_ADDRESS)) + fn byte_code_key(self) -> Key { + Key::Evm(EvmAddr::ByteCode(self.code_hash)) + } + + fn code_hash_value(self) -> Result { + Ok(StoredValue::CLValue(CLValue::from_t(self.code_hash)?)) + } + + fn byte_code_value(self) -> StoredValue { + StoredValue::ByteCode(ByteCode::new(ByteCodeKind::EvmPrague, self.code.to_vec())) + } } -fn beacon_roots_byte_code_key() -> Key { - Key::Evm(EvmAddr::ByteCode(evm::beacon_roots_code_hash())) +fn prague_predeploys() -> [EvmPredeploy; 2] { + [EvmPredeploy::eip4788(), EvmPredeploy::eip2935()] } -fn beacon_roots_code_hash_value() -> Result { - Ok(StoredValue::CLValue(CLValue::from_t( - evm::beacon_roots_code_hash(), - )?)) +/// Returns whether Prague EVM predeploys should be installed for the supplied EVM config. +pub(crate) fn should_upsert_prague_predeploys(config: &EvmConfig) -> bool { + config.enabled && config.spec == EvmSpec::Prague } -fn beacon_roots_byte_code_value() -> StoredValue { - StoredValue::ByteCode(ByteCode::new( - ByteCodeKind::EvmPrague, - evm::BEACON_ROOTS_CODE.to_vec(), - )) +/// Idempotently installs Prague EVM predeploys. +pub(crate) fn upsert_prague_predeploys( + tracking_copy: &mut TrackingCopy, +) -> Result<(), EvmPredeployError> +where + R: StateReader, +{ + for predeploy in prague_predeploys() { + upsert_code_hash(tracking_copy, predeploy)?; + upsert_byte_code(tracking_copy, predeploy)?; + } + Ok(()) } #[cfg(test)] -fn beacon_roots_predeploy_entries() -> Result, EvmPredeployError> { +fn predeploy_entries( + predeploy: EvmPredeploy, +) -> Result, EvmPredeployError> { Ok(vec![ - ( - beacon_roots_code_hash_key(), - beacon_roots_code_hash_value()?, - ), - (beacon_roots_byte_code_key(), beacon_roots_byte_code_value()), + (predeploy.code_hash_key(), predeploy.code_hash_value()?), + (predeploy.byte_code_key(), predeploy.byte_code_value()), ]) } -fn upsert_beacon_roots_code_hash( +#[cfg(test)] +fn prague_predeploy_entries() -> Result, EvmPredeployError> { + prague_predeploys() + .iter() + .copied() + .map(predeploy_entries) + .collect::, _>>() + .map(|entries| entries.into_iter().flatten().collect()) +} + +fn upsert_code_hash( tracking_copy: &mut TrackingCopy, + predeploy: EvmPredeploy, ) -> Result<(), EvmPredeployError> where R: StateReader, { - let key = beacon_roots_code_hash_key(); - let expected = evm::beacon_roots_code_hash(); + let key = predeploy.code_hash_key(); + let expected = predeploy.code_hash; match tracking_copy.read(&key)? { None => { - tracking_copy.write(key, beacon_roots_code_hash_value()?); + tracking_copy.write(key, predeploy.code_hash_value()?); } Some(StoredValue::CLValue(cl_value)) => { let actual = cl_value.to_t::()?; @@ -119,7 +158,7 @@ where return Ok(()); } if actual == evm::EMPTY_CODE_HASH || actual.is_zero() { - tracking_copy.write(key, beacon_roots_code_hash_value()?); + tracking_copy.write(key, predeploy.code_hash_value()?); return Ok(()); } return Err(EvmPredeployError::ConflictingCodeHash { actual }); @@ -135,27 +174,27 @@ where Ok(()) } -fn upsert_beacon_roots_byte_code( +fn upsert_byte_code( tracking_copy: &mut TrackingCopy, + predeploy: EvmPredeploy, ) -> Result<(), EvmPredeployError> where R: StateReader, { - let key = beacon_roots_byte_code_key(); + let key = predeploy.byte_code_key(); match tracking_copy.read(&key)? { None => { - tracking_copy.write(key, beacon_roots_byte_code_value()); + tracking_copy.write(key, predeploy.byte_code_value()); } Some(StoredValue::ByteCode(byte_code)) => { - if byte_code.kind() == ByteCodeKind::EvmPrague - && byte_code.bytes() == evm::BEACON_ROOTS_CODE - { + if byte_code.kind() == ByteCodeKind::EvmPrague && byte_code.bytes() == predeploy.code { return Ok(()); } return Err(EvmPredeployError::ConflictingByteCode { key: Box::new(key), details: format!( - "expected Prague EIP-4788 bytecode, found kind {} with {} bytes", + "expected Prague {} bytecode, found kind {} with {} bytes", + predeploy.name, byte_code.kind(), byte_code.bytes().len() ), @@ -198,65 +237,76 @@ mod tests { .expect("value should exist") } + fn assert_predeploy_present( + tracking_copy: &mut TrackingCopy, + predeploy: EvmPredeploy, + ) { + assert_eq!( + read(tracking_copy, &predeploy.code_hash_key()), + predeploy + .code_hash_value() + .expect("code hash value should build") + ); + assert_eq!( + read(tracking_copy, &predeploy.byte_code_key()), + predeploy.byte_code_value() + ); + } + #[test] - fn should_upsert_for_enabled_prague_or_later_evm() { - assert!(!should_upsert_eip4788_predeploy(&EvmConfig::default())); + fn should_only_upsert_for_enabled_prague_evm() { + assert!(!should_upsert_prague_predeploys(&EvmConfig::default())); let config = EvmConfig { enabled: true, ..Default::default() }; - assert!(should_upsert_eip4788_predeploy(&config)); + assert!(should_upsert_prague_predeploys(&config)); } #[test] - fn upsert_creates_missing_predeploy() { + fn upsert_creates_missing_predeploys() { let (mut tracking_copy, _tempdir) = tracking_copy([]); - upsert_eip4788_predeploy(&mut tracking_copy).expect("upsert should succeed"); + upsert_prague_predeploys(&mut tracking_copy).expect("upsert should succeed"); - assert_eq!( - read(&mut tracking_copy, &beacon_roots_code_hash_key()), - beacon_roots_code_hash_value().expect("code hash value should build") - ); - assert_eq!( - read(&mut tracking_copy, &beacon_roots_byte_code_key()), - beacon_roots_byte_code_value() - ); + assert_predeploy_present(&mut tracking_copy, EvmPredeploy::eip4788()); + assert_predeploy_present(&mut tracking_copy, EvmPredeploy::eip2935()); } #[test] - fn upsert_repairs_missing_bytecode() { + fn upsert_repairs_missing_eip2935_bytecode() { + let predeploy = EvmPredeploy::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( - beacon_roots_code_hash_key(), - beacon_roots_code_hash_value().expect("code hash value should build"), + predeploy.code_hash_key(), + predeploy + .code_hash_value() + .expect("code hash value should build"), )]); - upsert_eip4788_predeploy(&mut tracking_copy).expect("upsert should succeed"); + upsert_prague_predeploys(&mut tracking_copy).expect("upsert should succeed"); - assert_eq!( - read(&mut tracking_copy, &beacon_roots_byte_code_key()), - beacon_roots_byte_code_value() - ); + assert_predeploy_present(&mut tracking_copy, predeploy); } #[test] - fn upsert_noops_when_predeploy_is_present() { + fn upsert_noops_when_predeploys_are_present() { let (mut tracking_copy, _tempdir) = - tracking_copy(beacon_roots_predeploy_entries().expect("entries should build")); + tracking_copy(prague_predeploy_entries().expect("entries should build")); - upsert_eip4788_predeploy(&mut tracking_copy).expect("upsert should succeed"); + upsert_prague_predeploys(&mut tracking_copy).expect("upsert should succeed"); } #[test] - fn upsert_rejects_conflicting_non_empty_code_hash() { + fn upsert_rejects_conflicting_eip2935_non_empty_code_hash() { + let predeploy = EvmPredeploy::eip2935(); let conflicting_hash = evm::Hash::new([1; evm::HASH_LENGTH]); let (mut tracking_copy, _tempdir) = tracking_copy([( - beacon_roots_code_hash_key(), + predeploy.code_hash_key(), StoredValue::CLValue(CLValue::from_t(conflicting_hash).expect("hash should encode")), )]); - let error = upsert_eip4788_predeploy(&mut tracking_copy) + let error = upsert_prague_predeploys(&mut tracking_copy) .expect_err("conflicting code hash should fail"); assert!(matches!( @@ -268,19 +318,22 @@ mod tests { } #[test] - fn upsert_rejects_conflicting_bytecode() { + fn upsert_rejects_conflicting_eip2935_bytecode() { + let predeploy = EvmPredeploy::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([ ( - beacon_roots_code_hash_key(), - beacon_roots_code_hash_value().expect("code hash value should build"), + predeploy.code_hash_key(), + predeploy + .code_hash_value() + .expect("code hash value should build"), ), ( - beacon_roots_byte_code_key(), + predeploy.byte_code_key(), StoredValue::ByteCode(ByteCode::new(ByteCodeKind::EvmPrague, vec![0xfe])), ), ]); - let error = upsert_eip4788_predeploy(&mut tracking_copy) + let error = upsert_prague_predeploys(&mut tracking_copy) .expect_err("conflicting bytecode should fail"); assert!(matches!( @@ -290,33 +343,32 @@ mod tests { } #[test] - fn upsert_repairs_empty_code_hash() { + fn upsert_repairs_empty_eip2935_code_hash() { + let predeploy = EvmPredeploy::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( - beacon_roots_code_hash_key(), + predeploy.code_hash_key(), StoredValue::CLValue( CLValue::from_t(evm::EMPTY_CODE_HASH).expect("hash should encode"), ), )]); - upsert_eip4788_predeploy(&mut tracking_copy).expect("upsert should succeed"); + upsert_prague_predeploys(&mut tracking_copy).expect("upsert should succeed"); - assert_eq!( - read(&mut tracking_copy, &beacon_roots_code_hash_key()), - beacon_roots_code_hash_value().expect("code hash value should build") - ); + assert_predeploy_present(&mut tracking_copy, predeploy); } #[test] - fn upsert_rejects_unexpected_code_hash_value() { + fn upsert_rejects_unexpected_eip2935_code_hash_value() { + let predeploy = EvmPredeploy::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( - beacon_roots_code_hash_key(), + predeploy.code_hash_key(), StoredValue::CLValue( - CLValue::from_t(Key::Evm(EvmAddr::Account(evm::BEACON_ROOTS_ADDRESS))) + CLValue::from_t(Key::Evm(EvmAddr::Account(evm::BLOCK_HASH_HISTORY_ADDRESS))) .expect("key should encode"), ), )]); - let error = upsert_eip4788_predeploy(&mut tracking_copy) + let error = upsert_prague_predeploys(&mut tracking_copy) .expect_err("invalid code hash value should fail"); assert!(matches!(error, EvmPredeployError::CLValue(_))); diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index a93f3eb7a8..4ecd8cece2 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ global_state::state::StateProvider, system::{ - evm::{should_upsert_eip4788_predeploy, upsert_eip4788_predeploy}, + evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, protocol_upgrade::ProtocolUpgradeError, }, @@ -799,8 +799,8 @@ where } fn create_evm_predeploys(&self) -> Result<(), Box> { - if should_upsert_eip4788_predeploy(self.config.evm_config()) { - upsert_eip4788_predeploy(&mut self.tracking_copy.borrow_mut()) + if should_upsert_prague_predeploys(self.config.evm_config()) { + upsert_prague_predeploys(&mut self.tracking_copy.borrow_mut()) .map_err(|error| GenesisError::EvmPredeploy(error.to_string()))?; } Ok(()) diff --git a/storage/src/system/genesis/entity_installer.rs b/storage/src/system/genesis/entity_installer.rs index b03c5957a6..6236175726 100644 --- a/storage/src/system/genesis/entity_installer.rs +++ b/storage/src/system/genesis/entity_installer.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ global_state::state::StateProvider, system::{ - evm::{should_upsert_eip4788_predeploy, upsert_eip4788_predeploy}, + evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, }, AddressGenerator, TrackingCopy, @@ -868,8 +868,8 @@ where } fn create_evm_predeploys(&self) -> Result<(), Box> { - if should_upsert_eip4788_predeploy(self.config.evm_config()) { - upsert_eip4788_predeploy(&mut self.tracking_copy.borrow_mut()) + if should_upsert_prague_predeploys(self.config.evm_config()) { + upsert_prague_predeploys(&mut self.tracking_copy.borrow_mut()) .map_err(|error| GenesisError::EvmPredeploy(error.to_string()))?; } Ok(()) diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index 7402b2b381..66ef25bcdd 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -11,7 +11,7 @@ use tracing::{debug, error, info, warn}; use crate::{ global_state::state::StateProvider, - system::evm::{should_upsert_eip4788_predeploy, upsert_eip4788_predeploy}, + system::evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, tracking_copy::{AddResult, TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, AddressGenerator, }; @@ -1705,8 +1705,8 @@ where /// Handle EVM predeploy setup. pub fn handle_evm_predeploys(&mut self) -> Result<(), ProtocolUpgradeError> { - if should_upsert_eip4788_predeploy(self.config.evm_config()) { - upsert_eip4788_predeploy(&mut self.tracking_copy) + if should_upsert_prague_predeploys(self.config.evm_config()) { + upsert_prague_predeploys(&mut self.tracking_copy) .map_err(|error| ProtocolUpgradeError::EvmPredeploy(error.to_string()))?; } Ok(()) diff --git a/types/src/evm.rs b/types/src/evm.rs index e22cf8bc2d..c0dd77a5a7 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -8,6 +8,7 @@ mod account; mod address; mod config; +mod eip2935; mod eip4788; mod evm_addr; mod hash; @@ -17,6 +18,9 @@ mod transaction; pub use account::{deterministic_purse, StorageAddr, EMPTY_CODE_HASH}; pub use address::{Address, ADDRESS_LENGTH}; +pub use eip2935::{ + block_hash_history_code_hash, BLOCK_HASH_HISTORY_ADDRESS, BLOCK_HASH_HISTORY_CODE, +}; pub use eip4788::{beacon_roots_code_hash, BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE}; pub use hash::{Hash, HASH_LENGTH}; pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; diff --git a/types/src/evm/eip2935.rs b/types/src/evm/eip2935.rs new file mode 100644 index 0000000000..0171bf3f30 --- /dev/null +++ b/types/src/evm/eip2935.rs @@ -0,0 +1,47 @@ +//! EIP-2935 block hash history contract constants. + +use alloy_primitives::keccak256; + +use crate::evm; + +/// EIP-2935 block hash history contract address. +pub const BLOCK_HASH_HISTORY_ADDRESS: evm::Address = evm::Address::new([ + 0x00, 0x00, 0xf9, 0x08, 0x27, 0xf1, 0xc5, 0x3a, 0x10, 0xcb, 0x7a, 0x02, 0x33, 0x5b, 0x17, 0x53, + 0x20, 0x00, 0x29, 0x35, +]); + +/// Prague EIP-2935 block hash history runtime bytecode. +pub const BLOCK_HASH_HISTORY_CODE: &[u8] = &[ + 0x33, 0x73, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x14, 0x60, 0x46, 0x57, 0x60, 0x20, 0x36, 0x03, 0x60, 0x42, + 0x57, 0x5f, 0x35, 0x60, 0x01, 0x43, 0x03, 0x81, 0x11, 0x60, 0x42, 0x57, 0x61, 0x1f, 0xff, 0x81, + 0x43, 0x03, 0x11, 0x60, 0x42, 0x57, 0x61, 0x1f, 0xff, 0x90, 0x06, 0x54, 0x5f, 0x52, 0x60, 0x20, + 0x5f, 0xf3, 0x5b, 0x5f, 0x5f, 0xfd, 0x5b, 0x5f, 0x35, 0x61, 0x1f, 0xff, 0x60, 0x01, 0x43, 0x03, + 0x06, 0x55, 0x00, +]; + +/// Returns the Keccak-256 code hash for [`BLOCK_HASH_HISTORY_CODE`]. +pub fn block_hash_history_code_hash() -> evm::Hash { + let digest = keccak256(BLOCK_HASH_HISTORY_CODE); + let mut hash = [0u8; evm::HASH_LENGTH]; + hash.copy_from_slice(digest.as_slice()); + evm::Hash::new(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constants_match_eip2935() { + assert_eq!( + BLOCK_HASH_HISTORY_ADDRESS.to_hex_string(), + "0000f90827f1c53a10cb7a02335b175320002935" + ); + assert_eq!(BLOCK_HASH_HISTORY_CODE.len(), 83); + assert_eq!( + block_hash_history_code_hash(), + evm::Hash::new(keccak256(BLOCK_HASH_HISTORY_CODE).0) + ); + } +} From 8a5236b48f2a12a71935abdcf1c3ab45941c7496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 9 Jul 2026 15:05:13 +0200 Subject: [PATCH 2/2] Update EIP-2935 test to run finalized block execution --- .../components/contract_runtime/operations.rs | 141 ++++++++++++++---- 1 file changed, 112 insertions(+), 29 deletions(-) diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 986b564ea1..b7c70fea25 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -2455,8 +2455,16 @@ pub(crate) fn compute_execution_results_checksum<'a>( #[cfg(test)] mod tests { use super::*; - use casper_storage::global_state::state; - use casper_types::{evm, ByteCode, ByteCodeKind, EvmAddr, EvmConfig, DEFAULT_WEI_PER_MOTE}; + use casper_executor_wasm::{ExecutorConfigBuilder, ExecutorKind}; + use casper_storage::{ + data_access_layer::{BlockStore, GenesisRequest, GenesisResult}, + global_state::{state, state::lmdb::LmdbGlobalState}, + }; + use casper_types::{ + evm, ByteCode, ByteCodeKind, ChainspecRegistry, EvmAddr, EvmConfig, GenesisAccount, + GenesisConfig, HoldBalanceHandling, Motes, SecretKey, StorageCosts, SystemConfig, + Timestamp, WasmConfig, DEFAULT_WEI_PER_MOTE, + }; fn evm_word(value: u64) -> Vec { let mut bytes = vec![0u8; evm::HASH_LENGTH]; @@ -2478,6 +2486,71 @@ mod tests { } } + fn prague_evm_genesis( + chainspec: &Chainspec, + ) -> (DataAccessLayer, Digest, impl Send) { + let (global_state, _empty_root_hash, tempdir) = + state::lmdb::make_temporary_global_state([]); + let secret_key = SecretKey::ed25519_from_bytes([1; SecretKey::ED25519_LENGTH]).unwrap(); + let genesis_config = GenesisConfig::new( + vec![GenesisAccount::Account { + public_key: PublicKey::from(&secret_key), + balance: Motes::new(U512::from(1_000_000_000_000u64)), + validator: None, + }], + chainspec.evm_config, + WasmConfig::default(), + SystemConfig::default(), + 10, + 10, + 0, + Default::default(), + 14, + 0, + HoldBalanceHandling::Accrued, + 0, + chainspec.core_config.enable_addressable_entity(), + None, + StorageCosts::default(), + 0, + ); + let genesis_request = GenesisRequest::new( + Digest::hash("eip2935-block-execution-test-genesis"), + ProtocolVersion::V2_0_0, + genesis_config, + ChainspecRegistry::new_with_genesis(b"", b""), + ); + let post_state_hash = match global_state.genesis(genesis_request) { + GenesisResult::Failure(failure) => panic!("failed to run genesis: {failure:?}"), + GenesisResult::Fatal(fatal) => panic!("fatal error while running genesis: {fatal}"), + GenesisResult::Success { + post_state_hash, .. + } => post_state_hash, + }; + let data_access_layer = DataAccessLayer { + block_store: BlockStore::new(), + state: global_state, + max_query_depth: 5, + enable_addressable_entity: chainspec.core_config.enable_addressable_entity(), + }; + (data_access_layer, post_state_hash, tempdir) + } + + fn executor_v2( + chainspec: &Chainspec, + execution_engine_v1: Arc, + ) -> ExecutorV2 { + let executor_config = ExecutorConfigBuilder::default() + .with_memory_limit(chainspec.wasm_config.v2().max_memory()) + .with_executor_kind(ExecutorKind::Compiled) + .with_wasm_config(*chainspec.wasm_config.v2()) + .with_storage_costs(chainspec.storage_costs) + .with_message_limits(chainspec.wasm_config.messages_limits()) + .build() + .expect("executor config should build"); + ExecutorV2::new(executor_config, execution_engine_v1) + } + #[test] fn should_not_raise_evm_min_cost_above_converted_fee() { let gas_limit = Gas::new(21_000); @@ -2565,38 +2638,48 @@ mod tests { #[test] fn eip2935_hook_updates_block_hash_history_without_transactions() { let chainspec = enabled_prague_evm_chainspec(); - let (global_state, state_root_hash, _tempdir) = state::lmdb::make_temporary_global_state([ - ( - Key::Evm(EvmAddr::CodeHash(evm::BLOCK_HASH_HISTORY_ADDRESS)), - StoredValue::CLValue( - CLValue::from_t(evm::block_hash_history_code_hash()) - .expect("code hash should encode"), - ), - ), - ( - Key::Evm(EvmAddr::ByteCode(evm::block_hash_history_code_hash())), - StoredValue::ByteCode(ByteCode::new( - ByteCodeKind::EvmPrague, - evm::BLOCK_HASH_HISTORY_CODE.to_vec(), - )), - ), - ]); - let scratch_state = global_state.create_scratch(); - let block_time = BlockTime::new(2_000); - let block_context = evm_block_context(&chainspec, 1, block_time, &PublicKey::System); + let (data_access_layer, state_root_hash, _tempdir) = prague_evm_genesis(&chainspec); + let execution_engine_v1 = Arc::new(ExecutionEngineV1::default()); + let execution_engine_v2 = executor_v2(&chainspec, Arc::clone(&execution_engine_v1)); + let timestamp = Timestamp::from(2_000); let parent_hash = BlockHash::new(Digest::from_raw([0x55; 32])); + let execution_pre_state = + ExecutionPreState::new(1, state_root_hash, parent_hash, Digest::default()); + let executable_block = ExecutableBlock::from_finalized_block_and_transactions( + types::FinalizedBlock::new( + types::BlockPayload::default(), + None, + timestamp, + EraId::new(1), + 1, + PublicKey::System, + ), + vec![], + ); - let updated_state_root_hash = execute_eip2935_block_hash_history_update( - &scratch_state, - state_root_hash, + let artifacts = execute_finalized_block( + &data_access_layer, + execution_engine_v1.as_ref(), + execution_engine_v2, &chainspec, - block_context.clone(), - parent_hash, + None, + execution_pre_state, &StaticEvmBlockHashProvider::default(), + executable_block, + 0, + 1, + None, + None, ) - .expect("EIP-2935 hook should succeed"); - let mut tracking_copy = scratch_state - .tracking_copy(updated_state_root_hash) + .expect("finalized block execution should succeed"); + let block_context = evm_block_context( + &chainspec, + artifacts.block.height(), + BlockTime::new(timestamp.millis()), + artifacts.block.proposer(), + ); + let mut tracking_copy = data_access_layer + .tracking_copy(*artifacts.block.state_root_hash()) .expect("tracking copy should not fail") .expect("state root should exist"); let outcome = EvmExecutor::new(chainspec.evm_config)