diff --git a/Cargo.lock b/Cargo.lock index 42e34befe9..a9a57a88d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1688,6 +1688,7 @@ dependencies = [ name = "casper-storage" version = "5.0.0" dependencies = [ + "alloy-primitives", "anyhow", "assert_matches", "base16", diff --git a/execution_engine_testing/tests/src/test/explorer/faucet.rs b/execution_engine_testing/tests/src/test/explorer/faucet.rs index cdd2035b2c..77a77ae8c5 100644 --- a/execution_engine_testing/tests/src/test/explorer/faucet.rs +++ b/execution_engine_testing/tests/src/test/explorer/faucet.rs @@ -663,14 +663,14 @@ fn faucet_costs() { // This test will fail if execution costs vary. The expected costs should not be updated // without understanding why the cost has changed. If the costs do change, it should be // reflected in the "Costs by Entry Point" section of the faucet crate's README.md. - const EXPECTED_FAUCET_INSTALL_COST: u64 = 118_835_785_065; + const EXPECTED_FAUCET_INSTALL_COST: u64 = 119_810_320_929; const EXPECTED_FAUCET_INSTALL_COST_ALT: u64 = 149_230_872_143; - const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 79_790_440; + const EXPECTED_FAUCET_SET_VARIABLES_COST: u64 = 79_749_265; - const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 2_652_954_573; + const EXPECTED_FAUCET_CALL_BY_INSTALLER_COST: u64 = 2_652_913_398; - const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 2_558_820_996; + const EXPECTED_FAUCET_CALL_BY_USER_COST: u64 = 2_558_746_881; let installer_account = AccountHash::new([1u8; 32]); let user_account: AccountHash = AccountHash::new([2u8; 32]); diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index 2b1b8d9549..d408f5e67e 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -2,11 +2,13 @@ use casper_storage::{ global_state::{error::Error as GlobalStateError, state::StateReader}, + tracking_copy::TrackingCopyExt, TrackingCopy, }; use casper_types::{evm, CLValue, EvmAddr, Key, StoredValue, U512}; use revm::{ database_interface::Database, + interpreter::{Gas, InstructionResult, InterpreterResult}, primitives::{Address, Bytes, StorageKey, StorageValue, B256, U256}, state::{AccountInfo, Bytecode}, }; @@ -46,6 +48,55 @@ where None => Ok(U256::ZERO), } } + + /// Executes the native EIP-4788 `get` operation. + /// + /// The lookup is backed only by the current tracking copy; EVM execution must not depend on + /// locally retained block history. + pub(crate) fn eip4788_get( + &mut self, + input: &[u8], + gas_limit: u64, + reservoir: u64, + ) -> Result { + let revert = || InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::new(), + gas: Gas::new_with_regular_gas_and_reservoir(gas_limit, reservoir), + }; + + // EIP-4788 accepts one uint256 timestamp. Values wider than u64 must revert rather + // than truncate to a colliding ring-buffer timestamp. + if input.len() != 32 || input[..24].iter().any(|byte| *byte != 0) { + return Ok(revert()); + } + + let timestamp = u64::from_be_bytes( + input[24..] + .try_into() + .expect("the final 8 bytes of a 32-byte input have fixed length"), + ); + if timestamp == 0 { + return Ok(revert()); + } + + let Some((stored_timestamp, block_hash)) = + self.tracking_copy.get_eip4788_parent_hash(timestamp)? + else { + return Ok(revert()); + }; + if stored_timestamp != timestamp { + return Ok(revert()); + } + + Ok(InterpreterResult { + result: InstructionResult::Return, + output: Bytes::copy_from_slice(block_hash.as_ref()), + // Native EIP-4788 reads have no interpreted bytecode or SLOAD cost. Retain the call + // frame's reservoir so EIP-8037 accounting remains unchanged. + gas: Gas::new_with_regular_gas_and_reservoir(gas_limit, reservoir), + }) + } } impl Database for CasperDb<'_, R, B> diff --git a/executor/evm/src/precompiles.rs b/executor/evm/src/precompiles.rs index 6d307d9aea..f60676ee2e 100644 --- a/executor/evm/src/precompiles.rs +++ b/executor/evm/src/precompiles.rs @@ -1,15 +1,18 @@ //! Casper EVM precompile provider. -use casper_storage::global_state::{error::Error as GlobalStateError, state::StateReader}; +use casper_storage::{ + eip4788::BEACON_ROOTS_ADDRESS, + global_state::{error::Error as GlobalStateError, state::StateReader}, +}; use casper_types::{Key, StoredValue}; use revm::{ context_interface::{Cfg, ContextTr}, handler::{EthPrecompiles, PrecompileProvider}, - interpreter::{CallInputs, InterpreterResult}, + interpreter::{CallInputs, CallScheme, InterpreterResult}, primitives::{hardfork::SpecId, Address}, }; -use crate::{db::CasperDb, BlockHashProvider}; +use crate::{db::CasperDb, tx, BlockHashProvider}; /// Ethereum precompiles executing with access to Casper-backed state. #[derive(Clone, Debug)] @@ -39,6 +42,21 @@ where context: &mut CTX, inputs: &CallInputs, ) -> Result, String> { + let beacon_roots_address = tx::to_revm_address(BEACON_ROOTS_ADDRESS); + if inputs.target_address == beacon_roots_address + && inputs.bytecode_address == beacon_roots_address + && matches!(inputs.scheme, CallScheme::Call | CallScheme::StaticCall) + { + // Copy the input before borrowing the database mutably. The input may be backed by + // revm's shared memory buffer. + let input = inputs.input.bytes(context); + let result = context + .db_mut() + .eip4788_get(&input, inputs.gas_limit, inputs.reservoir) + .map_err(|error| error.to_string())?; + return Ok(Some(result)); + } + >::run(&mut self.0, context, inputs) } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 35c3caac4f..bb1a42fed3 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -10,15 +10,17 @@ use alloy_eips::{ use alloy_primitives::{keccak256, Address as AlloyAddress, Signature, TxKind, B256, U256}; use casper_executor_evm::{ BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, - EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, SystemCallRequest, EMPTY_CODE_HASH, + EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, }; use casper_storage::{ data_access_layer::{GenesisRequest, GenesisResult}, + eip4788, global_state::{ self, error::Error as GlobalStateError, state::{lmdb::LmdbGlobalStateView, CommitProvider, StateProvider, StateReader}, }, + tracking_copy::TrackingCopyExt, TrackingCopy, }; use casper_types::{ @@ -625,6 +627,16 @@ fn seed_evm_code>( ); } +fn seed_eip4788_parent_hash>( + tracking_copy: &mut TrackingCopy, + timestamp: u64, + parent_hash: BlockHash, +) { + tracking_copy + .set_eip4788_parent_hash(timestamp, parent_hash) + .expect("EIP-4788 tuple should encode"); +} + fn read_evm_nonce>( tracking_copy: &mut TrackingCopy, address: evm::Address, @@ -692,130 +704,166 @@ fn prague_bls12_g1_add_precompile_delegates_to_revm() { } #[test] -fn system_call_updates_eip4788_beacon_roots() { +fn eip4788_native_lookup_bypasses_predeploy_bytecode() { let executor = executor(EvmSpec::Prague); let (mut tracking_copy, _tempdir) = tracking_copy(); + let timestamp = block().timestamp; let root = [0xab; evm::HASH_LENGTH]; - // Install beacon roots predeploy for this executor fixture. + seed_eip4788_parent_hash( + &mut tracking_copy, + timestamp, + BlockHash::new(Digest::from_raw(root)), + ); + // A direct call must use the native lookup even when the installed code + // would revert, proving that the bytecode is not executed. seed_evm_code( &mut tracking_copy, - evm::BEACON_ROOTS_ADDRESS, - evm::BEACON_ROOTS_CODE.to_vec(), + eip4788::BEACON_ROOTS_ADDRESS, + reverting_runtime(), ); - // Execute the EIP-4788 update through revm's system-call path. - let outcome = executor - .execute_system_call( - &mut tracking_copy, - SystemCallRequest { - block: block(), - target: evm::BEACON_ROOTS_ADDRESS, - input: root.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::BEACON_ROOTS_ADDRESS), - word(block().timestamp).to_vec(), + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(timestamp).to_vec(), ); assert_eq!(query.output, root); } #[test] -fn eip4788_unknown_timestamp_reverts() { +fn eip4788_returns_a_matching_zero_parent_hash() { let executor = executor(EvmSpec::Prague); let (mut tracking_copy, _tempdir) = tracking_copy(); - let root = [0xcd; evm::HASH_LENGTH]; + let timestamp = block().timestamp; + let zero_hash = [0; evm::HASH_LENGTH]; - // Install beacon roots predeploy for this executor fixture. + seed_eip4788_parent_hash( + &mut tracking_copy, + timestamp, + BlockHash::new(Digest::from_raw(zero_hash)), + ); seed_evm_code( &mut tracking_copy, - evm::BEACON_ROOTS_ADDRESS, - evm::BEACON_ROOTS_CODE.to_vec(), + eip4788::BEACON_ROOTS_ADDRESS, + reverting_runtime(), ); - // Execute the EIP-4788 update through revm's system-call path. - let system_outcome = executor - .execute_system_call( - &mut tracking_copy, - SystemCallRequest { - block: block(), - target: evm::BEACON_ROOTS_ADDRESS, - input: root.to_vec(), - }, - ) - .expect("EVM system call should execute"); - assert_eq!(system_outcome.status, ExecutionStatus::Success); - - let outcome = executor - .execute( - &mut tracking_copy, - call_request( - evm::Address::ZERO, - Some(evm::BEACON_ROOTS_ADDRESS), - word(block().timestamp + 1).to_vec(), - CasperU256::zero(), - ), - ) - .expect("EVM call should execute"); + let query = execute_call( + &executor, + &mut tracking_copy, + evm::Address::ZERO, + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(timestamp).to_vec(), + ); - assert_eq!(outcome.status, ExecutionStatus::Revert); + assert_eq!(query.output, zero_hash); } #[test] -fn user_call_does_not_update_eip4788_beacon_roots() { +fn eip4788_unknown_and_overwritten_timestamps_revert() { let executor = executor(EvmSpec::Prague); let (mut tracking_copy, _tempdir) = tracking_copy(); - let system_root = [0x11; evm::HASH_LENGTH]; - let user_input = [0x22; evm::HASH_LENGTH]; + let timestamp = block().timestamp; + let replacement_timestamp = timestamp + eip4788::HISTORY_BUFFER_LENGTH; + let replacement_root = [0xcd; evm::HASH_LENGTH]; - // Install beacon roots predeploy for this executor fixture. + seed_eip4788_parent_hash( + &mut tracking_copy, + timestamp, + BlockHash::new(Digest::from_raw([0xab; evm::HASH_LENGTH])), + ); seed_evm_code( &mut tracking_copy, - evm::BEACON_ROOTS_ADDRESS, - evm::BEACON_ROOTS_CODE.to_vec(), + eip4788::BEACON_ROOTS_ADDRESS, + eip4788::BEACON_ROOTS_CODE.to_vec(), ); - // Execute the EIP-4788 update through revm's system-call path. - let system_outcome = executor - .execute_system_call( + let unknown = executor + .execute( &mut tracking_copy, - SystemCallRequest { - block: block(), - target: evm::BEACON_ROOTS_ADDRESS, - input: system_root.to_vec(), - }, + call_request( + evm::Address::ZERO, + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(timestamp + 1).to_vec(), + CasperU256::zero(), + ), ) - .expect("EVM system call should execute"); - assert_eq!(system_outcome.status, ExecutionStatus::Success); + .expect("EVM call should execute"); + assert_eq!(unknown.status, ExecutionStatus::Revert); - let outcome = executor + // Writing a timestamp one full ring ahead overwrites the same Global + // State key. The old timestamp must now revert rather than return the + // new root. + seed_eip4788_parent_hash( + &mut tracking_copy, + replacement_timestamp, + BlockHash::new(Digest::from_raw(replacement_root)), + ); + + let stale = executor .execute( &mut tracking_copy, call_request( - evm::Address::new([3; evm::ADDRESS_LENGTH]), - Some(evm::BEACON_ROOTS_ADDRESS), - user_input.to_vec(), + evm::Address::ZERO, + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(timestamp).to_vec(), CasperU256::zero(), ), ) .expect("EVM call should execute"); - assert_eq!(outcome.status, ExecutionStatus::Revert); + assert_eq!(stale.status, ExecutionStatus::Revert); let query = execute_call( &executor, &mut tracking_copy, evm::Address::ZERO, - Some(evm::BEACON_ROOTS_ADDRESS), - word(block().timestamp).to_vec(), + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(replacement_timestamp).to_vec(), ); - assert_eq!(query.output, system_root); + assert_eq!(query.output, replacement_root); +} + +#[test] +fn eip4788_rejects_invalid_calldata() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let timestamp = block().timestamp; + + seed_eip4788_parent_hash( + &mut tracking_copy, + timestamp, + BlockHash::new(Digest::from_raw([0xab; evm::HASH_LENGTH])), + ); + seed_evm_code( + &mut tracking_copy, + eip4788::BEACON_ROOTS_ADDRESS, + eip4788::BEACON_ROOTS_CODE.to_vec(), + ); + + let mut oversized_timestamp = word(timestamp); + oversized_timestamp[0] = 1; + for input in [ + vec![], + vec![0; 31], + vec![0; 32], + oversized_timestamp.to_vec(), + ] { + let outcome = executor + .execute( + &mut tracking_copy, + call_request( + evm::Address::ZERO, + Some(eip4788::BEACON_ROOTS_ADDRESS), + input, + CasperU256::zero(), + ), + ) + .expect("EVM call should execute"); + assert_eq!(outcome.status, ExecutionStatus::Revert); + } } #[test] diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index abef7d36b6..b8e4a8de3a 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -14,7 +14,6 @@ use casper_executor_evm::{ BlockHashProviderResult as EvmBlockHashProviderResult, CallRequest as EvmExecutorCallRequest, CallValidation as EvmCallValidation, EvmExecutor, ExecuteKind as EvmExecuteKind, ExecuteRequest as EvmExecuteRequest, ExecutionStatus as EvmExecutionStatus, - SystemCallRequest as EvmSystemCallRequest, }; use casper_storage::{ block_store::types::ApprovalsHashes, @@ -92,13 +91,13 @@ fn evm_block_context( } } -fn execute_eip4788_beacon_roots_update( +fn write_eip4788_beacon_roots( scratch_state: &ScratchGlobalState, state_root_hash: Digest, chainspec: &Chainspec, + protocol_version: ProtocolVersion, block_context: EvmBlockContext, parent_hash: BlockHash, - evm_block_hash_provider: &dyn EvmBlockHashProvider, ) -> Result { if !chainspec.evm_config.enabled || chainspec.evm_config.spec < EvmSpec::Prague { return Ok(state_root_hash); @@ -108,33 +107,20 @@ fn execute_eip4788_beacon_roots_update( return Ok(state_root_hash); } - let mut tracking_copy = scratch_state - .tracking_copy(state_root_hash)? - .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(), - }; - let outcome = EvmExecutor::new(chainspec.evm_config) - .execute_system_call_with_block_hash_provider( - &mut tracking_copy, - request, - evm_block_hash_provider, - ) - .map_err(|error| BlockExecutionError::TransactionConversion(error.to_string()))?; - - if !matches!(outcome.status, EvmExecutionStatus::Success) { - return Err(BlockExecutionError::TransactionConversion(format!( - "EIP-4788 beacon roots system call failed with status {:?}", - outcome.status - ))); + match scratch_state.block_global(BlockGlobalRequest::set_eip4788_parent_hash( + state_root_hash, + protocol_version, + block_context.timestamp, + parent_hash, + )) { + BlockGlobalResult::RootNotFound => Err(BlockExecutionError::RootNotFound(state_root_hash)), + BlockGlobalResult::Failure(err) => { + Err(BlockExecutionError::BlockGlobal(format!("{err:?}"))) + } + BlockGlobalResult::Success { + post_state_hash, .. + } => Ok(post_state_hash), } - - let execution_effects = tracking_copy.effects(); - scratch_state - .commit_effects(state_root_hash, execution_effects) - .map_err(BlockExecutionError::Lmdb) } fn evm_precondition_receipt(effective_gas_price: u128) -> EvmReceipt { @@ -755,13 +741,13 @@ pub fn execute_finalized_block( } } - state_root_hash = execute_eip4788_beacon_roots_update( + state_root_hash = write_eip4788_beacon_roots( &scratch_state, state_root_hash, chainspec, + protocol_version, evm_block_context(chainspec, block_height, block_time, &proposer), parent_hash, - evm_block_hash_provider, )?; let transaction_config = &chainspec.transaction_config; @@ -2396,14 +2382,8 @@ 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}; - - fn evm_word(value: u64) -> Vec { - let mut bytes = vec![0u8; evm::HASH_LENGTH]; - bytes[24..].copy_from_slice(&value.to_be_bytes()); - bytes - } + use casper_storage::{global_state::state, tracking_copy::TrackingCopyExt}; + use casper_types::{EvmConfig, DEFAULT_WEI_PER_MOTE}; #[test] fn should_not_raise_evm_min_cost_above_converted_fee() { @@ -2430,7 +2410,7 @@ mod tests { } #[test] - fn eip4788_hook_updates_beacon_roots_without_transactions() { + fn eip4788_hook_writes_beacon_roots_without_transactions() { let chainspec = Chainspec { evm_config: EvmConfig { enabled: true, @@ -2442,60 +2422,30 @@ mod tests { }, ..Default::default() }; - let (global_state, state_root_hash, _tempdir) = state::lmdb::make_temporary_global_state([ - ( - Key::Evm(EvmAddr::CodeHash(evm::BEACON_ROOTS_ADDRESS)), - StoredValue::CLValue( - CLValue::from_t(evm::beacon_roots_code_hash()) - .expect("code hash should encode"), - ), - ), - ( - Key::Evm(EvmAddr::ByteCode(evm::beacon_roots_code_hash())), - StoredValue::ByteCode(ByteCode::new( - ByteCodeKind::EvmPrague, - evm::BEACON_ROOTS_CODE.to_vec(), - )), - ), - ]); + let (global_state, state_root_hash, _tempdir) = + state::lmdb::make_temporary_global_state([]); 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([0x44; 32])); - let updated_state_root_hash = execute_eip4788_beacon_roots_update( + let updated_state_root_hash = write_eip4788_beacon_roots( &scratch_state, state_root_hash, &chainspec, + ProtocolVersion::V1_0_0, block_context.clone(), parent_hash, - &StaticEvmBlockHashProvider::default(), ) .expect("EIP-4788 hook should succeed"); - let mut tracking_copy = scratch_state + let 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::BEACON_ROOTS_ADDRESS), - value: casper_types::U256::from(0u8), - input: evm_word(block_time.value() / 1_000), - gas_limit: 5_000_000, - gas_price: 0, - nonce: 0, - validation: EvmCallValidation::UncheckedSimulation, - }), - }, - ) - .expect("EVM call should execute"); + let entry = tracking_copy + .get_eip4788_parent_hash(block_context.timestamp) + .expect("EIP-4788 beacon root should be readable"); - assert_eq!(outcome.status, EvmExecutionStatus::Success); - assert_eq!(outcome.output, parent_hash.as_ref()); + assert_eq!(entry, Some((block_context.timestamp, parent_hash))); } } diff --git a/node/src/reactor/main_reactor/tests/network_general.rs b/node/src/reactor/main_reactor/tests/network_general.rs index 5d3e2fc5a8..640cda3f03 100644 --- a/node/src/reactor/main_reactor/tests/network_general.rs +++ b/node/src/reactor/main_reactor/tests/network_general.rs @@ -456,7 +456,7 @@ async fn network_should_recover_from_stall() { } // Ensure all nodes progress until block 3 is marked complete. - fixture.run_until_block_height(3, TEN_SECS).await; + fixture.run_until_block_height(3, ONE_MIN).await; } #[tokio::test] diff --git a/smart_contracts/contracts/explorer/faucet/README.md b/smart_contracts/contracts/explorer/faucet/README.md index 9bdfdad11d..4ffc74d253 100644 --- a/smart_contracts/contracts/explorer/faucet/README.md +++ b/smart_contracts/contracts/explorer/faucet/README.md @@ -35,7 +35,7 @@ If you try to invoke the contract before these variables are set, then you'll ge | feature | cost | | ------------------------ | ----------------- | -| faucet install | `118_835_785_065` | -| faucet set variables | `79_790_440` | -| faucet call by installer | `2_652_954_573` | -| faucet call by user | `2_558_820_996` | +| faucet install | `119_810_320_929` | +| faucet set variables | `79_749_265` | +| faucet call by installer | `2_652_913_398` | +| faucet call by user | `2_558_746_881` | diff --git a/storage/Cargo.toml b/storage/Cargo.toml index 39b141f0ea..8e1de74053 100644 --- a/storage/Cargo.toml +++ b/storage/Cargo.toml @@ -32,6 +32,7 @@ rand = "0.8.3" rand_chacha = "0.3.0" itertools = "0.10.5" parking_lot = "0.12.1" +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["sha3-keccak"] } [dev-dependencies] assert_matches = "1.3.0" diff --git a/storage/src/data_access_layer/block_global.rs b/storage/src/data_access_layer/block_global.rs index a8dc1c7298..9c7e293605 100644 --- a/storage/src/data_access_layer/block_global.rs +++ b/storage/src/data_access_layer/block_global.rs @@ -1,5 +1,5 @@ use crate::tracking_copy::TrackingCopyError; -use casper_types::{execution::Effects, BlockTime, Digest, ProtocolVersion}; +use casper_types::{execution::Effects, BlockHash, BlockTime, Digest, ProtocolVersion}; use std::fmt::{Display, Formatter}; use thiserror::Error; @@ -14,6 +14,13 @@ pub enum BlockGlobalKind { ProtocolVersion(ProtocolVersion), /// Addressable entity flag. AddressableEntity(bool), + /// EIP-4788 parent block hash record. + Eip4788ParentHash { + /// EVM block timestamp in seconds. + timestamp_secs: u64, + /// Parent block hash associated with the timestamp. + parent_hash: BlockHash, + }, } impl Default for BlockGlobalKind { @@ -69,6 +76,24 @@ impl BlockGlobalRequest { } } + /// Returns an EIP-4788 parent block hash setting request. + pub fn set_eip4788_parent_hash( + state_hash: Digest, + protocol_version: ProtocolVersion, + timestamp_secs: u64, + parent_hash: BlockHash, + ) -> Self { + let block_global_kind = BlockGlobalKind::Eip4788ParentHash { + timestamp_secs, + parent_hash, + }; + BlockGlobalRequest { + state_hash, + protocol_version, + block_global_kind, + } + } + /// Returns state hash. pub fn state_hash(&self) -> Digest { self.state_hash diff --git a/storage/src/eip4788.rs b/storage/src/eip4788.rs new file mode 100644 index 0000000000..aabce9568d --- /dev/null +++ b/storage/src/eip4788.rs @@ -0,0 +1,185 @@ +//! Storage support for the EIP-4788 beacon block roots predeploy. + +use alloy_primitives::keccak256; +use casper_types::{evm, BlockGlobalAddr, BlockHash, CLValue, CLValueError, Digest, Key}; + +/// EIP-4788 beacon roots contract address. +pub const BEACON_ROOTS_ADDRESS: evm::Address = evm::Address::new([ + 0x00, 0x0f, 0x3d, 0xf6, 0xd7, 0x32, 0x80, 0x7e, 0xf1, 0x31, 0x9f, 0xb7, 0xb8, 0xbb, 0x85, 0x22, + 0xd0, 0xbe, 0xac, 0x02, +]); + +/// Number of timestamp and block-root slots maintained by EIP-4788. +pub const HISTORY_BUFFER_LENGTH: u64 = 8_191; + +/// Prague EIP-4788 beacon roots runtime bytecode. +pub const BEACON_ROOTS_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, 0x4d, 0x57, 0x60, 0x20, 0x36, 0x14, 0x60, 0x24, + 0x57, 0x5f, 0x5f, 0xfd, 0x5b, 0x5f, 0x35, 0x80, 0x15, 0x60, 0x49, 0x57, 0x62, 0x00, 0x1f, 0xff, + 0x81, 0x06, 0x90, 0x81, 0x54, 0x14, 0x60, 0x3c, 0x57, 0x5f, 0x5f, 0xfd, 0x5b, 0x62, 0x00, 0x1f, + 0xff, 0x01, 0x54, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3, 0x5b, 0x5f, 0x5f, 0xfd, 0x5b, 0x62, 0x00, + 0x1f, 0xff, 0x42, 0x06, 0x42, 0x81, 0x55, 0x5f, 0x35, 0x90, 0x62, 0x00, 0x1f, 0xff, 0x01, 0x55, + 0x00, +]; + +/// Returns the EIP-4788 ring-buffer slot for `timestamp_secs`. +pub const fn slot_for_timestamp(timestamp_secs: u64) -> u64 { + timestamp_secs % HISTORY_BUFFER_LENGTH +} + +/// Returns the Global State key containing the EIP-4788 record for `timestamp_secs`. +pub(crate) fn parent_hash_key(timestamp_secs: u64) -> Key { + Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { + slot: slot_for_timestamp(timestamp_secs), + }) +} + +/// Returns the CLValue used to persist an EIP-4788 timestamp and parent block hash. +pub(crate) fn parent_hash_value( + timestamp_secs: u64, + parent_hash: BlockHash, +) -> Result { + CLValue::from_t((timestamp_secs, Digest::from(parent_hash))) +} + +/// Returns the Keccak-256 code hash for [`BEACON_ROOTS_CODE`]. +pub fn beacon_roots_code_hash() -> evm::Hash { + let digest = keccak256(BEACON_ROOTS_CODE); + let mut hash = [0u8; evm::HASH_LENGTH]; + hash.copy_from_slice(digest.as_slice()); + evm::Hash::new(hash) +} + +#[cfg(test)] +mod tests { + use casper_types::{ByteCode, ByteCodeKind, Digest, StoredValue}; + + use super::*; + use crate::{ + global_state::state::{self, lmdb::LmdbGlobalStateView, StateProvider as _}, + tracking_copy::{TrackingCopy, TrackingCopyError, TrackingCopyExt}, + }; + + fn tracking_copy( + initial_data: impl IntoIterator, + ) -> (TrackingCopy, impl Send) { + let (global_state, root_hash, tempdir) = + state::lmdb::make_temporary_global_state(initial_data); + let reader = global_state + .checkout(root_hash) + .expect("checkout should not fail") + .expect("root should exist"); + (TrackingCopy::new(reader, 5, false), tempdir) + } + + #[test] + fn constants_match_eip4788() { + assert_eq!( + BEACON_ROOTS_ADDRESS.to_hex_string(), + "000f3df6d732807ef1319fb7b8bb8522d0beac02" + ); + assert_eq!(HISTORY_BUFFER_LENGTH, 8_191); + assert_eq!(BEACON_ROOTS_CODE.len(), 97); + assert!(!beacon_roots_code_hash().is_zero()); + } + + #[test] + fn slot_is_derived_from_timestamp() { + assert_eq!(slot_for_timestamp(0), 0); + assert_eq!(slot_for_timestamp(HISTORY_BUFFER_LENGTH - 1), 8_190); + assert_eq!(slot_for_timestamp(HISTORY_BUFFER_LENGTH), 0); + } + + #[test] + fn tracking_copy_ext_reads_parent_hash_tuple() { + let timestamp = 42; + let parent_hash = BlockHash::new(Digest::from([7; Digest::LENGTH])); + let value = parent_hash_value(timestamp, parent_hash).expect("tuple should encode"); + let (tracking_copy, _tempdir) = + tracking_copy([(parent_hash_key(timestamp), StoredValue::CLValue(value))]); + + assert_eq!( + tracking_copy + .get_eip4788_parent_hash(timestamp) + .expect("read should succeed"), + Some((timestamp, parent_hash)) + ); + } + + #[test] + fn tracking_copy_ext_sets_parent_hash_tuple() { + let timestamp = 42; + let parent_hash = BlockHash::new(Digest::from([7; Digest::LENGTH])); + let (mut tracking_copy, _tempdir) = tracking_copy([]); + + tracking_copy + .set_eip4788_parent_hash(timestamp, parent_hash) + .expect("tuple should encode"); + + let stored_value = tracking_copy + .read(&parent_hash_key(timestamp)) + .expect("read should succeed") + .expect("tuple should exist"); + let StoredValue::CLValue(cl_value) = stored_value else { + panic!("EIP-4788 tuple should be stored as a CLValue"); + }; + assert_eq!( + cl_value + .into_t::<(u64, Digest)>() + .expect("tuple should decode"), + (timestamp, Digest::from(parent_hash)) + ); + } + + #[test] + fn latest_entry_replaces_the_same_slot_after_a_full_ring() { + let (mut tracking_copy, _tempdir) = tracking_copy([]); + let replacement_timestamp = HISTORY_BUFFER_LENGTH + 1; + let mut replacement_raw_hash = [0; Digest::LENGTH]; + replacement_raw_hash[..8].copy_from_slice(&replacement_timestamp.to_le_bytes()); + let replacement_hash = BlockHash::new(Digest::from(replacement_raw_hash)); + + for timestamp in 1..=replacement_timestamp { + let mut raw_hash = [0; Digest::LENGTH]; + raw_hash[..8].copy_from_slice(×tamp.to_le_bytes()); + let parent_hash = BlockHash::new(Digest::from(raw_hash)); + tracking_copy + .set_eip4788_parent_hash(timestamp, parent_hash) + .expect("tuple should encode"); + } + + assert_eq!( + tracking_copy + .get_eip4788_parent_hash(1) + .expect("read should succeed"), + Some((replacement_timestamp, replacement_hash)) + ); + } + + #[test] + fn returns_none_when_slot_is_absent() { + let (tracking_copy, _tempdir) = tracking_copy([]); + + assert_eq!( + tracking_copy + .get_eip4788_parent_hash(42) + .expect("read should succeed"), + None + ); + } + + #[test] + fn rejects_unexpected_value_type() { + let timestamp = 42; + let (tracking_copy, _tempdir) = tracking_copy([( + parent_hash_key(timestamp), + StoredValue::ByteCode(ByteCode::new(ByteCodeKind::V1CasperWasm, vec![])), + )]); + + assert!(matches!( + tracking_copy.get_eip4788_parent_hash(timestamp), + Err(TrackingCopyError::UnexpectedStoredValueVariant) + )); + } +} diff --git a/storage/src/global_state/state/lmdb.rs b/storage/src/global_state/state/lmdb.rs index 89f84b6689..3cc4a070a7 100644 --- a/storage/src/global_state/state/lmdb.rs +++ b/storage/src/global_state/state/lmdb.rs @@ -539,9 +539,17 @@ pub fn make_temporary_global_state( #[cfg(test)] mod tests { - use casper_types::{account::AccountHash, execution::TransformKindV2, CLValue, Digest}; + use casper_types::{ + account::AccountHash, execution::TransformKindV2, BlockHash, CLValue, Digest, + ProtocolVersion, + }; - use crate::global_state::state::scratch::tests::TestPair; + use crate::{ + data_access_layer::{BlockGlobalRequest, BlockGlobalResult}, + eip4788, + global_state::state::{scratch::tests::TestPair, CommitProvider as _, StateProvider as _}, + tracking_copy::TrackingCopyExt, + }; use super::*; @@ -648,4 +656,48 @@ mod tests { original_checkout.read(&test_pairs_updated[2].key).unwrap() ); } + + #[test] + fn block_global_writes_and_overwrites_eip4788_parent_hash() { + let timestamp = 42; + let replacement_timestamp = timestamp + eip4788::HISTORY_BUFFER_LENGTH; + let initial_parent_hash = BlockHash::new(Digest::from([1; Digest::LENGTH])); + let replacement_parent_hash = BlockHash::new(Digest::from([2; Digest::LENGTH])); + let (state, root_hash, _tempdir) = make_temporary_global_state([]); + + let post_state_hash = match state.block_global(BlockGlobalRequest::set_eip4788_parent_hash( + root_hash, + ProtocolVersion::V1_0_0, + timestamp, + initial_parent_hash, + )) { + BlockGlobalResult::Success { + post_state_hash, .. + } => post_state_hash, + result => panic!("unexpected block-global result: {:?}", result), + }; + + let post_state_hash = match state.block_global(BlockGlobalRequest::set_eip4788_parent_hash( + post_state_hash, + ProtocolVersion::V1_0_0, + replacement_timestamp, + replacement_parent_hash, + )) { + BlockGlobalResult::Success { + post_state_hash, .. + } => post_state_hash, + result => panic!("unexpected block-global result: {:?}", result), + }; + + let tracking_copy = state + .tracking_copy(post_state_hash) + .expect("tracking copy should be available") + .expect("post-state root should exist"); + assert_eq!( + tracking_copy + .get_eip4788_parent_hash(timestamp) + .expect("read should succeed"), + Some((replacement_timestamp, replacement_parent_hash)) + ); + } } diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index d17850bd59..d2b591445f 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -633,6 +633,17 @@ pub trait CommitProvider: StateProvider { StoredValue::CLValue(cl_value), ); } + BlockGlobalKind::Eip4788ParentHash { + timestamp_secs, + parent_hash, + } => { + let mut tracking_copy = tc.borrow_mut(); + if let Err(error) = + tracking_copy.set_eip4788_parent_hash(timestamp_secs, parent_hash) + { + return BlockGlobalResult::Failure(error); + } + } } let effects = tc.borrow_mut().effects(); diff --git a/storage/src/lib.rs b/storage/src/lib.rs index fc242f4336..e3622d3549 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -14,6 +14,8 @@ pub mod address_generator; pub mod block_store; /// Data access layer logic. pub mod data_access_layer; +/// EIP-4788 beacon roots storage support. +pub mod eip4788; /// Global state logic. pub mod global_state; /// Storage layer logic. diff --git a/storage/src/system/evm.rs b/storage/src/system/evm.rs index f7438d874e..b21afa74bd 100644 --- a/storage/src/system/evm.rs +++ b/storage/src/system/evm.rs @@ -7,6 +7,7 @@ use casper_types::{ use thiserror::Error; use crate::{ + eip4788, global_state::{error::Error as GlobalStateError, state::StateReader}, tracking_copy::{TrackingCopy, TrackingCopyError}, }; @@ -70,23 +71,23 @@ where } fn beacon_roots_code_hash_key() -> Key { - Key::Evm(EvmAddr::CodeHash(evm::BEACON_ROOTS_ADDRESS)) + Key::Evm(EvmAddr::CodeHash(eip4788::BEACON_ROOTS_ADDRESS)) } fn beacon_roots_byte_code_key() -> Key { - Key::Evm(EvmAddr::ByteCode(evm::beacon_roots_code_hash())) + Key::Evm(EvmAddr::ByteCode(eip4788::beacon_roots_code_hash())) } fn beacon_roots_code_hash_value() -> Result { Ok(StoredValue::CLValue(CLValue::from_t( - evm::beacon_roots_code_hash(), + eip4788::beacon_roots_code_hash(), )?)) } fn beacon_roots_byte_code_value() -> StoredValue { StoredValue::ByteCode(ByteCode::new( ByteCodeKind::EvmPrague, - evm::BEACON_ROOTS_CODE.to_vec(), + eip4788::BEACON_ROOTS_CODE.to_vec(), )) } @@ -108,7 +109,7 @@ where R: StateReader, { let key = beacon_roots_code_hash_key(); - let expected = evm::beacon_roots_code_hash(); + let expected = eip4788::beacon_roots_code_hash(); match tracking_copy.read(&key)? { None => { tracking_copy.write(key, beacon_roots_code_hash_value()?); @@ -148,7 +149,7 @@ where } Some(StoredValue::ByteCode(byte_code)) => { if byte_code.kind() == ByteCodeKind::EvmPrague - && byte_code.bytes() == evm::BEACON_ROOTS_CODE + && byte_code.bytes() == eip4788::BEACON_ROOTS_CODE { return Ok(()); } @@ -311,7 +312,7 @@ mod tests { let (mut tracking_copy, _tempdir) = tracking_copy([( beacon_roots_code_hash_key(), StoredValue::CLValue( - CLValue::from_t(Key::Evm(EvmAddr::Account(evm::BEACON_ROOTS_ADDRESS))) + CLValue::from_t(Key::Evm(EvmAddr::Account(eip4788::BEACON_ROOTS_ADDRESS))) .expect("key should encode"), ), )]); diff --git a/storage/src/tracking_copy/ext.rs b/storage/src/tracking_copy/ext.rs index 41ee058ad2..9f80ab0826 100644 --- a/storage/src/tracking_copy/ext.rs +++ b/storage/src/tracking_copy/ext.rs @@ -8,6 +8,7 @@ use crate::{ data_access_layer::balance::{ AvailableBalanceChecker, BalanceHolds, BalanceHoldsWithProof, ProcessingHoldBalanceHandling, }, + eip4788, global_state::{error::Error as GlobalStateError, state::StateReader}, tracking_copy::{TrackingCopy, TrackingCopyEntityExt, TrackingCopyError}, KeyPrefix, @@ -26,9 +27,10 @@ use casper_types::{ }, MINT, }, - BlockGlobalAddr, BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, CLValue, ChecksumRegistry, - Contract, EntityAddr, EntryPoints, HashAddr, HoldBalanceHandling, HoldsEpoch, Key, Motes, - Package, StoredValue, StoredValueTypeMismatch, SystemHashRegistry, URef, URefAddr, U512, + BlockGlobalAddr, BlockHash, BlockTime, ByteCode, ByteCodeAddr, ByteCodeHash, CLValue, + ChecksumRegistry, Contract, Digest, EntityAddr, EntryPoints, HashAddr, HoldBalanceHandling, + HoldsEpoch, Key, Motes, Package, StoredValue, StoredValueTypeMismatch, SystemHashRegistry, + URef, URefAddr, U512, }; /// Higher-level operations on the state via a `TrackingCopy`. @@ -42,6 +44,19 @@ pub trait TrackingCopyExt { /// Returns block time associated with checked out root hash. fn get_block_time(&self) -> Result, Self::Error>; + /// Returns the EIP-4788 timestamp and parent hash for the timestamp's ring-buffer slot. + fn get_eip4788_parent_hash( + &self, + timestamp_secs: u64, + ) -> Result, Self::Error>; + + /// Stores an EIP-4788 timestamp and parent hash in the timestamp's ring-buffer slot. + fn set_eip4788_parent_hash( + &mut self, + timestamp_secs: u64, + parent_hash: BlockHash, + ) -> Result<(), Self::Error>; + /// Returns balance hold configuration settings for imputed kind of balance hold. fn get_balance_hold_config( &self, @@ -152,6 +167,41 @@ where } } + fn get_eip4788_parent_hash( + &self, + timestamp_secs: u64, + ) -> Result, Self::Error> { + match self.read(&eip4788::parent_hash_key(timestamp_secs))? { + None => Ok(None), + Some(StoredValue::CLValue(cl_value)) => { + let (timestamp, parent_hash): (u64, Digest) = + cl_value.into_t().map_err(Self::Error::CLValue)?; + Ok(Some((timestamp, BlockHash::from(parent_hash)))) + } + Some(unexpected) => { + warn!( + ?unexpected, + "EIP-4788 parent hash stored as unexpected value type" + ); + Err(Self::Error::UnexpectedStoredValueVariant) + } + } + } + + fn set_eip4788_parent_hash( + &mut self, + timestamp_secs: u64, + parent_hash: BlockHash, + ) -> Result<(), Self::Error> { + let value = eip4788::parent_hash_value(timestamp_secs, parent_hash) + .map_err(Self::Error::CLValue)?; + self.write( + eip4788::parent_hash_key(timestamp_secs), + StoredValue::CLValue(value), + ); + Ok(()) + } + fn get_balance_hold_config( &self, hold_kind: BalanceHoldAddrTag, diff --git a/types/src/block/block_global.rs b/types/src/block/block_global.rs index 739c16c2bb..e9ecf135bc 100644 --- a/types/src/block/block_global.rs +++ b/types/src/block/block_global.rs @@ -13,7 +13,7 @@ use crate::{ use core::{ convert::TryFrom, - fmt::{Debug, Display, Formatter}, + fmt::{Display, Formatter}, }; #[cfg(feature = "datasize")] use datasize::DataSize; @@ -29,6 +29,7 @@ const BLOCK_TIME_TAG: u8 = 0; const MESSAGE_COUNT_TAG: u8 = 1; const PROTOCOL_VERSION_TAG: u8 = 2; const ADDRESSABLE_ENTITY_TAG: u8 = 3; +const BLOCK_PARENT_HASH_TAG: u8 = 4; /// Serialization tag for BlockGlobalAddr variants. #[derive( @@ -47,13 +48,15 @@ pub enum BlockGlobalAddrTag { ProtocolVersion = PROTOCOL_VERSION_TAG, /// Tag for addressable entity variant. AddressableEntity = ADDRESSABLE_ENTITY_TAG, + /// Tag for parent block hash variant. + BlockParentHash = BLOCK_PARENT_HASH_TAG, } impl BlockGlobalAddrTag { /// The length in bytes of a [`BlockGlobalAddrTag`]. pub const BLOCK_GLOBAL_ADDR_TAG_LENGTH: usize = 1; - /// Attempts to map `BalanceHoldAddrTag` from a u8. + /// Attempts to map a `u8` to a `BlockGlobalAddrTag`. pub fn try_from_u8(value: u8) -> Option { // TryFrom requires std, so doing this instead. if value == BLOCK_TIME_TAG { @@ -68,6 +71,9 @@ impl BlockGlobalAddrTag { if value == ADDRESSABLE_ENTITY_TAG { return Some(BlockGlobalAddrTag::AddressableEntity); } + if value == BLOCK_PARENT_HASH_TAG { + return Some(BlockGlobalAddrTag::BlockParentHash); + } None } } @@ -79,6 +85,7 @@ impl Display for BlockGlobalAddrTag { BlockGlobalAddrTag::MessageCount => MESSAGE_COUNT_TAG, BlockGlobalAddrTag::ProtocolVersion => PROTOCOL_VERSION_TAG, BlockGlobalAddrTag::AddressableEntity => ADDRESSABLE_ENTITY_TAG, + BlockGlobalAddrTag::BlockParentHash => BLOCK_PARENT_HASH_TAG, }; write!(f, "{}", base16::encode_lower(&[tag])) } @@ -115,7 +122,9 @@ impl FromBytes for BlockGlobalAddrTag { /// Address for singleton values associated to specific block. These are values which are /// calculated or set during the execution of a block such as the block timestamp, or the /// total count of messages emitted during the execution of the block, and so on. -#[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Default)] +#[derive( + Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Default, +)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum BlockGlobalAddr { @@ -128,16 +137,19 @@ pub enum BlockGlobalAddr { ProtocolVersion, /// Addressable entity. AddressableEntity, + /// Parent block hash at a slot in a block-global ring buffer. + BlockParentHash { + /// Slot in the ring buffer. + slot: u64, + }, } impl BlockGlobalAddr { - /// The length in bytes of a [`BlockGlobalAddr`]. + /// The serialized length of a tag-only [`BlockGlobalAddr`]. pub const BLOCK_GLOBAL_ADDR_LENGTH: usize = BlockGlobalAddrTag::BLOCK_GLOBAL_ADDR_TAG_LENGTH; - /// How long is be the serialized value for this instance. - pub fn serialized_length(&self) -> usize { - Self::BLOCK_GLOBAL_ADDR_LENGTH - } + /// The serialized length of a [`BlockGlobalAddr::BlockParentHash`]. + pub const BLOCK_PARENT_HASH_ADDR_LENGTH: usize = 32; /// Returns the tag of this instance. pub fn tag(&self) -> BlockGlobalAddrTag { @@ -146,6 +158,7 @@ impl BlockGlobalAddr { BlockGlobalAddr::BlockTime => BlockGlobalAddrTag::BlockTime, BlockGlobalAddr::ProtocolVersion => BlockGlobalAddrTag::ProtocolVersion, BlockGlobalAddr::AddressableEntity => BlockGlobalAddrTag::AddressableEntity, + BlockGlobalAddr::BlockParentHash { .. } => BlockGlobalAddrTag::BlockParentHash, } } @@ -160,6 +173,11 @@ impl BlockGlobalAddr { BlockGlobalAddr::AddressableEntity => { base16::encode_lower(&ADDRESSABLE_ENTITY_TAG.to_le_bytes()) } + BlockGlobalAddr::BlockParentHash { slot } => { + let mut formatted = base16::encode_lower(&BLOCK_PARENT_HASH_TAG.to_le_bytes()); + formatted.push_str(&base16::encode_lower(&slot.to_be_bytes())); + formatted + } } } @@ -181,12 +199,18 @@ impl BlockGlobalAddr { FromStrError::BlockGlobal("failed to parse block global addr tag".to_string()) })?; - // if more tags are added, extend the below logic to handle every case. match tag { BlockGlobalAddrTag::BlockTime => Ok(BlockGlobalAddr::BlockTime), BlockGlobalAddrTag::MessageCount => Ok(BlockGlobalAddr::MessageCount), BlockGlobalAddrTag::ProtocolVersion => Ok(BlockGlobalAddr::ProtocolVersion), BlockGlobalAddrTag::AddressableEntity => Ok(BlockGlobalAddr::AddressableEntity), + BlockGlobalAddrTag::BlockParentHash => { + let slot_bytes = <[u8; core::mem::size_of::()]>::try_from(&bytes[1..]) + .map_err(|error| FromStrError::BlockGlobal(error.to_string()))?; + Ok(BlockGlobalAddr::BlockParentHash { + slot: u64::from_be_bytes(slot_bytes), + }) + } } } } @@ -194,18 +218,41 @@ impl BlockGlobalAddr { impl ToBytes for BlockGlobalAddr { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut buffer = bytesrepr::allocate_buffer(self)?; - buffer.push(self.tag() as u8); + self.write_bytes(&mut buffer)?; Ok(buffer) } fn serialized_length(&self) -> usize { - self.serialized_length() + match self { + BlockGlobalAddr::BlockParentHash { .. } => Self::BLOCK_PARENT_HASH_ADDR_LENGTH, + BlockGlobalAddr::BlockTime + | BlockGlobalAddr::MessageCount + | BlockGlobalAddr::ProtocolVersion + | BlockGlobalAddr::AddressableEntity => Self::BLOCK_GLOBAL_ADDR_LENGTH, + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + match self { + BlockGlobalAddr::BlockParentHash { slot } => { + let mut bytes = [0u8; Self::BLOCK_PARENT_HASH_ADDR_LENGTH]; + bytes[0] = self.tag() as u8; + bytes[Self::BLOCK_PARENT_HASH_ADDR_LENGTH - core::mem::size_of::()..] + .copy_from_slice(&slot.to_be_bytes()); + writer.extend_from_slice(&bytes); + } + BlockGlobalAddr::BlockTime + | BlockGlobalAddr::MessageCount + | BlockGlobalAddr::ProtocolVersion + | BlockGlobalAddr::AddressableEntity => writer.push(self.tag() as u8), + } + Ok(()) } } impl FromBytes for BlockGlobalAddr { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder): (u8, &[u8]) = FromBytes::from_bytes(bytes)?; + let (tag, remainder) = u8::from_bytes(bytes)?; match tag { tag if tag == BlockGlobalAddrTag::BlockTime as u8 => { Ok((BlockGlobalAddr::BlockTime, remainder)) @@ -219,6 +266,25 @@ impl FromBytes for BlockGlobalAddr { tag if tag == BlockGlobalAddrTag::AddressableEntity as u8 => { Ok((BlockGlobalAddr::AddressableEntity, remainder)) } + tag if tag == BlockGlobalAddrTag::BlockParentHash as u8 => { + if bytes.len() < Self::BLOCK_PARENT_HASH_ADDR_LENGTH { + return Err(bytesrepr::Error::EarlyEndOfStream); + } + let (serialized_addr, remainder) = + bytes.split_at(Self::BLOCK_PARENT_HASH_ADDR_LENGTH); + let slot_offset = Self::BLOCK_PARENT_HASH_ADDR_LENGTH - core::mem::size_of::(); + if serialized_addr[BlockGlobalAddrTag::BLOCK_GLOBAL_ADDR_TAG_LENGTH..slot_offset] + .iter() + .any(|byte| *byte != 0) + { + return Err(bytesrepr::Error::Formatting); + } + let slot_bytes = + <[u8; core::mem::size_of::()]>::try_from(&serialized_addr[slot_offset..]) + .map_err(|_| bytesrepr::Error::Formatting)?; + let slot = u64::from_be_bytes(slot_bytes); + Ok((BlockGlobalAddr::BlockParentHash { slot }, remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } @@ -245,18 +311,12 @@ impl TryFrom for BlockGlobalAddr { impl Display for BlockGlobalAddr { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - let tag = self.tag(); - write!(f, "{}", tag,) - } -} - -impl Debug for BlockGlobalAddr { - fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { match self { - BlockGlobalAddr::BlockTime => write!(f, "BlockTime",), - BlockGlobalAddr::MessageCount => write!(f, "MessageCount",), - BlockGlobalAddr::ProtocolVersion => write!(f, "ProtocolVersion"), - BlockGlobalAddr::AddressableEntity => write!(f, "AddressableEntity"), + BlockGlobalAddr::BlockParentHash { slot } => write!(f, "{}-{}", self.tag(), slot), + BlockGlobalAddr::BlockTime => write!(f, "{}", self.tag()), + BlockGlobalAddr::MessageCount => write!(f, "{}", self.tag()), + BlockGlobalAddr::ProtocolVersion => write!(f, "{}", self.tag()), + BlockGlobalAddr::AddressableEntity => write!(f, "{}", self.tag()), } } } @@ -264,11 +324,12 @@ impl Debug for BlockGlobalAddr { #[cfg(any(feature = "testing", test))] impl Distribution for Standard { fn sample(&self, rng: &mut R) -> BlockGlobalAddr { - match rng.gen_range(BLOCK_TIME_TAG..=ADDRESSABLE_ENTITY_TAG) { + match rng.gen_range(BLOCK_TIME_TAG..=BLOCK_PARENT_HASH_TAG) { BLOCK_TIME_TAG => BlockGlobalAddr::BlockTime, MESSAGE_COUNT_TAG => BlockGlobalAddr::MessageCount, PROTOCOL_VERSION_TAG => BlockGlobalAddr::ProtocolVersion, ADDRESSABLE_ENTITY_TAG => BlockGlobalAddr::AddressableEntity, + BLOCK_PARENT_HASH_TAG => BlockGlobalAddr::BlockParentHash { slot: rng.gen() }, _ => unreachable!(), } } @@ -276,7 +337,10 @@ impl Distribution for Standard { #[cfg(test)] mod tests { - use crate::{block::block_global::BlockGlobalAddr, bytesrepr}; + use crate::{ + block::block_global::BlockGlobalAddr, + bytesrepr::{self, FromBytes, ToBytes}, + }; #[test] fn serialization_roundtrip() { @@ -288,6 +352,71 @@ mod tests { bytesrepr::test_serialization_roundtrip(&addr); let addr = BlockGlobalAddr::AddressableEntity; bytesrepr::test_serialization_roundtrip(&addr); + let addr = BlockGlobalAddr::BlockParentHash { + slot: 0x0102_0304_0506_0708, + }; + bytesrepr::test_serialization_roundtrip(&addr); + } + + #[test] + fn legacy_variants_keep_tag_only_serialization() { + let variants = [ + (BlockGlobalAddr::BlockTime, 0), + (BlockGlobalAddr::MessageCount, 1), + (BlockGlobalAddr::ProtocolVersion, 2), + (BlockGlobalAddr::AddressableEntity, 3), + ]; + + for (addr, tag) in variants { + assert_eq!(addr.to_bytes().unwrap(), vec![tag]); + assert_eq!(addr.serialized_length(), 1); + + let bytes = [tag, 0xaa, 0xbb]; + let (decoded, remainder) = BlockGlobalAddr::from_bytes(&bytes).unwrap(); + assert_eq!(decoded, addr); + assert_eq!(remainder, &[0xaa, 0xbb]); + } + } + + #[test] + fn block_parent_hash_has_canonical_bytes_and_formatted_string() { + let addr = BlockGlobalAddr::BlockParentHash { + slot: 0x0102_0304_0506_0708, + }; + + assert_eq!(addr.to_bytes().unwrap(), { + let mut expected = vec![0x04]; + expected.extend_from_slice(&[0u8; 23]); + expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes()); + expected + }); + assert_eq!(addr.to_formatted_string(), "040102030405060708"); + assert_eq!( + BlockGlobalAddr::from_formatted_string(&addr.to_formatted_string()).unwrap(), + addr + ); + } + + #[test] + fn block_parent_hash_rejects_invalid_fixed_width_payloads() { + let addr = BlockGlobalAddr::BlockParentHash { slot: u64::MAX }; + let mut bytes = addr.to_bytes().unwrap(); + + assert_eq!(bytes.len(), BlockGlobalAddr::BLOCK_PARENT_HASH_ADDR_LENGTH); + assert_eq!(&bytes[24..], &u64::MAX.to_be_bytes()); + + bytes[1] = 1; + assert_eq!( + BlockGlobalAddr::from_bytes(&bytes).unwrap_err(), + bytesrepr::Error::Formatting + ); + + let encoded = addr.to_bytes().unwrap(); + let truncated = &encoded[..31]; + assert_eq!( + BlockGlobalAddr::from_bytes(truncated).unwrap_err(), + bytesrepr::Error::EarlyEndOfStream + ); } } @@ -299,7 +428,7 @@ mod prop_test_gas { proptest! { #[test] - fn test_variant_gas(addr in gens::balance_hold_addr_arb()) { + fn serialization_roundtrip(addr in gens::block_global_addr_arb()) { bytesrepr::test_serialization_roundtrip(&addr); } } diff --git a/types/src/evm.rs b/types/src/evm.rs index e22cf8bc2d..7de738e962 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -8,7 +8,6 @@ mod account; mod address; mod config; -mod eip4788; mod evm_addr; mod hash; mod receipt; @@ -17,7 +16,6 @@ mod transaction; pub use account::{deterministic_purse, StorageAddr, EMPTY_CODE_HASH}; pub use address::{Address, ADDRESS_LENGTH}; -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}; pub use topic::Topic; diff --git a/types/src/evm/eip4788.rs b/types/src/evm/eip4788.rs deleted file mode 100644 index f84b1a4146..0000000000 --- a/types/src/evm/eip4788.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! EIP-4788 beacon roots contract constants. - -use alloy_primitives::keccak256; - -use crate::evm; - -/// EIP-4788 beacon roots contract address. -pub const BEACON_ROOTS_ADDRESS: evm::Address = evm::Address::new([ - 0x00, 0x0f, 0x3d, 0xf6, 0xd7, 0x32, 0x80, 0x7e, 0xf1, 0x31, 0x9f, 0xb7, 0xb8, 0xbb, 0x85, 0x22, - 0xd0, 0xbe, 0xac, 0x02, -]); - -/// Prague EIP-4788 beacon roots runtime bytecode. -pub const BEACON_ROOTS_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, 0x4d, 0x57, 0x60, 0x20, 0x36, 0x14, 0x60, 0x24, - 0x57, 0x5f, 0x5f, 0xfd, 0x5b, 0x5f, 0x35, 0x80, 0x15, 0x60, 0x49, 0x57, 0x62, 0x00, 0x1f, 0xff, - 0x81, 0x06, 0x90, 0x81, 0x54, 0x14, 0x60, 0x3c, 0x57, 0x5f, 0x5f, 0xfd, 0x5b, 0x62, 0x00, 0x1f, - 0xff, 0x01, 0x54, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3, 0x5b, 0x5f, 0x5f, 0xfd, 0x5b, 0x62, 0x00, - 0x1f, 0xff, 0x42, 0x06, 0x42, 0x81, 0x55, 0x5f, 0x35, 0x90, 0x62, 0x00, 0x1f, 0xff, 0x01, 0x55, - 0x00, -]; - -/// Returns the Keccak-256 code hash for [`BEACON_ROOTS_CODE`]. -pub fn beacon_roots_code_hash() -> evm::Hash { - let digest = keccak256(BEACON_ROOTS_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_eip4788() { - assert_eq!( - BEACON_ROOTS_ADDRESS.to_hex_string(), - "000f3df6d732807ef1319fb7b8bb8522d0beac02" - ); - assert_eq!(BEACON_ROOTS_CODE.len(), 97); - } -} diff --git a/types/src/gens.rs b/types/src/gens.rs index 302a6bd4ae..2b259e47fc 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -289,8 +289,11 @@ pub fn balance_hold_addr_arb() -> impl Strategy { pub fn block_global_addr_arb() -> impl Strategy { prop_oneof![ - 0 => Just(BlockGlobalAddr::BlockTime), - 1 => Just(BlockGlobalAddr::MessageCount) + Just(BlockGlobalAddr::BlockTime), + Just(BlockGlobalAddr::MessageCount), + Just(BlockGlobalAddr::ProtocolVersion), + Just(BlockGlobalAddr::AddressableEntity), + any::().prop_map(|slot| BlockGlobalAddr::BlockParentHash { slot }), ] } diff --git a/types/src/key.rs b/types/src/key.rs index bd0d320e78..6ea8d4605e 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -82,6 +82,7 @@ const BLOCK_GLOBAL_TIME_PREFIX: &str = "block-time-"; const BLOCK_GLOBAL_MESSAGE_COUNT_PREFIX: &str = "block-message-count-"; const BLOCK_GLOBAL_PROTOCOL_VERSION_PREFIX: &str = "block-protocol-version-"; const BLOCK_GLOBAL_ADDRESSABLE_ENTITY_PREFIX: &str = "block-addressable-entity-"; +const BLOCK_GLOBAL_PARENT_HASH_PREFIX: &str = "block-parent-hash-"; const STATE_PREFIX: &str = "state-"; const REWARDS_HANDLING_PREFIX: &str = "rewards-handling-"; const EVM_ACCOUNT_PREFIX: &str = "evm-account-"; @@ -107,6 +108,7 @@ pub const DICTIONARY_ITEM_KEY_MAX_LENGTH: usize = 128; pub const ADDR_LENGTH: usize = 32; const PADDING_BYTES: [u8; 32] = [0u8; 32]; const BLOCK_GLOBAL_PADDING_BYTES: [u8; 31] = [0u8; 31]; +const BLOCK_GLOBAL_KEY_PAYLOAD_LENGTH: usize = PADDING_BYTES.len(); const KEY_ID_SERIALIZED_LENGTH: usize = 1; // u8 used to determine the ID const KEY_HASH_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; @@ -703,12 +705,16 @@ impl Key { Key::NamedKey(named_key) => { format!("{}", named_key) } + Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { slot }) => { + format!("{BLOCK_GLOBAL_PARENT_HASH_PREFIX}{slot}") + } Key::BlockGlobal(addr) => { let prefix = match addr { BlockGlobalAddr::BlockTime => BLOCK_GLOBAL_TIME_PREFIX, BlockGlobalAddr::MessageCount => BLOCK_GLOBAL_MESSAGE_COUNT_PREFIX, BlockGlobalAddr::ProtocolVersion => BLOCK_GLOBAL_PROTOCOL_VERSION_PREFIX, BlockGlobalAddr::AddressableEntity => BLOCK_GLOBAL_ADDRESSABLE_ENTITY_PREFIX, + BlockGlobalAddr::BlockParentHash { .. } => unreachable!(), }; format!( "{}{}", @@ -1055,6 +1061,13 @@ impl Key { return Ok(BlockGlobalAddr::AddressableEntity.into()); } + if let Some(parent_hash) = input.strip_prefix(BLOCK_GLOBAL_PARENT_HASH_PREFIX) { + let slot = parent_hash + .parse::() + .map_err(|error| FromStrError::BlockGlobal(error.to_string()))?; + return Ok(BlockGlobalAddr::BlockParentHash { slot }.into()); + } + match EntryPointAddr::from_formatted_str(input) { Ok(entry_point_addr) => return Ok(Key::EntryPoint(entry_point_addr)), Err(addressable_entity::FromStrError::InvalidPrefix) => {} @@ -1605,6 +1618,9 @@ impl Display for Key { Key::NamedKey(named_key_addr) => { write!(f, "Key::NamedKey({})", named_key_addr) } + Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { slot }) => { + write!(f, "Key::BlockGlobal({})", slot) + } Key::BlockGlobal(addr) => { write!( f, @@ -1781,11 +1797,7 @@ impl ToBytes for Key { Key::NamedKey(named_key_addr) => { KEY_ID_SERIALIZED_LENGTH + named_key_addr.serialized_length() } - Key::BlockGlobal(addr) => { - KEY_ID_SERIALIZED_LENGTH - + addr.serialized_length() - + BLOCK_GLOBAL_PADDING_BYTES.len() - } + Key::BlockGlobal(_) => KEY_ID_SERIALIZED_LENGTH + BLOCK_GLOBAL_KEY_PAYLOAD_LENGTH, Key::BalanceHold(balance_hold_addr) => { KEY_ID_SERIALIZED_LENGTH + balance_hold_addr.serialized_length() } @@ -1819,7 +1831,15 @@ impl ToBytes for Key { | Key::RewardsHandling => PADDING_BYTES.write_bytes(writer), Key::BlockGlobal(addr) => { addr.write_bytes(writer)?; - BLOCK_GLOBAL_PADDING_BYTES.write_bytes(writer) + match addr { + BlockGlobalAddr::BlockParentHash { .. } => Ok(()), + BlockGlobalAddr::BlockTime + | BlockGlobalAddr::MessageCount + | BlockGlobalAddr::ProtocolVersion + | BlockGlobalAddr::AddressableEntity => { + BLOCK_GLOBAL_PADDING_BYTES.write_bytes(writer) + } + } } Key::BidAddr(bid_addr) => bid_addr.write_bytes(writer), Key::SmartContract(package_addr) => package_addr.write_bytes(writer), @@ -1933,8 +1953,9 @@ impl FromBytes for Key { Ok((Key::NamedKey(named_key_addr), rem)) } KeyTag::BlockGlobal => { - let (addr, rem) = BlockGlobalAddr::from_bytes(remainder)?; - let (_, rem) = <[u8; 31]>::from_bytes(rem)?; // strip padding + let (serialized_addr, rem) = + <[u8; BLOCK_GLOBAL_KEY_PAYLOAD_LENGTH]>::from_bytes(remainder)?; + let (addr, _) = BlockGlobalAddr::from_bytes(&serialized_addr)?; Ok((Key::BlockGlobal(addr), rem)) } KeyTag::BalanceHold => { @@ -2262,6 +2283,9 @@ mod tests { )); const BLOCK_TIME_KEY: Key = Key::BlockGlobal(BlockGlobalAddr::BlockTime); const BLOCK_MESSAGE_COUNT_KEY: Key = Key::BlockGlobal(BlockGlobalAddr::MessageCount); + const BLOCK_PARENT_HASH_KEY: Key = Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { + slot: 0x0102_0304_0506_0708, + }); // const STATE_KEY: Key = Key::State(EntityAddr::new_contract_entity_addr([42; 32])); const BALANCE_HOLD: Key = Key::BalanceHold(BalanceHoldAddr::new_gas([42; 32], BlockTime::new(100))); @@ -2296,6 +2320,7 @@ mod tests { NAMED_KEY, BLOCK_TIME_KEY, BLOCK_MESSAGE_COUNT_KEY, + BLOCK_PARENT_HASH_KEY, BALANCE_HOLD, STATE_KEY, ]; @@ -2489,7 +2514,7 @@ mod tests { format!( "Key::BlockGlobal({}-{})", BlockGlobalAddr::BlockTime, - base16::encode_lower(&BLOCK_GLOBAL_PADDING_BYTES) + base16::encode_lower(&[0u8; 31]) ) ); assert_eq!( @@ -2497,9 +2522,62 @@ mod tests { format!( "Key::BlockGlobal({}-{})", BlockGlobalAddr::MessageCount, - base16::encode_lower(&BLOCK_GLOBAL_PADDING_BYTES) + base16::encode_lower(&[0u8; 31]) ) ); + assert_eq!( + format!("{}", BLOCK_PARENT_HASH_KEY), + "Key::BlockGlobal(72623859790382856)" + ); + } + + #[test] + fn block_parent_hash_key_uses_fixed_width_canonical_bytes() { + let key = BLOCK_PARENT_HASH_KEY; + let mut expected = vec![KeyTag::BlockGlobal as u8, 0x04]; + expected.extend_from_slice(&[0u8; 23]); + expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes()); + + assert_eq!(key.to_bytes().unwrap(), expected); + assert_eq!(key.serialized_length(), 33); + assert_eq!( + key.to_formatted_string(), + "block-parent-hash-72623859790382856" + ); + assert_eq!( + Key::from_formatted_str(&key.to_formatted_string()).unwrap(), + key + ); + + let legacy_keys = [ + (BlockGlobalAddr::BlockTime, 0), + (BlockGlobalAddr::MessageCount, 1), + (BlockGlobalAddr::ProtocolVersion, 2), + (BlockGlobalAddr::AddressableEntity, 3), + ]; + for (addr, tag) in legacy_keys { + let key = Key::BlockGlobal(addr); + let mut dev_bytes = vec![KeyTag::BlockGlobal as u8, tag]; + dev_bytes.extend_from_slice(&[0u8; 31]); + + assert_eq!(key.to_bytes().unwrap(), dev_bytes); + assert_eq!(key.serialized_length(), 33); + + dev_bytes.extend_from_slice(&[0xaa, 0xbb]); + let (decoded, remainder) = Key::from_bytes(&dev_bytes).unwrap(); + assert_eq!(decoded, key); + assert_eq!(remainder, &[0xaa, 0xbb]); + } + } + + #[test] + fn block_parent_hash_key_rejects_non_decimal_formatted_slot() { + let formatted = format!("{}not-a-slot", BLOCK_GLOBAL_PARENT_HASH_PREFIX); + + assert!(matches!( + Key::from_formatted_str(&formatted), + Err(FromStrError::BlockGlobal(_)) + )); } #[test] @@ -2890,6 +2968,9 @@ mod tests { bytesrepr::test_serialization_roundtrip(&MESSAGE_TOPIC_KEY); bytesrepr::test_serialization_roundtrip(&MESSAGE_KEY); bytesrepr::test_serialization_roundtrip(&NAMED_KEY); + bytesrepr::test_serialization_roundtrip(&BLOCK_TIME_KEY); + bytesrepr::test_serialization_roundtrip(&BLOCK_MESSAGE_COUNT_KEY); + bytesrepr::test_serialization_roundtrip(&BLOCK_PARENT_HASH_KEY); bytesrepr::test_serialization_roundtrip(&STATE_KEY); } @@ -2940,6 +3021,9 @@ mod tests { round_trip(&Key::BlockGlobal(BlockGlobalAddr::MessageCount)); round_trip(&Key::BlockGlobal(BlockGlobalAddr::ProtocolVersion)); round_trip(&Key::BlockGlobal(BlockGlobalAddr::AddressableEntity)); + round_trip(&Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { + slot: 0, + })); round_trip(&Key::BalanceHold(BalanceHoldAddr::default())); round_trip(&Key::State(EntityAddr::new_system(zeros))); }