use std::{ collections::{BTreeMap, HashMap}, str::FromStr, sync::Arc, thread, }; use bytes::Bytes; use common::{ errors::{BlockNotFoundError, SlotNotFoundError}, types::BlockTag, }; use ethers::{ abi::ethereum_types::BigEndianHash, types::transaction::eip2930::AccessListItem, types::{Address, H160, H256, U256}, }; use eyre::{Report, Result}; use futures::{executor::block_on, future::join_all}; use log::trace; use revm::{AccountInfo, Bytecode, Database, Env, TransactOut, TransactTo, EVM}; use consensus::types::ExecutionPayload; use crate::{ constants::PARALLEL_QUERY_BATCH_SIZE, errors::EvmError, rpc::ExecutionRpc, types::{Account, CallOpts}, }; use super::ExecutionClient; pub struct Evm<'a, R: ExecutionRpc> { evm: EVM>, chain_id: u64, } impl<'a, R: ExecutionRpc> Evm<'a, R> { pub fn new( execution: Arc>, current_payload: &'a ExecutionPayload, payloads: &'a BTreeMap, chain_id: u64, ) -> Self { let mut evm: EVM> = EVM::new(); let db = ProofDB::new(execution, current_payload, payloads); evm.database(db); Evm { evm, chain_id } } pub async fn call(&mut self, opts: &CallOpts) -> Result, EvmError> { let account_map = self.batch_fetch_accounts(opts).await?; self.evm.db.as_mut().unwrap().set_accounts(account_map); self.evm.env = self.get_env(opts); let tx = self.evm.transact().0; match tx.exit_reason { revm::Return::Revert => match tx.out { TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))), _ => Err(EvmError::Revert(None)), }, revm::Return::Return | revm::Return::Stop => { if let Some(err) = &self.evm.db.as_ref().unwrap().error { return Err(EvmError::Generic(err.clone())); } match tx.out { TransactOut::None => Err(EvmError::Generic("Invalid Call".to_string())), TransactOut::Create(..) => Err(EvmError::Generic("Invalid Call".to_string())), TransactOut::Call(bytes) => Ok(bytes.to_vec()), } } _ => Err(EvmError::Revm(tx.exit_reason)), } } pub async fn estimate_gas(&mut self, opts: &CallOpts) -> Result { let account_map = self.batch_fetch_accounts(opts).await?; self.evm.db.as_mut().unwrap().set_accounts(account_map); self.evm.env = self.get_env(opts); let tx = self.evm.transact().0; let gas = tx.gas_used; match tx.exit_reason { revm::Return::Revert => match tx.out { TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))), _ => Err(EvmError::Revert(None)), }, revm::Return::Return | revm::Return::Stop => { if let Some(err) = &self.evm.db.as_ref().unwrap().error { return Err(EvmError::Generic(err.clone())); } // overestimate to avoid out of gas reverts let gas_scaled = (1.10 * gas as f64) as u64; Ok(gas_scaled) } _ => Err(EvmError::Revm(tx.exit_reason)), } } async fn batch_fetch_accounts( &self, opts: &CallOpts, ) -> Result, EvmError> { let db = self.evm.db.as_ref().unwrap(); let rpc = db.execution.rpc.clone(); let payload = db.current_payload.clone(); let execution = db.execution.clone(); let block = db.current_payload.block_number; let opts_moved = CallOpts { from: opts.from, to: opts.to, value: opts.value, data: opts.data.clone(), gas: opts.gas, gas_price: opts.gas_price, }; let mut list = rpc .create_access_list(&opts_moved, block) .await .map_err(EvmError::RpcError)? .0; let from_access_entry = AccessListItem { address: opts_moved.from.unwrap_or_default(), storage_keys: Vec::default(), }; let to_access_entry = AccessListItem { address: opts_moved.to.unwrap_or_default(), storage_keys: Vec::default(), }; let producer_account = AccessListItem { address: Address::from_slice(&payload.fee_recipient), storage_keys: Vec::default(), }; list.push(from_access_entry); list.push(to_access_entry); list.push(producer_account); let mut account_map = HashMap::new(); for chunk in list.chunks(PARALLEL_QUERY_BATCH_SIZE) { let account_chunk_futs = chunk.iter().map(|account| { let account_fut = execution.get_account( &account.address, Some(account.storage_keys.as_slice()), &payload, ); async move { (account.address, account_fut.await) } }); let account_chunk = join_all(account_chunk_futs).await; account_chunk .into_iter() .filter(|i| i.1.is_ok()) .for_each(|(key, value)| { account_map.insert(key, value.ok().unwrap()); }); } Ok(account_map) } fn get_env(&self, opts: &CallOpts) -> Env { let mut env = Env::default(); let payload = &self.evm.db.as_ref().unwrap().current_payload; env.tx.transact_to = TransactTo::Call(opts.to.unwrap_or_default()); env.tx.caller = opts.from.unwrap_or(Address::zero()); env.tx.value = opts.value.unwrap_or(U256::from(0)); env.tx.data = Bytes::from(opts.data.clone().unwrap_or_default()); env.tx.gas_limit = opts.gas.map(|v| v.as_u64()).unwrap_or(u64::MAX); env.tx.gas_price = opts.gas_price.unwrap_or(U256::zero()); env.block.number = U256::from(payload.block_number); env.block.coinbase = Address::from_slice(&payload.fee_recipient); env.block.timestamp = U256::from(payload.timestamp); env.block.difficulty = U256::from_little_endian(&payload.prev_randao); env.cfg.chain_id = self.chain_id.into(); env } } struct ProofDB<'a, R: ExecutionRpc> { execution: Arc>, current_payload: &'a ExecutionPayload, payloads: &'a BTreeMap, accounts: HashMap, error: Option, } impl<'a, R: ExecutionRpc> ProofDB<'a, R> { pub fn new( execution: Arc>, current_payload: &'a ExecutionPayload, payloads: &'a BTreeMap, ) -> Self { ProofDB { execution, current_payload, payloads, accounts: HashMap::new(), error: None, } } pub fn set_accounts(&mut self, accounts: HashMap) { self.accounts = accounts; } fn get_account(&mut self, address: Address, slots: &[H256]) -> Result { let execution = self.execution.clone(); let payload = self.current_payload.clone(); let slots = slots.to_owned(); let handle = thread::spawn(move || { let account_fut = execution.get_account(&address, Some(&slots), &payload); block_on(account_fut) }); handle.join().unwrap() } } impl<'a, R: ExecutionRpc> Database for ProofDB<'a, R> { type Error = Report; fn basic(&mut self, address: H160) -> Result, Report> { if is_precompile(&address) { return Ok(Some(AccountInfo::default())); } trace!( "fetch basic evm state for address=0x{}", hex::encode(address.as_bytes()) ); let account = match self.accounts.get(&address) { Some(account) => account.clone(), None => self.get_account(address, &[])?, }; let bytecode = Bytecode::new_raw(Bytes::from(account.code.clone())); Ok(Some(AccountInfo::new( account.balance, account.nonce, bytecode, ))) } fn block_hash(&mut self, number: U256) -> Result { let number = number.as_u64(); let payload = self .payloads .get(&number) .ok_or(BlockNotFoundError::new(BlockTag::Number(number)))?; Ok(H256::from_slice(&payload.block_hash)) } fn storage(&mut self, address: H160, slot: U256) -> Result { trace!("fetch evm state for address={:?}, slot={}", address, slot); let slot = H256::from_uint(&slot); Ok(match self.accounts.get(&address) { Some(account) => match account.slots.get(&slot) { Some(slot) => *slot, None => *self .get_account(address, &[slot])? .slots .get(&slot) .ok_or(SlotNotFoundError::new(slot))?, }, None => *self .get_account(address, &[slot])? .slots .get(&slot) .ok_or(SlotNotFoundError::new(slot))?, }) } fn code_by_hash(&mut self, _code_hash: H256) -> Result { Err(eyre::eyre!("should never be called")) } } fn is_precompile(address: &Address) -> bool { address.le(&Address::from_str("0x0000000000000000000000000000000000000009").unwrap()) && address.gt(&Address::zero()) } #[cfg(test)] mod tests { use common::utils::hex_str_to_bytes; use ssz_rs::Vector; use crate::rpc::mock_rpc::MockRpc; use super::*; fn get_client() -> ExecutionClient { ExecutionClient::new("testdata/").unwrap() } #[test] fn test_proof_db() { // Construct proofdb params let execution = get_client(); let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap(); let payload = ExecutionPayload { state_root: Vector::from_iter( hex_str_to_bytes( "0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d", ) .unwrap(), ), ..ExecutionPayload::default() }; let mut payloads = BTreeMap::new(); payloads.insert(7530933, payload.clone()); // Construct the proof database with the given client and payloads let mut proof_db = ProofDB::new(Arc::new(execution), &payload, &payloads); // Set the proof db accounts let slot = U256::from(1337); let mut accounts = HashMap::new(); let account = Account { balance: U256::from(100), code: hex_str_to_bytes("0x").unwrap(), ..Default::default() }; accounts.insert(address, account); proof_db.set_accounts(accounts); // Get the account from the proof database let storage_proof = proof_db.storage(address, slot); // Check that the storage proof correctly returns a slot not found error let expected_err: eyre::Report = SlotNotFoundError::new(H256::from_uint(&slot)).into(); assert_eq!( expected_err.to_string(), storage_proof.unwrap_err().to_string() ); } }