diff --git a/Cargo.lock b/Cargo.lock index a383c7093d753..5a85f79de58d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,6 +1194,7 @@ dependencies = [ "anvil-core", "anvil-rpc", "anvil-server", + "assert_matches", "async-trait", "axum", "chrono", @@ -1213,6 +1214,7 @@ dependencies = [ "itertools 0.14.0", "jsonrpsee", "libsecp256k1", + "lru 0.16.0", "op-alloy-consensus 0.17.2", "op-alloy-rpc-types", "parity-scale-codec", @@ -10219,6 +10221,15 @@ dependencies = [ "hashbrown 0.15.4", ] +[[package]] +name = "lru" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "lru-cache" version = "0.1.2" diff --git a/crates/anvil-polkadot/Cargo.toml b/crates/anvil-polkadot/Cargo.toml index 55f3bfe3202cc..c0db820fdd58f 100644 --- a/crates/anvil-polkadot/Cargo.toml +++ b/crates/anvil-polkadot/Cargo.toml @@ -125,6 +125,7 @@ tempfile.workspace = true itertools.workspace = true rand_08.workspace = true eyre.workspace = true +lru = "0.16.0" # cli clap = { version = "4", features = [ @@ -146,6 +147,7 @@ alloy-provider = { workspace = true, features = ["txpool-api"] } alloy-pubsub.workspace = true foundry-test-utils.workspace = true tokio = { workspace = true, features = ["full"] } +assert_matches = "1.5.0" rstest = "0.26.1" op-alloy-rpc-types.workspace = true diff --git a/crates/anvil-polkadot/src/api_server/error.rs b/crates/anvil-polkadot/src/api_server/error.rs index 93366335e7044..200535b9a6fbb 100644 --- a/crates/anvil-polkadot/src/api_server/error.rs +++ b/crates/anvil-polkadot/src/api_server/error.rs @@ -1,6 +1,9 @@ -use crate::substrate_node::mining_engine::MiningError; +use crate::substrate_node::{mining_engine::MiningError, service::BackendError}; use anvil_rpc::{error::RpcError, response::ResponseResult}; -use polkadot_sdk::pallet_revive_eth_rpc::{EthRpcError, client::ClientError}; +use polkadot_sdk::{ + pallet_revive_eth_rpc::{EthRpcError, client::ClientError}, + sp_api, +}; use serde::Serialize; #[derive(Debug, thiserror::Error)] @@ -13,9 +16,18 @@ pub enum Error { InvalidParams(String), #[error("Revive call failed: {0}")] ReviveRpc(#[from] EthRpcError), + #[error(transparent)] + Backend(#[from] BackendError), + #[error("Nonce overflowing the substrate nonce type")] + NonceOverflow, + #[error(transparent)] + RuntimeApi(#[from] sp_api::ApiError), + #[error("Error encountered while creating a BalanceWithDust from a U256 balance")] + BalanceConversion, #[error("Internal error: {0}")] InternalError(String), } + impl From for Error { fn from(err: subxt::Error) -> Self { Self::ReviveRpc(EthRpcError::ClientError(err.into())) @@ -71,12 +83,7 @@ impl ToRpcResponseResult for Result { Error::InvalidParams(error_message) => { RpcError::invalid_params(error_message).into() } - Error::ReviveRpc(client_error) => { - RpcError::internal_error_with(format!("{client_error}")).into() - } - Error::InternalError(error_message) => { - RpcError::internal_error_with(error_message).into() - } + err => RpcError::internal_error_with(format!("{err}")).into(), }, } } diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index e8c1f811bf81f..f74d710e3b186 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -3,7 +3,7 @@ use crate::{ ApiRequest, error::{Error, Result, ToRpcResponseResult}, revive_conversions::{ - AlloyU256, ReviveAddress, ReviveBlockId, convert_to_generic_transaction, + AlloyU256, ReviveAddress, ReviveBlockId, SubstrateU256, convert_to_generic_transaction, }, }, logging::LoggingManager, @@ -12,35 +12,45 @@ use crate::{ impersonation::ImpersonationManager, in_mem_rpc::InMemoryRpcClient, mining_engine::MiningEngine, - service::{Backend, Service}, + service::{ + BackendWithOverlay, Client, Service, + storage::{ + AccountType, ByteCodeType, CodeInfo, ContractInfo, ReviveAccountInfo, + SystemAccountInfo, + }, + }, }, }; -use alloy_eips::{BlockId, BlockNumberOrTag}; +use alloy_eips::BlockId; use alloy_primitives::{Address, B256, U64, U256}; -use alloy_rpc_types::{TransactionRequest, anvil::MineOptions}; +use alloy_rpc_types::{anvil::MineOptions, request::TransactionRequest}; use alloy_serde::WithOtherFields; use anvil_core::eth::{EthRequest, Params as MineParams}; use anvil_rpc::response::ResponseResult; -use codec::Decode; +use codec::{Decode, Encode}; use futures::{StreamExt, channel::mpsc}; use polkadot_sdk::{ - pallet_revive::evm::{Account, Block, Bytes, ReceiptInfo, TransactionSigned}, + pallet_revive::{ + ReviveApi, + evm::{Account, Block, Bytes, ReceiptInfo, TransactionSigned}, + }, pallet_revive_eth_rpc::{ EthRpcError, ReceiptExtractor, ReceiptProvider, SubxtBlockInfoProvider, client::{Client as EthRpcClient, ClientError, SubscriptionType}, subxt_client::{self, SrcChainConfig}, }, - parachains_common::Hash, - sc_client_api::{Backend as _, HeaderBackend, StateBackend, TrieCacheContext}, + parachains_common::{AccountId, Hash, Nonce}, + sc_client_api::HeaderBackend, sp_api::{Metadata, ProvideRuntimeApi}, - sp_core::{self, keccak_256}, + sp_core::{self, H160, H256, Hasher, keccak_256}, + sp_runtime::traits::BlakeTwo256, }; use sqlx::sqlite::SqlitePoolOptions; use std::{sync::Arc, time::Duration}; +use substrate_runtime::Balance; use subxt::{ Metadata as SubxtMetadata, OnlineClient, backend::rpc::RpcClient, - client::RuntimeVersion as SubxtRuntimeVersion, config::substrate::H256, - ext::subxt_rpcs::LegacyRpcMethods, utils::H160, + client::RuntimeVersion as SubxtRuntimeVersion, ext::subxt_rpcs::LegacyRpcMethods, }; pub struct Wallet { @@ -49,8 +59,9 @@ pub struct Wallet { pub struct ApiServer { req_receiver: mpsc::Receiver, + backend: BackendWithOverlay, logging_manager: LoggingManager, - backend: Arc, + client: Arc, mining_engine: Arc, eth_rpc_client: EthRpcClient, wallet: Wallet, @@ -69,7 +80,11 @@ impl ApiServer { Ok(Self { req_receiver, logging_manager, - backend: substrate_service.backend.clone(), + backend: BackendWithOverlay::new( + substrate_service.backend.clone(), + substrate_service.storage_overrides.clone(), + ), + client: substrate_service.client.clone(), mining_engine: substrate_service.mining_engine.clone(), eth_rpc_client, impersonation_manager, @@ -94,6 +109,7 @@ impl ApiServer { pub async fn execute(&mut self, req: EthRequest) -> ResponseResult { let res = match req.clone() { EthRequest::SetLogging(enabled) => self.set_logging(enabled).to_rpc_result(), + //------- Mining--------- EthRequest::Mine(blocks, interval) => self.mine(blocks, interval).await.to_rpc_result(), EthRequest::SetIntervalMining(interval) => { @@ -102,8 +118,8 @@ impl ApiServer { EthRequest::GetIntervalMining(_) => self.get_interval_mining().to_rpc_result(), EthRequest::GetAutoMine(_) => self.get_auto_mine().to_rpc_result(), EthRequest::SetAutomine(enabled) => self.set_auto_mine(enabled).to_rpc_result(), - EthRequest::EvmMine(mine) => self.evm_mine(mine).await.to_rpc_result(), //------- TimeMachine--------- + EthRequest::EvmMine(mine) => self.evm_mine(mine).await.to_rpc_result(), EthRequest::EvmSetBlockTimeStampInterval(time) => { self.set_block_timestamp_interval(time).to_rpc_result() } @@ -115,6 +131,7 @@ impl ApiServer { } EthRequest::EvmIncreaseTime(time) => self.increase_time(time).to_rpc_result(), EthRequest::EvmSetTime(timestamp) => self.set_time(timestamp).to_rpc_result(), + //------- Eth RPCs--------- EthRequest::EthChainId(_) => self.eth_chain_id().to_rpc_result(), EthRequest::EthNetworkId(_) => self.network_id().to_rpc_result(), @@ -138,9 +155,27 @@ impl ApiServer { EthRequest::EthEstimateGas(call, block, _overrides, _block_overrides) => { self.estimate_gas(call, block).await.to_rpc_result() } + EthRequest::EthCall(call, block, _, _) => self.call(call, block).await.to_rpc_result(), EthRequest::EthSendTransaction(request) => { self.send_transaction(*request.clone()).await.to_rpc_result() } + EthRequest::EthGetTransactionCount(addr, block) => self + .get_transaction_count(ReviveAddress::from(addr).inner(), block) + .await + .map(|val| AlloyU256::from(val).inner()) + .to_rpc_result(), + + // ------- State injector --------- + EthRequest::SetBalance(address, value) => { + self.set_balance(address, value).to_rpc_result() + } + EthRequest::SetNonce(address, value) => self.set_nonce(address, value).to_rpc_result(), + EthRequest::SetCode(address, bytes) => self.set_code(address, bytes).to_rpc_result(), + EthRequest::SetStorageAt(address, key, value) => { + self.set_storage_at(address, key, value).to_rpc_result() + } + EthRequest::SetChainId(chain_id) => self.set_chain_id(chain_id).to_rpc_result(), + // -- Impersonation -- EthRequest::ImpersonateAccount(addr) => { self.impersonate_account(H160::from_slice(addr.0.as_ref())).to_rpc_result() @@ -266,14 +301,16 @@ impl ApiServer { // Eth RPCs fn eth_chain_id(&self) -> Result { node_info!("eth_chainId"); - let latest_block_hash = self.backend.blockchain().info().best_hash; - Ok(U256::from(self.chain_id(latest_block_hash)).to::()) + let latest_block = self.latest_block(); + + Ok(U256::from(self.chain_id(latest_block)).to::()) } fn network_id(&self) -> Result { node_info!("eth_networkId"); - let latest_block_hash = self.backend.blockchain().info().best_hash; - Ok(self.chain_id(latest_block_hash)) + let latest_block = self.latest_block(); + + Ok(self.chain_id(latest_block)) } fn net_listening(&self) -> Result { @@ -355,17 +392,31 @@ impl ApiServer { Ok(dry_run.eth_gas) } + async fn call( + &self, + request: WithOtherFields, + block: Option, + ) -> Result { + node_info!("eth_call"); + + let hash = self.get_block_hash_for_tag(block).await?; + let runtime_api = self.eth_rpc_client.runtime_api(hash); + let dry_run = + runtime_api.dry_run(convert_to_generic_transaction(request.into_inner())).await?; + + Ok(dry_run.data.into()) + } + async fn gas_price(&self) -> Result { node_info!("eth_gasPrice"); - let hash = - self.get_block_hash_for_tag(Some(BlockId::Number(BlockNumberOrTag::Latest))).await?; + let hash = self.latest_block(); let runtime_api = self.eth_rpc_client.runtime_api(hash); runtime_api.gas_price().await.map_err(Error::from) } - pub async fn get_transaction_count( + async fn get_transaction_count( &self, address: H160, block: Option, @@ -384,7 +435,7 @@ impl ApiServer { Ok(hash) } - pub(crate) async fn send_transaction( + async fn send_transaction( &self, transaction_req: WithOtherFields, ) -> Result { @@ -394,21 +445,23 @@ impl ApiServer { return Err(Error::ReviveRpc(EthRpcError::InvalidTransaction)); }; + let latest_block = self.latest_block(); + let latest_block_id = Some(BlockId::hash(B256::from_slice(latest_block.as_ref()))); + if transaction.gas.is_none() { - transaction.gas = Some(self.estimate_gas(transaction_req.clone(), None).await?); + transaction.gas = + Some(self.estimate_gas(transaction_req.clone(), latest_block_id).await?); } + if transaction.gas_price.is_none() { transaction.gas_price = Some(self.gas_price().await?); } if transaction.nonce.is_none() { - transaction.nonce = Some( - self.get_transaction_count(from, Some(BlockId::Number(BlockNumberOrTag::Latest))) - .await?, - ); + transaction.nonce = Some(self.get_transaction_count(from, latest_block_id).await?); } if transaction.chain_id.is_none() { transaction.chain_id = - Some(sp_core::U256::from_big_endian(&self.eth_chain_id()?.to_be_bytes::<8>())); + Some(sp_core::U256::from_big_endian(&self.chain_id(latest_block).to_be_bytes())); } let tx = transaction @@ -432,14 +485,166 @@ impl ApiServer { self.send_raw_transaction(Bytes(payload)).await } - // Helpers - async fn get_block_hash_for_tag(&self, block_id: Option) -> Result { - self.eth_rpc_client - .block_hash_for_tag(ReviveBlockId::from(block_id).inner()) - .await - .map_err(Error::from) + // State injector RPCs + fn set_chain_id(&self, chain_id: u64) -> Result<()> { + node_info!("anvil_setChainId"); + + let latest_block = self.latest_block(); + self.backend.inject_chain_id(latest_block, chain_id); + + Ok(()) } + fn set_balance(&self, address: Address, value: U256) -> Result<()> { + node_info!("anvil_setBalance"); + + let latest_block = self.latest_block(); + + let (new_balance, dust) = self.construct_balance_with_dust(latest_block, value)?; + + let account_id = self.get_account_id(latest_block, address)?; + self.set_frame_system_balance(latest_block, account_id, new_balance)?; + + let mut revive_account_info = self + .backend + .read_revive_account_info(latest_block, address)? + .unwrap_or(ReviveAccountInfo { account_type: AccountType::EOA, dust: 0 }); + + if revive_account_info.dust != dust { + revive_account_info.dust = dust; + + self.backend.inject_revive_account_info(latest_block, address, revive_account_info); + } + + Ok(()) + } + + fn set_nonce(&self, address: Address, value: U256) -> Result<()> { + node_info!("anvil_setNonce"); + + let latest_block = self.latest_block(); + + let account_id = self.get_account_id(latest_block, address)?; + + let mut account_info = self + .backend + .read_system_account_info(latest_block, account_id.clone())? + .unwrap_or_else(|| SystemAccountInfo { providers: 1, ..Default::default() }); + + account_info.nonce = value.try_into().map_err(|_| Error::NonceOverflow)?; + + self.backend.inject_system_account_info(latest_block, account_id, account_info); + + Ok(()) + } + + fn set_storage_at(&self, address: Address, key: U256, value: B256) -> Result<()> { + node_info!("anvil_setStorageAt"); + + let latest_block = self.latest_block(); + + let Some(ReviveAccountInfo { account_type: AccountType::Contract(contract_info), .. }) = + self.backend.read_revive_account_info(latest_block, address)? + else { + return Ok(()); + }; + + self.backend.inject_child_storage( + latest_block, + contract_info.trie_id.to_vec(), + key.to_be_bytes_vec(), + value.to_vec(), + ); + + Ok(()) + } + + fn set_code(&self, address: Address, bytes: alloy_primitives::Bytes) -> Result<()> { + node_info!("anvil_setCode"); + + let latest_block = self.latest_block(); + + let account_id = self.get_account_id(latest_block, address)?; + + let code_hash = H256(keccak_256(&bytes)); + + let maybe_system_account_info = + self.backend.read_system_account_info(latest_block, account_id.clone())?; + let nonce = maybe_system_account_info.as_ref().map(|info| info.nonce).unwrap_or_default(); + + if maybe_system_account_info.is_none() { + self.set_frame_system_balance( + latest_block, + account_id.clone(), + substrate_runtime::currency::DOLLARS, + )?; + } + + let mut old_code_info = None; + let revive_account_info = match self + .backend + .read_revive_account_info(latest_block, address)? + { + None => { + let contract_info = new_contract_info(&address, code_hash, nonce); + + ReviveAccountInfo { account_type: AccountType::Contract(contract_info), dust: 0 } + } + Some(ReviveAccountInfo { account_type: AccountType::EOA, dust }) => { + let contract_info = new_contract_info(&address, code_hash, nonce); + + ReviveAccountInfo { account_type: AccountType::Contract(contract_info), dust } + } + Some(ReviveAccountInfo { + account_type: AccountType::Contract(mut contract_info), + dust, + }) => { + if let Some(code_info) = + self.backend.read_code_info(latest_block, contract_info.code_hash)? + { + if code_info.refcount == 1 && contract_info.code_hash != code_hash { + // Remove the pristine code and code info for the old hash. + self.backend.inject_pristine_code( + latest_block, + contract_info.code_hash, + None, + ); + self.backend.inject_code_info(latest_block, contract_info.code_hash, None); + } + + old_code_info = Some(code_info); + } + + contract_info.code_hash = code_hash; + + ReviveAccountInfo { account_type: AccountType::Contract(contract_info), dust } + } + }; + + self.backend.inject_revive_account_info(latest_block, address, revive_account_info); + + let code_info = old_code_info + .map(|mut code_info| { + code_info.code_len = bytes.len() as u32; + code_info.code_type = ByteCodeType::Evm; + code_info + }) + .unwrap_or_else(|| CodeInfo { + owner: <[u8; 32]>::from(account_id).into(), + deposit: Default::default(), + refcount: 1, + code_len: bytes.len() as u32, + behaviour_version: 0, + code_type: ByteCodeType::Evm, + }); + + self.backend.inject_pristine_code(latest_block, code_hash, Some(bytes)); + self.backend.inject_code_info(latest_block, code_hash, Some(code_info)); + + Ok(()) + } + + // ---- Impersonation RPCs fn impersonate_account(&mut self, addr: H160) -> Result<()> { node_info!("anvil_impersonateAccount"); self.impersonation_manager.impersonate(addr); @@ -458,20 +663,81 @@ impl ApiServer { Ok(()) } + // ----- Helpers + async fn get_block_hash_for_tag(&self, block_id: Option) -> Result { + self.eth_rpc_client + .block_hash_for_tag(ReviveBlockId::from(block_id).inner()) + .await + .map_err(Error::from) + } + fn chain_id(&self, at: Hash) -> u64 { - let chain_id_key: [u8; 16] = [ - 149u8, 39u8, 54u8, 105u8, 39u8, 71u8, 142u8, 113u8, 13u8, 63u8, 127u8, 183u8, 124u8, - 109u8, 31u8, 137u8, - ]; - if let Ok(state_at) = self.backend.state_at(at, TrieCacheContext::Trusted) - && let Ok(Some(encoded_chain_id)) = state_at.storage(chain_id_key.as_slice()) - && let Ok(chain_id) = u64::decode(&mut &encoded_chain_id[..]) - { - return chain_id; + self.backend.read_chain_id(at).expect("Chain ID is populated on genesis") + } + + fn get_account_id(&self, block: Hash, address: Address) -> Result { + Ok(self.client.runtime_api().account_id(block, ReviveAddress::from(address).inner())?) + } + + fn construct_balance_with_dust(&self, block: Hash, value: U256) -> Result<(Balance, u32)> { + self.client + .runtime_api() + .new_balance_with_dust(block, SubstrateU256::from(value).inner())? + .map_err(|_| Error::BalanceConversion) + } + + fn latest_block(&self) -> H256 { + self.backend.blockchain().info().best_hash + } + + fn set_frame_system_balance( + &self, + latest_block: H256, + account_id: AccountId, + balance: Balance, + ) -> Result<()> { + let mut total_issuance = self.backend.read_total_issuance(latest_block)?; + + let mut system_account_info = self + .backend + .read_system_account_info(latest_block, account_id.clone())? + .unwrap_or_else(|| SystemAccountInfo { providers: 1, ..Default::default() }); + + if let Some(diff) = balance.checked_sub(system_account_info.data.free) { + total_issuance = total_issuance.saturating_add(diff); + } else { + total_issuance = total_issuance.saturating_sub(system_account_info.data.free - balance); } - // if the chain id is not found, use the default chain id - self.eth_rpc_client.chain_id() + system_account_info.data.free = balance; + + self.backend.inject_system_account_info(latest_block, account_id, system_account_info); + self.backend.inject_total_issuance(latest_block, total_issuance); + + Ok(()) + } +} + +fn new_contract_info(address: &Address, code_hash: H256, nonce: Nonce) -> ContractInfo { + let address = H160::from_slice(address.as_slice()); + + let trie_id = { + let buf = ("bcontract_trie_v1", address, nonce).using_encoded(BlakeTwo256::hash); + buf.as_ref() + .to_vec() + .try_into() + .expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed") + }; + + ContractInfo { + trie_id, + code_hash, + storage_bytes: 0, + storage_items: 0, + storage_byte_deposit: 0, + storage_item_deposit: 0, + storage_base_deposit: 0, + immutable_data_len: 0, } } diff --git a/crates/anvil-polkadot/src/config.rs b/crates/anvil-polkadot/src/config.rs index 917660e05b174..ffc3776117cc4 100644 --- a/crates/anvil-polkadot/src/config.rs +++ b/crates/anvil-polkadot/src/config.rs @@ -458,7 +458,7 @@ Genesis Number port: 0, no_mining: true, mixed_mining: false, - enable_tracing: true, + enable_tracing: false, silent: true, ..Default::default() } diff --git a/crates/anvil-polkadot/src/lib.rs b/crates/anvil-polkadot/src/lib.rs index 0a7d314fadd6a..cdd04a763ca37 100644 --- a/crates/anvil-polkadot/src/lib.rs +++ b/crates/anvil-polkadot/src/lib.rs @@ -119,10 +119,9 @@ pub async fn spawn( .map_err(sc_cli::Error::Service)?; // Spawn the other tasks. - let api_handle = - spawn_anvil_tasks(anvil_config, &substrate_service, &task_manager, logging_manager) - .await - .map_err(|err| sc_cli::Error::Application(err.into()))?; + let api_handle = spawn_anvil_tasks(anvil_config, &substrate_service, logging_manager) + .await + .map_err(|err| sc_cli::Error::Application(err.into()))?; Ok((substrate_service, task_manager, api_handle)) } @@ -130,7 +129,6 @@ pub async fn spawn( pub async fn spawn_anvil_tasks( anvil_config: AnvilNodeConfig, service: &Service, - task_manager: &TaskManager, logging_manager: LoggingManager, ) -> Result { // Spawn the api server. @@ -146,18 +144,15 @@ pub async fn spawn_anvil_tasks( // Spawn the server future on a new task. let srv = server::serve_on(tcp_listener, anvil_config.server_config.clone(), api_handle.clone()); - let spawn_handle = &service.spawn_handle; - spawn_handle.spawn( - "anvil", - "anvil-tcp", - async move { srv.await.expect("TCP server failure") }, - ); + service + .spawn_handle + .spawn("anvil", "anvil-tcp", async move { srv.await.expect("TCP server failure") }); } // If configured, spawn the IPC server. anvil_config .get_ipc_path() - .map(|path| try_spawn_ipc(task_manager, path, api_handle.clone())) + .map(|path| try_spawn_ipc(&service.spawn_handle, path, api_handle.clone())) .transpose()?; anvil_config.print()?; diff --git a/crates/anvil-polkadot/src/server/mod.rs b/crates/anvil-polkadot/src/server/mod.rs index f0addaa4dbe10..80861dca3fa56 100644 --- a/crates/anvil-polkadot/src/server/mod.rs +++ b/crates/anvil-polkadot/src/server/mod.rs @@ -4,7 +4,7 @@ use anvil_server::{ServerConfig, ipc::IpcEndpoint}; use axum::Router; use futures::StreamExt; use handler::{HttpEthRpcHandler, PubSubEthRpcHandler}; -use polkadot_sdk::sc_service::TaskManager; +use polkadot_sdk::sc_service::SpawnTaskHandle; use std::{io, net::SocketAddr, pin::pin}; use tokio::net::TcpListener; @@ -42,19 +42,9 @@ pub fn router(api_handle: ApiHandle, config: ServerConfig) -> Router { anvil_server::http_ws_router(config, http, ws) } -/// Launches an ipc server at the given path in a new task -/// -/// # Panics -/// -/// Panics if setting up the IPC connection was unsuccessful. -#[track_caller] -pub fn spawn_ipc(task_manager: &TaskManager, path: String, api_handle: ApiHandle) { - try_spawn_ipc(task_manager, path, api_handle).expect("failed to establish ipc connection") -} - /// Launches an ipc server at the given path in a new task. pub fn try_spawn_ipc( - task_manager: &TaskManager, + spawn_handle: &SpawnTaskHandle, path: String, api_handle: ApiHandle, ) -> io::Result<()> { @@ -62,7 +52,6 @@ pub fn try_spawn_ipc( let ipc = IpcEndpoint::new(handler, path); let incoming = ipc.incoming()?; - let spawn_handle = task_manager.spawn_handle(); let inner_spawn_handle = spawn_handle.clone(); spawn_handle.spawn("ipc", "anvil", async move { diff --git a/crates/anvil-polkadot/src/substrate_node/genesis.rs b/crates/anvil-polkadot/src/substrate_node/genesis.rs index 0373adb723c5f..18682cd440efc 100644 --- a/crates/anvil-polkadot/src/substrate_node/genesis.rs +++ b/crates/anvil-polkadot/src/substrate_node/genesis.rs @@ -1,6 +1,6 @@ //! Genesis settings -use crate::config::AnvilNodeConfig; +use crate::{config::AnvilNodeConfig, substrate_node::service::storage::well_known_keys}; use alloy_genesis::GenesisAccount; use alloy_primitives::Address; use codec::Encode; @@ -17,30 +17,6 @@ use polkadot_sdk::{ }; use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; -// Hex-encode key: 0x9527366927478e710d3f7fb77c6d1f89 -pub const CHAIN_ID_KEY: [u8; 16] = [ - 149u8, 39u8, 54u8, 105u8, 39u8, 71u8, 142u8, 113u8, 13u8, 63u8, 127u8, 183u8, 124u8, 109u8, - 31u8, 137u8, -]; - -// Hex-encode key: 0xf0c365c3cf59d671eb72da0e7a4113c49f1f0515f462cdcf84e0f1d6045dfcbb -// twox_128(b"Timestamp") ++ twox_128(b"Now") -// corresponds to `Timestamp::Now` storage item in pallet-timestamp -pub const TIMESTAMP_KEY: [u8; 32] = [ - 240u8, 195u8, 101u8, 195u8, 207u8, 89u8, 214u8, 113u8, 235u8, 114u8, 218u8, 14u8, 122u8, 65u8, - 19u8, 196u8, 159u8, 31u8, 5u8, 21u8, 244u8, 98u8, 205u8, 207u8, 132u8, 224u8, 241u8, 214u8, - 4u8, 93u8, 252u8, 187u8, -]; - -// Hex-encode key: 0x26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac -// twox_128(b"System") ++ twox_128(b"Number") -// corresponds to `System::Number` storage item in pallet-system -pub const BLOCK_NUMBER_KEY: [u8; 32] = [ - 38u8, 170u8, 57u8, 78u8, 234u8, 86u8, 48u8, 224u8, 124u8, 72u8, 174u8, 12u8, 149u8, 88u8, - 206u8, 247u8, 2u8, 165u8, 193u8, 177u8, 154u8, 183u8, 160u8, 79u8, 83u8, 108u8, 81u8, 154u8, - 202u8, 73u8, 131u8, 172u8, -]; - /// Genesis settings #[derive(Clone, Debug, Default)] pub struct GenesisConfig { @@ -81,9 +57,9 @@ impl<'a> From<&'a AnvilNodeConfig> for GenesisConfig { impl GenesisConfig { pub fn as_storage_key_value(&self) -> Vec<(Vec, Vec)> { let storage = vec![ - (CHAIN_ID_KEY.to_vec(), self.chain_id.encode()), - (TIMESTAMP_KEY.to_vec(), self.timestamp.encode()), - (BLOCK_NUMBER_KEY.to_vec(), self.number.encode()), + (well_known_keys::CHAIN_ID.to_vec(), self.chain_id.encode()), + (well_known_keys::TIMESTAMP.to_vec(), self.timestamp.encode()), + (well_known_keys::BLOCK_NUMBER_KEY.to_vec(), self.number.encode()), ]; // TODO: add other fields storage @@ -199,15 +175,16 @@ mod tests { GenesisConfig { number: block_number, timestamp, chain_id, ..Default::default() }; let genesis_storage = genesis_config.as_storage_key_value(); assert!( - genesis_storage.contains(&(BLOCK_NUMBER_KEY.to_vec(), block_number.encode())), + genesis_storage + .contains(&(well_known_keys::BLOCK_NUMBER_KEY.to_vec(), block_number.encode())), "Block number not found in genesis key-value storage" ); assert!( - genesis_storage.contains(&(TIMESTAMP_KEY.to_vec(), timestamp.encode())), + genesis_storage.contains(&(well_known_keys::TIMESTAMP.to_vec(), timestamp.encode())), "Timestamp not found in genesis key-value storage" ); assert!( - genesis_storage.contains(&(CHAIN_ID_KEY.to_vec(), chain_id.encode())), + genesis_storage.contains(&(well_known_keys::CHAIN_ID.to_vec(), chain_id.encode())), "Chain id not found in genesis key-value storage" ); } diff --git a/crates/anvil-polkadot/src/substrate_node/rpc.rs b/crates/anvil-polkadot/src/substrate_node/rpc.rs index 1ef605a25b795..4ef39aab4ee1a 100644 --- a/crates/anvil-polkadot/src/substrate_node/rpc.rs +++ b/crates/anvil-polkadot/src/substrate_node/rpc.rs @@ -1,6 +1,7 @@ -use crate::substrate_node::service::{Backend, FullClient}; +use crate::substrate_node::service::{Backend, Client, TransactionPoolHandle}; use jsonrpsee::RpcModule; use polkadot_sdk::{ + parachains_common::opaque::Block, sc_chain_spec::ChainSpec, sc_client_api::{Backend as ClientBackend, HeaderBackend}, sc_client_db::{BlocksPruning, PruningMode}, @@ -23,20 +24,18 @@ use polkadot_sdk::{ self, Configuration, RpcHandlers, SpawnTaskHandle, TaskManager, error::Error as ServiceError, }, - sc_transaction_pool::TransactionPoolWrapper, sc_utils::mpsc::{TracingUnboundedSender, tracing_unbounded}, sp_keystore::KeystorePtr, substrate_frame_rpc_system::SystemApiServer as _, }; use std::sync::Arc; -use substrate_runtime::OpaqueBlock as Block; pub fn spawn_rpc_server( genesis_number: u64, task_manager: &mut TaskManager, - client: Arc, + client: Arc, mut config: Configuration, - transaction_pool: Arc>, + transaction_pool: Arc, keystore: KeystorePtr, backend: Arc, ) -> Result { @@ -100,8 +99,8 @@ pub fn spawn_rpc_server( fn gen_rpc_module( genesis_number: u64, spawn_handle: SpawnTaskHandle, - client: Arc, - transaction_pool: Arc>, + client: Arc, + transaction_pool: Arc, keystore: KeystorePtr, system_rpc_tx: TracingUnboundedSender>, impl_name: String, diff --git a/crates/anvil-polkadot/src/substrate_node/service/backend.rs b/crates/anvil-polkadot/src/substrate_node/service/backend.rs new file mode 100644 index 0000000000000..d1a9983e4fafb --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/service/backend.rs @@ -0,0 +1,300 @@ +use crate::substrate_node::service::{ + Backend, + storage::{CodeInfo, ReviveAccountInfo, SystemAccountInfo, well_known_keys}, +}; +use alloy_primitives::{Address, Bytes}; +use codec::{Decode, Encode}; +use lru::LruCache; +use parking_lot::Mutex; +use polkadot_sdk::{ + parachains_common::{AccountId, Hash, opaque::Block}, + sc_client_api::{Backend as BackendT, StateBackend, TrieCacheContext}, + sc_client_db::BlockchainDb, + sp_blockchain, + sp_core::{H160, H256}, + sp_io::hashing::blake2_256, + sp_state_machine::{StorageKey, StorageValue}, +}; +use std::{collections::HashMap, num::NonZeroUsize, sync::Arc}; +use substrate_runtime::Balance; + +#[derive(Debug, thiserror::Error)] +pub enum BackendError { + #[error("Inner client error: {0}")] + Client(#[from] sp_blockchain::Error), + #[error("Could not find total issuance in the state")] + MissingTotalIssuance, + #[error("Could not find chain id in the state")] + MissingChainId, + #[error("Unable to decode total issuance {0}")] + DecodeTotalIssuance(codec::Error), + #[error("Unable to decode chain id {0}")] + DecodeChainId(codec::Error), + #[error("Unable to decode balance {0}")] + DecodeBalance(codec::Error), + #[error("Unable to decode revive account info {0}")] + DecodeReviveAccountInfo(codec::Error), + #[error("Unable to decode system account info {0}")] + DecodeSystemAccountInfo(codec::Error), + #[error("Unable to decode revive code info {0}")] + DecodeCodeInfo(codec::Error), +} + +type Result = std::result::Result; + +pub struct BackendWithOverlay { + backend: Arc, + overrides: Arc>, +} + +impl BackendWithOverlay { + pub fn new(backend: Arc, overrides: Arc>) -> Self { + Self { backend, overrides } + } + + pub fn blockchain(&self) -> &BlockchainDb { + self.backend.blockchain() + } + + pub fn read_chain_id(&self, hash: Hash) -> Result { + let key = well_known_keys::CHAIN_ID; + + let value = self.read_top_state(hash, key.to_vec())?.ok_or(BackendError::MissingChainId)?; + u64::decode(&mut &value[..]).map_err(BackendError::DecodeChainId) + } + + pub fn read_total_issuance(&self, hash: Hash) -> Result { + let key = well_known_keys::TOTAL_ISSUANCE; + + let value = + self.read_top_state(hash, key.to_vec())?.ok_or(BackendError::MissingTotalIssuance)?; + Balance::decode(&mut &value[..]).map_err(BackendError::DecodeTotalIssuance) + } + + pub fn read_system_account_info( + &self, + hash: Hash, + account_id: AccountId, + ) -> Result> { + let key = well_known_keys::system_account_info(account_id); + + self.read_top_state(hash, key)? + .map(|value| { + SystemAccountInfo::decode(&mut &value[..]) + .map_err(BackendError::DecodeSystemAccountInfo) + }) + .transpose() + } + + pub fn read_revive_account_info( + &self, + hash: Hash, + address: Address, + ) -> Result> { + let key = well_known_keys::revive_account_info(H160::from_slice(address.as_slice())); + + self.read_top_state(hash, key)? + .map(|value| { + ReviveAccountInfo::decode(&mut &value[..]) + .map_err(BackendError::DecodeReviveAccountInfo) + }) + .transpose() + } + + pub fn read_code_info(&self, hash: Hash, code_hash: H256) -> Result> { + let key = well_known_keys::code_info(code_hash); + + self.read_top_state(hash, key)? + .map(|value| CodeInfo::decode(&mut &value[..]).map_err(BackendError::DecodeCodeInfo)) + .transpose() + } + + pub fn inject_system_account_info( + &self, + at: Hash, + account_id: AccountId, + value: SystemAccountInfo, + ) { + let mut overrides = self.overrides.lock(); + overrides.set_system_account_info(at, account_id, value); + } + + pub fn inject_chain_id(&self, at: Hash, chain_id: u64) { + let mut overrides = self.overrides.lock(); + overrides.set_chain_id(at, chain_id); + } + + pub fn inject_total_issuance(&self, at: Hash, value: Balance) { + let mut overrides = self.overrides.lock(); + overrides.set_total_issuance(at, value); + } + + pub fn inject_revive_account_info(&self, at: Hash, address: Address, info: ReviveAccountInfo) { + let mut overrides = self.overrides.lock(); + overrides.set_revive_account_info(at, address, info); + } + + pub fn inject_pristine_code(&self, at: Hash, code_hash: H256, code: Option) { + let mut overrides = self.overrides.lock(); + overrides.set_pristine_code(at, code_hash, code); + } + + pub fn inject_code_info(&self, at: Hash, code_hash: H256, code_info: Option) { + let mut overrides = self.overrides.lock(); + overrides.set_code_info(at, code_hash, code_info); + } + + pub fn inject_child_storage( + &self, + at: Hash, + child_key: StorageKey, + key: StorageKey, + value: StorageValue, + ) { + let mut overrides = self.overrides.lock(); + overrides.set_child_storage(at, child_key, key, value); + } + + fn read_top_state(&self, hash: Hash, key: StorageKey) -> Result> { + let maybe_overridden_val = { + let mut guard = self.overrides.lock(); + + guard.per_block.get(&hash).and_then(|overrides| overrides.top.get(&key).cloned()) + }; + + if let Some(overridden_val) = maybe_overridden_val { + return Ok(overridden_val); + } + + let state = self.backend.state_at(hash, TrieCacheContext::Trusted)?; + Ok(state + .storage(key.as_slice()) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?) + } +} + +pub type Storage = HashMap>; + +#[derive(Default, Clone)] +pub struct BlockOverrides { + pub top: Storage, + pub children: HashMap, +} + +pub struct StorageOverrides { + // We keep N most recently used block state overrides because we may later get RPC calls which + // query the state of past blocks. When state is mutated by the `set_*` RPCs, it gets committed + // to the state DB only in the next block. + per_block: LruCache, +} + +impl Default for StorageOverrides { + fn default() -> Self { + Self { per_block: LruCache::new(NonZeroUsize::new(10).expect("10 is greater than 0")) } + } +} + +impl StorageOverrides { + pub fn get(&mut self, block: &Hash) -> Option { + self.per_block.get(block).cloned() + } + + fn set_chain_id(&mut self, latest_block: Hash, id: u64) { + let mut changeset = BlockOverrides::default(); + changeset.top.insert(well_known_keys::CHAIN_ID.to_vec(), Some(id.encode())); + + self.add(latest_block, changeset); + } + + #[allow(unused)] + fn set_timestamp(&mut self, latest_block: Hash, timestamp: u64) { + let mut changeset = BlockOverrides::default(); + changeset.top.insert(well_known_keys::TIMESTAMP.to_vec(), Some(timestamp.encode())); + + self.add(latest_block, changeset); + } + + fn set_system_account_info( + &mut self, + latest_block: Hash, + account_id: AccountId, + info: SystemAccountInfo, + ) { + let mut changeset = BlockOverrides::default(); + changeset.top.insert(well_known_keys::system_account_info(account_id), Some(info.encode())); + + self.add(latest_block, changeset); + } + + fn set_total_issuance(&mut self, latest_block: Hash, value: Balance) { + let mut changeset = BlockOverrides::default(); + changeset.top.insert(well_known_keys::TOTAL_ISSUANCE.to_vec(), Some(value.encode())); + + self.add(latest_block, changeset); + } + + fn set_revive_account_info( + &mut self, + latest_block: Hash, + address: Address, + info: ReviveAccountInfo, + ) { + let mut changeset = BlockOverrides::default(); + changeset.top.insert( + well_known_keys::revive_account_info(H160::from_slice(address.as_slice())), + Some(info.encode()), + ); + + self.add(latest_block, changeset); + } + + fn set_pristine_code(&mut self, latest_block: Hash, code_hash: H256, code: Option) { + let mut changeset = BlockOverrides::default(); + + changeset + .top + .insert(well_known_keys::pristine_code(code_hash), code.map(|code| code.0.encode())); + + self.add(latest_block, changeset); + } + + fn set_code_info(&mut self, latest_block: Hash, code_hash: H256, code_info: Option) { + let mut changeset = BlockOverrides::default(); + + changeset.top.insert( + well_known_keys::code_info(code_hash), + code_info.map(|code_info| code_info.encode()), + ); + + self.add(latest_block, changeset); + } + + fn set_child_storage( + &mut self, + latest_block: Hash, + child_key: StorageKey, + key: StorageKey, + value: StorageValue, + ) { + let mut changeset = BlockOverrides::default(); + + let mut child_map = Storage::with_capacity(1); + child_map.insert(blake2_256(key.as_slice()).to_vec(), Some(value)); + + changeset.children.insert(child_key, child_map); + + self.add(latest_block, changeset); + } + + fn add(&mut self, latest_block: Hash, changeset: BlockOverrides) { + if let Some(per_block) = self.per_block.get_mut(&latest_block) { + per_block.top.extend(changeset.top); + + for (child_key, child_map) in changeset.children { + per_block.children.entry(child_key).or_default().extend(child_map); + } + } else { + self.per_block.put(latest_block, changeset); + } + } +} diff --git a/crates/anvil-polkadot/src/substrate_node/service/client.rs b/crates/anvil-polkadot/src/substrate_node/service/client.rs new file mode 100644 index 0000000000000..c334c4a17c79f --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/service/client.rs @@ -0,0 +1,87 @@ +use crate::substrate_node::{ + genesis::DevelopmentGenesisBlockBuilder, + service::{ + Backend, + backend::StorageOverrides, + executor::{Executor, WasmExecutor}, + }, +}; +use parking_lot::Mutex; +use polkadot_sdk::{ + parachains_common::opaque::Block, + sc_chain_spec::get_extension, + sc_client_api::{BadBlocks, ForkBlocks, execution_extensions::ExecutionExtensions}, + sc_service::{self, KeystoreContainer, LocalCallExecutor, TaskManager, new_db_backend}, + sp_keystore::KeystorePtr, +}; +use std::{collections::HashMap, sync::Arc}; +use substrate_runtime::RuntimeApi; + +pub type Client = sc_service::client::Client; + +pub fn new_client( + genesis_block_number: u64, + config: &sc_service::Configuration, + executor: WasmExecutor, + storage_overrides: Arc>, +) -> Result<(Arc, Arc, KeystorePtr, TaskManager), sc_service::error::Error> { + let backend = new_db_backend(config.db_config())?; + + let genesis_block_builder = DevelopmentGenesisBlockBuilder::new( + genesis_block_number, + config.chain_spec.as_storage_builder(), + !config.no_genesis(), + backend.clone(), + executor.clone(), + )?; + + let keystore_container = KeystoreContainer::new(&config.keystore)?; + + let task_manager = { + let registry = config.prometheus_config.as_ref().map(|cfg| &cfg.registry); + TaskManager::new(config.tokio_handle.clone(), registry)? + }; + + let chain_spec = &config.chain_spec; + let fork_blocks = + get_extension::>(chain_spec.extensions()).cloned().unwrap_or_default(); + + let bad_blocks = + get_extension::>(chain_spec.extensions()).cloned().unwrap_or_default(); + + let execution_extensions = ExecutionExtensions::new(None, Arc::new(executor.clone())); + + let wasm_runtime_substitutes = HashMap::new(); + + let client = { + let client_config = sc_service::ClientConfig { + offchain_worker_enabled: config.offchain_worker.enabled, + offchain_indexing_api: config.offchain_worker.indexing_enabled, + wasm_runtime_overrides: config.wasm_runtime_overrides.clone(), + no_genesis: config.no_genesis(), + wasm_runtime_substitutes, + enable_import_proof_recording: false, + }; + let inner_executor = LocalCallExecutor::new( + backend.clone(), + executor, + client_config.clone(), + execution_extensions, + )?; + let executor = Executor::new(inner_executor, storage_overrides, backend.clone()); + + Client::new( + backend.clone(), + executor, + Box::new(task_manager.spawn_handle()), + genesis_block_builder, + fork_blocks, + bad_blocks, + None, + None, + client_config, + )? + }; + + Ok((Arc::new(client), backend, keystore_container.keystore(), task_manager)) +} diff --git a/crates/anvil-polkadot/src/substrate_node/service/executor.rs b/crates/anvil-polkadot/src/substrate_node/service/executor.rs new file mode 100644 index 0000000000000..cadcb075ba450 --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/service/executor.rs @@ -0,0 +1,153 @@ +use crate::substrate_node::{ + host::{PublicKeyToHashOverride, SenderAddressRecoveryOverride}, + service::{Backend, backend::StorageOverrides}, +}; +use parking_lot::Mutex; +use polkadot_sdk::{ + parachains_common::{Hash, opaque::Block}, + sc_client_api::{Backend as _, CallExecutor, execution_extensions::ExecutionExtensions}, + sc_executor::{self, RuntimeVersion, RuntimeVersionOf}, + sc_service, + sp_api::{CallContext, ProofRecorder}, + sp_blockchain::{self, HeaderBackend}, + sp_core, sp_externalities, sp_io, + sp_runtime::{generic::BlockId, traits::HashingFor}, + sp_state_machine::{OverlayedChanges, StorageProof}, + sp_storage::ChildInfo, + sp_version, + sp_wasm_interface::ExtendedHostFunctions, +}; +use std::{cell::RefCell, sync::Arc}; + +/// Wasm executor which overrides the signature checking host functions for impersonation. +pub type WasmExecutor = sc_executor::WasmExecutor< + ExtendedHostFunctions< + ExtendedHostFunctions, + PublicKeyToHashOverride, + >, +>; + +type InnerLocalCallExecutor = sc_service::client::LocalCallExecutor; + +#[derive(Clone)] +/// State-injecting executor implementation. +pub struct Executor { + inner: InnerLocalCallExecutor, + storage_overrides: Arc>, + backend: Arc, +} + +impl Executor { + pub fn new( + inner: InnerLocalCallExecutor, + storage_overrides: Arc>, + backend: Arc, + ) -> Self { + Self { inner, storage_overrides, backend } + } + + fn apply_overrides(&self, hash: &Hash, overlay: &mut OverlayedChanges>) { + let overrides = { self.storage_overrides.lock().get(hash) }; + let Some(overrides) = overrides else { return }; + + for (key, val) in overrides.top { + overlay.set_storage(key, val); + } + + for (child_key, child_map) in overrides.children { + let child_info = ChildInfo::new_default_from_vec(child_key); + + for (key, val) in child_map { + overlay.set_child_storage(&child_info, key, val); + } + } + } +} + +impl CallExecutor for Executor { + type Error = >::Error; + + type Backend = Backend; + + fn execution_extensions(&self) -> &ExecutionExtensions { + self.inner.execution_extensions() + } + + fn call( + &self, + at_hash: Hash, + method: &str, + call_data: &[u8], + context: CallContext, + ) -> sp_blockchain::Result> { + let at_number = + self.backend.blockchain().expect_block_number_from_id(&BlockId::Hash(at_hash))?; + let extensions = self.execution_extensions().extensions(at_hash, at_number); + + let mut changes = OverlayedChanges::default(); + + self.apply_overrides(&at_hash, &mut changes); + + self.contextual_call( + at_hash, + method, + call_data, + &RefCell::new(changes), + &None, + context, + &RefCell::new(extensions), + ) + } + + fn contextual_call( + &self, + at_hash: Hash, + method: &str, + call_data: &[u8], + changes: &RefCell>>, + recorder: &Option>, + call_context: CallContext, + extensions: &RefCell, + ) -> Result, sp_blockchain::Error> { + let apply_overrides = (method == "Core_initialize_block" + && call_context == CallContext::Onchain) + || call_context == CallContext::Offchain; + + if apply_overrides { + self.apply_overrides(&at_hash, &mut changes.borrow_mut()); + } + + self.inner.contextual_call( + at_hash, + method, + call_data, + changes, + recorder, + call_context, + extensions, + ) + } + + fn runtime_version(&self, at_hash: Hash) -> sp_blockchain::Result { + CallExecutor::runtime_version(&self.inner, at_hash) + } + + fn prove_execution( + &self, + at_hash: Hash, + method: &str, + call_data: &[u8], + ) -> sp_blockchain::Result<(Vec, StorageProof)> { + self.inner.prove_execution(at_hash, method, call_data) + } +} + +impl RuntimeVersionOf for Executor { + fn runtime_version( + &self, + ext: &mut dyn sp_externalities::Externalities, + runtime_code: &sp_core::traits::RuntimeCode<'_>, + ) -> Result { + RuntimeVersionOf::runtime_version(&self.inner, ext, runtime_code) + } +} diff --git a/crates/anvil-polkadot/src/substrate_node/service.rs b/crates/anvil-polkadot/src/substrate_node/service/mod.rs similarity index 76% rename from crates/anvil-polkadot/src/substrate_node/service.rs rename to crates/anvil-polkadot/src/substrate_node/service/mod.rs index 19081aa7ce19d..2365d3f0cb060 100644 --- a/crates/anvil-polkadot/src/substrate_node/service.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/mod.rs @@ -1,46 +1,47 @@ use crate::{ AnvilNodeConfig, substrate_node::{ - genesis::DevelopmentGenesisBlockBuilder, - host::{PublicKeyToHashOverride, SenderAddressRecoveryOverride}, mining_engine::{MiningEngine, MiningMode, run_mining_engine}, rpc::spawn_rpc_server, }, }; use anvil::eth::backend::time::TimeManager; +use parking_lot::Mutex; use polkadot_sdk::{ - sc_basic_authorship, sc_consensus, sc_consensus_manual_seal, sc_executor, + parachains_common::opaque::Block, + sc_basic_authorship, sc_consensus, sc_consensus_manual_seal, sc_service::{ self, Configuration, RpcHandlers, SpawnTaskHandle, TaskManager, error::Error as ServiceError, }, - sc_transaction_pool, sp_io, sp_timestamp, - sp_wasm_interface::ExtendedHostFunctions, + sc_transaction_pool, sp_timestamp, }; use std::sync::Arc; -use substrate_runtime::{OpaqueBlock as Block, RuntimeApi}; use tokio_stream::wrappers::ReceiverStream; -type Executor = sc_executor::WasmExecutor< - ExtendedHostFunctions< - ExtendedHostFunctions, - PublicKeyToHashOverride, - >, ->; -pub type FullClient = sc_service::TFullClient; +pub use backend::{BackendError, BackendWithOverlay, StorageOverrides}; +pub use client::Client; + +mod backend; +mod client; +mod executor; +pub mod storage; pub type Backend = sc_service::TFullBackend; -pub type TransactionPoolHandle = sc_transaction_pool::TransactionPoolHandle; + +pub type TransactionPoolHandle = sc_transaction_pool::TransactionPoolHandle; + type SelectChain = sc_consensus::LongestChain; #[derive(Clone)] pub struct Service { pub spawn_handle: SpawnTaskHandle, - pub client: Arc, + pub client: Arc, pub backend: Arc, pub tx_pool: Arc, pub rpc_handlers: RpcHandlers, pub mining_engine: Arc, + pub storage_overrides: Arc>, pub genesis_block_number: u64, } @@ -49,28 +50,15 @@ pub fn new( anvil_config: &AnvilNodeConfig, config: Configuration, ) -> Result<(Service, TaskManager), ServiceError> { - let backend = sc_service::new_db_backend(config.db_config())?; + let storage_overrides = Arc::new(Mutex::new(StorageOverrides::default())); - let wasm_executor = sc_service::new_wasm_executor(&config.executor); - let genesis_block_builder = DevelopmentGenesisBlockBuilder::new( + let (client, backend, keystore, mut task_manager) = client::new_client( anvil_config.get_genesis_number(), - config.chain_spec.as_storage_builder(), - !config.no_genesis(), - backend.clone(), - wasm_executor.clone(), + &config, + sc_service::new_wasm_executor(&config.executor), + storage_overrides.clone(), )?; - let (client, backend, keystore_container, mut task_manager) = - sc_service::new_full_parts_with_genesis_builder( - &config, - None, - wasm_executor, - backend, - genesis_block_builder, - false, - )?; - let client = Arc::new(client); - let transaction_pool = Arc::from( sc_transaction_pool::Builder::new( task_manager.spawn_essential_handle(), @@ -115,7 +103,7 @@ pub fn new( client.clone(), config, transaction_pool.clone(), - keystore_container.keystore(), + keystore, backend.clone(), )?; @@ -166,6 +154,7 @@ pub fn new( tx_pool: transaction_pool, rpc_handlers, mining_engine, + storage_overrides, genesis_block_number: anvil_config.get_genesis_number(), }, task_manager, diff --git a/crates/anvil-polkadot/src/substrate_node/service/storage.rs b/crates/anvil-polkadot/src/substrate_node/service/storage.rs new file mode 100644 index 0000000000000..e7e5dae4c8944 --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/service/storage.rs @@ -0,0 +1,125 @@ +use codec::{Decode, Encode}; +use polkadot_sdk::{ + frame_support::BoundedVec, + frame_system, + pallet_balances::AccountData, + parachains_common::{AccountId, Nonce}, + sp_core::ConstU32, +}; +use substrate_runtime::{Balance, Hash}; + +#[derive(Encode, Decode)] +pub struct ReviveAccountInfo { + pub account_type: AccountType, + pub dust: u32, +} + +#[derive(Encode, Decode)] +pub enum AccountType { + Contract(ContractInfo), + EOA, +} + +#[derive(Encode, Decode)] +pub struct ContractInfo { + pub trie_id: BoundedVec>, + pub code_hash: Hash, + pub storage_bytes: u32, + pub storage_items: u32, + pub storage_byte_deposit: Balance, + pub storage_item_deposit: Balance, + pub storage_base_deposit: Balance, + pub immutable_data_len: u32, +} + +#[derive(Encode, Decode)] +pub struct CodeInfo { + pub owner: AccountId, + #[codec(compact)] + pub deposit: Balance, + #[codec(compact)] + pub refcount: u64, + pub code_len: u32, + pub code_type: ByteCodeType, + pub behaviour_version: u32, +} + +#[derive(Encode, Decode)] +pub enum ByteCodeType { + Pvm, + Evm, +} + +pub type SystemAccountInfo = frame_system::AccountInfo>; + +pub mod well_known_keys { + use codec::Encode; + use polkadot_sdk::{ + parachains_common::AccountId, + sp_core::{H160, H256, blake2_128, twox_128}, + }; + + // Hex-encoded key: 0xc2261276cc9d1f8598ea4b6a74b15c2f57c875e4cff74148e4628f264b974c80 + pub const TOTAL_ISSUANCE: [u8; 32] = [ + 194, 38, 18, 118, 204, 157, 31, 133, 152, 234, 75, 106, 116, 177, 92, 47, 87, 200, 117, + 228, 207, 247, 65, 72, 228, 98, 143, 38, 75, 151, 76, 128, + ]; + + // Hex-encoded key: 0x9527366927478e710d3f7fb77c6d1f89 + pub const CHAIN_ID: [u8; 16] = [ + 149u8, 39u8, 54u8, 105u8, 39u8, 71u8, 142u8, 113u8, 13u8, 63u8, 127u8, 183u8, 124u8, 109u8, + 31u8, 137u8, + ]; + + // Hex-encoded key: 0xf0c365c3cf59d671eb72da0e7a4113c49f1f0515f462cdcf84e0f1d6045dfcbb + pub const TIMESTAMP: [u8; 32] = [ + 240, 195, 101, 195, 207, 89, 214, 113, 235, 114, 218, 14, 122, 65, 19, 196, 159, 31, 5, 21, + 244, 98, 205, 207, 132, 224, 241, 214, 4, 93, 252, 187, + ]; + + // Hex-encoded key: 0x26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac + // twox_128(b"System") ++ twox_128(b"Number") + // corresponds to `System::Number` storage item in pallet-system + pub const BLOCK_NUMBER_KEY: [u8; 32] = [ + 38u8, 170u8, 57u8, 78u8, 234u8, 86u8, 48u8, 224u8, 124u8, 72u8, 174u8, 12u8, 149u8, 88u8, + 206u8, 247u8, 2u8, 165u8, 193u8, 177u8, 154u8, 183u8, 160u8, 79u8, 83u8, 108u8, 81u8, + 154u8, 202u8, 73u8, 131u8, 172u8, + ]; + + pub fn system_account_info(account_id: AccountId) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("System".as_bytes())); + key.extend_from_slice(&twox_128("Account".as_bytes())); + key.extend_from_slice(&blake2_128(account_id.as_ref())); + key.extend_from_slice(&account_id.encode()); + + key + } + + pub fn revive_account_info(address: H160) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("Revive".as_bytes())); + key.extend_from_slice(&twox_128("AccountInfoOf".as_bytes())); + key.extend_from_slice(&address.encode()); + + key + } + + pub fn pristine_code(code_hash: H256) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("Revive".as_bytes())); + key.extend_from_slice(&twox_128("PristineCode".as_bytes())); + key.extend_from_slice(&code_hash.encode()); + + key + } + + pub fn code_info(code_hash: H256) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("Revive".as_bytes())); + key.extend_from_slice(&twox_128("CodeInfoOf".as_bytes())); + key.extend_from_slice(&code_hash.encode()); + + key + } +} diff --git a/crates/anvil-polkadot/substrate-runtime/src/lib.rs b/crates/anvil-polkadot/substrate-runtime/src/lib.rs index 557d1a4947f3f..20d70e2478451 100644 --- a/crates/anvil-polkadot/substrate-runtime/src/lib.rs +++ b/crates/anvil-polkadot/substrate-runtime/src/lib.rs @@ -17,18 +17,16 @@ use frame_system::limits::BlockWeights; use pallet_revive::{AccountId32Mapper, evm::runtime::EthExtra}; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; use polkadot_sdk::{ - parachains_common::{ - AccountId, Balance, BlockNumber, Hash as CommonHash, Header, Nonce, Signature, - }, + parachains_common::{AccountId, BlockNumber, Hash as CommonHash, Header, Nonce, Signature}, polkadot_sdk_frame::{ deps::sp_genesis_builder, runtime::{apis, prelude::*}, }, *, }; -use sp_weights::{ConstantMultiplier, IdentityFee}; -pub use polkadot_sdk::polkadot_sdk_frame::runtime::types_common::OpaqueBlock; +pub use polkadot_sdk::parachains_common::Balance; +use sp_weights::{ConstantMultiplier, IdentityFee}; pub mod currency { use super::Balance; diff --git a/crates/anvil-polkadot/test-data/DoubleStorage.json b/crates/anvil-polkadot/test-data/DoubleStorage.json new file mode 100644 index 0000000000000..17bf8a6124306 --- /dev/null +++ b/crates/anvil-polkadot/test-data/DoubleStorage.json @@ -0,0 +1,50 @@ +{ + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "getValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "setValue", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "storedValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bin": "6080604052348015600e575f5ffd5b505f5f819055506101eb806100225f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063209652551461004357806355241077146100615780636d619daa1461007d575b5f5ffd5b61004b61009b565b60405161005891906100d5565b60405180910390f35b61007b6004803603810190610076919061011c565b6100a3565b005b6100856100b8565b60405161009291906100d5565b60405180910390f35b5f5f54905090565b6002816100b09190610174565b5f8190555050565b5f5481565b5f819050919050565b6100cf816100bd565b82525050565b5f6020820190506100e85f8301846100c6565b92915050565b5f5ffd5b6100fb816100bd565b8114610105575f5ffd5b50565b5f81359050610116816100f2565b92915050565b5f60208284031215610131576101306100ee565b5b5f61013e84828501610108565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61017e826100bd565b9150610189836100bd565b9250828202610197816100bd565b915082820484148315176101ae576101ad610147565b5b509291505056fea26469706673582212205795e90990177cdfc564956b5bace22103cef47ba16fae6bd2759bfab02ab1ed64736f6c634300081e0033", + "bin-runtime": "608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063209652551461004357806355241077146100615780636d619daa1461007d575b5f5ffd5b61004b61009b565b60405161005891906100d5565b60405180910390f35b61007b6004803603810190610076919061011c565b6100a3565b005b6100856100b8565b60405161009291906100d5565b60405180910390f35b5f5f54905090565b6002816100b09190610174565b5f8190555050565b5f5481565b5f819050919050565b6100cf816100bd565b82525050565b5f6020820190506100e85f8301846100c6565b92915050565b5f5ffd5b6100fb816100bd565b8114610105575f5ffd5b50565b5f81359050610116816100f2565b92915050565b5f60208284031215610131576101306100ee565b5b5f61013e84828501610108565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61017e826100bd565b9150610189836100bd565b9250828202610197816100bd565b915082820484148315176101ae576101ad610147565b5b509291505056fea26469706673582212205795e90990177cdfc564956b5bace22103cef47ba16fae6bd2759bfab02ab1ed64736f6c634300081e0033" +} \ No newline at end of file diff --git a/crates/anvil-polkadot/test-data/DoubleStorage.sol b/crates/anvil-polkadot/test-data/DoubleStorage.sol new file mode 100644 index 0000000000000..22173308adc40 --- /dev/null +++ b/crates/anvil-polkadot/test-data/DoubleStorage.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.8.0; + +contract DoubleStorage { + // Storage slot 0 + uint256 public storedValue; + + constructor() { + storedValue = 0; + } + + function setValue(uint256 _value) public { + storedValue = _value * 2; + } + + function getValue() public view returns (uint256) { + return storedValue; + } +} diff --git a/crates/anvil-polkadot/tests/it/abi.rs b/crates/anvil-polkadot/tests/it/abi.rs index 4d032d980d3b8..109a44cd3495d 100644 --- a/crates/anvil-polkadot/tests/it/abi.rs +++ b/crates/anvil-polkadot/tests/it/abi.rs @@ -5,3 +5,9 @@ sol!( SimpleStorage, "test-data/SimpleStorage.json" ); + +sol!( + #[derive(Debug)] + DoubleStorage, + "test-data/DoubleStorage.json" +); diff --git a/crates/anvil-polkadot/tests/it/impersonation.rs b/crates/anvil-polkadot/tests/it/impersonation.rs index 136ce1c5bfeda..a65b3b83b6cc5 100644 --- a/crates/anvil-polkadot/tests/it/impersonation.rs +++ b/crates/anvil-polkadot/tests/it/impersonation.rs @@ -111,10 +111,9 @@ async fn test_impersonate_account() { TransactionRequest::default().value(transfer_amount).from(dest_addr).to(alith_addr); let err = node.send_transaction(transaction.clone(), None).await.unwrap_err(); assert_eq!(err.code, ErrorCode::InternalError); - assert_eq!( - err.message, - format!("Account not found for address {}", dest_addr.to_string().to_lowercase()) - ); + assert!(err.message.contains( + format!("Account not found for address {}", dest_addr.to_string().to_lowercase()).as_str() + )); } #[tokio::test(flavor = "multi_thread")] @@ -180,8 +179,7 @@ async fn test_auto_impersonate(#[case] rpc_driven: bool) { TransactionRequest::default().value(transfer_amount).from(dest_addr).to(alith_addr); let err = node.send_transaction(transaction.clone(), None).await.unwrap_err(); assert_eq!(err.code, ErrorCode::InternalError); - assert_eq!( - err.message, - format!("Account not found for address {}", dest_addr.to_string().to_lowercase()) - ); + assert!(err.message.contains( + format!("Account not found for address {}", dest_addr.to_string().to_lowercase()).as_str() + )); } diff --git a/crates/anvil-polkadot/tests/it/main.rs b/crates/anvil-polkadot/tests/it/main.rs index 525c25124627b..1bb2ff3aa0ad4 100644 --- a/crates/anvil-polkadot/tests/it/main.rs +++ b/crates/anvil-polkadot/tests/it/main.rs @@ -3,5 +3,6 @@ mod genesis; mod impersonation; mod mining; mod standard_rpc; +mod state_injector; mod time_machine; mod utils; diff --git a/crates/anvil-polkadot/tests/it/mining.rs b/crates/anvil-polkadot/tests/it/mining.rs index 8ff348a1707f9..d264d32fc8c49 100644 --- a/crates/anvil-polkadot/tests/it/mining.rs +++ b/crates/anvil-polkadot/tests/it/mining.rs @@ -54,7 +54,7 @@ async fn test_invalid_mining() { async fn test_manual_mining_with_no_of_blocks() { let node_args = NodeArgs::parse_from(["anvil", "--no-mining", "--port", "0"]); let (mut anvil_node_config, substrate_node_config) = node_args.into_node_config().unwrap(); - anvil_node_config = anvil_node_config.set_silent(true); + anvil_node_config = anvil_node_config.set_silent(true).with_tracing(false); let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); assert!( !unwrap_response::(node.eth_rpc(EthRequest::GetAutoMine(())).await.unwrap()).unwrap() @@ -127,7 +127,7 @@ async fn test_manual_mining_with_interval() { async fn test_interval_mining() { let node_args = NodeArgs::parse_from(["anvil", "--block-time", "3", "--port", "0"]); let (mut anvil_node_config, substrate_node_config) = node_args.into_node_config().unwrap(); - anvil_node_config = anvil_node_config.set_silent(true); + anvil_node_config = anvil_node_config.set_silent(true).with_tracing(false); let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); // enable interval mining assert_eq!( diff --git a/crates/anvil-polkadot/tests/it/standard_rpc.rs b/crates/anvil-polkadot/tests/it/standard_rpc.rs index 64643948c8f43..e3f2d9d20484b 100644 --- a/crates/anvil-polkadot/tests/it/standard_rpc.rs +++ b/crates/anvil-polkadot/tests/it/standard_rpc.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use crate::utils::{BlockWaitTimeout, TestNode, unwrap_response}; use alloy_primitives::{Address, U256}; use alloy_rpc_types::TransactionRequest; @@ -12,6 +10,7 @@ use polkadot_sdk::pallet_revive::{ self, evm::{Account, HashesOrTransactionInfos}, }; +use std::time::Duration; use subxt::utils::H160; #[tokio::test(flavor = "multi_thread")] @@ -102,7 +101,7 @@ async fn test_send_transaction() { .send_transaction(transaction, Some(BlockWaitTimeout::new(1, Duration::from_secs(1)))) .await .unwrap(); - std::thread::sleep(Duration::from_millis(500)); + tokio::time::sleep(Duration::from_millis(500)).await; let transaction_receipt = node.get_transaction_receipt(tx_hash).await; assert_eq!(transaction_receipt.block_number, pallet_revive::U256::from(1)); @@ -147,7 +146,7 @@ async fn test_send_to_uninitialized() { .send_transaction(transaction, Some(BlockWaitTimeout::new(1, Duration::from_secs(1)))) .await .unwrap(); - std::thread::sleep(std::time::Duration::from_millis(500)); + tokio::time::sleep(Duration::from_millis(500)).await; let alith_final_balance = node.get_balance(alith.address(), None).await; assert_eq!(node.get_balance(charleth.address(), None).await, transfer_amount); @@ -160,7 +159,7 @@ async fn test_send_to_uninitialized() { .send_transaction(transaction, Some(BlockWaitTimeout::new(2, Duration::from_secs(1)))) .await .unwrap(); - std::thread::sleep(std::time::Duration::from_millis(500)); + tokio::time::sleep(Duration::from_millis(500)).await; let transaction_receipt = node.get_transaction_receipt(tx_hash).await; let alith_final_balance_2 = node.get_balance(alith.address(), None).await; let charlet_final_balance = node.get_balance(charleth.address(), None).await; diff --git a/crates/anvil-polkadot/tests/it/state_injector.rs b/crates/anvil-polkadot/tests/it/state_injector.rs new file mode 100644 index 0000000000000..e442cd8e277e1 --- /dev/null +++ b/crates/anvil-polkadot/tests/it/state_injector.rs @@ -0,0 +1,647 @@ +use crate::{ + abi::SimpleStorage, + utils::{ContractCode, TestNode, get_contract_code, unwrap_response}, +}; +use alloy_eips::BlockId; +use alloy_primitives::{Address, B256, Bytes, U256}; +use alloy_rpc_types::{TransactionInput, TransactionRequest}; +use alloy_sol_types::SolCall; +use anvil_core::eth::EthRequest; +use anvil_polkadot::{ + api_server::revive_conversions::{AlloyU256, ReviveAddress}, + config::{AnvilNodeConfig, SubstrateNodeConfig}, +}; +use anvil_rpc::error::{ErrorCode, RpcError}; +use assert_matches::assert_matches; +use polkadot_sdk::pallet_revive::{self, evm::Account}; +use std::time::Duration; +use subxt::utils::H160; + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_chain_id() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + assert_eq!(node.best_block_number().await, 0); + + let default_chain_id = 31337u64; + + assert_eq!( + unwrap_response::(node.eth_rpc(EthRequest::EthChainId(())).await.unwrap()).unwrap(), + "0x7a69", + ); + + assert_eq!( + unwrap_response::(node.eth_rpc(EthRequest::EthNetworkId(())).await.unwrap()).unwrap(), + default_chain_id, + ); + + unwrap_response::<()>(node.eth_rpc(EthRequest::SetChainId(10)).await.unwrap()).unwrap(); + + assert_eq!( + unwrap_response::(node.eth_rpc(EthRequest::EthChainId(())).await.unwrap()).unwrap(), + "0xa", + ); + + assert_eq!( + unwrap_response::(node.eth_rpc(EthRequest::EthNetworkId(())).await.unwrap()).unwrap(), + 10u64, + ); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + assert_eq!( + unwrap_response::(node.eth_rpc(EthRequest::EthChainId(())).await.unwrap()).unwrap(), + "0xa", + ); + + let fr = + Address::from(ReviveAddress::new(Account::from(subxt_signer::eth::dev::alith()).address())); + let to = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::baltathar()).address(), + )); + let mut tx = TransactionRequest::default().value(U256::from(100)).from(fr).to(to); + + // Set the old chain id, the transaction will be rejected. + tx.chain_id = Some(default_chain_id); + + assert_matches!( + node.send_transaction(tx, None).await, + Err(RpcError {code, message, ..}) => { + assert_eq!(code, ErrorCode::InternalError); + message.contains("Invalid Transaction") + } + ); + + let tx = TransactionRequest::default().value(U256::from(100)).from(fr).to(to); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let transaction_receipt = node.get_transaction_receipt(tx_hash).await; + + assert_eq!(transaction_receipt.block_number, pallet_revive::U256::from(2)); + assert_eq!(transaction_receipt.transaction_hash, tx_hash); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_nonce() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + assert_eq!(node.best_block_number().await, 0); + + let address = + Address::from(ReviveAddress::new(Account::from(subxt_signer::eth::dev::alith()).address())); + + assert_eq!(node.get_nonce(address).await, U256::from(0)); + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetNonce(address, U256::from(10))).await.unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_nonce(address).await, U256::from(10)); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(node.get_nonce(address).await, U256::from(10)); + + let to = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::baltathar()).address(), + )); + let tx = TransactionRequest::default().value(U256::from(100)).from(address).to(to); + + // Send a transaction with the wrong nonce, it will be invalid. + assert_matches!( + node.send_transaction(tx.clone().nonce(5), None).await, + Err(RpcError {code, message, ..}) => { + assert_eq!(code, ErrorCode::InternalError); + message.contains("Invalid Transaction") + } + ); + + // Send a transaction with the right nonce and mine a block. + let tx_hash = node.send_transaction(tx.clone().nonce(10), None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let transaction_receipt = node.get_transaction_receipt(tx_hash).await; + + assert_eq!(transaction_receipt.block_number, pallet_revive::U256::from(2)); + assert_eq!(transaction_receipt.transaction_hash, tx_hash); + + // Now set the nonce to a lower value. It should work. + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetNonce(address, U256::from(5))).await.unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_nonce(address).await, U256::from(5)); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let transaction_receipt = node.get_transaction_receipt(tx_hash).await; + + assert_eq!(transaction_receipt.block_number, pallet_revive::U256::from(3)); + assert_eq!(transaction_receipt.transaction_hash, tx_hash); + + assert_eq!(node.get_nonce(address).await, U256::from(6)); + + // Set nonce for a non-existant account. Should work. + let address = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::dorothy()).address(), + )); + + assert_eq!(node.get_nonce(address).await, U256::from(0)); + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetNonce(address, U256::from(1))).await.unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_nonce(address).await, U256::from(1)); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(node.get_nonce(address).await, U256::from(1)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_balance() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + assert_eq!(node.best_block_number().await, 0); + + let alith = Account::from(subxt_signer::eth::dev::alith()).address(); + + assert_eq!( + node.get_balance(alith, None).await, + // 1000 dollars + U256::from_str_radix("100000000000000000000000", 10).unwrap() + ); + + // Test decreasing the balance to 5 dollars. + let new_balance = U256::from(5e20); + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetBalance(Address::from(ReviveAddress::new(alith)), new_balance)) + .await + .unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_balance(alith, None).await, new_balance); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(node.get_balance(alith, None).await, new_balance); + + // Send 2 dollars to another account. We'll actually send 3, to cover for the existential + // deposit of 1 dollar. + let charleth = Account::from(subxt_signer::eth::dev::charleth()); + let tx = TransactionRequest::default() + .value(U256::from(2e20)) + .from(Address::from(ReviveAddress::new(alith))) + .to(Address::from(ReviveAddress::new(charleth.address()))); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let transaction_receipt = node.get_transaction_receipt(tx_hash).await; + + assert_eq!(transaction_receipt.block_number, pallet_revive::U256::from(2)); + assert_eq!(transaction_receipt.transaction_hash, tx_hash); + + let alith_new_balance = U256::from(2e20) + - AlloyU256::from(transaction_receipt.effective_gas_price * transaction_receipt.gas_used) + .inner(); + assert_eq!(node.get_balance(alith, None).await, alith_new_balance); + assert_eq!(node.get_balance(charleth.address(), None).await, U256::from(2e20)); + + // Now try sending more money than we have (5 dollars), should fail. + let tx = TransactionRequest::default() + .value(U256::from(5e20)) + .from(Address::from(ReviveAddress::new(alith))) + .to(Address::from(ReviveAddress::new(charleth.address()))); + + assert_matches!( + node.send_transaction(tx, None).await, + Err(RpcError {code, message, ..}) => { + assert_eq!(code, ErrorCode::InternalError); + message.contains("Invalid Transaction") + } + ); + assert_eq!(node.get_balance(alith, None).await, alith_new_balance); + assert_eq!(node.get_balance(charleth.address(), None).await, U256::from(2e20)); + + // Test increasing the balance of an existing account to 2000 dollars. + let baltathar = Account::from(subxt_signer::eth::dev::baltathar()).address(); + + assert_eq!( + node.get_balance(baltathar, None).await, + // 1000 dollars + U256::from_str_radix("100000000000000000000000", 10).unwrap() + ); + + let new_balance = U256::from_str_radix("200000000000000000000", 10).unwrap(); + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetBalance( + Address::from(ReviveAddress::new(baltathar)), + new_balance, + )) + .await + .unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_balance(baltathar, None).await, new_balance); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(node.get_balance(baltathar, None).await, new_balance); + + // Now test adding balance for a random new account. + let random_addr = H160::from_slice(Address::random().as_slice()); + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetBalance( + Address::from(ReviveAddress::new(random_addr)), + new_balance, + )) + .await + .unwrap(), + ) + .unwrap(); + + assert_eq!(node.get_balance(random_addr, None).await, new_balance); + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + assert_eq!(node.get_balance(random_addr, None).await, new_balance); +} + +#[tokio::test(flavor = "multi_thread")] +// Test setting the code of an existing contract. +async fn test_set_code_existing_contract() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + let alith = + Address::from(ReviveAddress::new(Account::from(subxt_signer::eth::dev::alith()).address())); + + let ContractCode { init: bytecode, runtime: Some(runtime_bytecode) } = + get_contract_code("SimpleStorage") + else { + panic!("Missing runtime bytecode") + }; + + let tx_hash = node + .deploy_contract(&bytecode, Account::from(subxt_signer::eth::dev::alith()).address(), None) + .await; + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let receipt = node.get_transaction_receipt(tx_hash).await; + let contract_address = Address::from(ReviveAddress::new(receipt.contract_address.unwrap())); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, Some(BlockId::number(0)))) + .await + .unwrap(), + ) + .unwrap(); + assert!(code.is_empty()); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, None)).await.unwrap(), + ) + .unwrap(); + + assert_eq!(code, Bytes::from(runtime_bytecode)); + + let set_value_data = SimpleStorage::setValueCall::new((U256::from(69),)).abi_encode(); + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(set_value_data.into())); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let _receipt = node.get_transaction_receipt(tx_hash).await; + + // assert new value + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(SimpleStorage::getValueCall.abi_encode().into())); + + let value = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + + let value = SimpleStorage::getValueCall::abi_decode_returns(&value.0).unwrap(); + + assert_eq!(value, U256::from(69)); + + let ContractCode { runtime: Some(runtime_bytecode), .. } = get_contract_code("DoubleStorage") + else { + panic!("Missing runtime bytecode") + }; + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetCode(contract_address, Bytes(runtime_bytecode.clone().into()))) + .await + .unwrap(), + ) + .unwrap(); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, None)).await.unwrap(), + ) + .unwrap(); + + assert_eq!(code, Bytes::from(runtime_bytecode)); + + let set_value_data = SimpleStorage::setValueCall::new((U256::from(10),)).abi_encode(); + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(set_value_data.into())); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let _receipt = node.get_transaction_receipt(tx_hash).await; + + // assert new value. The new code is doubling all values before setting them in storage. + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(SimpleStorage::getValueCall.abi_encode().into())); + + let value = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + + let value = SimpleStorage::getValueCall::abi_decode_returns(&value.0).unwrap(); + + assert_eq!(value, U256::from(20)); +} + +#[tokio::test(flavor = "multi_thread")] +// Test setting the code of a non-existing contract. +async fn test_set_code_new() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + let ContractCode { runtime: Some(runtime_bytecode), .. } = get_contract_code("SimpleStorage") + else { + panic!("Missing runtime bytecode") + }; + + let alith = + Address::from(ReviveAddress::new(Account::from(subxt_signer::eth::dev::alith()).address())); + let contract_address = + Address::from(ReviveAddress::new(H160::from_slice(Address::random().as_slice()))); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, Some(BlockId::number(0)))) + .await + .unwrap(), + ) + .unwrap(); + assert!(code.is_empty()); + + // Set empty code first. + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetCode(contract_address, Bytes(vec![].into()))).await.unwrap(), + ) + .unwrap(); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, Some(BlockId::number(0)))) + .await + .unwrap(), + ) + .unwrap(); + assert!(code.is_empty()); + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetCode(contract_address, Bytes(runtime_bytecode.clone().into()))) + .await + .unwrap(), + ) + .unwrap(); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, None)).await.unwrap(), + ) + .unwrap(); + + assert_eq!(code, Bytes::from(runtime_bytecode.clone())); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(code, Bytes::from(runtime_bytecode)); + + let set_value_data = SimpleStorage::setValueCall::new((U256::from(10),)).abi_encode(); + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(set_value_data.into())); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let _receipt = node.get_transaction_receipt(tx_hash).await; + + // assert new value + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(SimpleStorage::getValueCall.abi_encode().into())); + + let value = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + + let value = SimpleStorage::getValueCall::abi_decode_returns(&value.0).unwrap(); + + assert_eq!(value, U256::from(10)); +} + +#[tokio::test(flavor = "multi_thread")] +// Test setting the code of an existing, EOA account +async fn test_set_code_of_regular_account() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + let ContractCode { runtime: Some(runtime_bytecode), .. } = get_contract_code("SimpleStorage") + else { + panic!("Missing runtime bytecode") + }; + + let alith = + Address::from(ReviveAddress::new(Account::from(subxt_signer::eth::dev::alith()).address())); + let contract_address = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::baltathar()).address(), + )); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, Some(BlockId::number(0)))) + .await + .unwrap(), + ) + .unwrap(); + assert!(code.is_empty()); + + // Set empty code first. + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetCode(contract_address, Bytes(vec![].into()))).await.unwrap(), + ) + .unwrap(); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, Some(BlockId::number(0)))) + .await + .unwrap(), + ) + .unwrap(); + assert!(code.is_empty()); + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetCode(contract_address, Bytes(runtime_bytecode.clone().into()))) + .await + .unwrap(), + ) + .unwrap(); + + let code = unwrap_response::( + node.eth_rpc(EthRequest::EthGetCodeAt(contract_address, None)).await.unwrap(), + ) + .unwrap(); + + assert_eq!(code, Bytes::from(runtime_bytecode.clone())); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + assert_eq!(code, Bytes::from(runtime_bytecode)); + + let set_value_data = SimpleStorage::setValueCall::new((U256::from(10),)).abi_encode(); + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(set_value_data.into())); + + let tx_hash = node.send_transaction(tx, None).await.unwrap(); + + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + + let _receipt = node.get_transaction_receipt(tx_hash).await; + + // assert new value + let tx = TransactionRequest::default() + .from(alith) + .to(contract_address) + .input(TransactionInput::both(SimpleStorage::getValueCall.abi_encode().into())); + + let value = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + + let value = SimpleStorage::getValueCall::abi_decode_returns(&value.0).unwrap(); + + assert_eq!(value, U256::from(10)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_storage() { + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config.clone(), substrate_node_config).await.unwrap(); + let alith = Account::from(subxt_signer::eth::dev::alith()); + + let contract_code = get_contract_code("SimpleStorage"); + let tx_hash = node.deploy_contract(&contract_code.init, alith.address(), None).await; + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + let receipt = node.get_transaction_receipt(tx_hash).await; + let contract_address = receipt.contract_address.unwrap(); + + // Check the default value for slot 0. + let result = node + .eth_rpc(EthRequest::EthGetStorageAt( + Address::from(ReviveAddress::new(contract_address)), + U256::from(0), + None, + )) + .await + .unwrap(); + let hex_string = unwrap_response::(result).unwrap(); + let hex_value = hex_string.strip_prefix("0x").unwrap_or(&hex_string); + let stored_value = U256::from_str_radix(hex_value, 16).unwrap(); + assert_eq!(stored_value, 0); + + // Set a new value for the slot 0. + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetStorageAt( + Address::from(ReviveAddress::new(contract_address)), + U256::from(0), + B256::from(U256::from(511)), + )) + .await + .unwrap(), + ) + .unwrap(); + + // Check that the value was updated + let result = node + .eth_rpc(EthRequest::EthGetStorageAt( + Address::from(ReviveAddress::new(contract_address)), + U256::from(0), + None, + )) + .await + .unwrap(); + let hex_string = unwrap_response::(result).unwrap(); + let hex_value = hex_string.strip_prefix("0x").unwrap_or(&hex_string); + let stored_value = U256::from_str_radix(hex_value, 16).unwrap(); + assert_eq!(stored_value, 511); +} diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 33880df88e111..e4299b5e7964a 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -1,6 +1,6 @@ use alloy_eips::BlockId; -use alloy_primitives::{Address, B256, U256, hex}; -use alloy_rpc_types::TransactionRequest; +use alloy_primitives::{Address, B256, Bytes, U256, hex}; +use alloy_rpc_types::{TransactionInput, TransactionRequest}; use alloy_serde::WithOtherFields; use anvil_core::eth::EthRequest; use anvil_polkadot::{ @@ -10,7 +10,10 @@ use anvil_polkadot::{ logging::LoggingManager, opts::SubstrateCli, spawn, - substrate_node::{genesis::GenesisConfig, service::Service}, + substrate_node::{ + genesis::GenesisConfig, + service::{Service, storage::well_known_keys}, + }, }; use anvil_rpc::{error::RpcError, response::ResponseResult}; use codec::Decode; @@ -22,7 +25,8 @@ use polkadot_sdk::{ sc_cli::CliConfiguration, sc_client_api::{BlockBackend, BlockchainEvents}, sc_service::TaskManager, - sp_core::{H256, storage::StorageKey, twox_128}, + sp_core::H256, + sp_state_machine::StorageKey, }; use serde_json::{Value, json}; use std::{fmt::Debug, time::Duration}; @@ -99,36 +103,6 @@ impl TestNode { rx.await.map_err(|e| eyre::eyre!("ApiRequest receiver dropped: {}", e)) } - pub async fn substrate_rpc(&self, method: &str, params: Value) -> Result { - let rpc = &self.service.rpc_handlers; - - let request = json!({ - "jsonrpc": "2.0", - "method": method, - "params": params, - "id": 1 - }); - - let (response, _receiver) = rpc - .rpc_query(&request.to_string()) - .await - .wrap_err(format!("RPC call failed for method: {method}"))?; - - let response_value: Value = - serde_json::from_str(&response).wrap_err("Failed to parse RPC response")?; - - if let Some(error) = response_value.get("error") { - return Err(eyre::eyre!("RPC error: {}", error)); - } - - response_value - .get("result") - .cloned() - .ok_or_else(|| eyre::eyre!("No result in RPC response")) - } -} - -impl TestNode { pub async fn block_hash_by_number(&self, n: u32) -> eyre::Result { self.service .client @@ -137,13 +111,6 @@ impl TestNode { .ok_or_else(|| eyre::eyre!("no hash for block {}", n)) } - pub fn create_storage_key(pallet: &str, item: &str) -> StorageKey { - let mut key = Vec::new(); - key.extend_from_slice(&twox_128(pallet.as_bytes())); - key.extend_from_slice(&twox_128(item.as_bytes())); - StorageKey(key) - } - /// Execute an ethereum transaction. pub async fn send_transaction( &mut self, @@ -164,37 +131,20 @@ impl TestNode { Ok(tx_hash) } - pub async fn state_get_storage( - &self, - key: StorageKey, - at: Option, - ) -> Result> { - let key_hex = format!("0x{}", hex::encode(&key.0)); - let result = match at { - Some(hash) => self.substrate_rpc("state_getStorageAt", json!([key_hex, hash])).await?, - None => self.substrate_rpc("state_getStorage", json!([key_hex])).await?, - }; - Ok(result.as_str().map(|s| s.to_string())) - } - pub async fn get_decoded_timestamp(&self, at: Option) -> u64 { - let storage_key = Self::create_storage_key("Timestamp", "Now"); - let encoded_value = self.state_get_storage(storage_key, at).await.unwrap().unwrap(); + let encoded_value = + self.state_get_storage(well_known_keys::TIMESTAMP.to_vec(), at).await.unwrap().unwrap(); let bytes = hex::decode(encoded_value.strip_prefix("0x").unwrap_or(&encoded_value)).unwrap(); let mut input = &bytes[..]; Decode::decode(&mut input).unwrap() } - async fn wait_for_block_with_number(&self, n: u32) { - let mut import_stream = self.service.client.import_notification_stream(); - - while let Some(notification) = import_stream.next().await { - let block_number = *notification.header.number(); - if block_number >= n { - break; - } - } + pub async fn get_nonce(&mut self, address: Address) -> U256 { + unwrap_response::( + self.eth_rpc(EthRequest::EthGetTransactionCount(address, None)).await.unwrap(), + ) + .unwrap() } pub async fn best_block_number(&self) -> u32 { @@ -218,9 +168,7 @@ impl TestNode { .await .map_err(|e| e.into()) } -} -impl TestNode { pub async fn get_balance(&mut self, address: H160, block: Option) -> U256 { unwrap_response::( self.eth_rpc(EthRequest::EthGetBalance( @@ -253,6 +201,70 @@ impl TestNode { ) .unwrap() } + + pub async fn deploy_contract( + &mut self, + code: &[u8], + deployer: H160, + block_number: Option, + ) -> H256 { + let deploy_contract_tx = TransactionRequest::default() + .from(Address::from(ReviveAddress::new(deployer))) + .input(TransactionInput::both(Bytes::copy_from_slice(code))); + let block_wait = block_number.map(|bn| BlockWaitTimeout { + block_number: bn, + timeout: std::time::Duration::from_millis(1000), + }); + self.send_transaction(deploy_contract_tx, block_wait).await.unwrap() + } + + async fn wait_for_block_with_number(&self, n: u32) { + let mut import_stream = self.service.client.import_notification_stream(); + + while let Some(notification) = import_stream.next().await { + let block_number = *notification.header.number(); + if block_number >= n { + break; + } + } + } + + async fn substrate_rpc(&self, method: &str, params: Value) -> Result { + let rpc = &self.service.rpc_handlers; + + let request = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": 1 + }); + + let (response, _receiver) = rpc + .rpc_query(&request.to_string()) + .await + .wrap_err(format!("RPC call failed for method: {method}"))?; + + let response_value: Value = + serde_json::from_str(&response).wrap_err("Failed to parse RPC response")?; + + if let Some(error) = response_value.get("error") { + return Err(eyre::eyre!("RPC error: {}", error)); + } + + response_value + .get("result") + .cloned() + .ok_or_else(|| eyre::eyre!("No result in RPC response")) + } + + async fn state_get_storage(&self, key: StorageKey, at: Option) -> Result> { + let key_hex = format!("0x{}", hex::encode(&key)); + let result = match at { + Some(hash) => self.substrate_rpc("state_getStorageAt", json!([key_hex, hash])).await?, + None => self.substrate_rpc("state_getStorage", json!([key_hex])).await?, + }; + Ok(result.as_str().map(|s| s.to_string())) + } } pub fn assert_with_tolerance(actual: T, expected: T, tolerance: T, message: &str) @@ -278,13 +290,11 @@ where } } -#[allow(unused)] pub struct ContractCode { pub init: Vec, pub runtime: Option>, } -#[allow(unused)] pub fn get_contract_code(name: &str) -> ContractCode { let contract_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("test-data/{name}.json")); diff --git a/typos.toml b/typos.toml index d32eb792cfc84..aadf803bbe12b 100644 --- a/typos.toml +++ b/typos.toml @@ -22,3 +22,4 @@ strat = "strat" ba = "ba" "inherents" = "inherents" "Inherents" = "Inherents" +"Overlayed" = "Overlayed"