diff --git a/Cargo.lock b/Cargo.lock index 94b6e403ce..47702b3bb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2952,6 +2952,7 @@ dependencies = [ "revm-state", "rstest", "serde", + "tokio", ] [[package]] diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 5e49f01f88..838ddc09a3 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -60,6 +60,7 @@ std = ["serde?/std"] serde = ["dep:serde"] alloydb = [ "std", + "database-interface/asyncdb", "dep:tokio", "dep:alloy-provider", "dep:alloy-eips", diff --git a/crates/database/interface/Cargo.toml b/crates/database/interface/Cargo.toml index 6406283099..64fe335906 100644 --- a/crates/database/interface/Cargo.toml +++ b/crates/database/interface/Cargo.toml @@ -33,6 +33,9 @@ serde = { version = "1.0", default-features = false, features = [ "rc", ], optional = true } +# asyncdb +tokio = { version = "1.40", optional = true } + [dev-dependencies] anyhow = "1.0.83" @@ -45,3 +48,6 @@ alloy-sol-types = "0.8" default = ["std"] std = ["serde?/std"] serde = ["dep:serde"] +asyncdb = [ + "dep:tokio", +] diff --git a/crates/database/interface/src/async_db.rs b/crates/database/interface/src/async_db.rs new file mode 100644 index 0000000000..85f2493623 --- /dev/null +++ b/crates/database/interface/src/async_db.rs @@ -0,0 +1,188 @@ +use core::future::Future; + +use primitives::{Address, B256, U256}; +use state::{AccountInfo, Bytecode}; +use tokio::runtime::{Handle, Runtime}; + +use crate::{Database, DatabaseRef}; + +/// The async EVM database interface. +/// +/// Contains the same methods as [Database], but it returns [Future] type instead. +/// +/// Use [WrapDatabaseAsync] to provide [Database] implementation for a type that only implements this trait. +pub trait DatabaseAsync { + /// The database error type. + type Error: Send; + + /// Get basic account information. + fn basic_async( + &mut self, + address: Address, + ) -> impl Future, Self::Error>> + Send; + + /// Get account code by its hash. + fn code_by_hash_async( + &mut self, + code_hash: B256, + ) -> impl Future> + Send; + + /// Get storage value of address at index. + fn storage_async( + &mut self, + address: Address, + index: U256, + ) -> impl Future> + Send; + + /// Get block hash by block number. + fn block_hash_async( + &mut self, + number: u64, + ) -> impl Future> + Send; +} + +/// The async EVM database interface. +/// +/// Contains the same methods as [DatabaseRef], but it returns [Future] type instead. +/// +/// Use [WrapDatabaseAsync] to provide [DatabaseRef] implementation for a type that only implements this trait. +pub trait DatabaseAsyncRef { + /// The database error type. + type Error: Send; + + /// Get basic account information. + fn basic_async_ref( + &self, + address: Address, + ) -> impl Future, Self::Error>> + Send; + + /// Get account code by its hash. + fn code_by_hash_async_ref( + &self, + code_hash: B256, + ) -> impl Future> + Send; + + /// Get storage value of address at index. + fn storage_async_ref( + &self, + address: Address, + index: U256, + ) -> impl Future> + Send; + + /// Get block hash by block number. + fn block_hash_async_ref( + &self, + number: u64, + ) -> impl Future> + Send; +} + +/// Wraps a [DatabaseAsync] or [DatabaseAsyncRef] to provide a [`Database`] implementation. +#[derive(Debug)] +pub struct WrapDatabaseAsync { + db: T, + rt: HandleOrRuntime, +} + +impl WrapDatabaseAsync { + /// Wrap a [DatabaseAsync] or [DatabaseAsyncRef] instance. + /// + /// Returns `None` if no tokio runtime is available or if the current runtime is a current-thread runtime. + pub fn new(db: T) -> Option { + let rt = match Handle::try_current() { + Ok(handle) => match handle.runtime_flavor() { + tokio::runtime::RuntimeFlavor::CurrentThread => return None, + _ => HandleOrRuntime::Handle(handle), + }, + Err(_) => return None, + }; + Some(Self { db, rt }) + } + + /// Wrap a [DatabaseAsync] or [DatabaseAsyncRef] instance, with a runtime. + /// + /// Refer to [tokio::runtime::Builder] on how to create a runtime if you are in synchronous world. + /// If you are already using something like [tokio::main], call [WrapDatabaseAsync::new] instead. + pub fn with_runtime(db: T, runtime: Runtime) -> Self { + let rt = HandleOrRuntime::Runtime(runtime); + Self { db, rt } + } + + /// Wrap a [DatabaseAsync] or [DatabaseAsyncRef] instance, with a runtime handle. + /// + /// This generally allows you to pass any valid runtime handle, refer to [tokio::runtime::Handle] on how + /// to obtain a handle. If you are already in asynchronous world, like [tokio::main], use [WrapDatabaseAsync::new] + /// instead. + pub fn with_handle(db: T, handle: Handle) -> Self { + let rt = HandleOrRuntime::Handle(handle); + Self { db, rt } + } +} + +impl Database for WrapDatabaseAsync { + type Error = T::Error; + + #[inline] + fn basic(&mut self, address: Address) -> Result, Self::Error> { + self.rt.block_on(self.db.basic_async(address)) + } + + #[inline] + fn code_by_hash(&mut self, code_hash: B256) -> Result { + self.rt.block_on(self.db.code_by_hash_async(code_hash)) + } + + #[inline] + fn storage(&mut self, address: Address, index: U256) -> Result { + self.rt.block_on(self.db.storage_async(address, index)) + } + + #[inline] + fn block_hash(&mut self, number: u64) -> Result { + self.rt.block_on(self.db.block_hash_async(number)) + } +} + +impl DatabaseRef for WrapDatabaseAsync { + type Error = T::Error; + + #[inline] + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + self.rt.block_on(self.db.basic_async_ref(address)) + } + + #[inline] + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + self.rt.block_on(self.db.code_by_hash_async_ref(code_hash)) + } + + #[inline] + fn storage_ref(&self, address: Address, index: U256) -> Result { + self.rt.block_on(self.db.storage_async_ref(address, index)) + } + + #[inline] + fn block_hash_ref(&self, number: u64) -> Result { + self.rt.block_on(self.db.block_hash_async_ref(number)) + } +} + +// Hold a tokio runtime handle or full runtime +#[derive(Debug)] +enum HandleOrRuntime { + Handle(Handle), + Runtime(Runtime), +} + +impl HandleOrRuntime { + #[inline] + fn block_on(&self, f: F) -> F::Output + where + F: Future + Send, + F::Output: Send, + { + match self { + Self::Handle(handle) => tokio::task::block_in_place(move || handle.block_on(f)), + Self::Runtime(rt) => rt.block_on(f), + } + } +} diff --git a/crates/database/interface/src/lib.rs b/crates/database/interface/src/lib.rs index a3b68fc90a..7a4469d09d 100644 --- a/crates/database/interface/src/lib.rs +++ b/crates/database/interface/src/lib.rs @@ -9,8 +9,12 @@ use auto_impl::auto_impl; use primitives::{Address, HashMap, B256, U256}; use state::{Account, AccountInfo, Bytecode}; +#[cfg(feature = "asyncdb")] +pub mod async_db; pub mod empty_db; +#[cfg(feature = "asyncdb")] +pub use async_db::{DatabaseAsync, WrapDatabaseAsync}; pub use empty_db::{EmptyDB, EmptyDBTyped}; /// EVM database interface. diff --git a/crates/database/src/alloydb.rs b/crates/database/src/alloydb.rs index cfa7d237ab..2572a6e70f 100644 --- a/crates/database/src/alloydb.rs +++ b/crates/database/src/alloydb.rs @@ -4,13 +4,11 @@ use alloy_provider::{ Network, Provider, }; use alloy_transport::{Transport, TransportError}; -use database_interface::{Database, DatabaseRef}; +use database_interface::async_db::DatabaseAsyncRef; use primitives::{Address, B256, U256}; use state::{AccountInfo, Bytecode}; -use std::future::IntoFuture; -use tokio::runtime::{Handle, Runtime}; -/// An alloy-powered REVM [Database]. +/// An alloy-powered REVM [database_interface::Database]. /// /// When accessing the database, it'll use the given provider to fetch the corresponding account's data. #[derive(Debug)] @@ -19,100 +17,43 @@ pub struct AlloyDB> { provider: P, /// The block number on which the queries will be based on. block_number: BlockId, - /// handle to the tokio runtime - rt: HandleOrRuntime, _marker: std::marker::PhantomData (T, N)>, } impl> AlloyDB { /// Create a new AlloyDB instance, with a [Provider] and a block. - /// - /// Returns `None` if no tokio runtime is available or if the current runtime is a current-thread runtime. - pub fn new(provider: P, block_number: BlockId) -> Option { - let rt = match Handle::try_current() { - Ok(handle) => match handle.runtime_flavor() { - tokio::runtime::RuntimeFlavor::CurrentThread => return None, - _ => HandleOrRuntime::Handle(handle), - }, - Err(_) => return None, - }; - Some(Self { - provider, - block_number, - rt, - _marker: std::marker::PhantomData, - }) - } - - /// Create a new AlloyDB instance, with a provider and a block and a runtime. - /// - /// Refer to [tokio::runtime::Builder] on how to create a runtime if you are in synchronous world. - /// If you are already using something like [tokio::main], call AlloyDB::new instead. - pub fn with_runtime(provider: P, block_number: BlockId, runtime: Runtime) -> Self { - let rt = HandleOrRuntime::Runtime(runtime); + pub fn new(provider: P, block_number: BlockId) -> Self { Self { provider, block_number, - rt, _marker: std::marker::PhantomData, } } - /// Create a new AlloyDB instance, with a provider and a block and a runtime handle. - /// - /// This generally allows you to pass any valid runtime handle, refer to [tokio::runtime::Handle] on how - /// to obtain a handle. If you are already in asynchronous world, like [tokio::main], use AlloyDB::new instead. - pub fn with_handle(provider: P, block_number: BlockId, handle: Handle) -> Self { - let rt = HandleOrRuntime::Handle(handle); - Self { - provider, - block_number, - rt, - _marker: std::marker::PhantomData, - } - } - - /// Internal utility function that allows us to block on a future regardless of the runtime flavor. - #[inline] - fn block_on(&self, f: F) -> F::Output - where - F: std::future::Future + Send, - F::Output: Send, - { - self.rt.block_on(f) - } - /// Set the block number on which the queries will be based on. pub fn set_block_number(&mut self, block_number: BlockId) { self.block_number = block_number; } } -impl> DatabaseRef for AlloyDB { +impl> DatabaseAsyncRef for AlloyDB { type Error = TransportError; - fn basic_ref(&self, address: Address) -> Result, Self::Error> { - let f = async { - let nonce = self - .provider - .get_transaction_count(address) - .block_id(self.block_number); - let balance = self - .provider - .get_balance(address) - .block_id(self.block_number); - let code = self - .provider - .get_code_at(address) - .block_id(self.block_number); - tokio::join!( - nonce.into_future(), - balance.into_future(), - code.into_future() - ) - }; + async fn basic_async_ref(&self, address: Address) -> Result, Self::Error> { + let nonce = self + .provider + .get_transaction_count(address) + .block_id(self.block_number); + let balance = self + .provider + .get_balance(address) + .block_id(self.block_number); + let code = self + .provider + .get_code_at(address) + .block_id(self.block_number); - let (nonce, balance, code) = self.block_on(f); + let (nonce, balance, code) = tokio::join!(nonce, balance, code,); let balance = balance?; let code = Bytecode::new_raw(code?.0.into()); @@ -122,73 +63,26 @@ impl> DatabaseRef for AlloyD Ok(Some(AccountInfo::new(balance, nonce, code_hash, code))) } - fn block_hash_ref(&self, number: u64) -> Result { - let block = self.block_on( - self.provider - // SAFETY: We know number <= u64::MAX, so we can safely convert it to u64 - .get_block_by_number(number.into(), false), - )?; + async fn block_hash_async_ref(&self, number: u64) -> Result { + let block = self + .provider + // SAFETY: We know number <= u64::MAX, so we can safely convert it to u64 + .get_block_by_number(number.into(), false) + .await?; // SAFETY: If the number is given, the block is supposed to be finalized, so unwrapping is safe. Ok(B256::new(*block.unwrap().header().hash())) } - fn code_by_hash_ref(&self, _code_hash: B256) -> Result { + async fn code_by_hash_async_ref(&self, _code_hash: B256) -> Result { panic!("This should not be called, as the code is already loaded"); // This is not needed, as the code is already loaded with basic_ref } - fn storage_ref(&self, address: Address, index: U256) -> Result { - let f = self - .provider + async fn storage_async_ref(&self, address: Address, index: U256) -> Result { + self.provider .get_storage_at(address, index) - .block_id(self.block_number); - let slot_val = self.block_on(f.into_future())?; - Ok(slot_val) - } -} - -impl> Database for AlloyDB { - type Error = TransportError; - - #[inline] - fn basic(&mut self, address: Address) -> Result, Self::Error> { - ::basic_ref(self, address) - } - - #[inline] - fn code_by_hash(&mut self, code_hash: B256) -> Result { - ::code_by_hash_ref(self, code_hash) - } - - #[inline] - fn storage(&mut self, address: Address, index: U256) -> Result { - ::storage_ref(self, address, index) - } - - #[inline] - fn block_hash(&mut self, number: u64) -> Result { - ::block_hash_ref(self, number) - } -} - -// Hold a tokio runtime handle or full runtime -#[derive(Debug)] -pub(crate) enum HandleOrRuntime { - Handle(Handle), - Runtime(Runtime), -} - -impl HandleOrRuntime { - #[inline] - pub(crate) fn block_on(&self, f: F) -> F::Output - where - F: std::future::Future + Send, - F::Output: Send, - { - match self { - Self::Handle(handle) => tokio::task::block_in_place(move || handle.block_on(f)), - Self::Runtime(rt) => rt.block_on(f), - } + .block_id(self.block_number) + .await } } @@ -196,6 +90,7 @@ impl HandleOrRuntime { mod tests { use super::*; use alloy_provider::ProviderBuilder; + use database_interface::{DatabaseRef, WrapDatabaseAsync}; #[test] #[ignore = "flaky RPC"] @@ -206,13 +101,14 @@ mod tests { .unwrap(), ); let alloydb = AlloyDB::new(client, BlockId::from(16148323)); + let wrapped_alloydb = WrapDatabaseAsync::new(alloydb).unwrap(); // ETH/USDT pair on Uniswap V2 let address: Address = "0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852" .parse() .unwrap(); - let acc_info = alloydb.unwrap().basic_ref(address).unwrap().unwrap(); + let acc_info = wrapped_alloydb.basic_ref(address).unwrap().unwrap(); assert!(acc_info.exists()); } } diff --git a/examples/block_traces/src/main.rs b/examples/block_traces/src/main.rs index 22143bc6ff..e1c516c0e7 100644 --- a/examples/block_traces/src/main.rs +++ b/examples/block_traces/src/main.rs @@ -7,6 +7,7 @@ use database::{AlloyDB, CacheDB, StateBuilder}; use indicatif::ProgressBar; use inspector::{inspector_handle_register, inspectors::TracerEip3155}; use revm::{ + database_interface::WrapDatabaseAsync, primitives::{TxKind, B256, U256}, specification::eip2930::AccessListItem, wiring::EthereumWiring, @@ -67,7 +68,7 @@ async fn main() -> anyhow::Result<()> { let prev_id: BlockId = previous_block_number.into(); // SAFETY: This cannot fail since this is in the top-level tokio runtime - let state_db = AlloyDB::new(client, prev_id).unwrap(); + let state_db = WrapDatabaseAsync::new(AlloyDB::new(client, prev_id)).unwrap(); let cache_db: CacheDB<_> = CacheDB::new(state_db); let mut state = StateBuilder::new_with_database(cache_db).build(); let mut evm = Evm::>::builder() diff --git a/examples/uniswap_get_reserves/src/main.rs b/examples/uniswap_get_reserves/src/main.rs index ff9144ff14..04caa38cb6 100644 --- a/examples/uniswap_get_reserves/src/main.rs +++ b/examples/uniswap_get_reserves/src/main.rs @@ -6,8 +6,9 @@ use alloy_provider::ProviderBuilder; use alloy_sol_types::sol; use alloy_sol_types::SolCall; use database::{AlloyDB, CacheDB}; +use revm::database_interface::WrapDatabaseAsync; use revm::{ - database_interface::Database, + database_interface::DatabaseRef, database_interface::EmptyDB, primitives::{address, TxKind, U256}, wiring::{ @@ -24,7 +25,7 @@ async fn main() -> anyhow::Result<()> { // create ethers client and wrap it in Arc let client = ProviderBuilder::new().on_http(rpc_url); - let mut client = AlloyDB::new(client, BlockId::latest()).unwrap(); + let client = WrapDatabaseAsync::new(AlloyDB::new(client, BlockId::latest())).unwrap(); // ----------------------------------------------------------- // // Storage slots of UniV2Pair contract // @@ -53,10 +54,10 @@ async fn main() -> anyhow::Result<()> { let encoded = getReservesCall::new(()).abi_encode(); // query basic properties of an account incl bytecode - let acc_info = client.basic(pool_address).unwrap().unwrap(); + let acc_info = client.basic_ref(pool_address).unwrap().unwrap(); // query value of storage slot at account address - let value = client.storage(pool_address, slot).unwrap(); + let value = client.storage_ref(pool_address, slot).unwrap(); // initialise empty in-memory-db let mut cache_db = CacheDB::new(EmptyDB::default()); diff --git a/examples/uniswap_v2_usdc_swap/src/main.rs b/examples/uniswap_v2_usdc_swap/src/main.rs index 012899b9bb..ccf7c9695a 100644 --- a/examples/uniswap_v2_usdc_swap/src/main.rs +++ b/examples/uniswap_v2_usdc_swap/src/main.rs @@ -9,6 +9,7 @@ use anyhow::{anyhow, Result}; use database::{AlloyDB, CacheDB}; use reqwest::Client; use revm::{ + database_interface::WrapDatabaseAsync, primitives::{address, keccak256, Address, Bytes, TxKind, U256}, state::AccountInfo, wiring::{ @@ -19,7 +20,8 @@ use revm::{ }; use std::ops::Div; -type AlloyCacheDB = CacheDB, Ethereum, RootProvider>>>; +type AlloyCacheDB = + CacheDB, Ethereum, RootProvider>>>>; #[tokio::main] async fn main() -> Result<()> { @@ -29,7 +31,7 @@ async fn main() -> Result<()> { // create ethers client and wrap it in Arc let client = ProviderBuilder::new().on_http(rpc_url); - let alloy = AlloyDB::new(client, BlockId::latest()).unwrap(); + let alloy = WrapDatabaseAsync::new(AlloyDB::new(client, BlockId::latest())).unwrap(); let mut cache_db = CacheDB::new(alloy); // Random empty account