diff --git a/lib/ain-evm/src/core.rs b/lib/ain-evm/src/core.rs index ce0e12aac1..5438a504d9 100644 --- a/lib/ain-evm/src/core.rs +++ b/lib/ain-evm/src/core.rs @@ -21,7 +21,7 @@ use crate::{ block::INITIAL_BASE_FEE, blocktemplate::BlockTemplate, eventlistener::ExecutionStep, - executor::{AinExecutor, ExecutorContext, TxResponse}, + executor::{AccessListInfo, AinExecutor, ExecutorContext, TxResponse}, fee::calculate_max_prepay_gas_fee, gas::check_tx_intrinsic_gas, receipt::ReceiptService, @@ -685,6 +685,42 @@ impl EVMCoreService { ) } + pub fn get_backend_from_block( + &self, + block_number: U256, + caller: Option, + gas_price: Option, + overlay: Option, + ) -> Result { + let block_header = self + .storage + .get_block_by_number(&block_number)? + .map(|block| block.header) + .ok_or(format_err!("Block number {:x?} not found", block_number))?; + let state_root = block_header.state_root; + debug!( + "Calling EVM at block number : {:#x}, state_root : {:#x}", + block_number, state_root + ); + + let mut vicinity = Vicinity::from(block_header); + if let Some(gas_price) = gas_price { + vicinity.gas_price = gas_price; + } + if let Some(caller) = caller { + vicinity.origin = caller; + } + debug!("Vicinity: {:?}", vicinity); + + EVMBackend::from_root( + state_root, + Arc::clone(&self.trie_store), + Arc::clone(&self.storage), + vicinity, + overlay, + ) + } + pub fn get_next_account_nonce(&self, address: H160, state_root: H256) -> Result { let state_root_nonce = self.get_nonce(address, state_root)?; let mut nonce_store = self.nonce_store.lock(); @@ -748,36 +784,9 @@ impl EVMCoreService { access_list, block_number, } = arguments; - debug!("[call] caller: {:?}", caller); - - let block_header = self - .storage - .get_block_by_number(&block_number)? - .map(|block| block.header) - .ok_or(format_err!( - "[call] Block number {:x?} not found", - block_number - ))?; - let state_root = block_header.state_root; - debug!( - "Calling EVM at block number : {:#x}, state_root : {:#x}", - block_number, state_root - ); - - let mut vicinity = Vicinity::from(block_header); - vicinity.gas_price = gas_price; - vicinity.origin = caller; - debug!("[call] vicinity: {:?}", vicinity); - - let mut backend = EVMBackend::from_root( - state_root, - Arc::clone(&self.trie_store), - Arc::clone(&self.storage), - vicinity, - overlay, - ) - .map_err(|e| format_err!("Could not restore backend {}", e))?; - + let mut backend = self + .get_backend_from_block(block_number, Some(caller), Some(gas_price), overlay) + .map_err(|e| format_err!("Could not restore backend {}", e))?; Ok(AinExecutor::new(&mut backend).call(ExecutorContext { caller, to, @@ -788,32 +797,38 @@ impl EVMCoreService { })) } + pub fn create_access_list(&self, arguments: EthCallArgs) -> Result { + let EthCallArgs { + caller, + to, + value, + data, + gas_limit, + gas_price, + access_list, + block_number, + } = arguments; + let mut backend = self + .get_backend_from_block(block_number, Some(caller), Some(gas_price), None) + .map_err(|e| format_err!("Could not restore backend {}", e))?; + AinExecutor::new(&mut backend).exec_access_list(ExecutorContext { + caller, + to, + value, + data, + gas_limit, + access_list, + }) + } + pub fn call_with_tracer( &self, tx: &SignedTx, block_number: U256, ) -> Result<(Vec, bool, Vec, u64)> { - let block_header = self - .storage - .get_block_by_number(&block_number)? - .ok_or_else(|| format_err!("Block not found")) - .map(|block| block.header)?; - let state_root = block_header.state_root; - debug!( - "Calling EVM at block number : {:#x}, state_root : {:#x}", - block_number, state_root - ); - - let vicinity = Vicinity::from(block_header); - let mut backend = EVMBackend::from_root( - state_root, - 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)?; + let mut backend = self + .get_backend_from_block(block_number, None, None, None) + .map_err(|e| format_err!("Could not restore backend {}", e))?; AinExecutor::new(&mut backend).exec_with_tracer(tx) } } diff --git a/lib/ain-evm/src/eventlistener.rs b/lib/ain-evm/src/eventlistener.rs index 87abedce8d..4d23ba0a2d 100644 --- a/lib/ain-evm/src/eventlistener.rs +++ b/lib/ain-evm/src/eventlistener.rs @@ -1,7 +1,7 @@ -use std::collections::VecDeque; +use std::collections::{HashMap, HashSet, VecDeque}; use crate::opcode; -use ethereum_types::H256; +use ethereum_types::{H160, H256}; use evm::gasometer::tracing::{Event as GasEvent, EventListener as GasEventListener}; use evm_runtime::{ tracing::{Event as RuntimeEvent, EventListener as RuntimeEventListener}, @@ -156,3 +156,23 @@ impl GasEventListener for GasListener { } } } + +#[derive(Default)] +pub struct StorageAccessListener { + pub access_list: HashMap>, +} + +impl RuntimeEventListener for StorageAccessListener { + fn event(&mut self, event: RuntimeEvent<'_>) { + debug!("event runtime : {:#?}", event); + match event { + RuntimeEvent::SLoad { address, index, .. } => { + self.access_list.entry(address).or_default().insert(index); + } + RuntimeEvent::SStore { address, index, .. } => { + self.access_list.entry(address).or_default().insert(index); + } + _ => {} + } + } +} diff --git a/lib/ain-evm/src/executor.rs b/lib/ain-evm/src/executor.rs index 2869fe06e8..f286551b29 100644 --- a/lib/ain-evm/src/executor.rs +++ b/lib/ain-evm/src/executor.rs @@ -1,6 +1,6 @@ use ain_contracts::{get_transfer_domain_contract, FixedContract}; use anyhow::format_err; -use ethereum::{AccessList, EIP658ReceiptData, Log, ReceiptV3}; +use ethereum::{AccessList, AccessListItem, EIP658ReceiptData, Log, ReceiptV3}; use ethereum_types::{Bloom, H160, H256, U256}; use evm::{ backend::{ApplyBackend, Backend}, @@ -20,7 +20,7 @@ use crate::{ dst20_deploy_info, DST20BridgeInfo, DeployContractInfo, }, core::EVMCoreService, - eventlistener::{ExecListener, ExecutionStep, GasListener}, + eventlistener::{ExecListener, ExecutionStep, GasListener, StorageAccessListener}, fee::{calculate_current_prepay_gas_fee, calculate_gas_fee}, precompiles::MetachainPrecompiles, transaction::{ @@ -42,6 +42,11 @@ impl From for ExecuteTx { } } +pub struct AccessListInfo { + pub access_list: AccessList, + pub gas_used: U256, +} + #[derive(Debug)] pub struct ExecutorContext<'a> { pub caller: H160, @@ -56,6 +61,7 @@ pub struct AinExecutor<'backend> { pub backend: &'backend mut EVMBackend, } +// State update methods impl<'backend> AinExecutor<'backend> { pub fn new(backend: &'backend mut EVMBackend) -> Self { Self { backend } @@ -95,6 +101,7 @@ impl<'backend> AinExecutor<'backend> { } } +// EVM executor methods impl<'backend> AinExecutor<'backend> { const CONFIG: Config = Config::shanghai(); @@ -249,6 +256,7 @@ impl<'backend> AinExecutor<'backend> { )) } + /// Execute tx with tracer pub fn exec_with_tracer( &self, signed_tx: &SignedTx, @@ -309,6 +317,89 @@ impl<'backend> AinExecutor<'backend> { Ok((listener.trace, exec_flag, data, used_gas)) } + /// Execute tx with storage access listener + pub fn exec_access_list(&self, ctx: ExecutorContext) -> Result { + let access_list = ctx + .access_list + .into_iter() + .map(|x| (x.address, x.storage_keys)) + .collect::>(); + + let metadata = StackSubstateMetadata::new(ctx.gas_limit, &Self::CONFIG); + let state = MemoryStackState::new(metadata.clone(), self.backend); + let al_state = MemoryStackState::new(metadata, self.backend); + let precompiles = MetachainPrecompiles; + let mut al_executor = + StackExecutor::new_with_precompiles(al_state, &Self::CONFIG, &precompiles); + let mut executor = StackExecutor::new_with_precompiles(state, &Self::CONFIG, &precompiles); + let mut listener = StorageAccessListener::default(); + + let (exit_reason, _) = runtime_using(&mut listener, move || match ctx.to { + Some(to) => executor.transact_call( + ctx.caller, + to, + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + None => executor.transact_create( + ctx.caller, + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + }); + if !exit_reason.is_succeed() { + return Err(format_err!("[exec_access_list] tx execution failed").into()); + } + + // Get access list from listener + let al: AccessList = listener + .access_list + .into_iter() + .map(|(address, storage_keys)| AccessListItem { + address, + storage_keys: Vec::from_iter(storage_keys), + }) + .collect(); + let access_list = al + .clone() + .into_iter() + .map(|x| (x.address, x.storage_keys)) + .collect::>(); + + // Get gas usage with accumulated access list + let (exit_reason, _) = match ctx.to { + Some(to) => al_executor.transact_call( + ctx.caller, + to, + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + None => al_executor.transact_create( + ctx.caller, + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + }; + if !exit_reason.is_succeed() { + return Err(format_err!("[exec_access_list] tx execution failed").into()); + } + Ok(AccessListInfo { + access_list: al, + gas_used: U256::from(al_executor.used_gas()), + }) + } +} + +impl<'backend> AinExecutor<'backend> { + /// System tx execution pub fn execute_tx(&mut self, tx: ExecuteTx, base_fee: U256) -> Result { match tx { ExecuteTx::SignedTx(signed_tx) => { diff --git a/lib/ain-grpc/src/call_request.rs b/lib/ain-grpc/src/call_request.rs index e8650083a0..2ce257950a 100644 --- a/lib/ain-grpc/src/call_request.rs +++ b/lib/ain-grpc/src/call_request.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, HashMap}; -use ain_evm::{backend::Overlay, bytes::Bytes}; -use ethereum::{AccessListItem, Account}; +use ain_evm::{backend::Overlay, bytes::Bytes, executor::AccessListInfo}; +use ethereum::{AccessList, AccessListItem, Account}; use ethereum_types::{H160, H256, U256}; use jsonrpsee::core::Error; use serde::Deserialize; @@ -173,3 +173,19 @@ pub fn override_to_overlay(r#override: BTreeMap) -> Ove overlay } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AccessListResult { + pub access_list: AccessList, + pub gas_used: U256, +} + +impl From for AccessListResult { + fn from(value: AccessListInfo) -> Self { + Self { + access_list: value.access_list, + gas_used: value.gas_used, + } + } +} diff --git a/lib/ain-grpc/src/lib.rs b/lib/ain-grpc/src/lib.rs index 125eeb6206..4614a5d9fa 100644 --- a/lib/ain-grpc/src/lib.rs +++ b/lib/ain-grpc/src/lib.rs @@ -12,13 +12,12 @@ pub mod logging; mod logs; mod receipt; pub mod rpc; +mod subscription; mod sync; mod transaction; mod transaction_request; mod utils; -mod subscription; - #[cfg(test)] mod tests; diff --git a/lib/ain-grpc/src/rpc/eth.rs b/lib/ain-grpc/src/rpc/eth.rs index bac7e1ae80..b1962ce2ce 100644 --- a/lib/ain-grpc/src/rpc/eth.rs +++ b/lib/ain-grpc/src/rpc/eth.rs @@ -22,7 +22,7 @@ use log::{debug, trace}; use crate::{ block::{BlockNumber, RpcBlock, RpcFeeHistory}, - call_request::{override_to_overlay, CallRequest, CallStateOverride}, + call_request::{override_to_overlay, AccessListResult, CallRequest, CallStateOverride}, codegen::types::EthTransactionInfo, errors::{to_custom_err, RPCError}, filters::{GetFilterChangesResult, NewFilterRequest}, @@ -153,6 +153,14 @@ pub trait MetachainRPC { #[method(name = "getTransactionReceipt")] fn get_receipt(&self, hash: H256) -> RpcResult>; + /// Create access list from a specified transaction call context. + #[method(name = "createAccessList")] + fn create_access_list( + &self, + call: CallRequest, + block_number: Option, + ) -> RpcResult; + // ---------------------------------------- // Account state // ---------------------------------------- @@ -636,7 +644,7 @@ impl MetachainRPCServer for MetachainRPCModule { None => { let accounts = self.accounts()?; - match accounts.get(0) { + match accounts.first() { Some(account) => H160::from_str(account.as_str()) .map_err(|_| to_custom_err("Wrong from address"))?, None => return Err(to_custom_err("from is not available")), @@ -983,6 +991,42 @@ impl MetachainRPCServer for MetachainRPCModule { ]) } + fn create_access_list( + &self, + call: CallRequest, + block_number: Option, + ) -> RpcResult { + let caller = call.from.unwrap_or_default(); + let byte_data = call.get_data()?; + let data = byte_data.0.as_slice(); + + // Get gas + let block_gas_limit = ain_cpp_imports::get_attribute_values(None).block_gas_limit; + let gas_limit = u64::try_from(call.gas.unwrap_or(U256::from(block_gas_limit))) + .map_err(to_custom_err)?; + + let block = self.get_block(block_number)?; + let block_base_fee = block.header.base_fee; + let gas_price = call.get_effective_gas_price()?.unwrap_or(block_base_fee); + + let res = self + .handler + .core + .create_access_list(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, + }) + .map_err(RPCError::EvmError)? + .into(); + Ok(res) + } + fn submit_work(&self, _nonce: String, _hash: String, _digest: String) -> RpcResult { Ok(false) } diff --git a/lib/ain-grpc/src/utils.rs b/lib/ain-grpc/src/utils.rs index c8de5a62c1..db2b329e3e 100644 --- a/lib/ain-grpc/src/utils.rs +++ b/lib/ain-grpc/src/utils.rs @@ -19,10 +19,7 @@ pub fn format_u256(number: U256) -> String { pub fn try_get_reverted_error_or_default(data: &[u8]) -> String { if data.len() > MESSAGE_START_IDX { - let Ok(message_len) = U256::try_from(&data[MESSAGE_LENGTH_START_IDX..MESSAGE_START_IDX]) - else { - return Default::default(); - }; + let message_len = U256::from(&data[MESSAGE_LENGTH_START_IDX..MESSAGE_START_IDX]); let Ok(message_len) = usize::try_from(message_len) else { return Default::default(); }; diff --git a/test/functional/feature_evm_dst20.py b/test/functional/feature_evm_dst20.py index f800055c4d..33fa18557d 100755 --- a/test/functional/feature_evm_dst20.py +++ b/test/functional/feature_evm_dst20.py @@ -5,11 +5,6 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. """Test EVM behaviour""" -import math -import json -import time -from decimal import Decimal - from test_framework.evm_key_pair import EvmKeyPair from test_framework.test_framework import DefiTestFramework from test_framework.util import ( @@ -17,6 +12,11 @@ assert_raises_rpc_error, get_solc_artifact_path, ) + +import math +import json +import time +from decimal import Decimal from web3 import Web3 diff --git a/test/functional/feature_evm_rpc_accesslist.py b/test/functional/feature_evm_rpc_accesslist.py new file mode 100644 index 0000000000..127474f100 --- /dev/null +++ b/test/functional/feature_evm_rpc_accesslist.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test eth_createAccessList RPC behaviour""" + +from test_framework.evm_key_pair import EvmKeyPair +from test_framework.test_framework import DefiTestFramework +from test_framework.evm_contract import EVMContract +from test_framework.util import ( + assert_equal, + get_solc_artifact_path, +) + +from decimal import Decimal +import math +import json +from web3 import Web3 + + +class AccessListTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [ + [ + "-txordering=2", + "-dummypos=0", + "-txnotokens=0", + "-amkheight=50", + "-bayfrontheight=51", + "-dakotaheight=51", + "-eunosheight=80", + "-fortcanningheight=82", + "-fortcanninghillheight=84", + "-fortcanningroadheight=86", + "-fortcanningcrunchheight=88", + "-fortcanningspringheight=90", + "-fortcanninggreatworldheight=94", + "-fortcanningepilogueheight=96", + "-grandcentralheight=101", + "-metachainheight=153", + "-df23height=153", + "-subsidytest=1", + ] + ] + + def setup(self): + self.w0 = self.nodes[0].w3 + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + self.erc55_address = self.nodes[0].addressmap(self.address, 1)["format"][ + "erc55" + ] + + # Contract addresses + self.contract_address_transfer_domain = self.w0.to_checksum_address( + "0xdf00000000000000000000000000000000000001" + ) + self.contract_address_usdt = self.w0.to_checksum_address( + "0xff00000000000000000000000000000000000001" + ) + + # Contract ABI + # Implementation ABI since we want to call functions from the implementation + self.abi = open( + get_solc_artifact_path("dst20_v1", "abi.json"), + "r", + encoding="utf8", + ).read() + + # Proxy bytecode since we want to check proxy deployment + self.bytecode = json.loads( + open( + get_solc_artifact_path("dst20", "deployed_bytecode.json"), + "r", + encoding="utf8", + ).read() + )["object"] + + self.reserved_bytecode = json.loads( + open( + get_solc_artifact_path("dfi_reserved", "deployed_bytecode.json"), + "r", + encoding="utf8", + ).read() + )["object"] + + # Generate chain + self.nodes[0].generate(153) + self.nodes[0].utxostoaccount({self.address: "1000@DFI"}) + + # Create token before EVM + self.nodes[0].createtoken( + { + "symbol": "USDT", + "name": "USDT token", + "isDAT": True, + "collateralAddress": self.address, + } + ) + self.nodes[0].generate(1) + self.nodes[0].minttokens("10@USDT") + self.nodes[0].generate(2) + + self.key_pair = EvmKeyPair.from_node(self.nodes[0]) + self.key_pair2 = EvmKeyPair.from_node(self.nodes[0]) + + self.nodes[0].setgov( + { + "ATTRIBUTES": { + "v0/params/feature/evm": "true", + "v0/params/feature/transferdomain": "true", + "v0/transferdomain/dvm-evm/enabled": "true", + "v0/transferdomain/dvm-evm/dat-enabled": "true", + "v0/transferdomain/evm-dvm/dat-enabled": "true", + } + } + ) + self.nodes[0].generate(2) + self.usdt = self.nodes[0].w3.eth.contract( + address=self.contract_address_usdt, abi=self.abi + ) + + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "1@USDT", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "1@USDT", + "domain": 3, + }, + "singlekeycheck": False, + } + ] + ) + self.nodes[0].generate(1) + balance = self.usdt.functions.balanceOf( + self.key_pair.address + ).call() / math.pow(10, self.usdt.functions.decimals().call()) + assert_equal(balance, Decimal(1)) + + self.start_height = self.nodes[0].getblockcount() + self.transfer_data = self.usdt.encodeABI( + fn_name="transfer", + args=[self.key_pair2.address, Web3.to_wei("0.5", "ether")], + ) + + def test_rpc_contract_call(self): + self.rollback_to(self.start_height) + + call_tx = { + "from": self.key_pair.address, + "to": self.contract_address_usdt, + "data": self.transfer_data, + } + access_list = self.nodes[0].eth_createAccessList(call_tx)["accessList"] + assert_equal(len(access_list), 1) + assert_equal(len(access_list[0]["storageKeys"]), 3) + assert_equal( + access_list[0]["address"], "0xff00000000000000000000000000000000000001" + ) + + def test_rpc_contract_creation(self): + self.rollback_to(self.start_height) + + _, bytecode, _ = EVMContract.from_file("Reverter.sol", "Reverter").compile() + call_tx = { + "from": self.key_pair.address, + "data": "0x" + bytecode, + } + access_list = self.nodes[0].eth_createAccessList(call_tx)["accessList"] + assert_equal(access_list, []) + + def run_test(self): + self.setup() + + self.test_rpc_contract_call() + + self.test_rpc_contract_creation() + + +if __name__ == "__main__": + AccessListTest().main() diff --git a/test/functional/feature_evm_rpc_fee.py b/test/functional/feature_evm_rpc_fee.py index 51c015ff07..889e4d5fe3 100755 --- a/test/functional/feature_evm_rpc_fee.py +++ b/test/functional/feature_evm_rpc_fee.py @@ -94,7 +94,7 @@ def setup(self): } ] ) - self.nodes[0].generate(2) + self.nodes[0].generate(1) balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") assert_equal(balance, int_to_eth_u256(50)) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 4ddafd5e5c..7db75fd660 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -312,6 +312,7 @@ "feature_evm_rollback.py", "feature_evm_rpc_transaction.py", "feature_evm_rpc.py", + "feature_evm_rpc_accesslist.py", "feature_evm_rpc_fee.py", "feature_evm_rpc_filters.py", "feature_evm_smart_contract.py",