From 2b09a676cac8cca665c882a28bd13b2acef39395 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 11 Apr 2023 09:28:32 +0200 Subject: [PATCH] implement eth_call state override (#1027) * make call_api_at work * make override work * implement default storage override * use address indexed stateOverrides * allow overriding state in eth_call * add tests * resolve conflicts * use explicit param * use concrete type in EthDeps * fix merge conflicts * format toml * try-debug failure * debug test * change value for AddressMapping * remove debug * fmt * fix build * fix build * name refactor * attempt simplifying rpc Eth traits * rename default implementations * cleanup * lint * bump * fmt * fix clippy * make default implementation no-op * fix build --- Cargo.lock | 5 + client/rpc-core/src/eth.rs | 8 +- client/rpc-core/src/types/call_request.rs | 22 +- client/rpc-core/src/types/mod.rs | 2 +- client/rpc/Cargo.toml | 4 + client/rpc/src/eth/block.rs | 4 +- client/rpc/src/eth/client.rs | 7 +- client/rpc/src/eth/execute.rs | 181 +++++++++++++--- client/rpc/src/eth/fee.rs | 7 +- client/rpc/src/eth/mining.rs | 6 +- client/rpc/src/eth/mod.rs | 43 ++-- client/rpc/src/eth/state.rs | 4 +- client/rpc/src/eth/submit.rs | 4 +- client/rpc/src/eth/transaction.rs | 4 +- client/rpc/src/lib.rs | 117 +++++++++- frame/evm/src/lib.rs | 1 + primitives/rpc/Cargo.toml | 2 + primitives/rpc/src/lib.rs | 38 ++++ template/node/src/rpc/eth.rs | 12 +- template/node/src/rpc/mod.rs | 21 +- ts-tests/contracts/StateOverrideTest.sol | 37 ++++ ts-tests/tests/test-state-override.ts | 252 ++++++++++++++++++++++ 22 files changed, 713 insertions(+), 68 deletions(-) create mode 100644 ts-tests/contracts/StateOverrideTest.sol create mode 100644 ts-tests/tests/test-state-override.ts diff --git a/Cargo.lock b/Cargo.lock index cbcfc664f5..06de807f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1970,6 +1970,7 @@ dependencies = [ "fc-rpc-core", "fc-storage", "fp-ethereum", + "fp-evm", "fp-rpc", "fp-storage", "futures", @@ -1978,6 +1979,7 @@ dependencies = [ "libsecp256k1", "log", "lru", + "pallet-evm", "parity-scale-codec", "prometheus", "rand 0.8.5", @@ -1998,6 +2000,8 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", + "sp-state-machine", + "sp-storage", "substrate-prometheus-endpoint", "substrate-test-runtime-client", "tempfile", @@ -2226,6 +2230,7 @@ dependencies = [ "sp-api", "sp-core", "sp-runtime", + "sp-state-machine", "sp-std", ] diff --git a/client/rpc-core/src/eth.rs b/client/rpc-core/src/eth.rs index 7c448f813a..407e495115 100644 --- a/client/rpc-core/src/eth.rs +++ b/client/rpc-core/src/eth.rs @@ -20,6 +20,7 @@ use ethereum_types::{H160, H256, H64, U256, U64}; use jsonrpsee::{core::RpcResult as Result, proc_macros::rpc}; +use std::collections::BTreeMap; use crate::types::*; @@ -151,7 +152,12 @@ pub trait EthApi { /// Call contract, returning the output data. #[method(name = "eth_call")] - fn call(&self, request: CallRequest, number: Option) -> Result; + fn call( + &self, + request: CallRequest, + number: Option, + state_overrides: Option>, + ) -> Result; /// Estimate gas needed for execution of given contract. #[method(name = "eth_estimateGas")] diff --git a/client/rpc-core/src/types/call_request.rs b/client/rpc-core/src/types/call_request.rs index 56d97a5431..0214ab6446 100644 --- a/client/rpc-core/src/types/call_request.rs +++ b/client/rpc-core/src/types/call_request.rs @@ -18,8 +18,9 @@ use crate::types::Bytes; use ethereum::AccessListItem; -use ethereum_types::{H160, U256}; +use ethereum_types::{H160, H256, U256}; use serde::Deserialize; +use std::collections::BTreeMap; /// Call request #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] @@ -50,3 +51,22 @@ pub struct CallRequest { #[serde(rename = "type")] pub transaction_type: Option, } + +// State override +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] +#[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>, +} diff --git a/client/rpc-core/src/types/mod.rs b/client/rpc-core/src/types/mod.rs index a9f8fa14a0..4384415428 100644 --- a/client/rpc-core/src/types/mod.rs +++ b/client/rpc-core/src/types/mod.rs @@ -40,7 +40,7 @@ pub use self::{ block::{Block, BlockTransactions, Header, Rich, RichBlock, RichHeader}, block_number::BlockNumber, bytes::Bytes, - call_request::CallRequest, + call_request::{CallRequest, CallStateOverride}, fee::{FeeHistory, FeeHistoryCache, FeeHistoryCacheItem, FeeHistoryCacheLimit}, filter::{ Filter, FilterAddress, FilterChanges, FilterPool, FilterPoolItem, FilterType, diff --git a/client/rpc/Cargo.toml b/client/rpc/Cargo.toml index 14b3c7f9a0..579537fa74 100644 --- a/client/rpc/Cargo.toml +++ b/client/rpc/Cargo.toml @@ -27,6 +27,7 @@ scale-codec = { package = "parity-scale-codec", workspace = true } tokio = { version = "1.24", features = ["sync"] } # Substrate +pallet-evm = { workspace = true } prometheus-endpoint = { workspace = true } sc-client-api = { workspace = true } sc-network = { workspace = true } @@ -42,11 +43,14 @@ sp-consensus = { workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } +sp-state-machine = { workspace = true } +sp-storage = { workspace = true } # Frontier fc-db = { workspace = true } fc-rpc-core = { workspace = true } fc-storage = { workspace = true } fp-ethereum = { workspace = true, features = ["default"] } +fp-evm = { workspace = true } fp-rpc = { workspace = true, features = ["default"] } fp-storage = { workspace = true, features = ["default"] } diff --git a/client/rpc/src/eth/block.rs b/client/rpc/src/eth/block.rs index 9f3e050ce2..aeb3be6be4 100644 --- a/client/rpc/src/eth/block.rs +++ b/client/rpc/src/eth/block.rs @@ -33,11 +33,11 @@ use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; use crate::{ - eth::{rich_block_build, Eth}, + eth::{rich_block_build, Eth, EthConfig}, frontier_backend_client, internal_err, }; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/eth/client.rs b/client/rpc/src/eth/client.rs index 82ba8948fa..114e2e2e9d 100644 --- a/client/rpc/src/eth/client.rs +++ b/client/rpc/src/eth/client.rs @@ -30,9 +30,12 @@ use sp_runtime::traits::{Block as BlockT, UniqueSaturatedInto}; use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; -use crate::{eth::Eth, internal_err}; +use crate::{ + eth::{Eth, EthConfig}, + internal_err, +}; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/eth/execute.rs b/client/rpc/src/eth/execute.rs index db73fe52f8..73c3ba1ac2 100644 --- a/client/rpc/src/eth/execute.rs +++ b/client/rpc/src/eth/execute.rs @@ -16,25 +16,28 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::sync::Arc; +use std::{collections::BTreeMap, sync::Arc}; -use ethereum_types::U256; +use ethereum_types::{H160, H256, U256}; use evm::{ExitError, ExitReason}; use jsonrpsee::core::RpcResult as Result; +use scale_codec::Encode; // Substrate use sc_client_api::backend::{Backend, StorageProvider}; use sc_network_common::ExHashT; use sc_transaction_pool::ChainApi; -use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_api::{ApiExt, CallApiAt, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::HeaderBackend; +use sp_io::hashing::{blake2_128, twox_128}; use sp_runtime::{traits::Block as BlockT, SaturatedConversion}; // Frontier use fc_rpc_core::types::*; -use fp_rpc::EthereumRuntimeRPCApi; +use fp_rpc::{EthereumRuntimeRPCApi, RuntimeStorageOverride}; +use fp_storage::{EVM_ACCOUNT_CODES, PALLET_EVM}; use crate::{ - eth::{pending_runtime_api, Eth}, + eth::{pending_runtime_api, Eth, EthConfig}, frontier_backend_client, internal_err, }; @@ -59,17 +62,21 @@ impl EstimateGasAdapter for () { } } -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, C::Api: BlockBuilderApi + EthereumRuntimeRPCApi, - C: HeaderBackend + StorageProvider + 'static, + C: HeaderBackend + CallApiAt + StorageProvider + 'static, BE: Backend + 'static, A: ChainApi + 'static, - EGA: EstimateGasAdapter, { - pub fn call(&self, request: CallRequest, number: Option) -> Result { + pub fn call( + &self, + request: CallRequest, + number: Option, + state_overrides: Option>, + ) -> Result { let CallRequest { from, to, @@ -201,26 +208,57 @@ where Ok(Bytes(info.value)) } else if api_version == 4 { // Post-london + access list support - let access_list = access_list.unwrap_or_default(); - let info = api - .call( - substrate_hash, - from.unwrap_or_default(), - to, - data, - value.unwrap_or_default(), - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - false, - Some( - access_list - .into_iter() - .map(|item| (item.address, item.storage_keys)) - .collect(), - ), - ) + let encoded_params = sp_api::Encode::encode(&( + &from.unwrap_or_default(), + &to, + &data, + &value.unwrap_or_default(), + &gas_limit, + &max_fee_per_gas, + &max_priority_fee_per_gas, + &nonce, + &false, + &Some( + access_list + .unwrap_or_default() + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect::)>>(), + ), + )); + + let overlayed_changes = self.create_overrides_overlay( + substrate_hash, + api_version, + state_overrides, + )?; + let storage_transaction_cache = std::cell::RefCell::< + sp_api::StorageTransactionCache, + >::default(); + let params = sp_api::CallApiAtParams { + at: substrate_hash, + function: "EthereumRuntimeRPCApi_call", + arguments: encoded_params, + overlayed_changes: &std::cell::RefCell::new(overlayed_changes), + storage_transaction_cache: &storage_transaction_cache, + context: sp_api::ExecutionContext::OffchainCall(None), + recorder: &None, + }; + let info = self + .client + .call_api_at(params) + .and_then(|r| { + std::result::Result::map_err( + as sp_api::Decode>::decode(&mut &r[..]), + |error| sp_api::ApiError::FailedToDecodeReturnValue { + function: "EthereumRuntimeRPCApi_call", + error, + }, + ) + }) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; @@ -324,7 +362,7 @@ where let substrate_hash = client.info().best_hash; // Adapt request for gas estimation. - let request = EGA::adapt_request(request); + let request = EC::EstimateGasAdapter::adapt_request(request); // For simple transfer to simple account, return MIN_GAS_PER_TX directly let is_simple_transfer = match &request.data { @@ -702,6 +740,89 @@ where Ok(highest) } } + + /// Given an address mapped `CallStateOverride`, creates `OverlayedChanges` to be used for + /// `CallApiAt` eth_call. + fn create_overrides_overlay( + &self, + block_hash: B::Hash, + api_version: u32, + state_overrides: Option>, + ) -> Result { + let mut overlayed_changes = sp_api::OverlayedChanges::default(); + if let Some(state_overrides) = state_overrides { + for (address, state_override) in state_overrides { + if EC::RuntimeStorageOverride::is_enabled() { + EC::RuntimeStorageOverride::set_overlayed_changes( + self.client.as_ref(), + &mut overlayed_changes, + block_hash, + api_version, + address, + state_override.balance, + state_override.nonce, + ); + } else if state_override.balance.is_some() || state_override.nonce.is_some() { + return Err(internal_err( + "state override unsupported for balance and nonce", + )); + } + + if let Some(code) = &state_override.code { + let mut key = [twox_128(PALLET_EVM), twox_128(EVM_ACCOUNT_CODES)] + .concat() + .to_vec(); + key.extend(blake2_128(address.as_bytes())); + key.extend(address.as_bytes()); + let encoded_code = code.clone().into_vec().encode(); + overlayed_changes.set_storage(key.clone(), Some(encoded_code)); + } + + let mut account_storage_key = [ + twox_128(PALLET_EVM), + twox_128(fp_storage::EVM_ACCOUNT_STORAGES), + ] + .concat() + .to_vec(); + account_storage_key.extend(blake2_128(address.as_bytes())); + account_storage_key.extend(address.as_bytes()); + + // Use `state` first. If `stateDiff` is also present, it resolves consistently + if let Some(state) = &state_override.state { + // clear all storage + if let Ok(all_keys) = self.client.storage_keys( + block_hash, + Some(&sp_storage::StorageKey(account_storage_key.clone())), + None, + ) { + for key in all_keys { + overlayed_changes.set_storage(key.0, None); + } + } + // set provided storage + for (k, v) in state { + let mut slot_key = account_storage_key.clone(); + slot_key.extend(blake2_128(k.as_bytes())); + slot_key.extend(k.as_bytes()); + + overlayed_changes.set_storage(slot_key, Some(v.as_bytes().to_owned())); + } + } + + if let Some(state_diff) = &state_override.state_diff { + for (k, v) in state_diff { + let mut slot_key = account_storage_key.clone(); + slot_key.extend(blake2_128(k.as_bytes())); + slot_key.extend(k.as_bytes()); + + overlayed_changes.set_storage(slot_key, Some(v.as_bytes().to_owned())); + } + } + } + } + + Ok(overlayed_changes) + } } pub fn error_on_execution_failure(reason: &ExitReason, data: &[u8]) -> Result<()> { diff --git a/client/rpc/src/eth/fee.rs b/client/rpc/src/eth/fee.rs index 1c7570ae06..ec5a7042e2 100644 --- a/client/rpc/src/eth/fee.rs +++ b/client/rpc/src/eth/fee.rs @@ -29,9 +29,12 @@ use sp_runtime::traits::{Block as BlockT, UniqueSaturatedInto}; use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; -use crate::{eth::Eth, frontier_backend_client, internal_err}; +use crate::{ + eth::{Eth, EthConfig}, + frontier_backend_client, internal_err, +}; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/eth/mining.rs b/client/rpc/src/eth/mining.rs index b86f05f9ed..190435f3e4 100644 --- a/client/rpc/src/eth/mining.rs +++ b/client/rpc/src/eth/mining.rs @@ -25,9 +25,11 @@ use sp_runtime::traits::Block as BlockT; // Frontier use fc_rpc_core::types::*; -use crate::eth::Eth; +use crate::eth::{Eth, EthConfig}; -impl Eth { +impl> + Eth +{ pub fn is_mining(&self) -> Result { Ok(self.is_authority) } diff --git a/client/rpc/src/eth/mod.rs b/client/rpc/src/eth/mod.rs index f89f70c398..4bbdca1bb7 100644 --- a/client/rpc/src/eth/mod.rs +++ b/client/rpc/src/eth/mod.rs @@ -39,7 +39,7 @@ use sc_network::NetworkService; use sc_network_common::ExHashT; use sc_transaction_pool::{ChainApi, Pool}; use sc_transaction_pool_api::{InPoolTransaction, TransactionPool}; -use sp_api::{Core, HeaderT, ProvideRuntimeApi}; +use sp_api::{CallApiAt, Core, HeaderT, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::HeaderBackend; use sp_core::hashing::keccak_256; @@ -48,7 +48,8 @@ use sp_runtime::traits::{Block as BlockT, UniqueSaturatedInto}; use fc_rpc_core::{types::*, EthApiServer}; use fc_storage::OverrideHandle; use fp_rpc::{ - ConvertTransaction, ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi, TransactionStatus, + ConvertTransaction, ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi, + RuntimeStorageOverride, TransactionStatus, }; use crate::{internal_err, public_key, signer::EthSigner}; @@ -59,8 +60,19 @@ pub use self::{ filter::EthFilter, }; +// Configuration trait for RPC configuration. +pub trait EthConfig: Send + Sync + 'static { + type EstimateGasAdapter: EstimateGasAdapter + Send + Sync; + type RuntimeStorageOverride: RuntimeStorageOverride; +} + +impl EthConfig for () { + type EstimateGasAdapter = (); + type RuntimeStorageOverride = (); +} + /// Eth API implementation. -pub struct Eth { +pub struct Eth> { pool: Arc

, graph: Arc>, client: Arc, @@ -76,7 +88,7 @@ pub struct Eth { /// When using eth_call/eth_estimateGas, the maximum allowed gas limit will be /// block.gas_limit * execute_gas_limit_multiplier execute_gas_limit_multiplier: u64, - _marker: PhantomData<(B, BE, EGA)>, + _marker: PhantomData<(B, BE, EC)>, } impl Eth { @@ -114,10 +126,10 @@ impl Eth Eth { - pub fn with_estimate_gas_adapter( - self, - ) -> Eth { +impl> + Eth +{ + pub fn replace_config>(self) -> Eth { let Self { client, pool, @@ -155,17 +167,17 @@ impl Eth EthApiServer for Eth +impl EthApiServer for Eth where B: BlockT, C: ProvideRuntimeApi, C::Api: BlockBuilderApi + ConvertTransactionRuntimeApi + EthereumRuntimeRPCApi, - C: HeaderBackend + StorageProvider + 'static, + C: HeaderBackend + CallApiAt + StorageProvider + 'static, BE: Backend + 'static, P: TransactionPool + 'static, CT: ConvertTransaction<::Extrinsic> + Send + Sync + 'static, A: ChainApi + 'static, - EGA: EstimateGasAdapter + Send + Sync + 'static, + EC: EthConfig + Send + Sync + 'static, { // ######################################################################## // Client @@ -288,8 +300,13 @@ where // Execute // ######################################################################## - fn call(&self, request: CallRequest, number: Option) -> Result { - self.call(request, number) + fn call( + &self, + request: CallRequest, + number: Option, + state_overrides: Option>, + ) -> Result { + self.call(request, number, state_overrides) } async fn estimate_gas( diff --git a/client/rpc/src/eth/state.rs b/client/rpc/src/eth/state.rs index c36c3308bf..d4b09b70be 100644 --- a/client/rpc/src/eth/state.rs +++ b/client/rpc/src/eth/state.rs @@ -33,11 +33,11 @@ use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; use crate::{ - eth::{pending_runtime_api, Eth}, + eth::{pending_runtime_api, Eth, EthConfig}, frontier_backend_client, internal_err, }; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/eth/submit.rs b/client/rpc/src/eth/submit.rs index 40ead2cd95..3d0ee2651b 100644 --- a/client/rpc/src/eth/submit.rs +++ b/client/rpc/src/eth/submit.rs @@ -35,11 +35,11 @@ use fc_rpc_core::types::*; use fp_rpc::{ConvertTransaction, ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi}; use crate::{ - eth::{format, Eth}, + eth::{format, Eth, EthConfig}, internal_err, }; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/eth/transaction.rs b/client/rpc/src/eth/transaction.rs index cd524fa375..71f1ef3534 100644 --- a/client/rpc/src/eth/transaction.rs +++ b/client/rpc/src/eth/transaction.rs @@ -35,11 +35,11 @@ use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; use crate::{ - eth::{transaction_build, Eth}, + eth::{transaction_build, Eth, EthConfig}, frontier_backend_client, internal_err, }; -impl Eth +impl> Eth where B: BlockT, C: ProvideRuntimeApi, diff --git a/client/rpc/src/lib.rs b/client/rpc/src/lib.rs index 7d4447cfbf..0bf618a3cb 100644 --- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -33,7 +33,7 @@ mod signer; mod web3; pub use self::{ - eth::{format, EstimateGasAdapter, Eth, EthBlockDataCacheTask, EthFilter, EthTask}, + eth::{format, EstimateGasAdapter, Eth, EthBlockDataCacheTask, EthConfig, EthFilter, EthTask}, eth_pubsub::{EthPubSub, EthereumSubIdProvider}, net::Net, signer::{EthDevSigner, EthSigner}, @@ -51,10 +51,13 @@ pub use fc_storage::{ pub mod frontier_backend_client { use super::internal_err; - use ethereum_types::H256; + use ethereum_types::{H160, H256, U256}; use jsonrpsee::core::RpcResult; + use scale_codec::Encode; // Substrate + use sc_client_api::{StorageKey, StorageProvider}; use sp_blockchain::HeaderBackend; + use sp_io::hashing::{blake2_128, twox_128}; use sp_runtime::{ generic::BlockId, traits::{Block as BlockT, UniqueSaturatedInto, Zero}, @@ -63,6 +66,116 @@ pub mod frontier_backend_client { use fc_rpc_core::types::BlockNumber; use fp_storage::EthereumStorageSchema; + /// Implements a default runtime storage override. + /// It assumes that the balances and nonces are stored in pallet `system.account`, and + /// have `nonce: Index` = `u32` for and `free: Balance` = `u128`. + /// Uses IdentityAddressMapping for the address. + pub struct SystemAccountId20StorageOverride(pub std::marker::PhantomData<(B, C, BE)>); + impl fp_rpc::RuntimeStorageOverride for SystemAccountId20StorageOverride + where + B: BlockT, + C: StorageProvider + Send + Sync, + BE: sc_client_api::Backend + Send + Sync, + { + fn is_enabled() -> bool { + true + } + + fn set_overlayed_changes( + client: &C, + overlayed_changes: &mut sp_state_machine::OverlayedChanges, + block: B::Hash, + _version: u32, + address: H160, + balance: Option, + nonce: Option, + ) { + let mut key = [twox_128(b"System"), twox_128(b"Account")] + .concat() + .to_vec(); + let account_id = Self::into_account_id_bytes(address); + key.extend(blake2_128(&account_id)); + key.extend(&account_id); + + if let Ok(Some(item)) = client.storage(block, &StorageKey(key.clone())) { + let mut new_item = item.0; + + if let Some(nonce) = nonce { + new_item.splice(0..4, nonce.low_u32().encode()); + } + + if let Some(balance) = balance { + new_item.splice(16..32, balance.low_u128().encode()); + } + + overlayed_changes.set_storage(key, Some(new_item)); + } + } + + fn into_account_id_bytes(address: H160) -> Vec { + use pallet_evm::AddressMapping; + let address: H160 = pallet_evm::IdentityAddressMapping::into_account_id(address); + address.as_ref().to_owned() + } + } + + /// Implements a runtime storage override. + /// It assumes that the balances and nonces are stored in pallet `system.account`, and + /// have `nonce: Index` = `u32` for and `free: Balance` = `u128`. + /// USes HashedAddressMapping for the address. + pub struct SystemAccountId32StorageOverride(pub std::marker::PhantomData<(B, C, BE)>); + impl fp_rpc::RuntimeStorageOverride for SystemAccountId32StorageOverride + where + B: BlockT, + C: StorageProvider + Send + Sync, + BE: sc_client_api::Backend + Send + Sync, + { + fn is_enabled() -> bool { + true + } + + fn set_overlayed_changes( + client: &C, + overlayed_changes: &mut sp_state_machine::OverlayedChanges, + block: B::Hash, + _version: u32, + address: H160, + balance: Option, + nonce: Option, + ) { + let mut key = [twox_128(b"System"), twox_128(b"Account")] + .concat() + .to_vec(); + let account_id = Self::into_account_id_bytes(address); + key.extend(blake2_128(&account_id)); + key.extend(&account_id); + + if let Ok(Some(item)) = client.storage(block, &StorageKey(key.clone())) { + let mut new_item = item.0; + + if let Some(nonce) = nonce { + new_item.splice(0..4, nonce.low_u32().encode()); + } + + if let Some(balance) = balance { + new_item.splice(16..32, balance.low_u128().encode()); + } + + overlayed_changes.set_storage(key, Some(new_item)); + } + } + + fn into_account_id_bytes(address: H160) -> Vec { + use pallet_evm::AddressMapping; + use sp_core::crypto::ByteArray; + use sp_runtime::traits::BlakeTwo256; + + pallet_evm::HashedAddressMapping::::into_account_id(address) + .as_slice() + .to_owned() + } + } + pub fn native_block_id( client: &C, backend: &fc_db::Backend, diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index fdabd8b4ab..19791f5b59 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -623,6 +623,7 @@ where } } +/// Trait to be implemented for evm address mapping. pub trait AddressMapping { fn into_account_id(address: H160) -> A; } diff --git a/primitives/rpc/Cargo.toml b/primitives/rpc/Cargo.toml index dca0714c06..5850c5e9e3 100644 --- a/primitives/rpc/Cargo.toml +++ b/primitives/rpc/Cargo.toml @@ -19,6 +19,7 @@ scale-info = { workspace = true } sp-api = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } +sp-state-machine = { workspace = true } sp-std = { workspace = true } # Frontier fp-evm = { workspace = true } @@ -32,6 +33,7 @@ std = [ "scale-info/std", # Substrate "sp-api/std", + "sp-state-machine/std", "sp-core/std", "sp-runtime/std", "sp-std/std", diff --git a/primitives/rpc/src/lib.rs b/primitives/rpc/src/lib.rs index a02882106d..e00d447dd1 100644 --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -26,6 +26,7 @@ use scale_info::TypeInfo; // Substrate use sp_core::{H160, H256, U256}; use sp_runtime::{traits::Block as BlockT, Permill, RuntimeDebug}; +use sp_state_machine::OverlayedChanges; use sp_std::vec::Vec; #[derive(Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode, TypeInfo)] @@ -39,6 +40,43 @@ pub struct TransactionStatus { pub logs_bloom: Bloom, } +pub trait RuntimeStorageOverride: Send + Sync { + fn is_enabled() -> bool; + + fn set_overlayed_changes( + client: &C, + overlayed_changes: &mut OverlayedChanges, + block: B::Hash, + version: u32, + address: H160, + balance: Option, + nonce: Option, + ); + + fn into_account_id_bytes(address: H160) -> Vec; +} + +impl RuntimeStorageOverride for () { + fn is_enabled() -> bool { + false + } + + fn set_overlayed_changes( + _client: &C, + _overlayed_changes: &mut OverlayedChanges, + _block: B::Hash, + _version: u32, + _address: H160, + _balance: Option, + _nonce: Option, + ) { + } + + fn into_account_id_bytes(_address: H160) -> Vec { + Vec::default() + } +} + sp_api::decl_runtime_apis! { /// API necessary for Ethereum-compatibility layer. #[api_version(4)] diff --git a/template/node/src/rpc/eth.rs b/template/node/src/rpc/eth.rs index 5e61585541..32bc4ecd25 100644 --- a/template/node/src/rpc/eth.rs +++ b/template/node/src/rpc/eth.rs @@ -10,13 +10,13 @@ use sc_network::NetworkService; use sc_rpc::SubscriptionTaskExecutor; use sc_transaction_pool::{ChainApi, Pool}; use sc_transaction_pool_api::TransactionPool; -use sp_api::ProvideRuntimeApi; +use sp_api::{CallApiAt, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; use sp_runtime::traits::Block as BlockT; // Frontier use fc_db::Backend as FrontierBackend; -pub use fc_rpc::{EthBlockDataCacheTask, OverrideHandle, StorageOverride}; +pub use fc_rpc::{EthBlockDataCacheTask, EthConfig, OverrideHandle, StorageOverride}; pub use fc_rpc_core::types::{FeeHistoryCache, FeeHistoryCacheLimit, FilterPool}; pub use fc_storage::overrides_handle; use fp_rpc::{ConvertTransaction, ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi}; @@ -79,7 +79,7 @@ impl Clone for EthDeps } /// Instantiate Ethereum-compatible RPC extensions. -pub fn create_eth( +pub fn create_eth>( mut io: RpcModule<()>, deps: EthDeps, subscription_task_executor: SubscriptionTaskExecutor, @@ -89,7 +89,10 @@ where C: ProvideRuntimeApi, C::Api: BlockBuilderApi + EthereumRuntimeRPCApi + ConvertTransactionRuntimeApi, C: BlockchainEvents + 'static, - C: HeaderBackend + HeaderMetadata + StorageProvider, + C: HeaderBackend + + CallApiAt + + HeaderMetadata + + StorageProvider, BE: Backend + 'static, P: TransactionPool + 'static, A: ChainApi + 'static, @@ -139,6 +142,7 @@ where fee_history_cache_limit, execute_gas_limit_multiplier, ) + .replace_config::() .into_rpc(), )?; diff --git a/template/node/src/rpc/mod.rs b/template/node/src/rpc/mod.rs index c8ece5713b..c14c9370d3 100644 --- a/template/node/src/rpc/mod.rs +++ b/template/node/src/rpc/mod.rs @@ -14,7 +14,7 @@ use sc_rpc::SubscriptionTaskExecutor; use sc_rpc_api::DenyUnsafe; use sc_service::TransactionPool; use sc_transaction_pool::ChainApi; -use sp_api::ProvideRuntimeApi; +use sp_api::{CallApiAt, ProvideRuntimeApi}; use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; use sp_runtime::traits::Block as BlockT; // Runtime @@ -37,6 +37,18 @@ pub struct FullDeps { pub eth: EthDeps, } +pub struct DefaultEthConfig(std::marker::PhantomData<(C, BE)>); + +impl fc_rpc::EthConfig for DefaultEthConfig +where + C: sc_client_api::StorageProvider + Sync + Send + 'static, + BE: Backend + 'static, +{ + type EstimateGasAdapter = (); + type RuntimeStorageOverride = + fc_rpc::frontier_backend_client::SystemAccountId20StorageOverride; +} + /// Instantiate all Full RPC extensions. pub fn create_full( deps: FullDeps, @@ -51,6 +63,7 @@ where C::Api: fp_rpc::EthereumRuntimeRPCApi, C: BlockchainEvents + 'static, C: HeaderBackend + + CallApiAt + HeaderMetadata + StorageProvider, BE: Backend + 'static, @@ -83,7 +96,11 @@ where } // Ethereum compatibility RPCs - let io = create_eth::<_, _, _, _, _, _>(io, eth, subscription_task_executor)?; + let io = create_eth::<_, _, _, _, _, _, DefaultEthConfig>( + io, + eth, + subscription_task_executor, + )?; Ok(io) } diff --git a/ts-tests/contracts/StateOverrideTest.sol b/ts-tests/contracts/StateOverrideTest.sol new file mode 100644 index 0000000000..f6af68ffb3 --- /dev/null +++ b/ts-tests/contracts/StateOverrideTest.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Smart contract to help test state override +contract StateOverrideTest { + /// @notice The maxmium allowed value + uint256 public MAX_ALLOWED = 3; + uint256 public availableFunds; + mapping(address => mapping(address => uint256)) public allowance; + + address owner; + + constructor(uint256 intialAmount) payable { + owner = msg.sender; + availableFunds = intialAmount; + } + + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + function getSenderBalance() external view returns (uint256) { + return address(msg.sender).balance; + } + + function getAllowance(address from, address who) + external + view + returns (uint256) + { + return allowance[from][who]; + } + + function setAllowance(address who, uint256 amount) external { + allowance[address(msg.sender)][who] = amount; + } +} diff --git a/ts-tests/tests/test-state-override.ts b/ts-tests/tests/test-state-override.ts new file mode 100644 index 0000000000..7ec10b9117 --- /dev/null +++ b/ts-tests/tests/test-state-override.ts @@ -0,0 +1,252 @@ +import { expect, use as chaiUse } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { RLP } from "ethers/lib/utils"; +import Web3 from "web3"; +import { AbiItem } from "web3-utils"; + +import StateOverrideTest from "../build/contracts/StateOverrideTest.json"; +import Test from "../build/contracts/Test.json"; +import { + GENESIS_ACCOUNT, + GENESIS_ACCOUNT_PRIVATE_KEY, + FIRST_CONTRACT_ADDRESS, + GENESIS_ACCOUNT_BALANCE, +} from "./config"; +import { createAndFinalizeBlock, customRequest, describeWithFrontier } from "./util"; + +chaiUse(chaiAsPromised); + +describeWithFrontier("Frontier RPC (StateOverride)", (context) => { + const STATE_OVERRIDE_TEST_CONTRACT_BYTECODE = StateOverrideTest.bytecode; + const otherAddress = "0xd43593c715fdd31c61141abd04a99fd6822c8558"; + + let contract; + let contractAddress; + before("create the contract", async function () { + this.timeout(15000); + contract = new context.web3.eth.Contract(StateOverrideTest.abi as AbiItem[]); + const data = contract + .deploy({ + data: STATE_OVERRIDE_TEST_CONTRACT_BYTECODE, + arguments: [100], + }) + .encodeABI(); + const tx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + data, + value: Web3.utils.numberToHex(Web3.utils.toWei("1", "ether")), + gas: "0x100000", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + const { result } = await customRequest(context.web3, "eth_sendRawTransaction", [tx.rawTransaction]); + await createAndFinalizeBlock(context.web3); + const receipt = await context.web3.eth.getTransactionReceipt(result); + contractAddress = receipt.contractAddress; + + const txSetAllowance = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.setAllowance(otherAddress, 10).encodeABI(), + gas: "0x100000", + gasPrice: "0x3B9ACA00", + value: "0x0", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + await customRequest(context.web3, "eth_sendRawTransaction", [txSetAllowance.rawTransaction]); + await createAndFinalizeBlock(context.web3); + }); + + it("should have balance above 1000 tether without state override", async function () { + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.getSenderBalance().encodeABI(), + }, + ]); + const balance = Web3.utils.toBN( + Web3.utils.fromWei(Web3.utils.hexToNumberString(result), "tether").split(".")[0] + ); + expect(balance.gten(1000), "balance was not above 1000 tether").to.be.true; + }); + + it("should have a sender balance of 4500 with state override", async function () { + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.getSenderBalance().encodeABI(), + }, + "latest", + { + [GENESIS_ACCOUNT]: { + balance: Web3.utils.numberToHex(5000), + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("4500"); // 500 is consumed as gas + }); + + it("should have availableFunds of 100 without state override", async function () { + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.availableFunds().encodeABI(), + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("100"); + }); + + it("should have availableFunds of 500 with state override", async function () { + const availableFundsKey = Web3.utils.padLeft(Web3.utils.numberToHex(1), 64); // slot 1 + const newValue = Web3.utils.padLeft(Web3.utils.numberToHex(500), 64); + + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.availableFunds().encodeABI(), + }, + "latest", + { + [contractAddress]: { + stateDiff: { + [availableFundsKey]: newValue, + }, + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("500"); + }); + + it("should have allowance of 10 without state override", async function () { + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.allowance(GENESIS_ACCOUNT, otherAddress).encodeABI(), + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("10"); + }); + + it("should have allowance of 50 with state override", async function () { + const allowanceKey = Web3.utils.soliditySha3( + { + type: "uint256", + value: otherAddress, + }, + { + type: "uint256", + value: Web3.utils.soliditySha3( + { + type: "uint256", + value: GENESIS_ACCOUNT, + }, + { + type: "uint256", + value: "2", // slot 2 + } + ), + } + ); + const newValue = Web3.utils.padLeft(Web3.utils.numberToHex(50), 64); + + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.allowance(GENESIS_ACCOUNT, otherAddress).encodeABI(), + }, + "latest", + { + [contractAddress]: { + stateDiff: { + [allowanceKey]: newValue, + }, + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("50"); + }); + + it("should have allowance of 50 but availableFunds 0 with full state override", async function () { + const allowanceKey = Web3.utils.soliditySha3( + { + type: "uint256", + value: otherAddress, + }, + { + type: "uint256", + value: Web3.utils.soliditySha3( + { + type: "uint256", + value: GENESIS_ACCOUNT, + }, + { + type: "uint256", + value: "2", // slot 2 + } + ), + } + ); + const newValue = Web3.utils.padLeft(Web3.utils.numberToHex(50), 64); + + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.allowance(GENESIS_ACCOUNT, otherAddress).encodeABI(), + }, + "latest", + { + [contractAddress]: { + state: { + [allowanceKey]: newValue, + }, + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("50"); + + const { result: result2 } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.availableFunds().encodeABI(), + }, + "latest", + { + [contractAddress]: { + state: { + [allowanceKey]: newValue, + }, + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result2)).to.equal("0"); + }); + + it("should set MultiplyBy7 deployedBytecode with state override", async function () { + const testContract = new context.web3.eth.Contract(Test.abi as AbiItem[]); + const { result } = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await testContract.methods.multiply(5).encodeABI(), // multiplies by 7 + }, + "latest", + { + [contractAddress]: { + code: Test.deployedBytecode, + }, + }, + ]); + expect(Web3.utils.hexToNumberString(result)).to.equal("35"); + }); +});