Skip to content

Commit

Permalink
implement eth_call state override (#1027)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nbaztec authored Apr 11, 2023
1 parent d13668c commit 2b09a67
Show file tree
Hide file tree
Showing 22 changed files with 713 additions and 68 deletions.
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion client/rpc-core/src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -151,7 +152,12 @@ pub trait EthApi {

/// Call contract, returning the output data.
#[method(name = "eth_call")]
fn call(&self, request: CallRequest, number: Option<BlockNumber>) -> Result<Bytes>;
fn call(
&self,
request: CallRequest,
number: Option<BlockNumber>,
state_overrides: Option<BTreeMap<H160, CallStateOverride>>,
) -> Result<Bytes>;

/// Estimate gas needed for execution of given contract.
#[method(name = "eth_estimateGas")]
Expand Down
22 changes: 21 additions & 1 deletion client/rpc-core/src/types/call_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -50,3 +51,22 @@ pub struct CallRequest {
#[serde(rename = "type")]
pub transaction_type: Option<U256>,
}

// 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<U256>,
/// Fake nonce to set for the account before executing the call.
pub nonce: Option<U256>,
/// Fake EVM bytecode to inject into the account before executing the call.
pub code: Option<Bytes>,
/// Fake key-value mapping to override all slots in the account storage before
/// executing the call.
pub state: Option<BTreeMap<H256, H256>>,
/// Fake key-value mapping to override individual slots in the account storage before
/// executing the call.
pub state_diff: Option<BTreeMap<H256, H256>>,
}
2 changes: 1 addition & 1 deletion client/rpc-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions client/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"] }

Expand Down
4 changes: 2 additions & 2 deletions client/rpc/src/eth/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EGA> Eth<B, C, P, CT, BE, H, A, EGA>
impl<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EC: EthConfig<B, C>> Eth<B, C, P, CT, BE, H, A, EC>
where
B: BlockT,
C: ProvideRuntimeApi<B>,
Expand Down
7 changes: 5 additions & 2 deletions client/rpc/src/eth/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EGA> Eth<B, C, P, CT, BE, H, A, EGA>
impl<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EC: EthConfig<B, C>> Eth<B, C, P, CT, BE, H, A, EC>
where
B: BlockT,
C: ProvideRuntimeApi<B>,
Expand Down
181 changes: 151 additions & 30 deletions client/rpc/src/eth/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,28 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

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,
};

Expand All @@ -59,17 +62,21 @@ impl EstimateGasAdapter for () {
}
}

impl<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EGA> Eth<B, C, P, CT, BE, H, A, EGA>
impl<B, C, P, CT, BE, H: ExHashT, A: ChainApi, EC: EthConfig<B, C>> Eth<B, C, P, CT, BE, H, A, EC>
where
B: BlockT,
C: ProvideRuntimeApi<B>,
C::Api: BlockBuilderApi<B> + EthereumRuntimeRPCApi<B>,
C: HeaderBackend<B> + StorageProvider<B, BE> + 'static,
C: HeaderBackend<B> + CallApiAt<B> + StorageProvider<B, BE> + 'static,
BE: Backend<B> + 'static,
A: ChainApi<Block = B> + 'static,
EGA: EstimateGasAdapter,
{
pub fn call(&self, request: CallRequest, number: Option<BlockNumber>) -> Result<Bytes> {
pub fn call(
&self,
request: CallRequest,
number: Option<BlockNumber>,
state_overrides: Option<BTreeMap<H160, CallStateOverride>>,
) -> Result<Bytes> {
let CallRequest {
from,
to,
Expand Down Expand Up @@ -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::<Vec<(sp_core::H160, Vec<H256>)>>(),
),
));

let overlayed_changes = self.create_overrides_overlay(
substrate_hash,
api_version,
state_overrides,
)?;
let storage_transaction_cache = std::cell::RefCell::<
sp_api::StorageTransactionCache<B, C::StateBackend>,
>::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(
<std::result::Result<
fp_evm::CallInfo,
sp_runtime::DispatchError,
> 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)))?;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<BTreeMap<H160, CallStateOverride>>,
) -> Result<sp_api::OverlayedChanges> {
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<()> {
Expand Down
Loading

0 comments on commit 2b09a67

Please sign in to comment.