diff --git a/Cargo.lock b/Cargo.lock index 813cfc97b7..10279a025b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,6 +1481,7 @@ dependencies = [ "alloy-primitives", "casper-storage", "casper-types", + "once_cell", "revm", "thiserror 2.0.18", ] diff --git a/EVM.md b/EVM.md index 126bbed393..1b07116c53 100644 --- a/EVM.md +++ b/EVM.md @@ -61,8 +61,11 @@ Implemented in this workspace: creating or funding the corresponding EVM-native purse identity. - [EIP-7702][eip-7702] type `0x04` set-code transactions, with authorization 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-4788][eip-4788] beacon roots predeploy, native pre-block state update, + and native direct-call lookup. Casper stores the parent Casper block hash as + the root value. +- [EIP-2935][eip-2935] block-hash history predeploy and native direct-call + lookup backed by indexed Casper block headers. Implemented in the sidecar workspace for validation: @@ -102,8 +105,8 @@ 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-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-2935][eip-2935] block-hash history contract | Implemented with Casper storage semantics. | The standard address, bytecode, and 8191-block interface are present, but direct calls read indexed LMDB block headers instead of Merkleized contract storage. There is no pre-block system call or gradual ring-buffer fill. | +| [EIP-4788][eip-4788] beacon roots contract | Implemented. | The standard address, bytecode, and interface are present. Casper writes and reads the parent Casper block hash natively through block-global state rather than executing the predeploy bytecode. | | [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. | | [EIP-7251][eip-7251] consolidation request predeploy | Missing / decision needed. | Contract-visible predeploy at `0x0000BBdDc7CE488642fb579F8B00f3a590007251` is absent. Full support has the same request-output dependency as EIP-7002. | @@ -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. | The most recent 256 indexed Casper block hashes are read by height from LMDB. This remains separate from the 8191-block [EIP-2935][eip-2935] interface. | | `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`. | @@ -176,8 +179,9 @@ executor. previous Cancun/Dencun upgrade. It is included in this review because a Prague Ethereum-compatible environment has this contract. In this branch, Casper installs the exact EIP-4788 runtime bytecode at -`0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02`, runs the pre-block system call -before user transactions, and uses: +`0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02`, writes the parent hash natively +before user transactions, and serves direct calls through the Casper +precompile provider. It uses: ```text parentBeaconBlockRoot := parent Casper block hash @@ -187,8 +191,14 @@ 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 +Casper installs the exact EIP-2935 runtime bytecode at +`0x0000F90827F1C53a10cb7A02335B175320002935`. Direct `CALL` and `STATICCALL` +lookups are intercepted natively and read indexed Casper block headers through +the data access layer. Casper does not execute an EIP-2935 block-boundary +system call or populate the standard contract-storage ring. + +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]. @@ -604,15 +614,34 @@ The tracking-copy effects are discarded. ## Block Hashes -The executor exposes `BLOCK_HASH_HISTORY`, mirroring revm's Ethereum -`BLOCKHASH` history window. Node runtime and binary-port EVM calls use that -public executor constant when loading recent block hashes, so `casper-node` -does not need a production dependency on `revm`. - -Block hashes are loaded as Casper `BlockHash` values and converted to revm's -hash type only at the executor API boundary. If a contract requests a future -block, the current block, or a block outside the supported history window, -`BLOCKHASH` returns zero. +The executor exposes `BLOCK_HASH_HISTORY`, mirroring revm's 256-block Ethereum +`BLOCKHASH` window. `CasperDb` uses its data-access-layer handle to open a +short-lived read transaction against the already-open node block store and +look up an indexed Casper block header by height. The stored Casper `BlockHash` +is converted to revm's hash type at that boundary; no recent-hash map is +preloaded by contract runtime or binary port. + +The EIP-2935 predeploy exposes the standard 8191-block lookup window at +`0x0000F90827F1C53a10cb7A02335B175320002935`. `CasperEvmPrecompiles` +intercepts direct `CALL` and `STATICCALL` operations, validates the standard +32-byte block-number input and range, then uses the same indexed-header lookup. +A valid but absent header returns zero. Current, future, too-old, and malformed +requests revert. Block-store errors fail execution. + +The native lookup does not execute the installed runtime bytecode or charge its +instruction and `SLOAD` gas. Normal call and address-access costs still apply, +and the address is not pre-warmed. `DELEGATECALL` and `CALLCODE` continue to +execute the deployed bytecode against their alternate storage context, while +`EXTCODE*` observes the exact standard runtime. + +Casper does not maintain EIP-2935 history in Global State. Retained ancestors, +including blocks predating activation, are visible immediately through LMDB; +there is no gradual 8191-block fill period and no contract-storage proof of the +history. Before executing a finalized block containing an EVM transaction, +contract runtime verifies the full applicable 8191-header range in one +block-store read transaction. Missing history prevents execution and +transitions the node to catch-up. Speculative execution has no preflight and +returns zero for locally missing valid history. ## Chain ID diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index 97c8dda244..b34c4a9f11 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -20,15 +20,16 @@ use casper_execution_engine::engine_state::{ EngineConfig, Error, ExecutionEngineV1, WasmV1Request, WasmV1Result, DEFAULT_MAX_QUERY_DEPTH, }; use casper_storage::{ + block_store::lmdb::LmdbBlockStore, data_access_layer::{ balance::BalanceHandling, AuctionMethod, BalanceIdentifier, BalanceRequest, BalanceResult, BiddingRequest, BiddingResult, BidsRequest, BlockRewardsRequest, BlockRewardsResult, - BlockStore, DataAccessLayer, EraValidatorsRequest, EraValidatorsResult, FeeRequest, - FeeResult, FlushRequest, FlushResult, GenesisRequest, GenesisResult, HandleFeeMode, - HandleFeeRequest, HandleFeeResult, MessageTopicsRequest, MessageTopicsResult, - ProofHandling, ProtocolUpgradeRequest, ProtocolUpgradeResult, PruneRequest, PruneResult, - QueryRequest, QueryResult, RoundSeigniorageRateRequest, RoundSeigniorageRateResult, - StepRequest, StepResult, SystemEntityRegistryPayload, SystemEntityRegistryRequest, + DataAccessLayer, EraValidatorsRequest, EraValidatorsResult, FeeRequest, FeeResult, + FlushRequest, FlushResult, GenesisRequest, GenesisResult, HandleFeeMode, HandleFeeRequest, + HandleFeeResult, MessageTopicsRequest, MessageTopicsResult, ProofHandling, + ProtocolUpgradeRequest, ProtocolUpgradeResult, PruneRequest, PruneResult, QueryRequest, + QueryResult, RoundSeigniorageRateRequest, RoundSeigniorageRateResult, StepRequest, + StepResult, SystemEntityRegistryPayload, SystemEntityRegistryRequest, SystemEntityRegistryResult, SystemEntityRegistrySelector, TotalSupplyRequest, TotalSupplyResult, TransferRequest, TrieRequest, }, @@ -85,6 +86,9 @@ pub(crate) const DEFAULT_LMDB_PAGES: usize = 256_000_000; /// The default value is chosen to be the same as the node itself. pub(crate) const DEFAULT_MAX_READERS: u32 = 512; +/// LMDB map size for the temporary block store used by test builders. +const DEFAULT_BLOCK_STORE_SIZE: usize = 64 * 1024 * 1024; + /// This is appended to the data dir path provided to the `LmdbWasmTestBuilder`". const GLOBAL_STATE_DIR: &str = "global_state"; @@ -316,8 +320,11 @@ impl LmdbWasmTestBuilder { ) .expect("should create LmdbGlobalState"); + let block_store = LmdbBlockStore::new_temporary(DEFAULT_BLOCK_STORE_SIZE) + .expect("should create block store"); + let data_access_layer = Arc::new(DataAccessLayer { - block_store: BlockStore::new(), + block_store, state: global_state, max_query_depth, enable_addressable_entity, @@ -396,8 +403,11 @@ impl LmdbWasmTestBuilder { } }; + let block_store = LmdbBlockStore::new_temporary(DEFAULT_BLOCK_STORE_SIZE) + .expect("should create block store"); + let data_access_layer = Arc::new(DataAccessLayer { - block_store: BlockStore::new(), + block_store, state: global_state, max_query_depth, enable_addressable_entity, diff --git a/executor/evm/Cargo.toml b/executor/evm/Cargo.toml index 4b3fd5dcde..91423ffcde 100644 --- a/executor/evm/Cargo.toml +++ b/executor/evm/Cargo.toml @@ -18,3 +18,4 @@ thiserror = "2" [dev-dependencies] alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak"] } +once_cell = "1" diff --git a/executor/evm/src/block_hash.rs b/executor/evm/src/block_hash.rs deleted file mode 100644 index 975cbade01..0000000000 --- a/executor/evm/src/block_hash.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Block hash provider abstractions for the EVM `BLOCKHASH` opcode. - -use std::sync::Arc; - -use casper_storage::block_store::{ - lmdb::LmdbBlockStore, types::BlockHeight, BlockStoreError, BlockStoreProvider, DataReader, -}; -use casper_types::{BlockHash, BlockHeader}; - -/// Result type returned by block hash providers. -pub type BlockHashProviderResult = core::result::Result; - -/// Errors returned while resolving historical block hashes. -#[derive(Debug, thiserror::Error)] -pub enum BlockHashProviderError { - /// Failed to read from Casper block storage. - #[error(transparent)] - BlockStore(#[from] BlockStoreError), -} - -/// Resolves Casper block hashes by height for the EVM `BLOCKHASH` opcode. -/// -/// The executor applies the EVM availability rules around the provider: current -/// and future block numbers, and block numbers older than 256 blocks, return -/// the zero hash. Providers only need to answer canonical historical heights. -pub trait BlockHashProvider { - /// Returns the block hash for `block_height`, or `None` when unavailable. - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult>; -} - -/// Block hash provider that returns no historical hashes. -#[derive(Clone, Copy, Debug, Default)] -pub struct NoBlockHashProvider; - -impl BlockHashProvider for NoBlockHashProvider { - fn block_hash(&self, _block_height: u64) -> BlockHashProviderResult> { - Ok(None) - } -} - -/// Block hash provider backed by Casper's indexed LMDB block store. -#[derive(Clone, Debug)] -pub struct IndexedLmdbBlockHashProvider { - block_store: Arc, -} - -impl IndexedLmdbBlockHashProvider { - /// Creates a block hash provider backed by `block_store`. - pub fn new(block_store: Arc) -> Self { - Self { block_store } - } -} - -impl BlockHashProvider for IndexedLmdbBlockHashProvider { - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { - let txn = self.block_store.checkout_ro()?; - let maybe_header: Option = - DataReader::::read(&txn, block_height)?; - Ok(maybe_header.map(|header| header.block_hash())) - } -} diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index d408f5e67e..d16b8133f7 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -1,6 +1,8 @@ //! revm database adapter backed by Casper tracking copy reads. use casper_storage::{ + data_access_layer::DataAccessLayer, + eip2935, global_state::{error::Error as GlobalStateError, state::StateReader}, tracking_copy::TrackingCopyExt, TrackingCopy, @@ -8,31 +10,31 @@ use casper_storage::{ 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}, }; -use crate::{account_state, tx, BlockHashProvider, DbError}; +use crate::{account_state, tx, DbError}; -pub(crate) struct CasperDb<'a, R, B> +pub(crate) struct CasperDb<'a, R, S> where R: StateReader, - B: BlockHashProvider + ?Sized, { + data_access_layer: &'a DataAccessLayer, tracking_copy: &'a mut TrackingCopy, - block_hash_provider: &'a B, } -impl<'a, R, B> CasperDb<'a, R, B> +impl<'a, R, S> CasperDb<'a, R, S> where R: StateReader, - B: BlockHashProvider + ?Sized, { - pub(crate) fn new(tracking_copy: &'a mut TrackingCopy, block_hash_provider: &'a B) -> Self { + pub(crate) fn new( + data_access_layer: &'a DataAccessLayer, + tracking_copy: &'a mut TrackingCopy, + ) -> Self { Self { + data_access_layer, tracking_copy, - block_hash_provider, } } @@ -49,26 +51,52 @@ where } } + fn block_hash_at_height(&self, height: u64) -> Result { + let transaction = self + .data_access_layer + .block_store + .checkout_ro() + .map_err(|error| DbError::BlockHash { height, error })?; + let maybe_header = transaction + .read_block_header_at_height(height) + .map_err(|error| DbError::BlockHash { height, error })?; + Ok(maybe_header + .map(|header| tx::to_revm_block_hash(header.block_hash())) + .unwrap_or(B256::ZERO)) + } + + /// Executes the native EIP-2935 block hash lookup. + pub(crate) fn eip2935_get( + &self, + input: &[u8], + block_number: U256, + ) -> Result, DbError> { + if input.len() != evm::HASH_LENGTH { + return Ok(None); + } + + let requested_height = U256::from_be_slice(input); + if requested_height >= block_number + || block_number - requested_height > U256::from(eip2935::HISTORY_BUFFER_LENGTH) + { + return Ok(None); + } + + let Ok(requested_height) = u64::try_from(requested_height) else { + return Ok(None); + }; + self.block_hash_at_height(requested_height).map(Some) + } + /// 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), - }; - + pub(crate) fn eip4788_get(&mut self, input: &[u8]) -> Result, DbError> { // 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()); + return Ok(None); } let timestamp = u64::from_be_bytes( @@ -77,32 +105,25 @@ where .expect("the final 8 bytes of a 32-byte input have fixed length"), ); if timestamp == 0 { - return Ok(revert()); + return Ok(None); } let Some((stored_timestamp, block_hash)) = self.tracking_copy.get_eip4788_parent_hash(timestamp)? else { - return Ok(revert()); + return Ok(None); }; if stored_timestamp != timestamp { - return Ok(revert()); + return Ok(None); } - 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), - }) + Ok(Some(tx::to_revm_block_hash(block_hash))) } } -impl Database for CasperDb<'_, R, B> +impl Database for CasperDb<'_, R, S> where R: StateReader, - B: BlockHashProvider + ?Sized, { type Error = DbError; @@ -172,16 +193,7 @@ where } fn block_hash(&mut self, number: u64) -> Result { - let maybe_block_hash = self - .block_hash_provider - .block_hash(number) - .map_err(|error| DbError::BlockHash { - height: number, - error, - })?; - Ok(maybe_block_hash - .map(tx::to_revm_block_hash) - .unwrap_or(B256::ZERO)) + self.block_hash_at_height(number) } } diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs index 22aa4e0a87..ba8125eb48 100644 --- a/executor/evm/src/error.rs +++ b/executor/evm/src/error.rs @@ -1,9 +1,9 @@ //! Error types returned by the Casper EVM executor. -use casper_storage::tracking_copy::TrackingCopyError; +use casper_storage::{block_store::BlockStoreError, tracking_copy::TrackingCopyError}; use casper_types::Key; -use crate::{account_state::AccountStorageError, BlockHashProviderError}; +use crate::account_state::AccountStorageError; /// Result type returned by the EVM executor. pub type Result = core::result::Result; @@ -79,13 +79,13 @@ pub enum DbError { /// Decode error text. error: String, }, - /// Failed to resolve a historical block hash for the EVM `BLOCKHASH` opcode. + /// Failed to resolve a historical EVM block hash from the block store. #[error("failed to resolve EVM block hash at height {height}: {error}")] BlockHash { /// Block height requested by the EVM. height: u64, - /// Provider error. - error: BlockHashProviderError, + /// Block store error. + error: BlockStoreError, }, } diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index d067c4c6c5..ec8bf60722 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -1,6 +1,7 @@ //! Public executor entry point. use casper_storage::{ + data_access_layer::DataAccessLayer, global_state::{error::Error as GlobalStateError, state::StateReader}, TrackingCopy, }; @@ -13,8 +14,8 @@ use revm::{ }; use crate::{ - db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, BlockHashProvider, DbError, Error, - ExecuteKind, ExecuteRequest, ExecutionOutcome, NoBlockHashProvider, Result, SystemCallRequest, + db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, DbError, Error, ExecuteKind, + ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -46,49 +47,14 @@ impl EvmExecutor { } /// Executes an EVM transaction or call against the supplied tracking copy. - pub fn execute( + pub fn execute( &self, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, request: ExecuteRequest, ) -> Result where R: StateReader, - { - let block_hash_provider = NoBlockHashProvider; - self.execute_with_block_hash_provider(tracking_copy, request, &block_hash_provider) - } - - /// Executes a system call against the supplied tracking copy. - pub fn execute_system_call( - &self, - tracking_copy: &mut TrackingCopy, - request: SystemCallRequest, - ) -> Result - where - R: StateReader, - { - let block_hash_provider = NoBlockHashProvider; - self.execute_system_call_with_block_hash_provider( - tracking_copy, - request, - &block_hash_provider, - ) - } - - /// Executes with a provider for historical block hashes. - /// - /// The provider is used by the EVM `BLOCKHASH` opcode. Current/future - /// blocks and block numbers older than the EVM 256-block lookup window - /// return the zero hash before the provider is consulted. - pub fn execute_with_block_hash_provider( - &self, - tracking_copy: &mut TrackingCopy, - request: ExecuteRequest, - block_hash_provider: &B, - ) -> Result - where - R: StateReader, - B: BlockHashProvider + ?Sized, { if !self.config.enabled { return Err(Error::Disabled); @@ -117,7 +83,7 @@ impl EvmExecutor { }; let result_and_state = { - let db = CasperDb::new(tracking_copy, block_hash_provider); + let db = CasperDb::new(data_access_layer, tracking_copy); let mut evm = Context::mainnet() .with_db(db) .with_block(block) @@ -141,16 +107,15 @@ impl EvmExecutor { Ok(outcome) } - /// Executes a system call with a provider for historical block hashes. - pub fn execute_system_call_with_block_hash_provider( + /// Executes a system call against the supplied tracking copy. + pub fn execute_system_call( &self, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, request: SystemCallRequest, - block_hash_provider: &B, ) -> Result where R: StateReader, - B: BlockHashProvider + ?Sized, { if !self.config.enabled { return Err(Error::Disabled); @@ -158,7 +123,7 @@ impl EvmExecutor { let block = request.block.to_revm_block(&self.config)?; let result_and_state = { - let db = CasperDb::new(tracking_copy, block_hash_provider); + let db = CasperDb::new(data_access_layer, tracking_copy); let mut evm = Context::mainnet() .with_db(db) .with_block(block) diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index 3a18652a56..43c3dee49b 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -4,7 +4,6 @@ //! `revm` details behind internal adapter modules. mod account_state; -mod block_hash; mod db; mod error; mod executor; @@ -14,10 +13,6 @@ mod request; mod state; mod tx; -pub use block_hash::{ - BlockHashProvider, BlockHashProviderError, BlockHashProviderResult, - IndexedLmdbBlockHashProvider, NoBlockHashProvider, -}; pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; pub use outcome::{ExecutionOutcome, ExecutionStatus}; diff --git a/executor/evm/src/precompiles.rs b/executor/evm/src/precompiles.rs index f60676ee2e..61ec952123 100644 --- a/executor/evm/src/precompiles.rs +++ b/executor/evm/src/precompiles.rs @@ -1,18 +1,20 @@ //! Casper EVM precompile provider. use casper_storage::{ + eip2935::BLOCK_HASH_HISTORY_ADDRESS, eip4788::BEACON_ROOTS_ADDRESS, global_state::{error::Error as GlobalStateError, state::StateReader}, }; use casper_types::{Key, StoredValue}; use revm::{ - context_interface::{Cfg, ContextTr}, + context_interface::{Block as _, Cfg, ContextError, ContextTr}, + database_interface::Database, handler::{EthPrecompiles, PrecompileProvider}, - interpreter::{CallInputs, CallScheme, InterpreterResult}, - primitives::{hardfork::SpecId, Address}, + interpreter::{CallInputs, CallScheme, Gas, InstructionResult, InterpreterResult}, + primitives::{hardfork::SpecId, Address, Bytes, B256}, }; -use crate::{db::CasperDb, tx, BlockHashProvider}; +use crate::{db::CasperDb, tx, DbError}; /// Ethereum precompiles executing with access to Casper-backed state. #[derive(Clone, Debug)] @@ -24,11 +26,44 @@ impl CasperEvmPrecompiles { } } -impl<'a, R, B, CTX> PrecompileProvider for CasperEvmPrecompiles +fn native_get_result( + context: &mut CTX, + lookup_result: Result, DbError>, + gas_limit: u64, + reservoir: u64, +) -> InterpreterResult +where + CTX: ContextTr, + ::Error: From, +{ + let (result, output) = match lookup_result { + Ok(Some(value)) => ( + InstructionResult::Return, + Bytes::copy_from_slice(value.as_slice()), + ), + Ok(None) => (InstructionResult::Revert, Bytes::new()), + Err(error) => { + // Preserve database errors in revm's typed error channel. Returning a string from + // this provider would turn the error into `EVMError::Custom`. + *context.error() = Err(ContextError::Db(error.into())); + (InstructionResult::FatalExternalError, Bytes::new()) + } + }; + + InterpreterResult { + result, + output, + // Native lookups 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<'a, R, S, CTX> PrecompileProvider for CasperEvmPrecompiles where R: StateReader + 'a, - B: BlockHashProvider + ?Sized + 'a, - CTX: ContextTr>, + S: 'a, + CTX: ContextTr>, { type Output = InterpreterResult; @@ -50,10 +85,24 @@ where // 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())?; + let lookup_result = context.db_mut().eip4788_get(&input); + let result = + native_get_result(context, lookup_result, inputs.gas_limit, inputs.reservoir); + return Ok(Some(result)); + } + + let block_hash_history_address = tx::to_revm_address(BLOCK_HASH_HISTORY_ADDRESS); + if inputs.target_address == block_hash_history_address + && inputs.bytecode_address == block_hash_history_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 block_number = context.block().number(); + let lookup_result = context.db_mut().eip2935_get(&input, block_number); + let result = + native_get_result(context, lookup_result, inputs.gas_limit, inputs.reservoir); return Ok(Some(result)); } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index bb1a42fed3..a114cbc0ac 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -9,33 +9,42 @@ 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, EMPTY_CODE_HASH, + BlockContext, CallRequest, CallValidation, Error, EvmExecutor, ExecuteKind, ExecuteRequest, + ExecutionStatus, BLOCK_HASH_HISTORY, EMPTY_CODE_HASH, }; use casper_storage::{ - data_access_layer::{GenesisRequest, GenesisResult}, - eip4788, + block_store::{lmdb::LmdbBlockStore, BlockStoreTransaction}, + data_access_layer::{DataAccessLayer, GenesisRequest, GenesisResult}, + eip2935, eip4788, global_state::{ self, error::Error as GlobalStateError, - state::{lmdb::LmdbGlobalStateView, CommitProvider, StateProvider, StateReader}, + state::{ + lmdb::{LmdbGlobalState, LmdbGlobalStateView}, + CommitProvider, StateProvider, StateReader, + }, }, tracking_copy::TrackingCopyExt, TrackingCopy, }; use casper_types::{ - contracts::NamedKeys, evm, AccessRights, Account, BlockHash, ByteCode, ByteCodeKind, CLValue, - ChainspecRegistry, Digest, EvmAddr, EvmConfig, EvmSpec, EvmTransaction, GenesisAccount, - GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, - StorageCosts, StoredValue, SystemConfig, Timestamp, URef, WasmConfig, DEFAULT_WEI_PER_MOTE, - U256 as CasperU256, U512, + contracts::NamedKeys, evm, AccessRights, Account, BlockGlobalAddr, BlockHash, BlockHeader, + BlockHeaderV2, ByteCode, ByteCodeKind, CLValue, ChainspecRegistry, Digest, EraId, EvmAddr, + EvmConfig, EvmSpec, EvmTransaction, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, + Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, + Timestamp, URef, WasmConfig, DEFAULT_WEI_PER_MOTE, U256 as CasperU256, U512, }; +use once_cell::sync::OnceCell; use revm::bytecode::opcode; const SIGNING_SECRET: [u8; 32] = [7; 32]; const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; -fn tracking_copy() -> (TrackingCopy, impl Send) { +fn tracking_copy() -> ( + TrackingCopy, + DataAccessLayer, + impl Send, +) { let accounts = (1u8..=3) .map(|seed| { let secret_key = @@ -85,7 +94,19 @@ fn tracking_copy() -> (TrackingCopy, impl Send) { .checkout(post_state_hash) .expect("checkout should not fail") .expect("post-genesis root should exist"); - (TrackingCopy::new(reader, 5, false), tempdir) + let block_store = + LmdbBlockStore::new_temporary(64 * 1024 * 1024).expect("should create block store"); + let data_access_layer = DataAccessLayer { + block_store, + state: global_state, + max_query_depth: 5, + enable_addressable_entity: false, + }; + ( + TrackingCopy::new(reader, 5, false), + data_access_layer, + tempdir, + ) } fn executor(spec: EvmSpec) -> EvmExecutor { @@ -109,19 +130,41 @@ fn block() -> BlockContext { } } -#[derive(Clone, Copy)] -struct HeightBlockHashProvider; - -impl BlockHashProvider for HeightBlockHashProvider { - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { - Ok(Some(block_hash_for_height(block_height))) - } +fn block_header(block_height: u64) -> BlockHeader { + let proposer = PublicKey::from( + &SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]) + .expect("should create secret key"), + ); + BlockHeader::V2(BlockHeaderV2::new( + BlockHash::new(Digest::hash("parent block")), + Digest::hash("state root"), + Digest::hash("body"), + false, + Digest::hash("accumulated seed"), + None, + Timestamp::from(1_714_000_000), + EraId::new(1), + block_height, + ProtocolVersion::V2_0_0, + proposer, + 1, + None, + OnceCell::new(), + )) } -fn block_hash_for_height(block_height: u64) -> BlockHash { - let mut bytes = [0u8; BlockHash::LENGTH]; - bytes[24..].copy_from_slice(&block_height.to_be_bytes()); - BlockHash::new(Digest::from_raw(bytes)) +fn write_block_header( + data_access_layer: &DataAccessLayer, + block_header: &BlockHeader, +) { + let mut block_store = data_access_layer.block_store.clone(); + let mut transaction = block_store + .checkout_rw() + .expect("should check out write transaction"); + transaction + .write_block_header(block_header) + .expect("should write block header"); + transaction.commit().expect("should commit block header"); } fn init_code_returning(runtime: Vec) -> Vec { @@ -271,15 +314,20 @@ fn checked_call_request( } } -fn execute_call>( +fn execute_call( executor: &EvmExecutor, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, from: evm::Address, to: Option, input: Vec, -) -> casper_executor_evm::ExecutionOutcome { +) -> casper_executor_evm::ExecutionOutcome +where + R: StateReader, +{ let outcome = executor .execute( + data_access_layer, tracking_copy, call_request(from, to, input, CasperU256::zero()), ) @@ -288,13 +336,18 @@ fn execute_call>( outcome } -fn execute_transaction>( +fn execute_transaction( executor: &EvmExecutor, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, transaction: EvmTransaction, -) -> casper_executor_evm::ExecutionOutcome { +) -> casper_executor_evm::ExecutionOutcome +where + R: StateReader, +{ executor .execute( + data_access_layer, tracking_copy, ExecuteRequest { block: block(), @@ -304,26 +357,41 @@ fn execute_transaction>( +fn deploy_code( executor: &EvmExecutor, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, from: evm::Address, code: Vec, -) -> evm::Address { - execute_call(executor, tracking_copy, from, None, code) +) -> evm::Address +where + R: StateReader, +{ + execute_call(executor, data_access_layer, tracking_copy, from, None, code) .created_contract_address .expect("deploy should return a contract address") } -fn deploy>( +fn deploy( executor: &EvmExecutor, + data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, from: evm::Address, name: &str, -) -> evm::Address { - execute_call(executor, tracking_copy, from, None, contract_bin(name)) - .created_contract_address - .expect("deploy should return a contract address") +) -> evm::Address +where + R: StateReader, +{ + execute_call( + executor, + data_access_layer, + tracking_copy, + from, + None, + contract_bin(name), + ) + .created_contract_address + .expect("deploy should return a contract address") } fn contract_bin(name: &str) -> Vec { @@ -688,12 +756,13 @@ fn delegation_code(delegate: evm::Address) -> Vec { #[test] fn prague_bls12_g1_add_precompile_delegates_to_revm() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let mut precompile_address = [0; evm::ADDRESS_LENGTH]; precompile_address[evm::ADDRESS_LENGTH - 1] = 0x0b; let outcome = execute_call( &executor, + &data_access_layer, &mut tracking_copy, evm::Address::ZERO, Some(evm::Address::new(precompile_address)), @@ -706,7 +775,7 @@ fn prague_bls12_g1_add_precompile_delegates_to_revm() { #[test] fn eip4788_native_lookup_bypasses_predeploy_bytecode() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let timestamp = block().timestamp; let root = [0xab; evm::HASH_LENGTH]; @@ -725,6 +794,7 @@ fn eip4788_native_lookup_bypasses_predeploy_bytecode() { let query = execute_call( &executor, + &data_access_layer, &mut tracking_copy, evm::Address::ZERO, Some(eip4788::BEACON_ROOTS_ADDRESS), @@ -736,7 +806,7 @@ fn eip4788_native_lookup_bypasses_predeploy_bytecode() { #[test] fn eip4788_returns_a_matching_zero_parent_hash() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let timestamp = block().timestamp; let zero_hash = [0; evm::HASH_LENGTH]; @@ -753,6 +823,7 @@ fn eip4788_returns_a_matching_zero_parent_hash() { let query = execute_call( &executor, + &data_access_layer, &mut tracking_copy, evm::Address::ZERO, Some(eip4788::BEACON_ROOTS_ADDRESS), @@ -765,7 +836,7 @@ fn eip4788_returns_a_matching_zero_parent_hash() { #[test] fn eip4788_unknown_and_overwritten_timestamps_revert() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let timestamp = block().timestamp; let replacement_timestamp = timestamp + eip4788::HISTORY_BUFFER_LENGTH; let replacement_root = [0xcd; evm::HASH_LENGTH]; @@ -783,6 +854,7 @@ fn eip4788_unknown_and_overwritten_timestamps_revert() { let unknown = executor .execute( + &data_access_layer, &mut tracking_copy, call_request( evm::Address::ZERO, @@ -805,6 +877,7 @@ fn eip4788_unknown_and_overwritten_timestamps_revert() { let stale = executor .execute( + &data_access_layer, &mut tracking_copy, call_request( evm::Address::ZERO, @@ -818,6 +891,7 @@ fn eip4788_unknown_and_overwritten_timestamps_revert() { let query = execute_call( &executor, + &data_access_layer, &mut tracking_copy, evm::Address::ZERO, Some(eip4788::BEACON_ROOTS_ADDRESS), @@ -829,7 +903,7 @@ fn eip4788_unknown_and_overwritten_timestamps_revert() { #[test] fn eip4788_rejects_invalid_calldata() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let timestamp = block().timestamp; seed_eip4788_parent_hash( @@ -853,6 +927,7 @@ fn eip4788_rejects_invalid_calldata() { ] { let outcome = executor .execute( + &data_access_layer, &mut tracking_copy, call_request( evm::Address::ZERO, @@ -867,12 +942,189 @@ fn eip4788_rejects_invalid_calldata() { } #[test] -fn blockhash_uses_supplied_provider() { +fn eip4788_preserves_tracking_copy_errors() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let timestamp = block().timestamp; + seed_evm_code( + &mut tracking_copy, + eip4788::BEACON_ROOTS_ADDRESS, + eip4788::BEACON_ROOTS_CODE.to_vec(), + ); + tracking_copy.write( + Key::BlockGlobal(BlockGlobalAddr::BlockParentHash { + slot: timestamp % eip4788::HISTORY_BUFFER_LENGTH, + }), + StoredValue::CLValue(CLValue::from_t(1u64).expect("value should encode")), + ); + + let error = executor + .execute( + &data_access_layer, + &mut tracking_copy, + call_request( + evm::Address::ZERO, + Some(eip4788::BEACON_ROOTS_ADDRESS), + word(timestamp).to_vec(), + CasperU256::zero(), + ), + ) + .expect_err("malformed EIP-4788 state should fail execution"); + assert!(matches!(error, Error::Database(DbError::TrackingCopy(_)))); +} + +#[test] +fn eip2935_native_lookup_reads_indexed_header_and_bypasses_bytecode() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let header = block_header(1); + let expected_hash = header.block_hash(); + write_block_header(&data_access_layer, &header); + + // Direct calls must use the native lookup even when the installed code would revert. + seed_evm_code( + &mut tracking_copy, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + reverting_runtime(), + ); + + let mut stored_request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + word(1).to_vec(), + CasperU256::zero(), + ); + stored_request.block.number = 2; + let stored = executor + .execute(&data_access_layer, &mut tracking_copy, stored_request) + .expect("EIP-2935 lookup should execute"); + assert_eq!(stored.status, ExecutionStatus::Success); + assert_eq!(stored.output.as_slice(), expected_hash.as_ref()); + + let mut missing_request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + word(0).to_vec(), + CasperU256::zero(), + ); + missing_request.block.number = 2; + let missing = executor + .execute(&data_access_layer, &mut tracking_copy, missing_request) + .expect("EIP-2935 lookup should execute"); + assert_eq!(missing.status, ExecutionStatus::Success); + assert_eq!(missing.output.as_slice(), &[0; evm::HASH_LENGTH]); + + let mut oldest_valid_request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + word(1).to_vec(), + CasperU256::zero(), + ); + oldest_valid_request.block.number = eip2935::HISTORY_BUFFER_LENGTH + 1; + let oldest_valid = executor + .execute(&data_access_layer, &mut tracking_copy, oldest_valid_request) + .expect("EIP-2935 lookup should execute"); + assert_eq!(oldest_valid.status, ExecutionStatus::Success); + assert_eq!(oldest_valid.output.as_slice(), expected_hash.as_ref()); + + let mut too_old_request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + word(1).to_vec(), + CasperU256::zero(), + ); + too_old_request.block.number = eip2935::HISTORY_BUFFER_LENGTH + 2; + let too_old = executor + .execute(&data_access_layer, &mut tracking_copy, too_old_request) + .expect("EIP-2935 lookup should execute"); + assert_eq!(too_old.status, ExecutionStatus::Revert); +} + +#[test] +fn eip2935_reverts_for_invalid_requests() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + seed_evm_code( + &mut tracking_copy, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + eip2935::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + let mut oversized_height = [0xff; evm::HASH_LENGTH]; + oversized_height[0] = 1; + let cases = [ + (1, vec![]), + (1, vec![0; evm::HASH_LENGTH - 1]), + (1, vec![0; evm::HASH_LENGTH + 1]), + (0, word(0).to_vec()), + (1, word(1).to_vec()), + (1, word(2).to_vec()), + (1, oversized_height.to_vec()), + ]; + + for (block_number, input) in cases { + let mut request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + input, + CasperU256::zero(), + ); + request.block.number = block_number; + let outcome = executor + .execute(&data_access_layer, &mut tracking_copy, request) + .expect("EIP-2935 lookup should execute"); + assert_eq!(outcome.status, ExecutionStatus::Revert); + } +} + +#[test] +fn eip2935_preserves_block_store_errors() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + seed_evm_code( + &mut tracking_copy, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + eip2935::BLOCK_HASH_HISTORY_CODE.to_vec(), + ); + + // Exhaust this environment's LMDB reader slots so the native lookup fails while checking out + // its short-lived read transaction. + let read_transactions = (0..512) + .map(|_| { + data_access_layer + .block_store + .checkout_ro() + .expect("should check out reader") + }) + .collect::>(); + + let mut request = call_request( + evm::Address::ZERO, + Some(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + word(1).to_vec(), + CasperU256::zero(), + ); + request.block.number = 2; + + let error = executor + .execute(&data_access_layer, &mut tracking_copy, request) + .expect_err("exhausted LMDB readers should fail execution"); + assert!(matches!( + error, + Error::Database(DbError::BlockHash { height: 1, .. }) + )); + + drop(read_transactions); +} + +#[test] +fn blockhash_reads_indexed_header_from_data_access_layer() { let executor = executor(EvmSpec::Prague); let from = evm::Address::new([1; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let contract = execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, None, @@ -880,38 +1132,60 @@ fn blockhash_uses_supplied_provider() { ) .created_contract_address .expect("deploy should return a contract address"); - let block_hash_provider = HeightBlockHashProvider; - let outcome = executor - .execute_with_block_hash_provider( + .execute( + &data_access_layer, &mut tracking_copy, call_request(from, Some(contract), Vec::new(), CasperU256::zero()), - &block_hash_provider, ) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); - let mut too_old_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); - too_old_request.block.number = 258; + let mut future_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); + future_request.block.number = 0; let outcome = executor - .execute_with_block_hash_provider(&mut tracking_copy, too_old_request, &block_hash_provider) + .execute(&data_access_layer, &mut tracking_copy, future_request) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); + let mut missing_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); + missing_request.block.number = 2; + let outcome = executor + .execute(&data_access_layer, &mut tracking_copy, missing_request) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); + + let header = block_header(1); + let expected_hash = header.block_hash(); + write_block_header(&data_access_layer, &header); + let mut historical_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); historical_request.block.number = 2; let outcome = executor - .execute_with_block_hash_provider( - &mut tracking_copy, - historical_request, - &block_hash_provider, - ) + .execute(&data_access_layer, &mut tracking_copy, historical_request) .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output.as_slice(), expected_hash.as_ref()); + let mut oldest_valid_request = + call_request(from, Some(contract), Vec::new(), CasperU256::zero()); + oldest_valid_request.block.number = BLOCK_HASH_HISTORY + 1; + let outcome = executor + .execute(&data_access_layer, &mut tracking_copy, oldest_valid_request) + .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); - assert_eq!(outcome.output.as_slice(), block_hash_for_height(1).as_ref()); + assert_eq!(outcome.output.as_slice(), expected_hash.as_ref()); + + let mut too_old_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); + too_old_request.block.number = BLOCK_HASH_HISTORY + 2; + let outcome = executor + .execute(&data_access_layer, &mut tracking_copy, too_old_request) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); } #[test] @@ -919,9 +1193,10 @@ fn eip7702_authorization_installs_delegation_and_executes_delegate_code() { let executor = executor(EvmSpec::Prague); let deployer = evm::Address::new([1; 20]); let authority = authorization_authority(); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let delegate = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, deployer, return_word_contract_init_code(42), @@ -936,7 +1211,12 @@ fn eip7702_authorization_installs_delegation_and_executes_delegate_code() { ); seed_evm_balance(&mut tracking_copy, authority, U512::zero()); - let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + transaction, + ); assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!(decode_word(&outcome.output), 42); @@ -953,9 +1233,10 @@ fn eip7702_delegation_persists_when_call_reverts() { let executor = executor(EvmSpec::Prague); let deployer = evm::Address::new([1; 20]); let authority = authorization_authority(); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let delegate = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, deployer, reverting_contract_init_code(), @@ -968,7 +1249,12 @@ fn eip7702_delegation_persists_when_call_reverts() { ); seed_evm_balance(&mut tracking_copy, authority, U512::zero()); - let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + transaction, + ); assert_eq!(outcome.status, ExecutionStatus::Revert); let code_hash = read_code_hash(&mut tracking_copy, authority); @@ -983,9 +1269,10 @@ fn eip7702_stale_authorization_is_skipped() { let executor = executor(EvmSpec::Prague); let deployer = evm::Address::new([1; 20]); let authority = authorization_authority(); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let delegate = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, deployer, return_word_contract_init_code(42), @@ -998,7 +1285,12 @@ fn eip7702_stale_authorization_is_skipped() { ); seed_evm_balance(&mut tracking_copy, authority, U512::zero()); - let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + transaction, + ); assert_eq!(outcome.status, ExecutionStatus::Success); assert!(outcome.output.is_empty()); @@ -1014,9 +1306,10 @@ fn eip7702_zero_address_authorization_clears_delegation() { let executor = executor(EvmSpec::Prague); let deployer = evm::Address::new([1; 20]); let authority = authorization_authority(); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let delegate = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, deployer, return_word_contract_init_code(42), @@ -1028,12 +1321,22 @@ fn eip7702_zero_address_authorization_clears_delegation() { U512::from(1_000_000_000u64), ); seed_evm_balance(&mut tracking_copy, authority, U512::zero()); - let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + transaction, + ); assert_eq!(outcome.status, ExecutionStatus::Success); let (clear_transaction, _) = eip7702_transaction(authority, evm::Address::ZERO, 1, 1, Vec::new()); - let outcome = execute_transaction(&executor, &mut tracking_copy, clear_transaction); + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + clear_transaction, + ); assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!( @@ -1046,11 +1349,18 @@ fn eip7702_zero_address_authorization_clears_delegation() { fn counter_supports_committed_and_discarded_execution() { let executor = executor(EvmSpec::Prague); let from = evm::Address::new([1; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); - let counter = deploy(&executor, &mut tracking_copy, from, "Counter"); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let counter = deploy( + &executor, + &data_access_layer, + &mut tracking_copy, + from, + "Counter", + ); let increment = execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, Some(counter), @@ -1061,6 +1371,7 @@ fn counter_supports_committed_and_discarded_execution() { let mut view = tracking_copy.fork(); let view_increment = execute_call( &executor, + &data_access_layer, &mut view, from, Some(counter), @@ -1070,6 +1381,7 @@ fn counter_supports_committed_and_discarded_execution() { let get = execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, Some(counter), @@ -1084,12 +1396,13 @@ fn erc20_and_native_purse_balances_update() { let owner = evm::Address::new([1; 20]); let recipient = evm::Address::new([2; 20]); let spender = evm::Address::new([3; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, owner, U512::from(1_000u64)); let transfer_value = CasperU256::from(250); let outcome = executor .execute( + &data_access_layer, &mut tracking_copy, call_request(owner, Some(recipient), Vec::new(), transfer_value), ) @@ -1101,9 +1414,16 @@ fn erc20_and_native_purse_balances_update() { U512::from(250u64) ); - let token = deploy(&executor, &mut tracking_copy, owner, "MinimalERC20"); + let token = deploy( + &executor, + &data_access_layer, + &mut tracking_copy, + owner, + "MinimalERC20", + ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1111,6 +1431,7 @@ fn erc20_and_native_purse_balances_update() { ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1121,6 +1442,7 @@ fn erc20_and_native_purse_balances_update() { ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1131,6 +1453,7 @@ fn erc20_and_native_purse_balances_update() { ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, spender, Some(token), @@ -1142,6 +1465,7 @@ fn erc20_and_native_purse_balances_update() { let owner_balance = execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1149,6 +1473,7 @@ fn erc20_and_native_purse_balances_update() { ); let recipient_balance = execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1156,6 +1481,7 @@ fn erc20_and_native_purse_balances_update() { ); let remaining_allowance = execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(token), @@ -1182,7 +1508,7 @@ fn coinbase_transfer_to_prelinked_beneficiary_credits_proposer_account() { let proposer_main_purse = URef::new([8; 32], AccessRights::READ_ADD_WRITE); let proposer_initial_balance = U512::from(1_000u64); let transfer_value = CasperU256::from(250u64); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); seed_account( @@ -1197,6 +1523,7 @@ fn coinbase_transfer_to_prelinked_beneficiary_credits_proposer_account() { ); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_transfer_init_code(), @@ -1205,7 +1532,7 @@ fn coinbase_transfer_to_prelinked_beneficiary_credits_proposer_account() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase transfer should execute"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1229,7 +1556,7 @@ fn coinbase_transfer_without_prelink_uses_evm_native_identity() { let proposer_account_hash = proposer.to_account_hash(); let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); let proposer_main_purse = URef::new([9; 32], AccessRights::READ_ADD_WRITE); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); seed_account( @@ -1240,6 +1567,7 @@ fn coinbase_transfer_without_prelink_uses_evm_native_identity() { ); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_transfer_init_code(), @@ -1248,7 +1576,7 @@ fn coinbase_transfer_without_prelink_uses_evm_native_identity() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase transfer should execute"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1274,11 +1602,12 @@ fn reading_coinbase_without_credit_creates_only_evm_native_identity() { SecretKey::ed25519_from_bytes([43; SecretKey::ED25519_LENGTH]).unwrap(); let proposer = PublicKey::from(&proposer_secret_key); let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_observer_init_code(), @@ -1287,7 +1616,7 @@ fn reading_coinbase_without_credit_creates_only_evm_native_identity() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase observer should execute"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1307,7 +1636,7 @@ fn coinbase_transfer_to_linked_beneficiary_with_code_executes_code() { let proposer_account_hash = proposer.to_account_hash(); let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); let proposer_main_purse = URef::new([10; 32], AccessRights::READ_ADD_WRITE); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); seed_account( @@ -1323,6 +1652,7 @@ fn coinbase_transfer_to_linked_beneficiary_with_code_executes_code() { seed_evm_code(&mut tracking_copy, beneficiary, reverting_runtime()); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_transfer_init_code(), @@ -1331,7 +1661,7 @@ fn coinbase_transfer_to_linked_beneficiary_with_code_executes_code() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase transfer should execute EVM code"); assert_eq!(outcome.status, ExecutionStatus::Revert); @@ -1356,7 +1686,7 @@ fn coinbase_transfer_keeps_existing_evm_native_beneficiary_identity() { let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); let proposer_main_purse = URef::new([12; 32], AccessRights::READ_ADD_WRITE); let existing_purse = URef::new([13; 32], AccessRights::READ_ADD_WRITE); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); seed_account( @@ -1379,6 +1709,7 @@ fn coinbase_transfer_keeps_existing_evm_native_beneficiary_identity() { ); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_transfer_init_code(), @@ -1387,7 +1718,7 @@ fn coinbase_transfer_keeps_existing_evm_native_beneficiary_identity() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase transfer should preserve existing identity"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1422,7 +1753,7 @@ fn coinbase_transfer_keeps_existing_account_beneficiary_identity() { let existing_main_purse = URef::new([15; 32], AccessRights::READ_ADD_WRITE); let existing_initial_balance = U512::from(500u64); let transfer_value = CasperU256::from(250u64); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); seed_account( @@ -1451,6 +1782,7 @@ fn coinbase_transfer_keeps_existing_account_beneficiary_identity() { ); let contract = deploy_code( &executor, + &data_access_layer, &mut tracking_copy, sender, coinbase_transfer_init_code(), @@ -1459,7 +1791,7 @@ fn coinbase_transfer_keeps_existing_account_beneficiary_identity() { request.block.beneficiary = beneficiary; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("coinbase transfer should preserve existing account identity"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1488,7 +1820,7 @@ fn nonzero_gas_price_does_not_charge_evm_balances() { let proposer_account_hash = proposer.to_account_hash(); let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); let proposer_main_purse = URef::new([11; 32], AccessRights::READ_ADD_WRITE); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let initial_balance = U512::from(10_000_000u64); let transfer_value = CasperU256::from(250u64); @@ -1516,7 +1848,7 @@ fn nonzero_gas_price_does_not_charge_evm_balances() { }; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("native EVM transfer should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1545,11 +1877,18 @@ fn erc721_mint_approve_and_transfer() { let owner = evm::Address::new([1; 20]); let recipient = evm::Address::new([2; 20]); let approved = evm::Address::new([3; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); - let nft = deploy(&executor, &mut tracking_copy, owner, "MinimalERC721"); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let nft = deploy( + &executor, + &data_access_layer, + &mut tracking_copy, + owner, + "MinimalERC721", + ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(nft), @@ -1557,6 +1896,7 @@ fn erc721_mint_approve_and_transfer() { ); let initial_owner = execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(nft), @@ -1566,6 +1906,7 @@ fn erc721_mint_approve_and_transfer() { execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(nft), @@ -1576,6 +1917,7 @@ fn erc721_mint_approve_and_transfer() { ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, approved, Some(nft), @@ -1586,6 +1928,7 @@ fn erc721_mint_approve_and_transfer() { ); let final_owner = execute_call( &executor, + &data_access_layer, &mut tracking_copy, owner, Some(nft), @@ -1598,11 +1941,18 @@ fn erc721_mint_approve_and_transfer() { fn storage_zeroes_are_pruned() { let executor = executor(EvmSpec::Prague); let from = evm::Address::new([1; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); - let contract = deploy(&executor, &mut tracking_copy, from, "StorageDelete"); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let contract = deploy( + &executor, + &data_access_layer, + &mut tracking_copy, + from, + "StorageDelete", + ); execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, Some(contract), @@ -1615,6 +1965,7 @@ fn storage_zeroes_are_pruned() { execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, Some(contract), @@ -1632,15 +1983,17 @@ fn selfdestruct_preserves_account_on_prague() { let beneficiary = evm::Address::new([2; 20]); let prague_executor = executor(EvmSpec::Prague); - let (mut prague_tracking_copy, _prague_tempdir) = tracking_copy(); + let (mut prague_tracking_copy, prague_data_access_layer, _prague_tempdir) = tracking_copy(); let prague_contract = deploy( &prague_executor, + &prague_data_access_layer, &mut prague_tracking_copy, from, "SelfDestruct", ); execute_call( &prague_executor, + &prague_data_access_layer, &mut prague_tracking_copy, from, Some(prague_contract), @@ -1655,14 +2008,14 @@ fn selfdestruct_preserves_account_on_prague() { #[test] fn signed_transactions_require_configured_chain_id() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let missing_chain_id = legacy_transaction_without_chain_id(); let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(Box::new(missing_chain_id)), }; assert!(matches!( - executor.execute(&mut tracking_copy, request), + executor.execute(&data_access_layer, &mut tracking_copy, request), Err(Error::MissingChainId) )); @@ -1680,7 +2033,7 @@ fn signed_transactions_require_configured_chain_id() { kind: ExecuteKind::Transaction(Box::new(transaction)), }; assert!(matches!( - wrong_chain_executor.execute(&mut tracking_copy, request), + wrong_chain_executor.execute(&data_access_layer, &mut tracking_copy, request), Err(Error::ChainIdMismatch { expected: 8, actual: 7 @@ -1691,7 +2044,7 @@ fn signed_transactions_require_configured_chain_id() { #[test] fn signed_transaction_sender_uses_linked_casper_account_identity() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let transaction = legacy_transaction(Some(7)); let signer = transaction .signer() @@ -1718,7 +2071,7 @@ fn signed_transaction_sender_uses_linked_casper_account_identity() { kind: ExecuteKind::Transaction(Box::new(transaction.clone())), }; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1741,7 +2094,7 @@ fn signed_transaction_sender_uses_linked_casper_account_identity() { #[test] fn signed_transaction_sender_keeps_evm_native_identity() { let executor = executor(EvmSpec::Prague); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let transaction = legacy_transaction(Some(7)); let signer = transaction .signer() @@ -1756,7 +2109,7 @@ fn signed_transaction_sender_keeps_evm_native_identity() { kind: ExecuteKind::Transaction(Box::new(transaction.clone())), }; let outcome = executor - .execute(&mut tracking_copy, request) + .execute(&data_access_layer, &mut tracking_copy, request) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -1790,12 +2143,12 @@ fn checked_calls_enforce_transaction_validation() { let executor = executor(EvmSpec::Prague); let from = evm::Address::new([1; 20]); let recipient = evm::Address::new([2; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let mut request = checked_call_request(from, Some(recipient), Vec::new(), CasperU256::zero()); request.block.base_fee = Some(1); assert!(matches!( - executor.execute(&mut tracking_copy, request), + executor.execute(&data_access_layer, &mut tracking_copy, request), Err(Error::Revm(_)) )); } diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 4fb9d365b9..c6de89b8bf 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -66,7 +66,7 @@ use futures::{future::BoxFuture, FutureExt}; use self::error::Error; use crate::{ - contract_runtime::{load_recent_evm_block_hashes, SpeculativeExecutionResult}, + contract_runtime::SpeculativeExecutionResult, effect::{ requests::{ AcceptTransactionRequest, BlockSynchronizerRequest, ChainspecRawBytesRequest, @@ -1368,10 +1368,8 @@ where None => return BinaryResponse::new_error(ErrorCode::NoCompleteBlocks), }; - let block_hashes = load_recent_evm_block_hashes(effect_builder, tip.height()).await; - let result = effect_builder - .speculatively_execute(Box::new(tip), block_hashes, Box::new(transaction)) + .speculatively_execute(Box::new(tip), Box::new(transaction)) .await; match result { diff --git a/node/src/components/binary_port/tests.rs b/node/src/components/binary_port/tests.rs index cda56ff013..715cbc7da6 100644 --- a/node/src/components/binary_port/tests.rs +++ b/node/src/components/binary_port/tests.rs @@ -289,6 +289,13 @@ impl Reactor for MockReactor { self.binary_port.handle_event(effect_builder, rng, event), ), Event::ControlAnnouncement(_) => panic!("unexpected control announcement"), + Event::ContractRuntimeRequest(ContractRuntimeRequest::SpeculativelyExecute { + block_header, + .. + }) => { + assert_eq!(block_header.height(), 42); + Effects::new() + } Event::ContractRuntimeRequest(_) | Event::ReactorInfoRequest(_) => { // We're only interested if the binary port actually created a request to Contract // Runtime component, but we're not interested in the result. @@ -306,7 +313,7 @@ impl Reactor for MockReactor { Default::default(), Timestamp::now(), Default::default(), - Default::default(), + 42, Default::default(), proposer, Default::default(), diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index 10e898c1ca..9aa002be97 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -29,8 +29,9 @@ use tracing::{debug, error, info, trace}; use casper_execution_engine::engine_state::{EngineConfigBuilder, ExecutionEngineV1}; use casper_storage::{ + block_store::lmdb::LmdbBlockStore, data_access_layer::{ - AddressableEntityRequest, AddressableEntityResult, BlockStore, DataAccessLayer, + AddressableEntityRequest, AddressableEntityResult, DataAccessLayer, EntryPointExistsRequest, ExecutionResultsChecksumRequest, FlushRequest, FlushResult, GenesisRequest, GenesisResult, ProtocolUpgradeRequest, ProtocolUpgradeResult, TrieRequest, }, @@ -77,7 +78,6 @@ pub(crate) use types::{ BlockAndExecutionArtifacts, ExecutionArtifact, ExecutionPreState, SpeculativeExecutionResult, StepOutcome, }; -pub(crate) use utils::load_recent_evm_block_hashes; use utils::{exec_and_check_next, run_intensive_task}; const COMPONENT_NAME: &str = "contract_runtime"; @@ -113,6 +113,7 @@ impl Debug for ContractRuntime { impl ContractRuntime { pub(crate) fn new( storage_dir: &Path, + block_store: LmdbBlockStore, contract_runtime_config: &Config, chainspec: Arc, registry: &Registry, @@ -158,6 +159,7 @@ impl ContractRuntime { let data_access_layer = Arc::new( Self::new_data_access_layer( storage_dir, + block_store, contract_runtime_config, enable_addressable_entity, ) @@ -213,6 +215,7 @@ impl ContractRuntime { fn new_data_access_layer( storage_dir: &Path, + block_store: LmdbBlockStore, contract_runtime_config: &Config, enable_addressable_entity: bool, ) -> Result, casper_storage::global_state::error::Error> { @@ -230,8 +233,6 @@ impl ContractRuntime { DatabaseFlags::empty(), )?); - let block_store = BlockStore::new(); - let max_query_depth = contract_runtime_config.max_query_depth_or_default(); let global_state = LmdbGlobalState::empty( environment, @@ -674,7 +675,6 @@ impl ContractRuntime { } ContractRuntimeRequest::SpeculativelyExecute { block_header, - block_hashes, transaction, responder, } => { @@ -688,7 +688,6 @@ impl ContractRuntime { chainspec.as_ref(), execution_engine_v1.as_ref(), *block_header, - block_hashes, *transaction, ) }) diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 9a607ea052..5b546ba3ef 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -10,8 +10,7 @@ use casper_execution_engine::engine_state::{ BlockInfo, ExecutionEngineV1, WasmV1Request, WasmV1Result, }; use casper_executor_evm::{ - BlockContext as EvmBlockContext, BlockHashProvider as EvmBlockHashProvider, - BlockHashProviderResult as EvmBlockHashProviderResult, CallRequest as EvmExecutorCallRequest, + BlockContext as EvmBlockContext, CallRequest as EvmExecutorCallRequest, CallValidation as EvmCallValidation, EvmExecutor, ExecuteKind as EvmExecuteKind, ExecuteRequest as EvmExecuteRequest, ExecutionStatus as EvmExecutionStatus, }; @@ -65,17 +64,6 @@ use crate::{ types::{self, Chunkable, ExecutableBlock, InternalEraReport, MetaTransaction}, }; -#[derive(Default)] -struct StaticEvmBlockHashProvider { - block_hashes: BTreeMap, -} - -impl EvmBlockHashProvider for StaticEvmBlockHashProvider { - fn block_hash(&self, block_height: u64) -> EvmBlockHashProviderResult> { - Ok(self.block_hashes.get(&block_height).copied()) - } -} - fn evm_block_context( chainspec: &Chainspec, block_height: u64, @@ -537,7 +525,6 @@ pub fn execute_finalized_block( chainspec: &Chainspec, metrics: Option>, execution_pre_state: ExecutionPreState, - evm_block_hash_provider: &dyn EvmBlockHashProvider, executable_block: ExecutableBlock, key_block_height_for_activation_point: u64, current_gas_price: u8, @@ -1211,11 +1198,7 @@ pub fn execute_finalized_block( } apply_evm_proposer_identity(&mut tracking_copy, protocol_version, &proposer)?; let outcome = EvmExecutor::new(chainspec.evm_config) - .execute_with_block_hash_provider( - &mut tracking_copy, - request, - evm_block_hash_provider, - ) + .execute(data_access_layer, &mut tracking_copy, request) .map_err(|error| { BlockExecutionError::TransactionConversion(error.to_string()) })?; @@ -1981,11 +1964,10 @@ pub fn execute_finalized_block( /// /// Returns effects of the execution. pub(super) fn speculatively_execute( - state_provider: &S, + data_access_layer: &DataAccessLayer, chainspec: &Chainspec, execution_engine_v1: &ExecutionEngineV1, block_header: BlockHeader, - block_hashes: BTreeMap, input_transaction: Transaction, ) -> SpeculativeExecutionResult where @@ -2037,7 +2019,7 @@ where } }; - let result = state_provider.transfer(TransferRequest::with_runtime_args( + let result = data_access_layer.transfer(TransferRequest::with_runtime_args( native_runtime_config.clone(), *state_root_hash, protocol_version, @@ -2065,7 +2047,9 @@ where gas_limit, &session_input_data, ) { - Ok(wasm_v1_request) => execution_engine_v1.execute(state_provider, wasm_v1_request), + Ok(wasm_v1_request) => { + execution_engine_v1.execute(data_access_layer, wasm_v1_request) + } Err(error) => WasmV1Result::invalid_executable_item(gas_limit, error), }; SpeculativeExecutionResult::WasmV1(Box::new(utils::spec_exec_from_wasm_v1_result( @@ -2094,7 +2078,7 @@ where gas_limit, &session_input_data, ) { - Ok(wasm_v1_request) => execution_engine_v1.execute(state_provider, wasm_v1_request), + Ok(wasm_v1_request) => execution_engine_v1.execute(data_access_layer, wasm_v1_request), Err(error) => WasmV1Result::invalid_executable_item(gas_limit, error), }; SpeculativeExecutionResult::WasmV1(Box::new(utils::spec_exec_from_wasm_v1_result( @@ -2102,13 +2086,7 @@ where block_header.block_hash(), ))) } else if let Some(evm_transaction) = transaction.as_evm() { - speculatively_execute_evm( - state_provider, - chainspec, - block_header, - block_hashes, - evm_transaction, - ) + speculatively_execute_evm(data_access_layer, chainspec, block_header, evm_transaction) } else { // TODO: placeholder error SpeculativeExecutionResult::InvalidTransaction(InvalidTransaction::V1( @@ -2118,10 +2096,9 @@ where } fn speculatively_execute_evm( - state_provider: &S, + data_access_layer: &DataAccessLayer, chainspec: &Chainspec, block_header: BlockHeader, - block_hashes: BTreeMap, evm_transaction: &casper_types::EvmTransaction, ) -> SpeculativeExecutionResult where @@ -2142,7 +2119,7 @@ where } let state_root_hash = block_header.state_root_hash(); - let mut tracking_copy = match state_provider.tracking_copy(*state_root_hash) { + let mut tracking_copy = match data_access_layer.tracking_copy(*state_root_hash) { Ok(Some(tracking_copy)) => tracking_copy, Ok(None) => { return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( @@ -2188,11 +2165,10 @@ where block: block_context, kind, }; - let block_hash_provider = StaticEvmBlockHashProvider { block_hashes }; - let outcome = match EvmExecutor::new(chainspec.evm_config).execute_with_block_hash_provider( + let outcome = match EvmExecutor::new(chainspec.evm_config).execute( + data_access_layer, &mut tracking_copy, execute_request, - &block_hash_provider, ) { Ok(outcome) => outcome, Err(error) => { diff --git a/node/src/components/contract_runtime/tests.rs b/node/src/components/contract_runtime/tests.rs index 714aaebf0d..c735305d0b 100644 --- a/node/src/components/contract_runtime/tests.rs +++ b/node/src/components/contract_runtime/tests.rs @@ -1,6 +1,9 @@ use std::{collections::BTreeMap, iter, path::PathBuf, sync::Arc, time::Duration}; -use casper_storage::data_access_layer::{QueryRequest, QueryResult}; +use casper_storage::{ + block_store::BlockStoreTransaction, + data_access_layer::{QueryRequest, QueryResult}, +}; use derive_more::{Display, From}; use fs_extra::dir; use prometheus::Registry; @@ -9,10 +12,11 @@ use serde::Serialize; use tempfile::TempDir; use casper_types::{ - bytesrepr::Bytes, contracts::ProtocolVersionMajor, runtime_args, BlockHash, Chainspec, - ChainspecRawBytes, Deploy, Digest, EntityVersion, EraId, ExecutableDeployItem, PackageHash, - PricingMode, PublicKey, RuntimeArgs, SecretKey, TimeDiff, Timestamp, Transaction, - TransactionConfig, TransactionRuntimeParams, MINT_LANE_ID, U512, + bytesrepr::Bytes, contracts::ProtocolVersionMajor, evm, runtime_args, BlockHash, BlockHeader, + Chainspec, ChainspecRawBytes, Deploy, Digest, EntityVersion, EraId, EvmTransaction, + ExecutableDeployItem, PackageHash, PricingMode, PublicKey, RuntimeArgs, SecretKey, + TestBlockBuilder, TimeDiff, Timestamp, Transaction, TransactionConfig, + TransactionRuntimeParams, MINT_LANE_ID, U256, U512, }; use super::*; @@ -61,6 +65,8 @@ enum Event { StorageRequest(StorageRequest), #[from] MetaBlockAnnouncement(MetaBlockAnnouncement), + #[from] + NonExecutableBlockAnnouncement(NonExecutableBlockAnnouncement), } impl ReactorEvent for Event { @@ -89,8 +95,6 @@ impl Unhandled for NetworkRequest {} impl Unhandled for UnexecutedBlockAnnouncement {} -impl Unhandled for NonExecutableBlockAnnouncement {} - struct TestConfig { config: Config, fixture_name: Option, @@ -132,6 +136,7 @@ impl reactor::Reactor for Reactor { chainspec.protocol_version(), ) .unwrap(); + let contract_runtime_block_store = storage_block_store.clone(); let mut storage = Storage::new( &storage_withdir, storage_root, @@ -148,8 +153,13 @@ impl reactor::Reactor for Reactor { .unwrap(); storage.initialize_for_test(); - let contract_runtime = - ContractRuntime::new(storage.root_path(), &config.config, chainspec, registry)?; + let contract_runtime = ContractRuntime::new( + storage.root_path(), + contract_runtime_block_store, + &config.config, + chainspec, + registry, + )?; let reactor = Reactor { storage, @@ -194,6 +204,10 @@ impl reactor::Reactor for Reactor { info!("{announcement}"); Effects::new() } + Event::NonExecutableBlockAnnouncement(announcement) => { + info!("{announcement}"); + Effects::new() + } } } } @@ -224,6 +238,182 @@ fn execution_completed(event: &Event) -> bool { matches!(event, Event::MetaBlockAnnouncement(_)) } +#[tokio::test] +async fn block_hash_history_guard_only_applies_to_evm_blocks() { + testing::init_logging(); + + let config = TestConfig { + config: Config::default(), + fixture_name: None, + }; + let (chainspec, chainspec_raw_bytes) = + <(Chainspec, ChainspecRawBytes)>::from_resources("local"); + let chainspec = Arc::new(chainspec); + let chainspec_raw_bytes = Arc::new(chainspec_raw_bytes); + + let mut rng = crate::new_rng(); + let mut runner: Runner> = Runner::new( + config, + Arc::clone(&chainspec), + Arc::clone(&chainspec_raw_bytes), + &mut rng, + ) + .await + .unwrap(); + + let post_commit_genesis_state_hash = runner + .reactor() + .inner() + .contract_runtime + .commit_genesis(chainspec.as_ref(), chainspec_raw_bytes.as_ref()) + .as_legacy() + .expect("should commit genesis") + .0; + // Start at height 1 without storing a block header at height 0. + runner + .reactor_mut() + .inner_mut() + .contract_runtime + .set_execution_pre_state(ExecutionPreState::new( + 1, + post_commit_genesis_state_hash, + BlockHash::default(), + Digest::default(), + )); + + // A non-EVM block bypasses the history preflight and executes. + let non_evm_block = ExecutableBlock::from_finalized_block_and_transactions( + FinalizedBlock::new( + BlockPayload::default(), + None, + Timestamp::now(), + EraId::new(0), + 1, + PublicKey::System, + ), + vec![], + ); + runner + .process_injected_effects(execute_block(non_evm_block)) + .await; + runner + .crank_until(&mut rng, execution_completed, TEST_TIMEOUT) + .await; + + let pre_state_after_non_evm_block = runner + .reactor() + .inner() + .contract_runtime + .execution_pre_state(); + assert_eq!(pre_state_after_non_evm_block.next_block_height(), 2); + + // Store every header in the 256-block BLOCKHASH window for height 257 while leaving height 0 + // absent. This distinguishes the EIP-2935 window from the shorter opcode window. + { + let mut block_store = runner + .reactor() + .inner() + .contract_runtime + .data_access_layer + .block_store + .clone(); + let mut transaction = block_store + .checkout_rw() + .expect("should check out block-store write transaction"); + for height in 2..=256 { + let block = TestBlockBuilder::new().height(height).build(&mut rng); + let header = BlockHeader::V2(block.header().clone()); + transaction + .write_block_header(&header) + .expect("should store block header"); + } + transaction.commit().expect("should commit block headers"); + } + runner + .reactor_mut() + .inner_mut() + .contract_runtime + .set_execution_pre_state(ExecutionPreState::new( + 257, + pre_state_after_non_evm_block.pre_state_root_hash(), + BlockHash::default(), + Digest::default(), + )); + let initial_pre_state = runner + .reactor() + .inner() + .contract_runtime + .execution_pre_state(); + + // The EVM block at height 257 requires headers in 0..257, so it must be refused even though + // the complete 256-block BLOCKHASH window is present. + let transaction = Transaction::from_evm(EvmTransaction::new_unsigned_call( + Timestamp::now(), + TimeDiff::from_seconds(60), + 1, + evm::Address::new([1; 20]), + Some(evm::Address::new([2; 20])), + U256::zero(), + vec![], + 21_000, + 1, + )); + let evm_block = ExecutableBlock::from_finalized_block_and_transactions( + FinalizedBlock::new( + BlockPayload::default(), + None, + Timestamp::now(), + EraId::new(0), + 257, + PublicKey::System, + ), + vec![transaction], + ); + + runner + .process_injected_effects(execute_block(evm_block)) + .await; + + runner + .crank_until( + &mut rng, + |event| { + assert!( + !matches!(event, Event::MetaBlockAnnouncement(_)), + "block with missing EVM block hash history should not be executed" + ); + matches!( + event, + Event::NonExecutableBlockAnnouncement(NonExecutableBlockAnnouncement(257)) + ) + }, + TEST_TIMEOUT, + ) + .await; + + let actual_pre_state = runner + .reactor() + .inner() + .contract_runtime + .execution_pre_state(); + assert_eq!( + actual_pre_state.next_block_height(), + initial_pre_state.next_block_height() + ); + assert_eq!( + actual_pre_state.pre_state_root_hash(), + initial_pre_state.pre_state_root_hash() + ); + assert_eq!( + actual_pre_state.parent_hash(), + initial_pre_state.parent_hash() + ); + assert_eq!( + actual_pre_state.parent_seed(), + initial_pre_state.parent_seed() + ); +} + #[tokio::test] async fn should_not_set_shared_pre_state_to_lower_block_height() { testing::init_logging(); @@ -880,6 +1070,7 @@ mod test_mod { use tempfile::tempdir; use casper_storage::{ + block_store::lmdb::LmdbBlockStore, data_access_layer::{EntryPointExistsRequest, EntryPointExistsResult}, global_state::{ state::{CommitProvider, StateProvider}, @@ -1034,8 +1225,10 @@ mod test_mod { system_costs_config: Default::default(), ..Chainspec::random(rng) }; + let block_store = LmdbBlockStore::new(temp_dir.path(), 64 * 1024 * 1024).unwrap(); let contract_runtime = ContractRuntime::new( temp_dir.path(), + block_store, &ContractRuntimeConfig::default(), Arc::new(chainspec), &Registry::default(), diff --git a/node/src/components/contract_runtime/utils.rs b/node/src/components/contract_runtime/utils.rs index 280a046340..c951d4652e 100644 --- a/node/src/components/contract_runtime/utils.rs +++ b/node/src/components/contract_runtime/utils.rs @@ -1,6 +1,3 @@ -use casper_executor_evm::{ - BlockHashProvider as EvmBlockHashProvider, BlockHashProviderResult, BLOCK_HASH_HISTORY, -}; use casper_executor_wasm::ExecutorV2; use num_rational::Ratio; use once_cell::sync::Lazy; @@ -36,9 +33,10 @@ use casper_binary_port::SpeculativeExecutionResult; use casper_execution_engine::engine_state::{ExecutionEngineV1, WasmV1Result}; use casper_storage::{ data_access_layer::{DataAccessLayer, TransferResult}, + eip2935, global_state::state::lmdb::LmdbGlobalState, }; -use casper_types::{BlockHash, Chainspec, EraId, Gas, Key, Transaction}; +use casper_types::{BlockHash, Chainspec, EraId, Gas, Key}; /// Maximum number of resource intensive tasks that can be run in parallel. /// @@ -48,45 +46,8 @@ const MAX_PARALLEL_INTENSIVE_TASKS: usize = 4; static INTENSIVE_TASKS_SEMAPHORE: Lazy = Lazy::new(|| tokio::sync::Semaphore::new(MAX_PARALLEL_INTENSIVE_TASKS)); -#[derive(Clone, Debug, Default)] -struct RecentBlockHashProvider { - block_hashes: BTreeMap, -} - -impl RecentBlockHashProvider { - async fn load(effect_builder: EffectBuilder, current_block_height: u64) -> Self - where - REv: From, - { - let block_hashes = load_recent_evm_block_hashes(effect_builder, current_block_height).await; - RecentBlockHashProvider { block_hashes } - } -} - -impl EvmBlockHashProvider for RecentBlockHashProvider { - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { - Ok(self.block_hashes.get(&block_height).copied()) - } -} - -pub(crate) async fn load_recent_evm_block_hashes( - effect_builder: EffectBuilder, - current_block_height: u64, -) -> BTreeMap -where - REv: From, -{ - let earliest_block_height = current_block_height.saturating_sub(BLOCK_HASH_HISTORY); - let mut block_hashes = BTreeMap::new(); - for block_height in earliest_block_height..current_block_height { - if let Some(header) = effect_builder - .get_block_header_at_height_from_storage(block_height, true) - .await - { - block_hashes.insert(block_height, header.block_hash()); - } - } - block_hashes +fn block_hash_history_range(block_height: u64) -> Range { + block_height.saturating_sub(eip2935::HISTORY_BUFFER_LENGTH)..block_height } /// Asynchronously runs a resource intensive task. @@ -260,6 +221,49 @@ pub(super) async fn exec_and_check_next( { debug!("ContractRuntime: execute_finalized_block_or_requeue"); + if executable_block + .transactions + .iter() + .any(|transaction| transaction.as_evm().is_some()) + { + let block_height = executable_block.height; + let block_history_range = block_hash_history_range(block_height); + let block_history_start = block_history_range.start; + let first_missing_block_header_height = { + // Keep the read transaction out of the async announcement and fatal paths below. + match data_access_layer.block_store.checkout_ro() { + Ok(txn) => txn.first_missing_block_header_height(block_history_range), + Err(error) => Err(error), + } + }; + + match first_missing_block_header_height { + Ok(Some(missing_block_height)) => { + info!( + %block_height, + %missing_block_height, + %block_history_start, + "ContractRuntime: not enough block history to execute block containing EVM \ + transactions. Abandoning the execution." + ); + effect_builder + .announce_not_executing_block(block_height) + .await; + return; + } + Ok(None) => {} + Err(error) => { + return fatal!( + effect_builder, + "failed to check EVM block hash history for block {}: {}", + block_height, + error + ) + .await; + } + } + } + // FIRST determine if we are aware of the last switch block header let era_id = executable_block.era_id; let last_switch_block_hash = match era_id.predecessor() { @@ -314,15 +318,6 @@ pub(super) async fn exec_and_check_next( }; let current_gas_price = executable_block.current_gas_price; - let evm_block_hash_provider = if executable_block - .transactions - .iter() - .any(|transaction| matches!(transaction, Transaction::Evm(_))) - { - RecentBlockHashProvider::load(effect_builder, executable_block.height).await - } else { - RecentBlockHashProvider::default() - }; let contract_runtime_metrics = metrics.clone(); let task = move || { debug!("ContractRuntime: execute_finalized_block"); @@ -333,7 +328,6 @@ pub(super) async fn exec_and_check_next( chainspec.as_ref(), Some(contract_runtime_metrics), current_pre_state, - &evm_block_hash_provider, executable_block, key_block_height_for_activation_point, current_gas_price, @@ -579,6 +573,14 @@ pub(crate) fn spec_exec_from_wasm_v1_result( mod tests { use super::*; + #[test] + fn block_hash_history_ranges_cover_boundary_heights() { + assert_eq!(block_hash_history_range(0), 0..0); + assert_eq!(block_hash_history_range(1), 0..1); + assert_eq!(block_hash_history_range(8_191), 0..8_191); + assert_eq!(block_hash_history_range(8_192), 1..8_192); + } + #[test] fn calculation_is_safe_with_invalid_input() { assert_eq!(calculate_prune_eras(EraId::new(0), 0, 0, 0), None); diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index 885a686715..4f8bce1acc 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -41,12 +41,12 @@ mod tests; mod utils; use casper_storage::block_store::{ - lmdb::LmdbBlockStore, + lmdb::{LmdbBlockStore, LmdbBlockStoreReadTransaction}, types::{ - ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockHeight, BlockTransfers, - LatestSwitchBlock, StateStore, StateStoreKey, Tip, TransactionFinalizedApprovals, + ApprovalsHashes, BlockExecutionResults, BlockHashHeightAndEra, BlockTransfers, StateStore, + StateStoreKey, TransactionFinalizedApprovals, }, - BlockStoreError, BlockStoreProvider, BlockStoreTransaction, DataReader, DataWriter, + BlockStoreTransaction, }; use std::{ @@ -60,7 +60,6 @@ use std::{ sync::Arc, }; -use casper_storage::DbRawBytesSpec; #[cfg(test)] use casper_types::BlockWithSignatures; use casper_types::{ @@ -353,7 +352,7 @@ pub fn prune_block_store( let tip_height = { let ro_txn = block_store.checkout_ro()?; - match DataReader::::read(&ro_txn, Tip)? { + match ro_txn.read_tip_block_header()? { Some(header) => header.height(), None => { info!("block store is empty, nothing to prune"); @@ -378,7 +377,7 @@ pub fn prune_block_store( height, total_headers, "pruning block store: scanning" ); } - let header: BlockHeader = match ro_txn.read(height)? { + let header = match ro_txn.read_block_header_at_height(height)? { Some(header) => header, None => continue, }; @@ -413,15 +412,16 @@ pub fn prune_block_store( { let mut rw_txn = block_store.checkout_rw()?; for (block_hash, block_height, era_id) in blocks_to_delete { - DataWriter::::delete( - &mut rw_txn, - BlockHashHeightAndEra::new(block_hash, block_height, era_id), - )?; - DataWriter::::delete(&mut rw_txn, block_hash)?; + rw_txn.delete_execution_results(BlockHashHeightAndEra::new( + block_hash, + block_height, + era_id, + ))?; + rw_txn.delete_block(block_hash)?; } for (body_hash, retained) in body_hash_retained { if !retained { - DataWriter::::delete(&mut rw_txn, body_hash)?; + rw_txn.delete_block_body(body_hash)?; } } rw_txn.commit()?; @@ -508,16 +508,16 @@ impl Storage { { let ro_txn = self.block_store.checkout_ro()?; - let maybe_state_store: Option> = ro_txn.read(StateStoreKey::new( - Cow::Borrowed(COMPLETED_BLOCKS_STORAGE_KEY), - ))?; + let maybe_state_store = ro_txn.read_state_store(StateStoreKey::new(Cow::Borrowed( + COMPLETED_BLOCKS_STORAGE_KEY, + )))?; match maybe_state_store { Some(raw) => { let (mut sequences, _) = DisjointSequences::from_vec(raw) .map_err(FatalStorageError::UnexpectedDeserializationFailure)?; // Truncate the sequences in case we removed blocks via a hard reset. - if let Some(header) = DataReader::::read(&ro_txn, Tip)? { + if let Some(header) = ro_txn.read_tip_block_header()? { sequences.truncate(header.height()); } else { // No tip left, the database is empty @@ -545,10 +545,10 @@ impl Storage { // `completed_blocks` in this None case, we'll only consider blocks // from a previous protocol version as complete. - let maybe_block_header: Option = ro_txn.read(Tip)?; + let maybe_block_header = ro_txn.read_tip_block_header()?; if let Some(highest_block_header) = maybe_block_header { for height in (0..=highest_block_header.height()).rev() { - let maybe_header: Option = ro_txn.read(height)?; + let maybe_header = ro_txn.read_block_header_at_height(height)?; match maybe_header { Some(header) if header.protocol_version() < self.protocol_version => @@ -592,8 +592,8 @@ impl Storage { { let txn_ro = self.block_store.checkout_ro()?; for block_height in (low..=high).rev() { - let block: Block = txn_ro - .read(block_height)? + let block = txn_ro + .read_block_at_height(block_height)? .ok_or(FatalStorageError::BlockNotFound(block_height))?; let block_hash = *block.hash(); let era_id = block.era_id(); @@ -701,11 +701,11 @@ impl Storage { } NetRequest::Block(ref serialized_id) => { let id = decode_item_id::(serialized_id)?; - let opt_item: Option = self + let opt_item = self .block_store .checkout_ro() .map_err(FatalStorageError::from)? - .read(id) + .read_block_by_hash(id) .map_err(FatalStorageError::from)?; let fetch_response = FetchResponse::from_opt(id, opt_item); @@ -718,11 +718,11 @@ impl Storage { } NetRequest::BlockHeader(ref serialized_id) => { let item_id = decode_item_id::(serialized_id)?; - let opt_item: Option = self + let opt_item = self .block_store .checkout_ro() .map_err(FatalStorageError::from)? - .read(item_id) + .read_block_header_by_hash(item_id) .map_err(FatalStorageError::from)?; let fetch_response = FetchResponse::from_opt(item_id, opt_item); @@ -739,7 +739,7 @@ impl Storage { .block_store .checkout_ro() .map_err(FatalStorageError::from)? - .read(*id.block_hash()) + .read_block_signatures_by_hash(*id.block_hash()) .map_err(FatalStorageError::from)? .and_then(|block_signatures: BlockSignatures| { block_signatures.finality_signature(id.public_key()) @@ -775,11 +775,11 @@ impl Storage { } NetRequest::ApprovalsHashes(ref serialized_id) => { let item_id = decode_item_id::(serialized_id)?; - let opt_item: Option = self + let opt_item = self .block_store .checkout_ro() .map_err(FatalStorageError::from)? - .read(item_id) + .read_approvals_hashes_by_hash(item_id) .map_err(FatalStorageError::from)?; let fetch_response = FetchResponse::from_opt(item_id, opt_item); @@ -816,7 +816,7 @@ impl Storage { Ok(match req { StorageRequest::PutBlock { block, responder } => { let mut rw_txn = self.block_store.checkout_rw()?; - let _ = rw_txn.write(&*block)?; + let _ = rw_txn.write_block(&block)?; rw_txn.commit()?; responder.respond(true).ignore() } @@ -825,7 +825,7 @@ impl Storage { responder, } => { let mut rw_txn = self.block_store.checkout_rw()?; - let _ = rw_txn.write(&*approvals_hashes)?; + let _ = rw_txn.write_approvals_hashes(&approvals_hashes)?; rw_txn.commit()?; responder.respond(true).ignore() } @@ -833,7 +833,10 @@ impl Storage { block_hash, responder, } => { - let maybe_block = self.block_store.checkout_ro()?.read(block_hash)?; + let maybe_block = self + .block_store + .checkout_ro()? + .read_block_by_hash(block_hash)?; responder.respond(maybe_block).ignore() } StorageRequest::IsBlockStored { @@ -842,14 +845,17 @@ impl Storage { } => { let txn = self.block_store.checkout_ro()?; responder - .respond(DataReader::::exists(&txn, block_hash)?) + .respond(txn.block_exists_by_hash(block_hash)?) .ignore() } StorageRequest::GetApprovalsHashes { block_hash, responder, } => { - let maybe_item = self.block_store.checkout_ro()?.read(block_hash)?; + let maybe_item = self + .block_store + .checkout_ro()? + .read_approvals_hashes_by_hash(block_hash)?; responder.respond(maybe_item).ignore() } StorageRequest::GetHighestCompleteBlock { responder } => responder @@ -865,8 +871,8 @@ impl Storage { let mut era_ids = HashSet::new(); let txn = self.block_store.checkout_ro()?; for transaction_hash in &transaction_hashes { - let maybe_block_info: Option = - txn.read(*transaction_hash)?; + let maybe_block_info = + txn.read_block_info_by_transaction_hash(*transaction_hash)?; if let Some(block_info) = maybe_block_info { era_ids.insert(block_info.era_id); } @@ -899,11 +905,10 @@ impl Storage { responder, } => { let mut rw_txn = self.block_store.checkout_rw()?; - if DataReader::::exists(&rw_txn, transaction.hash())? - { + if rw_txn.transaction_exists(transaction.hash())? { responder.respond(false).ignore() } else { - let _ = rw_txn.write(&*transaction)?; + let _ = rw_txn.write_transaction(&transaction)?; rw_txn.commit()?; responder.respond(true).ignore() } @@ -964,19 +969,19 @@ impl Storage { None => return Ok(responder.respond(None).ignore()), } } else { - match ro_txn.read(transaction_hash)? { + match ro_txn.read_transaction(transaction_hash)? { Some(transaction) => transaction, None => return Ok(responder.respond(None).ignore()), } }; - let block_hash_height_and_era: BlockHashHeightAndEra = - match ro_txn.read(transaction_hash)? { + let block_hash_height_and_era = + match ro_txn.read_block_info_by_transaction_hash(transaction_hash)? { Some(value) => value, None => return Ok(responder.respond(Some((transaction, None))).ignore()), }; - let execution_result = ro_txn.read(transaction_hash)?; + let execution_result = ro_txn.read_execution_result(transaction_hash)?; let execution_info = ExecutionInfo { block_hash: block_hash_height_and_era.block_hash, block_height: block_hash_height_and_era.block_height, @@ -992,10 +997,7 @@ impl Storage { responder, } => { let txn = self.block_store.checkout_ro()?; - let has_transaction = DataReader::::exists( - &txn, - transaction_id.transaction_hash(), - )?; + let has_transaction = txn.transaction_exists(transaction_id.transaction_hash())?; responder.respond(has_transaction).ignore() } StorageRequest::GetExecutionResults { @@ -1022,7 +1024,7 @@ impl Storage { } => { let block_hash = *block_hash; let mut rw_txn = self.block_store.checkout_rw()?; - let maybe_block: Option = rw_txn.read(block_hash)?; + let maybe_block = rw_txn.read_block_by_hash(block_hash)?; if let Some(block) = maybe_block { let utilization = Self::calculate_block_utilization( &self.transaction_config, @@ -1036,7 +1038,7 @@ impl Storage { utilization, ); } - let _ = rw_txn.write(&BlockExecutionResults { + let _ = rw_txn.write_execution_results(&BlockExecutionResults { block_info: BlockHashHeightAndEra::new(block_hash, block_height, era_id), exec_results: execution_results, })?; @@ -1047,7 +1049,7 @@ impl Storage { let maybe_sig = self .block_store .checkout_ro()? - .read(*id.block_hash())? + .read_block_signatures_by_hash(*id.block_hash())? .and_then(|sigs: BlockSignatures| sigs.finality_signature(id.public_key())) .filter(|sig| sig.era_id() == id.era_id()); responder.respond(maybe_sig).ignore() @@ -1056,7 +1058,7 @@ impl Storage { let has_signature = self .block_store .checkout_ro()? - .read(*id.block_hash())? + .read_block_signatures_by_hash(*id.block_hash())? .map(|sigs: BlockSignatures| sigs.has_finality_signature(id.public_key())) .unwrap_or(false); responder.respond(has_signature).ignore() @@ -1072,8 +1074,8 @@ impl Storage { let ro_txn = self.block_store.checkout_ro()?; - let block: Block = { - if let Some(block) = ro_txn.read(block_height)? { + let block = { + if let Some(block) = ro_txn.read_block_at_height(block_height)? { block } else { return Ok(responder.respond(None).ignore()); @@ -1081,7 +1083,7 @@ impl Storage { }; let hash = block.hash(); - let block_signatures = match ro_txn.read(*hash)? { + let block_signatures = match ro_txn.read_block_signatures_by_hash(*hash)? { Some(signatures) => signatures, None => self.get_default_block_signatures(&block), }; @@ -1104,7 +1106,7 @@ impl Storage { return Ok(responder.respond(false).ignore()); } let mut txn = self.block_store.checkout_rw()?; - let old_data: Option = txn.read(*signatures.block_hash())?; + let old_data = txn.read_block_signatures_by_hash(*signatures.block_hash())?; let new_data = match old_data { None => signatures, Some(mut data) => { @@ -1115,7 +1117,7 @@ impl Storage { data } }; - let _ = txn.write(&new_data)?; + let _ = txn.write_block_signatures(&new_data)?; txn.commit()?; responder.respond(true).ignore() } @@ -1125,24 +1127,25 @@ impl Storage { } => { let mut rw_txn = self.block_store.checkout_rw()?; let block_hash = signature.block_hash(); - let mut block_signatures: BlockSignatures = - if let Some(existing_signatures) = rw_txn.read(*block_hash)? { - existing_signatures - } else { - match &*signature { - FinalitySignature::V1(signature) => { - BlockSignaturesV1::new(*signature.block_hash(), signature.era_id()) - .into() - } - FinalitySignature::V2(signature) => BlockSignaturesV2::new( - *signature.block_hash(), - signature.block_height(), - signature.era_id(), - signature.chain_name_hash(), - ) - .into(), + let mut block_signatures = if let Some(existing_signatures) = + rw_txn.read_block_signatures_by_hash(*block_hash)? + { + existing_signatures + } else { + match &*signature { + FinalitySignature::V1(signature) => { + BlockSignaturesV1::new(*signature.block_hash(), signature.era_id()) + .into() } - }; + FinalitySignature::V2(signature) => BlockSignaturesV2::new( + *signature.block_hash(), + signature.block_height(), + signature.era_id(), + signature.chain_name_hash(), + ) + .into(), + } + }; match (&mut block_signatures, *signature) { ( BlockSignatures::V1(ref mut block_signatures), @@ -1169,7 +1172,7 @@ impl Storage { } } - let _ = rw_txn.write(&block_signatures); + let _ = rw_txn.write_block_signatures(&block_signatures); rw_txn.commit()?; responder.respond(true).ignore() } @@ -1178,8 +1181,10 @@ impl Storage { public_key, responder, } => { - let maybe_signatures: Option = - self.block_store.checkout_ro()?.read(block_hash)?; + let maybe_signatures = self + .block_store + .checkout_ro()? + .read_block_signatures_by_hash(block_hash)?; responder .respond( maybe_signatures @@ -1198,12 +1203,12 @@ impl Storage { } StorageRequest::GetLatestSwitchBlockHeader { responder } => { let txn = self.block_store.checkout_ro()?; - let maybe_header = txn.read(LatestSwitchBlock)?; + let maybe_header = txn.read_latest_switch_block_header()?; responder.respond(maybe_header).ignore() } StorageRequest::GetSwitchBlockHeaderByEra { era_id, responder } => { let txn = self.block_store.checkout_ro()?; - let maybe_header = txn.read(era_id)?; + let maybe_header = txn.read_block_header_by_era(era_id)?; responder.respond(maybe_header).ignore() } StorageRequest::PutBlockHeader { @@ -1211,7 +1216,7 @@ impl Storage { responder, } => { let mut rw_txn = self.block_store.checkout_rw()?; - let _ = rw_txn.write(&*block_header)?; + let _ = rw_txn.write_block_header(&block_header)?; rw_txn.commit()?; responder.respond(true).ignore() } @@ -1248,7 +1253,7 @@ impl Storage { if self.key_block_height_for_activation_point.is_none() { let key_block_era = self.activation_era.predecessor().unwrap_or_default(); let txn = self.block_store.checkout_ro()?; - let key_block_header: BlockHeader = match txn.read(key_block_era)? { + let key_block_header = match txn.read_block_header_by_era(key_block_era)? { Some(block_header) => block_header, None => return Ok(responder.respond(None).ignore()), }; @@ -1266,7 +1271,7 @@ impl Storage { let db_table_id = utils::db_table_id_from_record_id(record_id) .map_err(|_| FatalStorageError::UnexpectedRecordId(record_id))?; let txn = self.block_store.checkout_ro()?; - let maybe_data: Option = txn.read((db_table_id, key))?; + let maybe_data = txn.read_raw(db_table_id, key)?; match maybe_data { None => responder.respond(None).ignore(), Some(db_raw) => responder.respond(Some(db_raw)).ignore(), @@ -1295,7 +1300,8 @@ impl Storage { Ok(None) } else { let txn = self.block_store.checkout_ro()?; - txn.read(block_height).map_err(FatalStorageError::from) + txn.read_block_header_at_height(block_height) + .map_err(FatalStorageError::from) } } @@ -1304,7 +1310,8 @@ impl Storage { era_id: &EraId, ) -> Result, FatalStorageError> { let txn = self.block_store.checkout_ro()?; - txn.read(*era_id).map_err(FatalStorageError::from) + txn.read_switch_block_by_era(*era_id) + .map_err(FatalStorageError::from) } /// Retrieves a set of transactions, along with their potential finalized approvals. @@ -1331,15 +1338,15 @@ impl Storage { ) -> Result { let mut txn = self.block_store.checkout_rw()?; let era_id = block.era_id(); - let block_hash = txn.write(block)?; - let _ = txn.write(approvals_hashes)?; + let block_hash = txn.write_block(block)?; + let _ = txn.write_approvals_hashes(approvals_hashes)?; let utilization = Self::calculate_block_utilization(&self.transaction_config, block, &execution_results); let block_info = BlockHashHeightAndEra::new(block_hash, block.height(), block.era_id()); debug!("Utilization for block is {utilization}"); - let _ = txn.write(&BlockExecutionResults { + let _ = txn.write_execution_results(&BlockExecutionResults { block_info, exec_results: execution_results, })?; @@ -1395,7 +1402,7 @@ impl Storage { .to_bytes() .map_err(FatalStorageError::UnexpectedSerializationFailure)?; let mut rw_txn = self.block_store.checkout_rw()?; - rw_txn.write(&StateStore { + rw_txn.write_state_store(&StateStore { key: Cow::Borrowed(COMPLETED_BLOCKS_STORAGE_KEY), value: serialized, })?; @@ -1418,20 +1425,19 @@ impl Storage { ) -> Result, FatalStorageError> { let ro_txn = self.block_store.checkout_ro()?; - let timestamp = - match DataReader::::read(&ro_txn, LatestSwitchBlock)? { - Some(last_era_header) => last_era_header - .timestamp() - .saturating_sub(self.max_ttl.value()), - None => Timestamp::now(), - }; + let timestamp = match ro_txn.read_latest_switch_block_header()? { + Some(last_era_header) => last_era_header + .timestamp() + .saturating_sub(self.max_ttl.value()), + None => Timestamp::now(), + }; let mut blocks = Vec::new(); for sequence in self.completed_blocks.sequences().iter().rev() { let hi = sequence.high(); let low = sequence.low(); for idx in (low..=hi).rev() { - let maybe_block: Result, BlockStoreError> = ro_txn.read(idx); + let maybe_block = ro_txn.read_block_at_height(idx); match maybe_block { Ok(Some(block)) => { let should_continue = block.timestamp() >= timestamp; @@ -1466,8 +1472,10 @@ impl Storage { return Ok(None); } }; - let maybe_finalized_approvals: Option = - self.block_store.checkout_ro()?.read(*block.hash())?; + let maybe_finalized_approvals = self + .block_store + .checkout_ro()? + .read_approvals_hashes_by_hash(*block.hash())?; if let Some(finalized_approvals) = maybe_finalized_approvals { if transactions.len() != finalized_approvals.approvals_hashes().len() { error!( @@ -1513,7 +1521,7 @@ impl Storage { ) -> Result)>, FatalStorageError> { let txn = self.block_store.checkout_ro()?; - let Some(block) = txn.read(block_hash)? else { + let Some(block) = txn.read_block_by_hash(block_hash)? else { debug!( ?block_hash, "Storage: read_block_and_finalized_transactions_by_hash failed to get block for {}", @@ -1554,7 +1562,7 @@ impl Storage { }; let txn = self.block_store.checkout_ro()?; - txn.read(highest_complete_block_height) + txn.read_block_header_at_height(highest_complete_block_height) .map_err(FatalStorageError::from) } @@ -1562,7 +1570,7 @@ impl Storage { /// LMDB error. fn get_highest_complete_block_header_with_signatures( &self, - txn: &(impl DataReader + DataReader), + txn: &LmdbBlockStoreReadTransaction<'_>, ) -> Result, FatalStorageError> { let highest_complete_block_height = match self.completed_blocks.highest_sequence() { Some(sequence) => sequence.high(), @@ -1571,11 +1579,11 @@ impl Storage { } }; - let block_header: Option = txn.read(highest_complete_block_height)?; + let block_header = txn.read_block_header_at_height(highest_complete_block_height)?; match block_header { Some(header) => { let block_header_hash = header.block_hash(); - let block_signatures: BlockSignatures = match txn.read(block_header_hash)? { + let block_signatures = match txn.read_block_signatures_by_hash(block_header_hash)? { Some(signatures) => signatures, None => match &header { BlockHeader::V1(header) => BlockSignatures::V1(BlockSignaturesV1::new( @@ -1609,7 +1617,7 @@ impl Storage { }; let txn = self.block_store.checkout_ro()?; - txn.read(highest_complete_block_height) + txn.read_block_at_height(highest_complete_block_height) .map_err(FatalStorageError::from) } @@ -1618,11 +1626,11 @@ impl Storage { /// should be present in the available blocks index. fn get_single_block_header_restricted( &self, - txn: &impl DataReader, + txn: &LmdbBlockStoreReadTransaction<'_>, block_hash: &BlockHash, only_from_available_block_range: bool, ) -> Result, FatalStorageError> { - let block_header = match txn.read(*block_hash)? { + let block_header = match txn.read_block_header_by_hash(*block_hash)? { Some(header) => header, None => return Ok(None), }; @@ -1638,7 +1646,7 @@ impl Storage { /// recent switch block. fn get_trusted_ancestor_headers( &self, - txn: &impl DataReader, + txn: &LmdbBlockStoreReadTransaction<'_>, trusted_block_header: &BlockHeader, ) -> Result>, FatalStorageError> { if trusted_block_header.is_genesis() { @@ -1649,7 +1657,7 @@ impl Storage { let mut current_trusted_block_header = trusted_block_header.clone(); loop { let parent_hash = current_trusted_block_header.parent_hash(); - let parent_block_header: BlockHeader = match txn.read(*parent_hash)? { + let parent_block_header = match txn.read_block_header_by_hash(*parent_hash)? { Some(block_header) => block_header, None => { warn!(%parent_hash, "block header not found"); @@ -1675,7 +1683,7 @@ impl Storage { /// highest block, with signatures, plus the signed highest block. fn get_block_headers_with_signatures( &self, - txn: &(impl DataReader + DataReader), + txn: &LmdbBlockStoreReadTransaction<'_>, trusted_block_header: &BlockHeader, highest_block_header_with_signatures: &BlockHeaderWithSignatures, ) -> Result>, FatalStorageError> { @@ -1696,10 +1704,12 @@ impl Storage { let mut result = vec![]; for era_id in start_era_id..current_era_id { - let maybe_block_header: Option = txn.read(EraId::from(era_id))?; + let maybe_block_header = txn.read_block_header_by_era(EraId::from(era_id))?; match maybe_block_header { Some(block_header) => { - let block_signatures = match txn.read(block_header.block_hash())? { + let block_signatures = match txn + .read_block_signatures_by_hash(block_header.block_hash())? + { Some(signatures) => signatures, None => match &block_header { BlockHeader::V1(header) => BlockSignatures::V1(BlockSignaturesV1::new( @@ -1737,22 +1747,21 @@ impl Storage { finalized_approvals: &BTreeSet, ) -> Result { let mut txn = self.block_store.checkout_rw()?; - let original_transaction: Transaction = txn.read(*transaction_hash)?.ok_or({ + let original_transaction = txn.read_transaction(*transaction_hash)?.ok_or({ FatalStorageError::UnexpectedFinalizedApprovals { transaction_hash: *transaction_hash, } })?; // Only store the finalized approvals if they are different from the original ones. - let maybe_existing_finalized_approvals: Option> = - txn.read(*transaction_hash)?; + let maybe_existing_finalized_approvals = txn.read_finalized_approvals(*transaction_hash)?; if maybe_existing_finalized_approvals.as_ref() == Some(finalized_approvals) { return Ok(false); } let original_approvals = original_transaction.approvals(); if &original_approvals != finalized_approvals { - let _ = txn.write(&TransactionFinalizedApprovals { + let _ = txn.write_finalized_approvals(&TransactionFinalizedApprovals { transaction_hash: *transaction_hash, finalized_approvals: finalized_approvals.clone(), })?; @@ -1775,14 +1784,14 @@ impl Storage { block_hash: &BlockHash, ) -> Result>, FatalStorageError> { let mut rw_txn = self.block_store.checkout_rw()?; - let maybe_transfers: Option> = rw_txn.read(*block_hash)?; + let maybe_transfers = rw_txn.read_transfers(*block_hash)?; if let Some(transfers) = maybe_transfers { if !transfers.is_empty() { return Ok(Some(transfers)); } } - let block: Block = match rw_txn.read(*block_hash)? { + let block = match rw_txn.read_block_by_hash(*block_hash)? { Some(block) => block, None => return Ok(None), }; @@ -1801,7 +1810,7 @@ impl Storage { let mut transfers: Vec = vec![]; for deploy_hash in deploy_hashes { let transaction_hash = TransactionHash::Deploy(deploy_hash); - let successful_xfers = match rw_txn.read(transaction_hash)? { + let successful_xfers = match rw_txn.read_execution_result(transaction_hash)? { Some(exec_result) => successful_transfers(&exec_result), None => { error!(%deploy_hash, %block_hash, "should have exec result"); @@ -1810,7 +1819,7 @@ impl Storage { }; transfers.extend(successful_xfers); } - rw_txn.write(&BlockTransfers { + rw_txn.write_transfers(&BlockTransfers { block_hash: *block_hash, transfers: transfers.clone(), })?; @@ -1855,8 +1864,8 @@ impl Storage { let transaction_hash = transaction_id.transaction_hash(); let txn = self.block_store.checkout_ro()?; - let maybe_transaction: Option = txn.read(transaction_hash)?; - let transaction: Transaction = match maybe_transaction { + let maybe_transaction = txn.read_transaction(transaction_hash)?; + let transaction = match maybe_transaction { None => return Ok(None), Some(transaction) if transaction.fetch_id() == transaction_id => { return Ok(Some(transaction)); @@ -1864,7 +1873,7 @@ impl Storage { Some(transaction) => transaction, }; - let finalized_approvals = match txn.read(transaction_hash)? { + let finalized_approvals = match txn.read_finalized_approvals(transaction_hash)? { None => return Ok(None), Some(approvals) => approvals, }; @@ -1921,17 +1930,16 @@ impl Storage { /// Retrieves a single transaction along with its finalized approvals. #[allow(clippy::type_complexity)] fn get_transaction_with_finalized_approvals( - txn: &(impl DataReader - + DataReader>), + txn: &LmdbBlockStoreReadTransaction<'_>, transaction_hash: &TransactionHash, ) -> Result>)>, FatalStorageError> { - let maybe_transaction: Option = txn.read(*transaction_hash)?; + let maybe_transaction = txn.read_transaction(*transaction_hash)?; let transaction = match maybe_transaction { Some(transaction) => transaction, None => return Ok(None), }; - let maybe_finalized_approvals: Option> = txn.read(*transaction_hash)?; + let maybe_finalized_approvals = txn.read_finalized_approvals(*transaction_hash)?; let ret = (transaction, maybe_finalized_approvals); Ok(Some(ret)) @@ -2077,7 +2085,7 @@ impl Storage { .checkout_ro() .expect("Could not start transaction for lmdb"); - match txn.read(low) { + match txn.read_block_at_height(low) { Ok(Some(block)) => match block { Block::V1(_) | Block::V2(_) => { HighestOrphanedBlockResult::Orphan(block.clone_header()) @@ -2095,9 +2103,7 @@ impl Storage { count: u64, ) -> Result, FatalStorageError> { let txn = self.block_store.checkout_ro()?; - if let Some(last_era_header) = - DataReader::::read(&txn, LatestSwitchBlock)? - { + if let Some(last_era_header) = txn.read_latest_switch_block_header()? { let mut result = vec![]; let last_era_id = last_era_header.era_id(); result.push(last_era_header); @@ -2106,7 +2112,7 @@ impl Storage { .take(count as usize) .map(EraId::new) { - match txn.read(era_id)? { + match txn.read_block_header_by_era(era_id)? { None => break, Some(header) => result.push(header), } @@ -2173,14 +2179,16 @@ impl Storage { ) -> Result, FatalStorageError> { let ro_txn = self.block_store.checkout_ro()?; - ro_txn.read(*block_hash).map_err(FatalStorageError::from) + ro_txn + .read_block_header_by_hash(*block_hash) + .map_err(FatalStorageError::from) } fn get_execution_results( - txn: &(impl DataReader + DataReader), + txn: &LmdbBlockStoreReadTransaction<'_>, block_hash: &BlockHash, ) -> Result>, FatalStorageError> { - let block = txn.read(*block_hash)?; + let block = txn.read_block_by_hash(*block_hash)?; let block_body = match block { Some(block) => block.take_body(), @@ -2199,9 +2207,7 @@ impl Storage { #[allow(clippy::type_complexity)] fn get_execution_results_with_transaction_headers( - txn: &(impl DataReader - + DataReader - + DataReader), + txn: &LmdbBlockStoreReadTransaction<'_>, block_hash: &BlockHash, ) -> Result>, FatalStorageError> { @@ -2212,7 +2218,7 @@ impl Storage { let mut ret = Vec::with_capacity(execution_results.len()); for (transaction_hash, execution_result) in execution_results { - match txn.read(transaction_hash)? { + match txn.read_transaction(transaction_hash)? { None => { error!( %block_hash, @@ -2374,13 +2380,13 @@ impl Storage { } fn fetch_results_for_transactions( - txn: &(impl DataReader + DataReader), + txn: &LmdbBlockStoreReadTransaction<'_>, block_hash: &BlockHash, transaction_hashes: Vec, ) -> Result>, FatalStorageError> { let mut execution_results = vec![]; for transaction_hash in transaction_hashes { - match txn.read(transaction_hash)? { + match txn.read_execution_result(transaction_hash)? { None => { debug!( %block_hash, @@ -2559,7 +2565,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(*transaction_hash) + .read_execution_result(*transaction_hash) .expect("could not retrieve execution result from storage") } @@ -2575,7 +2581,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(transaction_hash) + .read_transaction(transaction_hash) .expect("could not retrieve value from storage") } @@ -2583,7 +2589,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(block_hash) + .read_block_by_hash(block_hash) .expect("could not retrieve value from storage") } @@ -2591,7 +2597,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(height) + .read_block_at_height(height) .expect("could not retrieve value from storage") } @@ -2599,7 +2605,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(Tip) + .read_tip_block() .expect("could not retrieve value from storage") } @@ -2607,7 +2613,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(Tip) + .read_tip_block_header() .expect("could not retrieve value from storage") } @@ -2619,8 +2625,8 @@ impl Storage { .block_store .checkout_ro() .expect("could not create RO transaction"); - let res: Option = txn - .read(block_hash) + let res = txn + .read_block_signatures_by_hash(block_hash) .expect("could not retrieve value from storage"); txn.commit().expect("Could not commit transaction"); res @@ -2630,7 +2636,7 @@ impl Storage { self.block_store .checkout_ro() .expect("could not create RO transaction") - .read(era_id) + .read_switch_block_by_era(era_id) .expect("could not retrieve value from storage") } @@ -2643,7 +2649,9 @@ impl Storage { .block_store .checkout_ro() .expect("should create ro txn"); - let block: Block = ro_txn.read(block_hash).expect("should read block")?; + let block = ro_txn + .read_block_by_hash(block_hash) + .expect("should read block")?; if !(self.should_return_block(block.height(), only_from_available_block_range)) { return None; @@ -2658,7 +2666,7 @@ impl Storage { return None; } let block_signatures = ro_txn - .read(block_hash) + .read_block_signatures_by_hash(block_hash) .expect("should read block signatures") .unwrap_or_else(|| self.get_default_block_signatures(&block)); if block_signatures.is_verified().is_err() { @@ -2681,10 +2689,12 @@ impl Storage { .block_store .checkout_ro() .expect("should create ro txn"); - let block: Block = ro_txn.read(height).expect("should read block")?; + let block = ro_txn + .read_block_at_height(height) + .expect("should read block")?; let hash = block.hash(); let block_signatures = ro_txn - .read(*hash) + .read_block_signatures_by_hash(*hash) .expect("should read block signatures") .unwrap_or_else(|| self.get_default_block_signatures(&block)); Some(BlockWithSignatures::new(block, block_signatures)) @@ -2700,12 +2710,17 @@ impl Storage { .expect("should create ro txn"); let highest_block = if only_from_available_block_range { let height = self.highest_complete_block_height()?; - ro_txn.read(height).expect("should read block")? + ro_txn + .read_block_at_height(height) + .expect("should read block")? } else { - DataReader::::read(&ro_txn, Tip).expect("should read block")? + ro_txn.read_tip_block().expect("should read block")? }; let hash = highest_block.hash(); - let block_signatures = match ro_txn.read(*hash).expect("should read block signatures") { + let block_signatures = match ro_txn + .read_block_signatures_by_hash(*hash) + .expect("should read block signatures") + { Some(signatures) => signatures, None => self.get_default_block_signatures(&highest_block), }; @@ -2720,11 +2735,11 @@ impl Storage { .block_store .checkout_ro() .expect("should create ro txn"); - let block_hash_and_height: BlockHashHeightAndEra = txn - .read(transaction_hash) + let block_hash_and_height = txn + .read_block_info_by_transaction_hash(transaction_hash) .expect("should read block hash and height")?; let execution_result = txn - .read(transaction_hash) + .read_execution_result(transaction_hash) .expect("should read execution result"); Some(ExecutionInfo { block_hash: block_hash_and_height.block_hash, @@ -2735,8 +2750,8 @@ impl Storage { pub(crate) fn delete_block_utilization_score_by_block_hash(&mut self, block_hash: BlockHash) { let txn = self.block_store.checkout_ro().expect("mut get read only"); - let block_header: BlockHeader = txn - .read(block_hash) + let block_header = txn + .read_block_header_by_hash(block_hash) .expect("should read") .expect("must have header"); diff --git a/node/src/components/storage/tests.rs b/node/src/components/storage/tests.rs index 6623309070..9e9dd1ff47 100644 --- a/node/src/components/storage/tests.rs +++ b/node/src/components/storage/tests.rs @@ -17,7 +17,7 @@ use smallvec::smallvec; use casper_storage::block_store::{ types::{ApprovalsHashes, BlockHashHeightAndEra, BlockTransfers}, - BlockStoreProvider, BlockStoreTransaction, DataReader, DataWriter, + BlockStoreTransaction, }; use casper_types::{ execution::{Effects, ExecutionResult, ExecutionResultV2}, @@ -1510,7 +1510,7 @@ fn should_provide_transfers_if_not_stored() { // Check the empty collection has been stored. let reader = storage.block_store.checkout_rw().unwrap(); - let maybe_transfers: Option> = reader.read(block_hash).unwrap(); + let maybe_transfers = reader.read_transfers(block_hash).unwrap(); assert_eq!(Some(vec![]), maybe_transfers); } @@ -1555,7 +1555,10 @@ fn should_provide_transfers_after_emptied() { block_hash, transfers: Vec::::new(), }; - assert_eq!(writer.write(&empty_transfers).unwrap(), block_hash); + assert_eq!( + writer.write_transfers(&empty_transfers).unwrap(), + block_hash + ); writer.commit().unwrap(); // Check the correct value is returned. @@ -1566,7 +1569,7 @@ fn should_provide_transfers_after_emptied() { // Check the correct value has been stored. let reader = storage.block_store.checkout_rw().unwrap(); - let maybe_transfers: Option> = reader.read(block_hash).unwrap(); + let maybe_transfers = reader.read_transfers(block_hash).unwrap(); assert_eq!(Some(vec![transfer]), maybe_transfers); } @@ -2405,7 +2408,7 @@ fn store_and_purge_signatures() { // Purging for block_1 should leave sigs for block_2 and block_3 intact. let mut writer = storage.block_store.checkout_rw().unwrap(); - let _ = DataWriter::::delete(&mut writer, *block_1.hash()); + let _ = writer.delete_block_signatures(*block_1.hash()); writer.commit().unwrap(); assert_signatures(&storage, *block_1.hash(), vec![]); assert_signatures( @@ -2422,7 +2425,7 @@ fn store_and_purge_signatures() { // Purging for block_4 (which has no signatures) should not modify state. let mut writer = storage.block_store.checkout_rw().unwrap(); - let _ = DataWriter::::delete(&mut writer, *block_4.hash()); + let _ = writer.delete_block_signatures(*block_4.hash()); writer.commit().unwrap(); assert_signatures(&storage, *block_1.hash(), vec![]); assert_signatures( @@ -2439,10 +2442,10 @@ fn store_and_purge_signatures() { // Purging for all blocks should leave no signatures. let mut writer = storage.block_store.checkout_rw().unwrap(); - let _ = DataWriter::::delete(&mut writer, *block_1.hash()); - let _ = DataWriter::::delete(&mut writer, *block_2.hash()); - let _ = DataWriter::::delete(&mut writer, *block_3.hash()); - let _ = DataWriter::::delete(&mut writer, *block_4.hash()); + let _ = writer.delete_block_signatures(*block_1.hash()); + let _ = writer.delete_block_signatures(*block_2.hash()); + let _ = writer.delete_block_signatures(*block_3.hash()); + let _ = writer.delete_block_signatures(*block_4.hash()); writer.commit().unwrap(); assert_signatures(&storage, *block_1.hash(), vec![]); diff --git a/node/src/effect.rs b/node/src/effect.rs index ba09f3b1b8..54b6b28557 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -2271,7 +2271,6 @@ impl EffectBuilder { pub(crate) async fn speculatively_execute( self, block_header: Box, - block_hashes: BTreeMap, transaction: Box, ) -> SpeculativeExecutionResult where @@ -2280,7 +2279,6 @@ impl EffectBuilder { self.make_request( |responder| ContractRuntimeRequest::SpeculativelyExecute { block_header, - block_hashes, transaction, responder, }, diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index 4fea7eda52..9c180eba91 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -877,8 +877,6 @@ pub(crate) enum ContractRuntimeRequest { SpeculativelyExecute { /// Pre-state. block_header: Box, - /// Recent block hashes available to the EVM `BLOCKHASH` opcode. - block_hashes: BTreeMap, /// Transaction to execute. transaction: Box, /// Results diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 872e4c3459..d5dacc2737 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -26,10 +26,9 @@ use prometheus::Registry; use tracing::{debug, error, info, warn}; use casper_binary_port::{LastProgress, NetworkName, Uptime}; -use casper_storage::block_store::{types::Tip, BlockStoreProvider, DataReader}; use casper_types::{ - bytesrepr, Block, BlockHash, BlockHeader, BlockV2, Chainspec, ChainspecRawBytes, EraId, - FinalitySignature, FinalitySignatureV2, PublicKey, TimeDiff, Timestamp, Transaction, U512, + bytesrepr, Block, BlockHash, BlockV2, Chainspec, ChainspecRawBytes, EraId, FinalitySignature, + FinalitySignatureV2, PublicKey, TimeDiff, Timestamp, Transaction, U512, }; #[cfg(test)] @@ -1144,6 +1143,7 @@ impl reactor::Reactor for MainReactor { let contract_runtime = ContractRuntime::new( &storage_root, + block_store.clone(), &config.contract_runtime, chainspec.clone(), registry, @@ -1153,11 +1153,12 @@ impl reactor::Reactor for MainReactor { // synchronously now. The resulting immediate switch block is *not* produced yet: that's // deferred until the node can actually sign and gossip it // (see `MainReactor::maybe_finish_pending_upgrade`). - let local_tip: Option = { + let local_tip = { let ro_txn = block_store .checkout_ro() .map_err(storage::FatalStorageError::from)?; - DataReader::::read(&ro_txn, Tip) + ro_txn + .read_tip_block_header() .map_err(storage::FatalStorageError::from)? }; diff --git a/node/src/reactor/main_reactor/protocol_upgrade.rs b/node/src/reactor/main_reactor/protocol_upgrade.rs index 5dad94a76e..30973c1af5 100644 --- a/node/src/reactor/main_reactor/protocol_upgrade.rs +++ b/node/src/reactor/main_reactor/protocol_upgrade.rs @@ -13,13 +13,13 @@ use crate::{components::contract_runtime::ContractRuntime, reactor::main_reactor /// protocol upgrade, once the node is ready to sign and gossip it. #[derive(Clone, DataSize, Debug)] pub(super) struct PendingImmediateSwitchBlock { - next_block_height: u64, + pub(super) next_block_height: u64, #[data_size(skip)] - post_state_hash: Digest, - parent_hash: BlockHash, - parent_seed: Digest, - era_id: EraId, - timestamp: Timestamp, + pub(super) post_state_hash: Digest, + pub(super) parent_hash: BlockHash, + pub(super) parent_seed: Digest, + pub(super) era_id: EraId, + pub(super) timestamp: Timestamp, } impl PendingImmediateSwitchBlock { diff --git a/storage/src/block_store/block_provider.rs b/storage/src/block_store/block_provider.rs index d3290360c0..676e733c80 100644 --- a/storage/src/block_store/block_provider.rs +++ b/storage/src/block_store/block_provider.rs @@ -27,7 +27,7 @@ pub trait BlockStoreTransaction { } /// Data reader definition. -pub trait DataReader { +pub(crate) trait DataReader { /// Read item at key. fn read(&self, key: K) -> Result, BlockStoreError>; /// Returns true if item exists at key, else false. @@ -35,7 +35,7 @@ pub trait DataReader { } /// Data write definition. -pub trait DataWriter { +pub(crate) trait DataWriter { /// Write item to store and return key. fn write(&mut self, data: &T) -> Result; /// Delete item at key from store. diff --git a/storage/src/block_store/lmdb/lmdb_block_store.rs b/storage/src/block_store/lmdb/lmdb_block_store.rs index 7601ef7b22..af37d86e69 100644 --- a/storage/src/block_store/lmdb/lmdb_block_store.rs +++ b/storage/src/block_store/lmdb/lmdb_block_store.rs @@ -1,11 +1,13 @@ use std::{ borrow::Cow, collections::{btree_map, BTreeMap, BTreeSet, HashMap}, + ops::Range, path::{Path, PathBuf}, sync::Arc, }; use datasize::DataSize; +use tempfile::TempDir; use tracing::{debug, error, info}; use casper_types::{ @@ -40,9 +42,8 @@ use lmdb::{ /// Filename for the LMDB database created by the Storage component. const STORAGE_DB_FILENAME: &str = "storage.lmdb"; -/// We can set this very low, as there is only a single reader/writer accessing the component at any -/// one time. -const MAX_TRANSACTIONS: u32 = 5; +/// Maximum number of concurrent LMDB read transactions. +const MAX_READERS: u32 = 512; /// Maximum number of allowed dbs. const MAX_DB_COUNT: u32 = 20; @@ -58,7 +59,7 @@ const OS_FLAGS: EnvironmentFlags = EnvironmentFlags::WRITE_MAP; const OS_FLAGS: EnvironmentFlags = EnvironmentFlags::empty(); /// Lmdb block store. -#[derive(DataSize, Debug)] +#[derive(Clone, DataSize, Debug)] pub struct LmdbBlockStore { /// Storage location. root: PathBuf, @@ -96,6 +97,12 @@ pub struct LmdbBlockStore { /// it. #[data_size(skip)] pub(super) transaction_hash_index_db: Database, + /// Keeps a temporary storage directory alive for stores created by [`Self::new_temporary`]. + /// + /// The `Arc` ensures shallow-cloned store handles retain the directory until the last handle + /// is dropped. + #[data_size(skip)] + temporary_directory: Option>, } impl LmdbBlockStore { @@ -155,9 +162,31 @@ impl LmdbBlockStore { block_height_index_db, switch_block_era_id_index_db, transaction_hash_index_db, + temporary_directory: None, }) } + /// Creates a block store which owns its temporary directory. + pub fn new_temporary(total_size: usize) -> Result { + let temp_dir = tempfile::tempdir() + .map_err(|error| BlockStoreError::InternalStorage(Box::new(error)))?; + let mut block_store = Self::new(temp_dir.path(), total_size)?; + block_store.temporary_directory = Some(Arc::new(temp_dir)); + Ok(block_store) + } + + /// Checks out a read-only transaction. + pub fn checkout_ro(&self) -> Result, BlockStoreError> { + ::checkout_ro(self) + } + + /// Checks out a read-write transaction. + pub fn checkout_rw( + &mut self, + ) -> Result, BlockStoreError> { + ::checkout_rw(self) + } + /// Initializes the disk-backed indexes. This operation can be time /// consuming because it needs to go through all entries in block /// headers db. If the index has data it assumes that no reindexing @@ -704,7 +733,7 @@ impl LmdbBlockStore { Ok(block_hash) } - pub(crate) fn write_block_header( + pub(crate) fn write_block_header_raw( &self, txn: &mut RwTransaction, block_header: &BlockHeader, @@ -770,10 +799,10 @@ impl LmdbBlockStore { for (transaction_hash, execution_result) in execution_results.into_iter() { transfers.extend(successful_transfers(&execution_result)); - let maybe_stored_execution_result: Option = self + let maybe_stored_execution_result = self .checkout_ro() .map_err(|err| BlockStoreError::InternalStorage(Box::new(err)))? - .read(transaction_hash)?; + .read_execution_result(transaction_hash)?; // If we have a previous execution result, we can continue if it is the same. match maybe_stored_execution_result { @@ -867,7 +896,7 @@ pub(crate) fn new_environment( // Disable read-ahead. Our data is not stored/read in sequence that would benefit from the read-ahead. | EnvironmentFlags::NO_READAHEAD, ) - .set_max_readers(MAX_TRANSACTIONS) + .set_max_readers(MAX_READERS) .set_max_dbs(MAX_DB_COUNT) .set_map_size(total_size) .open(&root.join(STORAGE_DB_FILENAME)) @@ -935,6 +964,7 @@ impl BlockStoreProvider for LmdbBlockStore { } } +/// A transaction checked out from an [`LmdbBlockStore`]. pub struct LmdbBlockStoreTransaction<'t, T> where T: LmdbTransaction, @@ -943,6 +973,12 @@ where block_store: &'t LmdbBlockStore, } +/// A read-only block store transaction. +pub type LmdbBlockStoreReadTransaction<'t> = LmdbBlockStoreTransaction<'t, RoTransaction<'t>>; + +/// A read-write block store transaction. +pub type LmdbBlockStoreReadWriteTransaction<'t> = LmdbBlockStoreTransaction<'t, RwTransaction<'t>>; + impl BlockStoreTransaction for LmdbBlockStoreTransaction<'_, T> where T: LmdbTransaction, @@ -993,7 +1029,7 @@ where } fn exists(&self, key: BlockHash) -> Result { - self.block_store.block_header_exists(&self.txn, &key) + self.block_store.approvals_hashes_exist(&self.txn, &key) } } @@ -1112,6 +1148,165 @@ impl LmdbBlockStoreTransaction<'_, T> where T: LmdbTransaction, { + /// Reads a block by its hash. + pub fn read_block_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, block_hash) + } + + /// Returns whether a block exists at the given hash. + pub fn block_exists_by_hash(&self, block_hash: BlockHash) -> Result { + DataReader::::exists(self, block_hash) + } + + /// Reads a block header by its hash. + pub fn read_block_header_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, block_hash) + } + + /// Reads approvals hashes by block hash. + pub fn read_approvals_hashes_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, block_hash) + } + + /// Reads block signatures by block hash. + pub fn read_block_signatures_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, block_hash) + } + + /// Reads a transaction by its hash. + pub fn read_transaction( + &self, + transaction_hash: TransactionHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, transaction_hash) + } + + /// Returns whether a transaction exists at the given hash. + pub fn transaction_exists( + &self, + transaction_hash: TransactionHash, + ) -> Result { + DataReader::::exists(self, transaction_hash) + } + + /// Reads finalized approvals by transaction hash. + pub fn read_finalized_approvals( + &self, + transaction_hash: TransactionHash, + ) -> Result>, BlockStoreError> { + DataReader::>::read(self, transaction_hash) + } + + /// Reads an execution result by transaction hash. + pub fn read_execution_result( + &self, + transaction_hash: TransactionHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, transaction_hash) + } + + /// Reads transfers by block hash. + pub fn read_transfers( + &self, + block_hash: BlockHash, + ) -> Result>, BlockStoreError> { + DataReader::>::read(self, block_hash) + } + + /// Reads a state-store value. + pub fn read_state_store(&self, key: StateStoreKey) -> Result>, BlockStoreError> { + DataReader::>::read(self, key) + } + + /// Reads a block by height. + pub fn read_block_at_height(&self, height: u64) -> Result, BlockStoreError> { + DataReader::::read(self, height) + } + + /// Reads a block header by height. + pub fn read_block_header_at_height( + &self, + height: u64, + ) -> Result, BlockStoreError> { + DataReader::::read(self, height) + } + + /// Returns the first height in `range` without a stored block header. + /// + /// Header existence is checked through the block-height index without deserializing the + /// header. + pub fn first_missing_block_header_height( + &self, + range: Range, + ) -> Result, BlockStoreError> { + for height in range { + if !DataReader::::exists(self, height)? { + return Ok(Some(height)); + } + } + Ok(None) + } + + /// Reads a switch block by era ID. + pub fn read_switch_block_by_era( + &self, + era_id: EraId, + ) -> Result, BlockStoreError> { + DataReader::::read(self, era_id) + } + + /// Reads a switch block header by era ID. + pub fn read_block_header_by_era( + &self, + era_id: EraId, + ) -> Result, BlockStoreError> { + DataReader::::read(self, era_id) + } + + /// Reads the highest block header. + pub fn read_tip_block_header(&self) -> Result, BlockStoreError> { + DataReader::::read(self, Tip) + } + + /// Reads the highest block. + pub fn read_tip_block(&self) -> Result, BlockStoreError> { + DataReader::::read(self, Tip) + } + + /// Reads the latest switch block header. + pub fn read_latest_switch_block_header(&self) -> Result, BlockStoreError> { + DataReader::::read(self, LatestSwitchBlock) + } + + /// Reads the block information indexed by transaction hash. + pub fn read_block_info_by_transaction_hash( + &self, + transaction_hash: TransactionHash, + ) -> Result, BlockStoreError> { + DataReader::::read(self, transaction_hash) + } + + /// Reads raw bytes from a database table. + pub fn read_raw( + &self, + table_id: DbTableId, + key: Vec, + ) -> Result, BlockStoreError> { + DataReader::<(DbTableId, Vec), DbRawBytesSpec>::read(self, (table_id, key)) + } + fn block_hash_from_index( &self, index: LmdbBlockStoreIndex, @@ -1216,7 +1411,7 @@ where let index = LmdbBlockStoreIndex::SwitchBlockEraId(IndexPosition::Key(era_id)); match self.block_hash_from_index(index)? { Some(block_hash) => { - let maybe_header: Option = self.read(block_hash)?; + let maybe_header = self.read_block_header_by_hash(block_hash)?; Ok(maybe_header.map(|header| header.height())) } None => Ok(None), @@ -1473,11 +1668,109 @@ where } fn exists(&self, key: (DbTableId, Vec)) -> Result { - self.read(key).map(|res| res.is_some()) + self.read_raw(key.0, key.1).map(|res| res.is_some()) } } impl<'t> LmdbBlockStoreTransaction<'t, RwTransaction<'t>> { + /// Writes a block and its indexes. + pub fn write_block(&mut self, block: &Block) -> Result { + DataWriter::::write(self, block) + } + + /// Deletes a block and its indexes. + pub fn delete_block(&mut self, block_hash: BlockHash) -> Result<(), BlockStoreError> { + DataWriter::::delete(self, block_hash) + } + + /// Deletes a block body by its hash. + pub fn delete_block_body(&mut self, body_hash: Digest) -> Result<(), BlockStoreError> { + DataWriter::::delete(self, body_hash) + } + + /// Writes approvals hashes. + pub fn write_approvals_hashes( + &mut self, + approvals_hashes: &ApprovalsHashes, + ) -> Result { + DataWriter::::write(self, approvals_hashes) + } + + /// Writes block signatures. + pub fn write_block_signatures( + &mut self, + block_signatures: &BlockSignatures, + ) -> Result { + DataWriter::::write(self, block_signatures) + } + + /// Deletes block signatures by block hash. + pub fn delete_block_signatures( + &mut self, + block_hash: BlockHash, + ) -> Result<(), BlockStoreError> { + DataWriter::::delete(self, block_hash) + } + + /// Writes a block header and its indexes. + pub fn write_block_header( + &mut self, + block_header: &BlockHeader, + ) -> Result { + DataWriter::::write(self, block_header) + } + + /// Writes a transaction. + pub fn write_transaction( + &mut self, + transaction: &Transaction, + ) -> Result { + DataWriter::::write(self, transaction) + } + + /// Writes the transfers for a block. + pub fn write_transfers( + &mut self, + block_transfers: &BlockTransfers, + ) -> Result { + DataWriter::::write(self, block_transfers) + } + + /// Writes a state-store value. + pub fn write_state_store( + &mut self, + state_store: &StateStore, + ) -> Result, BlockStoreError> { + DataWriter::, StateStore>::write(self, state_store) + } + + /// Writes finalized approvals for a transaction. + pub fn write_finalized_approvals( + &mut self, + finalized_approvals: &TransactionFinalizedApprovals, + ) -> Result { + DataWriter::::write( + self, + finalized_approvals, + ) + } + + /// Writes the execution results and transaction index entries for a block. + pub fn write_execution_results( + &mut self, + execution_results: &BlockExecutionResults, + ) -> Result { + DataWriter::::write(self, execution_results) + } + + /// Deletes execution results for a block. + pub fn delete_execution_results( + &mut self, + block_info: BlockHashHeightAndEra, + ) -> Result<(), BlockStoreError> { + DataWriter::::delete(self, block_info) + } + /// Check if the block height index can be updated. fn should_update_block_height_index( &self, @@ -1724,7 +2017,9 @@ impl<'t> DataWriter for LmdbBlockStoreTransaction<'t, Rw self.should_update_block_height_index(block_height, &block_hash)?; let update_switch_block_index = self.should_update_switch_block_index(data)?; - let key = self.block_store.write_block_header(&mut self.txn, data)?; + let key = self + .block_store + .write_block_header_raw(&mut self.txn, data)?; if update_height_index { put_by_be_u64_key( @@ -1928,6 +2223,118 @@ mod tests { )) } + #[test] + fn supports_more_than_five_simultaneous_read_transactions() { + const CONCURRENT_READERS: usize = 6; + + let tempdir = TempDir::new().expect("should create tempdir"); + let store = + LmdbBlockStore::new(tempdir.path(), 64 * 1024 * 1024).expect("should create store"); + + let read_transactions = (0..CONCURRENT_READERS) + .map(|_| store.checkout_ro().expect("should checkout ro")) + .collect::>(); + + assert_eq!(read_transactions.len(), CONCURRENT_READERS); + } + + #[test] + fn cloned_temporary_store_keeps_its_directory_alive() { + let store = LmdbBlockStore::new_temporary(64 * 1024 * 1024).expect("should create store"); + let storage_path = store.root.clone(); + let cloned_store = store.clone(); + + drop(store); + + assert!(storage_path.exists()); + cloned_store + .checkout_ro() + .expect("clone should retain the temporary directory"); + } + + #[test] + fn approvals_hashes_existence_does_not_follow_header_existence() { + let rng = &mut TestRng::new(); + let mut store = + LmdbBlockStore::new_temporary(64 * 1024 * 1024).expect("should create store"); + + let secret_key = SecretKey::random(rng); + let proposer = PublicKey::from(&secret_key); + let header = header_at_height(rng, 0, &proposer); + + { + let mut rw_txn = store.checkout_rw().expect("should checkout rw"); + rw_txn + .write_block_header(&header) + .expect("should write header"); + rw_txn.commit().expect("should commit"); + } + + let ro_txn = store.checkout_ro().expect("should checkout ro"); + assert!( + DataReader::::exists(&ro_txn, header.block_hash()) + .expect("should check header existence") + ); + assert!( + !DataReader::::exists(&ro_txn, header.block_hash()) + .expect("should check approvals hashes existence") + ); + } + + #[test] + fn finds_first_missing_block_header_height() { + let rng = &mut TestRng::new(); + let mut store = + LmdbBlockStore::new_temporary(64 * 1024 * 1024).expect("should create store"); + + let secret_key = SecretKey::random(rng); + let proposer = PublicKey::from(&secret_key); + + { + let mut rw_txn = store.checkout_rw().expect("should checkout rw"); + for height in [10, 11, 13, 14] { + let header = header_at_height(rng, height, &proposer); + rw_txn + .write_block_header(&header) + .expect("should write header"); + } + rw_txn.commit().expect("should commit"); + } + + let ro_txn = store.checkout_ro().expect("should checkout ro"); + + assert_eq!( + ro_txn + .first_missing_block_header_height(10..12) + .expect("should check complete range"), + None + ); + assert_eq!( + ro_txn + .first_missing_block_header_height(9..12) + .expect("should find missing first height"), + Some(9) + ); + assert_eq!( + ro_txn + .first_missing_block_header_height(10..14) + .expect("should find missing middle height"), + Some(12) + ); + assert_eq!( + ro_txn + .first_missing_block_header_height(13..16) + .expect("should find missing last height"), + Some(15) + ); + assert_eq!( + ro_txn + .first_missing_block_header_height(10..10) + .expect("empty range should be complete"), + None + ); + } + #[test] fn reindex_rebuilds_disk_backed_indexes_via_append() { let rng = &mut TestRng::new(); @@ -1943,7 +2350,8 @@ mod tests { let mut rw_txn = store.checkout_rw().expect("should checkout rw"); for height in 0..HEADER_COUNT { let header = header_at_height(rng, height, &proposer); - let _ = DataWriter::::write(&mut rw_txn, &header) + let _ = rw_txn + .write_block_header(&header) .expect("should write header"); headers.push(header); } @@ -1958,7 +2366,9 @@ mod tests { // little-endian single-byte boundary (e.g. 255 -> 256) to catch ordering bugs. for &height in &[0u64, 1, 254, 255, 256, 257, HEADER_COUNT - 1] { let expected_hash = headers[height as usize].block_hash(); - let actual: Option = ro_txn.read(height).expect("read by height"); + let actual = ro_txn + .read_block_header_at_height(height) + .expect("read by height"); assert_eq!( actual.expect("header should exist").block_hash(), expected_hash, @@ -1967,7 +2377,7 @@ mod tests { } // Tip should be the highest height. - let tip: Option = ro_txn.read(Tip).expect("read tip"); + let tip = ro_txn.read_tip_block_header().expect("read tip"); assert_eq!( tip.expect("tip should exist").height(), HEADER_COUNT - 1, @@ -1983,7 +2393,9 @@ mod tests { ); let era_id = EraId::new(height / 10); let expected_hash = headers[height as usize].block_hash(); - let actual: Option = ro_txn.read(era_id).expect("read by era id"); + let actual = ro_txn + .read_block_header_by_era(era_id) + .expect("read by era id"); assert_eq!( actual .expect("switch block header should exist") @@ -2001,9 +2413,9 @@ mod tests { .map(|header| header.height()) .max() .expect("should have at least one switch block"); - let latest_switch: Option = - DataReader::::read(&ro_txn, LatestSwitchBlock) - .expect("read latest switch block"); + let latest_switch = ro_txn + .read_latest_switch_block_header() + .expect("read latest switch block"); assert_eq!( latest_switch .expect("latest switch block should exist") @@ -2024,13 +2436,13 @@ mod tests { let proposer = PublicKey::from(&secret_key); // Write a header directly via a raw transaction, bypassing the index-maintaining - // `DataWriter` impl -- simulating a migration from a binary version that didn't yet + // transaction method. This simulates a migration from a binary version that didn't yet // maintain these disk-backed indexes. let header = header_at_height(rng, 0, &proposer); { let mut txn = store.env.begin_rw_txn().expect("should begin rw txn"); let _ = store - .write_block_header(&mut txn, &header) + .write_block_header_raw(&mut txn, &header) .expect("should write header"); txn.commit().expect("should commit"); } @@ -2038,14 +2450,18 @@ mod tests { // The index hasn't been told about this header yet. { let ro_txn = store.checkout_ro().expect("should checkout ro"); - let by_height: Option = ro_txn.read(0u64).expect("read by height"); + let by_height = ro_txn + .read_block_header_at_height(0) + .expect("read by height"); assert!(by_height.is_none(), "index should not exist yet"); } store.init().expect("init should succeed"); let ro_txn = store.checkout_ro().expect("should checkout ro"); - let by_height: Option = ro_txn.read(0u64).expect("read by height"); + let by_height = ro_txn + .read_block_header_at_height(0) + .expect("read by height"); assert_eq!( by_height .expect("header should be indexed after init") @@ -2072,7 +2488,8 @@ mod tests { { let mut rw_txn = store.checkout_rw().expect("should checkout rw"); for header in &headers { - let _ = DataWriter::::write(&mut rw_txn, header) + let _ = rw_txn + .write_block_header(header) .expect("should write header"); } rw_txn.commit().expect("should commit"); @@ -2092,12 +2509,16 @@ mod tests { // Since the index wasn't empty (height 1's entry is still present), `init` must have // skipped rebuilding it -- so the deleted entry for height 0 stays missing. let ro_txn = store.checkout_ro().expect("should checkout ro"); - let by_height_0: Option = ro_txn.read(0u64).expect("read by height"); + let by_height_0 = ro_txn + .read_block_header_at_height(0) + .expect("read by height"); assert!( by_height_0.is_none(), "init should not have rebuilt an already-populated index" ); - let by_height_1: Option = ro_txn.read(1u64).expect("read by height"); + let by_height_1 = ro_txn + .read_block_header_at_height(1) + .expect("read by height"); assert_eq!( by_height_1 .expect("height 1 should still be indexed") diff --git a/storage/src/block_store/lmdb/mod.rs b/storage/src/block_store/lmdb/mod.rs index 98892c5556..d3bd04096e 100644 --- a/storage/src/block_store/lmdb/mod.rs +++ b/storage/src/block_store/lmdb/mod.rs @@ -4,7 +4,9 @@ mod versioned_databases; mod lmdb_block_store; use core::convert::TryFrom; -pub use lmdb_block_store::LmdbBlockStore; +pub use lmdb_block_store::{ + LmdbBlockStore, LmdbBlockStoreReadTransaction, LmdbBlockStoreReadWriteTransaction, +}; #[cfg(test)] use rand::Rng; diff --git a/storage/src/block_store/mod.rs b/storage/src/block_store/mod.rs index baa9ae7d50..6350d7e1de 100644 --- a/storage/src/block_store/mod.rs +++ b/storage/src/block_store/mod.rs @@ -5,7 +5,8 @@ pub mod lmdb; /// Block store types. pub mod types; -pub use block_provider::{BlockStoreProvider, BlockStoreTransaction, DataReader, DataWriter}; +pub use block_provider::{BlockStoreProvider, BlockStoreTransaction}; +pub(crate) use block_provider::{DataReader, DataWriter}; pub use error::BlockStoreError; /// Stores raw bytes from the DB along with the flag indicating whether data come from legacy or diff --git a/storage/src/block_store/types/mod.rs b/storage/src/block_store/types/mod.rs index 96fbc90ab6..9fec3bbd9d 100644 --- a/storage/src/block_store/types/mod.rs +++ b/storage/src/block_store/types/mod.rs @@ -10,17 +10,12 @@ use std::{ pub use approvals_hashes::{ApprovalsHashes, ApprovalsHashesValidationError}; pub use block_hash_height_and_era::BlockHashHeightAndEra; -use casper_types::{ - execution::ExecutionResult, Approval, Block, BlockHash, BlockHeader, TransactionHash, Transfer, -}; +use casper_types::{execution::ExecutionResult, Approval, BlockHash, TransactionHash, Transfer}; pub(crate) use approvals_hashes::LegacyApprovalsHashes; pub(crate) use deploy_metadata_v1::DeployMetadataV1; pub(in crate::block_store) use transfers::Transfers; -/// Exeuction results. -pub type ExecutionResults = HashMap; - /// Transaction finalized approvals. pub struct TransactionFinalizedApprovals { /// Transaction hash. @@ -34,7 +29,7 @@ pub struct BlockExecutionResults { /// Block info. pub block_info: BlockHashHeightAndEra, /// Execution results. - pub exec_results: ExecutionResults, + pub exec_results: HashMap, } /// Block transfers. @@ -64,16 +59,10 @@ impl StateStoreKey { } /// Block tip anchor. -pub struct Tip; +pub(crate) struct Tip; /// Latest switch block anchor. -pub struct LatestSwitchBlock; +pub(crate) struct LatestSwitchBlock; /// Block height. -pub type BlockHeight = u64; - -/// Switch block header alias. -pub type SwitchBlockHeader = BlockHeader; - -/// Switch block alias. -pub type SwitchBlock = Block; +pub(crate) type BlockHeight = u64; diff --git a/storage/src/data_access_layer.rs b/storage/src/data_access_layer.rs index 7af13031ca..4c6afcee49 100644 --- a/storage/src/data_access_layer.rs +++ b/storage/src/data_access_layer.rs @@ -1,6 +1,9 @@ -use crate::global_state::{ - error::Error as GlobalStateError, - state::{CommitProvider, StateProvider}, +use crate::{ + block_store::lmdb::LmdbBlockStore, + global_state::{ + error::Error as GlobalStateError, + state::{CommitProvider, StateProvider}, + }, }; use casper_types::{execution::Effects, Digest}; @@ -97,22 +100,11 @@ pub use system_entity_registry::{ pub use total_supply::{TotalSupplyRequest, TotalSupplyResult}; pub use trie::{PutTrieRequest, PutTrieResult, TrieElement, TrieRequest, TrieResult}; -/// Anchor struct for block store functionality. -#[derive(Default, Copy, Clone)] -pub struct BlockStore(()); - -impl BlockStore { - /// Ctor. - pub fn new() -> Self { - BlockStore(()) - } -} - /// Data access layer. -#[derive(Copy, Clone)] +#[derive(Clone)] pub struct DataAccessLayer { /// Block store instance. - pub block_store: BlockStore, + pub block_store: LmdbBlockStore, /// Memoized state. pub state: S, /// Max query depth. diff --git a/storage/src/eip2935.rs b/storage/src/eip2935.rs new file mode 100644 index 0000000000..0072c02900 --- /dev/null +++ b/storage/src/eip2935.rs @@ -0,0 +1,50 @@ +//! Storage-facing constants for the EIP-2935 block hash history predeploy. + +use alloy_primitives::keccak256; +use casper_types::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, +]); + +/// Number of historical block hashes served by EIP-2935. +pub const HISTORY_BUFFER_LENGTH: u64 = 8_191; + +/// 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!(HISTORY_BUFFER_LENGTH, 8_191); + assert_eq!(BLOCK_HASH_HISTORY_CODE.len(), 83); + assert_eq!( + block_hash_history_code_hash(), + evm::Hash::new(keccak256(BLOCK_HASH_HISTORY_CODE).0) + ); + } +} diff --git a/storage/src/lib.rs b/storage/src/lib.rs index e3622d3549..d15e80eff7 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-2935 block hash history predeploy support. +pub mod eip2935; /// EIP-4788 beacon roots storage support. pub mod eip4788; /// Global state logic. diff --git a/storage/src/system/evm.rs b/storage/src/system/evm.rs index b21afa74bd..784fa51db7 100644 --- a/storage/src/system/evm.rs +++ b/storage/src/system/evm.rs @@ -7,7 +7,7 @@ use casper_types::{ use thiserror::Error; use crate::{ - eip4788, + eip2935, eip4788, global_state::{error::Error as GlobalStateError, state::StateReader}, tracking_copy::{TrackingCopy, TrackingCopyError}, }; @@ -53,66 +53,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 eip2935() -> Self { + Self { + name: "EIP-2935", + address: eip2935::BLOCK_HASH_HISTORY_ADDRESS, + code: eip2935::BLOCK_HASH_HISTORY_CODE, + code_hash: eip2935::block_hash_history_code_hash(), + } + } -fn beacon_roots_code_hash_key() -> Key { - Key::Evm(EvmAddr::CodeHash(eip4788::BEACON_ROOTS_ADDRESS)) + fn eip4788() -> Self { + Self { + name: "EIP-4788", + address: eip4788::BEACON_ROOTS_ADDRESS, + code: eip4788::BEACON_ROOTS_CODE, + code_hash: eip4788::beacon_roots_code_hash(), + } + } + + fn code_hash_key(self) -> Key { + Key::Evm(EvmAddr::CodeHash(self.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(eip4788::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( - eip4788::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, - eip4788::BEACON_ROOTS_CODE.to_vec(), - )) +/// Idempotently installs the 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 = eip4788::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::()?; @@ -120,7 +159,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 }); @@ -136,27 +175,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() == eip4788::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() ), @@ -199,65 +238,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_upsert_prague_predeploys_for_enabled_prague_or_later_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_prague_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_prague_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!( @@ -269,19 +319,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!( @@ -291,33 +344,34 @@ 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(eip4788::BEACON_ROOTS_ADDRESS))) - .expect("key should encode"), + CLValue::from_t(Key::Evm(EvmAddr::Account( + eip2935::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(())