diff --git a/lib/ain-evm/src/backend.rs b/lib/ain-evm/src/backend.rs index e565c39717a..662db090033 100644 --- a/lib/ain-evm/src/backend.rs +++ b/lib/ain-evm/src/backend.rs @@ -64,8 +64,8 @@ struct OverlayData { storage: HashMap, } -#[derive(Debug, Clone)] -struct Overlay { +#[derive(Debug, Clone, Default)] +pub struct Overlay { state: HashMap, changeset: Vec>, deletes: HashSet, @@ -145,6 +145,7 @@ impl EVMBackend { trie_store: Arc, storage: Arc, vicinity: Vicinity, + overlay: Option, ) -> Result { let state = trie_store .trie_db @@ -156,7 +157,7 @@ impl EVMBackend { trie_store, storage, vicinity, - overlay: Overlay::new(), + overlay: overlay.unwrap_or_default(), }) } diff --git a/lib/ain-evm/src/core.rs b/lib/ain-evm/src/core.rs index d26d4fed65d..530b6cb6441 100644 --- a/lib/ain-evm/src/core.rs +++ b/lib/ain-evm/src/core.rs @@ -28,7 +28,7 @@ use parking_lot::Mutex; use vsdb_core::vsdb_set_base_dir; use crate::{ - backend::{BackendError, EVMBackend, Vicinity}, + backend::{BackendError, EVMBackend, Overlay, Vicinity}, block::INITIAL_BASE_FEE, blocktemplate::BlockTemplate, executor::{AinExecutor, ExecutorContext, TxResponse}, @@ -239,7 +239,7 @@ impl EVMCoreService { self.trie_store.flush() } - pub fn call(&self, arguments: EthCallArgs) -> Result { + pub fn call(&self, arguments: EthCallArgs, overlay: Option) -> Result { let EthCallArgs { caller, to, @@ -276,6 +276,7 @@ impl EVMCoreService { Arc::clone(&self.trie_store), Arc::clone(&self.storage), vicinity, + overlay, ) .map_err(|e| format_err!("------ Could not restore backend {}", e))?; @@ -766,6 +767,7 @@ impl EVMCoreService { Arc::clone(&self.trie_store), Arc::clone(&self.storage), vicinity, + None, ) .map_err(|e| format_err!("Could not restore backend {}", e))?; backend.update_vicinity_from_tx(tx)?; @@ -832,6 +834,7 @@ impl EVMCoreService { Arc::clone(&self.trie_store), Arc::clone(&self.storage), Vicinity::default(), + None, )?; Ok(backend.get_account(&address)) } @@ -901,6 +904,7 @@ impl EVMCoreService { Arc::clone(&self.trie_store), Arc::clone(&self.storage), vicinity, + None, ) } @@ -918,6 +922,7 @@ impl EVMCoreService { ), ..Vicinity::default() }, + None, ) } diff --git a/lib/ain-evm/src/evm.rs b/lib/ain-evm/src/evm.rs index 8e7c701fec7..3297a552617 100644 --- a/lib/ain-evm/src/evm.rs +++ b/lib/ain-evm/src/evm.rs @@ -526,6 +526,7 @@ impl EVMServices { Arc::clone(&self.core.trie_store), Arc::clone(&self.storage), vicinity.clone(), + None, )?; let template = BlockTemplate::new(vicinity, ctx, backend); diff --git a/lib/ain-evm/src/lib.rs b/lib/ain-evm/src/lib.rs index 0b171178e5e..f1244523e9a 100644 --- a/lib/ain-evm/src/lib.rs +++ b/lib/ain-evm/src/lib.rs @@ -1,6 +1,6 @@ //! Defichain EVM consensus, runtime and storage implementation -mod backend; +pub mod backend; pub mod block; pub mod blocktemplate; pub mod bytes; diff --git a/lib/ain-evm/src/trie.rs b/lib/ain-evm/src/trie.rs index 7b828ac80a0..03cdbccb477 100644 --- a/lib/ain-evm/src/trie.rs +++ b/lib/ain-evm/src/trie.rs @@ -66,6 +66,7 @@ impl TrieDBStore { Arc::clone(trie_store), Arc::clone(storage), Vicinity::default(), + None, ) .expect("Could not restore backend"); diff --git a/lib/ain-grpc/src/call_request.rs b/lib/ain-grpc/src/call_request.rs index bc232349f9d..98c7b7efba1 100644 --- a/lib/ain-grpc/src/call_request.rs +++ b/lib/ain-grpc/src/call_request.rs @@ -1,6 +1,8 @@ -use ain_evm::bytes::Bytes; -use ethereum::AccessListItem; -use ethereum_types::{H160, U256}; +use std::collections::{BTreeMap, HashMap}; + +use ain_evm::{backend::Overlay, bytes::Bytes}; +use ethereum::{AccessListItem, Account}; +use ethereum_types::{H160, H256, U256}; use jsonrpsee::core::Error; use serde::Deserialize; @@ -127,3 +129,53 @@ impl CallRequest { } } } + +// State override +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +pub struct CallStateOverride { + /// Fake balance to set for the account before executing the call. + pub balance: Option, + /// Fake nonce to set for the account before executing the call. + pub nonce: Option, + /// Fake EVM bytecode to inject into the account before executing the call. + pub code: Option, + /// Fake key-value mapping to override all slots in the account storage before + /// executing the call. + pub state: Option>, + /// Fake key-value mapping to override individual slots in the account storage before + /// executing the call. + pub state_diff: Option>, +} + +pub fn override_to_overlay(r#override: BTreeMap) -> Overlay { + let mut overlay = Overlay::default(); + + for (address, state_override) in r#override { + let code = state_override.code.map(|b| b.into_vec()); + let mut storage = state_override + .state + .unwrap_or_default() + .into_iter() + .collect::>(); + + let account = Account { + balance: state_override.balance.unwrap_or_default(), + nonce: state_override.nonce.unwrap_or_default(), + storage_root: H256::zero(), + code_hash: H256::zero(), + }; + + let reset_storage = storage.is_empty(); + if let Some(diff) = state_override.state_diff { + for (k, v) in diff { + storage.insert(k, v); + } + } + + overlay.apply(address, account, code, storage, reset_storage); + } + + overlay +} diff --git a/lib/ain-grpc/src/rpc/debug.rs b/lib/ain-grpc/src/rpc/debug.rs index 27a4b063a7c..50326a3d877 100644 --- a/lib/ain-grpc/src/rpc/debug.rs +++ b/lib/ain-grpc/src/rpc/debug.rs @@ -130,16 +130,19 @@ impl MetachainDebugRPCServer for MetachainDebugRPCModule { let TxResponse { used_gas, .. } = self .handler .core - .call(EthCallArgs { - caller, - to: call.to, - value: call.value.unwrap_or_default(), - data, - gas_limit, - gas_price, - access_list: call.access_list.unwrap_or_default(), - block_number, - }) + .call( + EthCallArgs { + caller, + to: call.to, + value: call.value.unwrap_or_default(), + data, + gas_limit, + gas_price, + access_list: call.access_list.unwrap_or_default(), + block_number, + }, + None, + ) .map_err(RPCError::EvmError)?; let used_gas = U256::from(used_gas); diff --git a/lib/ain-grpc/src/rpc/eth.rs b/lib/ain-grpc/src/rpc/eth.rs index 3e56fd971e6..106449b3543 100644 --- a/lib/ain-grpc/src/rpc/eth.rs +++ b/lib/ain-grpc/src/rpc/eth.rs @@ -1,4 +1,4 @@ -use std::{convert::Into, str::FromStr, sync::Arc}; +use std::{collections::BTreeMap, convert::Into, str::FromStr, sync::Arc}; use ain_cpp_imports::get_eth_priv_key; use ain_evm::{ @@ -22,7 +22,7 @@ use log::{debug, trace}; use crate::{ block::{BlockNumber, RpcBlock, RpcFeeHistory}, - call_request::CallRequest, + call_request::{override_to_overlay, CallRequest, CallStateOverride}, codegen::types::EthTransactionInfo, errors::{to_custom_err, RPCError}, filters::{GetFilterChangesResult, NewFilterRequest}, @@ -42,7 +42,12 @@ pub trait MetachainRPC { /// Makes a call to the Ethereum node without creating a transaction on the blockchain. /// Returns the output data as a hexadecimal string. #[method(name = "call")] - fn call(&self, call: CallRequest, block_number: Option) -> RpcResult; + fn call( + &self, + input: CallRequest, + block_number: Option, + state_overrides: Option>, + ) -> RpcResult; /// Retrieves the list of accounts managed by the node. /// Returns a vector of Ethereum addresses as hexadecimal strings. @@ -207,8 +212,12 @@ pub trait MetachainRPC { /// Estimate gas needed for execution of given contract. #[method(name = "estimateGas")] - fn estimate_gas(&self, call: CallRequest, block_number: Option) - -> RpcResult; + fn estimate_gas( + &self, + input: CallRequest, + block_number: Option, + state_overrides: Option>, + ) -> RpcResult; /// Returns current gas_price. #[method(name = "gasPrice")] @@ -309,7 +318,12 @@ impl MetachainRPCModule { } impl MetachainRPCServer for MetachainRPCModule { - fn call(&self, call: CallRequest, block_number: Option) -> RpcResult { + fn call( + &self, + call: CallRequest, + block_number: Option, + state_overrides: Option>, + ) -> RpcResult { debug!(target:"rpc", "Call, input {:#?}", call); let caller = call.from.unwrap_or_default(); @@ -330,16 +344,19 @@ impl MetachainRPCServer for MetachainRPCModule { } = self .handler .core - .call(EthCallArgs { - caller, - to: call.to, - value: call.value.unwrap_or_default(), - data, - gas_limit, - gas_price, - access_list: call.access_list.unwrap_or_default(), - block_number: block.header.number, - }) + .call( + EthCallArgs { + caller, + to: call.to, + value: call.value.unwrap_or_default(), + data, + gas_limit, + gas_price, + access_list: call.access_list.unwrap_or_default(), + block_number: block.header.number, + }, + state_overrides.map(override_to_overlay), + ) .map_err(RPCError::EvmError)?; match exit_reason { @@ -762,6 +779,7 @@ impl MetachainRPCServer for MetachainRPCModule { &self, call: CallRequest, block_number: Option, + state_overrides: Option>, ) -> RpcResult { debug!(target:"rpc", "Estimate gas, input {:#?}", call); @@ -774,6 +792,7 @@ impl MetachainRPCServer for MetachainRPCModule { let call_gas = u64::try_from(call.gas.unwrap_or(U256::from(block_gas_limit))) .map_err(to_custom_err)?; + let overlay = state_overrides.map(override_to_overlay); // Determine the lowest and highest possible gas limits to binary search in between let mut lo = Self::CONFIG.gas_transaction_call - 1; let mut hi = call_gas; @@ -826,16 +845,19 @@ impl MetachainRPCServer for MetachainRPCModule { let tx_response = self .handler .core - .call(EthCallArgs { - caller, - to: call.to, - value: call.value.unwrap_or_default(), - data, - gas_limit, - gas_price: fee_cap, - access_list: call.access_list.clone().unwrap_or_default(), - block_number: block.header.number, - }) + .call( + EthCallArgs { + caller, + to: call.to, + value: call.value.unwrap_or_default(), + data, + gas_limit, + gas_price: fee_cap, + access_list: call.access_list.clone().unwrap_or_default(), + block_number: block.header.number, + }, + overlay.clone(), + ) .map_err(RPCError::EvmError)?; match tx_response.exit_reason { diff --git a/test/functional/feature_evm_rpc.py b/test/functional/feature_evm_rpc.py index 19c727935da..f310b5488d5 100755 --- a/test/functional/feature_evm_rpc.py +++ b/test/functional/feature_evm_rpc.py @@ -246,10 +246,17 @@ def test_eth_call_transfer(self): self.invalid_balance_transfer_tx_insufficient_funds, ) + # Should pass with state override + self.nodes[0].eth_call( + self.invalid_balance_transfer_tx_insufficient_funds, + "latest", + {self.ethAddress: {"balance": "0x152D02C7E14AF6800000"}}, + ) + def test_eth_call_contract(self): self.rollback_to(self.start_height) - abi, bytecode, _ = EVMContract.from_file("Loop.sol", "Loop").compile() + abi, bytecode, deployed = EVMContract.from_file("Loop.sol", "Loop").compile() compiled = self.nodes[0].w3.eth.contract(abi=abi, bytecode=bytecode) tx = compiled.constructor().build_transaction( { @@ -271,6 +278,20 @@ def test_eth_call_contract(self): res = contract.functions.loop(10_000).call() assert_equal(res, []) + def test_eth_call_contract_override(self): + self.rollback_to(self.start_height) + + contractAddress = self.nodes[0].getnewaddress("", "erc55") + abi, _, deployed = EVMContract.from_file("Loop.sol", "Loop").compile() + + contract = self.nodes[0].w3.eth.contract(address=contractAddress, abi=abi) + + # Test valid contract function eth call overriding contract code + res = contract.functions.loop(10_000).call( + {}, "latest", {contractAddress: {"code": "0x" + deployed}} + ) + assert_equal(res, []) + def test_eth_call_revert(self): self.rollback_to(self.start_height) @@ -304,6 +325,30 @@ def test_eth_call_revert(self): contract.functions.value_check(0).call, ) + def test_eth_call_revert_override(self): + self.rollback_to(self.start_height) + + contractAddress = self.nodes[0].getnewaddress("", "erc55") + abi, _, deployed = EVMContract.from_file("Require.sol", "Require").compile() + + contract = self.nodes[0].w3.eth.contract(address=contractAddress, abi=abi) + + # Test valid contract function eth call overriding contract code + res = contract.functions.value_check(1).call( + {}, "latest", {contractAddress: {"code": "0x" + deployed}} + ) + assert_equal(res, []) + + # Test invalid contract function eth call with revert overriding contract code + assert_raises_web3_error( + 3, + "execution reverted: Value must be greater than 0", + contract.functions.value_check(0).call, + {}, + "latest", + {contractAddress: {"code": "0x" + deployed}}, + ) + def test_accounts(self): self.rollback_to(self.start_height) @@ -463,8 +508,12 @@ def run_test(self): self.test_eth_call_contract() + self.test_eth_call_contract_override() + self.test_eth_call_revert() + self.test_eth_call_revert_override() + self.test_accounts() self.test_address_state() # TODO test smart contract