From 808fb2873a928b1f67c8e6a1b21cf641f1498099 Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Sun, 1 Oct 2023 23:07:28 -0600 Subject: [PATCH 1/4] add support for gdb --- .cargo/config | 2 + Cargo.lock | 216 ++++++++++----- Cargo.toml | 18 +- src/main.rs | 90 ++++--- src/project.rs | 2 +- src/replay/hostio.rs | 609 +++++++++++++++++++++++++++++++++++++++++++ src/replay/mod.rs | 104 ++++++++ src/replay/query.js | 26 ++ src/replay/trace.rs | 568 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1534 insertions(+), 101 deletions(-) create mode 100644 .cargo/config create mode 100644 src/replay/hostio.rs create mode 100644 src/replay/mod.rs create mode 100644 src/replay/query.js create mode 100644 src/replay/trace.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..52e8b72 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "link-args=-rdynamic"] diff --git a/Cargo.lock b/Cargo.lock index 0c3264a..25cc3e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,7 +64,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fe46acf61ad5bd7a5c21839bb2526358382fb8a6526f7ba393bdf593e2ae43f" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.3.3", "alloy-sol-type-parser", "serde", "serde_json", @@ -89,6 +89,26 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "alloy-primitives" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4084879b7257d5b95b9009837c07a1868bd7d60e66418a7764b9b580ae64e0" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "proptest", + "rand", + "ruint", + "serde", + "tiny-keccak", +] + [[package]] name = "alloy-rlp" version = "0.3.2" @@ -108,9 +128,9 @@ checksum = "627a32998aee7a7eedd351e9b6d4cacef9426935219a3a61befa332db1be5ca3" [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -146,9 +166,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -186,7 +206,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -440,16 +460,23 @@ name = "cargo-stylus" version = "0.2.0" dependencies = [ "alloy-json-abi", + "alloy-primitives 0.4.0", "brotli2", "bytes", "bytesize", "clap", "ethers", "eyre", + "function_name", "hex", + "lazy_static", + "libc", + "libloading", + "parking_lot", "rustc-host", "serde", "serde_json", + "sneks", "thiserror", "tiny-keccak", "tokio", @@ -507,20 +534,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.1" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.1" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -530,14 +556,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -634,6 +660,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "corosensei" version = "0.1.4" @@ -848,7 +883,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -859,7 +894,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -914,7 +949,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1101,7 +1136,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -1203,9 +1238,9 @@ dependencies = [ [[package]] name = "ethers" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba3fd516c15a9a587135229466dbbfc85796de55c5660afbbb1b1c78517d85c" +checksum = "1ad13497f6e0a24292fc7b408e30d22fe9dc262da1f40d7b542c3a44e7fc0476" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1219,9 +1254,9 @@ dependencies = [ [[package]] name = "ethers-addressbook" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0245617f11b8178fa50b52e433e2c34ac69f39116b62c8be2437decf2edf1986" +checksum = "c6e9e8acd0ed348403cc73a670c24daba3226c40b98dc1a41903766b3ab6240a" dependencies = [ "ethers-core", "once_cell", @@ -1231,9 +1266,9 @@ dependencies = [ [[package]] name = "ethers-contract" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bb80fd2c22631a5eb8a02cbf373cc5fd86937fc966bb670b9a884580c8e71c" +checksum = "d79269278125006bb0552349c03593ffa9702112ca88bc7046cc669f148fb47c" dependencies = [ "const-hex", "ethers-contract-abigen", @@ -1250,9 +1285,9 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22c54db0d393393e732a5b20273e4f8ab89f0cce501c84e75fab9c126799a6e6" +checksum = "ce95a43c939b2e4e2f3191c5ad4a1f279780b8a39139c9905b43a7433531e2ab" dependencies = [ "Inflector", "const-hex", @@ -1267,16 +1302,16 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.29", + "syn 2.0.37", "toml", "walkdir", ] [[package]] name = "ethers-contract-derive" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ee4f216184a1304b707ed258f4f70aa40bf7e1522ab8963d127a8d516eaa1a" +checksum = "8e9ce44906fc871b3ee8c69a695ca7ec7f70e50cb379c9b9cb5e532269e492f6" dependencies = [ "Inflector", "const-hex", @@ -1285,14 +1320,14 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] name = "ethers-core" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c29523f73c12753165781c6e5dc11c84d3e44c080a15f7c6cfbd70b514cb6f1" +checksum = "c0a17f0708692024db9956b31d7a20163607d2745953f5ae8125ab368ba280ad" dependencies = [ "arrayvec", "bytes", @@ -1311,7 +1346,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.29", + "syn 2.0.37", "tempfile", "thiserror", "tiny-keccak", @@ -1320,9 +1355,9 @@ dependencies = [ [[package]] name = "ethers-etherscan" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aab5af432b3fe5b7756b60df5c9ddeb85a13414575ad8a9acd707c24f0a77a5" +checksum = "0e53451ea4a8128fbce33966da71132cf9e1040dcfd2a2084fd7733ada7b2045" dependencies = [ "ethers-core", "reqwest", @@ -1335,9 +1370,9 @@ dependencies = [ [[package]] name = "ethers-middleware" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356151d5ded56d4918146366abc9dfc9df367cf0096492a7a5477b21b7693615" +checksum = "473f1ccd0c793871bbc248729fa8df7e6d2981d6226e4343e3bbaa9281074d5d" dependencies = [ "async-trait", "auto_impl", @@ -1362,9 +1397,9 @@ dependencies = [ [[package]] name = "ethers-providers" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c84664b294e47fc2860d6db0db0246f79c4c724e552549631bb9505b834bee" +checksum = "6838fa110e57d572336178b7c79e94ff88ef976306852d8cb87d9e5b1fc7c0b5" dependencies = [ "async-trait", "auto_impl", @@ -1399,9 +1434,9 @@ dependencies = [ [[package]] name = "ethers-signers" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170b299698702ef1f53d2275af7d6d97409cfa4f9398ee9ff518f6bc9102d0ad" +checksum = "5ea44bec930f12292866166f9ddbea6aa76304850e4d8dcd66dc492b43d00ff1" dependencies = [ "async-trait", "coins-bip32", @@ -1418,9 +1453,9 @@ dependencies = [ [[package]] name = "ethers-solc" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66559c8f774712df303c907d087275a52a2046b256791aaa566d5abc8ea66731" +checksum = "de34e484e7ae3cab99fbfd013d6c5dc7f9013676a4e0e414d8b12e1213e8b3ba" dependencies = [ "cfg-if", "const-hex", @@ -1533,6 +1568,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "function_name" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ab577a896d09940b5fe12ec5ae71f9d8211fff62c919c03a3750a9901e98a7" +dependencies = [ + "function_name-proc-macro", +] + +[[package]] +name = "function_name-proc-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673464e1e314dd67a0fd9544abc99e8eb28d0c7e3b69b033bcff9b2d00b87333" + [[package]] name = "funty" version = "2.0.0" @@ -1605,7 +1655,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2009,6 +2059,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2081,7 +2140,7 @@ dependencies = [ "diff", "ena", "is-terminal", - "itertools", + "itertools 0.10.5", "lalrpop-util", "petgraph", "regex", @@ -2112,9 +2171,19 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libloading" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] [[package]] name = "libm" @@ -2304,7 +2373,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2506,7 +2575,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2544,7 +2613,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2594,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2647,9 +2716,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -3275,7 +3344,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -3415,6 +3484,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sneks" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1754a9953937d149fb279d362241e4b92c4f237a491f4524835e8e78dd43810d" +dependencies = [ + "convert_case 0.6.0", + "quote", + "syn 2.0.37", +] + [[package]] name = "socket2" version = "0.4.9" @@ -3437,11 +3517,11 @@ dependencies = [ [[package]] name = "solang-parser" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c792fe9fae2a2f716846f214ca10d5a1e21133e0bf36cef34bcc4a852467b21" +checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457" dependencies = [ - "itertools", + "itertools 0.11.0", "lalrpop", "lalrpop-util", "phf", @@ -3515,7 +3595,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -3557,9 +3637,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -3619,7 +3699,7 @@ checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -3699,7 +3779,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -3801,7 +3881,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -3894,6 +3974,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" @@ -4018,7 +4104,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -4075,7 +4161,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index a148a7f..dee8eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,28 @@ keywords = ["arbitrum", "ethereum", "stylus", "alloy", "cargo"] description = "CLI tool for deploying Stylus contracts on Arbitrum chains" [dependencies] -rustc-host = "0.1" +alloy-primitives = "0.4.0" +rustc-host = "0.1.7" brotli2 = "0.3.2" bytes = "1.4.0" -clap = { version = "4.3.17", features = [ "derive" ] } +clap = { version = "4.4.5", features = [ "derive" ] } eyre = "0.6.8" hex = "0.4.3" -serde = { version = "1.0.174", features = ["derive"] } +serde = { version = "1.0.188", features = ["derive"] } alloy-json-abi = "0.3.2" bytesize = "1.2.0" -ethers = "2.0.8" +ethers = "2.0.10" serde_json = "1.0.103" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread" ] } wasmer = "3.1.0" thiserror = "1.0.47" tiny-keccak = { version = "2.0.2", features = ["keccak"] } + +# replay tool +function_name = "0.3.0" +lazy_static = "1.4.0" +libc = "0.2.148" +libloading = "0.8.0" +parking_lot = "0.12.1" +sneks = "0.1.2" + diff --git a/src/main.rs b/src/main.rs index 5a70a12..237ce9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ // Copyright 2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md -use std::path::PathBuf; - +use alloy_primitives::TxHash; use clap::{Args, Parser, ValueEnum}; use color::Color; use ethers::types::H160; +use eyre::Result; +use std::path::PathBuf; +use tokio::runtime::Builder; mod c; mod check; @@ -15,6 +17,7 @@ mod deploy; mod export_abi; mod new; mod project; +mod replay; mod tx; mod util; mod wallet; @@ -49,50 +52,45 @@ struct StylusArgs { #[derive(Parser, Debug, Clone)] enum StylusSubcommands { - /// Initialize a Stylus Rust project using the https://github.com/OffchainLabs/stylus-hello-world template. + /// Create a new Rust project. New { - /// Name of the Stylus project. + /// Project name. #[arg(required = true)] name: String, - /// Initializes a minimal version of a Stylus program, with just a barebones entrypoint and the Stylus SDK. + /// Create a minimal program. #[arg(long)] minimal: bool, }, - /// Export the Solidity ABI for a Stylus project directly using the cargo stylus tool. + /// Export a Solidity ABI. ExportAbi { /// Build in release mode. #[arg(long)] release: bool, - /// Specify an output file to write the ABI to. + /// The Output file (defaults to stdout). #[arg(long)] output: Option, - /// Output the JSON ABI instead, created by solc. Must have solc installed for this option to work. + /// Output a JSON ABI instead using solc. Requires solc. /// See https://docs.soliditylang.org/en/latest/installing-solidity.html #[arg(long)] json: bool, }, - /// Instrument a Rust project using Stylus. - /// This command runs compiled WASM code through Stylus instrumentation checks and reports any failures. + /// Check that a contract can be activated onchain. #[command(alias = "c")] Check(CheckConfig), - /// Instruments a Rust project using Stylus and by outputting its brotli-compressed WASM code. - /// Then, it submits two transactions: the first deploys the WASM - /// program to an address and the second triggers an activation onchain - /// Developers can choose to split up the deploy and activate steps via this command as desired. + /// Deploy a stylus contract. #[command(alias = "d")] Deploy(DeployConfig), + /// Replay a transaction in gdb. + #[command(alias = "r")] + Replay(ReplayConfig), } #[derive(Debug, Args, Clone)] pub struct CheckConfig { - /// The endpoint of the L2 node to connect to. See https://docs.arbitrum.io/stylus/reference/testnet-information - /// for latest Stylus testnet information including public endpoints. Defaults - /// to the current Stylus testnet RPC endpoint. + /// RPC endpoint of the Stylus node to connect to. #[arg(short, long, default_value = "https://stylus-testnet.arbitrum.io/rpc")] endpoint: String, - /// If desired, it loads a WASM file from a specified path. If not provided, it will try to find - /// a WASM file under the current working directory's Rust target release directory and use its - /// contents for the deploy command. + /// Specifies a WASM file instead of looking for one in the current directory. #[arg(long)] wasm_file_path: Option, /// Specify the program address we want to check activation for. If unspecified, it will @@ -100,7 +98,7 @@ pub struct CheckConfig { /// wallet-related flags to be specified. #[arg(long, default_value = "0x0000000000000000000000000000000000000000")] expected_program_address: H160, - /// File path to a text file containing a private key to use with the cargo stylus plugin. + /// File path to a text file containing a private key. #[arg(long)] private_key_path: Option, /// Private key 0x-prefixed hex string to use with the cargo stylus plugin. Warning: this exposes @@ -111,8 +109,7 @@ pub struct CheckConfig { /// Wallet source to use with the cargo stylus plugin. #[command(flatten)] keystore_opts: KeystoreOpts, - /// Whether or not to compile the Rust program using the nightly Rust version. Nightly can help - /// with reducing compressed WASM sizes, however, can be a security risk if used liberally. + /// Whether to use Rust nightly. #[arg(long)] nightly: bool, } @@ -121,8 +118,7 @@ pub struct CheckConfig { pub struct DeployConfig { #[command(flatten)] check_cfg: CheckConfig, - /// Does not submit a transaction, but instead estimates the gas required - /// to complete the operation. + /// Estimates deployment gas costs. #[arg(long)] estimate_gas_only: bool, /// By default, submits two transactions to deploy and activate the program to Arbitrum. @@ -137,6 +133,25 @@ pub struct DeployConfig { tx_sending_opts: TxSendingOpts, } +#[derive(Debug, Args, Clone)] +pub struct ReplayConfig { + /// RPC endpoint. + #[arg(short, long, default_value = "http://localhost:8545")] + endpoint: String, + /// Tx to replay. + #[arg(short, long)] + tx: TxHash, + /// Project path. + #[arg(short, long, default_value = ".")] + project: PathBuf, + /// Whether to use stable Rust. Note that nightly is needed to expand macros. + #[arg(short, long)] + stable_rust: bool, + /// Whether this process is the child of another. + #[arg(short, long, hide(true))] + child: bool, +} + #[derive(Debug, Clone, ValueEnum)] pub enum DeployMode { DeployOnly, @@ -168,8 +183,7 @@ pub struct TxSendingOpts { output_tx_data_to_dir: Option, } -#[tokio::main] -async fn main() -> eyre::Result<()> { +fn main() -> Result<()> { let args = match CargoCli::parse() { CargoCli::Stylus(args) => args, CargoCli::CGen(args) => { @@ -177,6 +191,17 @@ async fn main() -> eyre::Result<()> { } }; + // use the current thread for replay + let mut runtime = match &args.command { + StylusSubcommands::Replay(_) => Builder::new_current_thread(), + _ => Builder::new_multi_thread(), + }; + + let runtime = runtime.enable_all().build()?; + runtime.block_on(main_impl(args)) +} + +async fn main_impl(args: StylusArgs) -> Result<()> { match args.command { StylusSubcommands::New { name, minimal } => { if let Err(e) = new::new_stylus_project(&name, minimal) { @@ -202,16 +227,19 @@ async fn main() -> eyre::Result<()> { println!("Could not export Stylus program Solidity ABI: {}", e.pink()); } } - StylusSubcommands::Check(cfg) => { - if let Err(e) = check::run_checks(cfg).await { + StylusSubcommands::Check(config) => { + if let Err(e) = check::run_checks(config).await { println!("Stylus checks failed: {}", e.pink()); }; } - StylusSubcommands::Deploy(cfg) => { - if let Err(e) = deploy::deploy(cfg).await { + StylusSubcommands::Deploy(config) => { + if let Err(e) = deploy::deploy(config).await { println!("Deploy / activation command failed: {}", e.pink()); }; } + StylusSubcommands::Replay(config) => { + replay::replay(config).await?; + } } Ok(()) } diff --git a/src/project.rs b/src/project.rs index 6fc75c7..cd22ada 100644 --- a/src/project.rs +++ b/src/project.rs @@ -108,7 +108,7 @@ pub fn build_project_dylib(cfg: BuildConfig) -> Result { .into_iter() .find(|p| { if let Some(ext) = p.file_name() { - return ext.to_str().unwrap_or_default().contains(".wasm"); + return ext.to_string_lossy().contains(".wasm"); } false }) diff --git a/src/replay/hostio.rs b/src/replay/hostio.rs new file mode 100644 index 0000000..a048433 --- /dev/null +++ b/src/replay/hostio.rs @@ -0,0 +1,609 @@ +// Copyright 2022-2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +#![allow(unused)] + +use super::trace::{FrameReader, HostioKind::*}; +use function_name::named; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use std::{ + mem::{self, MaybeUninit}, + ptr::copy_nonoverlapping as memcpy, +}; + +lazy_static! { + pub static ref FRAME: Mutex> = Mutex::new(None); + pub static ref START_INK: Mutex = Mutex::new(0); + pub static ref END_INK: Mutex = Mutex::new(0); +} + +macro_rules! frame { + ($dec:pat) => { + let hostio = FRAME.lock().as_mut().unwrap().next_hostio(function_name!()); + *START_INK.lock() = hostio.start_ink; + *END_INK.lock() = hostio.end_ink; + + let $dec = hostio.kind else { unreachable!() }; + }; +} + +macro_rules! copy { + ($src:expr, $dest:expr) => { + memcpy($src.as_ptr(), $dest, mem::size_of_val(&$src)) + }; + ($src:expr, $dest:expr, $len:expr) => { + memcpy($src.as_ptr(), $dest, $len) + }; +} + +/// Reads the program calldata. The semantics are equivalent to that of the EVM's +/// [`CALLDATA_COPY`] opcode when requesting the entirety of the current call's calldata. +/// +/// [`CALLDATA_COPY`]: https://www.evm.codes/#37 +#[named] +#[no_mangle] +pub unsafe extern "C" fn read_args(dest: *mut u8) { + frame!(ReadArgs { args }); + copy!(args, dest, args.len()); +} + +/// Writes the final return data. If not called before the program exists, the return data will +/// be 0 bytes long. Note that this hostio does not cause the program to exit, which happens +/// naturally when `user_entrypoint` returns. +#[named] +#[no_mangle] +pub unsafe extern "C" fn write_result(data: *const u8, len: usize) { + frame!(WriteResult { result }); + assert_eq!(read_bytes(data, len), &*result); +} + +/// Reads a 32-byte value from permanent storage. Stylus's storage format is identical to +/// that of the EVM. This means that, under the hood, this hostio is accessing the 32-byte +/// value stored in the EVM state trie at offset `key`, which will be `0` when not previously +/// set. The semantics, then, are equivalent to that of the EVM's [`SLOAD`] opcode. +/// +/// [`SLOAD`]: https://www.evm.codes/#54 +#[named] +#[no_mangle] +pub unsafe extern "C" fn storage_load_bytes32(key_ptr: *const u8, dest: *mut u8) { + frame!(StorageLoadBytes32 { key, value }); + assert_eq!(read_fixed(key_ptr), key); + copy!(value, dest); +} + +/// Stores a 32-byte value to permanent storage. Stylus's storage format is identical to that +/// of the EVM. This means that, under the hood, this hostio is storing a 32-byte value into +/// the EVM state trie at offset `key`. Furthermore, refunds are tabulated exactly as in the +/// EVM. The semantics, then, are equivalent to that of the EVM's [`SSTORE`] opcode. +/// +/// [`SSTORE`]: https://www.evm.codes/#55 +#[named] +#[no_mangle] +pub unsafe extern "C" fn storage_store_bytes32(key_ptr: *const u8, value_ptr: *const u8) { + frame!(StorageStoreBytes32 { key, value }); + assert_eq!(read_fixed(key_ptr), key); + assert_eq!(read_fixed(value_ptr), value); +} + +/// Gets the ETH balance in wei of the account at the given address. +/// The semantics are equivalent to that of the EVM's [`BALANCE`] opcode. +/// +/// [`BALANCE`]: https://www.evm.codes/#31 +#[named] +#[no_mangle] +pub unsafe extern "C" fn account_balance(address_ptr: *const u8, dest: *mut u8) { + frame!(AccountBalance { address, balance }); + assert_eq!(read_fixed(address_ptr), address); + copy!(balance.to_be_bytes::<32>(), dest); +} + +/// Gets the code hash of the account at the given address. The semantics are equivalent +/// to that of the EVM's [`EXT_CODEHASH`] opcode. Note that the code hash of an account without +/// code will be the empty hash +/// `keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. +/// +/// [`EXT_CODEHASH`]: https://www.evm.codes/#3F +#[named] +#[no_mangle] +pub unsafe extern "C" fn account_codehash(address_ptr: *const u8, dest: *mut u8) { + frame!(AccountCodehash { address, codehash }); + assert_eq!(read_fixed(address_ptr), address); + copy!(codehash, dest); +} + +/// Gets the basefee of the current block. The semantics are equivalent to that of the EVM's +/// [`BASEFEE`] opcode. +/// +/// [`BASEFEE`]: https://www.evm.codes/#48 +#[named] +#[no_mangle] +pub unsafe extern "C" fn block_basefee(dest: *mut u8) { + frame!(BlockBasefee { basefee }); + copy!(basefee.to_be_bytes::<32>(), dest); +} + +/// Gets the coinbase of the current block, which on Arbitrum chains is the L1 batch poster's +/// address. This differs from Ethereum where the validator including the transaction +/// determines the coinbase. +#[named] +#[no_mangle] +pub unsafe extern "C" fn block_coinbase(dest: *mut u8) { + frame!(BlockCoinbase { coinbase }); + copy!(coinbase, dest); +} + +/// Gets the gas limit of the current block. The semantics are equivalent to that of the EVM's +/// [`GAS_LIMIT`] opcode. Note that as of the time of this writing, `evm.codes` incorrectly +/// implies that the opcode returns the gas limit of the current transaction. When in doubt, +/// consult [`The Ethereum Yellow Paper`]. +/// +/// [`GAS_LIMIT`]: https://www.evm.codes/#45 +/// [`The Ethereum Yellow Paper`]: https://ethereum.github.io/yellowpaper/paper.pdf +#[named] +#[no_mangle] +pub unsafe extern "C" fn block_gas_limit() -> u64 { + frame!(BlockGasLimit { limit }); + limit +} + +/// Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the +/// transaction. See [`Block Numbers and Time`] for more information on how this value is +/// determined. +/// +/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time +#[named] +#[no_mangle] +pub unsafe extern "C" fn block_number() -> u64 { + frame!(BlockNumber { number }); + number +} + +/// Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the +/// transaction. See [`Block Numbers and Time`] for more information on how this value is +/// determined. +/// +/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time +#[named] +#[no_mangle] +pub unsafe extern "C" fn block_timestamp() -> u64 { + frame!(BlockTimestamp { timestamp }); + timestamp +} + +/// Gets the unique chain identifier of the Arbitrum chain. The semantics are equivalent to +/// that of the EVM's [`CHAIN_ID`] opcode. +/// +/// [`CHAIN_ID`]: https://www.evm.codes/#46 +#[named] +#[no_mangle] +pub unsafe extern "C" fn chainid() -> u64 { + frame!(Chainid { chainid }); + chainid +} + +/// Calls the contract at the given address with options for passing value and to limit the +/// amount of gas supplied. The return status indicates whether the call succeeded, and is +/// nonzero on failure. +/// +/// In both cases `return_data_len` will store the length of the result, the bytes of which can +/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the +/// programmer can potentially save gas by choosing which subset of the return result they'd +/// like to copy. +/// +/// The semantics are equivalent to that of the EVM's [`CALL`] opcode, including callvalue +/// stipends and the 63/64 gas rule. This means that supplying the `u64::MAX` gas can be used +/// to send as much as possible. +/// +/// [`CALL`]: https://www.evm.codes/#f1 +#[named] +#[no_mangle] +pub unsafe extern "C" fn call_contract( + address_ptr: *const u8, + calldata: *const u8, + calldata_len: usize, + value_ptr: *const u8, + gas_supplied: u64, + return_data_len: *mut usize, +) -> u8 { + frame!(CallContract { + address, + data, + gas, + value, + outs_len, + status, + frame, + }); + assert_eq!(read_fixed(address_ptr), address); + assert_eq!(read_bytes(calldata, calldata_len), &*data); + assert_eq!(read_fixed(value_ptr), value.to_be_bytes::<32>()); + assert_eq!(gas_supplied, gas); + *return_data_len = outs_len as usize; + status +} + +/// Delegate calls the contract at the given address, with the option to limit the amount of +/// gas supplied. The return status indicates whether the call succeeded, and is nonzero on +/// failure. +/// +/// In both cases `return_data_len` will store the length of the result, the bytes of which +/// can be read via the `read_return_data` hostio. The bytes are not returned directly so that +/// the programmer can potentially save gas by choosing which subset of the return result +/// they'd like to copy. +/// +/// The semantics are equivalent to that of the EVM's [`DELEGATE_CALL`] opcode, including the +/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as +/// possible. +/// +/// [`DELEGATE_CALL`]: https://www.evm.codes/#F4 +#[named] +#[no_mangle] +pub unsafe extern "C" fn delegate_call_contract( + address_ptr: *const u8, + calldata: *const u8, + calldata_len: usize, + gas_supplied: u64, + return_data_len: *mut usize, +) -> u8 { + frame!(DelegateCallContract { + address, + data, + gas, + outs_len, + status, + frame, + }); + assert_eq!(read_fixed(address_ptr), address); + assert_eq!(read_bytes(calldata, calldata_len), &*data); + assert_eq!(gas_supplied, gas); + *return_data_len = outs_len as usize; + status +} + +/// Static calls the contract at the given address, with the option to limit the amount of gas +/// supplied. The return status indicates whether the call succeeded, and is nonzero on +/// failure. +/// +/// In both cases `return_data_len` will store the length of the result, the bytes of which can +/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the +/// programmer can potentially save gas by choosing which subset of the return result they'd +/// like to copy. +/// +/// The semantics are equivalent to that of the EVM's [`STATIC_CALL`] opcode, including the +/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as +/// possible. +/// +/// [`STATIC_CALL`]: https://www.evm.codes/#FA +#[named] +#[no_mangle] +pub unsafe extern "C" fn static_call_contract( + address_ptr: *const u8, + calldata: *const u8, + calldata_len: usize, + gas_supplied: u64, + return_data_len: *mut usize, +) -> u8 { + frame!(StaticCallContract { + address, + data, + gas, + outs_len, + status, + frame, + }); + assert_eq!(read_fixed(address_ptr), address); + assert_eq!(read_bytes(calldata, calldata_len), &*data); + assert_eq!(gas_supplied, gas); + *return_data_len = outs_len as usize; + status +} + +/// Gets the address of the current program. The semantics are equivalent to that of the EVM's +/// [`ADDRESS`] opcode. +/// +/// [`ADDRESS`]: https://www.evm.codes/#30 +#[named] +#[no_mangle] +pub unsafe extern "C" fn contract_address(dest: *mut u8) { + frame!(ContractAddress { address }); + copy!(address, dest); +} + +/// Deploys a new contract using the init code provided, which the EVM executes to construct +/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but +/// the code it deploys can be that of a Stylus program. The code returned will be treated as +/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be +/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`] +/// for more information on writing init code. +/// +/// On success, this hostio returns the address of the newly created account whose address is +/// a function of the sender and nonce. On failure the address will be `0`, `return_data_len` +/// will store the length of the revert data, the bytes of which can be read via the +/// `read_return_data` hostio. The semantics are equivalent to that of the EVM's [`CREATE`] +/// opcode, which notably includes the exact address returned. +/// +/// [`Deploying Stylus Programs`]: https://developer.arbitrum.io/TODO +/// [`CREATE`]: https://www.evm.codes/#f0 +#[named] +#[no_mangle] +pub unsafe extern "C" fn create1( + code_ptr: *const u8, + code_len: usize, + value: *const u8, + contract: *mut u8, + revert_data_len_ptr: *mut usize, +) { + frame!(Create1 { + code, + endowment, + address, + revert_data_len + }); + assert_eq!(read_bytes(code_ptr, code_len), &*code); + assert_eq!(read_fixed(value), endowment.to_be_bytes::<32>()); + copy!(address, contract); + *revert_data_len_ptr = revert_data_len; +} + +/// Deploys a new contract using the init code provided, which the EVM executes to construct +/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but +/// the code it deploys can be that of a Stylus program. The code returned will be treated as +/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be +/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`] +/// for more information on writing init code. +/// +/// On success, this hostio returns the address of the newly created account whose address is a +/// function of the sender, salt, and init code. On failure the address will be `0`, +/// `return_data_len` will store the length of the revert data, the bytes of which can be read +/// via the `read_return_data` hostio. The semantics are equivalent to that of the EVM's +/// `[CREATE2`] opcode, which notably includes the exact address returned. +/// +/// [`Deploying Stylus Programs`]: https://developer.arbitrum.io/TODO +/// [`CREATE2`]: https://www.evm.codes/#f5 +#[named] +#[no_mangle] +pub unsafe extern "C" fn create2( + code_ptr: *const u8, + code_len: usize, + value_ptr: *const u8, + salt_ptr: *const u8, + contract: *mut u8, + revert_data_len_ptr: *mut usize, +) { + frame!(Create2 { + code, + endowment, + salt, + address, + revert_data_len + }); + assert_eq!(read_bytes(code_ptr, code_len), &*code); + assert_eq!(read_fixed(value_ptr), endowment.to_be_bytes::<32>()); + assert_eq!(read_fixed(salt_ptr), salt); + copy!(address, contract); + *revert_data_len_ptr = revert_data_len; +} + +/// Emits an EVM log with the given number of topics and data, the first bytes of which should +/// be the 32-byte-aligned topic data. The semantics are equivalent to that of the EVM's +/// [`LOG0`], [`LOG1`], [`LOG2`], [`LOG3`], and [`LOG4`] opcodes based on the number of topics +/// specified. Requesting more than `4` topics will induce a revert. +/// +/// [`LOG0`]: https://www.evm.codes/#a0 +/// [`LOG1`]: https://www.evm.codes/#a1 +/// [`LOG2`]: https://www.evm.codes/#a2 +/// [`LOG3`]: https://www.evm.codes/#a3 +/// [`LOG4`]: https://www.evm.codes/#a4 +#[named] +#[no_mangle] +pub unsafe extern "C" fn emit_log(data_ptr: *const u8, len: usize, topic_count: usize) { + frame!(EmitLog { data, topics }); + assert_eq!(read_bytes(data_ptr, len), &*data); + assert_eq!(topics, topic_count); +} + +/// Gets the amount of gas left after paying for the cost of this hostio. The semantics are +/// equivalent to that of the EVM's [`GAS`] opcode. +/// +/// [`GAS`]: https://www.evm.codes/#5a +#[named] +#[no_mangle] +pub unsafe extern "C" fn evm_gas_left() -> u64 { + frame!(EvmGasLeft { gas_left }); + gas_left +} + +/// Gets the amount of ink remaining after paying for the cost of this hostio. The semantics +/// are equivalent to that of the EVM's [`GAS`] opcode, except the units are in ink. See +/// [`Ink and Gas`] for more information on Stylus's compute pricing. +/// +/// [`GAS`]: https://www.evm.codes/#5a +/// [`Ink and Gas`]: https://developer.arbitrum.io/TODO +#[named] +#[no_mangle] +pub unsafe extern "C" fn evm_ink_left() -> u64 { + frame!(EvmInkLeft { ink_left }); + ink_left +} + +/// The `entrypoint!` macro handles importing this hostio, which is required if the +/// program's memory grows. Otherwise compilation through the `ArbWasm` precompile will revert. +/// Internally the Stylus VM forces calls to this hostio whenever new WASM pages are allocated. +/// Calls made voluntarily will unproductively consume gas. +#[named] +#[no_mangle] +pub unsafe extern "C" fn memory_grow(new_pages: u16) { + frame!(MemoryGrow { pages }); + assert_eq!(new_pages, pages); +} + +/// Whether the current call is reentrant. +#[named] +#[no_mangle] +pub unsafe extern "C" fn msg_reentrant() -> bool { + frame!(MsgReentrant { reentrant }); + reentrant +} + +/// Gets the address of the account that called the program. For normal L2-to-L2 transactions +/// the semantics are equivalent to that of the EVM's [`CALLER`] opcode, including in cases +/// arising from [`DELEGATE_CALL`]. +/// +/// For L1-to-L2 retryable ticket transactions, the top-level sender's address will be aliased. +/// See [`Retryable Ticket Address Aliasing`] for more information on how this works. +/// +/// [`CALLER`]: https://www.evm.codes/#33 +/// [`DELEGATE_CALL`]: https://www.evm.codes/#f4 +/// [`Retryable Ticket Address Aliasing`]: https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing +#[named] +#[no_mangle] +pub unsafe extern "C" fn msg_sender(dest: *mut u8) { + frame!(MsgSender { sender }); + copy!(sender, dest); +} + +/// Get the ETH value in wei sent to the program. The semantics are equivalent to that of the +/// EVM's [`CALLVALUE`] opcode. +/// +/// [`CALLVALUE`]: https://www.evm.codes/#34 +#[named] +#[no_mangle] +pub unsafe extern "C" fn msg_value(dest: *mut u8) { + frame!(MsgValue { value }); + copy!(value, dest); +} + +/// Efficiently computes the [`keccak256`] hash of the given preimage. +/// The semantics are equivalent to that of the EVM's [`SHA3`] opcode. +/// +/// [`keccak256`]: https://en.wikipedia.org/wiki/SHA-3 +/// [`SHA3`]: https://www.evm.codes/#20 +#[named] +#[no_mangle] +pub unsafe extern "C" fn native_keccak256(bytes: *const u8, len: usize, output: *mut u8) { + frame!(NativeKeccak256 { preimage, digest }); + assert_eq!(read_bytes(bytes, len), &*preimage); + copy!(digest, output); +} + +/// Copies the bytes of the last EVM call or deployment return result. Does not revert if out of +/// bounds, but rather copies the overlapping portion. The semantics are otherwise equivalent +/// to that of the EVM's [`RETURN_DATA_COPY`] opcode. +/// +/// [`RETURN_DATA_COPY`]: https://www.evm.codes/#3e +#[named] +#[no_mangle] +pub unsafe extern "C" fn read_return_data( + dest: *mut u8, + offset_value: usize, + size_value: usize, +) -> usize { + frame!(ReadReturnData { offset, size, data }); + assert_eq!(offset_value, offset); + assert_eq!(size_value, size); + copy!(data, dest, data.len()); + data.len() +} + +/// Returns the length of the last EVM call or deployment return result, or `0` if neither have +/// happened during the program's execution. The semantics are equivalent to that of the EVM's +/// [`RETURN_DATA_SIZE`] opcode. +/// +/// [`RETURN_DATA_SIZE`]: https://www.evm.codes/#3d +#[named] +#[no_mangle] +pub unsafe extern "C" fn return_data_size() -> usize { + frame!(ReturnDataSize { size }); + size +} + +/// Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. The +/// semantics are equivalent to that of the EVM's [`GAS_PRICE`] opcode. +/// +/// [`GAS_PRICE`]: https://www.evm.codes/#3A +#[named] +#[no_mangle] +pub unsafe extern "C" fn tx_gas_price(dest: *mut u8) { + frame!(TxGasPrice { gas_price }); + copy!(gas_price.to_be_bytes::<32>(), dest); +} + +/// Gets the price of ink in evm gas basis points. See [`Ink and Gas`] for more information on +/// Stylus's compute-pricing model. +/// +/// [`Ink and Gas`]: https://developer.arbitrum.io/TODO +#[named] +#[no_mangle] +pub unsafe extern "C" fn tx_ink_price() -> u32 { + frame!(TxInkPrice { ink_price }); + ink_price +} + +/// Gets the top-level sender of the transaction. The semantics are equivalent to that of the +/// EVM's [`ORIGIN`] opcode. +/// +/// [`ORIGIN`]: https://www.evm.codes/#32 +#[named] +#[no_mangle] +pub unsafe extern "C" fn tx_origin(dest: *mut u8) { + frame!(TxOrigin { origin }); + copy!(origin, dest); +} + +/// Prints a 32-bit floating point number to the console. Only available in debug mode with +/// floating point enabled. +#[named] +#[no_mangle] +pub unsafe extern "C" fn log_f32(value: f32) { + frame!(ConsoleLog { text }); + println!("{text}"); +} + +/// Prints a 64-bit floating point number to the console. Only available in debug mode with +/// floating point enabled. +#[named] +#[no_mangle] +pub unsafe extern "C" fn log_f64(value: f64) { + frame!(ConsoleLog { text }); + println!("{text}"); +} + +/// Prints a 32-bit integer to the console, which can be either signed or unsigned. +/// Only available in debug mode. +#[named] +#[no_mangle] +pub unsafe extern "C" fn log_i32(value: i32) { + frame!(ConsoleLog { text }); + println!("{text}"); +} + +/// Prints a 64-bit integer to the console, which can be either signed or unsigned. +/// Only available in debug mode. +#[named] +#[no_mangle] +pub unsafe extern "C" fn log_i64(value: i64) { + frame!(ConsoleLog { text }); + println!("{text}"); +} + +/// Prints a UTF-8 encoded string to the console. Only available in debug mode. +#[named] +#[no_mangle] +pub unsafe extern "C" fn log_txt(text_ptr: *const u8, len: usize) { + frame!(ConsoleLogText { text }); + assert_eq!(read_bytes(text_ptr, len), &*text); +} + +unsafe fn read_fixed(ptr: *const u8) -> [u8; N] { + let mut value = MaybeUninit::<[u8; N]>::uninit(); + memcpy(ptr, value.as_mut_ptr() as *mut _, N); + value.assume_init() +} + +unsafe fn read_bytes(ptr: *const u8, len: usize) -> Vec { + let mut data = Vec::with_capacity(len); + memcpy(ptr, data.as_mut_ptr(), len); + data.set_len(len); + data +} diff --git a/src/replay/mod.rs b/src/replay/mod.rs new file mode 100644 index 0000000..6915be3 --- /dev/null +++ b/src/replay/mod.rs @@ -0,0 +1,104 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +use crate::{color::Color, util, ReplayConfig}; +use eyre::{bail, eyre, Result}; +use std::{ + os::unix::process::CommandExt, + path::{Path, PathBuf}, +}; + +use trace::Trace; + +mod hostio; +mod trace; + +pub async fn replay(config: ReplayConfig) -> Result<()> { + if !config.child { + let mut cmd = util::new_command("rust-gdb"); // TODO: fall back to gdb + cmd.arg("-ex=set breakpoint pending on"); + cmd.arg("-ex=b user_entrypoint"); + cmd.arg("-ex=r"); + cmd.arg("--args"); + + for arg in std::env::args() { + cmd.arg(arg); + } + cmd.arg("--child"); + let err = cmd.exec(); + + bail!("failed to exec gdb {}", err); + } + + let provider = util::new_provider(&config.endpoint)?; + + let trace = Trace::new(provider, config.tx).await?; + + build_so(&config.project, config.stable_rust)?; + let so = find_so(&config.project)?; + + // TODO: don't assume the contract is top-level + let args_len = trace.tx.input.len(); + + unsafe { + *hostio::FRAME.lock() = Some(trace.reader()); + + type Entrypoint = unsafe extern "C" fn(usize) -> usize; + let lib = libloading::Library::new(so)?; + let main: libloading::Symbol = lib.get(b"user_entrypoint")?; + + match main(args_len) { + 0 => println!("call completed successfully"), + 1 => println!("call reverted"), + x => println!("call exited with unknown status code: {}", x.red()), + } + } + Ok(()) +} + +pub fn build_so(path: &Path, stable: bool) -> Result<()> { + let mut cargo = util::new_command("cargo"); + + if !stable { + cargo.arg("+nightly"); + } + cargo + .current_dir(path) + .arg("build") + .arg("--lib") + .arg("--target") + .arg(rustc_host::from_cli()?) + .output()?; + Ok(()) +} + +pub fn find_so(project: &Path) -> Result { + let triple = rustc_host::from_cli()?; + let so_dir = project.join(format!("target/{triple}/debug/")); + let so_dir = std::fs::read_dir(&so_dir) + .map_err(|e| eyre!("failed to open {}: {e}", so_dir.to_string_lossy()))? + .filter_map(|r| r.ok()) + .map(|r| r.path()) + .filter(|r| r.is_file()); + + let mut file: Option = None; + for entry in so_dir { + let Some(ext) = entry.file_name() else { + continue; + }; + if ext.to_string_lossy().contains(".so") { + if let Some(other) = file { + let other = other.file_name().unwrap().to_string_lossy(); + bail!( + "more than one .so found: {other} and {}", + ext.to_string_lossy() + ); + } + file = Some(entry); + } + } + let Some(file) = file else { + bail!("failed to find .so"); + }; + Ok(file) +} diff --git a/src/replay/query.js b/src/replay/query.js new file mode 100644 index 0000000..43802f2 --- /dev/null +++ b/src/replay/query.js @@ -0,0 +1,26 @@ +{ + "hostio": function(info) { + if (this.nests.includes(info.name)) { + info.info = this.open.pop(); + } + this.open.push(info); + }, + "enter": function(frame) { + let inner = []; + this.open.push({ + address: frame.getTo(), + steps: inner, + }); + + this.stack.push(this.open); // save where we were + this.open = inner; + }, + "exit": function(result) { + this.open = this.stack.pop(); + }, + "result": function() { return this.open; }, + "fault": function() { return this.open; }, + stack: [], + open: [], + nests: ["call_contract", "delegate_call_contract", "static_call_contract"] +} diff --git a/src/replay/trace.rs b/src/replay/trace.rs new file mode 100644 index 0000000..52fe011 --- /dev/null +++ b/src/replay/trace.rs @@ -0,0 +1,568 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md + +#![allow(clippy::redundant_closure_call)] + +use crate::color::{Color, DebugColor}; +use alloy_primitives::{Address, TxHash, B256, U256}; +use ethers::{ + providers::{JsonRpcClient, Middleware, Provider}, + types::{ + GethDebugTracerType, GethDebugTracingOptions, GethTrace, Transaction, TransactionReceipt, + }, + utils::__serde_json::Value, +}; +use eyre::{bail, Result}; +use sneks::SimpleSnakeNames; +use std::{collections::VecDeque, mem}; + +#[derive(Debug)] +pub struct Trace { + top_frame: TraceFrame, + pub receipt: TransactionReceipt, + pub tx: Transaction, +} + +impl Trace { + pub async fn new(provider: Provider, tx: TxHash) -> Result { + let hash = tx.0.into(); + + let Some(receipt) = provider.get_transaction_receipt(hash).await? else { + bail!("failed to get receipt for tx: {}", hash) + }; + let Some(tx) = provider.get_transaction(hash).await? else { + bail!("failed to get tx data: {}", hash) + }; + + let query = include_str!("query.js"); + let tracer = GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::JsTracer(query.to_owned())), + ..GethDebugTracingOptions::default() + }; + let GethTrace::Unknown(trace) = provider.debug_trace_transaction(hash, tracer).await? + else { + bail!("malformed tracing result") + }; + + let to = receipt.to.map(|x| Address::from(x.0)); + let top_frame = TraceFrame::parse_frame(to, trace)?; + + Ok(Self { + top_frame, + receipt, + tx, + }) + } + + pub fn reader(self) -> FrameReader { + FrameReader { + steps: self.top_frame.steps.clone().into(), + frame: self.top_frame, + } + } +} + +#[derive(Clone, Debug)] +pub struct TraceFrame { + steps: Vec, + address: Option
, +} + +impl TraceFrame { + fn new(address: Option
) -> Self { + let steps = vec![]; + Self { steps, address } + } + + pub fn parse_frame(address: Option
, array: Value) -> Result { + let mut frame = TraceFrame::new(address); + + let Value::Array(array) = array else { + bail!("not an array: {}", array); + }; + + for step in array { + let Value::Object(mut keys) = step else { + bail!("not a valid step: {}", step); + }; + + macro_rules! get_typed { + ($keys:expr, $ty:ident, $name:expr) => {{ + let value = match $keys.remove($name) { + Some(name) => name, + None => bail!("object missing {}: {:?}", $name, $keys), + }; + match value { + Value::$ty(string) => string, + x => bail!("unexpected type for {}: {}", $name, x), + } + }}; + } + macro_rules! get_int { + ($name:expr) => { + get_typed!(keys, Number, $name).as_u64().unwrap() + }; + } + + let name = get_typed!(keys, String, "name"); + let args = get_typed!(keys, Array, "args"); + let outs = get_typed!(keys, Array, "outs"); + + let mut args = args.as_slice(); + let mut outs = outs.as_slice(); + + let start_ink = get_int!("startInk"); + let end_ink = get_int!("endInk"); + + fn read_data(values: &[Value]) -> Result> { + let mut vec = vec![]; + for value in values { + let Value::Number(byte) = value else { + bail!("expected a byte but found {value}"); + }; + let byte = byte.as_u64().unwrap(); + if byte > 255 { + bail!("expected a byte but found {byte}"); + }; + vec.push(byte as u8); + } + Ok(vec.into_boxed_slice()) + } + + macro_rules! read_data { + ($src:ident) => {{ + let data = read_data(&$src)?; + $src = &[]; + data + }}; + } + macro_rules! read_ty { + ($src:ident, $ty:ident, $conv:expr) => {{ + let size = mem::size_of::<$ty>(); + let data = read_data(&$src[..size])?; + $src = &$src[size..]; + $conv(&data[..]) + }}; + } + macro_rules! read_string { + ($src:ident) => {{ + let conv = |x: &[_]| String::from_utf8_lossy(&x).to_string(); + read_ty!($src, String, conv) + }}; + } + macro_rules! read_u256 { + ($src:ident) => { + read_ty!($src, U256, |x| B256::from_slice(x).into()) + }; + } + macro_rules! read_b256 { + ($src:ident) => { + read_ty!($src, B256, B256::from_slice) + }; + } + macro_rules! read_address { + ($src:ident) => { + read_ty!($src, Address, Address::from_slice) + }; + } + macro_rules! read_num { + ($src:ident, $ty:ident) => {{ + let conv = |x: &[_]| $ty::from_be_bytes(x.try_into().unwrap()); + read_ty!($src, $ty, conv) + }}; + } + macro_rules! read_u8 { + ($src:ident) => { + read_num!($src, u8) + }; + } + macro_rules! read_u16 { + ($src:ident) => { + read_num!($src, u16) + }; + } + macro_rules! read_u32 { + ($src:ident) => { + read_num!($src, u32) + }; + } + macro_rules! read_u64 { + ($src:ident) => { + read_num!($src, u64) + }; + } + macro_rules! read_usize { + ($src:ident) => { + read_num!($src, usize) + }; + } + + macro_rules! frame { + () => {{ + let mut info = get_typed!(keys, Object, "info"); + + // geth uses the pattern { "0": Number, "1": Number, ... } + let address = get_typed!(info, Object, "address"); + let mut address: Vec<_> = address.into_iter().collect(); + address.sort_by_key(|x| x.0.parse::().unwrap()); + let address: Vec<_> = address.into_iter().map(|x| x.1).collect(); + let address = Address::from_slice(&*read_data(&address)?); + + let steps = info.remove("steps").unwrap(); + TraceFrame::parse_frame(Some(address), steps)? + }}; + } + + use HostioKind::*; + let kind = match name.as_str() { + "user_entrypoint" => UserEntrypoint { + args_len: read_u32!(args), + }, + "user_returned" => UserReturned { + status: read_u32!(outs), + }, + "read_args" => ReadArgs { + args: read_data!(outs), + }, + "write_result" => WriteResult { + result: read_data!(args), + }, + "storage_load_bytes32" => StorageLoadBytes32 { + key: read_b256!(args), + value: read_b256!(outs), + }, + "storage_store_bytes32" => StorageStoreBytes32 { + key: read_b256!(args), + value: read_b256!(args), + }, + "account_balance" => AccountBalance { + address: read_address!(args), + balance: read_u256!(outs), + }, + "account_codehash" => AccountCodehash { + address: read_address!(args), + codehash: read_b256!(outs), + }, + "block_basefee" => BlockBasefee { + basefee: read_u256!(outs), + }, + "block_coinbase" => BlockCoinbase { + coinbase: read_address!(outs), + }, + "block_gas_limit" => BlockGasLimit { + limit: read_u64!(outs), + }, + "block_number" => BlockNumber { + number: read_u64!(outs), + }, + "block_timestamp" => BlockTimestamp { + timestamp: read_u64!(outs), + }, + "chainid" => Chainid { + chainid: read_u64!(outs), + }, + "contract_address" => ContractAddress { + address: read_address!(outs), + }, + "evm_gas_left" => EvmGasLeft { + gas_left: read_u64!(outs), + }, + "evm_ink_left" => EvmInkLeft { + ink_left: read_u64!(outs), + }, + "msg_reentrant" => MsgReentrant { + reentrant: read_u32!(outs) != 0, + }, + "msg_sender" => MsgSender { + sender: read_address!(outs), + }, + "msg_value" => MsgValue { + value: read_b256!(outs), + }, + "native_keccak256" => NativeKeccak256 { + preimage: read_data!(args), + digest: read_b256!(outs), + }, + "tx_gas_price" => TxGasPrice { + gas_price: read_u256!(outs), + }, + "tx_ink_price" => TxInkPrice { + ink_price: read_u32!(outs), + }, + "tx_origin" => TxOrigin { + origin: read_address!(outs), + }, + "memory_grow" => MemoryGrow { + pages: read_u16!(args), + }, + "call_contract" => CallContract { + address: read_address!(args), + gas: read_u64!(args), + value: read_u256!(args), + data: read_data!(args), + outs_len: read_u32!(outs), + status: read_u8!(outs), + frame: frame!(), + }, + "delegate_call_contract" => DelegateCallContract { + address: read_address!(args), + gas: read_u64!(args), + data: read_data!(args), + outs_len: read_u32!(outs), + status: read_u8!(outs), + frame: frame!(), + }, + "static_call_contract" => StaticCallContract { + address: read_address!(args), + gas: read_u64!(args), + data: read_data!(args), + outs_len: read_u32!(outs), + status: read_u8!(outs), + frame: frame!(), + }, + "create1" => Create1 { + endowment: read_u256!(args), + code: read_data!(args), + address: read_address!(outs), + revert_data_len: read_usize!(outs), + }, + "create2" => Create2 { + endowment: read_u256!(args), + salt: read_b256!(args), + code: read_data!(args), + address: read_address!(outs), + revert_data_len: read_usize!(outs), + }, + "emit_log" => EmitLog { + topics: read_usize!(args), + data: read_data!(args), + }, + "read_return_data" => ReadReturnData { + offset: read_usize!(args), + size: read_usize!(args), + data: read_data!(outs), + }, + "return_data_size" => ReturnDataSize { + size: read_usize!(outs), + }, + "console_log_text" => ConsoleLogText { + text: read_data!(args), + }, + "console_log" => ConsoleLog { + text: read_string!(args), + }, + x => todo!("Missing hostio {x}"), + }; + + assert!(args.is_empty(), "{name}"); + assert!(outs.is_empty(), "{name}"); + + frame.steps.push(Hostio { + kind, + start_ink, + end_ink, + }); + } + Ok(frame) + } +} + +#[derive(Clone, Debug)] +pub struct Hostio { + pub kind: HostioKind, + pub start_ink: u64, + pub end_ink: u64, +} + +#[derive(Clone, Debug, SimpleSnakeNames)] +pub enum HostioKind { + UserEntrypoint { + args_len: u32, + }, + UserReturned { + status: u32, + }, + ReadArgs { + args: Box<[u8]>, + }, + WriteResult { + result: Box<[u8]>, + }, + StorageLoadBytes32 { + key: B256, + value: B256, + }, + StorageStoreBytes32 { + key: B256, + value: B256, + }, + AccountBalance { + address: Address, + balance: U256, + }, + AccountCodehash { + address: Address, + codehash: B256, + }, + BlockBasefee { + basefee: U256, + }, + BlockCoinbase { + coinbase: Address, + }, + BlockGasLimit { + limit: u64, + }, + BlockNumber { + number: u64, + }, + BlockTimestamp { + timestamp: u64, + }, + Chainid { + chainid: u64, + }, + ContractAddress { + address: Address, + }, + EvmGasLeft { + gas_left: u64, + }, + EvmInkLeft { + ink_left: u64, + }, + MemoryGrow { + pages: u16, + }, + MsgReentrant { + reentrant: bool, + }, + MsgSender { + sender: Address, + }, + MsgValue { + value: B256, + }, + NativeKeccak256 { + preimage: Box<[u8]>, + digest: B256, + }, + TxGasPrice { + gas_price: U256, + }, + TxInkPrice { + ink_price: u32, + }, + TxOrigin { + origin: Address, + }, + ConsoleLog { + text: String, + }, + ConsoleLogText { + text: Box<[u8]>, + }, + CallContract { + address: Address, + data: Box<[u8]>, + gas: u64, + value: U256, + outs_len: u32, + status: u8, + frame: TraceFrame, + }, + DelegateCallContract { + address: Address, + data: Box<[u8]>, + gas: u64, + outs_len: u32, + status: u8, + frame: TraceFrame, + }, + StaticCallContract { + address: Address, + data: Box<[u8]>, + gas: u64, + outs_len: u32, + status: u8, + frame: TraceFrame, + }, + Create1 { + code: Box<[u8]>, + endowment: U256, + address: Address, + revert_data_len: usize, + }, + Create2 { + code: Box<[u8]>, + endowment: U256, + salt: B256, + address: Address, + revert_data_len: usize, + }, + EmitLog { + data: Box<[u8]>, + topics: usize, + }, + ReadReturnData { + offset: usize, + size: usize, + data: Box<[u8]>, + }, + ReturnDataSize { + size: usize, + }, +} + +#[derive(Debug)] +pub struct FrameReader { + frame: TraceFrame, + steps: VecDeque, +} + +impl FrameReader { + fn next(&mut self) -> Result { + match self.steps.pop_front() { + Some(item) => Ok(item), + None => bail!("No next hostio"), + } + } + + pub fn next_hostio(&mut self, expected: &'static str) -> Hostio { + fn detected(reader: &FrameReader, expected: &'static str) { + let expected = expected.red(); + let which = match reader.frame.address { + Some(call) => format!("call to {}", call.red()), + None => "contract deployment".to_string(), + }; + println!("{}", "\n════════ Divergence ════════".red()); + println!("Divegence detected while simulating a {which} via local assembly."); + println!("The simulated environment expected a call to the {expected} Host I/O.",); + } + + loop { + let Ok(hostio) = self.next() else { + detected(self, expected); + println!("However, no such call is made onchain. Are you sure this the right contract?\n"); + panic!(); + }; + + if hostio.kind.name() == expected { + return hostio; + } + + let kind = hostio.kind; + let name = kind.name(); + match name { + "memory_grow" | "user_entrypoint" | "user_returned" => continue, + _ => { + detected(self, expected); + println!("However, onchain there's a call to {name}. Are you sure this the right contract?\n"); + println!("expected: {}", expected.red()); + println!("but have: {}\n", kind.debug_red()); + panic!(); + } + } + } + } +} From ab50e86d962ab04486c33db9a7544efc6ae05c21 Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Mon, 2 Oct 2023 14:19:23 -0600 Subject: [PATCH 2/4] add cargo trace --- src/export_abi.rs | 6 +-- src/main.rs | 91 +++++++++++++++++++++++++++------------------ src/replay/mod.rs | 21 +++++++---- src/replay/trace.rs | 7 ++-- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/src/export_abi.rs b/src/export_abi.rs index fdac03b..8f8111b 100644 --- a/src/export_abi.rs +++ b/src/export_abi.rs @@ -1,6 +1,6 @@ // Copyright 2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md -use eyre::{bail, eyre}; +use eyre::{bail, eyre, Result}; use std::{ fs::File, path::PathBuf, @@ -8,7 +8,7 @@ use std::{ }; /// Exports the Solidity ABI for a Stylus Rust program in the current directory to stdout. -pub fn export_solidity_abi(release: bool, output_file: Option) -> eyre::Result<()> { +pub fn export_solidity_abi(release: bool, output_file: Option) -> Result<()> { let target_host = rustc_host::from_cli().map_err(|e| eyre!("could not get host target architecture: {e}"))?; let mut cmd = Command::new("cargo"); @@ -48,7 +48,7 @@ pub fn export_solidity_abi(release: bool, output_file: Option) -> eyre: /// Exports the Solidity JSON ABI output from solc given a Stylus Rust project in the current directory. /// The solc binary must be installed for this command to succeed. -pub fn export_json_abi(release: bool, output_file: Option) -> eyre::Result<()> { +pub fn export_json_abi(release: bool, output_file: Option) -> Result<()> { // We first check if solc is installed. let output = Command::new("solc") .arg("--version") diff --git a/src/main.rs b/src/main.rs index 237ce9f..313d903 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,8 @@ use alloy_primitives::TxHash; use clap::{Args, Parser, ValueEnum}; -use color::Color; use ethers::types::H160; -use eyre::Result; +use eyre::{eyre, Context, Result}; use std::path::PathBuf; use tokio::runtime::Builder; @@ -47,11 +46,11 @@ struct CGenArgs { #[command(propagate_version = true)] struct StylusArgs { #[command(subcommand)] - command: StylusSubcommands, + command: Subcommands, } #[derive(Parser, Debug, Clone)] -enum StylusSubcommands { +enum Subcommands { /// Create a new Rust project. New { /// Project name. @@ -83,9 +82,12 @@ enum StylusSubcommands { /// Replay a transaction in gdb. #[command(alias = "r")] Replay(ReplayConfig), + /// Trace a transaction. + #[command(alias = "t")] + Trace(TraceConfig), } -#[derive(Debug, Args, Clone)] +#[derive(Args, Clone, Debug)] pub struct CheckConfig { /// RPC endpoint of the Stylus node to connect to. #[arg(short, long, default_value = "https://stylus-testnet.arbitrum.io/rpc")] @@ -114,7 +116,7 @@ pub struct CheckConfig { nightly: bool, } -#[derive(Debug, Args, Clone)] +#[derive(Args, Clone, Debug)] pub struct DeployConfig { #[command(flatten)] check_cfg: CheckConfig, @@ -133,7 +135,7 @@ pub struct DeployConfig { tx_sending_opts: TxSendingOpts, } -#[derive(Debug, Args, Clone)] +#[derive(Args, Clone, Debug)] pub struct ReplayConfig { /// RPC endpoint. #[arg(short, long, default_value = "http://localhost:8545")] @@ -152,6 +154,20 @@ pub struct ReplayConfig { child: bool, } +#[derive(Args, Clone, Debug)] +pub struct TraceConfig { + /// RPC endpoint. + #[arg(short, long, default_value = "http://localhost:8545")] + endpoint: String, + /// Tx to replay. + #[arg(short, long)] + tx: TxHash, + /// Project path. + #[arg(short, long, default_value = ".")] + project: PathBuf, +} + + #[derive(Debug, Clone, ValueEnum)] pub enum DeployMode { DeployOnly, @@ -193,7 +209,7 @@ fn main() -> Result<()> { // use the current thread for replay let mut runtime = match &args.command { - StylusSubcommands::Replay(_) => Builder::new_current_thread(), + Subcommands::Replay(_) => Builder::new_current_thread(), _ => Builder::new_multi_thread(), }; @@ -202,43 +218,44 @@ fn main() -> Result<()> { } async fn main_impl(args: StylusArgs) -> Result<()> { + macro_rules! run { + ($expr:expr, $($msg:expr),+) => { + $expr.wrap_err_with(|| eyre!($($msg),+))? + }; + } + match args.command { - StylusSubcommands::New { name, minimal } => { - if let Err(e) = new::new_stylus_project(&name, minimal) { - println!( - "Could not create new stylus project with name {name}: {}", - e.pink() - ); - }; + Subcommands::New { name, minimal } => { + run!( + new::new_stylus_project(&name, minimal), + "failed to create project" + ); } - StylusSubcommands::ExportAbi { + Subcommands::ExportAbi { release, json, output, - } => { - if json { - if let Err(e) = export_abi::export_json_abi(release, output) { - println!( - "Could not export Stylus program Solidity ABI as JSON: {}", - e.pink() - ); - }; - } else if let Err(e) = export_abi::export_solidity_abi(release, output) { - println!("Could not export Stylus program Solidity ABI: {}", e.pink()); - } + } => match json { + true => run!( + export_abi::export_json_abi(release, output), + "failed to export json" + ), + false => run!( + export_abi::export_solidity_abi(release, output), + "failed to export abi" + ), + }, + Subcommands::Check(config) => { + run!(check::run_checks(config).await, "stylus checks failed"); } - StylusSubcommands::Check(config) => { - if let Err(e) = check::run_checks(config).await { - println!("Stylus checks failed: {}", e.pink()); - }; + Subcommands::Deploy(config) => { + run!(deploy::deploy(config).await, "failed to deploy"); } - StylusSubcommands::Deploy(config) => { - if let Err(e) = deploy::deploy(config).await { - println!("Deploy / activation command failed: {}", e.pink()); - }; + Subcommands::Replay(config) => { + run!(replay::replay(config).await, "failed to replay tx"); } - StylusSubcommands::Replay(config) => { - replay::replay(config).await?; + Subcommands::Trace(config) => { + run!(replay::trace(config).await, "failed to trace"); } } Ok(()) diff --git a/src/replay/mod.rs b/src/replay/mod.rs index 6915be3..6d5c728 100644 --- a/src/replay/mod.rs +++ b/src/replay/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023, Offchain Labs, Inc. -// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md -use crate::{color::Color, util, ReplayConfig}; +use crate::{color::Color, util, ReplayConfig, TraceConfig}; use eyre::{bail, eyre, Result}; use std::{ os::unix::process::CommandExt, @@ -31,7 +31,6 @@ pub async fn replay(config: ReplayConfig) -> Result<()> { } let provider = util::new_provider(&config.endpoint)?; - let trace = Trace::new(provider, config.tx).await?; build_so(&config.project, config.stable_rust)?; @@ -56,6 +55,13 @@ pub async fn replay(config: ReplayConfig) -> Result<()> { Ok(()) } +pub async fn trace(config: TraceConfig) -> Result<()> { + let provider = util::new_provider(&config.endpoint)?; + let trace = Trace::new(provider, config.tx).await?; + println!("{}", trace.json); + Ok(()) +} + pub fn build_so(path: &Path, stable: bool) -> Result<()> { let mut cargo = util::new_command("cargo"); @@ -86,13 +92,12 @@ pub fn find_so(project: &Path) -> Result { let Some(ext) = entry.file_name() else { continue; }; - if ext.to_string_lossy().contains(".so") { + let ext = ext.to_string_lossy(); + + if ext.contains(".so") { if let Some(other) = file { let other = other.file_name().unwrap().to_string_lossy(); - bail!( - "more than one .so found: {other} and {}", - ext.to_string_lossy() - ); + bail!("more than one .so found: {ext} and {other}",); } file = Some(entry); } diff --git a/src/replay/trace.rs b/src/replay/trace.rs index 52fe011..90b23c5 100644 --- a/src/replay/trace.rs +++ b/src/replay/trace.rs @@ -21,6 +21,7 @@ pub struct Trace { top_frame: TraceFrame, pub receipt: TransactionReceipt, pub tx: Transaction, + pub json: Value, } impl Trace { @@ -39,18 +40,18 @@ impl Trace { tracer: Some(GethDebugTracerType::JsTracer(query.to_owned())), ..GethDebugTracingOptions::default() }; - let GethTrace::Unknown(trace) = provider.debug_trace_transaction(hash, tracer).await? - else { + let GethTrace::Unknown(json) = provider.debug_trace_transaction(hash, tracer).await? else { bail!("malformed tracing result") }; let to = receipt.to.map(|x| Address::from(x.0)); - let top_frame = TraceFrame::parse_frame(to, trace)?; + let top_frame = TraceFrame::parse_frame(to, json.clone())?; Ok(Self { top_frame, receipt, tx, + json, }) } From f747dd5330b7301883fa76e3f15534c439adb756 Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Mon, 2 Oct 2023 14:33:24 -0600 Subject: [PATCH 3/4] vanilla fallback --- src/main.rs | 1 - src/replay/mod.rs | 15 ++++++++++++++- src/util.rs | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 313d903..a74c562 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,7 +167,6 @@ pub struct TraceConfig { project: PathBuf, } - #[derive(Debug, Clone, ValueEnum)] pub enum DeployMode { DeployOnly, diff --git a/src/replay/mod.rs b/src/replay/mod.rs index 6d5c728..b58de3f 100644 --- a/src/replay/mod.rs +++ b/src/replay/mod.rs @@ -15,7 +15,20 @@ mod trace; pub async fn replay(config: ReplayConfig) -> Result<()> { if !config.child { - let mut cmd = util::new_command("rust-gdb"); // TODO: fall back to gdb + let rust_gdb = util::command_exists("rust-gdb"); + if !rust_gdb { + println!( + "{} not installed, falling back to {}", + "rust-gdb".red(), + "gdb".red() + ); + } + + let mut cmd = match rust_gdb { + true => util::new_command("rust-gdb"), + false => util::new_command("gdb"), + }; + cmd.arg("--quiet"); cmd.arg("-ex=set breakpoint pending on"); cmd.arg("-ex=b user_entrypoint"); cmd.arg("-ex=r"); diff --git a/src/util.rs b/src/util.rs index bfdee39..a3acb03 100644 --- a/src/util.rs +++ b/src/util.rs @@ -23,3 +23,13 @@ pub fn new_command>(program: S) -> Command { command.stdout(Stdio::inherit()).stderr(Stdio::inherit()); command } + +pub fn command_exists>(program: S) -> bool { + Command::new(program) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("--version") + .output() + .map(|x| x.status.success()) + .unwrap_or_default() +} From 0e5d367b3af5a7d92aebfd4cbdb31735ca80b9f4 Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Mon, 2 Oct 2023 18:21:48 -0600 Subject: [PATCH 4/4] remove Windows CI --- .github/workflows/windows.yml | 56 ----------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/windows.yml diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index 195c457..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: windows -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: windows-latest - name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) - env: - CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }} - strategy: - matrix: - target: [ - i686-pc-windows-gnu, - i686-pc-windows-msvc, - x86_64-pc-windows-gnu, - x86_64-pc-windows-msvc, - ] - cfg_release_channel: [nightly, stable] - - steps: - # The Windows runners have autocrlf enabled by default - # which causes failures for some of rustfmt's line-ending sensitive tests - - name: disable git eol translation - run: git config --global core.autocrlf false - - name: checkout - uses: actions/checkout@v3 - - # Run build - - name: Install Rustup using win.rustup.rs - run: | - # Disable the download progress bar which can cause perf issues - $ProgressPreference = "SilentlyContinue" - Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe - .\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --default-toolchain=none - del rustup-init.exe - rustup target add ${{ matrix.target }} - shell: powershell - - - name: Add mingw32 to path for i686-gnu - run: | - echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH - if: matrix.target == 'i686-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - - name: Add mingw64 to path for x86_64-gnu - run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH - if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - - name: Build and Test - shell: cmd - run: ci\build_and_test.bat \ No newline at end of file