diff --git a/Cargo.lock b/Cargo.lock index 1c18395c8d..de486313f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2709,6 +2709,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" @@ -4330,6 +4333,8 @@ dependencies = [ "jsonrpc-core 15.1.0", "jsonrpc-pubsub 15.1.0", "log", + "moonbeam-rpc-debug", + "moonbeam-rpc-primitives-debug", "moonbeam-rpc-primitives-txpool", "moonbeam-rpc-txpool", "moonbeam-runtime", @@ -4391,6 +4396,36 @@ dependencies = [ "trie-root 0.15.2", ] +[[package]] +name = "moonbeam-extensions-evm" +version = "0.1.0" +dependencies = [ + "ethereum-types", + "evm", + "evm-core", + "fp-evm", + "moonbeam-rpc-primitives-debug", + "pallet-evm", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "moonbeam-rpc-core-debug" +version = "0.1.0" +dependencies = [ + "ethereum-types", + "jsonrpc-core 15.1.0", + "jsonrpc-core-client 14.2.0", + "jsonrpc-derive 14.2.2", + "moonbeam-rpc-primitives-debug", + "serde", + "serde_json", + "sp-core", +] + [[package]] name = "moonbeam-rpc-core-txpool" version = "0.6.0" @@ -4405,6 +4440,44 @@ dependencies = [ "serde_json", ] +[[package]] +name = "moonbeam-rpc-debug" +version = "0.1.0" +dependencies = [ + "ethereum", + "ethereum-types", + "fc-consensus", + "fc-db", + "fp-rpc", + "jsonrpc-core 15.1.0", + "moonbeam-rpc-core-debug", + "moonbeam-rpc-primitives-debug", + "sc-client-api", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-core", + "sp-io", + "sp-runtime", +] + +[[package]] +name = "moonbeam-rpc-primitives-debug" +version = "0.1.0" +dependencies = [ + "ethereum", + "ethereum-types", + "hex", + "parity-scale-codec", + "serde", + "serde_json", + "sp-api", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "moonbeam-rpc-primitives-txpool" version = "0.6.0" @@ -4451,6 +4524,8 @@ dependencies = [ "frame-system-rpc-runtime-api", "hex-literal", "log", + "moonbeam-extensions-evm", + "moonbeam-rpc-primitives-debug", "moonbeam-rpc-primitives-txpool", "pallet-author-filter", "pallet-balances", @@ -4471,6 +4546,7 @@ dependencies = [ "parity-scale-codec", "precompiles", "serde", + "sha3 0.9.1", "sp-api", "sp-block-builder", "sp-core", diff --git a/client/rpc-core/debug/Cargo.toml b/client/rpc-core/debug/Cargo.toml new file mode 100644 index 0000000000..b9525c241d --- /dev/null +++ b/client/rpc-core/debug/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "moonbeam-rpc-core-debug" +version = '0.1.0' +authors = ['PureStake'] +edition = '2018' +homepage = 'https://moonbeam.network' +license = 'GPL-3.0-only' +repository = 'https://github.com/PureStake/moonbeam/' + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ethereum-types = "0.11.0" +jsonrpc-core = "15.0.0" +jsonrpc-core-client = "14.0.3" +jsonrpc-derive = "14.0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sp-core = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +moonbeam-rpc-primitives-debug = { path = "../../../primitives/rpc/debug" } +#evm = { version = "0.20.0", default-features = false, features = ["with-codec"] } diff --git a/client/rpc-core/debug/src/lib.rs b/client/rpc-core/debug/src/lib.rs new file mode 100644 index 0000000000..3810271d1e --- /dev/null +++ b/client/rpc-core/debug/src/lib.rs @@ -0,0 +1,52 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use ethereum_types::H256; +use jsonrpc_core::Result; +use jsonrpc_derive::rpc; +use serde::Deserialize; + +pub use crate::types::{StepLog, TraceExecutorResponse}; + +pub use rpc_impl_Debug::gen_server::Debug as DebugServer; + +pub mod types { + pub use moonbeam_rpc_primitives_debug::{StepLog, TraceExecutorResponse}; +} + +// TODO: Add support for additional params. +// - `disableStorage`, `disableMemory`, `disableStack`. +// - `timeout` should be ignored unless we find out a way for actually evaluating the tracer input. +#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceParams { + /// Javascript tracer (we just check if it's Blockscout tracer string) + pub disable_storage: Option, + pub disable_memory: Option, + pub disable_stack: Option, + pub tracer: Option, + pub timeout: Option, +} + +#[rpc(server)] +pub trait Debug { + #[rpc(name = "debug_traceTransaction")] + fn trace_transaction( + &self, + transaction_hash: H256, + params: Option, + ) -> Result; +} diff --git a/client/rpc-core/txpool/src/types/content.rs b/client/rpc-core/txpool/src/types/content.rs index 9a15476cfb..afda323343 100644 --- a/client/rpc-core/txpool/src/types/content.rs +++ b/client/rpc-core/txpool/src/types/content.rs @@ -45,6 +45,8 @@ pub struct Transaction { pub gas: U256, /// Data pub input: Bytes, + /// Transaction Index + pub transaction_index: Option, } fn block_hash_serialize(hash: &Option, serializer: S) -> Result @@ -77,6 +79,7 @@ impl GetT for Transaction { gas_price: txn.gas_price, gas: txn.gas_limit, input: Bytes(txn.input.clone()), + transaction_index: None, } } } diff --git a/client/rpc/debug/Cargo.toml b/client/rpc/debug/Cargo.toml new file mode 100644 index 0000000000..46a76e2147 --- /dev/null +++ b/client/rpc/debug/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "moonbeam-rpc-debug" +version = '0.1.0' +authors = ['PureStake'] +edition = '2018' +homepage = 'https://moonbeam.network' +license = 'GPL-3.0-only' +repository = 'https://github.com/PureStake/moonbeam/' + +[dependencies] +jsonrpc-core = "15.0.0" +ethereum = { version = "0.7.1", default-features = false, features = ["with-codec"] } +ethereum-types = "0.11.0" +sp-core = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-api = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sc-client-api = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-block-builder = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } + +moonbeam-rpc-core-debug = { path = "../../rpc-core/debug" } +moonbeam-rpc-primitives-debug = { path = "../../../primitives/rpc/debug" } +fc-consensus = { git = "https://github.com/purestake/frontier", branch = "notlesh-moonbeam-v0.7" } +fc-db = { git = "https://github.com/purestake/frontier", branch = "notlesh-moonbeam-v0.7" } +fp-rpc = { git = "https://github.com/purestake/frontier", branch = "notlesh-moonbeam-v0.7" } \ No newline at end of file diff --git a/client/rpc/debug/src/lib.rs b/client/rpc/debug/src/lib.rs new file mode 100644 index 0000000000..075a98b4ad --- /dev/null +++ b/client/rpc/debug/src/lib.rs @@ -0,0 +1,214 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +pub use moonbeam_rpc_core_debug::{ + Debug as DebugT, DebugServer, StepLog, TraceExecutorResponse, TraceParams, +}; + +use ethereum_types::{H128, H256}; +use fp_rpc::EthereumRuntimeRPCApi; +use jsonrpc_core::Result as RpcResult; +use jsonrpc_core::{Error as RpcError, ErrorCode}; +use moonbeam_rpc_primitives_debug::{DebugRuntimeApi, TraceType}; +use sc_client_api::backend::{AuxStore, Backend, StateBackend}; +use sp_api::{BlockId, HeaderT, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, +}; +use sp_runtime::traits::{BlakeTwo256, Block as BlockT}; +use std::{str::FromStr, sync::Arc}; + +pub fn internal_err(message: T) -> RpcError { + RpcError { + code: ErrorCode::InternalError, + message: message.to_string(), + data: None, + } +} + +pub struct Debug { + client: Arc, + backend: Arc, + frontier_backend: Arc>, +} + +impl Debug { + pub fn new(client: Arc, backend: Arc, frontier_backend: Arc>) -> Self { + Self { + client, + backend, + frontier_backend, + } + } +} + +impl Debug +where + BE: Backend + 'static, + BE::State: StateBackend, + BE::Blockchain: BlockchainBackend, + C: ProvideRuntimeApi + AuxStore, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, + B: BlockT + Send + Sync + 'static, + C::Api: BlockBuilder, + C::Api: DebugRuntimeApi, + C::Api: EthereumRuntimeRPCApi, +{ + // Asumes there is only one mapped canonical block in the AuxStore, otherwise something is wrong + fn load_hash(&self, hash: H256) -> RpcResult>> { + let hashes = self + .frontier_backend + .mapping() + .block_hashes(&hash) + .map_err(|err| internal_err(format!("fetch aux store failed: {:?}", err)))?; + let out: Vec = hashes + .into_iter() + .filter_map(|h| if self.is_canon(h) { Some(h) } else { None }) + .collect(); + + if out.len() == 1 { + return Ok(Some(BlockId::Hash(out[0]))); + } + Ok(None) + } + + fn is_canon(&self, target_hash: H256) -> bool { + if let Ok(Some(number)) = self.client.number(target_hash) { + if let Ok(Some(header)) = self.client.header(BlockId::Number(number)) { + return header.hash() == target_hash; + } + } + false + } + + fn load_transactions(&self, transaction_hash: H256) -> RpcResult> { + let transaction_metadata = self + .frontier_backend + .mapping() + .transaction_metadata(&transaction_hash) + .map_err(|err| internal_err(format!("fetch aux store failed: {:?}", err)))?; + + if transaction_metadata.len() == 1 { + Ok(Some(( + transaction_metadata[0].ethereum_block_hash, + transaction_metadata[0].ethereum_index, + ))) + } else if transaction_metadata.len() > 1 { + transaction_metadata + .iter() + .find(|meta| self.is_canon(meta.block_hash)) + .map_or( + Ok(Some(( + transaction_metadata[0].ethereum_block_hash, + transaction_metadata[0].ethereum_index, + ))), + |meta| Ok(Some((meta.ethereum_block_hash, meta.ethereum_index))), + ) + } else { + Ok(None) + } + } +} + +impl DebugT for Debug +where + BE: Backend + 'static, + BE::State: StateBackend, + BE::Blockchain: BlockchainBackend, + C: ProvideRuntimeApi + AuxStore, + C: HeaderMetadata + HeaderBackend, + C: Send + Sync + 'static, + B: BlockT + Send + Sync + 'static, + C::Api: BlockBuilder, + C::Api: DebugRuntimeApi, + C::Api: EthereumRuntimeRPCApi, +{ + fn trace_transaction( + &self, + transaction_hash: H256, + params: Option, + ) -> RpcResult { + let (hash, index) = match self + .load_transactions(transaction_hash) + .map_err(|err| internal_err(format!("{:?}", err)))? + { + Some((hash, index)) => (hash, index as usize), + None => return Err(internal_err("Transaction hash not found".to_string())), + }; + + let reference_id = match self + .load_hash(hash) + .map_err(|err| internal_err(format!("{:?}", err)))? + { + Some(hash) => hash, + _ => return Err(internal_err("Block hash not found".to_string())), + }; + + // Get ApiRef + let api = self.client.runtime_api(); + // Get Blockchain backend + let blockchain = self.backend.blockchain(); + // Get the header I want to work with. + let header = self.client.header(reference_id).unwrap().unwrap(); + // Get parent blockid. + let parent_block_id = BlockId::Hash(*header.parent_hash()); + + // Get the extrinsics. + let ext = blockchain.body(reference_id).unwrap().unwrap(); + + // Get the block that contains the requested transaction. + let reference_block = api + .current_block(&reference_id) + .map_err(|err| internal_err(format!("Runtime block call failed: {:?}", err)))?; + + // Set trace type + let trace_type = match params { + Some(TraceParams { + tracer: Some(tracer), + .. + }) => { + let hash: H128 = sp_io::hashing::twox_128(&tracer.as_bytes()).into(); + let blockscout_hash = H128::from_str("0x94d9f08796f91eb13a2e82a6066882f7").unwrap(); + if hash == blockscout_hash { + TraceType::Blockscout + } else { + return Err(internal_err(format!( + "javascript based tracing is not available (hash :{:?})", + hash + ))); + } + } + _ => TraceType::Raw, + }; + + // Get the actual ethereum transaction. + if let Some(block) = reference_block { + let transactions = block.transactions; + if let Some(transaction) = transactions.get(index) { + let res = api + .trace_transaction(&parent_block_id, ext, transaction, trace_type) + .map_err(|err| internal_err(format!("Runtime trace call failed: {:?}", err)))? + .unwrap(); + + return Ok(res); + } + } + + Err(internal_err("Runtime block call failed".to_string())) + } +} diff --git a/node/Cargo.toml b/node/Cargo.toml index 0ea8e4b19c..61e34d2e6c 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -35,6 +35,8 @@ tiny-bip39 = {version = "0.6", default-features = false} moonbeam-runtime = { path = "../runtime" } moonbeam-rpc-txpool = { path = "../client/rpc/txpool" } moonbeam-rpc-primitives-txpool = { path = "../primitives/rpc/txpool" } +moonbeam-rpc-debug = { path = "../client/rpc/debug" } +moonbeam-rpc-primitives-debug = { path = "../primitives/rpc/debug" } author-inherent = { path = "../pallets/author-inherent"} # Substrate dependencies diff --git a/node/src/cli.rs b/node/src/cli.rs index 71ef3a6915..4f51810298 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -131,6 +131,15 @@ pub struct RunCmd { /// Public identity for participating in staking and receiving rewards #[structopt(long, parse(try_from_str = parse_h160))] pub author_id: Option, + + /// Enable EVM tracing module on a non-authority node. + #[structopt( + long, + conflicts_with = "collator", + conflicts_with = "validator", + require_delimiter = true + )] + pub ethapi: Vec, } fn parse_h160(input: &str) -> Result { @@ -224,3 +233,26 @@ impl FromStr for Sealing { }) } } + +#[derive(Debug, PartialEq, Clone)] +pub enum EthApi { + Txpool, + Debug, +} + +impl FromStr for EthApi { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "txpool" => Self::Txpool, + "debug" => Self::Debug, + _ => { + return Err(format!( + "`{}` is not recognized as a supported Ethereum Api", + s + )) + } + }) + } +} diff --git a/node/src/command.rs b/node/src/command.rs index 0856c0a37c..a52cac849e 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -343,6 +343,7 @@ pub fn run() -> Result<()> { cli.run.sealing, author_id, collator, + cli.run.ethapi, ); } @@ -386,6 +387,7 @@ pub fn run() -> Result<()> { polkadot_config, id, collator, + cli.run.ethapi, ) .await .map(|r| r.0) diff --git a/node/src/rpc.rs b/node/src/rpc.rs index b885678308..fd163a1bd1 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -19,6 +19,7 @@ use std::collections::BTreeMap; use std::sync::Arc; +use crate::cli::EthApi as EthApiCmd; use ethereum::EthereumStorageSchema; use fc_rpc::{SchemaV1Override, StorageOverride}; use fc_rpc_core::types::{FilterPool, PendingTransactions}; @@ -35,12 +36,14 @@ use sc_rpc_api::DenyUnsafe; use sc_transaction_graph::{ChainApi, Pool}; use sp_api::ProvideRuntimeApi; use sp_block_builder::BlockBuilder; -use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, +}; use sp_runtime::traits::BlakeTwo256; use sp_transaction_pool::TransactionPool; /// Full client dependencies. -pub struct FullDeps { +pub struct FullDeps { /// The client instance to use. pub client: Arc, /// Transaction pool instance. @@ -57,20 +60,25 @@ pub struct FullDeps { pub pending_transactions: PendingTransactions, /// EthFilterApi pool. pub filter_pool: Option, + /// The list of optional RPC extensions. + pub ethapi_cmd: Vec, + /// Frontier Backend. + pub frontier_backend: Arc>, /// Backend. - pub backend: Arc>, + pub backend: Arc, /// Manual seal command sink pub command_sink: Option>>, } /// Instantiate all Full RPC extensions. pub fn create_full( - deps: FullDeps, + deps: FullDeps, subscription_task_executor: SubscriptionTaskExecutor, ) -> jsonrpc_core::IoHandler where BE: Backend + 'static, BE::State: StateBackend, + BE::Blockchain: BlockchainBackend, C: ProvideRuntimeApi + StorageProvider + AuxStore, C: BlockchainEvents, C: HeaderBackend + HeaderMetadata + 'static, @@ -80,6 +88,7 @@ where C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, A: ChainApi + 'static, C::Api: fp_rpc::EthereumRuntimeRPCApi, + C::Api: moonbeam_rpc_primitives_debug::DebugRuntimeApi, C::Api: moonbeam_rpc_primitives_txpool::TxPoolRuntimeApi, P: TransactionPool + 'static, { @@ -87,6 +96,7 @@ where EthApi, EthApiServer, EthFilterApi, EthFilterApiServer, EthPubSubApi, EthPubSubApiServer, HexEncodedIdProvider, NetApi, NetApiServer, Web3Api, Web3ApiServer, }; + use moonbeam_rpc_debug::{Debug, DebugServer}; use moonbeam_rpc_txpool::{TxPool, TxPoolServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApi}; use substrate_frame_rpc_system::{FullSystem, SystemApi}; @@ -101,8 +111,10 @@ where network, pending_transactions, filter_pool, + ethapi_cmd, command_sink, - backend: frontier_backend, + frontier_backend, + backend, } = deps; io.extend_with(SystemApi::to_delegate(FullSystem::new( @@ -133,7 +145,7 @@ where pending_transactions, signers, overrides, - frontier_backend, + frontier_backend.clone(), is_authority, ))); @@ -159,7 +171,16 @@ where Arc::new(subscription_task_executor), ), ))); - io.extend_with(TxPoolServer::to_delegate(TxPool::new(client, pool))); + if ethapi_cmd.contains(&EthApiCmd::Debug) { + io.extend_with(DebugServer::to_delegate(Debug::new( + client.clone(), + backend, + frontier_backend, + ))); + } + if ethapi_cmd.contains(&EthApiCmd::Txpool) { + io.extend_with(TxPoolServer::to_delegate(TxPool::new(client, pool))); + } if let Some(command_sink) = command_sink { io.extend_with( diff --git a/node/src/service.rs b/node/src/service.rs index b22f56c2a3..8e62f50d6d 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -22,6 +22,7 @@ //! Full Service: A complete parachain node including the pool, rpc, network, embedded relay chain //! Dev Service: A leaner service without the relay chain backing. +use crate::cli::EthApi as EthApiCmd; use crate::{cli::Sealing, inherents::build_inherent_data_providers}; use async_io::Timer; use cumulus_client_consensus_relay_chain::{ @@ -219,6 +220,7 @@ async fn start_node_impl( polkadot_config: Configuration, id: polkadot_primitives::v0::Id, collator: bool, + ethapi_cmd: Vec, _rpc_ext_builder: RB, ) -> sc_service::error::Result<(TaskManager, Arc)> where @@ -288,6 +290,8 @@ where let pending = pending_transactions.clone(); let filter_pool = filter_pool.clone(); let frontier_backend = frontier_backend.clone(); + let backend = backend.clone(); + let ethapi_cmd = ethapi_cmd.clone(); Box::new(move |deny_unsafe, _| { let deps = crate::rpc::FullDeps { @@ -299,8 +303,10 @@ where network: network.clone(), pending_transactions: pending.clone(), filter_pool: filter_pool.clone(), + ethapi_cmd: ethapi_cmd.clone(), command_sink: None, - backend: frontier_backend.clone(), + frontier_backend: frontier_backend.clone(), + backend: backend.clone(), }; crate::rpc::create_full(deps, subscription_task_executor.clone()) @@ -421,6 +427,7 @@ pub async fn start_node( polkadot_config: Configuration, id: polkadot_primitives::v0::Id, collator: bool, + ethapi_cmd: Vec, ) -> sc_service::error::Result<(TaskManager, Arc)> { start_node_impl( parachain_config, @@ -429,6 +436,7 @@ pub async fn start_node( polkadot_config, id, collator, + ethapi_cmd, |_| Default::default(), ) .await @@ -443,6 +451,7 @@ pub fn new_dev( // TODO I guess we should use substrate-cli's validator flag for this. // Resolve after https://github.com/paritytech/cumulus/pull/380 is reviewed. collator: bool, + ethapi_cmd: Vec, ) -> Result { let sc_service::PartialComponents { client, @@ -556,9 +565,11 @@ pub fn new_dev( let rpc_extensions_builder = { let client = client.clone(); let pool = transaction_pool.clone(); + let backend = backend.clone(); let network = network.clone(); let pending = pending_transactions.clone(); let filter_pool = filter_pool.clone(); + let ethapi_cmd = ethapi_cmd.clone(); let frontier_backend = frontier_backend.clone(); Box::new(move |deny_unsafe, _| { @@ -571,8 +582,10 @@ pub fn new_dev( network: network.clone(), pending_transactions: pending.clone(), filter_pool: filter_pool.clone(), + ethapi_cmd: ethapi_cmd.clone(), command_sink: command_sink.clone(), - backend: frontier_backend.clone(), + frontier_backend: frontier_backend.clone(), + backend: backend.clone(), }; crate::rpc::create_full(deps, subscription_task_executor.clone()) }) diff --git a/primitives/rpc/debug/Cargo.toml b/primitives/rpc/debug/Cargo.toml new file mode 100644 index 0000000000..a47d233883 --- /dev/null +++ b/primitives/rpc/debug/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "moonbeam-rpc-primitives-debug" +version = '0.1.0' +authors = ['PureStake'] +edition = '2018' +homepage = 'https://moonbeam.network' +license = 'GPL-3.0-only' +repository = 'https://github.com/PureStake/moonbeam/' + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +ethereum = { version = "0.7.1", default-features = false, features = ["with-codec"] } +ethereum-types = { version = "0.11.0", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1", default-features = false } +sp-api = { git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1", default-features = false } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } +hex = { version = "0.4", features = ["serde"], optional = true} +#moonbeam-rpc-core-debug = { path = "../../../client/rpc-core/debug" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-api/std", + "sp-runtime/std", + "sp-io/std", + "sp-std/std", + "sp-core/std", + "ethereum/std", + "ethereum-types/std", + "serde", + "serde_json", + "hex" +] \ No newline at end of file diff --git a/primitives/rpc/debug/src/lib.rs b/primitives/rpc/debug/src/lib.rs new file mode 100644 index 0000000000..48a3908e0c --- /dev/null +++ b/primitives/rpc/debug/src/lib.rs @@ -0,0 +1,221 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use ethereum::Transaction; +use ethereum_types::{H160, H256, U256}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +#[cfg(feature = "std")] +use serde::{ser::SerializeSeq, Serialize, Serializer}; + +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase", untagged))] +pub enum TraceExecutorResponse { + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + Raw { + gas: U256, + #[cfg_attr(feature = "std", serde(with = "hex"))] + return_value: Vec, + step_logs: Vec, + }, + Blockscout(Vec), +} + +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct StepLog { + #[cfg_attr(feature = "std", serde(serialize_with = "u256_serialize"))] + pub depth: U256, + + #[cfg_attr(feature = "std", serde(serialize_with = "u256_serialize"))] + pub gas: U256, + + #[cfg_attr(feature = "std", serde(serialize_with = "u256_serialize"))] + pub gas_cost: U256, + + #[cfg_attr(feature = "std", serde(serialize_with = "seq_h256_serialize"))] + pub memory: Vec, + + #[cfg_attr(feature = "std", serde(serialize_with = "opcode_serialize"))] + pub op: Vec, + + #[cfg_attr(feature = "std", serde(serialize_with = "u256_serialize"))] + pub pc: U256, + + #[cfg_attr(feature = "std", serde(serialize_with = "seq_h256_serialize"))] + pub stack: Vec, + + pub storage: BTreeMap, +} + +pub mod blockscout { + use super::*; + + #[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] + #[cfg_attr(feature = "std", derive(Serialize))] + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + pub enum CallResult { + Output(#[cfg_attr(feature = "std", serde(serialize_with = "bytes_0x_serialize"))] Vec), + // field "error" + Error(#[cfg_attr(feature = "std", serde(serialize_with = "string_serialize"))] Vec), + } + + #[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode)] + #[cfg_attr(feature = "std", derive(Serialize))] + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + pub enum CallType { + Call, + CallCode, + DelegateCall, + StaticCall, + } + + #[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] + #[cfg_attr(feature = "std", derive(Serialize))] + #[cfg_attr(feature = "std", serde(rename_all = "camelCase", untagged))] + pub enum CreateResult { + Error { + #[cfg_attr(feature = "std", serde(serialize_with = "string_serialize"))] + error: Vec, + }, + Success { + #[cfg_attr(feature = "std", serde(rename = "createdContractAddressHash"))] + created_contract_address_hash: H160, + #[cfg_attr( + feature = "std", + serde(serialize_with = "bytes_0x_serialize", rename = "createdContractCode") + )] + created_contract_code: Vec, + }, + } + + #[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] + #[cfg_attr(feature = "std", derive(Serialize))] + #[cfg_attr(feature = "std", serde(rename_all = "camelCase", tag = "type"))] + pub enum EntryInner { + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + Call { + /// Type of call. + call_type: CallType, + to: H160, + #[cfg_attr(feature = "std", serde(serialize_with = "bytes_0x_serialize"))] + input: Vec, + /// "output" or "error" field + #[cfg_attr(feature = "std", serde(flatten))] + res: CallResult, + }, + + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + Create { + #[cfg_attr(feature = "std", serde(serialize_with = "bytes_0x_serialize"))] + init: Vec, + #[cfg_attr(feature = "std", serde(flatten))] + res: CreateResult, + }, + // Revert, + SelfDestruct, + } + + #[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] + #[cfg_attr(feature = "std", derive(Serialize))] + #[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] + pub struct Entry { + pub from: H160, + /// Indices of parent calls. + pub trace_address: Vec, + /// Sends funds to the (payable) function + pub value: U256, + /// Remaining gas in the runtime. + pub gas: U256, + /// Gas used by this context. + pub gas_used: U256, + #[cfg_attr(feature = "std", serde(flatten))] + pub inner: EntryInner, + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug, Encode, Decode)] +pub enum TraceType { + /// Classic geth with no javascript based tracing. + Raw, + /// Output Blockscout expects. + Blockscout, +} + +#[cfg(feature = "std")] +fn seq_h256_serialize(data: &[H256], serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(data.len()))?; + for h in data { + seq.serialize_element(&format!("{:x}", h))?; + } + seq.end() +} + +#[cfg(feature = "std")] +fn bytes_0x_serialize(bytes: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) +} + +#[cfg(feature = "std")] +fn opcode_serialize(opcode: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + // TODO: how to propagate Err here (i.e. `from_utf8` fails), so the rpc requests also + // returns an error? + serializer.serialize_str(&std::str::from_utf8(opcode).unwrap_or("").to_uppercase()) +} + +#[cfg(feature = "std")] +fn string_serialize(value: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + // TODO: how to propagate Err here (i.e. `from_utf8` fails), so the rpc requests also + // returns an error? + serializer.serialize_str(&format!("{}", std::str::from_utf8(value).unwrap_or(""))) +} + +#[cfg(feature = "std")] +fn u256_serialize(data: &U256, serializer: S) -> Result +where + S: Serializer, +{ + // TODO: how to propagate Err here (i.e. `from_utf8` fails), so the rpc requests also + // returns an error? + serializer.serialize_u64(data.low_u64()) +} + +sp_api::decl_runtime_apis! { + pub trait DebugRuntimeApi { + fn trace_transaction( + extrinsics: Vec, + transaction: &Transaction, + trace_type: TraceType, + ) -> Result; + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 36b6747c47..e277903bbb 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -12,6 +12,7 @@ serde = { version = "1.0.101", default-features = false, optional = true, featur parity-scale-codec = { version = "2.0.0", default-features = false, features = ["derive"] } log = "0.4" hex-literal = "0.3.1" +sha3 = { version = "0.9", default-features = false } precompiles = { path = "precompiles/", default-features = false } account = { path = "account/", default-features = false } @@ -54,6 +55,8 @@ pallet-democracy = { git = "https://github.com/paritytech/substrate", default-fe pallet-scheduler = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "rococo-v1" } pallet-collective = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "rococo-v1" } +moonbeam-rpc-primitives-debug = { path = "../primitives/rpc/debug", default-features = false } +moonbeam-extensions-evm = { path = "extensions/evm", default-features = false } moonbeam-rpc-primitives-txpool = { path = "../primitives/rpc/txpool", default-features = false } # Cumulus dependencies @@ -74,6 +77,7 @@ default = [ std = [ "parity-scale-codec/std", "serde", + "sha3/std", "sp-api/std", "sp-std/std", "sp-io/std", @@ -96,6 +100,7 @@ std = [ "pallet-utility/std", "pallet-ethereum/std", "pallet-evm/std", + "moonbeam-rpc-primitives-debug/std", "moonbeam-rpc-primitives-txpool/std", "fp-rpc/std", "frame-system-rpc-runtime-api/std", @@ -105,6 +110,7 @@ std = [ "pallet-scheduler/std", "pallet-collective/std", "author-inherent/std", + "moonbeam-extensions-evm/std", "parachain-info/std", "cumulus-pallet-parachain-system/std", "cumulus-primitives-core/std", diff --git a/runtime/extensions/evm/Cargo.toml b/runtime/extensions/evm/Cargo.toml new file mode 100644 index 0000000000..4b7febbbc1 --- /dev/null +++ b/runtime/extensions/evm/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "moonbeam-extensions-evm" +version = '0.1.0' +authors = ['PureStake'] +edition = '2018' +homepage = 'https://moonbeam.network' +license = 'GPL-3.0-only' +repository = 'https://github.com/PureStake/moonbeam/' + +[dependencies] +fp-evm = { git = "https://github.com/purestake/frontier", default-features = false, branch = "notlesh-moonbeam-v0.7" } +pallet-evm = { git = "https://github.com/purestake/frontier", default-features = false, branch = "notlesh-moonbeam-v0.7" } +evm = { git = "https://github.com/rust-blockchain/evm", branch = "master", default-features = false, features = ["with-codec"] } +evm-core = { git = "https://github.com/rust-blockchain/evm", branch = "master", default-features = false, features = ["with-codec"] } +sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1" } +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1" } +sp-io = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1" } +sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "rococo-v1" } +moonbeam-rpc-primitives-debug = { path = "../../../primitives/rpc/debug", default-features = false } +ethereum-types = { version = "0.11.0", default-features = false } + +[features] +default = ["std"] +std = [ + "evm/std", + "evm/with-serde", + "pallet-evm/std", + "sp-core/std", + "sp-std/std", + "sp-runtime/std", + "ethereum-types/std", + "fp-evm/std", + "moonbeam-rpc-primitives-debug/std" +] \ No newline at end of file diff --git a/runtime/extensions/evm/src/executor/mod.rs b/runtime/extensions/evm/src/executor/mod.rs new file mode 100644 index 0000000000..61d0106cb0 --- /dev/null +++ b/runtime/extensions/evm/src/executor/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +pub mod util; +pub mod wrapper; diff --git a/runtime/extensions/evm/src/executor/util.rs b/runtime/extensions/evm/src/executor/util.rs new file mode 100644 index 0000000000..afaccbfacb --- /dev/null +++ b/runtime/extensions/evm/src/executor/util.rs @@ -0,0 +1,181 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use evm::Opcode; +extern crate alloc; +use alloc::string::ToString; +use sp_std::vec::Vec; + +pub fn opcodes(opcode: Opcode) -> Vec { + let out = match opcode { + Opcode(0) => "Stop", + Opcode(1) => "Add", + Opcode(2) => "Mul", + Opcode(3) => "Sub", + Opcode(4) => "Div", + Opcode(5) => "SDiv", + Opcode(6) => "Mod", + Opcode(7) => "SMod", + Opcode(8) => "AddMod", + Opcode(9) => "MulMod", + Opcode(10) => "Exp", + Opcode(11) => "SignExtend", + Opcode(16) => "Lt", + Opcode(17) => "Gt", + Opcode(18) => "Slt", + Opcode(19) => "Sgt", + Opcode(20) => "Eq", + Opcode(21) => "IsZero", + Opcode(22) => "And", + Opcode(23) => "Or", + Opcode(24) => "Xor", + Opcode(25) => "Not", + Opcode(26) => "Byte", + Opcode(27) => "Shl", + Opcode(28) => "Shr", + Opcode(29) => "Sar", + Opcode(32) => "Keccak256", + Opcode(48) => "Address", + Opcode(49) => "Balance", + Opcode(50) => "Origin", + Opcode(51) => "Caller", + Opcode(52) => "CallValue", + Opcode(53) => "CallDataLoad", + Opcode(54) => "CallDataSize", + Opcode(55) => "CallDataCopy", + Opcode(56) => "CodeSize", + Opcode(57) => "CodeCopy", + Opcode(58) => "GasPrice", + Opcode(59) => "ExtCodeSize", + Opcode(60) => "ExtCodeCopy", + Opcode(61) => "ReturnDataSize", + Opcode(62) => "ReturnDataCopy", + Opcode(63) => "ExtCodeHash", + Opcode(64) => "BlockHash", + Opcode(65) => "Coinbase", + Opcode(66) => "Timestamp", + Opcode(67) => "Number", + Opcode(68) => "Difficulty", + Opcode(69) => "GasLimit", + Opcode(70) => "ChainId", + Opcode(80) => "Pop", + Opcode(81) => "MLoad", + Opcode(82) => "MStore", + Opcode(83) => "MStore8", + Opcode(84) => "SLoad", + Opcode(85) => "SStore", + Opcode(86) => "Jump", + Opcode(87) => "JumpI", + Opcode(88) => "GetPc", + Opcode(89) => "MSize", + Opcode(90) => "Gas", + Opcode(91) => "JumpDest", + Opcode(96) => "Push1", + Opcode(97) => "Push2", + Opcode(98) => "Push3", + Opcode(99) => "Push4", + Opcode(100) => "Push5", + Opcode(101) => "Push6", + Opcode(102) => "Push7", + Opcode(103) => "Push8", + Opcode(104) => "Push9", + Opcode(105) => "Push10", + Opcode(106) => "Push11", + Opcode(107) => "Push12", + Opcode(108) => "Push13", + Opcode(109) => "Push14", + Opcode(110) => "Push15", + Opcode(111) => "Push16", + Opcode(112) => "Push17", + Opcode(113) => "Push18", + Opcode(114) => "Push19", + Opcode(115) => "Push20", + Opcode(116) => "Push21", + Opcode(117) => "Push22", + Opcode(118) => "Push23", + Opcode(119) => "Push24", + Opcode(120) => "Push25", + Opcode(121) => "Push26", + Opcode(122) => "Push27", + Opcode(123) => "Push28", + Opcode(124) => "Push29", + Opcode(125) => "Push30", + Opcode(126) => "Push31", + Opcode(127) => "Push32", + Opcode(128) => "Dup1", + Opcode(129) => "Dup2", + Opcode(130) => "Dup3", + Opcode(131) => "Dup4", + Opcode(132) => "Dup5", + Opcode(133) => "Dup6", + Opcode(134) => "Dup7", + Opcode(135) => "Dup8", + Opcode(136) => "Dup9", + Opcode(137) => "Dup10", + Opcode(138) => "Dup11", + Opcode(139) => "Dup12", + Opcode(140) => "Dup13", + Opcode(141) => "Dup14", + Opcode(142) => "Dup15", + Opcode(143) => "Dup16", + Opcode(144) => "Swap1", + Opcode(145) => "Swap2", + Opcode(146) => "Swap3", + Opcode(147) => "Swap4", + Opcode(148) => "Swap5", + Opcode(149) => "Swap6", + Opcode(150) => "Swap7", + Opcode(151) => "Swap8", + Opcode(152) => "Swap9", + Opcode(153) => "Swap10", + Opcode(154) => "Swap11", + Opcode(155) => "Swap12", + Opcode(156) => "Swap13", + Opcode(157) => "Swap14", + Opcode(158) => "Swap15", + Opcode(159) => "Swap16", + Opcode(160) => "Log0", + Opcode(161) => "Log1", + Opcode(162) => "Log2", + Opcode(163) => "Log3", + Opcode(164) => "Log4", + Opcode(176) => "JumpTo", + Opcode(177) => "JumpIf", + Opcode(178) => "JumpSub", + Opcode(180) => "JumpSubv", + Opcode(181) => "BeginSub", + Opcode(182) => "BeginData", + Opcode(184) => "ReturnSub", + Opcode(185) => "PutLocal", + Opcode(186) => "GetLocal", + Opcode(225) => "SLoadBytes", + Opcode(226) => "SStoreBytes", + Opcode(227) => "SSize", + Opcode(240) => "Create", + Opcode(241) => "Call", + Opcode(242) => "CallCode", + Opcode(243) => "Return", + Opcode(244) => "DelegateCall", + Opcode(245) => "Create2", + Opcode(250) => "StaticCall", + Opcode(252) => "TxExecGas", + Opcode(253) => "Revert", + Opcode(254) => "Invalid", + Opcode(255) => "SelfDestruct", + _ => unreachable!("Unreachable Opcode identifier."), + }; + out.to_string().as_bytes().to_vec() +} diff --git a/runtime/extensions/evm/src/executor/wrapper.rs b/runtime/extensions/evm/src/executor/wrapper.rs new file mode 100644 index 0000000000..2b41d515f7 --- /dev/null +++ b/runtime/extensions/evm/src/executor/wrapper.rs @@ -0,0 +1,743 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +extern crate alloc; +use crate::executor::util::opcodes; +use ethereum_types::{H160, H256, U256}; +pub use evm::{ + backend::{Apply, Backend as BackendT, Log}, + executor::{StackExecutor, StackExitKind, StackState as StackStateT}, + gasometer::{self as gasometer}, + Capture, Config, Context, CreateScheme, ExitError, ExitFatal, ExitReason, ExitSucceed, + Handler as HandlerT, Opcode, Runtime, Stack, Transfer, +}; +use moonbeam_rpc_primitives_debug::{ + blockscout::{CallResult, CallType, CreateResult, Entry, EntryInner}, + StepLog, TraceType, +}; +use sp_std::{ + cmp::min, collections::btree_map::BTreeMap, convert::Infallible, rc::Rc, vec, vec::Vec, +}; + +pub struct TraceExecutorWrapper<'config, S> { + // Common parts. + pub inner: &'config mut StackExecutor<'config, S>, + is_tracing: bool, + trace_type: TraceType, + + // Raw state. + pub step_logs: Vec, + + // Blockscout state. + pub entries: BTreeMap, + entries_next_index: u32, + call_type: Option, + trace_address: Vec, +} + +enum ContextType { + Call, + Create, +} + +impl<'config, S: StackStateT<'config>> TraceExecutorWrapper<'config, S> { + pub fn new( + inner: &'config mut StackExecutor<'config, S>, + is_tracing: bool, + trace_type: TraceType, + ) -> TraceExecutorWrapper<'config, S> { + TraceExecutorWrapper { + inner, + is_tracing, + trace_type, + step_logs: vec![], + entries: BTreeMap::new(), + entries_next_index: 0, + call_type: None, + trace_address: vec![], + } + } + + fn trace( + &mut self, + runtime: &mut Runtime, + context_type: ContextType, + code: Vec, + ) -> ExitReason { + match self.trace_type { + TraceType::Raw => self.trace_raw(runtime), + TraceType::Blockscout => self.trace_blockscout(runtime, context_type, code), + } + } + + fn trace_raw(&mut self, runtime: &mut Runtime) -> ExitReason { + // TODO : If subcalls on a same contract access more storage, does it cache it here too ? + // (not done yet) + let mut storage_cache: BTreeMap = BTreeMap::new(); + let address = runtime.context().address; + + loop { + let mut storage_complete_scan = false; + let mut storage_key_scan: Option = None; + let mut steplog = None; + + if let Some((opcode, stack)) = runtime.machine().inspect() { + // Will opcode modify storage + if matches!( + opcode, + Opcode(0x54) | // sload + Opcode(0x55) // sstore + ) { + if let Ok(key) = stack.peek(0) { + storage_key_scan = Some(key); + } + } + + // Any call might modify the storage values outside if this instance of the loop, + // rendering the cache obsolete. In this case we'll refresh the cache after the next + // step. + storage_complete_scan = matches!( + opcode, + Opcode(240) | // create + Opcode(241) | // call + Opcode(242) | // call code + Opcode(244) | // delegate call + Opcode(245) | // create 2 + Opcode(250) // static call + ); + + let gas = self.inner.state().metadata().gasometer().gas(); + + let gas_cost = match gasometer::static_opcode_cost(opcode) { + Some(cost) => cost, + _ => { + match gasometer::dynamic_opcode_cost( + runtime.context().address, + opcode, + stack, + self.inner.state().metadata().is_static(), + self.inner.config(), + self, + ) { + Ok((opcode_cost, _)) => match self + .inner + .state() + .metadata() + .gasometer() + .gas_cost(opcode_cost, gas) + { + Ok(cost) => cost, + Err(e) => return ExitReason::Error(e), + }, + Err(e) => break ExitReason::Error(e), + } + } + }; + let position = match runtime.machine().position() { + Ok(p) => p, + Err(reason) => break reason.clone(), + }; + + steplog = Some(StepLog { + // EVM's returned depth is depth output format - 1. + depth: U256::from( + self.inner.state().metadata().depth().unwrap_or_default() + 1, + ), + gas: U256::from(self.inner.gas()), + gas_cost: U256::from(gas_cost), + memory: { + // Vec to Vec conversion. + let memory = &runtime.machine().memory().data()[..]; + let size = 32; + memory + .chunks(size) + .map(|c| { + let mut msg = [0u8; 32]; + let chunk = c.len(); + if chunk < size { + let left = size - chunk; + let remainder = vec![0; left]; + msg[0..left].copy_from_slice(&remainder[..]); + msg[left..size].copy_from_slice(c); + } else { + msg[0..size].copy_from_slice(c) + } + H256::from_slice(&msg[..]) + }) + .collect() + }, + op: opcodes(opcode), + pc: U256::from(*position), + stack: runtime.machine().stack().data().clone(), + storage: BTreeMap::new(), + }); + } + + let step_result = runtime.step(self); + + // Update cache if needed. + if let Some(key) = storage_key_scan { + let _ = storage_cache.insert(key, self.storage(address, key)); + } + + if storage_complete_scan { + for (key, value) in storage_cache.iter_mut() { + *value = self.storage(address, *key); + } + } + + // Push log into vec here instead here (for SLOAD/STORE "early" update). + if let Some(mut steplog) = steplog { + steplog.storage = storage_cache.clone(); + self.step_logs.push(steplog); + } + + // Do we continue ? + match step_result { + Ok(_) => {} + Err(Capture::Exit(s)) => { + break s; + } + Err(Capture::Trap(_)) => { + break ExitReason::Fatal(ExitFatal::UnhandledInterrupt); + } + } + } + } + + fn trace_blockscout( + &mut self, + runtime: &mut Runtime, + context_type: ContextType, + data: Vec, + ) -> ExitReason { + // Starting new entry. + // + // traceAddress field matches this explanation : + // https://openethereum.github.io/JSONRPC-trace-module#traceaddress-field + // + // We update "trace_address" for a potential subcall. + // Will be popped at the end of this context. + self.trace_address.push(0); + + let entries_index = self.entries_next_index; + self.entries_next_index += 1; + + // Fetch all data we currently can for the entry. + let call_type = self.call_type; + let from = runtime.context().caller; + let to = runtime.context().address; + let value = runtime.context().apparent_value; + + let gas_at_start = self.inner.gas(); + let mut return_stack_offset = None; + let mut return_stack_len = None; + + // Execute the call/create. + let exit_reason = loop { + let mut subcall = false; + + if let Some((opcode, _stack)) = runtime.machine().inspect() { + self.call_type = match opcode { + Opcode(241) => Some(CallType::Call), + Opcode(242) => Some(CallType::CallCode), + Opcode(244) => Some(CallType::DelegateCall), + Opcode(250) => Some(CallType::StaticCall), + _ => None, + }; + + subcall = self.call_type.is_some(); + + if opcode == Opcode(0xf3) { + let stack = runtime.machine().stack().data(); + + return_stack_offset = stack.get(stack.len() - 1).cloned(); + return_stack_len = stack.get(stack.len() - 2).cloned(); + } + } + + match runtime.step(self) { + Ok(_) => {} + Err(Capture::Exit(s)) => { + break s; + } + Err(Capture::Trap(_)) => { + break ExitReason::Fatal(ExitFatal::UnhandledInterrupt); + } + } + + if subcall { + // We increase the last value of "trace_address" for a potential next subcall. + *self.trace_address.last_mut().unwrap() += 1; + } + }; + + // Compute used gas. + let gas_at_end = self.inner.gas(); + let gas_used = gas_at_start - gas_at_end; + + // Insert entry. + + // We pop the children item, giving back this context trace_address. + self.trace_address.pop(); + + self.entries.insert( + entries_index, + match context_type { + ContextType::Call => { + let res = match &exit_reason { + ExitReason::Succeed(ExitSucceed::Returned) => { + CallResult::Output(runtime.machine().return_value()) + } + ExitReason::Succeed(_) => CallResult::Output(vec![]), + ExitReason::Error(error) => CallResult::Error(Self::error_message(error)), + + ExitReason::Revert(_) => CallResult::Error(b"execution reverted".to_vec()), + ExitReason::Fatal(_) => CallResult::Error(vec![]), + }; + + Entry { + from, + trace_address: self.trace_address.clone(), + value, + gas: U256::from(gas_at_end), + gas_used: U256::from(gas_used), + inner: EntryInner::Call { + call_type: call_type.expect("should always have a call type"), + to, + input: data, + res, + }, + } + } + ContextType::Create => { + // let offset = runtine.machine().stack().data(); + let contract_code = if let (Some(offset), Some(len)) = + (return_stack_offset, return_stack_len) + { + let offset = offset.to_low_u64_be() as usize; + let len = len.to_low_u64_be() as usize; + + let memory = runtime.machine().memory().data(); + + if memory.len() >= offset + len { + memory[offset..offset + len].to_vec() + } else { + vec![] // TODO : Should not be possible + } + } else { + vec![] // TODO : Should not be possible + }; + + let res = match &exit_reason { + ExitReason::Succeed(_) => CreateResult::Success { + created_contract_address_hash: to, + created_contract_code: contract_code, + }, + ExitReason::Error(error) => CreateResult::Error { + error: Self::error_message(error), + }, + + ExitReason::Revert(_) => CreateResult::Error { + error: b"execution reverted".to_vec(), + }, + ExitReason::Fatal(_) => CreateResult::Error { error: vec![] }, + }; + + Entry { + value, + trace_address: self.trace_address.clone(), + gas: U256::from(gas_at_end), + gas_used: U256::from(gas_used), + from, + inner: EntryInner::Create { init: data, res }, + } + } + }, + ); + + exit_reason + } + + fn error_message(error: &ExitError) -> Vec { + match error { + ExitError::StackUnderflow => "stack underflow", + ExitError::StackOverflow => "stack overflow", + ExitError::InvalidJump => "invalid jump", + ExitError::InvalidRange => "invalid range", + ExitError::DesignatedInvalid => "designated invalid", + ExitError::CallTooDeep => "call too deep", + ExitError::CreateCollision => "create collision", + ExitError::CreateContractLimit => "create contract limit", + ExitError::OutOfOffset => "out of offset", + ExitError::OutOfGas => "out of gas", + ExitError::OutOfFund => "out of funds", + ExitError::Other(err) => err, + _ => "unexpected error", + } + .as_bytes() + .to_vec() + } + + pub fn trace_call( + &mut self, + address: H160, + transfer: Option, + data: Vec, + target_gas: Option, + is_static: bool, + take_l64: bool, + take_stipend: bool, + context: Context, + ) -> Capture<(ExitReason, Vec), Infallible> { + macro_rules! try_or_fail { + ( $e:expr ) => { + match $e { + Ok(v) => v, + Err(e) => return Capture::Exit((e.into(), Vec::new())), + } + }; + } + + // let after_gas = self.inner.state().metadata().gasometer().gas(); + fn l64(gas: u64) -> u64 { + gas - gas / 64 + } + + let after_gas = if take_l64 && self.inner.config().call_l64_after_gas { + if self.inner.config().estimate { + let initial_after_gas = self.inner.state().metadata().gasometer().gas(); + let diff = initial_after_gas - l64(initial_after_gas); + try_or_fail!(self + .inner + .state_mut() + .metadata_mut() + .gasometer_mut() + .record_cost(diff)); + self.inner.state().metadata().gasometer().gas() + } else { + l64(self.inner.state().metadata().gasometer().gas()) + } + } else { + self.inner.state().metadata().gasometer().gas() + }; + let target_gas = target_gas.unwrap_or(after_gas); + let mut gas_limit = min(target_gas, after_gas); + + try_or_fail!(self + .inner + .state_mut() + .metadata_mut() + .gasometer_mut() + .record_cost(gas_limit)); + + if let Some(transfer) = transfer.as_ref() { + if take_stipend && transfer.value != U256::zero() { + gas_limit = gas_limit.saturating_add(self.inner.config().call_stipend); + } + } + + let code = self.inner.code(address); + self.inner.enter_substate(gas_limit, is_static); + self.inner.state_mut().touch(context.address); + + if let Some(depth) = self.inner.state().metadata().depth() { + if depth > self.inner.config().call_stack_limit { + let _ = self.inner.exit_substate(StackExitKind::Reverted); + return Capture::Exit((ExitError::CallTooDeep.into(), Vec::new())); + } + } + + if let Some(transfer) = transfer { + match self.inner.state_mut().transfer(transfer) { + Ok(()) => (), + Err(e) => { + let _ = self.inner.exit_substate(StackExitKind::Reverted); + return Capture::Exit((ExitReason::Error(e), Vec::new())); + } + } + } + + let mut runtime = Runtime::new( + Rc::new(code), + Rc::new(data.clone()), + context, + self.inner.config(), + ); + + self.call_type = Some(CallType::Call); + match self.trace(&mut runtime, ContextType::Call, data) { + ExitReason::Succeed(s) => { + let _ = self.inner.exit_substate(StackExitKind::Succeeded); + Capture::Exit((ExitReason::Succeed(s), runtime.machine().return_value())) + } + ExitReason::Error(e) => { + let _ = self.inner.exit_substate(StackExitKind::Failed); + Capture::Exit((ExitReason::Error(e), Vec::new())) + } + ExitReason::Revert(e) => { + let _ = self.inner.exit_substate(StackExitKind::Reverted); + Capture::Exit((ExitReason::Revert(e), runtime.machine().return_value())) + } + ExitReason::Fatal(e) => { + self.inner.state_mut().metadata_mut().gasometer_mut().fail(); + let _ = self.inner.exit_substate(StackExitKind::Failed); + Capture::Exit((ExitReason::Fatal(e), Vec::new())) + } + } + } + + pub fn trace_create( + &mut self, + caller: H160, + scheme: CreateScheme, + value: U256, + code: Vec, + target_gas: Option, + ) -> Capture<(ExitReason, Option, Vec), Infallible> { + macro_rules! try_or_fail { + ( $e:expr ) => { + match $e { + Ok(v) => v, + Err(e) => return Capture::Exit((e.into(), None, Vec::new())), + } + }; + } + + if let Some(depth) = self.inner.state().metadata().depth() { + if depth > self.inner.config().call_stack_limit { + return Capture::Exit((ExitError::CallTooDeep.into(), None, Vec::new())); + } + } + + let after_gas = self.inner.state().metadata().gasometer().gas(); + let target_gas = target_gas.unwrap_or(after_gas); + let gas_limit = min(target_gas, after_gas); + + try_or_fail!(self + .inner + .state_mut() + .metadata_mut() + .gasometer_mut() + .record_cost(gas_limit)); + let address = self.inner.create_address(scheme); + self.inner.state_mut().inc_nonce(caller); + self.inner.enter_substate(gas_limit, false); + + let context = Context { + address, + caller, + apparent_value: value, + }; + + let transfer = Transfer { + source: caller, + target: address, + value, + }; + + match self.inner.state_mut().transfer(transfer) { + Ok(()) => (), + Err(e) => { + let _ = self.inner.exit_substate(StackExitKind::Reverted); + return Capture::Exit((ExitReason::Error(e), None, Vec::new())); + } + } + + let mut runtime = Runtime::new( + Rc::new(code.clone()), + Rc::new(Vec::new()), + context, + self.inner.config(), + ); + + match self.trace(&mut runtime, ContextType::Create, code) { + ExitReason::Succeed(s) => { + let out = runtime.machine().return_value(); + + if let Some(limit) = self.inner.config().create_contract_limit { + if out.len() > limit { + self.inner.state_mut().metadata_mut().gasometer_mut().fail(); + let _ = self.inner.exit_substate(StackExitKind::Failed); + return Capture::Exit(( + ExitError::CreateContractLimit.into(), + None, + Vec::new(), + )); + } + } + + match self + .inner + .state_mut() + .metadata_mut() + .gasometer_mut() + .record_deposit(out.len()) + { + Ok(()) => { + let e = self.inner.exit_substate(StackExitKind::Succeeded); + self.inner.state_mut().set_code(address, out); + try_or_fail!(e); + Capture::Exit((ExitReason::Succeed(s), Some(address), Vec::new())) + } + Err(e) => { + let _ = self.inner.exit_substate(StackExitKind::Failed); + Capture::Exit((ExitReason::Error(e), None, Vec::new())) + } + } + } + ExitReason::Error(e) => Capture::Exit((ExitReason::Error(e), None, Vec::new())), + ExitReason::Revert(e) => Capture::Exit(( + ExitReason::Revert(e), + None, + runtime.machine().return_value(), + )), + ExitReason::Fatal(e) => Capture::Exit((ExitReason::Fatal(e), None, Vec::new())), + } + } +} + +impl<'config, S: StackStateT<'config>> HandlerT for TraceExecutorWrapper<'config, S> { + type CreateInterrupt = Infallible; + type CreateFeedback = Infallible; + type CallInterrupt = Infallible; + type CallFeedback = Infallible; + + fn balance(&self, address: H160) -> U256 { + self.inner.balance(address) + } + + fn code_size(&self, address: H160) -> U256 { + self.inner.code_size(address) + } + + fn code_hash(&self, address: H160) -> H256 { + self.inner.code_hash(address) + } + + fn code(&self, address: H160) -> Vec { + self.inner.code(address) + } + + fn storage(&self, address: H160, index: H256) -> H256 { + self.inner.storage(address, index) + } + + fn original_storage(&self, address: H160, index: H256) -> H256 { + self.inner.original_storage(address, index) + } + + fn exists(&self, address: H160) -> bool { + self.inner.exists(address) + } + + fn gas_left(&self) -> U256 { + self.inner.gas_left() + } + + fn gas_price(&self) -> U256 { + self.inner.state().gas_price() + } + fn origin(&self) -> H160 { + self.inner.state().origin() + } + fn block_hash(&self, number: U256) -> H256 { + self.inner.state().block_hash(number) + } + fn block_number(&self) -> U256 { + self.inner.state().block_number() + } + fn block_coinbase(&self) -> H160 { + self.inner.state().block_coinbase() + } + fn block_timestamp(&self) -> U256 { + self.inner.state().block_timestamp() + } + fn block_difficulty(&self) -> U256 { + self.inner.state().block_difficulty() + } + fn block_gas_limit(&self) -> U256 { + self.inner.state().block_gas_limit() + } + fn chain_id(&self) -> U256 { + self.inner.state().chain_id() + } + + fn deleted(&self, address: H160) -> bool { + self.inner.deleted(address) + } + + fn set_storage(&mut self, address: H160, index: H256, value: H256) -> Result<(), ExitError> { + self.inner.set_storage(address, index, value) + } + + fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), ExitError> { + self.inner.log(address, topics, data) + } + + fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError> { + self.inner.mark_delete(address, target) + } + + fn create( + &mut self, + caller: H160, + scheme: CreateScheme, + value: U256, + init_code: Vec, + target_gas: Option, + ) -> Capture<(ExitReason, Option, Vec), Self::CreateInterrupt> { + if self.is_tracing { + self.trace_create(caller, scheme, value, init_code, target_gas) + } else { + unreachable!("TODO StackExecutorWrapper only available on tracing enabled."); + } + } + + fn call( + &mut self, + code_address: H160, + transfer: Option, + input: Vec, + target_gas: Option, + is_static: bool, + context: Context, + ) -> Capture<(ExitReason, Vec), Self::CallInterrupt> { + if self.is_tracing { + self.trace_call( + code_address, + transfer, + input, + target_gas, + is_static, + true, + true, + context, + ) + } else { + unreachable!("TODO StackExecutorWrapper only available on tracing enabled."); + } + } + + fn pre_validate( + &mut self, + context: &Context, + opcode: Opcode, + stack: &Stack, + ) -> Result<(), ExitError> { + self.inner.pre_validate(context, opcode, stack) + } +} diff --git a/runtime/extensions/evm/src/lib.rs b/runtime/extensions/evm/src/lib.rs new file mode 100644 index 0000000000..a6ce58c296 --- /dev/null +++ b/runtime/extensions/evm/src/lib.rs @@ -0,0 +1,82 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! Substrate EVM tracing. +//! +//! The purpose of this extension is enable tracing the EVM opcode execution and will be used by +//! both Dapp developers - to get a granular view on their transactions - and indexers to access +//! the EVM callstack (internal transactions). +//! +//! The extension composed of two modules: +//! +//! - Runner - interfaces the runtime Api with the EVM Executor wrapper. +//! - Executor Wrapper - an evm::Handler implementor that wraps an evm::StackExecutor. +//! +//! The wrapper replaces the original recursive opcode execution done by the evm's +//! `create_inner` and `call_inner` methods, with one that allows capturing the intermediate machine +//! state between opcode executions (stepping), resulting in either a granular per opcode response: +//! +//! ```json +//! { +//! "pc": 230, +//! "op": "SSTORE", +//! "gas": 62841, +//! "gasCost": 20000, +//! "depth": 1, +//! "stack": [ +//! "00000000000000000000000000000000000000000000000000000000398f7223", +//! ], +//! "memory": [ +//! "0000000000000000000000000000000000000000000000000000000000000000", +//! ], +//! "storage": {"0x":"0x"} +//! } +//! ``` +//! +//! or an overview of the internal transactions in a context type. +//! +//! ```json +//! [ +//! { +//! "type": "call", +//! "callType": "call", +//! "from": "0xfe2882ac0a337a976aa73023c2a2a917f57ba2ed", +//! "to": "0x3ca17a1c4995b95c600275e52da93d2e64dd591f", +//! "input": "0x", +//! "output": "0x", +//! "traceAddress": [], +//! "value": "0x0", +//! "gas": "0xf9be", +//! "gasUsed": "0xf9be" +//! }, +//! { +//! "type": "call", +//! "callType": "call", +//! "from": "0x3ca17a1c4995b95c600275e52da93d2e64dd591f", +//! "to": "0x1416aa2a27db08ce3a81a01cdfc981417d28a6e6", +//! "input": "0xfd63983b0000000000000000000000000000000000000000000000000000000000000006", +//! "output": "0x000000000000000000000000000000000000000000000000000000000000000d", +//! "traceAddress": [0], +//! "value": "0x0", +//! "gas": "0x9b9b", +//! "gasUsed": "0x4f6d" +//! } +//! ] +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] +pub mod executor; +pub mod runner; diff --git a/runtime/extensions/evm/src/runner/mod.rs b/runtime/extensions/evm/src/runner/mod.rs new file mode 100644 index 0000000000..7ea5dbfd86 --- /dev/null +++ b/runtime/extensions/evm/src/runner/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +pub mod stack; diff --git a/runtime/extensions/evm/src/runner/stack.rs b/runtime/extensions/evm/src/runner/stack.rs new file mode 100644 index 0000000000..3bfd565c65 --- /dev/null +++ b/runtime/extensions/evm/src/runner/stack.rs @@ -0,0 +1,206 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use crate::executor::wrapper::TraceExecutorWrapper; +use ethereum_types::{H160, U256}; +use evm::{ + executor::{StackExecutor, StackState as StackStateT, StackSubstateMetadata}, + Capture, Config as EvmConfig, Context, CreateScheme, Transfer, +}; +use moonbeam_rpc_primitives_debug::{TraceExecutorResponse, TraceType}; +use pallet_evm::{ + runner::stack::{Runner, SubstrateStackState}, + Config, ExitError, ExitReason, PrecompileSet, Vicinity, +}; +use sp_std::{convert::Infallible, vec::Vec}; + +/// EVM Tracing for pallet_evm::stack::Runner. +pub trait TraceRunner { + /// Handle an Executor wrapper `call`. Used by `trace_call`. + fn execute_call<'config, F>( + executor: &'config mut StackExecutor<'config, SubstrateStackState<'_, 'config, T>>, + trace_type: TraceType, + f: F, + ) -> Result + where + F: FnOnce( + &mut TraceExecutorWrapper<'config, SubstrateStackState<'_, 'config, T>>, + ) -> Capture<(ExitReason, Vec), Infallible>; + + /// Handle an Executor wrapper `create`. Used by `trace_create`. + fn execute_create<'config, F>( + executor: &'config mut StackExecutor<'config, SubstrateStackState<'_, 'config, T>>, + trace_type: TraceType, + f: F, + ) -> Result + where + F: FnOnce( + &mut TraceExecutorWrapper<'config, SubstrateStackState<'_, 'config, T>>, + ) -> Capture<(ExitReason, Option, Vec), Infallible>; + + /// Context creation for `call`. Typically called by the Runtime Api. + fn trace_call( + source: H160, + target: H160, + input: Vec, + value: U256, + gas_limit: u64, + config: &EvmConfig, + trace_type: TraceType, + ) -> Result; + + /// Context creation for create. Typically called by the Runtime Api. + fn trace_create( + source: H160, + init: Vec, + value: U256, + gas_limit: u64, + config: &EvmConfig, + trace_type: TraceType, + ) -> Result; +} + +impl TraceRunner for Runner { + fn execute_call<'config, F>( + executor: &'config mut StackExecutor<'config, SubstrateStackState<'_, 'config, T>>, + trace_type: TraceType, + f: F, + ) -> Result + where + F: FnOnce( + &mut TraceExecutorWrapper<'config, SubstrateStackState<'_, 'config, T>>, + ) -> Capture<(ExitReason, Vec), Infallible>, + { + let mut wrapper = TraceExecutorWrapper::new(executor, true, trace_type); + + let execution_result = match f(&mut wrapper) { + Capture::Exit((_reason, result)) => result, + _ => unreachable!("Never reached?"), + }; + + match trace_type { + TraceType::Raw => Ok(TraceExecutorResponse::Raw { + gas: U256::from(wrapper.inner.state().metadata().gasometer().gas()), + return_value: execution_result, + step_logs: wrapper.step_logs, + }), + TraceType::Blockscout => Ok(TraceExecutorResponse::Blockscout( + wrapper + .entries + .into_iter() + .map(|(_, value)| value) + .collect(), + )), + } + } + + fn execute_create<'config, F>( + executor: &'config mut StackExecutor<'config, SubstrateStackState<'_, 'config, T>>, + trace_type: TraceType, + f: F, + ) -> Result + where + F: FnOnce( + &mut TraceExecutorWrapper<'config, SubstrateStackState<'_, 'config, T>>, + ) -> Capture<(ExitReason, Option, Vec), Infallible>, + { + let mut wrapper = TraceExecutorWrapper::new(executor, true, trace_type); + + let execution_result = match f(&mut wrapper) { + Capture::Exit((_reason, _address, result)) => result, + _ => unreachable!("Never reached?"), + }; + + match trace_type { + TraceType::Raw => Ok(TraceExecutorResponse::Raw { + gas: U256::from(wrapper.inner.state().metadata().gasometer().gas()), + return_value: execution_result, + step_logs: wrapper.step_logs, + }), + TraceType::Blockscout => Ok(TraceExecutorResponse::Blockscout( + wrapper + .entries + .into_iter() + .map(|(_, value)| value) + .collect(), + )), + } + } + + fn trace_call( + source: H160, + target: H160, + input: Vec, + value: U256, + gas_limit: u64, + config: &EvmConfig, + trace_type: TraceType, + ) -> Result { + let vicinity = Vicinity { + gas_price: U256::zero(), + origin: source, + }; + let metadata = StackSubstateMetadata::new(gas_limit, &config); + let state = SubstrateStackState::new(&vicinity, metadata); + let mut executor = + StackExecutor::new_with_precompile(state, config, T::Precompiles::execute); + let context = Context { + caller: source, + address: target, + apparent_value: value, + }; + + Self::execute_call(&mut executor, trace_type, |executor| { + executor.trace_call( + target, + Some(Transfer { + source, + target, + value, + }), + input, + Some(gas_limit as u64), + false, + false, + false, + context, + ) + }) + } + + fn trace_create( + source: H160, + init: Vec, + value: U256, + gas_limit: u64, + config: &EvmConfig, + trace_type: TraceType, + ) -> Result { + let vicinity = Vicinity { + gas_price: U256::zero(), + origin: source, + }; + + let metadata = StackSubstateMetadata::new(gas_limit, &config); + let state = SubstrateStackState::new(&vicinity, metadata); + let mut executor = + StackExecutor::new_with_precompile(state, config, T::Precompiles::execute); + let scheme = CreateScheme::Legacy { caller: source }; + Self::execute_create(&mut executor, trace_type, |executor| { + executor.trace_create(source, scheme, value, init, Some(gas_limit as u64)) + }) + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 4488b8f1d7..af5d74e70b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -30,14 +30,16 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use fp_rpc::TransactionStatus; use frame_support::{ - construct_runtime, + construct_runtime, debug, pallet_prelude::PhantomData, parameter_types, traits::{Get, Randomness}, weights::{constants::WEIGHT_PER_SECOND, IdentityFee, Weight}, }; use frame_system::{EnsureOneOf, EnsureRoot}; +use moonbeam_extensions_evm::runner::stack::TraceRunner as TraceRunnerT; use pallet_ethereum::Call::transact; +use pallet_ethereum::{Transaction as EthereumTransaction, TransactionAction}; use pallet_evm::{ Account as EVMAccount, EnsureAddressNever, EnsureAddressSame, FeeCalculator, IdentityAddressMapping, Runner, @@ -45,6 +47,7 @@ use pallet_evm::{ use pallet_transaction_payment::CurrencyAdapter; pub use parachain_staking::{InflationInfo, Range}; use parity_scale_codec::{Decode, Encode}; +use sha3::{Digest, Keccak256}; use sp_api::impl_runtime_apis; use sp_core::{u32_trait::*, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ @@ -644,6 +647,85 @@ impl_runtime_apis! { System::account_nonce(account) } } + impl moonbeam_rpc_primitives_debug::DebugRuntimeApi for Runtime { + fn trace_transaction( + extrinsics: Vec<::Extrinsic>, + transaction: &EthereumTransaction, + trace_type: moonbeam_rpc_primitives_debug::TraceType, + ) -> Result< + moonbeam_rpc_primitives_debug::TraceExecutorResponse, + sp_runtime::DispatchError + > { + // Get the caller; + let mut sig = [0u8; 65]; + let mut msg = [0u8; 32]; + sig[0..32].copy_from_slice(&transaction.signature.r()[..]); + sig[32..64].copy_from_slice(&transaction.signature.s()[..]); + sig[64] = transaction.signature.standard_v(); + msg.copy_from_slice( + &pallet_ethereum::TransactionMessage::from(transaction.clone()).hash()[..] + ); + + let from = match sp_io::crypto::secp256k1_ecdsa_recover(&sig, &msg) { + Ok(pk) => H160::from( + H256::from_slice(Keccak256::digest(&pk).as_slice()) + ), + _ => H160::default() + }; + + // Apply the a subset of extrinsics: all the substrate-specific or ethereum transactions + // that preceded the requested transaction. + for ext in extrinsics.into_iter() { + let _ = match &ext.function { + Call::Ethereum(transact(t)) => { + if t == transaction { + break; + } + Executive::apply_extrinsic(ext) + }, + _ => Executive::apply_extrinsic(ext) + }; + } + + let mut c = ::config().clone(); + c.estimate = true; + let config = Some(c); + + // Use the runner extension to interface with our evm's trace executor and return the + // TraceExecutorResult. + match transaction.action { + TransactionAction::Call(to) => { + if let Ok(res) = ::Runner::trace_call( + from, + to, + transaction.input.clone(), + transaction.value, + transaction.gas_limit.low_u64(), + config.as_ref().unwrap_or(::config()), + trace_type, + ) { + return Ok(res); + } else { + return Err(sp_runtime::DispatchError::Other("Evm error")); + } + }, + TransactionAction::Create => { + if let Ok(res) = ::Runner::trace_create( + from, + transaction.input.clone(), + transaction.value, + transaction.gas_limit.low_u64(), + config.as_ref().unwrap_or(::config()), + trace_type, + ) { + return Ok(res); + } else { + return Err(sp_runtime::DispatchError::Other("Evm error")); + } + } + } + } + } impl moonbeam_rpc_primitives_txpool::TxPoolRuntimeApi for Runtime { fn extrinsic_filter( diff --git a/tests/replay/.gitignore b/tests/replay/.gitignore new file mode 100644 index 0000000000..5e63f7dbc7 --- /dev/null +++ b/tests/replay/.gitignore @@ -0,0 +1,3 @@ +package-lock.json +replay.js +db/ diff --git a/tests/replay/package.json b/tests/replay/package.json new file mode 100644 index 0000000000..db39fc8f1f --- /dev/null +++ b/tests/replay/package.json @@ -0,0 +1,15 @@ +{ + "name": "eth-replay", + "version": "1.0.0", + "description": "", + "main": "replay.js", + "scripts": { + "replay": "tsc && node replay.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "typescript": "^3.9.6", + "web3": "^1.3.3" + } +} diff --git a/tests/replay/replay.ts b/tests/replay/replay.ts new file mode 100644 index 0000000000..8dafeb2261 --- /dev/null +++ b/tests/replay/replay.ts @@ -0,0 +1,139 @@ +import Web3 from "web3"; +import { JsonRpcResponse } from "web3-core-helpers"; + +const fs = require("fs"); +const URL = "http://localhost:9990"; +const ERROR_FILE = "db/error.json"; +const PROGRESS_FILE = "db/progress.json"; + +function lastProcessedBlock(): any { + try { + if (fs.existsSync(PROGRESS_FILE)) { + let p = JSON.parse(fs.readFileSync(PROGRESS_FILE)); + if (p.hasOwnProperty("lastProcessedBlock")) { + return p; + } else { + throw Error("Progress file is corrupted."); + } + } else { + let p = { + lastProcessedBlock: 0, + ethTransactionsProcessed: 0, + }; + fs.writeFileSync(PROGRESS_FILE, JSON.stringify(p)); + return p; + } + } catch (err) { + throw err; + } +} + +function createErrorFile() { + try { + if (!fs.existsSync(ERROR_FILE)) { + fs.writeFileSync( + ERROR_FILE, + JSON.stringify({ + errors: [], + }) + ); + } + } catch (err) { + throw err; + } +} + +async function processBlock(web3: Web3, n: number): Promise { + // Get current block and iterate over its transaction hashes. + let block = await web3.eth.getBlock(n); + for (let txn of block.transactions) { + let params = [txn]; + // Replay the current transaction. + let req = new Promise((resolve, reject) => { + (web3.currentProvider as any).send( + { + jsonrpc: "2.0", + id: 1, + method: "debug_traceTransaction", + params, + }, + (error: Error | null, result?: JsonRpcResponse) => { + // We are only interested in errors. Error in HTTP request. + if (error) { + let e = JSON.parse(fs.readFileSync(ERROR_FILE)); + let current = e.errors; + current.push({ + block_number: n, + txn: txn, + error: error.message || error.toString(), + }); + // Update error file. + fs.writeFileSync(ERROR_FILE, JSON.stringify(current)); + reject(`Failed ((${params.join(",")})): ${error.message || error.toString()}`); + } + resolve(result); + } + ); + }); + let response = await req; + // We are only interested in errors. Error on processing the request. + if (response.hasOwnProperty("error")) { + let e = JSON.parse(fs.readFileSync(ERROR_FILE)); + let current = e.errors; + current.push({ + block_number: n, + txn: txn, + error: response.error, + }); + // Update error file. + fs.writeFileSync(ERROR_FILE, JSON.stringify(current)); + } + } + // Return the number of transactions processed in this block. + return block.transactions.length; +} + +(async () => { + let web3 = new Web3(URL); + // Check if there is connectivity. + await web3.eth.net + .isListening() + .then(() => {}) + .catch((e) => { + throw Error("Url cannot be accessed. Exit."); + }); + + // Create db directory if not exists. + if (!fs.existsSync("db")) { + fs.mkdirSync("db"); + } + + // Create error file if not exists. + createErrorFile(); + + // Get last processed block number. Create progress file if not exists. + let last = lastProcessedBlock(); + let from = last.lastProcessedBlock; + let totalTxn = last.ethTransactionsProcessed; + let to = await web3.eth.getBlockNumber(); + + // Progress is corrupted + // a.k.a. network purged but progress file still holding previous progress. + if (from >= to) { + throw Error("Outdated progress file."); + } + + let i; + for (i = from + 1; i <= to; i++) { + // Process a single block. + totalTxn += await processBlock(web3, i); + // Update progress. + fs.writeFileSync( + PROGRESS_FILE, + JSON.stringify({ + lastProcessedBlock: i, + ethTransactionsProcessed: totalTxn, + }) + ); + } +})(); diff --git a/tests/replay/tsconfig.json b/tests/replay/tsconfig.json new file mode 100644 index 0000000000..12ae8b03a7 --- /dev/null +++ b/tests/replay/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true + } +} diff --git a/tests/tests/constants/Callee.json b/tests/tests/constants/Callee.json new file mode 100644 index 0000000000..f6f1ce99c6 --- /dev/null +++ b/tests/tests/constants/Callee.json @@ -0,0 +1,41 @@ +{ + "contractName": "Callee", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "addtwo", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "store", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "linkReferences": {}, + "bytecode": "0x608060405234801561001057600080fd5b5060e78061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c8063975057e7146037578063fd63983b146053575b600080fd5b603d6092565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506098565b6040518082815260200191505060405180910390f35b60005481565b600080600790508260008190555080830191505091905056fea26469706673582212204ac9cd6b4fedb571a3e5bb42d6f06e90328d2a5646b75243a8d4404cca3bc0c564736f6c63430007040033", + "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xE7 DUP1 PUSH2 0x1F PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH1 0x32 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x975057E7 EQ PUSH1 0x37 JUMPI DUP1 PUSH4 0xFD63983B EQ PUSH1 0x53 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x3D PUSH1 0x92 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH1 0x7C PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH1 0x67 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x98 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH1 0x0 SLOAD DUP2 JUMP JUMPDEST PUSH1 0x0 DUP1 PUSH1 0x7 SWAP1 POP DUP3 PUSH1 0x0 DUP2 SWAP1 SSTORE POP DUP1 DUP4 ADD SWAP2 POP POP SWAP2 SWAP1 POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 0x4A 0xC9 0xCD PUSH12 0x4FEDB571A3E5BB42D6F06E90 ORIGIN DUP14 0x2A JUMP CHAINID 0xB7 MSTORE NUMBER 0xA8 0xD4 BLOCKHASH 0x4C 0xCA EXTCODESIZE 0xC0 0xC5 PUSH5 0x736F6C6343 STOP SMOD DIV STOP CALLER ", + "sourceMap": "235:178:0:-:0;;;;;;;;;;;;;;;;;;;" +} diff --git a/tests/tests/constants/Caller.json b/tests/tests/constants/Caller.json new file mode 100644 index 0000000000..09d848752c --- /dev/null +++ b/tests/tests/constants/Caller.json @@ -0,0 +1,40 @@ +{ + "contractName": "Caller", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_number", + "type": "uint256" + } + ], + "name": "someAction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "store", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "linkReferences": {}, + "bytecode": "0x608060405234801561001057600080fd5b506101db806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063398f72231461003b578063975057e714610089575b600080fd5b6100876004803603604081101561005157600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506100a7565b005b61009161019f565b6040518082815260200191505060405180910390f35b816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fd63983b826040518263ffffffff1660e01b815260040180828152602001915050602060405180830381600087803b15801561015a57600080fd5b505af115801561016e573d6000803e3d6000fd5b505050506040513d602081101561018457600080fd5b81019080805190602001909291905050506001819055505050565b6001548156fea26469706673582212205a75600d7b32d5d1ffd6daaefa83041fe1584487889a2390754a44722f5baa4164736f6c63430007040033", + "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x1DB DUP1 PUSH2 0x20 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH2 0x36 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x398F7223 EQ PUSH2 0x3B JUMPI DUP1 PUSH4 0x975057E7 EQ PUSH2 0x89 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0x87 PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x40 DUP2 LT ISZERO PUSH2 0x51 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH2 0xA7 JUMP JUMPDEST STOP JUMPDEST PUSH2 0x91 PUSH2 0x19F JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST DUP2 PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MUL NOT AND SWAP1 DUP4 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND MUL OR SWAP1 SSTORE POP PUSH1 0x0 DUP1 SLOAD SWAP1 PUSH2 0x100 EXP SWAP1 DIV PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH4 0xFD63983B DUP3 PUSH1 0x40 MLOAD DUP3 PUSH4 0xFFFFFFFF AND PUSH1 0xE0 SHL DUP2 MSTORE PUSH1 0x4 ADD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x20 PUSH1 0x40 MLOAD DUP1 DUP4 SUB DUP2 PUSH1 0x0 DUP8 DUP1 EXTCODESIZE ISZERO DUP1 ISZERO PUSH2 0x15A JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP GAS CALL ISZERO DUP1 ISZERO PUSH2 0x16E JUMPI RETURNDATASIZE PUSH1 0x0 DUP1 RETURNDATACOPY RETURNDATASIZE PUSH1 0x0 REVERT JUMPDEST POP POP POP POP PUSH1 0x40 MLOAD RETURNDATASIZE PUSH1 0x20 DUP2 LT ISZERO PUSH2 0x184 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 MLOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x1 DUP2 SWAP1 SSTORE POP POP POP JUMP JUMPDEST PUSH1 0x1 SLOAD DUP2 JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 GAS PUSH22 0x600D7B32D5D1FFD6DAAEFA83041FE1584487889A2390 PUSH22 0x4A44722F5BAA4164736F6C6343000704003300000000 ", + "sourceMap": "24:210:0:-:0;;;;;;;;;;;;;;;;;;;" +} diff --git a/tests/tests/constants/Incrementer.json b/tests/tests/constants/Incrementer.json new file mode 100644 index 0000000000..02fc776dbf --- /dev/null +++ b/tests/tests/constants/Incrementer.json @@ -0,0 +1,28 @@ +{ + "contractName": "Incrementer", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "num", + "type": "uint256" + } + ], + "name": "sum", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "linkReferences": {}, + "bytecode": "0x6080604052348015600f57600080fd5b5060bd8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063188b85b414602d575b600080fd5b605660048036036020811015604157600080fd5b8101908080359060200190929190505050606c565b6040518082815260200191505060405180910390f35b6000816000808282540192505081905550600054905091905056fea26469706673582212209cfa345bd24fe74702b4d1e0828e151e3052c24448b36898211d29d81d7536f764736f6c634300060c0033", + "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xBD DUP1 PUSH2 0x1E PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH1 0x28 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x188B85B4 EQ PUSH1 0x2D JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x56 PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH1 0x41 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x6C JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH1 0x0 DUP2 PUSH1 0x0 DUP1 DUP3 DUP3 SLOAD ADD SWAP3 POP POP DUP2 SWAP1 SSTORE POP PUSH1 0x0 SLOAD SWAP1 POP SWAP2 SWAP1 POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 SWAP13 STATICCALL CALLVALUE JUMPDEST 0xD2 0x4F 0xE7 SELFBALANCE MUL 0xB4 0xD1 0xE0 DUP3 DUP15 ISZERO 0x1E ADDRESS MSTORE 0xC2 DIFFICULTY 0x48 0xB3 PUSH9 0x98211D29D81D7536F7 PUSH5 0x736F6C6343 STOP MOD 0xC STOP CALLER ", + "sourceMap": "32:148:0:-:0;;;;;;;;;;;;;;;;;;;" +} diff --git a/tests/tests/constants/blockscout_tracer.min.json b/tests/tests/constants/blockscout_tracer.min.json new file mode 100644 index 0000000000..e81fe875de --- /dev/null +++ b/tests/tests/constants/blockscout_tracer.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw \"Unknown custom call op \" + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n result.createdContractCode = toHex(db.getCode(ctx.to));\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/tests/tests/test-block.ts b/tests/tests/test-block.ts index d83fa3f209..8f3579f060 100644 --- a/tests/tests/test-block.ts +++ b/tests/tests/test-block.ts @@ -58,7 +58,7 @@ describeWithMoonbeam("Moonbeam RPC (Block)", `simple-specs.json`, (context) => { number: 0, receiptsRoot: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - totalDifficulty: null, + totalDifficulty: "0", transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", }); }); diff --git a/tests/tests/test-trace.ts b/tests/tests/test-trace.ts new file mode 100644 index 0000000000..68ea158dfd --- /dev/null +++ b/tests/tests/test-trace.ts @@ -0,0 +1,167 @@ +import { expect } from "chai"; +import { Keyring } from "@polkadot/keyring"; +import { step } from "mocha-steps"; + +import { createAndFinalizeBlock, describeWithMoonbeam, customRequest } from "./util"; + +const INCREMENTER = require("./constants/Incrementer.json"); +const CALLEE = require("./constants/Callee.json"); +const CALLER = require("./constants/Caller.json"); +const BS_TRACER = require("./constants/blockscout_tracer.min.json"); + +const GENESIS_ACCOUNT = "0x6be02d1d3665660d22ff9624b7be0551ee1ac91b"; +const GENESIS_ACCOUNT_PRIVATE_KEY = + "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342"; + +async function nested(context) { + // Create Callee contract. + const calleeTx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + data: CALLEE.bytecode, + value: "0x00", + gasPrice: "0x01", + gas: "0x100000", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + let send = await customRequest(context.web3, "eth_sendRawTransaction", [calleeTx.rawTransaction]); + await createAndFinalizeBlock(context.polkadotApi); + let receipt = await context.web3.eth.getTransactionReceipt(send.result); + const callee_addr = receipt.contractAddress; + const callee = new context.web3.eth.Contract(CALLEE.abi, callee_addr); + // Create Caller contract. + const callerTx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + data: CALLER.bytecode, + value: "0x00", + gasPrice: "0x01", + gas: "0x100000", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + send = await customRequest(context.web3, "eth_sendRawTransaction", [callerTx.rawTransaction]); + await createAndFinalizeBlock(context.polkadotApi); + receipt = await context.web3.eth.getTransactionReceipt(send.result); + const caller_addr = receipt.contractAddress; + const caller = new context.web3.eth.Contract(CALLER.abi, caller_addr); + // Nested call + let callTx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: caller_addr, + gas: "0x100000", + value: "0x00", + data: caller.methods.someAction(callee_addr, 6).encodeABI(), // calls callee + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + return await customRequest(context.web3, "eth_sendRawTransaction", [callTx.rawTransaction]); +} + +describeWithMoonbeam("Moonbeam RPC (Trace)", `simple-specs.json`, (context) => { + step("[Raw] should replay over an intermediate state", async function () { + const createTx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + data: INCREMENTER.bytecode, + value: "0x00", + gasPrice: "0x01", + gas: "0x100000", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + let send = await customRequest(context.web3, "eth_sendRawTransaction", [ + createTx.rawTransaction, + ]); + await createAndFinalizeBlock(context.polkadotApi); + let receipt = await context.web3.eth.getTransactionReceipt(send.result); + // This contract's `sum` method receives a number as an argument, increments the storage and + // returns the current value. + let contract = new context.web3.eth.Contract(INCREMENTER.abi, receipt.contractAddress); + + // In our case, the total number of transactions == the max value of the incrementer. + // If we trace the last transaction of the block, should return the total number of + // transactions we executed (10). + // If we trace the 5th transaction, should return 5 and so on. + // + // So we set 5 different target txs for a single block: the 1st, 3 intermediate, and + // the last. + const total_txs = 10; + let targets = [1, 2, 5, 8, 10]; + let iteration = 0; + let txs = []; + let num_txs; + // Create 10 transactions in a block. + for (num_txs = 1; num_txs <= total_txs; num_txs++) { + let callTx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: receipt.contractAddress, + gas: "0x100000", + value: "0x00", + nonce: num_txs, + data: contract.methods.sum(1).encodeABI(), // increments by one + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + + send = await customRequest(context.web3, "eth_sendRawTransaction", [callTx.rawTransaction]); + txs.push(send.result); + } + await createAndFinalizeBlock(context.polkadotApi); + // Trace 5 target transactions on it. + for (let target of targets) { + let index = target - 1; + + let receipt = await context.web3.eth.getTransactionReceipt(txs[index]); + + let intermediate_tx = await customRequest(context.web3, "debug_traceTransaction", [ + txs[index], + ]); + + let evm_result = context.web3.utils.hexToNumber("0x" + intermediate_tx.result.returnValue); + + // console.log(`Matching target ${target} against evm result ${evm_result}`); + expect(evm_result).to.equal(target); + } + }); + + step("[Raw] should trace nested contract calls", async function () { + const send = await nested(context); + await createAndFinalizeBlock(context.polkadotApi); + let traceTx = await customRequest(context.web3, "debug_traceTransaction", [send.result]); + let logs = []; + for (let log of traceTx.result.stepLogs) { + if (logs.length == 1) { + logs.push(log); + } + if (log.op == "RETURN") { + logs.push(log); + } + } + expect(logs.length).to.be.equal(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }); + + step("[Blockscout] should trace nested contract calls", async function () { + const send = await nested(context); + await createAndFinalizeBlock(context.polkadotApi); + let traceTx = await customRequest(context.web3, "debug_traceTransaction", [ + send.result, + { tracer: BS_TRACER.body }, + ]); + let entries = traceTx.result; + expect(entries.length).to.be.equal(2); + let resCaller = entries[0]; + let resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + }); +}); diff --git a/tests/tests/util/testWithMoonbeam.ts b/tests/tests/util/testWithMoonbeam.ts index 279cada1e9..56babee4cb 100644 --- a/tests/tests/util/testWithMoonbeam.ts +++ b/tests/tests/util/testWithMoonbeam.ts @@ -47,6 +47,7 @@ export async function startMoonbeamNode( `--no-telemetry`, `--no-prometheus`, `--dev`, + `--ethapi=txpool,debug`, `--sealing=manual`, `-l${MOONBEAM_LOG}`, `--port=${PORT}`,