diff --git a/Cargo.lock b/Cargo.lock index ac8ca79a7..296b5beba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -499,6 +499,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.1.0" @@ -794,9 +800,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -815,9 +821,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -828,14 +834,64 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" dependencies = [ "cfg-if 1.0.0", "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" +dependencies = [ + "bitflags", + "crossterm_winapi 0.8.0", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags", + "crossterm_winapi 0.9.0", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" +dependencies = [ + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1198,7 +1254,7 @@ dependencies = [ [[package]] name = "ethers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1213,7 +1269,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "0.1.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "ethers-core", "once_cell", @@ -1224,7 +1280,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -1242,7 +1298,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "Inflector", "anyhow", @@ -1265,7 +1321,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -1279,7 +1335,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "arrayvec 0.7.2", "bytes", @@ -1307,7 +1363,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "0.2.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "ethers-core", "reqwest", @@ -1320,7 +1376,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "async-trait", "ethers-contract", @@ -1343,7 +1399,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "async-trait", "auto_impl", @@ -1373,7 +1429,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "async-trait", "coins-bip32", @@ -1396,7 +1452,7 @@ dependencies = [ [[package]] name = "ethers-solc" version = "0.1.0" -source = "git+https://github.com/gakonst/ethers-rs#48bd3f13e23722de4a6d0a0b80307fa3214a87ce" +source = "git+https://github.com/gakonst/ethers-rs#1287614e532662f0f2bf9caed24679a2b0843771" dependencies = [ "colored", "dunce", @@ -1546,6 +1602,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + [[package]] name = "ff" version = "0.11.0" @@ -1657,6 +1722,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "ui", ] [[package]] @@ -2154,9 +2220,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg 1.0.1", "hashbrown 0.11.2", @@ -3863,6 +3929,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4088,13 +4175,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -4417,6 +4504,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.20.0", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "tungstenite" version = "0.16.0" @@ -4445,6 +4545,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "ui" +version = "0.1.0" +dependencies = [ + "crossterm 0.22.1", + "ethers", + "evm-adapters", + "eyre", + "hex", + "tui", +] + [[package]] name = "uint" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index af479da93..6c4951049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,4 @@ opt-level = "z" lto = true codegen-units = 1 panic = "abort" -debug = true +debug = true \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 24dcd39f1..31a29bcb4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,6 +12,7 @@ foundry-utils = { path = "../utils" } forge = { path = "../forge" } cast = { path = "../cast" } evm-adapters = { path = "../evm-adapters" } +ui = { path = "../ui" } dunce = "1.0.2" # ethers = "0.5" ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index 6599b50ab..b6672787f 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -10,6 +10,7 @@ pub mod verify; use crate::opts::forge::ContractInfo; use ethers::{ abi::Abi, + prelude::Graph, solc::{ artifacts::{Source, Sources}, cache::SolFilesCache, @@ -54,6 +55,38 @@ If you are in a subdirectory in a Git repository, try adding `--root .`"#, Ok(output) } +/// Manually compile a project with added sources +pub fn manual_compile( + project: &Project, + added_sources: Vec, +) -> eyre::Result> { + let mut sources = project.paths.read_input_files()?; + sources.extend(Source::read_all_files(added_sources)?); + println!("compiling..."); + if project.auto_detect { + tracing::trace!("using solc auto detection to compile sources"); + let output = project.svm_compile(sources)?; + if output.has_compiler_errors() { + // return the diagnostics error back to the user. + eyre::bail!(output.to_string()) + } + return Ok(output) + } + + let mut solc = project.solc.clone(); + if !project.allowed_lib_paths.is_empty() { + solc = solc.arg("--allow-paths").arg(project.allowed_lib_paths.to_string()); + } + + let sources = Graph::resolve_sources(&project.paths, sources)?.into_sources(); + let output = project.compile_with_version(&solc, sources)?; + if output.has_compiler_errors() { + // return the diagnostics error back to the user. + eyre::bail!(output.to_string()) + } + Ok(output) +} + /// Given a project and its compiled artifacts, proceeds to return the ABI, Bytecode and /// Runtime Bytecode of the given contract. pub fn read_artifact( diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index dfe153160..3076879d5 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -1,23 +1,26 @@ use crate::{ - cmd::{compile, Cmd}, - opts::forge::{CompilerArgs, EvmOpts}, + cmd::{build::BuildArgs, compile, manual_compile, Cmd}, + opts::forge::EvmOpts, }; +use ethers::abi::Abi; use forge::ContractRunner; use foundry_utils::IntoFunction; -use std::path::PathBuf; +use std::{collections::BTreeMap, path::PathBuf}; use structopt::StructOpt; +use ui::{TUIExitReason, Tui, Ui}; -use ethers::{ - prelude::artifacts::CompactContract, - solc::{ - artifacts::{Optimizer, Settings}, - Project, ProjectPathsConfig, SolcConfig, - }, +use ethers::solc::{ + artifacts::{Optimizer, Settings}, + MinimalCombinedArtifacts, Project, ProjectPathsConfig, SolcConfig, }; use evm_adapters::Evm; use ansi_term::Colour; +use ethers::{ + prelude::{artifacts::ContractBytecode, Artifact}, + solc::artifacts::{CompactContractSome, ContractBytecodeSome}, +}; #[derive(Debug, Clone, StructOpt)] pub struct RunArgs { @@ -25,30 +28,24 @@ pub struct RunArgs { pub path: PathBuf, #[structopt(flatten)] - pub compiler: CompilerArgs, + pub evm_opts: EvmOpts, #[structopt(flatten)] - pub evm_opts: EvmOpts, + opts: BuildArgs, #[structopt( long, short, - help = "the function you want to call on the script contract, defaults to run()" + help = "the contract you want to call and deploy, only necessary if there are more than 1 contract (Interfaces do not count) definitions on the script" )] - pub sig: Option, + pub target_contract: Option, #[structopt( long, short, - help = "the contract you want to call and deploy, only necessary if there are more than 1 contract (Interfaces do not count) definitions on the script" - )] - pub contract: Option, - - #[structopt( - help = "if set to true, skips auto-detecting solc and uses what is in the user's $PATH ", - long + help = "the function you want to call on the script contract, defaults to run()" )] - pub no_auto_detect: bool, + pub sig: Option, } impl Cmd for RunArgs { @@ -58,16 +55,35 @@ impl Cmd for RunArgs { #[cfg(not(feature = "sputnik-evm"))] unimplemented!("`run` does not work with EVMs other than Sputnik yet"); + let mut evm_opts = self.evm_opts.clone(); + if evm_opts.debug { + evm_opts.verbosity = 3; + } + let func = IntoFunction::into(self.sig.as_deref().unwrap_or("run()")); - let (abi, bytecode, _) = self.build()?.into_parts(); + let BuildOutput { project, contract, highlevel_known_contracts, sources } = self.build()?; + + let known_contracts = highlevel_known_contracts + .iter() + .map(|(name, c)| { + ( + name.clone(), + ( + c.abi.clone(), + c.deployed_bytecode.clone().into_bytes().expect("not bytecode").to_vec(), + ), + ) + }) + .collect::)>>(); + + let CompactContractSome { abi, bin, .. } = contract; // this should never fail if compilation was successful - let abi = abi.unwrap(); - let bytecode = bytecode.unwrap(); + let bytecode = bin.into_bytes().unwrap(); // 2. instantiate the EVM w forked backend if needed / pre-funded account(s) - let mut cfg = crate::utils::sputnik_cfg(self.compiler.evm_version); + let mut cfg = crate::utils::sputnik_cfg(self.opts.compiler.evm_version); let vicinity = self.evm_opts.vicinity()?; - let mut evm = crate::utils::sputnik_helpers::evm(&self.evm_opts, &mut cfg, &vicinity)?; + let mut evm = crate::utils::sputnik_helpers::evm(&evm_opts, &mut cfg, &vicinity)?; // 3. deploy the contract let (addr, _, _, logs) = evm.deploy(self.evm_opts.sender, bytecode, 0u32.into())?; @@ -76,38 +92,114 @@ impl Cmd for RunArgs { let mut runner = ContractRunner::new(&mut evm, &abi, addr, Some(self.evm_opts.sender), &logs); - // 5. run the test function - let result = runner.run_test(&func, false, None)?; + // 5. run the test function & potentially the setup + let needs_setup = abi.functions().any(|func| func.name == "setUp"); + let result = runner.run_test(&func, needs_setup, Some(&known_contracts))?; + + if self.evm_opts.debug { + // 6. Boot up debugger + let source_code: BTreeMap = sources + .iter() + .map(|(id, path)| { + if let Some(resolved) = + project.paths.resolve_library_import(&PathBuf::from(path)) + { + ( + *id, + std::fs::read_to_string(resolved).expect(&*format!( + "Something went wrong reading the source file: {:?}", + path + )), + ) + } else { + ( + *id, + std::fs::read_to_string(path).expect(&*format!( + "Something went wrong reading the source file: {:?}", + path + )), + ) + } + }) + .collect(); + + let calls = evm.debug_calls(); + println!("debugging"); + let index = if needs_setup && calls.len() > 1 { 1 } else { 0 }; + let mut flattened = Vec::new(); + calls[index].flatten(0, &mut flattened); + flattened = flattened[1..].to_vec(); + let tui = Tui::new( + flattened, + 0, + result.identified_contracts.expect("debug but not verbosity"), + highlevel_known_contracts, + source_code, + )?; + match tui.start().expect("Failed to start tui") { + TUIExitReason::CharExit => return Ok(()), + } + } else if evm_opts.verbosity > 2 { + // support traces + if let (Some(traces), Some(identified_contracts)) = + (&result.traces, &result.identified_contracts) + { + if !result.success && evm_opts.verbosity == 3 || evm_opts.verbosity > 3 { + let mut ident = identified_contracts.clone(); + if evm_opts.verbosity > 4 || !result.success { + // print setup calls as well + traces.iter().for_each(|trace| { + trace.pretty_print(0, &known_contracts, &mut ident, runner.evm, ""); + }); + } else if !traces.is_empty() { + traces.last().expect("no last but not empty").pretty_print( + 0, + &known_contracts, + &mut ident, + runner.evm, + "", + ); + } + } - // 6. print the result nicely - if result.success { - println!("{}", Colour::Green.paint("Script ran successfully.")); + println!(); + } } else { - println!("{}", Colour::Red.paint("Script failed.")); + // 6. print the result nicely + if result.success { + println!("{}", Colour::Green.paint("Script ran successfully.")); + } else { + println!("{}", Colour::Red.paint("Script failed.")); + } + + println!("Gas Used: {}", result.gas_used); + println!("== Logs == "); + result.logs.iter().for_each(|log| println!("{}", log)); } - println!("Gas Used: {}", result.gas_used); - println!("== Logs == "); - result.logs.iter().for_each(|log| println!("{}", log)); Ok(()) } } +pub struct BuildOutput { + pub project: Project, + pub contract: CompactContractSome, + pub highlevel_known_contracts: BTreeMap, + pub sources: BTreeMap, +} + impl RunArgs { - /// Compiles the file with auto-detection and compiler params. - // TODO: This is too verbose. We definitely want an easier way to do "take this file, detect - // its solc version and give me all its ABIs & Bytecodes in memory w/o touching disk". - pub fn build(&self) -> eyre::Result { + fn target_project(&self) -> eyre::Result> { let paths = ProjectPathsConfig::builder().root(&self.path).sources(&self.path).build()?; let optimizer = Optimizer { - enabled: Some(self.compiler.optimize), - runs: Some(self.compiler.optimize_runs as usize), + enabled: Some(self.opts.compiler.optimize), + runs: Some(self.opts.compiler.optimize_runs as usize), }; let solc_settings = Settings { optimizer, - evm_version: Some(self.compiler.evm_version), + evm_version: Some(self.opts.compiler.evm_version), ..Default::default() }; let solc_cfg = SolcConfig::builder().settings(solc_settings).build()?; @@ -121,45 +213,104 @@ impl RunArgs { .no_artifacts() // no cache .ephemeral(); - if self.no_auto_detect { + if self.opts.no_auto_detect { builder = builder.no_auto_detect(); } - let project = builder.build()?; - let output = compile(&project)?; + Ok(builder.build()?) + } + + /// Compiles the file with auto-detection and compiler params. + pub fn build(&self) -> eyre::Result { + let root = dunce::canonicalize(&self.path)?; + let (project, output) = if let Ok(mut project) = self.opts.project() { + // TODO: caching causes no output until https://github.com/gakonst/ethers-rs/issues/727 + // is fixed + project.cached = false; + project.no_artifacts = true; + // target contract may not be in the compilation path, add it and manually compile + match manual_compile(&project, vec![root.clone()]) { + Ok(output) => (project, output), + Err(e) => { + println!("No extra contracts compiled {:?}", e); + let mut target_project = self.target_project()?; + target_project.cached = false; + target_project.no_artifacts = true; + let res = compile(&target_project)?; + (target_project, res) + } + } + } else { + let mut target_project = self.target_project()?; + target_project.cached = false; + target_project.no_artifacts = true; + let res = compile(&target_project)?; + (target_project, res) + }; + println!("success."); // get the contracts - let contracts = output.output(); + let (sources, contracts) = output.output().split(); // get the specific contract - let contract = if let Some(ref contract) = self.contract { - let contract = contracts.find(contract).ok_or_else(|| { - eyre::Error::msg("contract not found, did you type the name wrong?") - })?; - CompactContract::from(contract) + let contract_bytecode = if let Some(contract_name) = self.target_contract.clone() { + let contract_bytecode: ContractBytecode = contracts + .0 + .get(root.to_str().expect("OsString from path")) + .ok_or_else(|| { + eyre::Error::msg( + "contract path not found; This is likely a bug, please report it", + ) + })? + .get(&contract_name) + .ok_or_else(|| { + eyre::Error::msg("contract not found, did you type the name wrong?") + })? + .clone() + .into(); + contract_bytecode.unwrap() } else { - let mut contracts = contracts.contracts_into_iter().filter(|(_, contract)| { - // TODO: Should have a helper function for finding if a contract's bytecode is - // empty or not. - match contract.evm { - Some(ref evm) => match evm.bytecode { - Some(ref bytecode) => bytecode - .object - .as_bytes() - .map(|x| !x.as_ref().is_empty()) - .unwrap_or(false), - _ => false, - }, - _ => false, + let contract = contracts + .0 + .get(root.to_str().expect("OsString from path")) + .ok_or_else(|| { + eyre::Error::msg( + "contract path not found; This is likely a bug, please report it", + ) + })? + .clone() + .into_iter() + .filter_map(|(name, c)| { + let c: ContractBytecode = c.into(); + ContractBytecodeSome::try_from(c).ok().map(|c| (name, c)) + }) + .find(|(_, c)| c.bytecode.object.is_non_empty_bytecode()) + .ok_or_else(|| eyre::Error::msg("no contract found"))?; + contract.1 + }; + + let contract = contract_bytecode.into_compact_contract().unwrap(); + + let mut highlevel_known_contracts = BTreeMap::new(); + + // build the entire highlevel_known_contracts based on all compiled contracts + contracts.0.into_iter().for_each(|(src, mapping)| { + mapping.into_iter().for_each(|(name, c)| { + let cb: ContractBytecode = c.into(); + if let Ok(cbs) = ContractBytecodeSome::try_from(cb) { + if highlevel_known_contracts.contains_key(&name) { + highlevel_known_contracts.insert(src.to_string() + ":" + &name, cbs); + } else { + highlevel_known_contracts.insert(name, cbs); + } } }); - let contract = contracts.next().ok_or_else(|| eyre::Error::msg("no contract found"))?.1; - if contracts.peekable().peek().is_some() { - eyre::bail!( - ">1 contracts found, please provide a contract name to choose one of them" - ) - } - CompactContract::from(contract) - }; - Ok(contract) + }); + + Ok(BuildOutput { + project, + contract, + highlevel_known_contracts, + sources: sources.into_ids().collect(), + }) } } diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index aad7d4d97..8f84732d8 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -170,6 +170,9 @@ pub struct EvmOpts { parse(from_occurrences) )] pub verbosity: u8, + + #[structopt(help = "enable debugger", long)] + pub debug: bool, } impl EvmOpts { diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 3307fa330..089b1781d 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -147,6 +147,7 @@ pub mod sputnik_helpers { &*PRECOMPILES_MAP, opts.ffi, opts.verbosity > 2, + opts.debug, )) } } diff --git a/cli/testdata/run_test.sol b/cli/testdata/run_test.sol new file mode 100644 index 000000000..5ac4e1fdf --- /dev/null +++ b/cli/testdata/run_test.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.7.6; + +interface ERC20 { + function balanceOf(address) external view returns (uint256); + function deposit() payable external; +} + +interface VM { + function startPrank(address) external; +} + +contract C { + ERC20 weth = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + VM constant vm = VM(address(bytes20(uint160(uint256(keccak256('hevm cheat code')))))); + address who = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; + + event log_uint(uint256); + + function run() external { + // impersonate the account + vm.startPrank(who); + + uint256 balanceBefore = weth.balanceOf(who); + emit log_uint(balanceBefore); + + weth.deposit{value: 15 ether}(); + + uint256 balanceAfter = weth.balanceOf(who); + emit log_uint(balanceAfter); + + } +} \ No newline at end of file diff --git a/evm-adapters/src/evmodin.rs b/evm-adapters/src/evmodin.rs index 0c19c1751..e403ecf8e 100644 --- a/evm-adapters/src/evmodin.rs +++ b/evm-adapters/src/evmodin.rs @@ -1,5 +1,8 @@ use crate::Evm; +#[cfg(feature = "sputnik")] +use crate::sputnik::cheatcodes::debugger::DebugArena; + use ethers::types::{Address, Bytes, U256}; use evmodin::{tracing::Tracer, AnalyzedCode, CallKind, Host, Message, Revision, StatusCode}; @@ -71,6 +74,12 @@ impl Evm for EvmOdin { false } + /// Grabs debug steps + #[cfg(feature = "sputnik")] + fn debug_calls(&self) -> Vec { + vec![] + } + fn initialize_contracts>(&mut self, contracts: I) { contracts.into_iter().for_each(|(address, bytecode)| { self.host.set_code(address, bytecode.0); diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index ddd7e62ec..7a4b6084f 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -2,6 +2,8 @@ #[cfg(feature = "sputnik")] /// Abstraction over [Sputnik EVM](https://github.com/rust-blockchain/evm) pub mod sputnik; +#[cfg(feature = "sputnik")] +use crate::sputnik::cheatcodes::debugger::DebugArena; /// Abstraction over [evmodin](https://github.com/rust-blockchain/evm) #[cfg(feature = "evmodin")] @@ -9,6 +11,7 @@ pub mod evmodin; mod blocking_provider; use crate::call_tracing::CallTraceArena; + pub use blocking_provider::BlockingProvider; pub mod fuzz; @@ -82,6 +85,10 @@ pub trait Evm { /// Returns whether tracing is enabled fn tracing_enabled(&self) -> bool; + /// Grabs debug steps + #[cfg(feature = "sputnik")] + fn debug_calls(&self) -> Vec; + /// Gets all logs from the execution, regardless of reverts fn all_logs(&self) -> Vec; diff --git a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs index 799e4a844..f70fd9c77 100644 --- a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs +++ b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs @@ -8,6 +8,7 @@ use crate::{ sputnik::{cheatcodes::memory_stackstate_owned::ExpectedEmit, Executor, SputnikExecutor}, Evm, }; +use std::collections::BTreeMap; use sputnik::{ backend::Backend, @@ -16,7 +17,7 @@ use sputnik::{ StackState, StackSubstateMetadata, }, gasometer, Capture, Config, Context, CreateScheme, ExitError, ExitReason, ExitRevert, - ExitSucceed, Handler, Runtime, Transfer, + ExitSucceed, Handler, Memory, Opcode, Runtime, Transfer, }; use std::{process::Command, rc::Rc}; @@ -29,7 +30,10 @@ use ethers::{ }; use std::{convert::Infallible, str::FromStr}; -use crate::sputnik::cheatcodes::patch_hardhat_console_log_selector; +use crate::sputnik::cheatcodes::{ + debugger::{CheatOp, DebugArena, DebugNode, DebugStep, OpCode}, + patch_hardhat_console_log_selector, +}; use once_cell::sync::Lazy; use ethers::abi::Tokenize; @@ -164,6 +168,10 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor Vec { + self.state().debug_steps.clone() + } + fn gas_left(&self) -> U256 { // NB: We do this to avoid `function cannot return without recursing` U256::from(self.state().metadata().gasometer().gas()) @@ -309,6 +317,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> precompiles: &'b P, enable_ffi: bool, enable_trace: bool, + debug: bool, ) -> Self { // make this a cheatcode-enabled backend let backend = CheatcodeBackend { backend, cheats: Default::default() }; @@ -316,7 +325,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> // create the memory stack state (owned, so that we can modify the backend via // self.state_mut on the transact_call fn) let metadata = StackSubstateMetadata::new(gas_limit, config); - let state = MemoryStackStateOwned::new(metadata, backend, enable_trace); + let state = MemoryStackStateOwned::new(metadata, backend, enable_trace, debug); // create the executor and wrap it with the cheatcode handler let executor = StackExecutor::new_with_precompiles(state, config, precompiles); @@ -461,6 +470,27 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> Capture::Exit((ExitReason::Succeed(ExitSucceed::Stopped), vec![])) } + /// Adds CheatOp to the latest DebugArena + fn add_debug(&mut self, cheatop: CheatOp) { + if self.state().debug_enabled { + let depth = + if let Some(depth) = self.state().metadata().depth() { depth + 1 } else { 0 }; + self.state_mut().debug_mut().push_node( + 0, + DebugNode { + address: *CHEATCODE_ADDRESS, + depth, + steps: vec![DebugStep { + op: OpCode::from(cheatop), + memory: Memory::new(0), + ..Default::default() + }], + ..Default::default() + }, + ); + } + } + /// Given a transaction's calldata, it tries to parse it as an [`HEVM cheatcode`](super::HEVM) /// call and modify the state accordingly. fn apply_cheatcode( @@ -472,7 +502,6 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> let pre_index = self.state().trace_index; let trace = self.start_trace(*CHEATCODE_ADDRESS, input.clone(), 0.into(), false); // Get a mutable ref to the state so we can apply the cheats - let state = self.state_mut(); let decoded = match HEVMCalls::decode(&input) { Ok(inner) => inner, Err(err) => return evm_error(&err.to_string()), @@ -480,21 +509,27 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> match decoded { HEVMCalls::Warp(inner) => { - state.backend.cheats.block_timestamp = Some(inner.0); + self.add_debug(CheatOp::WARP); + self.state_mut().backend.cheats.block_timestamp = Some(inner.0); } HEVMCalls::Roll(inner) => { - state.backend.cheats.block_number = Some(inner.0); + self.add_debug(CheatOp::ROLL); + self.state_mut().backend.cheats.block_number = Some(inner.0); } HEVMCalls::Fee(inner) => { - state.backend.cheats.block_base_fee_per_gas = Some(inner.0); + self.add_debug(CheatOp::FEE); + self.state_mut().backend.cheats.block_base_fee_per_gas = Some(inner.0); } HEVMCalls::Store(inner) => { - state.set_storage(inner.0, inner.1.into(), inner.2.into()); + self.add_debug(CheatOp::STORE); + self.state_mut().set_storage(inner.0, inner.1.into(), inner.2.into()); } HEVMCalls::Load(inner) => { - res = state.storage(inner.0, inner.1.into()).0.to_vec(); + self.add_debug(CheatOp::LOAD); + res = self.state_mut().storage(inner.0, inner.1.into()).0.to_vec(); } HEVMCalls::Ffi(inner) => { + self.add_debug(CheatOp::FFI); let args = inner.0; // if FFI is not explicitly enabled at runtime, do not let this be called // (we could have an FFI cheatcode executor instead but feels like @@ -522,6 +557,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> res = ethers::abi::encode(&[Token::Bytes(decoded.to_vec())]); } HEVMCalls::Addr(inner) => { + self.add_debug(CheatOp::ADDR); let sk = inner.0; if sk.is_zero() { return evm_error("Bad Cheat Code. Private Key cannot be 0.") @@ -537,6 +573,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> res = ethers::abi::encode(&[Token::Address(addr)]); } HEVMCalls::Sign(inner) => { + self.add_debug(CheatOp::SIGN); let sk = inner.0; let digest = inner.1; if sk.is_zero() { @@ -569,6 +606,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> ])]); } HEVMCalls::Prank(inner) => { + self.add_debug(CheatOp::PRANK); let caller = inner.0; if let Some((orginal_pranker, caller, depth)) = self.state().msg_sender { let start_prank_depth = if let Some(depth) = self.state().metadata().depth() { @@ -586,6 +624,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> self.state_mut().next_msg_sender = Some(caller); } HEVMCalls::StartPrank(inner) => { + self.add_debug(CheatOp::STARTPRANK); // startPrank works by using frame depth to determine whether to overwrite // msg.sender if we set a prank caller at a particular depth, it // will continue to use the prank caller for any subsequent calls @@ -610,9 +649,11 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } } HEVMCalls::StopPrank(_) => { + self.add_debug(CheatOp::STOPPRANK); self.state_mut().msg_sender = None; } HEVMCalls::ExpectRevert(inner) => { + self.add_debug(CheatOp::EXPECTREVERT); if self.state().expected_revert.is_some() { return evm_error( "You must call another function prior to expecting a second revert.", @@ -622,20 +663,24 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } } HEVMCalls::Deal(inner) => { + self.add_debug(CheatOp::DEAL); let who = inner.0; let value = inner.1; - state.reset_balance(who); - state.deposit(who, value); + self.state_mut().reset_balance(who); + self.state_mut().deposit(who, value); } HEVMCalls::Etch(inner) => { + self.add_debug(CheatOp::ETCH); let who = inner.0; let code = inner.1; - state.set_code(who, code.to_vec()); + self.state_mut().set_code(who, code.to_vec()); } HEVMCalls::Record(_) => { + self.add_debug(CheatOp::RECORD); self.state_mut().accesses = Some(Default::default()); } HEVMCalls::Accesses(inner) => { + self.add_debug(CheatOp::ACCESSES); let address = inner.0; // we dont reset all records in case user wants to query multiple address if let Some(record_accesses) = &self.state().accesses { @@ -665,6 +710,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } } HEVMCalls::ExpectEmit(inner) => { + self.add_debug(CheatOp::EXPECTEMIT); let expected_emit = ExpectedEmit { depth: if let Some(depth) = self.state().metadata().depth() { depth + 1 @@ -694,6 +740,187 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } } + /// Executes the call/create while also tracking the state of the machine (including opcodes) + fn debug_execute( + &mut self, + runtime: &mut Runtime, + address: Address, + code: Rc>, + creation: bool, + ) -> ExitReason { + let depth = if let Some(depth) = self.state().metadata().depth() { depth + 1 } else { 0 }; + + match self.debug_run(runtime, address, depth, code, creation) { + Capture::Exit(s) => s, + Capture::Trap(_) => unreachable!("Trap is Infallible"), + } + } + + /// Does *not* actually perform a step, just records the debug information for the step + fn debug_step( + &mut self, + runtime: &mut Runtime, + code: Rc>, + steps: &mut Vec, + pc_ic: Rc>, + ) -> bool { + // grab the pc, opcode and stack + let pc = runtime.machine().position().as_ref().map(|p| *p).unwrap_or_default(); + let mut push_bytes = None; + + if let Some((op, stack)) = runtime.machine().inspect() { + // wrap the op to make it compatible with opcode extensions for cheatops + let wrapped_op = OpCode::from(op); + + // check how big the push size is, and grab the pushed bytes if possible + if let Some(push_size) = wrapped_op.push_size() { + let push_start = pc + 1; + let push_end = pc + 1 + push_size as usize; + if push_end < code.len() { + push_bytes = Some(code[push_start..push_end].to_vec()); + } else { + panic!("PUSH{} exceeds limit of codesize", push_size) + } + } + + // grab the stack data and reverse it (last element is "top" of stack) + let mut stack = stack.data().clone(); + stack.reverse(); + // push the step into the vector + steps.push(DebugStep { + pc, + stack, + memory: runtime.machine().memory().clone(), + op: wrapped_op, + push_bytes, + ic: *pc_ic.get(&pc).as_ref().copied().unwrap_or(&0usize), + total_gas_used: self.handler.used_gas(), + }); + match op { + Opcode::CREATE | + Opcode::CREATE2 | + Opcode::CALL | + Opcode::CALLCODE | + Opcode::DELEGATECALL | + Opcode::STATICCALL => { + // this would create an interrupt, have `debug_run` construct a new vec + // to commit the current vector of steps into the debugarena + // this maintains the call heirarchy correctly + true + } + _ => false, + } + } else { + // failure case. + let mut stack = runtime.machine().stack().data().clone(); + stack.reverse(); + steps.push(DebugStep { + pc, + stack, + memory: runtime.machine().memory().clone(), + op: OpCode::from(Opcode::INVALID), + push_bytes, + ic: *pc_ic.get(&pc).as_ref().copied().unwrap_or(&0usize), + total_gas_used: self.handler.used_gas(), + }); + true + } + } + + fn debug_run( + &mut self, + runtime: &mut Runtime, + address: Address, + depth: usize, + code: Rc>, + creation: bool, + ) -> Capture { + let mut done = false; + let mut res = Capture::Exit(ExitReason::Succeed(ExitSucceed::Returned)); + let mut steps = Vec::new(); + // grab the debug instruction pointers for either construct or runtime bytecode + let dip = if creation { + &mut self.state_mut().debug_instruction_pointers.0 + } else { + &mut self.state_mut().debug_instruction_pointers.1 + }; + // get the program counter => instruction counter mapping from memory or construct it + let ics = if let Some(pc_ic) = dip.get(&address) { + // grabs an Rc of an already created pc -> ic mapping + pc_ic.clone() + } else { + // builds a program counter to instruction counter map + // basically this strips away bytecodes to make it work + // with the sourcemap output from the solc compiler + let mut pc_ic: BTreeMap = BTreeMap::new(); + + let mut i = 0; + let mut push_ctr = 0usize; + while i < code.len() { + let wrapped_op = OpCode::from(Opcode(code[i])); + pc_ic.insert(i, i - push_ctr); + + if let Some(push_size) = wrapped_op.push_size() { + i += push_size as usize; + i += 1; + push_ctr += push_size as usize; + } else { + i += 1; + } + } + let pc_ic = Rc::new(pc_ic); + + dip.insert(address, pc_ic.clone()); + pc_ic + }; + while !done { + // debug step doesnt actually execute the step, it just peeks into the machine + // will return true or false, which signifies whether to push the steps + // as a node and reset the steps vector or not + if self.debug_step(runtime, code.clone(), &mut steps, ics.clone()) && !steps.is_empty() + { + self.state_mut().debug_mut().push_node( + 0, + DebugNode { + address, + depth, + steps: steps.clone(), + creation, + ..Default::default() + }, + ); + steps = Vec::new(); + } + // actually executes the opcode step + let r = runtime.step(self); + match r { + Ok(()) => {} + Err(e) => { + done = true; + // we wont hit an interrupt when we finish stepping + // so we have add the accumulated steps as if debug_step returned true + if !steps.is_empty() { + self.state_mut().debug_mut().push_node( + 0, + DebugNode { + address, + depth, + steps: steps.clone(), + creation, + ..Default::default() + }, + ); + } + match e { + Capture::Exit(s) => res = Capture::Exit(s), + Capture::Trap(_) => unreachable!("Trap is Infallible"), + } + } + } + } + res + } + fn start_trace( &mut self, address: H160, @@ -868,8 +1095,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> // each cfg is about 200 bytes, is this a lot to clone? why does this error // not manifest upstream? let config = self.config().clone(); - let mut runtime = Runtime::new(Rc::new(code), Rc::new(input), context, &config); - let reason = self.execute(&mut runtime); + let mut runtime; + let reason = if self.state().debug_enabled { + let code = Rc::new(code); + runtime = Runtime::new(code.clone(), Rc::new(input), context, &config); + self.debug_execute(&mut runtime, code_address, code, false) + } else { + runtime = Runtime::new(Rc::new(code), Rc::new(input), context, &config); + self.execute(&mut runtime) + }; // // log::debug!(target: "evm", "Call execution using address {}: {:?}", code_address, // reason); @@ -1008,9 +1242,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P> } let config = self.config().clone(); - let mut runtime = Runtime::new(Rc::new(init_code), Rc::new(Vec::new()), context, &config); - - let reason = self.execute(&mut runtime); + let mut runtime; + let reason = if self.state().debug_enabled { + let code = Rc::new(init_code); + runtime = Runtime::new(code.clone(), Rc::new(Vec::new()), context, &config); + self.debug_execute(&mut runtime, address, code, true) + } else { + runtime = Runtime::new(Rc::new(init_code), Rc::new(Vec::new()), context, &config); + self.execute(&mut runtime) + }; // log::debug!(target: "evm", "Create execution using address {}: {:?}", address, reason); match reason { @@ -1145,6 +1385,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a new_context, ); + // handle expected emits if !self.state_mut().expected_emits.is_empty() && !self .state() diff --git a/evm-adapters/src/sputnik/cheatcodes/debugger.rs b/evm-adapters/src/sputnik/cheatcodes/debugger.rs new file mode 100644 index 000000000..92dc4b9cb --- /dev/null +++ b/evm-adapters/src/sputnik/cheatcodes/debugger.rs @@ -0,0 +1,400 @@ +use sputnik::{Memory, Opcode}; + +use ethers::types::{Address, H256}; + +use std::{borrow::Cow, fmt::Display}; + +#[derive(Debug, Clone)] +/// An arena of `DebugNode`s +pub struct DebugArena { + /// The arena of nodes + pub arena: Vec, + /// The entry index, denoting the first node's index in the arena + pub entry: usize, +} + +impl Default for DebugArena { + fn default() -> Self { + DebugArena { arena: vec![Default::default()], entry: 0 } + } +} + +impl DebugArena { + /// Pushes a new debug node into the arena + pub fn push_node(&mut self, entry: usize, mut new_node: DebugNode) { + match new_node.depth { + // The entry node, just update it + 0 => { + self.arena[entry] = new_node; + } + // we found the parent node, add the new node as a child + _ if self.arena[entry].depth == new_node.depth - 1 => { + new_node.idx = self.arena.len(); + new_node.location = self.arena[entry].children.len(); + self.arena[entry].children.push(new_node.idx); + self.arena.push(new_node); + } + // we haven't found the parent node, go deeper + _ => self.push_node( + *self.arena[entry].children.last().expect("Disconnected debug node"), + new_node, + ), + } + } + + /// Recursively traverses the tree of debug step nodes and flattens it into a + /// vector where each element contains + /// 1. the address of the contract being executed + /// 2. a vector of all the debug steps along that contract's execution path. + /// + /// This then makes it easy to pretty print the execution steps. + pub fn flatten(&self, entry: usize, flattened: &mut Vec<(Address, Vec, bool)>) { + let node = &self.arena[entry]; + flattened.push((node.address, node.steps.clone(), node.creation)); + node.children.iter().for_each(|child| { + self.flatten(*child, flattened); + }); + } +} + +#[derive(Default, Debug, Clone)] +/// A node in the arena +pub struct DebugNode { + /// Parent node index in the arena + pub parent: Option, + /// Children node indexes in the arena + pub children: Vec, + /// Location in parent + pub location: usize, + /// This node's index in the arena + pub idx: usize, + /// Address context + pub address: Address, + /// Depth + pub depth: usize, + /// The debug steps + pub steps: Vec, + /// Contract Creation + pub creation: bool, +} + +impl DebugNode { + pub fn new(address: Address, depth: usize, steps: Vec) -> Self { + Self { address, depth, steps, ..Default::default() } + } +} + +/// A `DebugStep` is a snapshot of the EVM's runtime state. It holds the current program counter +/// (where in the program you are), the stack and memory (prior to the opcodes execution), any bytes +/// to be pushed onto the stack, and the instruction counter for use with sourcemaps +#[derive(Debug, Clone)] +pub struct DebugStep { + /// Program Counter + pub pc: usize, + /// Stack *prior* to running this struct's associated opcode + pub stack: Vec, + /// Memory *prior* to running this struct's associated opcode + pub memory: Memory, + /// Opcode to be executed + pub op: OpCode, + /// Optional bytes that are being pushed onto the stack + pub push_bytes: Option>, + /// Instruction counter, used for sourcemap mapping to source code + pub ic: usize, + /// Cumulative gas usage + pub total_gas_used: u64, +} + +impl Default for DebugStep { + fn default() -> Self { + Self { + pc: 0, + stack: vec![], + memory: Memory::new(0), + op: OpCode(Opcode::INVALID, None), + push_bytes: None, + ic: 0, + total_gas_used: 0, + } + } +} + +impl DebugStep { + /// Pretty print the step's opcode + pub fn pretty_opcode(&self) -> String { + if let Some(push_bytes) = &self.push_bytes { + format!("{}(0x{})", self.op, hex::encode(push_bytes)) + } else { + self.op.to_string() + } + } +} + +impl Display for DebugStep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(push_bytes) = &self.push_bytes { + write!( + f, + "pc: {:?}\nop: {}(0x{})\nstack: {:#?}\nmemory: 0x{}\n\n", + self.pc, + self.op, + hex::encode(push_bytes), + self.stack, + hex::encode(self.memory.data()) + ) + } else { + write!( + f, + "pc: {:?}\nop: {}\nstack: {:#?}\nmemory: 0x{}\n\n", + self.pc, + self.op, + self.stack, + hex::encode(self.memory.data()) + ) + } + } +} + +/// CheatOps are `forge` specific identifiers for cheatcodes since cheatcodes don't touch the evm +#[derive(Debug, Copy, Clone)] +pub enum CheatOp { + ROLL, + WARP, + FEE, + STORE, + LOAD, + FFI, + ADDR, + SIGN, + PRANK, + STARTPRANK, + STOPPRANK, + DEAL, + ETCH, + EXPECTREVERT, + RECORD, + ACCESSES, + EXPECTEMIT, +} + +impl From for OpCode { + fn from(cheat: CheatOp) -> OpCode { + OpCode(Opcode(0x0C), Some(cheat)) + } +} + +impl CheatOp { + /// Gets the `CheatOp` as a string for printing purposes + pub const fn name(&self) -> &'static str { + match self { + CheatOp::ROLL => "VM_ROLL", + CheatOp::WARP => "VM_WARP", + CheatOp::FEE => "VM_FEE", + CheatOp::STORE => "VM_STORE", + CheatOp::LOAD => "VM_LOAD", + CheatOp::FFI => "VM_FFI", + CheatOp::ADDR => "VM_ADDR", + CheatOp::SIGN => "VM_SIGN", + CheatOp::PRANK => "VM_PRANK", + CheatOp::STARTPRANK => "VM_STARTPRANK", + CheatOp::STOPPRANK => "VM_STOPPRANK", + CheatOp::DEAL => "VM_DEAL", + CheatOp::ETCH => "VM_ETCH", + CheatOp::EXPECTREVERT => "VM_EXPECTREVERT", + CheatOp::RECORD => "VM_RECORD", + CheatOp::ACCESSES => "VM_ACCESSES", + CheatOp::EXPECTEMIT => "VM_EXPECTEMIT", + } + } +} + +impl Default for CheatOp { + fn default() -> Self { + CheatOp::ROLL + } +} + +#[derive(Debug, Clone, Copy)] +pub struct OpCode(pub Opcode, pub Option); + +impl From for OpCode { + fn from(op: Opcode) -> OpCode { + OpCode(op, None) + } +} + +impl OpCode { + /// Gets the name of the opcode as a string + pub const fn name(&self) -> &'static str { + match self.0 { + Opcode::STOP => "STOP", + Opcode::ADD => "ADD", + Opcode::MUL => "MUL", + Opcode::SUB => "SUB", + Opcode::DIV => "DIV", + Opcode::SDIV => "SDIV", + Opcode::MOD => "MOD", + Opcode::SMOD => "SMOD", + Opcode::ADDMOD => "ADDMOD", + Opcode::MULMOD => "MULMOD", + Opcode::EXP => "EXP", + Opcode::SIGNEXTEND => "SIGNEXTEND", + Opcode::LT => "LT", + Opcode::GT => "GT", + Opcode::SLT => "SLT", + Opcode::SGT => "SGT", + Opcode::EQ => "EQ", + Opcode::ISZERO => "ISZERO", + Opcode::AND => "AND", + Opcode::OR => "OR", + Opcode::XOR => "XOR", + Opcode::NOT => "NOT", + Opcode::BYTE => "BYTE", + Opcode::SHL => "SHL", + Opcode::SHR => "SHR", + Opcode::SAR => "SAR", + Opcode::SHA3 => "KECCAK256", + Opcode::ADDRESS => "ADDRESS", + Opcode::BALANCE => "BALANCE", + Opcode::ORIGIN => "ORIGIN", + Opcode::CALLER => "CALLER", + Opcode::CALLVALUE => "CALLVALUE", + Opcode::CALLDATALOAD => "CALLDATALOAD", + Opcode::CALLDATASIZE => "CALLDATASIZE", + Opcode::CALLDATACOPY => "CALLDATACOPY", + Opcode::CODESIZE => "CODESIZE", + Opcode::CODECOPY => "CODECOPY", + Opcode::GASPRICE => "GASPRICE", + Opcode::EXTCODESIZE => "EXTCODESIZE", + Opcode::EXTCODECOPY => "EXTCODECOPY", + Opcode::RETURNDATASIZE => "RETURNDATASIZE", + Opcode::RETURNDATACOPY => "RETURNDATACOPY", + Opcode::EXTCODEHASH => "EXTCODEHASH", + Opcode::BLOCKHASH => "BLOCKHASH", + Opcode::COINBASE => "COINBASE", + Opcode::TIMESTAMP => "TIMESTAMP", + Opcode::NUMBER => "NUMBER", + Opcode::DIFFICULTY => "DIFFICULTY", + Opcode::GASLIMIT => "GASLIMIT", + Opcode::CHAINID => "CHAINID", + Opcode::SELFBALANCE => "SELFBALANCE", + Opcode::BASEFEE => "BASEFEE", + Opcode::POP => "POP", + Opcode::MLOAD => "MLOAD", + Opcode::MSTORE => "MSTORE", + Opcode::MSTORE8 => "MSTORE8", + Opcode::SLOAD => "SLOAD", + Opcode::SSTORE => "SSTORE", + Opcode::JUMP => "JUMP", + Opcode::JUMPI => "JUMPI", + Opcode::PC => "PC", + Opcode::MSIZE => "MSIZE", + Opcode::GAS => "GAS", + Opcode::JUMPDEST => "JUMPDEST", + Opcode::PUSH1 => "PUSH1", + Opcode::PUSH2 => "PUSH2", + Opcode::PUSH3 => "PUSH3", + Opcode::PUSH4 => "PUSH4", + Opcode::PUSH5 => "PUSH5", + Opcode::PUSH6 => "PUSH6", + Opcode::PUSH7 => "PUSH7", + Opcode::PUSH8 => "PUSH8", + Opcode::PUSH9 => "PUSH9", + Opcode::PUSH10 => "PUSH10", + Opcode::PUSH11 => "PUSH11", + Opcode::PUSH12 => "PUSH12", + Opcode::PUSH13 => "PUSH13", + Opcode::PUSH14 => "PUSH14", + Opcode::PUSH15 => "PUSH15", + Opcode::PUSH16 => "PUSH16", + Opcode::PUSH17 => "PUSH17", + Opcode::PUSH18 => "PUSH18", + Opcode::PUSH19 => "PUSH19", + Opcode::PUSH20 => "PUSH20", + Opcode::PUSH21 => "PUSH21", + Opcode::PUSH22 => "PUSH22", + Opcode::PUSH23 => "PUSH23", + Opcode::PUSH24 => "PUSH24", + Opcode::PUSH25 => "PUSH25", + Opcode::PUSH26 => "PUSH26", + Opcode::PUSH27 => "PUSH27", + Opcode::PUSH28 => "PUSH28", + Opcode::PUSH29 => "PUSH29", + Opcode::PUSH30 => "PUSH30", + Opcode::PUSH31 => "PUSH31", + Opcode::PUSH32 => "PUSH32", + Opcode::DUP1 => "DUP1", + Opcode::DUP2 => "DUP2", + Opcode::DUP3 => "DUP3", + Opcode::DUP4 => "DUP4", + Opcode::DUP5 => "DUP5", + Opcode::DUP6 => "DUP6", + Opcode::DUP7 => "DUP7", + Opcode::DUP8 => "DUP8", + Opcode::DUP9 => "DUP9", + Opcode::DUP10 => "DUP10", + Opcode::DUP11 => "DUP11", + Opcode::DUP12 => "DUP12", + Opcode::DUP13 => "DUP13", + Opcode::DUP14 => "DUP14", + Opcode::DUP15 => "DUP15", + Opcode::DUP16 => "DUP16", + Opcode::SWAP1 => "SWAP1", + Opcode::SWAP2 => "SWAP2", + Opcode::SWAP3 => "SWAP3", + Opcode::SWAP4 => "SWAP4", + Opcode::SWAP5 => "SWAP5", + Opcode::SWAP6 => "SWAP6", + Opcode::SWAP7 => "SWAP7", + Opcode::SWAP8 => "SWAP8", + Opcode::SWAP9 => "SWAP9", + Opcode::SWAP10 => "SWAP10", + Opcode::SWAP11 => "SWAP11", + Opcode::SWAP12 => "SWAP12", + Opcode::SWAP13 => "SWAP13", + Opcode::SWAP14 => "SWAP14", + Opcode::SWAP15 => "SWAP15", + Opcode::SWAP16 => "SWAP16", + Opcode::LOG0 => "LOG0", + Opcode::LOG1 => "LOG1", + Opcode::LOG2 => "LOG2", + Opcode::LOG3 => "LOG3", + Opcode::LOG4 => "LOG4", + Opcode::CREATE => "CREATE", + Opcode::CALL => "CALL", + Opcode::CALLCODE => "CALLCODE", + Opcode::RETURN => "RETURN", + Opcode::DELEGATECALL => "DELEGATECALL", + Opcode::CREATE2 => "CREATE2", + Opcode::STATICCALL => "STATICCALL", + Opcode::REVERT => "REVERT", + Opcode::INVALID => "INVALID", + Opcode::SUICIDE => "SELFDESTRUCT", + _ => { + if let Some(cheat) = self.1 { + cheat.name() + } else { + "UNDEFINED" + } + } + } + } + + /// Optionally return the push size of the opcode if it is a push + pub fn push_size(self) -> Option { + self.0.is_push() + } +} + +impl Display for OpCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = self.name(); + + let n = if name == "UNDEFINED" { + Cow::Owned(format!("UNDEFINED(0x{:02x})", self.0 .0)) + } else { + Cow::Borrowed(name) + }; + write!(f, "{}", n) + } +} diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index e69599db0..5cee85fbf 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -4,14 +4,14 @@ use sputnik::{ ExitError, Transfer, }; -use crate::call_tracing::CallTraceArena; +use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugArena}; use ethers::{ abi::RawLog, types::{H160, H256, U256}, }; -use std::{cell::RefCell, collections::BTreeMap}; +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; #[derive(Clone, Default)] pub struct RecordAccess { @@ -37,16 +37,32 @@ pub struct ExpectedEmit { pub struct MemoryStackStateOwned<'config, B> { pub backend: B, pub substate: MemoryStackSubstate<'config>, + /// Tracing enabled pub trace_enabled: bool, + /// Current call index used for incrementing traces index vec below pub call_index: usize, + /// Temporary value used for putting logs in the correct trace pub trace_index: usize, + /// Arena allocator that holds a tree of traces pub traces: Vec, + /// Expected revert storage of bytes pub expected_revert: Option>, + /// Msg.sender of next call pub next_msg_sender: Option, + /// Tuple of (address that called startPrank, new address to use, depth) pub msg_sender: Option<(H160, H160, usize)>, + /// List of accesses done during a call pub accesses: Option, + /// All logs accumulated (regardless of revert status) pub all_logs: Vec, + /// Expected events by end of the next call pub expected_emits: Vec, + /// Debug enabled + pub debug_enabled: bool, + /// An arena allocator of DebugNodes for debugging purposes + pub debug_steps: Vec, + /// Instruction pointers that maps an address to a mapping of pc to ic + pub debug_instruction_pointers: Dip, } impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { @@ -56,12 +72,17 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { pub fn increment_call_index(&mut self) { self.traces.push(Default::default()); + self.debug_steps.push(Default::default()); self.call_index += 1; } pub fn trace_mut(&mut self) -> &mut CallTraceArena { &mut self.traces[self.call_index] } + pub fn debug_mut(&mut self) -> &mut DebugArena { + &mut self.debug_steps[self.call_index] + } + pub fn trace(&self) -> &CallTraceArena { &self.traces[self.call_index] } @@ -72,8 +93,24 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { } } +/// Debug Instruction pointers: a tuple with 2 maps, the first being for creation +/// sourcemaps, the second for runtime sourcemaps. +/// +/// Each has a structure of (Address => (program_counter => instruction_counter)) +/// For sourcemap usage, we need to convert a program counter to an instruction counter and use the +/// instruction counter as the index into the sourcemap vector. An instruction counter (pointer) is +/// just the program counter minus the sum of push bytes (i.e. PUSH1(0x01), would apply a -1 effect +/// to all subsequent instruction counters) +pub type Dip = + (BTreeMap>>, BTreeMap>>); + impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { - pub fn new(metadata: StackSubstateMetadata<'config>, backend: B, trace_enabled: bool) -> Self { + pub fn new( + metadata: StackSubstateMetadata<'config>, + backend: B, + trace_enabled: bool, + debug_enabled: bool, + ) -> Self { Self { backend, substate: MemoryStackSubstate::new(metadata), @@ -87,6 +124,9 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { accesses: None, all_logs: Default::default(), expected_emits: Default::default(), + debug_enabled, + debug_steps: vec![Default::default()], + debug_instruction_pointers: (BTreeMap::new(), BTreeMap::new()), } } } diff --git a/evm-adapters/src/sputnik/cheatcodes/mod.rs b/evm-adapters/src/sputnik/cheatcodes/mod.rs index 85066b67c..b8cd5b6b3 100644 --- a/evm-adapters/src/sputnik/cheatcodes/mod.rs +++ b/evm-adapters/src/sputnik/cheatcodes/mod.rs @@ -9,6 +9,8 @@ pub use cheatcode_handler::CheatcodeHandler; pub mod backend; +pub mod debugger; + use ethers::types::{Address, Selector, H256, U256}; use once_cell::sync::Lazy; use sputnik::backend::{Backend, MemoryAccount, MemoryBackend}; diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index cd31b18fd..a8e5ab25c 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -1,6 +1,8 @@ use crate::{call_tracing::CallTraceArena, Evm, FAUCET_ACCOUNT}; use ethers::types::{Address, Bytes, U256}; +use crate::sputnik::cheatcodes::debugger::DebugArena; + use sputnik::{ backend::{Backend, MemoryAccount}, executor::stack::{ @@ -91,6 +93,12 @@ where self.executor.tracing_enabled() } + /// Grabs debug steps + #[cfg(feature = "sputnik")] + fn debug_calls(&self) -> Vec { + self.executor.debug_calls() + } + /// given an iterator of contract address to contract bytecode, initializes /// the state with the contract deployed at the specified address fn initialize_contracts>(&mut self, contracts: T) { @@ -227,7 +235,15 @@ pub mod helpers { /// backend and tracing disabled pub fn vm<'a>() -> TestSputnikVM<'a, MemoryBackend<'a>> { let backend = new_backend(&*VICINITY, Default::default()); - Executor::new_with_cheatcodes(backend, GAS_LIMIT, &*CFG, &*PRECOMPILES_MAP, true, false) + Executor::new_with_cheatcodes( + backend, + GAS_LIMIT, + &*CFG, + &*PRECOMPILES_MAP, + true, + false, + false, + ) } /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory @@ -241,6 +257,7 @@ pub mod helpers { &*PRECOMPILES_MAP, true, false, + false, ) } @@ -249,7 +266,42 @@ pub mod helpers { pub fn vm_tracing<'a>(with_contract_limit: bool) -> TestSputnikVM<'a, MemoryBackend<'a>> { let backend = new_backend(&*VICINITY, Default::default()); if with_contract_limit { - Executor::new_with_cheatcodes(backend, GAS_LIMIT, &*CFG, &*PRECOMPILES_MAP, true, true) + Executor::new_with_cheatcodes( + backend, + GAS_LIMIT, + &*CFG, + &*PRECOMPILES_MAP, + true, + true, + false, + ) + } else { + Executor::new_with_cheatcodes( + backend, + GAS_LIMIT, + &*CFG_NO_LMT, + &*PRECOMPILES_MAP, + true, + true, + false, + ) + } + } + + /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory + /// backend and debug enabled, and tracing disabled + pub fn vm_debug<'a>(with_contract_limit: bool) -> TestSputnikVM<'a, MemoryBackend<'a>> { + let backend = new_backend(&*VICINITY, Default::default()); + if with_contract_limit { + Executor::new_with_cheatcodes( + backend, + GAS_LIMIT, + &*CFG, + &*PRECOMPILES_MAP, + true, + false, + true, + ) } else { Executor::new_with_cheatcodes( backend, @@ -257,6 +309,7 @@ pub mod helpers { &*CFG_NO_LMT, &*PRECOMPILES_MAP, true, + false, true, ) } diff --git a/evm-adapters/src/sputnik/mod.rs b/evm-adapters/src/sputnik/mod.rs index b00825dae..10110e749 100644 --- a/evm-adapters/src/sputnik/mod.rs +++ b/evm-adapters/src/sputnik/mod.rs @@ -19,7 +19,7 @@ use sputnik::{ Config, CreateScheme, ExitError, ExitReason, ExitSucceed, }; -use crate::call_tracing::CallTraceArena; +use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugArena}; pub use sputnik as sputnik_evm; use sputnik_evm::executor::stack::PrecompileSet; @@ -66,6 +66,7 @@ pub trait SputnikExecutor { fn expected_revert(&self) -> Option<&[u8]>; fn set_tracing_enabled(&mut self, enabled: bool) -> bool; fn tracing_enabled(&self) -> bool; + fn debug_calls(&self) -> Vec; fn all_logs(&self) -> Vec; fn gas_left(&self) -> U256; fn transact_call( @@ -137,6 +138,10 @@ impl<'a, 'b, S: StackState<'a>, P: PrecompileSet> SputnikExecutor false } + fn debug_calls(&self) -> Vec { + vec![] + } + fn all_logs(&self) -> Vec { vec![] } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index d0a90e0ab..77989c213 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -564,7 +564,7 @@ mod tests { } } - mod evmodin { + mod evmodin_test { use super::*; use ::evmodin::{tracing::NoopTracer, util::mocked_host::MockedHost, Revision}; use evm_adapters::evmodin::EvmOdin; diff --git a/ui/Cargo.toml b/ui/Cargo.toml new file mode 100644 index 000000000..af7268b7a --- /dev/null +++ b/ui/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ui" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# TUI for debug +crossterm = "0.22.1" +tui = { version = "0.16.0", default-features = false, features = ["crossterm"] } +evm-adapters = {path = "../evm-adapters", features = ["sputnik"] } +eyre = "0.6.5" +hex = "0.4.3" +ethers = { git = "https://github.com/gakonst/ethers-rs" } \ No newline at end of file diff --git a/ui/src/lib.rs b/ui/src/lib.rs new file mode 100644 index 000000000..7ecf7f7b9 --- /dev/null +++ b/ui/src/lib.rs @@ -0,0 +1,911 @@ +use ethers::abi::Abi; +use std::{ + cmp::{max, min}, + collections::{BTreeMap, VecDeque}, + time::{Duration, Instant}, +}; +use tui::text::Text; + +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEvent, + MouseEventKind, + }, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ethers::solc::artifacts::ContractBytecodeSome; +use std::{ + io::{self}, + sync::mpsc, + thread, +}; + +use evm_adapters::sputnik::cheatcodes::debugger::DebugStep; +use eyre::Result; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + terminal::Frame, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + +use ethers::types::Address; + +/// Trait for starting the ui +pub trait Ui { + /// Start the agent that will now take over. + fn start(self) -> Result; +} + +/// Used to indicate why the Ui stopped +pub enum TUIExitReason { + /// 'q' exit + CharExit, +} + +pub struct Tui { + debug_arena: Vec<(Address, Vec, bool)>, + terminal: Terminal>, + /// Buffer for keys prior to execution, i.e. '10' + 'k' => move up 10 operations + key_buffer: String, + /// current step in the debug steps + current_step: usize, + identified_contracts: BTreeMap, + known_contracts: BTreeMap, + source_code: BTreeMap, +} + +impl Tui { + /// Create a tui + #[allow(unused_must_use)] + pub fn new( + debug_arena: Vec<(Address, Vec, bool)>, + current_step: usize, + identified_contracts: BTreeMap, + known_contracts: BTreeMap, + source_code: BTreeMap, + ) -> Result { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor(); + Ok(Tui { + debug_arena, + terminal, + key_buffer: String::new(), + current_step, + identified_contracts, + known_contracts, + source_code, + }) + } + + /// Grab number from buffer. Used for something like '10k' to move up 10 operations + fn buffer_as_number(buffer: &str, default_value: usize) -> usize { + if let Ok(num) = buffer.parse() { + if num >= 1 { + num + } else { + default_value + } + } else { + default_value + } + } + + /// Create layout and subcomponents + #[allow(clippy::too_many_arguments)] + fn draw_layout( + f: &mut Frame, + address: Address, + identified_contracts: &BTreeMap, + known_contracts: &BTreeMap, + source_code: &BTreeMap, + debug_steps: &[DebugStep], + opcode_list: &[String], + current_step: usize, + creation: bool, + draw_memory: &mut DrawMemory, + ) { + let total_size = f.size(); + + // split in 2 vertically + + if let [app, footer] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(98, 100), Constraint::Ratio(2, 100)].as_ref()) + .split(total_size)[..] + { + if let [left_pane, right_pane] = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)].as_ref()) + .split(app)[..] + { + // split right pane horizontally to construct stack and memory + if let [op_pane, src_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)].as_ref()) + .split(left_pane)[..] + { + if let [stack_pane, memory_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)].as_ref()) + .split(right_pane)[..] + { + Tui::draw_footer(f, footer); + Tui::draw_src( + f, + address, + identified_contracts, + known_contracts, + source_code, + debug_steps[current_step].ic, + creation, + src_pane, + ); + Tui::draw_op_list( + f, + address, + debug_steps, + opcode_list, + current_step, + draw_memory, + op_pane, + ); + Tui::draw_stack(f, debug_steps, current_step, stack_pane); + Tui::draw_memory(f, debug_steps, current_step, memory_pane); + } + } else { + panic!("Couldn't generate horizontal split layout 1:2."); + } + } else { + panic!("Couldn't generate vertical split layout 1:2."); + } + } else { + panic!("Couldn't generate application & footer") + } + } + + fn draw_footer(f: &mut Frame, area: Rect) { + let block_controls = Block::default(); + + let text_output = Text::from(Span::styled( + "[q]: Quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end", + Style::default().add_modifier(Modifier::DIM) + )); + let paragraph = Paragraph::new(text_output) + .block(block_controls) + .alignment(Alignment::Center) + .wrap(Wrap { trim: false }); + f.render_widget(paragraph, area); + } + + #[allow(clippy::too_many_arguments)] + fn draw_src( + f: &mut Frame, + address: Address, + identified_contracts: &BTreeMap, + known_contracts: &BTreeMap, + source_code: &BTreeMap, + ic: usize, + creation: bool, + area: Rect, + ) { + let block_source_code = Block::default() + .title(format!("Contract construction: {}", creation)) + .borders(Borders::ALL); + + let mut text_output: Text = Text::from(""); + + if let Some(contract_name) = identified_contracts.get(&address) { + if let Some(known) = known_contracts.get(&contract_name.0) { + // grab either the creation source map or runtime sourcemap + if let Some(sourcemap) = if creation { + known.bytecode.source_map() + } else { + known.deployed_bytecode.bytecode.as_ref().expect("no bytecode").source_map() + } { + match sourcemap { + Ok(sourcemap) => { + // we are handed a vector of SourceElements that give + // us a span of sourcecode that is currently being executed + // This includes an offset and length. This vector is in + // instruction pointer order, meaning the location of + // the instruction - sum(push_bytes[..pc]) + if let Some(source_idx) = sourcemap[ic].index { + if let Some(source) = source_code.get(&source_idx) { + let offset = sourcemap[ic].offset; + let len = sourcemap[ic].length; + + // split source into before, relevant, and after chunks + // split by line as well to do some formatting stuff + let mut before = source[..offset] + .split_inclusive('\n') + .collect::>(); + let actual = source[offset..offset + len] + .split_inclusive('\n') + .map(|s| s.to_string()) + .collect::>(); + let mut after = source[offset + len..] + .split_inclusive('\n') + .collect::>(); + + let mut line_number = 0; + + let num_lines = before.len() + actual.len() + after.len(); + let height = area.height as usize; + let needed_highlight = actual.len(); + let mid_len = before.len() + actual.len(); + + // adjust what text we show of the source code + let (start_line, end_line) = if needed_highlight > height { + // highlighted section is more lines than we have avail + (before.len(), before.len() + needed_highlight) + } else if height > num_lines { + // we can fit entire source + (0, num_lines) + } else { + let remaining = height - needed_highlight; + let mut above = remaining / 2; + let mut below = remaining / 2; + if below > after.len() { + // unused space below the highlight + above += below - after.len(); + } else if above > before.len() { + // we have unused space above the highlight + below += above - before.len(); + } else { + // no unused space + } + + (before.len().saturating_sub(above), mid_len + below) + }; + + let max_line_num = num_lines.to_string().len(); + // We check if there is other text on the same line before the + // highlight starts + if let Some(last) = before.pop() { + if !last.ends_with('\n') { + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Gray) + .bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default() + .add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); + + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::raw(last), + Span::styled( + actual[0].to_string(), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + + actual.iter().skip(1).for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + // this is a hack to add coloring + // because tui does weird trimming + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() + } else { + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } else { + before.push(last); + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Gray) + .bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default() + .add_modifier(Modifier::DIM), + ), + ])); + + line_number += 1; + }); + actual.iter().for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() + } else { + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } + } else { + actual.iter().for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() + } else { + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } + + // fill in the rest of the line as unhighlighted + if let Some(last) = actual.last() { + if !last.ends_with('\n') { + if let Some(post) = after.pop_front() { + if let Some(last) = text_output.lines.last_mut() { + last.0.push(Span::raw(post)); + } + } + } + } + + // add after highlighted text + while mid_len + after.len() > end_line { + after.pop_back(); + } + after.iter().for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Gray) + .bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); + } else { + text_output.extend(Text::from("No source for srcmap index")); + } + } else { + text_output.extend(Text::from("No srcmap index")); + } + } + Err(e) => text_output.extend(Text::from(format!( + "Error in source map parsing: '{}', please open an issue", + e + ))), + } + } else { + text_output.extend(Text::from("No sourcemap for contract")); + } + } else { + text_output.extend(Text::from(format!("Unknown contract at address {}", address))); + } + } else { + text_output.extend(Text::from(format!("Unknown contract at address {}", address))); + } + + let paragraph = + Paragraph::new(text_output).block(block_source_code).wrap(Wrap { trim: false }); + f.render_widget(paragraph, area); + } + + /// Draw opcode list into main component + fn draw_op_list( + f: &mut Frame, + address: Address, + debug_steps: &[DebugStep], + opcode_list: &[String], + current_step: usize, + draw_memory: &mut DrawMemory, + area: Rect, + ) { + let block_source_code = Block::default() + .title(format!( + " Address: {}, pc: {}, call's gas used: {} ", + address, + if let Some(step) = debug_steps.get(current_step) { + step.pc.to_string() + } else { + "END".to_string() + }, + debug_steps[current_step].total_gas_used, + )) + .borders(Borders::ALL); + let mut text_output: Vec = Vec::new(); + + // Scroll: + // Focused line is line that should always be at the center of the screen. + let display_start; + let scroll_offset = 4; + let extra_top_lines = 10; + let height = area.height as i32; + let prev_start = draw_memory.current_startline; + // Absolute minimum start line + let abs_min_start = 0; + // Adjust for weird scrolling for max top line + let abs_max_start = (opcode_list.len() as i32 - 1) - height + scroll_offset; + // actual minumum start line + let mut min_start = + max(current_step as i32 - height + extra_top_lines, abs_min_start) as usize; + + // actual max start line + let mut max_start = + max(min(current_step as i32 - extra_top_lines, abs_max_start), abs_min_start) as usize; + + // Sometimes, towards end of file, maximum and minim lines have swapped values. Swap if the + // case + if min_start > max_start { + std::mem::swap(&mut min_start, &mut max_start); + } + + if prev_start < min_start { + display_start = min_start; + } else if prev_start > max_start { + display_start = max_start; + } else { + display_start = prev_start; + } + draw_memory.current_startline = display_start; + + let max_pc_len = + debug_steps.iter().fold(0, |max_val, val| val.pc.max(max_val)).to_string().len(); + + // Define closure that prints one more line of source code + let mut add_new_line = |line_number| { + let bg_color = if line_number == current_step { Color::DarkGray } else { Color::Reset }; + + // Format line number + let line_number_format = if line_number == current_step { + let step: &DebugStep = &debug_steps[line_number]; + format!("{:0>max_pc_len$x} ▶", step.pc, max_pc_len = max_pc_len) + } else if line_number < debug_steps.len() { + let step: &DebugStep = &debug_steps[line_number]; + format!("{:0>max_pc_len$x}: ", step.pc, max_pc_len = max_pc_len) + } else { + "END CALL".to_string() + }; + + if let Some(op) = opcode_list.get(line_number) { + text_output.push(Spans::from(Span::styled( + format!("{} {}", line_number_format, op), + Style::default().fg(Color::White).bg(bg_color), + ))); + } else { + text_output.push(Spans::from(Span::styled( + line_number_format, + Style::default().fg(Color::White).bg(bg_color), + ))); + } + }; + for number in display_start..opcode_list.len() { + add_new_line(number); + } + // Add one more "phantom" line so we see line where current segment execution ends + add_new_line(opcode_list.len()); + let paragraph = + Paragraph::new(text_output).block(block_source_code).wrap(Wrap { trim: true }); + f.render_widget(paragraph, area); + } + + /// Draw the stack into the stack pane + fn draw_stack( + f: &mut Frame, + debug_steps: &[DebugStep], + current_step: usize, + area: Rect, + ) { + let stack_space = + Block::default().title(format!(" Stack: {} ", current_step)).borders(Borders::ALL); + let stack = &debug_steps[current_step].stack; + let min_len = usize::max(format!("{}", stack.len()).len(), 2); + + let text: Vec = stack + .iter() + .enumerate() + .map(|(i, stack_item)| { + Spans::from(Span::styled( + format!("{: ( + f: &mut Frame, + debug_steps: &[DebugStep], + current_step: usize, + area: Rect, + ) { + let memory = &debug_steps[current_step].memory; + let stack_space = Block::default() + .title(format!(" Memory - Max Expansion: {} bytes", memory.effective_len())) + .borders(Borders::ALL); + let memory = memory.data(); + let max_i = memory.len() / 32; + let min_len = format!("{:x}", max_i * 32).len(); + + let text: Vec = memory + .chunks(32) + .enumerate() + .map(|(i, mem_word)| { + let strings: String = mem_word + .chunks(4) + .map(|bytes4| { + bytes4 + .iter() + .map(|byte| { + let v: Vec = vec![*byte]; + hex::encode(&v[..]) + }) + .collect::>() + .join(" ") + }) + .collect::>() + .join(" "); + Spans::from(Span::styled( + format!("{:0min_len$x}: {} \n", i * 32, strings, min_len = min_len), + Style::default().fg(Color::White), + )) + }) + .collect(); + let paragraph = Paragraph::new(text).block(stack_space).wrap(Wrap { trim: true }); + f.render_widget(paragraph, area); + } +} + +impl Ui for Tui { + fn start(mut self) -> Result { + // if something panics inside here, we should do everything we can to + // not corrupt the user's terminal. + std::panic::set_hook(Box::new(|e| { + disable_raw_mode().expect("Unable to disable raw mode"); + execute!(std::io::stdout(), LeaveAlternateScreen, DisableMouseCapture) + .expect("unable to execute disable mouse capture"); + println!("{}", e); + })); + // this is the recommend tick rate from tui-rs, based on their examples + let tick_rate = Duration::from_millis(200); + + // setup a channel to send interrupts + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let mut last_tick = Instant::now(); + loop { + // poll events since last tick + if event::poll(tick_rate - last_tick.elapsed()).unwrap() { + let event = event::read().unwrap(); + if let Event::Key(key) = event { + if tx.send(Interrupt::KeyPressed(key)).is_err() { + return + } + } else if let Event::Mouse(mouse) = event { + if tx.send(Interrupt::MouseEvent(mouse)).is_err() { + return + } + } + } + // force update if time has passed + if last_tick.elapsed() > tick_rate { + if tx.send(Interrupt::IntervalElapsed).is_err() { + return + } + last_tick = Instant::now(); + } + } + }); + + self.terminal.clear()?; + let mut draw_memory: DrawMemory = DrawMemory::default(); + + let debug_call: Vec<(Address, Vec, bool)> = self.debug_arena.clone(); + let mut opcode_list: Vec = + debug_call[0].1.iter().map(|step| step.pretty_opcode()).collect(); + let mut last_index = 0; + // UI thread that manages drawing + loop { + if last_index != draw_memory.inner_call_index { + opcode_list = debug_call[draw_memory.inner_call_index] + .1 + .iter() + .map(|step| step.pretty_opcode()) + .collect(); + last_index = draw_memory.inner_call_index; + } + // grab interrupt + match rx.recv()? { + // key press + Interrupt::KeyPressed(event) => match event.code { + // Exit + KeyCode::Char('q') => { + disable_raw_mode()?; + execute!( + self.terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + return Ok(TUIExitReason::CharExit) + } + // Move down + KeyCode::Char('j') | KeyCode::Down => { + // grab number of times to do it + for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { + if self.current_step < opcode_list.len() - 1 { + self.current_step += 1; + } else if draw_memory.inner_call_index < debug_call.len() - 1 { + draw_memory.inner_call_index += 1; + self.current_step = 0; + } + } + self.key_buffer.clear(); + } + // Move up + KeyCode::Char('k') | KeyCode::Up => { + for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { + if self.current_step > 0 { + self.current_step -= 1; + } else if draw_memory.inner_call_index > 0 { + draw_memory.inner_call_index -= 1; + self.current_step = + debug_call[draw_memory.inner_call_index].1.len() - 1; + } + } + self.key_buffer.clear(); + } + // Go to top of file + KeyCode::Char('g') => { + draw_memory.inner_call_index = 0; + self.current_step = 0; + self.key_buffer.clear(); + } + // Go to bottom of file + KeyCode::Char('G') => { + draw_memory.inner_call_index = debug_call.len() - 1; + self.current_step = debug_call[draw_memory.inner_call_index].1.len() - 1; + self.key_buffer.clear(); + } + // Go to previous call + KeyCode::Char('c') => { + draw_memory.inner_call_index = + draw_memory.inner_call_index.saturating_sub(1); + self.current_step = debug_call[draw_memory.inner_call_index].1.len() - 1; + self.key_buffer.clear(); + } + // Go to next call + KeyCode::Char('C') => { + if debug_call.len() > draw_memory.inner_call_index + 1 { + draw_memory.inner_call_index += 1; + self.current_step = 0; + } + self.key_buffer.clear(); + } + // Step forward + KeyCode::Char('s') => { + for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { + let remaining_ops = opcode_list[self.current_step..].to_vec().clone(); + self.current_step += remaining_ops + .iter() + .enumerate() + .find_map(|(i, op)| { + if i < remaining_ops.len() - 1 { + match ( + op.contains("JUMP") && op != "JUMPDEST", + &*remaining_ops[i + 1], + ) { + (true, "JUMPDEST") => Some(i + 1), + _ => None, + } + } else { + None + } + }) + .unwrap_or(opcode_list.len() - 1); + if self.current_step > opcode_list.len() { + self.current_step = opcode_list.len() - 1 + }; + } + self.key_buffer.clear(); + } + // Step backwards + KeyCode::Char('a') => { + for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { + let prev_ops = opcode_list[..self.current_step].to_vec().clone(); + self.current_step = prev_ops + .iter() + .enumerate() + .rev() + .find_map(|(i, op)| { + if i > 0 { + match ( + prev_ops[i - 1].contains("JUMP") && + prev_ops[i - 1] != "JUMPDEST", + &**op, + ) { + (true, "JUMPDEST") => Some(i - 1), + _ => None, + } + } else { + None + } + }) + .unwrap_or_default(); + } + self.key_buffer.clear(); + } + KeyCode::Char(other) => match other { + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => { + self.key_buffer.push(other); + } + _ => { + // Invalid key, clear buffer + self.key_buffer.clear(); + } + }, + _ => { + self.key_buffer.clear(); + } + }, + Interrupt::MouseEvent(event) => match event.kind { + MouseEventKind::ScrollUp => { + if self.current_step > 0 { + self.current_step -= 1; + } else if draw_memory.inner_call_index > 0 { + draw_memory.inner_call_index -= 1; + self.current_step = + debug_call[draw_memory.inner_call_index].1.len() - 1; + } + } + MouseEventKind::ScrollDown => { + if self.current_step < opcode_list.len() - 1 { + self.current_step += 1; + } else if draw_memory.inner_call_index < debug_call.len() - 1 { + draw_memory.inner_call_index += 1; + self.current_step = 0; + } + } + _ => {} + }, + Interrupt::IntervalElapsed => {} + } + // Draw + let current_step = self.current_step; + self.terminal.draw(|f| { + Tui::draw_layout( + f, + debug_call[draw_memory.inner_call_index].0, + &self.identified_contracts, + &self.known_contracts, + &self.source_code, + &debug_call[draw_memory.inner_call_index].1[..], + &opcode_list, + current_step, + debug_call[draw_memory.inner_call_index].2, + &mut draw_memory, + ) + })?; + } + } +} + +/// Why did we wake up drawing thread? +enum Interrupt { + KeyPressed(KeyEvent), + MouseEvent(MouseEvent), + IntervalElapsed, +} + +/// This is currently used to remember last scroll +/// position so screen doesn't wiggle as much. +struct DrawMemory { + pub current_startline: usize, + pub inner_call_index: usize, +} +impl DrawMemory { + fn default() -> Self { + DrawMemory { current_startline: 0, inner_call_index: 0 } + } +}