From 2544793db0896f8f34e661195f8ad90a76dfc279 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 22 Jul 2024 18:50:28 +0800 Subject: [PATCH] feat: `cast decode-eof` & `forge inspect eof` (#8478) * feat: cast decode-eof & forge inspect eof * add docs * clippy * fix tests * review fixes --- Cargo.lock | 2 + Cargo.toml | 1 + crates/cast/Cargo.toml | 2 +- crates/cast/bin/main.rs | 4 ++ crates/cast/bin/opts.rs | 6 ++- crates/cast/src/lib.rs | 19 +++++++++ crates/common/Cargo.toml | 2 +- crates/common/fmt/Cargo.toml | 2 + crates/common/fmt/src/eof.rs | 75 +++++++++++++++++++++++++++++++++ crates/common/fmt/src/lib.rs | 3 ++ crates/forge/Cargo.toml | 2 +- crates/forge/bin/cmd/inspect.rs | 68 ++++++++++++++++++++++++------ 12 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 crates/common/fmt/src/eof.rs diff --git a/Cargo.lock b/Cargo.lock index 328b8f59d1a3..42579ff89448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,9 @@ dependencies = [ "alloy-rpc-types", "alloy-serde", "chrono", + "comfy-table", "foundry-macros", + "revm-primitives", "serde", "serde_json", "similar-asserts", diff --git a/Cargo.toml b/Cargo.toml index 354f1549b279..83e270668db4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -263,3 +263,4 @@ tower-http = "0.5" soldeer = "0.2.19" proptest = "1" +comfy-table = "7" diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index 602ec57ae495..1d899bbba2f4 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -68,7 +68,7 @@ foundry-cli.workspace = true clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } clap_complete = "4" clap_complete_fig = "4" -comfy-table = "7" +comfy-table.workspace = true dunce.workspace = true indicatif = "0.17" itertools.workspace = true diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index b42e4231d0c0..ff4f799cdbdd 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -567,6 +567,10 @@ async fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&tx)?); } + CastSubcommand::DecodeEof { eof } => { + let eof = stdin::unwrap_line(eof)?; + println!("{}", SimpleCast::decode_eof(&eof)?); + } }; Ok(()) } diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index 975ce2041aff..5b3c09c8a869 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -905,7 +905,7 @@ pub enum CastSubcommand { }, /// Decodes a raw signed EIP 2718 typed transaction - #[command(visible_alias = "dt")] + #[command(visible_aliases = &["dt", "decode-tx"])] DecodeTransaction { tx: Option }, /// Extracts function selectors and arguments from bytecode @@ -918,6 +918,10 @@ pub enum CastSubcommand { #[arg(long, short)] resolve: bool, }, + + /// Decodes EOF container bytes + #[command()] + DecodeEof { eof: Option }, } /// CLI arguments for `cast --to-base`. diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index e1a70b36c832..46610d6303b7 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -34,6 +34,7 @@ use foundry_compilers::flatten::Flattener; use foundry_config::Chain; use futures::{future::Either, FutureExt, StreamExt}; use rayon::prelude::*; +use revm::primitives::Eof; use std::{ borrow::Cow, io, @@ -1990,6 +1991,24 @@ impl SimpleCast { let tx = TxEnvelope::decode_2718(&mut tx_hex.as_slice())?; Ok(tx) } + + /// Decodes EOF container bytes + /// Pretty prints the decoded EOF container contents + /// + /// # Example + /// + /// ``` + /// use cast::SimpleCast as Cast; + /// + /// let eof = "0xef0001010004020001005604002000008000046080806040526004361015e100035f80fd5f3560e01c63773d45e01415e1ffee6040600319360112e10028600435906024358201809211e100066020918152f3634e487b7160e01b5f52601160045260245ffd5f80fd0000000000000000000000000124189fc71496f8660db5189f296055ed757632"; + /// let decoded = Cast::decode_eof(&eof)?; + /// println!("{}", decoded); + /// # Ok::<(), eyre::Report>(()) + pub fn decode_eof(eof: &str) -> Result { + let eof_hex = hex::decode(eof)?; + let eof = Eof::decode(eof_hex.into())?; + Ok(pretty_eof(&eof)?) + } } fn strip_0x(s: &str) -> &str { diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index b9c872c652cf..094f32bed652 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -46,7 +46,7 @@ tower.workspace = true async-trait.workspace = true clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } -comfy-table = "7" +comfy-table.workspace = true dunce.workspace = true eyre.workspace = true num-format.workspace = true diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 285c3052dc0c..fabe35a7a1a1 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -25,6 +25,8 @@ alloy-serde.workspace = true serde.workspace = true serde_json.workspace = true chrono.workspace = true +revm-primitives.workspace = true +comfy-table.workspace = true [dev-dependencies] foundry-macros.workspace = true diff --git a/crates/common/fmt/src/eof.rs b/crates/common/fmt/src/eof.rs new file mode 100644 index 000000000000..639e175b45ea --- /dev/null +++ b/crates/common/fmt/src/eof.rs @@ -0,0 +1,75 @@ +use comfy_table::{ContentArrangement, Table}; +use revm_primitives::{ + eof::{EofBody, EofHeader}, + Eof, +}; +use std::fmt::{self, Write}; + +pub fn pretty_eof(eof: &Eof) -> Result { + let Eof { + header: + EofHeader { + types_size, + code_sizes, + container_sizes, + data_size, + sum_code_sizes: _, + sum_container_sizes: _, + }, + body: + EofBody { types_section, code_section, container_section, data_section, is_data_filled: _ }, + raw: _, + } = eof; + + let mut result = String::new(); + + let mut table = Table::new(); + table.add_row(vec!["type_size", &types_size.to_string()]); + table.add_row(vec!["num_code_sections", &code_sizes.len().to_string()]); + if !code_sizes.is_empty() { + table.add_row(vec!["code_sizes", &format!("{code_sizes:?}")]); + } + table.add_row(vec!["num_container_sections", &container_sizes.len().to_string()]); + if !container_sizes.is_empty() { + table.add_row(vec!["container_sizes", &format!("{container_sizes:?}")]); + } + table.add_row(vec!["data_size", &data_size.to_string()]); + + write!(result, "Header:\n{table}")?; + + if !code_section.is_empty() { + let mut table = Table::new(); + table.set_content_arrangement(ContentArrangement::Dynamic); + table.set_header(vec!["", "Inputs", "Outputs", "Max stack height", "Code"]); + for (idx, (code, type_section)) in code_section.iter().zip(types_section).enumerate() { + table.add_row(vec![ + &idx.to_string(), + &type_section.inputs.to_string(), + &type_section.outputs.to_string(), + &type_section.max_stack_size.to_string(), + &code.to_string(), + ]); + } + + write!(result, "\n\nCode sections:\n{table}")?; + } + + if !container_section.is_empty() { + let mut table = Table::new(); + table.set_content_arrangement(ContentArrangement::Dynamic); + for (idx, container) in container_section.iter().enumerate() { + table.add_row(vec![&idx.to_string(), &container.to_string()]); + } + + write!(result, "\n\nContainer sections:\n{table}")?; + } + + if !data_section.is_empty() { + let mut table = Table::new(); + table.set_content_arrangement(ContentArrangement::Dynamic); + table.add_row(vec![&data_section.to_string()]); + write!(result, "\n\nData section:\n{table}")?; + } + + Ok(result) +} diff --git a/crates/common/fmt/src/lib.rs b/crates/common/fmt/src/lib.rs index 5271b73c755b..c02090809777 100644 --- a/crates/common/fmt/src/lib.rs +++ b/crates/common/fmt/src/lib.rs @@ -11,3 +11,6 @@ pub use exp::{format_int_exp, format_uint_exp, to_exp_notation}; mod ui; pub use ui::{get_pretty_block_attr, get_pretty_tx_attr, EthValue, UIfmt}; + +mod eof; +pub use eof::pretty_eof; diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 02bc6d0c8a64..eb3e94406a08 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -38,7 +38,7 @@ ethers-contract-abigen = { workspace = true, features = ["providers"] } revm-inspectors.workspace = true -comfy-table = "7" +comfy-table.workspace = true eyre.workspace = true proptest.workspace = true rayon.workspace = true diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index a6f162556027..ac0ed41b3f34 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -1,15 +1,17 @@ +use alloy_primitives::Address; use clap::Parser; use comfy_table::{presets::ASCII_MARKDOWN, Table}; -use eyre::Result; +use eyre::{Context, Result}; +use forge::revm::primitives::Eof; use foundry_cli::opts::{CompilerArgs, CoreBuildArgs}; -use foundry_common::compile::ProjectCompiler; +use foundry_common::{compile::ProjectCompiler, fmt::pretty_eof}; use foundry_compilers::{ artifacts::{ output_selection::{ BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection, EvmOutputSelection, EwasmOutputSelection, }, - StorageLayout, + CompactBytecode, StorageLayout, }, info::ContractInfo, utils::canonicalize, @@ -39,7 +41,7 @@ pub struct InspectArgs { impl InspectArgs { pub fn run(self) -> Result<()> { - let Self { mut contract, field, build, pretty } = self; + let Self { contract, field, build, pretty } = self; trace!(target: "forge", ?field, ?contract, "running forge inspect"); @@ -64,16 +66,16 @@ impl InspectArgs { // Build the project let project = modified_build_args.project()?; - let mut compiler = ProjectCompiler::new().quiet(true); - if let Some(contract_path) = &mut contract.path { - let target_path = canonicalize(&*contract_path)?; - *contract_path = target_path.to_string_lossy().to_string(); - compiler = compiler.files([target_path]); - } - let output = compiler.compile(&project)?; + let compiler = ProjectCompiler::new().quiet(true); + let target_path = if let Some(path) = &contract.path { + canonicalize(project.root().join(path))? + } else { + project.find_contract_path(&contract.name)? + }; + let mut output = compiler.files([target_path.clone()]).compile(&project)?; // Find the artifact - let artifact = output.find_contract(&contract).ok_or_else(|| { + let artifact = output.remove(&target_path, &contract.name).ok_or_else(|| { eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts") })?; @@ -160,6 +162,12 @@ impl InspectArgs { } print_json(&out)?; } + ContractArtifactField::Eof => { + print_eof(artifact.deployed_bytecode.and_then(|b| b.bytecode))?; + } + ContractArtifactField::EofInit => { + print_eof(artifact.bytecode)?; + } }; Ok(()) @@ -214,6 +222,8 @@ pub enum ContractArtifactField { Ewasm, Errors, Events, + Eof, + EofInit, } macro_rules! impl_value_enum { @@ -300,6 +310,8 @@ impl_value_enum! { Ewasm => "ewasm" | "e-wasm", Errors => "errors" | "er", Events => "events" | "ev", + Eof => "eof" | "eof-container" | "eof-deployed", + EofInit => "eof-init" | "eof-initcode" | "eof-initcontainer", } } @@ -324,6 +336,10 @@ impl From for ContractOutputSelection { Caf::Ewasm => Self::Ewasm(EwasmOutputSelection::All), Caf::Errors => Self::Abi, Caf::Events => Self::Abi, + Caf::Eof => Self::Evm(EvmOutputSelection::DeployedByteCode( + DeployedBytecodeOutputSelection::All, + )), + Caf::EofInit => Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)), } } } @@ -347,7 +363,9 @@ impl PartialEq for ContractArtifactField { (Self::IrOptimized, Cos::IrOptimized) | (Self::Metadata, Cos::Metadata) | (Self::UserDoc, Cos::UserDoc) | - (Self::Ewasm, Cos::Ewasm(_)) + (Self::Ewasm, Cos::Ewasm(_)) | + (Self::Eof, Cos::Evm(Eos::DeployedByteCode(_))) | + (Self::EofInit, Cos::Evm(Eos::ByteCode(_))) ) } } @@ -407,6 +425,30 @@ fn get_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result) -> Result<()> { + let Some(mut bytecode) = bytecode else { eyre::bail!("No bytecode") }; + + // Replace link references with zero address. + if bytecode.object.is_unlinked() { + for (file, references) in bytecode.link_references.clone() { + for (name, _) in references { + bytecode.link(&file, &name, Address::ZERO); + } + } + } + + let Some(bytecode) = bytecode.object.into_bytes() else { + eyre::bail!("Failed to link bytecode"); + }; + + let eof = Eof::decode(bytecode).wrap_err("Failed to decode EOF")?; + + println!("{}", pretty_eof(&eof)?); + + Ok(()) +} + #[cfg(test)] mod tests { use super::*;