From ff91596cc7ee5c7f9b45c024be746f32981580d1 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 9 Jan 2024 21:14:36 +0100 Subject: [PATCH] feat: fork from `reth-revm-inspectors` --- .github/workflows/ci.yml | 84 +++ .github/workflows/deps.yml | 18 + .gitignore | 3 + CHANGELOG.md | 14 + Cargo.toml | 43 +- LICENSE-APACHE | 176 +++++++ LICENSE-MIT | 23 + README.md | 24 + clippy.toml | 1 + deny.toml | 54 ++ release.toml | 10 + rustfmt.toml | 12 + src/access_list.rs | 99 ++++ src/lib.rs | 38 +- src/stack/maybe_owned.rs | 178 +++++++ src/stack/mod.rs | 215 ++++++++ src/tracing/arena.rs | 92 ++++ src/tracing/builder/geth.rs | 325 ++++++++++++ src/tracing/builder/mod.rs | 10 + src/tracing/builder/parity.rs | 633 +++++++++++++++++++++++ src/tracing/builder/walker.rs | 39 ++ src/tracing/config.rs | 225 ++++++++ src/tracing/fourbyte.rs | 78 +++ src/tracing/js/bigint.js | 1 + src/tracing/js/bindings.rs | 936 ++++++++++++++++++++++++++++++++++ src/tracing/js/builtins.rs | 258 ++++++++++ src/tracing/js/mod.rs | 583 +++++++++++++++++++++ src/tracing/mod.rs | 563 ++++++++++++++++++++ src/tracing/opcount.rs | 29 ++ src/tracing/types.rs | 684 +++++++++++++++++++++++++ src/tracing/utils.rs | 91 ++++ 31 files changed, 5523 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deps.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 clippy.toml create mode 100644 deny.toml create mode 100644 release.toml create mode 100644 rustfmt.toml create mode 100644 src/access_list.rs create mode 100644 src/stack/maybe_owned.rs create mode 100644 src/stack/mod.rs create mode 100644 src/tracing/arena.rs create mode 100644 src/tracing/builder/geth.rs create mode 100644 src/tracing/builder/mod.rs create mode 100644 src/tracing/builder/parity.rs create mode 100644 src/tracing/builder/walker.rs create mode 100644 src/tracing/config.rs create mode 100644 src/tracing/fourbyte.rs create mode 100644 src/tracing/js/bigint.js create mode 100644 src/tracing/js/bindings.rs create mode 100644 src/tracing/js/builtins.rs create mode 100644 src/tracing/js/mod.rs create mode 100644 src/tracing/mod.rs create mode 100644 src/tracing/opcount.rs create mode 100644 src/tracing/types.rs create mode 100644 src/tracing/utils.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..581b9e5c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: test ${{ matrix.rust }} ${{ matrix.flags }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + rust: ["stable", "nightly", "1.74"] # MSRV + flags: ["", "--all-features"] + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + # Only run tests on latest stable and above + - name: build + if: ${{ matrix.rust == '1.74' }} # MSRV + run: cargo build --workspace ${{ matrix.flags }} + - name: test + if: ${{ matrix.rust != '1.74' }} # MSRV + run: cargo test --workspace ${{ matrix.flags }} + + feature-checks: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@cargo-hack + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: cargo hack + run: cargo hack check --feature-powerset --depth 2 + + clippy: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@clippy + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo clippy --workspace --all-targets --all-features + env: + RUSTFLAGS: -Dwarnings + + docs: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo doc --workspace --all-features --no-deps --document-private-items + env: + RUSTDOCFLAGS: "--cfg docsrs -D warnings" + + fmt: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - run: cargo fmt --all --check diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml new file mode 100644 index 00000000..9df7e564 --- /dev/null +++ b/.github/workflows/deps.yml @@ -0,0 +1,18 @@ +name: deps + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: [cron: "00 00 * * *"] + +jobs: + cargo-deny: + name: cargo deny check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check all diff --git a/.gitignore b/.gitignore index ea8c4bf7..5cb8c46e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +Cargo.lock +.vscode +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eaa6ceb6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2024-01-XX + +### Added + +- Initial release, forked from `reth-revm-inspectors` diff --git a/Cargo.toml b/Cargo.toml index ad14c1ea..db9894ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,47 @@ [package] -name = "evm-inspectors" +name = "reth-revm-inspectors" +description = "Revm inspector implementations" version = "0.1.0" edition = "2021" +rust-version = "1.74.0" +license = "MIT OR Apache-2.0" +homepage = "https://github.com/paradigmxyz/evm-inspectors" +repository = "https://github.com/paradigmxyz/evm-inspectors" +categories = ["cryptography"] +keywords = ["ethereum", "evm", "inspectors", "tracing", "debugging"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +rust.missing_debug_implementations = "warn" +rust.missing_docs = "warn" +rust.unreachable_pub = "warn" +rustdoc.all = "warn" +rust.unused_must_use = "deny" +rust.rust_2018_idioms = "deny" [dependencies] +# eth +alloy-sol-types = "0.5" +alloy-primitives = "0.5" +alloy-rpc-types = "0.1" +alloy-rpc-trace-types = "0.1" + +revm = "3.4" + +# js-tracing-inspector +boa_engine = { version = "0.17", optional = true } +boa_gc = { version = "0.17", optional = true } + +serde = { version = "1", features = ["derive"] } +thiserror = { version = "1", optional = true } +serde_json = { version = "1", optional = true } + +tokio = { version = "1", features = ["sync"], optional = true } + +[features] +default = [] +js-tracer = ["boa_engine", "boa_gc", "tokio", "thiserror", "serde_json"] + +[patch.crates-io] +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" } +alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy" } +revm = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze" } diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..17b64aca --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# evm-inspectors + +Common [`revm`] inspector implementations. + +Originally part of [`reth`] as the [`reth-revm-inspectors`] crate. + +[`revm`]: https://github.com/bluealloy/revm/ +[`reth`]: https://github.com/paradigmxyz/reth/ +[`reth-revm-inspectors`]: https://github.com/paradigmxyz/reth/tree/3fdb24ebd328e9d22d1e6849a3a869e3c4e83485/crates/revm/revm-inspectors/ + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in these crates by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..866add68 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.74" diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..8d7bf3ea --- /dev/null +++ b/deny.toml @@ -0,0 +1,54 @@ +[advisories] +vulnerability = "deny" +unmaintained = "warn" +unsound = "warn" +yanked = "warn" +notice = "warn" + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" + +[licenses] +unlicensed = "deny" +confidence-threshold = 0.9 +# copyleft = "deny" + +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "Unlicense", + "MPL-2.0", + # https://github.com/briansmith/ring/issues/902 + "LicenseRef-ring", + # https://github.com/briansmith/webpki/issues/148 + "LicenseRef-webpki", +] + +exceptions = [ + # CC0 is a permissive license but somewhat unclear status for source code + # so we prefer to not have dependencies using it + # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal + { allow = ["CC0-1.0"], name = "tiny-keccak" }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[[licenses.clarify]] +name = "webpki" +expression = "LicenseRef-webpki" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/release.toml b/release.toml new file mode 100644 index 00000000..12bd6dc4 --- /dev/null +++ b/release.toml @@ -0,0 +1,10 @@ +# Configuration file for [`cargo-release`](https://github.com/crate-ci/cargo-release) +# See: https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md + +allow-branch = ["main"] +sign-commit = true +sign-tag = true +shared-version = true +pre-release-commit-message = "chore: release {{version}}" +tag-prefix = "" # tag only once instead of per every crate +pre-release-hook = ["sh", "-c", "$WORKSPACE_ROOT/scripts/changelog.sh --tag {{version}}"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..3063df70 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,12 @@ +reorder_imports = true +use_field_init_shorthand = true +use_small_heuristics = "Max" + +# Nightly +max_width = 100 +comment_width = 100 +imports_granularity = "Crate" +wrap_comments = true +format_code_in_doc_comments = true +doc_comment_code_block_width = 100 +format_macro_matchers = true diff --git a/src/access_list.rs b/src/access_list.rs new file mode 100644 index 00000000..e591a4f1 --- /dev/null +++ b/src/access_list.rs @@ -0,0 +1,99 @@ +use alloy_primitives::{Address, B256}; +use alloy_rpc_types::{AccessList, AccessListItem}; +use revm::{ + interpreter::{opcode, Interpreter}, + Database, EVMData, Inspector, +}; +use std::collections::{BTreeSet, HashMap, HashSet}; + +/// An [Inspector] that collects touched accounts and storage slots. +/// +/// This can be used to construct an [AccessList] for a transaction via `eth_createAccessList` +#[derive(Default, Debug)] +pub struct AccessListInspector { + /// All addresses that should be excluded from the final accesslist + excluded: HashSet
, + /// All addresses and touched slots + access_list: HashMap>, +} + +impl AccessListInspector { + /// Creates a new inspector instance + /// + /// The `access_list` is the provided access list from the call request + pub fn new( + access_list: AccessList, + from: Address, + to: Address, + precompiles: impl IntoIterator, + ) -> Self { + AccessListInspector { + excluded: [from, to].into_iter().chain(precompiles).collect(), + access_list: access_list + .0 + .into_iter() + .map(|v| (v.address, v.storage_keys.into_iter().collect())) + .collect(), + } + } + + /// Returns list of addresses and storage keys used by the transaction. It gives you the list of + /// addresses and storage keys that were touched during execution. + pub fn into_access_list(self) -> AccessList { + let items = self.access_list.into_iter().map(|(address, slots)| AccessListItem { + address, + storage_keys: slots.into_iter().collect(), + }); + AccessList(items.collect()) + } + + /// Returns list of addresses and storage keys used by the transaction. It gives you the list of + /// addresses and storage keys that were touched during execution. + pub fn access_list(&self) -> AccessList { + let items = self.access_list.iter().map(|(address, slots)| AccessListItem { + address: *address, + storage_keys: slots.iter().copied().collect(), + }); + AccessList(items.collect()) + } +} + +impl Inspector for AccessListInspector +where + DB: Database, +{ + fn step(&mut self, interpreter: &mut Interpreter<'_>, _data: &mut EVMData<'_, DB>) { + match interpreter.current_opcode() { + opcode::SLOAD | opcode::SSTORE => { + if let Ok(slot) = interpreter.stack().peek(0) { + let cur_contract = interpreter.contract.address; + self.access_list + .entry(cur_contract) + .or_default() + .insert(B256::from(slot.to_be_bytes())); + } + } + opcode::EXTCODECOPY | + opcode::EXTCODEHASH | + opcode::EXTCODESIZE | + opcode::BALANCE | + opcode::SELFDESTRUCT => { + if let Ok(slot) = interpreter.stack().peek(0) { + let addr = Address::from_word(B256::from(slot.to_be_bytes())); + if !self.excluded.contains(&addr) { + self.access_list.entry(addr).or_default(); + } + } + } + opcode::DELEGATECALL | opcode::CALL | opcode::STATICCALL | opcode::CALLCODE => { + if let Ok(slot) = interpreter.stack().peek(1) { + let addr = Address::from_word(B256::from(slot.to_be_bytes())); + if !self.excluded.contains(&addr) { + self.access_list.entry(addr).or_default(); + } + } + } + _ => (), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9af..6e2b1a3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,24 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +//! revm [Inspector](revm::Inspector) implementations, such as call tracers +//! +//! ## Feature Flags +//! +//! - `js-tracer`: Enables a JavaScript tracer implementation. This pulls in extra +//! dependencies (such as `boa`, `tokio` and `serde_json`). + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", + html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", + issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +/// An inspector implementation for an EIP2930 Accesslist +pub mod access_list; + +/// An inspector stack abstracting the implementation details of +/// each inspector and allowing to hook on block/transaction execution, +/// used in the main RETH executor. +pub mod stack; + +/// An inspector for recording traces +pub mod tracing; diff --git a/src/stack/maybe_owned.rs b/src/stack/maybe_owned.rs new file mode 100644 index 00000000..c02ce6e4 --- /dev/null +++ b/src/stack/maybe_owned.rs @@ -0,0 +1,178 @@ +use alloy_primitives::U256; +use revm::{ + interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter}, + primitives::{db::Database, Address, Bytes, B256}, + EVMData, Inspector, +}; +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; + +/// An [Inspector] that is either owned by an individual [Inspector] or is shared as part of a +/// series of inspectors in a [InspectorStack](crate::stack::InspectorStack). +/// +/// Caution: if the [Inspector] is _stacked_ then it _must_ be called first. +#[derive(Debug)] +pub enum MaybeOwnedInspector { + /// Inspector is owned. + Owned(Rc>), + /// Inspector is shared and part of a stack + Stacked(Rc>), +} + +impl MaybeOwnedInspector { + /// Create a new _owned_ instance + pub fn new_owned(inspector: INSP) -> Self { + MaybeOwnedInspector::Owned(Rc::new(RefCell::new(inspector))) + } + + /// Creates a [MaybeOwnedInspector::Stacked] clone of this type. + pub fn clone_stacked(&self) -> Self { + match self { + MaybeOwnedInspector::Owned(gas) | MaybeOwnedInspector::Stacked(gas) => { + MaybeOwnedInspector::Stacked(Rc::clone(gas)) + } + } + } + + /// Returns a reference to the inspector. + pub fn as_ref(&self) -> Ref<'_, INSP> { + match self { + MaybeOwnedInspector::Owned(insp) => insp.borrow(), + MaybeOwnedInspector::Stacked(insp) => insp.borrow(), + } + } +} + +impl MaybeOwnedInspector { + /// Create a new _owned_ instance + pub fn owned() -> Self { + Self::new_owned(Default::default()) + } +} + +impl Default for MaybeOwnedInspector { + fn default() -> Self { + Self::owned() + } +} + +impl Clone for MaybeOwnedInspector { + fn clone(&self) -> Self { + self.clone_stacked() + } +} + +impl Inspector for MaybeOwnedInspector +where + DB: Database, + INSP: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + match self { + MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().initialize_interp(interp, data), + MaybeOwnedInspector::Stacked(_) => {} + } + } + + fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + match self { + MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().step(interp, data), + MaybeOwnedInspector::Stacked(_) => {} + } + } + + fn log( + &mut self, + evm_data: &mut EVMData<'_, DB>, + address: &Address, + topics: &[B256], + data: &Bytes, + ) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().log(evm_data, address, topics, data) + } + MaybeOwnedInspector::Stacked(_) => {} + } + } + + fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + match self { + MaybeOwnedInspector::Owned(insp) => insp.borrow_mut().step_end(interp, data), + MaybeOwnedInspector::Stacked(_) => {} + } + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + ) -> (InstructionResult, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => return insp.borrow_mut().call(data, inputs), + MaybeOwnedInspector::Stacked(_) => {} + } + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + remaining_gas: Gas, + ret: InstructionResult, + out: Bytes, + ) -> (InstructionResult, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().call_end(data, inputs, remaining_gas, ret, out) + } + MaybeOwnedInspector::Stacked(_) => {} + } + (ret, remaining_gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option
, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => return insp.borrow_mut().create(data, inputs), + MaybeOwnedInspector::Stacked(_) => {} + } + + (InstructionResult::Continue, None, Gas::new(0), Bytes::default()) + } + + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + ret: InstructionResult, + address: Option
, + remaining_gas: Gas, + out: Bytes, + ) -> (InstructionResult, Option
, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().create_end(data, inputs, ret, address, remaining_gas, out) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + (ret, address, remaining_gas, out) + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().selfdestruct(contract, target, value) + } + MaybeOwnedInspector::Stacked(_) => {} + } + } +} diff --git a/src/stack/mod.rs b/src/stack/mod.rs new file mode 100644 index 00000000..f8ea9179 --- /dev/null +++ b/src/stack/mod.rs @@ -0,0 +1,215 @@ +use alloy_primitives::{Address, Bytes, B256, U256}; +use revm::{ + inspectors::CustomPrintTracer, + interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter}, + primitives::Env, + Database, EVMData, Inspector, +}; +use std::fmt::Debug; + +/// A wrapped [Inspector] that can be reused in the stack +mod maybe_owned; +pub use maybe_owned::MaybeOwnedInspector; + +/// One can hook on inspector execution in 3 ways: +/// - Block: Hook on block execution +/// - BlockWithIndex: Hook on block execution transaction index +/// - Transaction: Hook on a specific transaction hash +#[derive(Debug, Default, Clone)] +pub enum Hook { + #[default] + /// No hook. + None, + /// Hook on a specific block. + Block(u64), + /// Hook on a specific transaction hash. + Transaction(B256), + /// Hooks on every transaction in a block. + All, +} + +/// An inspector that calls multiple inspectors in sequence. +/// +/// If a call to an inspector returns a value other than [InstructionResult::Continue] (or +/// equivalent) the remaining inspectors are not called. +#[derive(Default, Clone)] +pub struct InspectorStack { + /// An inspector that prints the opcode traces to the console. + pub custom_print_tracer: Option, + /// The provided hook + pub hook: Hook, +} + +impl Debug for InspectorStack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InspectorStack") + .field("custom_print_tracer", &self.custom_print_tracer.is_some()) + .field("hook", &self.hook) + .finish() + } +} + +impl InspectorStack { + /// Create a new inspector stack. + pub fn new(config: InspectorStackConfig) -> Self { + let mut stack = InspectorStack { hook: config.hook, ..Default::default() }; + + if config.use_printer_tracer { + stack.custom_print_tracer = Some(CustomPrintTracer::default()); + } + + stack + } + + /// Check if the inspector should be used. + pub fn should_inspect(&self, env: &Env, tx_hash: B256) -> bool { + match self.hook { + Hook::None => false, + Hook::Block(block) => env.block.number.to::() == block, + Hook::Transaction(hash) => hash == tx_hash, + Hook::All => true, + } + } +} + +/// Configuration for the inspectors. +#[derive(Debug, Default)] +pub struct InspectorStackConfig { + /// Enable revm inspector printer. + /// In execution this will print opcode level traces directly to console. + pub use_printer_tracer: bool, + + /// Hook on a specific block or transaction. + pub hook: Hook, +} + +/// Helper macro to call the same method on multiple inspectors without resorting to dynamic +/// dispatch +#[macro_export] +macro_rules! call_inspectors { + ($id:ident, [ $($inspector:expr),+ ], $call:block) => { + $({ + if let Some($id) = $inspector { + $call; + } + })+ + } +} + +impl Inspector for InspectorStack +where + DB: Database, +{ + fn initialize_interp(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + inspector.initialize_interp(interpreter, data); + }); + } + + fn step(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + inspector.step(interpreter, data); + }); + } + + fn log( + &mut self, + evm_data: &mut EVMData<'_, DB>, + address: &Address, + topics: &[B256], + data: &Bytes, + ) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + inspector.log(evm_data, address, topics, data); + }); + } + + fn step_end(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + inspector.step_end(interpreter, data); + }); + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + ) -> (InstructionResult, Gas, Bytes) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + let (status, gas, retdata) = inspector.call(data, inputs); + + // Allow inspectors to exit early + if status != InstructionResult::Continue { + return (status, gas, retdata) + } + }); + + (InstructionResult::Continue, Gas::new(inputs.gas_limit), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + remaining_gas: Gas, + ret: InstructionResult, + out: Bytes, + ) -> (InstructionResult, Gas, Bytes) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + let (new_ret, new_gas, new_out) = + inspector.call_end(data, inputs, remaining_gas, ret, out.clone()); + + // If the inspector returns a different ret or a revert with a non-empty message, + // we assume it wants to tell us something + if new_ret != ret || (new_ret == InstructionResult::Revert && new_out != out) { + return (new_ret, new_gas, new_out) + } + }); + + (ret, remaining_gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option
, Gas, Bytes) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + let (status, addr, gas, retdata) = inspector.create(data, inputs); + + // Allow inspectors to exit early + if status != InstructionResult::Continue { + return (status, addr, gas, retdata) + } + }); + + (InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::new()) + } + + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + ret: InstructionResult, + address: Option
, + remaining_gas: Gas, + out: Bytes, + ) -> (InstructionResult, Option
, Gas, Bytes) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + let (new_ret, new_address, new_gas, new_retdata) = + inspector.create_end(data, inputs, ret, address, remaining_gas, out.clone()); + + if new_ret != ret { + return (new_ret, new_address, new_gas, new_retdata) + } + }); + + (ret, address, remaining_gas, out) + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + call_inspectors!(inspector, [&mut self.custom_print_tracer], { + Inspector::::selfdestruct(inspector, contract, target, value); + }); + } +} diff --git a/src/tracing/arena.rs b/src/tracing/arena.rs new file mode 100644 index 00000000..cb7c6b51 --- /dev/null +++ b/src/tracing/arena.rs @@ -0,0 +1,92 @@ +use crate::tracing::types::{CallTrace, CallTraceNode, LogCallOrder}; + +/// An arena of recorded traces. +/// +/// This type will be populated via the [TracingInspector](crate::tracing::TracingInspector). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallTraceArena { + /// The arena of recorded trace nodes + pub(crate) arena: Vec, +} + +impl CallTraceArena { + /// Pushes a new trace into the arena, returning the trace ID + /// + /// This appends a new trace to the arena, and also inserts a new entry in the node's parent + /// node children set if `attach_to_parent` is `true`. E.g. if calls to precompiles should + /// not be included in the call graph this should be called with [PushTraceKind::PushOnly]. + pub(crate) fn push_trace( + &mut self, + mut entry: usize, + kind: PushTraceKind, + new_trace: CallTrace, + ) -> usize { + loop { + match new_trace.depth { + // The entry node, just update it + 0 => { + self.arena[0].trace = new_trace; + return 0 + } + // We found the parent node, add the new trace as a child + _ if self.arena[entry].trace.depth == new_trace.depth - 1 => { + let id = self.arena.len(); + let node = CallTraceNode { + parent: Some(entry), + trace: new_trace, + idx: id, + ..Default::default() + }; + self.arena.push(node); + + // also track the child in the parent node + if kind.is_attach_to_parent() { + let parent = &mut self.arena[entry]; + let trace_location = parent.children.len(); + parent.ordering.push(LogCallOrder::Call(trace_location)); + parent.children.push(id); + } + + return id + } + _ => { + // We haven't found the parent node, go deeper + entry = *self.arena[entry].children.last().expect("Disconnected trace"); + } + } + } + } + + /// Returns the nodes in the arena + pub fn nodes(&self) -> &[CallTraceNode] { + &self.arena + } + + /// Consumes the arena and returns the nodes + pub fn into_nodes(self) -> Vec { + self.arena + } +} + +/// How to push a trace into the arena +pub(crate) enum PushTraceKind { + /// This will _only_ push the trace into the arena. + PushOnly, + /// This will push the trace into the arena, and also insert a new entry in the node's parent + /// node children set. + PushAndAttachToParent, +} + +impl PushTraceKind { + #[inline] + fn is_attach_to_parent(&self) -> bool { + matches!(self, PushTraceKind::PushAndAttachToParent) + } +} + +impl Default for CallTraceArena { + fn default() -> Self { + // The first node is the root node + CallTraceArena { arena: vec![Default::default()] } + } +} diff --git a/src/tracing/builder/geth.rs b/src/tracing/builder/geth.rs new file mode 100644 index 00000000..b4a6b654 --- /dev/null +++ b/src/tracing/builder/geth.rs @@ -0,0 +1,325 @@ +//! Geth trace builder + +use crate::tracing::{ + types::{CallTraceNode, CallTraceStepStackItem}, + utils::load_account_code, + TracingInspectorConfig, +}; +use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_rpc_trace_types::geth::{ + AccountChangeKind, AccountState, CallConfig, CallFrame, DefaultFrame, DiffMode, + GethDefaultTracingOptions, PreStateConfig, PreStateFrame, PreStateMode, StructLog, +}; +use revm::{db::DatabaseRef, primitives::ResultAndState}; +use std::collections::{btree_map::Entry, BTreeMap, HashMap, VecDeque}; + +/// A type for creating geth style traces +#[derive(Clone, Debug)] +pub struct GethTraceBuilder { + /// Recorded trace nodes. + nodes: Vec, + /// How the traces were recorded + _config: TracingInspectorConfig, +} + +impl GethTraceBuilder { + /// Returns a new instance of the builder + pub fn new(nodes: Vec, _config: TracingInspectorConfig) -> Self { + Self { nodes, _config } + } + + /// Fill in the geth trace with all steps of the trace and its children traces in the order they + /// appear in the transaction. + fn fill_geth_trace( + &self, + main_trace_node: &CallTraceNode, + opts: &GethDefaultTracingOptions, + storage: &mut HashMap>, + struct_logs: &mut Vec, + ) { + // A stack with all the steps of the trace and all its children's steps. + // This is used to process the steps in the order they appear in the transactions. + // Steps are grouped by their Call Trace Node, in order to process them all in the order + // they appear in the transaction, we need to process steps of call nodes when they appear. + // When we find a call step, we push all the steps of the child trace on the stack, so they + // are processed next. The very next step is the last item on the stack + let mut step_stack = VecDeque::with_capacity(main_trace_node.trace.steps.len()); + + main_trace_node.push_steps_on_stack(&mut step_stack); + + // Iterate over the steps inside the given trace + while let Some(CallTraceStepStackItem { trace_node, step, call_child_id }) = + step_stack.pop_back() + { + let mut log = step.convert_to_geth_struct_log(opts); + + // Fill in memory and storage depending on the options + if opts.is_storage_enabled() { + let contract_storage = storage.entry(step.contract).or_default(); + if let Some(change) = step.storage_change { + contract_storage.insert(change.key.into(), change.value.into()); + log.storage = Some(contract_storage.clone()); + } + } + + if opts.is_return_data_enabled() { + log.return_data = Some(trace_node.trace.output.clone()); + } + + // Add step to geth trace + struct_logs.push(log); + + // If the step is a call, we first push all the steps of the child trace on the stack, + // so they are processed next + if let Some(call_child_id) = call_child_id { + let child_trace = &self.nodes[call_child_id]; + child_trace.push_steps_on_stack(&mut step_stack); + } + } + } + + /// Generate a geth-style trace e.g. for `debug_traceTransaction` + /// + /// This expects the gas used and return value for the + /// [ExecutionResult](revm::primitives::ExecutionResult) of the executed transaction. + pub fn geth_traces( + &self, + receipt_gas_used: u64, + return_value: Bytes, + opts: GethDefaultTracingOptions, + ) -> DefaultFrame { + if self.nodes.is_empty() { + return Default::default() + } + // Fetch top-level trace + let main_trace_node = &self.nodes[0]; + let main_trace = &main_trace_node.trace; + + let mut struct_logs = Vec::new(); + let mut storage = HashMap::new(); + self.fill_geth_trace(main_trace_node, &opts, &mut storage, &mut struct_logs); + + DefaultFrame { + // If the top-level trace succeeded, then it was a success + failed: !main_trace.success, + gas: receipt_gas_used, + return_value, + struct_logs, + } + } + + /// Generate a geth-style traces for the call tracer. + /// + /// This decodes all call frames from the recorded traces. + /// + /// This expects the gas used and return value for the + /// [ExecutionResult](revm::primitives::ExecutionResult) of the executed transaction. + pub fn geth_call_traces(&self, opts: CallConfig, gas_used: u64) -> CallFrame { + if self.nodes.is_empty() { + return Default::default() + } + + let include_logs = opts.with_log.unwrap_or_default(); + // first fill up the root + let main_trace_node = &self.nodes[0]; + let mut root_call_frame = main_trace_node.geth_empty_call_frame(include_logs); + root_call_frame.gas_used = U256::from(gas_used); + + // selfdestructs are not recorded as individual call traces but are derived from + // the call trace and are added as additional `CallFrame` objects to the parent call + if let Some(selfdestruct) = main_trace_node.geth_selfdestruct_call_trace() { + root_call_frame.calls.push(selfdestruct); + } + + if opts.only_top_call.unwrap_or_default() { + return root_call_frame + } + + // fill all the call frames in the root call frame with the recorded traces. + // traces are identified by their index in the arena + // so we can populate the call frame tree by walking up the call tree + let mut call_frames = Vec::with_capacity(self.nodes.len()); + call_frames.push((0, root_call_frame)); + + for (idx, trace) in self.nodes.iter().enumerate().skip(1) { + // selfdestructs are not recorded as individual call traces but are derived from + // the call trace and are added as additional `CallFrame` objects to the parent call + if let Some(selfdestruct) = trace.geth_selfdestruct_call_trace() { + call_frames.last_mut().expect("not empty").1.calls.push(selfdestruct); + } + call_frames.push((idx, trace.geth_empty_call_frame(include_logs))); + } + + // pop the _children_ calls frame and move it to the parent + // this will roll up the child frames to their parent; this works because `child idx > + // parent idx` + loop { + let (idx, call) = call_frames.pop().expect("call frames not empty"); + let node = &self.nodes[idx]; + if let Some(parent) = node.parent { + let parent_frame = &mut call_frames[parent]; + // we need to ensure that calls are in order they are called: the last child node is + // the last call, but since we walk up the tree, we need to always + // insert at position 0 + parent_frame.1.calls.insert(0, call); + } else { + debug_assert!(call_frames.is_empty(), "only one root node has no parent"); + return call + } + } + } + + /// Returns the accounts necessary for transaction execution. + /// + /// The prestate mode returns the accounts necessary to execute a given transaction. + /// diff_mode returns the differences between the transaction's pre and post-state. + /// + /// * `state` - The state post-transaction execution. + /// * `diff_mode` - if prestate is in diff or prestate mode. + /// * `db` - The database to fetch state pre-transaction execution. + pub fn geth_prestate_traces( + &self, + ResultAndState { state, .. }: &ResultAndState, + prestate_config: PreStateConfig, + db: DB, + ) -> Result { + let account_diffs = state.into_iter().map(|(addr, acc)| (*addr, acc)); + + if prestate_config.is_default_mode() { + let mut prestate = PreStateMode::default(); + // in default mode we __only__ return the touched state + for node in self.nodes.iter() { + let addr = node.trace.address; + + let acc_state = match prestate.0.entry(addr) { + Entry::Vacant(entry) => { + let db_acc = db.basic_ref(addr)?.unwrap_or_default(); + let code = load_account_code(&db, &db_acc); + let acc_state = + AccountState::from_account_info(db_acc.nonce, db_acc.balance, code); + entry.insert(acc_state) + } + Entry::Occupied(entry) => entry.into_mut(), + }; + + for (key, value) in node.touched_slots() { + match acc_state.storage.entry(key.into()) { + Entry::Vacant(entry) => { + entry.insert(value.into()); + } + Entry::Occupied(_) => { + // we've already recorded this slot + } + } + } + } + + // also need to check changed accounts for things like balance changes etc + for (addr, changed_acc) in account_diffs { + let acc_state = match prestate.0.entry(addr) { + Entry::Vacant(entry) => { + let db_acc = db.basic_ref(addr)?.unwrap_or_default(); + let code = load_account_code(&db, &db_acc); + let acc_state = + AccountState::from_account_info(db_acc.nonce, db_acc.balance, code); + entry.insert(acc_state) + } + Entry::Occupied(entry) => { + // already recorded via touched accounts + entry.into_mut() + } + }; + + // in case we missed anything during the trace, we need to add the changed accounts + // storage + for (key, slot) in changed_acc.storage.iter() { + match acc_state.storage.entry((*key).into()) { + Entry::Vacant(entry) => { + entry.insert(slot.previous_or_original_value.into()); + } + Entry::Occupied(_) => { + // we've already recorded this slot + } + } + } + } + + Ok(PreStateFrame::Default(prestate)) + } else { + let mut state_diff = DiffMode::default(); + let mut account_change_kinds = HashMap::with_capacity(account_diffs.len()); + for (addr, changed_acc) in account_diffs { + let db_acc = db.basic_ref(addr)?.unwrap_or_default(); + + let pre_code = load_account_code(&db, &db_acc); + + let mut pre_state = + AccountState::from_account_info(db_acc.nonce, db_acc.balance, pre_code); + + let mut post_state = AccountState::from_account_info( + changed_acc.info.nonce, + changed_acc.info.balance, + changed_acc.info.code.as_ref().map(|code| code.original_bytes()), + ); + + // handle storage changes + for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) + { + pre_state.storage.insert((*key).into(), slot.previous_or_original_value.into()); + post_state.storage.insert((*key).into(), slot.present_value.into()); + } + + state_diff.pre.insert(addr, pre_state); + state_diff.post.insert(addr, post_state); + + // determine the change type + let pre_change = if changed_acc.is_created() { + AccountChangeKind::Create + } else { + AccountChangeKind::Modify + }; + let post_change = if changed_acc.is_selfdestructed() { + AccountChangeKind::SelfDestruct + } else { + AccountChangeKind::Modify + }; + + account_change_kinds.insert(addr, (pre_change, post_change)); + } + + // ensure we're only keeping changed entries + state_diff.retain_changed().remove_zero_storage_values(); + + self.diff_traces(&mut state_diff.pre, &mut state_diff.post, account_change_kinds); + Ok(PreStateFrame::Diff(state_diff)) + } + } + + /// Returns the difference between the pre and post state of the transaction depending on the + /// kind of changes of that account (pre,post) + fn diff_traces( + &self, + pre: &mut BTreeMap, + post: &mut BTreeMap, + change_type: HashMap, + ) { + post.retain(|addr, post_state| { + // Don't keep destroyed accounts in the post state + if change_type.get(addr).map(|ty| ty.1.is_selfdestruct()).unwrap_or(false) { + return false + } + if let Some(pre_state) = pre.get(addr) { + // remove any unchanged account info + post_state.remove_matching_account_info(pre_state); + } + + true + }); + + // Don't keep created accounts the pre state + pre.retain(|addr, _pre_state| { + // only keep accounts that are not created + change_type.get(addr).map(|ty| !ty.0.is_created()).unwrap_or(true) + }); + } +} diff --git a/src/tracing/builder/mod.rs b/src/tracing/builder/mod.rs new file mode 100644 index 00000000..e6e58d8c --- /dev/null +++ b/src/tracing/builder/mod.rs @@ -0,0 +1,10 @@ +//! Builder types for building traces + +/// Geth style trace builders for `debug_` namespace +pub mod geth; + +/// Parity style trace builders for `trace_` namespace +pub mod parity; + +/// Walker types used for traversing various callgraphs +mod walker; diff --git a/src/tracing/builder/parity.rs b/src/tracing/builder/parity.rs new file mode 100644 index 00000000..a36b155f --- /dev/null +++ b/src/tracing/builder/parity.rs @@ -0,0 +1,633 @@ +use super::walker::CallTraceNodeWalkerBF; +use crate::tracing::{ + types::{CallTraceNode, CallTraceStep}, + utils::load_account_code, + TracingInspectorConfig, +}; +use alloy_primitives::{Address, U64}; +use alloy_rpc_trace_types::parity::*; +use alloy_rpc_types::TransactionInfo; +use revm::{ + db::DatabaseRef, + interpreter::{ + opcode::{self, spec_opcode_gas}, + OpCode, + }, + primitives::{Account, ExecutionResult, ResultAndState, SpecId, KECCAK_EMPTY}, +}; +use std::collections::{HashSet, VecDeque}; + +/// A type for creating parity style traces +/// +/// Note: Parity style traces always ignore calls to precompiles. +#[derive(Clone, Debug)] +pub struct ParityTraceBuilder { + /// Recorded trace nodes + nodes: Vec, + /// The spec id of the EVM. + spec_id: Option, + + /// How the traces were recorded + _config: TracingInspectorConfig, +} + +impl ParityTraceBuilder { + /// Returns a new instance of the builder + pub fn new( + nodes: Vec, + spec_id: Option, + _config: TracingInspectorConfig, + ) -> Self { + Self { nodes, spec_id, _config } + } + + /// Returns a list of all addresses that appeared as callers. + pub fn callers(&self) -> HashSet
{ + self.nodes.iter().map(|node| node.trace.caller).collect() + } + + /// Manually the gas used of the root trace. + /// + /// The root trace's gasUsed should mirror the actual gas used by the transaction. + /// + /// This allows setting it manually by consuming the execution result's gas for example. + #[inline] + pub fn set_transaction_gas_used(&mut self, gas_used: u64) { + if let Some(node) = self.nodes.first_mut() { + node.trace.gas_used = gas_used; + } + } + + /// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the + /// type. + #[inline] + pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self { + self.set_transaction_gas_used(gas_used); + self + } + + /// Returns the trace addresses of all call nodes in the set + /// + /// Each entry in the returned vector represents the [Self::trace_address] of the corresponding + /// node in the nodes set. + /// + /// CAUTION: This also includes precompiles, which have an empty trace address. + fn trace_addresses(&self) -> Vec> { + let mut all_addresses = Vec::with_capacity(self.nodes.len()); + for idx in 0..self.nodes.len() { + all_addresses.push(self.trace_address(idx)); + } + all_addresses + } + + /// Returns the `traceAddress` of the node in the arena + /// + /// The `traceAddress` field of all returned traces, gives the exact location in the call trace + /// [index in root, index in first CALL, index in second CALL, …]. + /// + /// # Panics + /// + /// if the `idx` does not belong to a node + /// + /// Note: if the call node of `idx` is a precompile, the returned trace address will be empty. + fn trace_address(&self, idx: usize) -> Vec { + if idx == 0 { + // root call has empty traceAddress + return vec![] + } + let mut graph = vec![]; + let mut node = &self.nodes[idx]; + if node.is_precompile() { + return graph + } + while let Some(parent) = node.parent { + // the index of the child call in the arena + let child_idx = node.idx; + node = &self.nodes[parent]; + // find the index of the child call in the parent node + let call_idx = node + .children + .iter() + .position(|child| *child == child_idx) + .expect("non precompile child call exists in parent"); + graph.push(call_idx); + } + graph.reverse(); + graph + } + + /// Returns an iterator over all nodes to trace + /// + /// This excludes nodes that represent calls to precompiles. + fn iter_traceable_nodes(&self) -> impl Iterator { + self.nodes.iter().filter(|node| !node.is_precompile()) + } + + /// Returns an iterator over all recorded traces for `trace_transaction` + pub fn into_localized_transaction_traces_iter( + self, + info: TransactionInfo, + ) -> impl Iterator { + self.into_transaction_traces_iter().map(move |trace| { + let TransactionInfo { hash, index, block_hash, block_number, .. } = info; + LocalizedTransactionTrace { + trace, + transaction_position: index, + transaction_hash: hash, + block_number, + block_hash, + } + }) + } + + /// Returns all recorded traces for `trace_transaction` + pub fn into_localized_transaction_traces( + self, + info: TransactionInfo, + ) -> Vec { + self.into_localized_transaction_traces_iter(info).collect() + } + + /// Consumes the inspector and returns the trace results according to the configured trace + /// types. + /// + /// Warning: If `trace_types` contains [TraceType::StateDiff] the returned [StateDiff] will not + /// be filled. Use [ParityTraceBuilder::into_trace_results_with_state] or + /// [populate_state_diff] to populate the balance and nonce changes for the [StateDiff] + /// using the [DatabaseRef]. + pub fn into_trace_results( + self, + res: &ExecutionResult, + trace_types: &HashSet, + ) -> TraceResults { + let gas_used = res.gas_used(); + let output = res.output().cloned().unwrap_or_default(); + + let (trace, vm_trace, state_diff) = self.into_trace_type_traces(trace_types); + + let mut trace = + TraceResults { output, trace: trace.unwrap_or_default(), vm_trace, state_diff }; + + // we're setting the gas used of the root trace explicitly to the gas used of the execution + // result + trace.set_root_trace_gas_used(gas_used); + + trace + } + + /// Consumes the inspector and returns the trace results according to the configured trace + /// types. + /// + /// This also takes the [DatabaseRef] to populate the balance and nonce changes for the + /// [StateDiff]. + /// + /// Note: this is considered a convenience method that takes the state map of + /// [ResultAndState] after inspecting a transaction + /// with the [TracingInspector](crate::tracing::TracingInspector). + pub fn into_trace_results_with_state( + self, + res: &ResultAndState, + trace_types: &HashSet, + db: DB, + ) -> Result { + let ResultAndState { ref result, ref state } = res; + + let breadth_first_addresses = if trace_types.contains(&TraceType::VmTrace) { + CallTraceNodeWalkerBF::new(&self.nodes) + .map(|node| node.trace.address) + .collect::>() + } else { + vec![] + }; + + let mut trace_res = self.into_trace_results(result, trace_types); + + // check the state diff case + if let Some(ref mut state_diff) = trace_res.state_diff { + populate_state_diff(state_diff, &db, state.iter())?; + } + + // check the vm trace case + if let Some(ref mut vm_trace) = trace_res.vm_trace { + populate_vm_trace_bytecodes(&db, vm_trace, breadth_first_addresses)?; + } + + Ok(trace_res) + } + + /// Returns the tracing types that are configured in the set. + /// + /// Warning: if [TraceType::StateDiff] is provided this does __not__ fill the state diff, since + /// this requires access to the account diffs. + /// + /// See [Self::into_trace_results_with_state] and [populate_state_diff]. + pub fn into_trace_type_traces( + self, + trace_types: &HashSet, + ) -> (Option>, Option, Option) { + if trace_types.is_empty() || self.nodes.is_empty() { + return (None, None, None) + } + + let with_traces = trace_types.contains(&TraceType::Trace); + let with_diff = trace_types.contains(&TraceType::StateDiff); + + let vm_trace = + if trace_types.contains(&TraceType::VmTrace) { Some(self.vm_trace()) } else { None }; + + let mut traces = Vec::with_capacity(if with_traces { self.nodes.len() } else { 0 }); + + for node in self.iter_traceable_nodes() { + let trace_address = self.trace_address(node.idx); + + if with_traces { + let trace = node.parity_transaction_trace(trace_address); + traces.push(trace); + + // check if the trace node is a selfdestruct + if node.is_selfdestruct() { + // selfdestructs are not recorded as individual call traces but are derived from + // the call trace and are added as additional `TransactionTrace` objects in the + // trace array + let addr = { + let last = traces.last_mut().expect("exists"); + let mut addr = last.trace_address.clone(); + addr.push(last.subtraces); + // need to account for the additional selfdestruct trace + last.subtraces += 1; + addr + }; + + if let Some(trace) = node.parity_selfdestruct_trace(addr) { + traces.push(trace); + } + } + } + } + + let traces = with_traces.then_some(traces); + let diff = with_diff.then_some(StateDiff::default()); + + (traces, vm_trace, diff) + } + + /// Returns an iterator over all recorded traces for `trace_transaction` + pub fn into_transaction_traces_iter(self) -> impl Iterator { + let trace_addresses = self.trace_addresses(); + TransactionTraceIter { + next_selfdestruct: None, + iter: self + .nodes + .into_iter() + .zip(trace_addresses) + .filter(|(node, _)| !node.is_precompile()) + .map(|(node, trace_address)| (node.parity_transaction_trace(trace_address), node)), + } + } + + /// Returns the raw traces of the transaction + pub fn into_transaction_traces(self) -> Vec { + self.into_transaction_traces_iter().collect() + } + + /// Returns the last recorded step + #[inline] + fn last_step(&self) -> Option<&CallTraceStep> { + self.nodes.last().and_then(|node| node.trace.steps.last()) + } + + /// Returns true if the last recorded step is a STOP + #[inline] + fn is_last_step_stop_op(&self) -> bool { + self.last_step().map(|step| step.is_stop()).unwrap_or(false) + } + + /// Creates a VM trace by walking over `CallTraceNode`s + /// + /// does not have the code fields filled in + pub fn vm_trace(&self) -> VmTrace { + self.nodes.first().map(|node| self.make_vm_trace(node)).unwrap_or_default() + } + + /// Returns a VM trace without the code filled in + /// + /// Iteratively creates a VM trace by traversing the recorded nodes in the arena + fn make_vm_trace(&self, start: &CallTraceNode) -> VmTrace { + let mut child_idx_stack = Vec::with_capacity(self.nodes.len()); + let mut sub_stack = VecDeque::with_capacity(self.nodes.len()); + + let mut current = start; + let mut child_idx: usize = 0; + + // finds the deepest nested calls of each call frame and fills them up bottom to top + let instructions = 'outer: loop { + match current.children.get(child_idx) { + Some(child) => { + child_idx_stack.push(child_idx + 1); + + child_idx = 0; + current = self.nodes.get(*child).expect("there should be a child"); + } + None => { + let mut instructions = Vec::with_capacity(current.trace.steps.len()); + + for step in ¤t.trace.steps { + let maybe_sub_call = if step.is_calllike_op() { + sub_stack.pop_front().flatten() + } else { + None + }; + + if step.is_stop() && instructions.is_empty() && self.is_last_step_stop_op() + { + // This is a special case where there's a single STOP which is + // "optimised away", transfers for example + break 'outer instructions + } + + instructions.push(self.make_instruction(step, maybe_sub_call)); + } + + match current.parent { + Some(parent) => { + sub_stack.push_back(Some(VmTrace { + code: Default::default(), + ops: instructions, + })); + + child_idx = child_idx_stack.pop().expect("there should be a child idx"); + + current = self.nodes.get(parent).expect("there should be a parent"); + } + None => break instructions, + } + } + } + }; + + VmTrace { code: Default::default(), ops: instructions } + } + + /// Creates a VM instruction from a [CallTraceStep] and a [VmTrace] for the subcall if there is + /// one + fn make_instruction( + &self, + step: &CallTraceStep, + maybe_sub_call: Option, + ) -> VmInstruction { + let maybe_storage = step.storage_change.map(|storage_change| StorageDelta { + key: storage_change.key, + val: storage_change.value, + }); + + let maybe_memory = if step.memory.is_empty() { + None + } else { + Some(MemoryDelta { + off: step.memory_size, + data: step.memory.as_bytes().to_vec().into(), + }) + }; + + let maybe_execution = Some(VmExecutedOperation { + used: step.gas_remaining, + push: step.push_stack.clone().unwrap_or_default(), + mem: maybe_memory, + store: maybe_storage, + }); + + let cost = self + .spec_id + .and_then(|spec_id| { + spec_opcode_gas(spec_id).get(step.op.get() as usize).map(|op| op.get_gas()) + }) + .unwrap_or_default(); + + VmInstruction { + pc: step.pc, + cost: cost as u64, + ex: maybe_execution, + sub: maybe_sub_call, + op: Some(step.op.to_string()), + idx: None, + } + } +} + +/// An iterator for [TransactionTrace]s +struct TransactionTraceIter { + iter: Iter, + next_selfdestruct: Option, +} + +impl Iterator for TransactionTraceIter +where + Iter: Iterator, +{ + type Item = TransactionTrace; + + fn next(&mut self) -> Option { + if let Some(selfdestruct) = self.next_selfdestruct.take() { + return Some(selfdestruct) + } + let (mut trace, node) = self.iter.next()?; + if node.is_selfdestruct() { + // since selfdestructs are emitted as additional trace, increase the trace count + let mut addr = trace.trace_address.clone(); + addr.push(trace.subtraces); + // need to account for the additional selfdestruct trace + trace.subtraces += 1; + self.next_selfdestruct = node.parity_selfdestruct_trace(addr); + } + Some(trace) + } +} + +/// addresses are presorted via breadth first walk thru [CallTraceNode]s, this can be done by a +/// walker in [crate::tracing::builder::walker] +/// +/// iteratively fill the [VmTrace] code fields +pub(crate) fn populate_vm_trace_bytecodes( + db: DB, + trace: &mut VmTrace, + breadth_first_addresses: I, +) -> Result<(), DB::Error> +where + DB: DatabaseRef, + I: IntoIterator, +{ + let mut stack: VecDeque<&mut VmTrace> = VecDeque::new(); + stack.push_back(trace); + + let mut addrs = breadth_first_addresses.into_iter(); + + while let Some(curr_ref) = stack.pop_front() { + for op in curr_ref.ops.iter_mut() { + if let Some(sub) = op.sub.as_mut() { + stack.push_back(sub); + } + } + + let addr = addrs.next().expect("there should be an address"); + + let db_acc = db.basic_ref(addr)?.unwrap_or_default(); + + let code_hash = if db_acc.code_hash != KECCAK_EMPTY { db_acc.code_hash } else { continue }; + + curr_ref.code = db.code_by_hash_ref(code_hash)?.original_bytes(); + } + + Ok(()) +} + +/// Loops over all state accounts in the accounts diff that contains all accounts that are included +/// in the [ExecutionResult] state map and compares the balance and nonce against what's in the +/// `db`, which should point to the beginning of the transaction. +/// +/// It's expected that `DB` is a revm [Database](revm::db::Database) which at this point already +/// contains all the accounts that are in the state map and never has to fetch them from disk. +pub fn populate_state_diff<'a, DB, I>( + state_diff: &mut StateDiff, + db: DB, + account_diffs: I, +) -> Result<(), DB::Error> +where + I: IntoIterator, + DB: DatabaseRef, +{ + for (addr, changed_acc) in account_diffs.into_iter() { + // if the account was selfdestructed and created during the transaction, we can ignore it + if changed_acc.is_selfdestructed() && changed_acc.is_created() { + continue + } + + let addr = *addr; + let entry = state_diff.entry(addr).or_default(); + + // we check if this account was created during the transaction + if changed_acc.is_created() || changed_acc.is_loaded_as_not_existing() { + entry.balance = Delta::Added(changed_acc.info.balance); + entry.nonce = Delta::Added(U64::from(changed_acc.info.nonce)); + + // accounts without code are marked as added + let account_code = load_account_code(&db, &changed_acc.info).unwrap_or_default(); + entry.code = Delta::Added(account_code); + + // new storage values are marked as added, + // however we're filtering changed here to avoid adding entries for the zero value + for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) { + entry.storage.insert((*key).into(), Delta::Added(slot.present_value.into())); + } + } else { + // account already exists, we need to fetch the account from the db + let db_acc = db.basic_ref(addr)?.unwrap_or_default(); + + // update _changed_ storage values + for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) { + entry.storage.insert( + (*key).into(), + Delta::changed( + slot.previous_or_original_value.into(), + slot.present_value.into(), + ), + ); + } + + // check if the account was changed at all + if entry.storage.is_empty() && + db_acc == changed_acc.info && + !changed_acc.is_selfdestructed() + { + // clear the entry if the account was not changed + state_diff.remove(&addr); + continue + } + + entry.balance = if db_acc.balance == changed_acc.info.balance { + Delta::Unchanged + } else { + Delta::Changed(ChangedType { from: db_acc.balance, to: changed_acc.info.balance }) + }; + + // this is relevant for the caller and contracts + entry.nonce = if db_acc.nonce == changed_acc.info.nonce { + Delta::Unchanged + } else { + Delta::Changed(ChangedType { + from: U64::from(db_acc.nonce), + to: U64::from(changed_acc.info.nonce), + }) + }; + } + } + + Ok(()) +} + +/// Returns the number of items pushed on the stack by a given opcode. +/// This used to determine how many stack etries to put in the `push` element +/// in a parity vmTrace. +/// The value is obvious for most opcodes, but SWAP* and DUP* are a bit weird, +/// and we handle those as they are handled in parity vmtraces. +/// For reference: +pub(crate) fn stack_push_count(step_op: OpCode) -> usize { + let step_op = step_op.get(); + match step_op { + opcode::PUSH0..=opcode::PUSH32 => 1, + opcode::SWAP1..=opcode::SWAP16 => (step_op - opcode::SWAP1) as usize + 2, + opcode::DUP1..=opcode::DUP16 => (step_op - opcode::DUP1) as usize + 2, + opcode::CALLDATALOAD | + opcode::SLOAD | + opcode::MLOAD | + opcode::CALLDATASIZE | + opcode::LT | + opcode::GT | + opcode::DIV | + opcode::SDIV | + opcode::SAR | + opcode::AND | + opcode::EQ | + opcode::CALLVALUE | + opcode::ISZERO | + opcode::ADD | + opcode::EXP | + opcode::CALLER | + opcode::KECCAK256 | + opcode::SUB | + opcode::ADDRESS | + opcode::GAS | + opcode::MUL | + opcode::RETURNDATASIZE | + opcode::NOT | + opcode::SHR | + opcode::SHL | + opcode::EXTCODESIZE | + opcode::SLT | + opcode::OR | + opcode::NUMBER | + opcode::PC | + opcode::TIMESTAMP | + opcode::BALANCE | + opcode::SELFBALANCE | + opcode::MULMOD | + opcode::ADDMOD | + opcode::BASEFEE | + opcode::BLOCKHASH | + opcode::BYTE | + opcode::XOR | + opcode::ORIGIN | + opcode::CODESIZE | + opcode::MOD | + opcode::SIGNEXTEND | + opcode::GASLIMIT | + opcode::DIFFICULTY | + opcode::SGT | + opcode::GASPRICE | + opcode::MSIZE | + opcode::EXTCODEHASH | + opcode::SMOD | + opcode::CHAINID | + opcode::COINBASE => 1, + _ => 0, + } +} diff --git a/src/tracing/builder/walker.rs b/src/tracing/builder/walker.rs new file mode 100644 index 00000000..4d88a2af --- /dev/null +++ b/src/tracing/builder/walker.rs @@ -0,0 +1,39 @@ +use crate::tracing::types::CallTraceNode; +use std::collections::VecDeque; + +/// Traverses Reths internal tracing structure breadth-first +/// +/// This is a lazy iterator +pub(crate) struct CallTraceNodeWalkerBF<'trace> { + /// the entire arena + nodes: &'trace Vec, + + /// holds indexes of nodes to visit as we traverse + queue: VecDeque, +} + +impl<'trace> CallTraceNodeWalkerBF<'trace> { + pub(crate) fn new(nodes: &'trace Vec) -> Self { + let mut queue = VecDeque::with_capacity(nodes.len()); + queue.push_back(0); + + Self { nodes, queue } + } +} + +impl<'trace> Iterator for CallTraceNodeWalkerBF<'trace> { + type Item = &'trace CallTraceNode; + + fn next(&mut self) -> Option { + match self.queue.pop_front() { + Some(idx) => { + let curr = self.nodes.get(idx).expect("there should be a node"); + + self.queue.extend(curr.children.iter()); + + Some(curr) + } + None => None, + } + } +} diff --git a/src/tracing/config.rs b/src/tracing/config.rs new file mode 100644 index 00000000..e3940849 --- /dev/null +++ b/src/tracing/config.rs @@ -0,0 +1,225 @@ +use alloy_rpc_trace_types::{geth::GethDefaultTracingOptions, parity::TraceType}; +use std::collections::HashSet; + +/// Gives guidance to the [TracingInspector](crate::tracing::TracingInspector). +/// +/// Use [TracingInspectorConfig::default_parity] or [TracingInspectorConfig::default_geth] to get +/// the default configs for specific styles of traces. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct TracingInspectorConfig { + /// Whether to record every individual opcode level step. + pub record_steps: bool, + /// Whether to record individual memory snapshots. + pub record_memory_snapshots: bool, + /// Whether to record individual stack snapshots. + pub record_stack_snapshots: StackSnapshotType, + /// Whether to record state diffs. + pub record_state_diff: bool, + /// Whether to ignore precompile calls. + pub exclude_precompile_calls: bool, + /// Whether to record individual return data + pub record_call_return_data: bool, + /// Whether to record logs + pub record_logs: bool, +} + +impl TracingInspectorConfig { + /// Returns a config with everything enabled. + pub const fn all() -> Self { + Self { + record_steps: true, + record_memory_snapshots: true, + record_stack_snapshots: StackSnapshotType::Full, + record_state_diff: false, + exclude_precompile_calls: false, + record_call_return_data: false, + record_logs: true, + } + } + + /// Returns a config for parity style traces. + /// + /// This config does _not_ record opcode level traces and is suited for `trace_transaction` + pub const fn default_parity() -> Self { + Self { + record_steps: false, + record_memory_snapshots: false, + record_stack_snapshots: StackSnapshotType::None, + record_state_diff: false, + exclude_precompile_calls: true, + record_call_return_data: false, + record_logs: false, + } + } + + /// Returns a config for geth style traces. + /// + /// This config does _not_ record opcode level traces and is suited for `debug_traceTransaction` + pub const fn default_geth() -> Self { + Self { + record_steps: true, + record_memory_snapshots: true, + record_stack_snapshots: StackSnapshotType::Full, + record_state_diff: true, + exclude_precompile_calls: false, + record_call_return_data: false, + record_logs: false, + } + } + + /// Returns the [TracingInspectorConfig] depending on the enabled [TraceType]s + /// + /// Note: the parity statediffs can be populated entirely via the execution result, so we don't + /// need statediff recording + #[inline] + pub fn from_parity_config(trace_types: &HashSet) -> Self { + let needs_vm_trace = trace_types.contains(&TraceType::VmTrace); + let snap_type = + if needs_vm_trace { StackSnapshotType::Pushes } else { StackSnapshotType::None }; + TracingInspectorConfig::default_parity() + .set_steps(needs_vm_trace) + .set_stack_snapshots(snap_type) + .set_memory_snapshots(needs_vm_trace) + } + + /// Returns a config for geth style traces based on the given [GethDefaultTracingOptions]. + #[inline] + pub fn from_geth_config(config: &GethDefaultTracingOptions) -> Self { + Self { + record_memory_snapshots: config.enable_memory.unwrap_or_default(), + record_stack_snapshots: if config.disable_stack.unwrap_or_default() { + StackSnapshotType::None + } else { + StackSnapshotType::Full + }, + record_state_diff: !config.disable_storage.unwrap_or_default(), + ..Self::default_geth() + } + } + + /// Configure whether calls to precompiles should be ignored. + /// + /// If set to `true`, calls to precompiles without value transfers will be ignored. + pub fn set_exclude_precompile_calls(mut self, exclude_precompile_calls: bool) -> Self { + self.exclude_precompile_calls = exclude_precompile_calls; + self + } + + /// Configure whether individual opcode level steps should be recorded + pub fn set_steps(mut self, record_steps: bool) -> Self { + self.record_steps = record_steps; + self + } + + /// Configure whether the tracer should record memory snapshots + pub fn set_memory_snapshots(mut self, record_memory_snapshots: bool) -> Self { + self.record_memory_snapshots = record_memory_snapshots; + self + } + + /// Configure how the tracer should record stack snapshots + pub fn set_stack_snapshots(mut self, record_stack_snapshots: StackSnapshotType) -> Self { + self.record_stack_snapshots = record_stack_snapshots; + self + } + + /// Sets state diff recording to true. + pub fn with_state_diffs(self) -> Self { + self.set_steps_and_state_diffs(true) + } + + /// Configure whether the tracer should record state diffs + pub fn set_state_diffs(mut self, record_state_diff: bool) -> Self { + self.record_state_diff = record_state_diff; + self + } + + /// Configure whether the tracer should record steps and state diffs. + /// + /// This is a convenience method for setting both [TracingInspectorConfig::set_steps] and + /// [TracingInspectorConfig::set_state_diffs] since tracking state diffs requires steps tracing. + pub fn set_steps_and_state_diffs(mut self, steps_and_diffs: bool) -> Self { + self.record_steps = steps_and_diffs; + self.record_state_diff = steps_and_diffs; + self + } + + /// Configure whether the tracer should record logs + pub fn set_record_logs(mut self, record_logs: bool) -> Self { + self.record_logs = record_logs; + self + } +} + +/// How much of the stack to record. Nothing, just the items pushed, or the full stack +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum StackSnapshotType { + /// Don't record stack snapshots + None, + /// Record only the items pushed to the stack + Pushes, + /// Record the full stack + Full, +} + +impl StackSnapshotType { + /// Returns true if this is the [StackSnapshotType::Full] variant + #[inline] + pub fn is_full(self) -> bool { + matches!(self, Self::Full) + } + + /// Returns true if this is the [StackSnapshotType::Pushes] variant + #[inline] + pub fn is_pushes(self) -> bool { + matches!(self, Self::Pushes) + } +} + +/// What kind of tracing style this is. +/// +/// This affects things like error messages. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum TraceStyle { + /// Parity style tracer + Parity, + /// Geth style tracer + #[allow(dead_code)] + Geth, +} + +impl TraceStyle { + /// Returns true if this is a parity style tracer. + pub(crate) const fn is_parity(self) -> bool { + matches!(self, Self::Parity) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parity_config() { + let mut s = HashSet::new(); + s.insert(TraceType::StateDiff); + let config = TracingInspectorConfig::from_parity_config(&s); + // not required + assert!(!config.record_steps); + assert!(!config.record_state_diff); + + let mut s = HashSet::new(); + s.insert(TraceType::VmTrace); + let config = TracingInspectorConfig::from_parity_config(&s); + assert!(config.record_steps); + assert!(!config.record_state_diff); + + let mut s = HashSet::new(); + s.insert(TraceType::VmTrace); + s.insert(TraceType::StateDiff); + let config = TracingInspectorConfig::from_parity_config(&s); + assert!(config.record_steps); + // not required for StateDiff + assert!(!config.record_state_diff); + } +} diff --git a/src/tracing/fourbyte.rs b/src/tracing/fourbyte.rs new file mode 100644 index 00000000..5abf378d --- /dev/null +++ b/src/tracing/fourbyte.rs @@ -0,0 +1,78 @@ +//! Fourbyte tracing inspector +//! +//! Solidity contract functions are addressed using the first four byte of the Keccak-256 hash of +//! their signature. Therefore when calling the function of a contract, the caller must send this +//! function selector as well as the ABI-encoded arguments as call data. +//! +//! The 4byteTracer collects the function selectors of every function executed in the lifetime of a +//! transaction, along with the size of the supplied call data. The result is a map of +//! SELECTOR-CALLDATASIZE to number of occurrences entries, where the keys are SELECTOR-CALLDATASIZE +//! and the values are number of occurrences of this key. For example: +//! +//! ```json +//! { +//! "0x27dc297e-128": 1, +//! "0x38cc4831-0": 2, +//! "0x524f3889-96": 1, +//! "0xadf59f99-288": 1, +//! "0xc281d19e-0": 1 +//! } +//! ``` +//! +//! See also + +use alloy_primitives::{hex, Bytes, Selector}; +use alloy_rpc_trace_types::geth::FourByteFrame; +use revm::{ + interpreter::{CallInputs, Gas, InstructionResult}, + Database, EVMData, Inspector, +}; +use std::collections::HashMap; + +/// Fourbyte tracing inspector that records all function selectors and their calldata sizes. +#[derive(Debug, Clone, Default)] +pub struct FourByteInspector { + /// The map of SELECTOR to number of occurrences entries + inner: HashMap<(Selector, usize), u64>, +} + +impl FourByteInspector { + /// Returns the map of SELECTOR to number of occurrences entries + pub fn inner(&self) -> &HashMap<(Selector, usize), u64> { + &self.inner + } +} + +impl Inspector for FourByteInspector +where + DB: Database, +{ + fn call( + &mut self, + _data: &mut EVMData<'_, DB>, + call: &mut CallInputs, + ) -> (InstructionResult, Gas, Bytes) { + if call.input.len() >= 4 { + let selector = Selector::try_from(&call.input[..4]).expect("input is at least 4 bytes"); + let calldata_size = call.input[4..].len(); + *self.inner.entry((selector, calldata_size)).or_default() += 1; + } + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } +} + +impl From for FourByteFrame { + fn from(value: FourByteInspector) -> Self { + FourByteFrame( + value + .inner + .into_iter() + .map(|((selector, calldata_size), count)| { + let key = format!("0x{}-{}", hex::encode(&selector[..]), calldata_size); + (key, count) + }) + .collect(), + ) + } +} diff --git a/src/tracing/js/bigint.js b/src/tracing/js/bigint.js new file mode 100644 index 00000000..d9a8411b --- /dev/null +++ b/src/tracing/js/bigint.js @@ -0,0 +1 @@ +var bigInt=function(undefined){"use strict";var BASE=1e7,LOG_BASE=7,MAX_INT=9007199254740992,MAX_INT_ARR=smallToArray(MAX_INT),LOG_MAX_INT=Math.log(MAX_INT);function Integer(v,radix){if(typeof v==="undefined")return Integer[0];if(typeof radix!=="undefined")return+radix===10?parseValue(v):parseBase(v,radix);return parseValue(v)}function BigInteger(value,sign){this.value=value;this.sign=sign;this.isSmall=false}BigInteger.prototype=Object.create(Integer.prototype);function SmallInteger(value){this.value=value;this.sign=value<0;this.isSmall=true}SmallInteger.prototype=Object.create(Integer.prototype);function isPrecise(n){return-MAX_INT0)return Math.floor(n);return Math.ceil(n)}function add(a,b){var l_a=a.length,l_b=b.length,r=new Array(l_a),carry=0,base=BASE,sum,i;for(i=0;i=base?1:0;r[i]=sum-carry*base}while(i0)r.push(carry);return r}function addAny(a,b){if(a.length>=b.length)return add(a,b);return add(b,a)}function addSmall(a,carry){var l=a.length,r=new Array(l),base=BASE,sum,i;for(i=0;i0){r[i++]=carry%base;carry=Math.floor(carry/base)}return r}BigInteger.prototype.add=function(v){var n=parseValue(v);if(this.sign!==n.sign){return this.subtract(n.negate())}var a=this.value,b=n.value;if(n.isSmall){return new BigInteger(addSmall(a,Math.abs(b)),this.sign)}return new BigInteger(addAny(a,b),this.sign)};BigInteger.prototype.plus=BigInteger.prototype.add;SmallInteger.prototype.add=function(v){var n=parseValue(v);var a=this.value;if(a<0!==n.sign){return this.subtract(n.negate())}var b=n.value;if(n.isSmall){if(isPrecise(a+b))return new SmallInteger(a+b);b=smallToArray(Math.abs(b))}return new BigInteger(addSmall(b,Math.abs(a)),a<0)};SmallInteger.prototype.plus=SmallInteger.prototype.add;function subtract(a,b){var a_l=a.length,b_l=b.length,r=new Array(a_l),borrow=0,base=BASE,i,difference;for(i=0;i=0){value=subtract(a,b)}else{value=subtract(b,a);sign=!sign}value=arrayToSmall(value);if(typeof value==="number"){if(sign)value=-value;return new SmallInteger(value)}return new BigInteger(value,sign)}function subtractSmall(a,b,sign){var l=a.length,r=new Array(l),carry=-b,base=BASE,i,difference;for(i=0;i=0)};SmallInteger.prototype.minus=SmallInteger.prototype.subtract;BigInteger.prototype.negate=function(){return new BigInteger(this.value,!this.sign)};SmallInteger.prototype.negate=function(){var sign=this.sign;var small=new SmallInteger(-this.value);small.sign=!sign;return small};BigInteger.prototype.abs=function(){return new BigInteger(this.value,false)};SmallInteger.prototype.abs=function(){return new SmallInteger(Math.abs(this.value))};function multiplyLong(a,b){var a_l=a.length,b_l=b.length,l=a_l+b_l,r=createArray(l),base=BASE,product,carry,i,a_i,b_j;for(i=0;i0){r[i++]=carry%base;carry=Math.floor(carry/base)}return r}function shiftLeft(x,n){var r=[];while(n-- >0)r.push(0);return r.concat(x)}function multiplyKaratsuba(x,y){var n=Math.max(x.length,y.length);if(n<=30)return multiplyLong(x,y);n=Math.ceil(n/2);var b=x.slice(n),a=x.slice(0,n),d=y.slice(n),c=y.slice(0,n);var ac=multiplyKaratsuba(a,c),bd=multiplyKaratsuba(b,d),abcd=multiplyKaratsuba(addAny(a,b),addAny(c,d));var product=addAny(addAny(ac,shiftLeft(subtract(subtract(abcd,ac),bd),n)),shiftLeft(bd,2*n));trim(product);return product}function useKaratsuba(l1,l2){return-.012*l1-.012*l2+15e-6*l1*l2>0}BigInteger.prototype.multiply=function(v){var n=parseValue(v),a=this.value,b=n.value,sign=this.sign!==n.sign,abs;if(n.isSmall){if(b===0)return Integer[0];if(b===1)return this;if(b===-1)return this.negate();abs=Math.abs(b);if(abs=0;shift--){quotientDigit=base-1;if(remainder[shift+b_l]!==divisorMostSignificantDigit){quotientDigit=Math.floor((remainder[shift+b_l]*base+remainder[shift+b_l-1])/divisorMostSignificantDigit)}carry=0;borrow=0;l=divisor.length;for(i=0;ib_l){highx=(highx+1)*base}guess=Math.ceil(highx/highy);do{check=multiplySmall(b,guess);if(compareAbs(check,part)<=0)break;guess--}while(guess);result.push(guess);part=subtract(part,check)}result.reverse();return[arrayToSmall(result),arrayToSmall(part)]}function divModSmall(value,lambda){var length=value.length,quotient=createArray(length),base=BASE,i,q,remainder,divisor;remainder=0;for(i=length-1;i>=0;--i){divisor=remainder*base+value[i];q=truncate(divisor/lambda);remainder=divisor-q*lambda;quotient[i]=q|0}return[quotient,remainder|0]}function divModAny(self,v){var value,n=parseValue(v);var a=self.value,b=n.value;var quotient;if(b===0)throw new Error("Cannot divide by zero");if(self.isSmall){if(n.isSmall){return[new SmallInteger(truncate(a/b)),new SmallInteger(a%b)]}return[Integer[0],self]}if(n.isSmall){if(b===1)return[self,Integer[0]];if(b==-1)return[self.negate(),Integer[0]];var abs=Math.abs(b);if(absb.length?1:-1}for(var i=a.length-1;i>=0;i--){if(a[i]!==b[i])return a[i]>b[i]?1:-1}return 0}BigInteger.prototype.compareAbs=function(v){var n=parseValue(v),a=this.value,b=n.value;if(n.isSmall)return 1;return compareAbs(a,b)};SmallInteger.prototype.compareAbs=function(v){var n=parseValue(v),a=Math.abs(this.value),b=n.value;if(n.isSmall){b=Math.abs(b);return a===b?0:a>b?1:-1}return-1};BigInteger.prototype.compare=function(v){if(v===Infinity){return-1}if(v===-Infinity){return 1}var n=parseValue(v),a=this.value,b=n.value;if(this.sign!==n.sign){return n.sign?1:-1}if(n.isSmall){return this.sign?-1:1}return compareAbs(a,b)*(this.sign?-1:1)};BigInteger.prototype.compareTo=BigInteger.prototype.compare;SmallInteger.prototype.compare=function(v){if(v===Infinity){return-1}if(v===-Infinity){return 1}var n=parseValue(v),a=this.value,b=n.value;if(n.isSmall){return a==b?0:a>b?1:-1}if(a<0!==n.sign){return a<0?-1:1}return a<0?1:-1};SmallInteger.prototype.compareTo=SmallInteger.prototype.compare;BigInteger.prototype.equals=function(v){return this.compare(v)===0};SmallInteger.prototype.eq=SmallInteger.prototype.equals=BigInteger.prototype.eq=BigInteger.prototype.equals;BigInteger.prototype.notEquals=function(v){return this.compare(v)!==0};SmallInteger.prototype.neq=SmallInteger.prototype.notEquals=BigInteger.prototype.neq=BigInteger.prototype.notEquals;BigInteger.prototype.greater=function(v){return this.compare(v)>0};SmallInteger.prototype.gt=SmallInteger.prototype.greater=BigInteger.prototype.gt=BigInteger.prototype.greater;BigInteger.prototype.lesser=function(v){return this.compare(v)<0};SmallInteger.prototype.lt=SmallInteger.prototype.lesser=BigInteger.prototype.lt=BigInteger.prototype.lesser;BigInteger.prototype.greaterOrEquals=function(v){return this.compare(v)>=0};SmallInteger.prototype.geq=SmallInteger.prototype.greaterOrEquals=BigInteger.prototype.geq=BigInteger.prototype.greaterOrEquals;BigInteger.prototype.lesserOrEquals=function(v){return this.compare(v)<=0};SmallInteger.prototype.leq=SmallInteger.prototype.lesserOrEquals=BigInteger.prototype.leq=BigInteger.prototype.lesserOrEquals;BigInteger.prototype.isEven=function(){return(this.value[0]&1)===0};SmallInteger.prototype.isEven=function(){return(this.value&1)===0};BigInteger.prototype.isOdd=function(){return(this.value[0]&1)===1};SmallInteger.prototype.isOdd=function(){return(this.value&1)===1};BigInteger.prototype.isPositive=function(){return!this.sign};SmallInteger.prototype.isPositive=function(){return this.value>0};BigInteger.prototype.isNegative=function(){return this.sign};SmallInteger.prototype.isNegative=function(){return this.value<0};BigInteger.prototype.isUnit=function(){return false};SmallInteger.prototype.isUnit=function(){return Math.abs(this.value)===1};BigInteger.prototype.isZero=function(){return false};SmallInteger.prototype.isZero=function(){return this.value===0};BigInteger.prototype.isDivisibleBy=function(v){var n=parseValue(v);var value=n.value;if(value===0)return false;if(value===1)return true;if(value===2)return this.isEven();return this.mod(n).equals(Integer[0])};SmallInteger.prototype.isDivisibleBy=BigInteger.prototype.isDivisibleBy;function isBasicPrime(v){var n=v.abs();if(n.isUnit())return false;if(n.equals(2)||n.equals(3)||n.equals(5))return true;if(n.isEven()||n.isDivisibleBy(3)||n.isDivisibleBy(5))return false;if(n.lesser(25))return true}BigInteger.prototype.isPrime=function(){var isPrime=isBasicPrime(this);if(isPrime!==undefined)return isPrime;var n=this.abs(),nPrev=n.prev();var a=[2,3,5,7,11,13,17,19],b=nPrev,d,t,i,x;while(b.isEven())b=b.divide(2);for(i=0;i-MAX_INT)return new SmallInteger(value-1);return new BigInteger(MAX_INT_ARR,true)};var powersOfTwo=[1];while(2*powersOfTwo[powersOfTwo.length-1]<=BASE)powersOfTwo.push(2*powersOfTwo[powersOfTwo.length-1]);var powers2Length=powersOfTwo.length,highestPower2=powersOfTwo[powers2Length-1];function shift_isSmall(n){return(typeof n==="number"||typeof n==="string")&&+Math.abs(n)<=BASE||n instanceof BigInteger&&n.value.length<=1}BigInteger.prototype.shiftLeft=function(n){if(!shift_isSmall(n)){throw new Error(String(n)+" is too large for shifting.")}n=+n;if(n<0)return this.shiftRight(-n);var result=this;while(n>=powers2Length){result=result.multiply(highestPower2);n-=powers2Length-1}return result.multiply(powersOfTwo[n])};SmallInteger.prototype.shiftLeft=BigInteger.prototype.shiftLeft;BigInteger.prototype.shiftRight=function(n){var remQuo;if(!shift_isSmall(n)){throw new Error(String(n)+" is too large for shifting.")}n=+n;if(n<0)return this.shiftLeft(-n);var result=this;while(n>=powers2Length){if(result.isZero())return result;remQuo=divModAny(result,highestPower2);result=remQuo[1].isNegative()?remQuo[0].prev():remQuo[0];n-=powers2Length-1}remQuo=divModAny(result,powersOfTwo[n]);return remQuo[1].isNegative()?remQuo[0].prev():remQuo[0]};SmallInteger.prototype.shiftRight=BigInteger.prototype.shiftRight;function bitwise(x,y,fn){y=parseValue(y);var xSign=x.isNegative(),ySign=y.isNegative();var xRem=xSign?x.not():x,yRem=ySign?y.not():y;var xDigit=0,yDigit=0;var xDivMod=null,yDivMod=null;var result=[];while(!xRem.isZero()||!yRem.isZero()){xDivMod=divModAny(xRem,highestPower2);xDigit=xDivMod[1].toJSNumber();if(xSign){xDigit=highestPower2-1-xDigit}yDivMod=divModAny(yRem,highestPower2);yDigit=yDivMod[1].toJSNumber();if(ySign){yDigit=highestPower2-1-yDigit}xRem=xDivMod[0];yRem=yDivMod[0];result.push(fn(xDigit,yDigit))}var sum=fn(xSign?1:0,ySign?1:0)!==0?bigInt(-1):bigInt(0);for(var i=result.length-1;i>=0;i-=1){sum=sum.multiply(highestPower2).add(bigInt(result[i]))}return sum}BigInteger.prototype.not=function(){return this.negate().prev()};SmallInteger.prototype.not=BigInteger.prototype.not;BigInteger.prototype.and=function(n){return bitwise(this,n,function(a,b){return a&b})};SmallInteger.prototype.and=BigInteger.prototype.and;BigInteger.prototype.or=function(n){return bitwise(this,n,function(a,b){return a|b})};SmallInteger.prototype.or=BigInteger.prototype.or;BigInteger.prototype.xor=function(n){return bitwise(this,n,function(a,b){return a^b})};SmallInteger.prototype.xor=BigInteger.prototype.xor;var LOBMASK_I=1<<30,LOBMASK_BI=(BASE&-BASE)*(BASE&-BASE)|LOBMASK_I;function roughLOB(n){var v=n.value,x=typeof v==="number"?v|LOBMASK_I:v[0]+v[1]*BASE|LOBMASK_BI;return x&-x}function max(a,b){a=parseValue(a);b=parseValue(b);return a.greater(b)?a:b}function min(a,b){a=parseValue(a);b=parseValue(b);return a.lesser(b)?a:b}function gcd(a,b){a=parseValue(a).abs();b=parseValue(b).abs();if(a.equals(b))return a;if(a.isZero())return b;if(b.isZero())return a;var c=Integer[1],d,t;while(a.isEven()&&b.isEven()){d=Math.min(roughLOB(a),roughLOB(b));a=a.divide(d);b=b.divide(d);c=c.multiply(d)}while(a.isEven()){a=a.divide(roughLOB(a))}do{while(b.isEven()){b=b.divide(roughLOB(b))}if(a.greater(b)){t=b;b=a;a=t}b=b.subtract(a)}while(!b.isZero());return c.isUnit()?a:a.multiply(c)}function lcm(a,b){a=parseValue(a).abs();b=parseValue(b).abs();return a.divide(gcd(a,b)).multiply(b)}function randBetween(a,b){a=parseValue(a);b=parseValue(b);var low=min(a,b),high=max(a,b);var range=high.subtract(low).add(1);if(range.isSmall)return low.add(Math.floor(Math.random()*range));var length=range.value.length-1;var result=[],restricted=true;for(var i=length;i>=0;i--){var top=restricted?range.value[i]:BASE;var digit=truncate(Math.random()*top);result.unshift(digit);if(digit=absBase){if(c==="1"&&absBase===1)continue;throw new Error(c+" is not a valid digit in base "+base+".")}else if(c.charCodeAt(0)-87>=absBase){throw new Error(c+" is not a valid digit in base "+base+".")}}}if(2<=base&&base<=36){if(length<=LOG_MAX_INT/Math.log(base)){var result=parseInt(text,base);if(isNaN(result)){throw new Error(c+" is not a valid digit in base "+base+".")}return new SmallInteger(parseInt(text,base))}}base=parseValue(base);var digits=[];var isNegative=text[0]==="-";for(i=isNegative?1:0;i");digits.push(parseValue(text.slice(start+1,i)))}else throw new Error(c+" is not a valid character")}return parseBaseFromArray(digits,base,isNegative)};function parseBaseFromArray(digits,base,isNegative){var val=Integer[0],pow=Integer[1],i;for(i=digits.length-1;i>=0;i--){val=val.add(digits[i].times(pow));pow=pow.times(base)}return isNegative?val.negate():val}function stringify(digit){var v=digit.value;if(typeof v==="number")v=[v];if(v.length===1&&v[0]<=35){return"0123456789abcdefghijklmnopqrstuvwxyz".charAt(v[0])}return"<"+v+">"}function toBase(n,base){base=bigInt(base);if(base.isZero()){if(n.isZero())return"0";throw new Error("Cannot convert nonzero numbers to base 0.")}if(base.equals(-1)){if(n.isZero())return"0";if(n.isNegative())return new Array(1-n).join("10");return"1"+new Array(+n).join("01")}var minusSign="";if(n.isNegative()&&base.isPositive()){minusSign="-";n=n.abs()}if(base.equals(1)){if(n.isZero())return"0";return minusSign+new Array(+n+1).join(1)}var out=[];var left=n,divmod;while(left.isNegative()||left.compareAbs(base)>=0){divmod=left.divmod(base);left=divmod.quotient;var digit=divmod.remainder;if(digit.isNegative()){digit=base.minus(digit).abs();left=left.next()}out.push(stringify(digit))}out.push(stringify(left));return minusSign+out.reverse().join("")}BigInteger.prototype.toString=function(radix){if(radix===undefined)radix=10;if(radix!==10)return toBase(this,radix);var v=this.value,l=v.length,str=String(v[--l]),zeros="0000000",digit;while(--l>=0){digit=String(v[l]);str+=zeros.slice(digit.length)+digit}var sign=this.sign?"-":"";return sign+str};SmallInteger.prototype.toString=function(radix){if(radix===undefined)radix=10;if(radix!=10)return toBase(this,radix);return String(this.value)};BigInteger.prototype.toJSON=SmallInteger.prototype.toJSON=function(){return this.toString()};BigInteger.prototype.valueOf=function(){return+this.toString()};BigInteger.prototype.toJSNumber=BigInteger.prototype.valueOf;SmallInteger.prototype.valueOf=function(){return this.value};SmallInteger.prototype.toJSNumber=SmallInteger.prototype.valueOf;function parseStringValue(v){if(isPrecise(+v)){var x=+v;if(x===truncate(x))return new SmallInteger(x);throw"Invalid integer: "+v}var sign=v[0]==="-";if(sign)v=v.slice(1);var split=v.split(/e/i);if(split.length>2)throw new Error("Invalid integer: "+split.join("e"));if(split.length===2){var exp=split[1];if(exp[0]==="+")exp=exp.slice(1);exp=+exp;if(exp!==truncate(exp)||!isPrecise(exp))throw new Error("Invalid integer: "+exp+" is not a valid exponent.");var text=split[0];var decimalPlace=text.indexOf(".");if(decimalPlace>=0){exp-=text.length-decimalPlace-1;text=text.slice(0,decimalPlace)+text.slice(decimalPlace+1)}if(exp<0)throw new Error("Cannot include negative exponent part for integers");text+=new Array(exp+1).join("0");v=text}var isValid=/^([0-9][0-9]*)$/.test(v);if(!isValid)throw new Error("Invalid integer: "+v);var r=[],max=v.length,l=LOG_BASE,min=max-l;while(max>0){r.push(+v.slice(min,max));min-=l;if(min<0)min=0;max-=l}trim(r);return new BigInteger(r,sign)}function parseNumberValue(v){if(isPrecise(v)){if(v!==truncate(v))throw new Error(v+" is not an integer.");return new SmallInteger(v)}return parseStringValue(v.toString())}function parseValue(v){if(typeof v==="number"){return parseNumberValue(v)}if(typeof v==="string"){return parseStringValue(v)}return v}for(var i=0;i<1e3;i++){Integer[i]=new SmallInteger(i);if(i>0)Integer[-i]=new SmallInteger(-i)}Integer.one=Integer[1];Integer.zero=Integer[0];Integer.minusOne=Integer[-1];Integer.max=max;Integer.min=min;Integer.gcd=gcd;Integer.lcm=lcm;Integer.isInstance=function(x){return x instanceof BigInteger||x instanceof SmallInteger};Integer.randBetween=randBetween;Integer.fromArray=function(digits,base,isNegative){return parseBaseFromArray(digits.map(parseValue),parseValue(base||10),isNegative)};return Integer}();if(typeof module!=="undefined"&&module.hasOwnProperty("exports")){module.exports=bigInt}if(typeof define==="function"&&define.amd){define("big-integer",[],function(){return bigInt})}; bigInt \ No newline at end of file diff --git a/src/tracing/js/bindings.rs b/src/tracing/js/bindings.rs new file mode 100644 index 00000000..438571a5 --- /dev/null +++ b/src/tracing/js/bindings.rs @@ -0,0 +1,936 @@ +//! Type bindings for js tracing inspector + +use crate::tracing::{ + js::{ + builtins::{ + address_to_buf, bytes_to_address, bytes_to_hash, from_buf, to_bigint, to_buf, + to_buf_value, + }, + JsDbRequest, + }, + types::CallKind, +}; +use alloy_primitives::{Address, Bytes, B256, U256}; +use boa_engine::{ + native_function::NativeFunction, + object::{builtins::JsArrayBuffer, FunctionObjectBuilder}, + Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsValue, +}; +use boa_gc::{empty_trace, Finalize, Trace}; +use revm::{ + interpreter::{ + opcode::{PUSH0, PUSH32}, + OpCode, SharedMemory, Stack, + }, + primitives::{AccountInfo, State, KECCAK_EMPTY}, +}; +use std::{cell::RefCell, rc::Rc, sync::mpsc::channel}; +use tokio::sync::mpsc; + +/// A macro that creates a native function that returns via [JsValue::from] +macro_rules! js_value_getter { + ($value:ident, $ctx:ident) => { + FunctionObjectBuilder::new( + $ctx, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from($value))), + ) + .length(0) + .build() + }; +} + +/// A macro that creates a native function that returns a captured JsValue +macro_rules! js_value_capture_getter { + ($value:ident, $ctx:ident) => { + FunctionObjectBuilder::new( + $ctx, + NativeFunction::from_copy_closure_with_captures( + move |_this, _args, input, _ctx| Ok(JsValue::from(input.clone())), + $value, + ), + ) + .length(0) + .build() + }; +} + +/// A reference to a value that can be garbagae collected, but will not give access to the value if +/// it has been dropped. +/// +/// This is used to allow the JS tracer functions to access values at a certain point during +/// inspection by ref without having to clone them and capture them in the js object. +/// +/// JS tracer functions get access to evm internals via objects or function arguments, for example +/// `function step(log,evm)` where log has an object `stack` that has a function `peek(number)` that +/// returns a value from the stack. +/// +/// These functions could get garbage collected, however the data accessed by the function is +/// supposed to be ephemeral and only valid for the duration of the function call. +/// +/// This type supports garbage collection of (rust) references and prevents access to the value if +/// it has been dropped. +#[derive(Debug, Clone)] +pub(crate) struct GuardedNullableGcRef { + /// The lifetime is a lie to make it possible to use a reference in boa which requires 'static + inner: Rc>>, +} + +impl GuardedNullableGcRef { + /// Creates a garbage collectible reference to the given reference. + /// + /// SAFETY; the caller must ensure that the guard is dropped before the value is dropped. + pub(crate) fn new(val: &Val) -> (Self, RefGuard<'_, Val>) { + let inner = Rc::new(RefCell::new(Some(val))); + let guard = RefGuard { inner: Rc::clone(&inner) }; + + // SAFETY: guard enforces that the value is removed from the refcell before it is dropped + let this = Self { inner: unsafe { std::mem::transmute(inner) } }; + + (this, guard) + } + + /// Executes the given closure with a reference to the inner value if it is still present. + pub(crate) fn with_inner(&self, f: F) -> Option + where + F: FnOnce(&Val) -> R, + { + self.inner.borrow().map(f) + } +} + +impl Finalize for GuardedNullableGcRef {} + +unsafe impl Trace for GuardedNullableGcRef { + empty_trace!(); +} + +/// Guard the inner references, once this value is dropped the inner reference is also removed. +/// +/// This type guarantees that it never outlives the wrapped reference. +#[derive(Debug)] +pub(crate) struct RefGuard<'a, Val> { + inner: Rc>>, +} + +impl<'a, Val> Drop for RefGuard<'a, Val> { + fn drop(&mut self) { + self.inner.borrow_mut().take(); + } +} + +/// The Log object that is passed to the javascript inspector. +#[derive(Debug)] +pub(crate) struct StepLog { + /// Stack before step execution + pub(crate) stack: StackRef, + /// Opcode to be executed + pub(crate) op: OpObj, + /// All allocated memory in a step + pub(crate) memory: MemoryRef, + /// Program counter before step execution + pub(crate) pc: u64, + /// Remaining gas before step execution + pub(crate) gas_remaining: u64, + /// Gas cost of step execution + pub(crate) cost: u64, + /// Call depth + pub(crate) depth: u64, + /// Gas refund counter before step execution + pub(crate) refund: u64, + /// returns information about the error if one occurred, otherwise returns undefined + pub(crate) error: Option, + /// The contract object available to the js inspector + pub(crate) contract: Contract, +} + +impl StepLog { + /// Converts the contract object into a js object + /// + /// Caution: this expects a global property `bigint` to be present. + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let Self { + stack, + op, + memory, + pc, + gas_remaining: gas, + cost, + depth, + refund, + error, + contract, + } = self; + let obj = JsObject::default(); + + // fields + let op = op.into_js_object(context)?; + let memory = memory.into_js_object(context)?; + let stack = stack.into_js_object(context)?; + let contract = contract.into_js_object(context)?; + + obj.set("op", op, false, context)?; + obj.set("memory", memory, false, context)?; + obj.set("stack", stack, false, context)?; + obj.set("contract", contract, false, context)?; + + // methods + let error = + if let Some(error) = error { JsValue::from(error) } else { JsValue::undefined() }; + let get_error = js_value_capture_getter!(error, context); + let get_pc = js_value_getter!(pc, context); + let get_gas = js_value_getter!(gas, context); + let get_cost = js_value_getter!(cost, context); + let get_refund = js_value_getter!(refund, context); + let get_depth = js_value_getter!(depth, context); + + obj.set("getPc", get_pc, false, context)?; + obj.set("getError", get_error, false, context)?; + obj.set("getGas", get_gas, false, context)?; + obj.set("getCost", get_cost, false, context)?; + obj.set("getDepth", get_depth, false, context)?; + obj.set("getRefund", get_refund, false, context)?; + + Ok(obj) + } +} + +/// Represents the memory object +#[derive(Debug, Clone)] +pub(crate) struct MemoryRef(pub(crate) GuardedNullableGcRef); + +impl MemoryRef { + /// Creates a new stack reference + pub(crate) fn new(mem: &SharedMemory) -> (Self, RefGuard<'_, SharedMemory>) { + let (inner, guard) = GuardedNullableGcRef::new(mem); + (MemoryRef(inner), guard) + } + + fn len(&self) -> usize { + self.0.with_inner(|mem| mem.len()).unwrap_or_default() + } + + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let obj = JsObject::default(); + let len = self.len(); + + let length = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| { + Ok(JsValue::from(len as u64)) + }), + ) + .length(0) + .build(); + + // slice returns the requested range of memory as a byte slice. + let slice = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, memory, ctx| { + let start = args.get_or_undefined(0).to_number(ctx)?; + let end = args.get_or_undefined(1).to_number(ctx)?; + if end < start || start < 0. || (end as usize) < memory.len() { + return Err(JsError::from_native(JsNativeError::typ().with_message( + format!( + "tracer accessed out of bound memory: offset {start}, end {end}" + ), + ))) + } + let start = start as usize; + let end = end as usize; + let size = end - start; + let slice = memory + .0 + .with_inner(|mem| mem.slice(start, size).to_vec()) + .unwrap_or_default(); + + to_buf_value(slice, ctx) + }, + self.clone(), + ), + ) + .length(2) + .build(); + + let get_uint = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, memory, ctx| { + let offset_f64 = args.get_or_undefined(0).to_number(ctx)?; + let len = memory.len(); + let offset = offset_f64 as usize; + if len < offset+32 || offset_f64 < 0. { + return Err(JsError::from_native( + JsNativeError::typ().with_message(format!("tracer accessed out of bound memory: available {len}, offset {offset}, size 32")) + )); + } + let slice = memory.0.with_inner(|mem| mem.slice(offset, 32).to_vec()).unwrap_or_default(); + to_buf_value(slice, ctx) + }, + self + ), + ) + .length(1) + .build(); + + obj.set("slice", slice, false, context)?; + obj.set("getUint", get_uint, false, context)?; + obj.set("length", length, false, context)?; + Ok(obj) + } +} + +impl Finalize for MemoryRef {} + +unsafe impl Trace for MemoryRef { + empty_trace!(); +} + +/// Represents the state object +#[derive(Debug, Clone)] +pub(crate) struct StateRef(pub(crate) GuardedNullableGcRef); + +impl StateRef { + /// Creates a new stack reference + pub(crate) fn new(state: &State) -> (Self, RefGuard<'_, State>) { + let (inner, guard) = GuardedNullableGcRef::new(state); + (StateRef(inner), guard) + } + + fn get_account(&self, address: &Address) -> Option { + self.0.with_inner(|state| state.get(address).map(|acc| acc.info.clone()))? + } +} + +impl Finalize for StateRef {} + +unsafe impl Trace for StateRef { + empty_trace!(); +} + +/// Represents the opcode object +#[derive(Debug)] +pub(crate) struct OpObj(pub(crate) u8); + +impl OpObj { + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let obj = JsObject::default(); + let value = self.0; + let is_push = (PUSH0..=PUSH32).contains(&value); + + let to_number = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(value))), + ) + .length(0) + .build(); + + let is_push = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(is_push))), + ) + .length(0) + .build(); + + let to_string = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| { + let op = OpCode::new(value) + .or_else(|| { + // if the opcode is invalid, we'll use the invalid opcode to represent it + // because this is invoked before the opcode is + // executed, the evm will eventually return a `Halt` + // with invalid/unknown opcode as result + let invalid_opcode = 0xfe; + OpCode::new(invalid_opcode) + }) + .expect("is valid opcode;"); + let s = op.to_string(); + Ok(JsValue::from(s)) + }), + ) + .length(0) + .build(); + + obj.set("toNumber", to_number, false, context)?; + obj.set("toString", to_string, false, context)?; + obj.set("isPush", is_push, false, context)?; + Ok(obj) + } +} + +impl From for OpObj { + fn from(op: u8) -> Self { + Self(op) + } +} + +/// Represents the stack object +#[derive(Debug)] +pub(crate) struct StackRef(pub(crate) GuardedNullableGcRef); + +impl StackRef { + /// Creates a new stack reference + pub(crate) fn new(stack: &Stack) -> (Self, RefGuard<'_, Stack>) { + let (inner, guard) = GuardedNullableGcRef::new(stack); + (StackRef(inner), guard) + } + + fn peek(&self, idx: usize, ctx: &mut Context<'_>) -> JsResult { + self.0 + .with_inner(|stack| { + let value = stack.peek(idx).map_err(|_| { + JsError::from_native(JsNativeError::typ().with_message(format!( + "tracer accessed out of bound stack: size {}, index {}", + stack.len(), + idx + ))) + })?; + to_bigint(value, ctx) + }) + .ok_or_else(|| { + JsError::from_native(JsNativeError::typ().with_message(format!( + "tracer accessed out of bound stack: size 0, index {}", + idx + ))) + })? + } + + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let obj = JsObject::default(); + let len = self.0.with_inner(|stack| stack.len()).unwrap_or_default(); + let length = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(len))), + ) + .length(0) + .build(); + + // peek returns the nth-from-the-top element of the stack. + let peek = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, stack, ctx| { + let idx_f64 = args.get_or_undefined(0).to_number(ctx)?; + let idx = idx_f64 as usize; + if len <= idx || idx_f64 < 0. { + return Err(JsError::from_native(JsNativeError::typ().with_message( + format!( + "tracer accessed out of bound stack: size {len}, index {idx_f64}" + ), + ))) + } + stack.peek(idx, ctx) + }, + self, + ), + ) + .length(1) + .build(); + + obj.set("length", length, false, context)?; + obj.set("peek", peek, false, context)?; + Ok(obj) + } +} + +impl Finalize for StackRef {} + +unsafe impl Trace for StackRef { + empty_trace!(); +} + +/// Represents the contract object +#[derive(Debug, Clone, Default)] +pub(crate) struct Contract { + pub(crate) caller: Address, + pub(crate) contract: Address, + pub(crate) value: U256, + pub(crate) input: Bytes, +} + +impl Contract { + /// Converts the contract object into a js object + /// + /// Caution: this expects a global property `bigint` to be present. + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let Contract { caller, contract, value, input } = self; + let obj = JsObject::default(); + + let get_caller = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, ctx| { + to_buf_value(caller.as_slice().to_vec(), ctx) + }), + ) + .length(0) + .build(); + + let get_address = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, ctx| { + to_buf_value(contract.as_slice().to_vec(), ctx) + }), + ) + .length(0) + .build(); + + let get_value = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)), + ) + .length(0) + .build(); + + let input = to_buf_value(input.to_vec(), context)?; + let get_input = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, _args, input, _ctx| Ok(input.clone()), + input, + ), + ) + .length(0) + .build(); + + obj.set("getCaller", get_caller, false, context)?; + obj.set("getAddress", get_address, false, context)?; + obj.set("getValue", get_value, false, context)?; + obj.set("getInput", get_input, false, context)?; + + Ok(obj) + } +} + +/// Represents the call frame object for exit functions +pub(crate) struct FrameResult { + pub(crate) gas_used: u64, + pub(crate) output: Bytes, + pub(crate) error: Option, +} + +impl FrameResult { + pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult { + let Self { gas_used, output, error } = self; + let obj = JsObject::default(); + + let output = to_buf_value(output.to_vec(), ctx)?; + let get_output = FunctionObjectBuilder::new( + ctx, + NativeFunction::from_copy_closure_with_captures( + move |_this, _args, output, _ctx| Ok(output.clone()), + output, + ), + ) + .length(0) + .build(); + + let error = error.map(JsValue::from).unwrap_or_default(); + let get_error = js_value_capture_getter!(error, ctx); + let get_gas_used = js_value_getter!(gas_used, ctx); + + obj.set("getGasUsed", get_gas_used, false, ctx)?; + obj.set("getOutput", get_output, false, ctx)?; + obj.set("getError", get_error, false, ctx)?; + + Ok(obj) + } +} + +/// Represents the call frame object for enter functions +pub(crate) struct CallFrame { + pub(crate) contract: Contract, + pub(crate) kind: CallKind, + pub(crate) gas: u64, +} + +impl CallFrame { + pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult { + let CallFrame { contract: Contract { caller, contract, value, input }, kind, gas } = self; + let obj = JsObject::default(); + + let get_from = FunctionObjectBuilder::new( + ctx, + NativeFunction::from_copy_closure(move |_this, _args, ctx| { + to_buf_value(caller.as_slice().to_vec(), ctx) + }), + ) + .length(0) + .build(); + + let get_to = FunctionObjectBuilder::new( + ctx, + NativeFunction::from_copy_closure(move |_this, _args, ctx| { + to_buf_value(contract.as_slice().to_vec(), ctx) + }), + ) + .length(0) + .build(); + + let get_value = FunctionObjectBuilder::new( + ctx, + NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)), + ) + .length(0) + .build(); + + let input = to_buf_value(input.to_vec(), ctx)?; + let get_input = FunctionObjectBuilder::new( + ctx, + NativeFunction::from_copy_closure_with_captures( + move |_this, _args, input, _ctx| Ok(input.clone()), + input, + ), + ) + .length(0) + .build(); + + let get_gas = js_value_getter!(gas, ctx); + let ty = kind.to_string(); + let get_type = js_value_capture_getter!(ty, ctx); + + obj.set("getFrom", get_from, false, ctx)?; + obj.set("getTo", get_to, false, ctx)?; + obj.set("getValue", get_value, false, ctx)?; + obj.set("getInput", get_input, false, ctx)?; + obj.set("getGas", get_gas, false, ctx)?; + obj.set("getType", get_type, false, ctx)?; + + Ok(obj) + } +} + +/// The `ctx` object that represents the context in which the transaction is executed. +pub(crate) struct EvmContext { + /// String, one of the two values CALL and CREATE + pub(crate) r#type: String, + /// Sender of the transaction + pub(crate) from: Address, + /// Target of the transaction + pub(crate) to: Option
, + pub(crate) input: Bytes, + /// Gas limit + pub(crate) gas: u64, + /// Number, amount of gas used in executing the transaction (excludes txdata costs) + pub(crate) gas_used: u64, + /// Number, gas price configured in the transaction being executed + pub(crate) gas_price: u64, + /// Number, intrinsic gas for the transaction being executed + pub(crate) intrinsic_gas: u64, + /// big.int Amount to be transferred in wei + pub(crate) value: U256, + /// Number, block number + pub(crate) block: u64, + pub(crate) output: Bytes, + /// Number, block number + pub(crate) time: String, + pub(crate) block_hash: Option, + pub(crate) tx_index: Option, + pub(crate) tx_hash: Option, +} + +impl EvmContext { + pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult { + let Self { + r#type, + from, + to, + input, + gas, + gas_used, + gas_price, + intrinsic_gas, + value, + block, + output, + time, + block_hash, + tx_index, + tx_hash, + } = self; + let obj = JsObject::default(); + + // add properties + + obj.set("type", r#type, false, ctx)?; + obj.set("from", address_to_buf(from, ctx)?, false, ctx)?; + if let Some(to) = to { + obj.set("to", address_to_buf(to, ctx)?, false, ctx)?; + } else { + obj.set("to", JsValue::null(), false, ctx)?; + } + + obj.set("input", to_buf(input.to_vec(), ctx)?, false, ctx)?; + obj.set("gas", gas, false, ctx)?; + obj.set("gasUsed", gas_used, false, ctx)?; + obj.set("gasPrice", gas_price, false, ctx)?; + obj.set("intrinsicGas", intrinsic_gas, false, ctx)?; + obj.set("value", to_bigint(value, ctx)?, false, ctx)?; + obj.set("block", block, false, ctx)?; + obj.set("output", to_buf(output.to_vec(), ctx)?, false, ctx)?; + obj.set("time", time, false, ctx)?; + if let Some(block_hash) = block_hash { + obj.set("blockHash", to_buf(block_hash.as_slice().to_vec(), ctx)?, false, ctx)?; + } + if let Some(tx_index) = tx_index { + obj.set("txIndex", tx_index as u64, false, ctx)?; + } + if let Some(tx_hash) = tx_hash { + obj.set("txHash", to_buf(tx_hash.as_slice().to_vec(), ctx)?, false, ctx)?; + } + + Ok(obj) + } +} + +/// DB is the object that allows the js inspector to interact with the database. +#[derive(Debug, Clone)] +pub(crate) struct EvmDbRef { + state: StateRef, + to_db: mpsc::Sender, +} + +impl EvmDbRef { + /// Creates a new DB reference + pub(crate) fn new( + state: &State, + to_db: mpsc::Sender, + ) -> (Self, RefGuard<'_, State>) { + let (state, guard) = StateRef::new(state); + let this = Self { state, to_db }; + (this, guard) + } + + fn read_basic(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult> { + let buf = from_buf(address, ctx)?; + let address = bytes_to_address(buf); + if let acc @ Some(_) = self.state.get_account(&address) { + return Ok(acc) + } + let (tx, rx) = channel(); + if self.to_db.try_send(JsDbRequest::Basic { address, resp: tx }).is_err() { + return Err(JsError::from_native( + JsNativeError::error() + .with_message(format!("Failed to read address {address:?} from database",)), + )) + } + + match rx.recv() { + Ok(Ok(maybe_acc)) => Ok(maybe_acc), + _ => Err(JsError::from_native( + JsNativeError::error() + .with_message(format!("Failed to read address {address:?} from database",)), + )), + } + } + + fn read_code(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult { + let acc = self.read_basic(address, ctx)?; + let code_hash = acc.map(|acc| acc.code_hash).unwrap_or(KECCAK_EMPTY); + if code_hash == KECCAK_EMPTY { + return JsArrayBuffer::new(0, ctx) + } + + let (tx, rx) = channel(); + if self.to_db.try_send(JsDbRequest::Code { code_hash, resp: tx }).is_err() { + return Err(JsError::from_native( + JsNativeError::error() + .with_message(format!("Failed to read code hash {code_hash:?} from database",)), + )) + } + + let code = match rx.recv() { + Ok(Ok(code)) => code, + _ => { + return Err(JsError::from_native(JsNativeError::error().with_message(format!( + "Failed to read code hash {code_hash:?} from database", + )))) + } + }; + + to_buf(code.to_vec(), ctx) + } + + fn read_state( + &self, + address: JsValue, + slot: JsValue, + ctx: &mut Context<'_>, + ) -> JsResult { + let buf = from_buf(address, ctx)?; + let address = bytes_to_address(buf); + + let buf = from_buf(slot, ctx)?; + let slot = bytes_to_hash(buf); + + let (tx, rx) = channel(); + if self + .to_db + .try_send(JsDbRequest::StorageAt { address, index: slot.into(), resp: tx }) + .is_err() + { + return Err(JsError::from_native(JsNativeError::error().with_message(format!( + "Failed to read state for {address:?} at {slot:?} from database", + )))) + } + + let value = match rx.recv() { + Ok(Ok(value)) => value, + _ => { + return Err(JsError::from_native(JsNativeError::error().with_message(format!( + "Failed to read state for {address:?} at {slot:?} from database", + )))) + } + }; + let value: B256 = value.into(); + to_buf(value.as_slice().to_vec(), ctx) + } + + pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult { + let obj = JsObject::default(); + let exists = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, db, ctx| { + let val = args.get_or_undefined(0).clone(); + let acc = db.read_basic(val, ctx)?; + let exists = acc.is_some(); + Ok(JsValue::from(exists)) + }, + self.clone(), + ), + ) + .length(1) + .build(); + + let get_balance = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, db, ctx| { + let val = args.get_or_undefined(0).clone(); + let acc = db.read_basic(val, ctx)?; + let balance = acc.map(|acc| acc.balance).unwrap_or_default(); + to_bigint(balance, ctx) + }, + self.clone(), + ), + ) + .length(1) + .build(); + + let get_nonce = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, db, ctx| { + let val = args.get_or_undefined(0).clone(); + let acc = db.read_basic(val, ctx)?; + let nonce = acc.map(|acc| acc.nonce).unwrap_or_default(); + Ok(JsValue::from(nonce)) + }, + self.clone(), + ), + ) + .length(1) + .build(); + + let get_code = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, db, ctx| { + let val = args.get_or_undefined(0).clone(); + Ok(db.read_code(val, ctx)?.into()) + }, + self.clone(), + ), + ) + .length(1) + .build(); + + let get_state = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + move |_this, args, db, ctx| { + let addr = args.get_or_undefined(0).clone(); + let slot = args.get_or_undefined(1).clone(); + Ok(db.read_state(addr, slot, ctx)?.into()) + }, + self, + ), + ) + .length(2) + .build(); + + obj.set("getBalance", get_balance, false, context)?; + obj.set("getNonce", get_nonce, false, context)?; + obj.set("getCode", get_code, false, context)?; + obj.set("getState", get_state, false, context)?; + obj.set("exists", exists, false, context)?; + Ok(obj) + } +} + +impl Finalize for EvmDbRef {} + +unsafe impl Trace for EvmDbRef { + empty_trace!(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tracing::js::builtins::BIG_INT_JS; + use boa_engine::{object::builtins::JsArrayBuffer, property::Attribute, Source}; + + #[test] + fn test_contract() { + let mut ctx = Context::default(); + let contract = Contract { + caller: Address::ZERO, + contract: Address::ZERO, + value: U256::from(1337u64), + input: vec![0x01, 0x02, 0x03].into(), + }; + let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS)).unwrap(); + ctx.register_global_property("bigint", big_int, Attribute::all()).unwrap(); + + let obj = contract.clone().into_js_object(&mut ctx).unwrap(); + let s = "({ + call: function(contract) { return contract.getCaller(); }, + value: function(contract) { return contract.getValue(); }, + input: function(contract) { return contract.getInput(); } + })"; + + let eval_obj = ctx.eval(Source::from_bytes(s)).unwrap(); + + let call = eval_obj.as_object().unwrap().get("call", &mut ctx).unwrap(); + let res = call + .as_callable() + .unwrap() + .call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx) + .unwrap(); + assert!(res.is_object()); + assert!(res.as_object().unwrap().is_array_buffer()); + + let call = eval_obj.as_object().unwrap().get("value", &mut ctx).unwrap(); + let res = call + .as_callable() + .unwrap() + .call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx) + .unwrap(); + assert_eq!( + res.to_string(&mut ctx).unwrap().to_std_string().unwrap(), + contract.value.to_string() + ); + + let call = eval_obj.as_object().unwrap().get("input", &mut ctx).unwrap(); + let res = call + .as_callable() + .unwrap() + .call(&JsValue::undefined(), &[obj.into()], &mut ctx) + .unwrap(); + + let buffer = JsArrayBuffer::from_object(res.as_object().unwrap().clone()).unwrap(); + let input = buffer.take().unwrap(); + assert_eq!(input, contract.input); + } +} diff --git a/src/tracing/js/builtins.rs b/src/tracing/js/builtins.rs new file mode 100644 index 00000000..91a7a267 --- /dev/null +++ b/src/tracing/js/builtins.rs @@ -0,0 +1,258 @@ +//! Builtin functions + +use alloy_primitives::{hex, Address, B256, U256}; +use boa_engine::{ + object::builtins::{JsArray, JsArrayBuffer}, + property::Attribute, + Context, JsArgs, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction, Source, +}; +use boa_gc::{empty_trace, Finalize, Trace}; +use std::collections::HashSet; + +/// bigIntegerJS is the minified version of . +pub(crate) const BIG_INT_JS: &str = include_str!("bigint.js"); + +/// Registers all the builtin functions and global bigint property +/// +/// Note: this does not register the `isPrecompiled` builtin, as this requires the precompile +/// addresses, see [PrecompileList::register_callable]. +pub(crate) fn register_builtins(ctx: &mut Context<'_>) -> JsResult<()> { + let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes()))?; + ctx.register_global_property("bigint", big_int, Attribute::all())?; + ctx.register_global_builtin_callable("toHex", 1, NativeFunction::from_fn_ptr(to_hex))?; + ctx.register_global_callable("toWord", 1, NativeFunction::from_fn_ptr(to_word))?; + ctx.register_global_callable("toAddress", 1, NativeFunction::from_fn_ptr(to_address))?; + ctx.register_global_callable("toContract", 2, NativeFunction::from_fn_ptr(to_contract))?; + ctx.register_global_callable("toContract2", 3, NativeFunction::from_fn_ptr(to_contract2))?; + + Ok(()) +} + +/// Converts an array, hex string or Uint8Array to a []byte +pub(crate) fn from_buf(val: JsValue, context: &mut Context<'_>) -> JsResult> { + if let Some(obj) = val.as_object().cloned() { + if obj.is_array_buffer() { + let buf = JsArrayBuffer::from_object(obj)?; + return buf.take() + } else if obj.is_string() { + let js_string = obj.borrow().as_string().unwrap(); + return hex_decode_js_string(js_string) + } else if obj.is_array() { + let array = JsArray::from_object(obj)?; + let len = array.length(context)?; + let mut buf = Vec::with_capacity(len as usize); + for i in 0..len { + let val = array.get(i, context)?; + buf.push(val.to_number(context)? as u8); + } + return Ok(buf) + } + } + + Err(JsError::from_native(JsNativeError::typ().with_message("invalid buffer type"))) +} + +/// Create a new array buffer from the address' bytes. +pub(crate) fn address_to_buf(addr: Address, context: &mut Context<'_>) -> JsResult { + to_buf(addr.0.to_vec(), context) +} + +/// Create a new array buffer from byte block. +pub(crate) fn to_buf(bytes: Vec, context: &mut Context<'_>) -> JsResult { + JsArrayBuffer::from_byte_block(bytes, context) +} + +/// Create a new array buffer object from byte block. +pub(crate) fn to_buf_value(bytes: Vec, context: &mut Context<'_>) -> JsResult { + Ok(to_buf(bytes, context)?.into()) +} + +/// Converts a buffer type to an address. +/// +/// If the buffer is larger than the address size, it will be cropped from the left +pub(crate) fn bytes_to_address(buf: Vec) -> Address { + let mut address = Address::default(); + let mut buf = &buf[..]; + let address_len = address.0.len(); + if buf.len() > address_len { + // crop from left + buf = &buf[buf.len() - address.0.len()..]; + } + let address_slice = &mut address.0[address_len - buf.len()..]; + address_slice.copy_from_slice(buf); + address +} + +/// Converts a buffer type to a hash. +/// +/// If the buffer is larger than the hash size, it will be cropped from the left +pub(crate) fn bytes_to_hash(buf: Vec) -> B256 { + let mut hash = B256::default(); + let mut buf = &buf[..]; + let hash_len = hash.0.len(); + if buf.len() > hash_len { + // crop from left + buf = &buf[buf.len() - hash.0.len()..]; + } + let hash_slice = &mut hash.0[hash_len - buf.len()..]; + hash_slice.copy_from_slice(buf); + hash +} + +/// Converts a U256 to a bigint using the global bigint property +pub(crate) fn to_bigint(value: U256, ctx: &mut Context<'_>) -> JsResult { + let bigint = ctx.global_object().get("bigint", ctx)?; + if !bigint.is_callable() { + return Ok(JsValue::undefined()) + } + bigint.as_callable().unwrap().call( + &JsValue::undefined(), + &[JsValue::from(value.to_string())], + ctx, + ) +} +/// Takes three arguments: a JavaScript value that represents the sender's address, a string salt +/// value, and the initcode for the contract. Compute the address of a contract created by the +/// sender with the given salt and code hash, then converts the resulting address back into a byte +/// buffer for output. +pub(crate) fn to_contract2( + _: &JsValue, + args: &[JsValue], + ctx: &mut Context<'_>, +) -> JsResult { + // Extract the sender's address, salt and initcode from the arguments + let from = args.get_or_undefined(0).clone(); + let salt = match args.get_or_undefined(1).to_string(ctx) { + Ok(js_string) => { + let buf = hex_decode_js_string(js_string)?; + bytes_to_hash(buf) + } + Err(_) => { + return Err(JsError::from_native(JsNativeError::typ().with_message("invalid salt type"))) + } + }; + + let initcode = args.get_or_undefined(2).clone(); + + // Convert the sender's address to a byte buffer and then to an Address + let buf = from_buf(from, ctx)?; + let addr = bytes_to_address(buf); + + // Convert the initcode to a byte buffer + let code_buf = from_buf(initcode, ctx)?; + + // Compute the contract address + let contract_addr = addr.create2_from_code(salt, code_buf); + + // Convert the contract address to a byte buffer and return it as an ArrayBuffer + to_buf_value(contract_addr.0.to_vec(), ctx) +} + +/// Converts the sender's address to a byte buffer +pub(crate) fn to_contract( + _: &JsValue, + args: &[JsValue], + ctx: &mut Context<'_>, +) -> JsResult { + // Extract the sender's address and nonce from the arguments + let from = args.get_or_undefined(0).clone(); + let nonce = args.get_or_undefined(1).to_number(ctx)? as u64; + + // Convert the sender's address to a byte buffer and then to an Address + let buf = from_buf(from, ctx)?; + let addr = bytes_to_address(buf); + + // Compute the contract address + let contract_addr = addr.create(nonce); + + // Convert the contract address to a byte buffer and return it as an ArrayBuffer + to_buf_value(contract_addr.0.to_vec(), ctx) +} + +/// Converts a buffer type to an address +pub(crate) fn to_address( + _: &JsValue, + args: &[JsValue], + ctx: &mut Context<'_>, +) -> JsResult { + let val = args.get_or_undefined(0).clone(); + let buf = from_buf(val, ctx)?; + let address = bytes_to_address(buf); + to_buf_value(address.0.to_vec(), ctx) +} + +/// Converts a buffer type to a word +pub(crate) fn to_word(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult { + let val = args.get_or_undefined(0).clone(); + let buf = from_buf(val, ctx)?; + let hash = bytes_to_hash(buf); + to_buf_value(hash.0.to_vec(), ctx) +} + +/// Converts a buffer type to a hex string +pub(crate) fn to_hex(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult { + let val = args.get_or_undefined(0).clone(); + let buf = from_buf(val, ctx)?; + Ok(JsValue::from(hex::encode(buf))) +} + +/// Decodes a hex decoded js-string +fn hex_decode_js_string(js_string: JsString) -> JsResult> { + match js_string.to_std_string() { + Ok(s) => match hex::decode(s.as_str()) { + Ok(data) => Ok(data), + Err(err) => Err(JsError::from_native( + JsNativeError::error().with_message(format!("invalid hex string {s}: {err}",)), + )), + }, + Err(err) => Err(JsError::from_native( + JsNativeError::error() + .with_message(format!("invalid utf8 string {js_string:?}: {err}",)), + )), + } +} + +/// A container for all precompile addresses used for the `isPrecompiled` global callable. +#[derive(Debug, Clone)] +pub(crate) struct PrecompileList(pub(crate) HashSet
); + +impl PrecompileList { + /// Registers the global callable `isPrecompiled` + pub(crate) fn register_callable(self, ctx: &mut Context<'_>) -> JsResult<()> { + let is_precompiled = NativeFunction::from_copy_closure_with_captures( + move |_this, args, precompiles, ctx| { + let val = args.get_or_undefined(0).clone(); + let buf = from_buf(val, ctx)?; + let addr = bytes_to_address(buf); + Ok(precompiles.0.contains(&addr).into()) + }, + self, + ); + + ctx.register_global_callable("isPrecompiled", 1, is_precompiled)?; + + Ok(()) + } +} + +impl Finalize for PrecompileList {} + +unsafe impl Trace for PrecompileList { + empty_trace!(); +} + +#[cfg(test)] +mod tests { + use super::*; + use boa_engine::Source; + + #[test] + fn test_install_bigint() { + let mut ctx = Context::default(); + let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes())).unwrap(); + let value = JsValue::from(100); + let result = + big_int.as_callable().unwrap().call(&JsValue::undefined(), &[value], &mut ctx).unwrap(); + assert_eq!(result.to_string(&mut ctx).unwrap().to_std_string().unwrap(), "100"); + } +} diff --git a/src/tracing/js/mod.rs b/src/tracing/js/mod.rs new file mode 100644 index 00000000..f6982b6f --- /dev/null +++ b/src/tracing/js/mod.rs @@ -0,0 +1,583 @@ +//! Javascript inspector + +use crate::tracing::{ + js::{ + bindings::{ + CallFrame, Contract, EvmContext, EvmDbRef, FrameResult, MemoryRef, StackRef, StepLog, + }, + builtins::{register_builtins, PrecompileList}, + }, + types::CallKind, +}; +use alloy_primitives::{Address, Bytes, B256, U256}; +use boa_engine::{Context, JsError, JsObject, JsResult, JsValue, Source}; +use revm::{ + interpreter::{ + return_revert, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, Interpreter, + }, + precompile::Precompiles, + primitives::{AccountInfo, Env, ExecutionResult, Output, ResultAndState, TransactTo}, + Database, EVMData, Inspector, +}; +use tokio::sync::mpsc; + +pub(crate) mod bindings; +pub(crate) mod builtins; + +/// A javascript inspector that will delegate inspector functions to javascript functions +/// +/// See also +#[derive(Debug)] +pub struct JsInspector { + ctx: Context<'static>, + /// The javascript config provided to the inspector. + _config: JsValue, + /// The evaluated object that contains the inspector functions. + obj: JsObject, + + /// The javascript function that will be called when the result is requested. + result_fn: JsObject, + fault_fn: JsObject, + + // EVM inspector hook functions + /// Invoked when the EVM enters a new call that is _NOT_ the top level call. + /// + /// Corresponds to [Inspector::call] and [Inspector::create_end] but is also invoked on + /// [Inspector::selfdestruct]. + enter_fn: Option, + /// Invoked when the EVM exits a call that is _NOT_ the top level call. + /// + /// Corresponds to [Inspector::call_end] and [Inspector::create_end] but also invoked after + /// selfdestruct. + exit_fn: Option, + /// Executed before each instruction is executed. + step_fn: Option, + /// Keeps track of the current call stack. + call_stack: Vec, + /// sender half of a channel to communicate with the database service. + to_db_service: mpsc::Sender, + /// Marker to track whether the precompiles have been registered. + precompiles_registered: bool, +} + +impl JsInspector { + /// Creates a new inspector from a javascript code snipped that evaluates to an object with the + /// expected fields and a config object. + /// + /// The object must have the following fields: + /// - `result`: a function that will be called when the result is requested. + /// - `fault`: a function that will be called when the transaction fails. + /// + /// Optional functions are invoked during inspection: + /// - `setup`: a function that will be called before the inspection starts. + /// - `enter`: a function that will be called when the execution enters a new call. + /// - `exit`: a function that will be called when the execution exits a call. + /// - `step`: a function that will be called when the execution steps to the next instruction. + /// + /// This also accepts a sender half of a channel to communicate with the database service so the + /// DB can be queried from inside the inspector. + pub fn new( + code: String, + config: serde_json::Value, + to_db_service: mpsc::Sender, + ) -> Result { + // Instantiate the execution context + let mut ctx = Context::default(); + register_builtins(&mut ctx)?; + + // evaluate the code + let code = format!("({})", code); + let obj = + ctx.eval(Source::from_bytes(code.as_bytes())).map_err(JsInspectorError::EvalCode)?; + + let obj = obj.as_object().cloned().ok_or(JsInspectorError::ExpectedJsObject)?; + + // ensure all the fields are callables, if present + + let result_fn = obj + .get("result", &mut ctx)? + .as_object() + .cloned() + .ok_or(JsInspectorError::ResultFunctionMissing)?; + if !result_fn.is_callable() { + return Err(JsInspectorError::ResultFunctionMissing) + } + + let fault_fn = obj + .get("fault", &mut ctx)? + .as_object() + .cloned() + .ok_or(JsInspectorError::FaultFunctionMissing)?; + if !result_fn.is_callable() { + return Err(JsInspectorError::FaultFunctionMissing) + } + + let enter_fn = obj.get("enter", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable()); + let exit_fn = obj.get("exit", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable()); + let step_fn = obj.get("step", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable()); + + let config = + JsValue::from_json(&config, &mut ctx).map_err(JsInspectorError::InvalidJsonConfig)?; + + if let Some(setup_fn) = obj.get("setup", &mut ctx)?.as_object() { + if !setup_fn.is_callable() { + return Err(JsInspectorError::SetupFunctionNotCallable) + } + + // call setup() + setup_fn + .call(&(obj.clone().into()), &[config.clone()], &mut ctx) + .map_err(JsInspectorError::SetupCallFailed)?; + } + + Ok(Self { + ctx, + _config: config, + obj, + result_fn, + fault_fn, + enter_fn, + exit_fn, + step_fn, + call_stack: Default::default(), + to_db_service, + precompiles_registered: false, + }) + } + + /// Calls the result function and returns the result as [serde_json::Value]. + /// + /// Note: This is supposed to be called after the inspection has finished. + pub fn json_result( + &mut self, + res: ResultAndState, + env: &Env, + ) -> Result { + Ok(self.result(res, env)?.to_json(&mut self.ctx)?) + } + + /// Calls the result function and returns the result. + pub fn result(&mut self, res: ResultAndState, env: &Env) -> Result { + let ResultAndState { result, state } = res; + let (db, _db_guard) = EvmDbRef::new(&state, self.to_db_service.clone()); + + let gas_used = result.gas_used(); + let mut to = None; + let mut output_bytes = None; + match result { + ExecutionResult::Success { output, .. } => match output { + Output::Call(out) => { + output_bytes = Some(out); + } + Output::Create(out, addr) => { + to = addr; + output_bytes = Some(out); + } + }, + ExecutionResult::Revert { output, .. } => { + output_bytes = Some(output); + } + ExecutionResult::Halt { .. } => {} + }; + + let ctx = EvmContext { + r#type: match env.tx.transact_to { + TransactTo::Call(target) => { + to = Some(target); + "CALL" + } + TransactTo::Create(_) => "CREATE", + } + .to_string(), + from: env.tx.caller, + to, + input: env.tx.data.clone(), + gas: env.tx.gas_limit, + gas_used, + gas_price: env.tx.gas_price.try_into().unwrap_or(u64::MAX), + value: env.tx.value, + block: env.block.number.try_into().unwrap_or(u64::MAX), + output: output_bytes.unwrap_or_default(), + time: env.block.timestamp.to_string(), + // TODO: fill in the following fields + intrinsic_gas: 0, + block_hash: None, + tx_index: None, + tx_hash: None, + }; + let ctx = ctx.into_js_object(&mut self.ctx)?; + let db = db.into_js_object(&mut self.ctx)?; + Ok(self.result_fn.call( + &(self.obj.clone().into()), + &[ctx.into(), db.into()], + &mut self.ctx, + )?) + } + + fn try_fault(&mut self, step: StepLog, db: EvmDbRef) -> JsResult<()> { + let step = step.into_js_object(&mut self.ctx)?; + let db = db.into_js_object(&mut self.ctx)?; + self.fault_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?; + Ok(()) + } + + fn try_step(&mut self, step: StepLog, db: EvmDbRef) -> JsResult<()> { + if let Some(step_fn) = &self.step_fn { + let step = step.into_js_object(&mut self.ctx)?; + let db = db.into_js_object(&mut self.ctx)?; + step_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?; + } + Ok(()) + } + + fn try_enter(&mut self, frame: CallFrame) -> JsResult<()> { + if let Some(enter_fn) = &self.enter_fn { + let frame = frame.into_js_object(&mut self.ctx)?; + enter_fn.call(&(self.obj.clone().into()), &[frame.into()], &mut self.ctx)?; + } + Ok(()) + } + + fn try_exit(&mut self, frame: FrameResult) -> JsResult<()> { + if let Some(exit_fn) = &self.exit_fn { + let frame = frame.into_js_object(&mut self.ctx)?; + exit_fn.call(&(self.obj.clone().into()), &[frame.into()], &mut self.ctx)?; + } + Ok(()) + } + + /// Returns the currently active call + /// + /// Panics: if there's no call yet + #[track_caller] + fn active_call(&self) -> &CallStackItem { + self.call_stack.last().expect("call stack is empty") + } + + #[inline] + fn pop_call(&mut self) { + self.call_stack.pop(); + } + + /// Returns true whether the active call is the root call. + #[inline] + fn is_root_call_active(&self) -> bool { + self.call_stack.len() == 1 + } + + /// Returns true if there's an enter function and the active call is not the root call. + #[inline] + fn can_call_enter(&self) -> bool { + self.enter_fn.is_some() && !self.is_root_call_active() + } + + /// Returns true if there's an exit function and the active call is not the root call. + #[inline] + fn can_call_exit(&mut self) -> bool { + self.enter_fn.is_some() && !self.is_root_call_active() + } + + /// Pushes a new call to the stack + fn push_call( + &mut self, + address: Address, + data: Bytes, + value: U256, + kind: CallKind, + caller: Address, + gas_limit: u64, + ) -> &CallStackItem { + let call = CallStackItem { + contract: Contract { caller, contract: address, value, input: data }, + kind, + gas_limit, + }; + self.call_stack.push(call); + self.active_call() + } + + /// Registers the precompiles in the JS context + fn register_precompiles(&mut self, precompiles: &Precompiles) { + if !self.precompiles_registered { + return + } + let precompiles = PrecompileList(precompiles.addresses().into_iter().copied().collect()); + + let _ = precompiles.register_callable(&mut self.ctx); + + self.precompiles_registered = true + } +} + +impl Inspector for JsInspector +where + DB: Database, +{ + fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + if self.step_fn.is_none() { + return + } + + let (db, _db_guard) = + EvmDbRef::new(&data.journaled_state.state, self.to_db_service.clone()); + + let (stack, _stack_guard) = StackRef::new(&interp.stack); + let (memory, _memory_guard) = MemoryRef::new(interp.shared_memory); + let step = StepLog { + stack, + op: interp.current_opcode().into(), + memory, + pc: interp.program_counter() as u64, + gas_remaining: interp.gas.remaining(), + cost: interp.gas.spend(), + depth: data.journaled_state.depth(), + refund: interp.gas.refunded() as u64, + error: None, + contract: self.active_call().contract.clone(), + }; + + if self.try_step(step, db).is_err() { + interp.instruction_result = InstructionResult::Revert; + } + } + + fn log( + &mut self, + _evm_data: &mut EVMData<'_, DB>, + _address: &Address, + _topics: &[B256], + _data: &Bytes, + ) { + } + + fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + if self.step_fn.is_none() { + return + } + + if matches!(interp.instruction_result, return_revert!()) { + let (db, _db_guard) = + EvmDbRef::new(&data.journaled_state.state, self.to_db_service.clone()); + + let (stack, _stack_guard) = StackRef::new(&interp.stack); + let (memory, _memory_guard) = MemoryRef::new(interp.shared_memory); + let step = StepLog { + stack, + op: interp.current_opcode().into(), + memory, + pc: interp.program_counter() as u64, + gas_remaining: interp.gas.remaining(), + cost: interp.gas.spend(), + depth: data.journaled_state.depth(), + refund: interp.gas.refunded() as u64, + error: Some(format!("{:?}", interp.instruction_result)), + contract: self.active_call().contract.clone(), + }; + + let _ = self.try_fault(step, db); + } + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + ) -> (InstructionResult, Gas, Bytes) { + self.register_precompiles(&data.precompiles); + + // determine correct `from` and `to` based on the call scheme + let (from, to) = match inputs.context.scheme { + CallScheme::DelegateCall | CallScheme::CallCode => { + (inputs.context.address, inputs.context.code_address) + } + _ => (inputs.context.caller, inputs.context.address), + }; + + let value = inputs.transfer.value; + self.push_call( + to, + inputs.input.clone(), + value, + inputs.context.scheme.into(), + from, + inputs.gas_limit, + ); + + if self.can_call_enter() { + let call = self.active_call(); + let frame = CallFrame { + contract: call.contract.clone(), + kind: call.kind, + gas: inputs.gas_limit, + }; + if let Err(err) = self.try_enter(frame) { + return (InstructionResult::Revert, Gas::new(0), err.to_string().into()) + } + } + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + _data: &mut EVMData<'_, DB>, + _inputs: &CallInputs, + remaining_gas: Gas, + ret: InstructionResult, + out: Bytes, + ) -> (InstructionResult, Gas, Bytes) { + if self.can_call_exit() { + let frame_result = + FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None }; + if let Err(err) = self.try_exit(frame_result) { + return (InstructionResult::Revert, Gas::new(0), err.to_string().into()) + } + } + + self.pop_call(); + + (ret, remaining_gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option
, Gas, Bytes) { + self.register_precompiles(&data.precompiles); + + let _ = data.journaled_state.load_account(inputs.caller, data.db); + let nonce = data.journaled_state.account(inputs.caller).info.nonce; + let address = inputs.created_address(nonce); + self.push_call( + address, + inputs.init_code.clone(), + inputs.value, + inputs.scheme.into(), + inputs.caller, + inputs.gas_limit, + ); + + if self.can_call_enter() { + let call = self.active_call(); + let frame = + CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit }; + if let Err(err) = self.try_enter(frame) { + return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into()) + } + } + + (InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default()) + } + + fn create_end( + &mut self, + _data: &mut EVMData<'_, DB>, + _inputs: &CreateInputs, + ret: InstructionResult, + address: Option
, + remaining_gas: Gas, + out: Bytes, + ) -> (InstructionResult, Option
, Gas, Bytes) { + if self.can_call_exit() { + let frame_result = + FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None }; + if let Err(err) = self.try_exit(frame_result) { + return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into()) + } + } + + self.pop_call(); + + (ret, address, remaining_gas, out) + } + + fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) { + // This is exempt from the root call constraint, because selfdestruct is treated as a + // new scope that is entered and immediately exited. + if self.enter_fn.is_some() { + let call = self.active_call(); + let frame = + CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit }; + let _ = self.try_enter(frame); + } + + // exit with empty frame result ref + if self.exit_fn.is_some() { + let frame_result = FrameResult { gas_used: 0, output: Bytes::new(), error: None }; + let _ = self.try_exit(frame_result); + } + } +} + +/// Request variants to be sent from the inspector to the database +#[derive(Debug, Clone)] +pub enum JsDbRequest { + /// Bindings for [Database::basic] + Basic { + /// The address of the account to be loaded + address: Address, + /// The response channel + resp: std::sync::mpsc::Sender, String>>, + }, + /// Bindings for [Database::code_by_hash] + Code { + /// The code hash of the code to be loaded + code_hash: B256, + /// The response channel + resp: std::sync::mpsc::Sender>, + }, + /// Bindings for [Database::storage] + StorageAt { + /// The address of the account + address: Address, + /// Index of the storage slot + index: U256, + /// The response channel + resp: std::sync::mpsc::Sender>, + }, +} + +/// Represents an active call +#[derive(Debug)] +struct CallStackItem { + contract: Contract, + kind: CallKind, + gas_limit: u64, +} + +/// Error variants that can occur during JavaScript inspection. +#[derive(Debug, thiserror::Error)] +pub enum JsInspectorError { + /// Error originating from a JavaScript operation. + #[error(transparent)] + JsError(#[from] JsError), + + /// Failure during the evaluation of JavaScript code. + #[error("failed to evaluate JS code: {0}")] + EvalCode(JsError), + + /// The evaluated code is not a JavaScript object. + #[error("the evaluated code is not a JS object")] + ExpectedJsObject, + + /// The trace object must expose a function named `result()`. + #[error("trace object must expose a function result()")] + ResultFunctionMissing, + + /// The trace object must expose a function named `fault()`. + #[error("trace object must expose a function fault()")] + FaultFunctionMissing, + + /// The setup object must be a callable function. + #[error("setup object must be a function")] + SetupFunctionNotCallable, + + /// Failure during the invocation of the `setup()` function. + #[error("failed to call setup(): {0}")] + SetupCallFailed(JsError), + + /// Invalid JSON configuration encountered. + #[error("invalid JSON config: {0}")] + InvalidJsonConfig(JsError), +} diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs new file mode 100644 index 00000000..8b4f7eb8 --- /dev/null +++ b/src/tracing/mod.rs @@ -0,0 +1,563 @@ +use self::parity::stack_push_count; +use crate::tracing::{ + arena::PushTraceKind, + types::{ + CallKind, CallTraceNode, LogCallOrder, RecordedMemory, StorageChange, StorageChangeReason, + }, + utils::gas_used, +}; +use alloy_primitives::{Address, Bytes, Log, B256, U256}; +pub use arena::CallTraceArena; +use revm::{ + inspectors::GasInspector, + interpreter::{ + opcode, return_ok, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, + Interpreter, OpCode, + }, + primitives::SpecId, + Database, EVMData, Inspector, JournalEntry, +}; +use types::{CallTrace, CallTraceStep}; + +mod arena; +mod builder; +mod config; +mod fourbyte; +mod opcount; +pub mod types; +mod utils; +pub use builder::{ + geth::{self, GethTraceBuilder}, + parity::{self, ParityTraceBuilder}, +}; +pub use config::{StackSnapshotType, TracingInspectorConfig}; +pub use fourbyte::FourByteInspector; +pub use opcount::OpcodeCountInspector; + +#[cfg(feature = "js-tracer")] +pub mod js; + +/// An inspector that collects call traces. +/// +/// This [Inspector] can be hooked into the [EVM](revm::EVM) which then calls the inspector +/// functions, such as [Inspector::call] or [Inspector::call_end]. +/// +/// The [TracingInspector] keeps track of everything by: +/// 1. start tracking steps/calls on [Inspector::step] and [Inspector::call] +/// 2. complete steps/calls on [Inspector::step_end] and [Inspector::call_end] +#[derive(Debug, Clone)] +pub struct TracingInspector { + /// Configures what and how the inspector records traces. + config: TracingInspectorConfig, + /// Records all call traces + traces: CallTraceArena, + /// Tracks active calls + trace_stack: Vec, + /// Tracks active steps + step_stack: Vec, + /// Tracks the return value of the last call + last_call_return_data: Option, + /// The gas inspector used to track remaining gas. + gas_inspector: GasInspector, + /// The spec id of the EVM. + /// + /// This is filled during execution. + spec_id: Option, +} + +// === impl TracingInspector === + +impl TracingInspector { + /// Returns a new instance for the given config + pub fn new(config: TracingInspectorConfig) -> Self { + Self { + config, + traces: Default::default(), + trace_stack: vec![], + step_stack: vec![], + last_call_return_data: None, + gas_inspector: Default::default(), + spec_id: None, + } + } + + /// Returns the config of the inspector. + pub fn config(&self) -> &TracingInspectorConfig { + &self.config + } + + /// Gets a reference to the recorded call traces. + pub fn get_traces(&self) -> &CallTraceArena { + &self.traces + } + + /// Gets a mutable reference to the recorded call traces. + pub fn get_traces_mut(&mut self) -> &mut CallTraceArena { + &mut self.traces + } + + /// Manually the gas used of the root trace. + /// + /// This is useful if the root trace's gasUsed should mirror the actual gas used by the + /// transaction. + /// + /// This allows setting it manually by consuming the execution result's gas for example. + #[inline] + pub fn set_transaction_gas_used(&mut self, gas_used: u64) { + if let Some(node) = self.traces.arena.first_mut() { + node.trace.gas_used = gas_used; + } + } + + /// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the + /// type. + #[inline] + pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self { + self.set_transaction_gas_used(gas_used); + self + } + + /// Consumes the Inspector and returns a [ParityTraceBuilder]. + #[inline] + pub fn into_parity_builder(self) -> ParityTraceBuilder { + ParityTraceBuilder::new(self.traces.arena, self.spec_id, self.config) + } + + /// Consumes the Inspector and returns a [GethTraceBuilder]. + #[inline] + pub fn into_geth_builder(self) -> GethTraceBuilder { + GethTraceBuilder::new(self.traces.arena, self.config) + } + + /// Returns true if we're no longer in the context of the root call. + fn is_deep(&self) -> bool { + // the root call will always be the first entry in the trace stack + !self.trace_stack.is_empty() + } + + /// Returns true if this a call to a precompile contract. + /// + /// Returns true if the `to` address is a precompile contract and the value is zero. + #[inline] + fn is_precompile_call( + &self, + data: &EVMData<'_, DB>, + to: &Address, + value: U256, + ) -> bool { + if data.precompiles.contains(to) { + // only if this is _not_ the root call + return self.is_deep() && value.is_zero() + } + false + } + + /// Returns the currently active call trace. + /// + /// This will be the last call trace pushed to the stack: the call we entered most recently. + #[track_caller] + #[inline] + fn active_trace(&self) -> Option<&CallTraceNode> { + self.trace_stack.last().map(|idx| &self.traces.arena[*idx]) + } + + /// Returns the last trace [CallTrace] index from the stack. + /// + /// This will be the currently active call trace. + /// + /// # Panics + /// + /// If no [CallTrace] was pushed + #[track_caller] + #[inline] + fn last_trace_idx(&self) -> usize { + self.trace_stack.last().copied().expect("can't start step without starting a trace first") + } + + /// _Removes_ the last trace [CallTrace] index from the stack. + /// + /// # Panics + /// + /// If no [CallTrace] was pushed + #[track_caller] + #[inline] + fn pop_trace_idx(&mut self) -> usize { + self.trace_stack.pop().expect("more traces were filled than started") + } + + /// Starts tracking a new trace. + /// + /// Invoked on [Inspector::call]. + #[allow(clippy::too_many_arguments)] + fn start_trace_on_call( + &mut self, + data: &EVMData<'_, DB>, + address: Address, + input_data: Bytes, + value: U256, + kind: CallKind, + caller: Address, + mut gas_limit: u64, + maybe_precompile: Option, + ) { + // This will only be true if the inspector is configured to exclude precompiles and the call + // is to a precompile + let push_kind = if maybe_precompile.unwrap_or(false) { + // We don't want to track precompiles + PushTraceKind::PushOnly + } else { + PushTraceKind::PushAndAttachToParent + }; + + if self.trace_stack.is_empty() { + // this is the root call which should get the original gas limit of the transaction, + // because initialization costs are already subtracted from gas_limit + // For the root call this value should use the transaction's gas limit + // See and + gas_limit = data.env.tx.gas_limit; + + // we set the spec id here because we only need to do this once and this condition is + // hit exactly once + self.spec_id = Some(data.env.cfg.spec_id); + } + + self.trace_stack.push(self.traces.push_trace( + 0, + push_kind, + CallTrace { + depth: data.journaled_state.depth() as usize, + address, + kind, + data: input_data, + value, + status: InstructionResult::Continue, + caller, + maybe_precompile, + gas_limit, + ..Default::default() + }, + )); + } + + /// Fills the current trace with the outcome of a call. + /// + /// Invoked on [Inspector::call_end]. + /// + /// # Panics + /// + /// This expects an existing trace [Self::start_trace_on_call] + fn fill_trace_on_call_end( + &mut self, + data: &EVMData<'_, DB>, + status: InstructionResult, + gas: &Gas, + output: Bytes, + created_address: Option
, + ) { + let trace_idx = self.pop_trace_idx(); + let trace = &mut self.traces.arena[trace_idx].trace; + + if trace_idx == 0 { + // this is the root call which should get the gas used of the transaction + // refunds are applied after execution, which is when the root call ends + trace.gas_used = gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64); + } else { + trace.gas_used = gas.spend(); + } + + trace.status = status; + trace.success = matches!(status, return_ok!()); + trace.output = output.clone(); + + self.last_call_return_data = Some(output); + + if let Some(address) = created_address { + // A new contract was created via CREATE + trace.address = address; + } + } + + /// Starts tracking a step + /// + /// Invoked on [Inspector::step] + /// + /// # Panics + /// + /// This expects an existing [CallTrace], in other words, this panics if not within the context + /// of a call. + fn start_step(&mut self, interp: &Interpreter<'_>, data: &EVMData<'_, DB>) { + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx]; + + self.step_stack.push(StackStep { trace_idx, step_idx: trace.trace.steps.len() }); + + let memory = self + .config + .record_memory_snapshots + .then(|| RecordedMemory::new(interp.shared_memory.context_memory().to_vec())) + .unwrap_or_default(); + let stack = if self.config.record_stack_snapshots.is_full() { + Some(interp.stack.data().clone()) + } else { + None + }; + + let op = OpCode::new(interp.current_opcode()) + .or_else(|| { + // if the opcode is invalid, we'll use the invalid opcode to represent it because + // this is invoked before the opcode is executed, the evm will eventually return a + // `Halt` with invalid/unknown opcode as result + let invalid_opcode = 0xfe; + OpCode::new(invalid_opcode) + }) + .expect("is valid opcode;"); + + trace.trace.steps.push(CallTraceStep { + depth: data.journaled_state.depth(), + pc: interp.program_counter(), + op, + contract: interp.contract.address, + stack, + push_stack: None, + memory_size: memory.len(), + memory, + gas_remaining: self.gas_inspector.gas_remaining(), + gas_refund_counter: interp.gas.refunded() as u64, + + // fields will be populated end of call + gas_cost: 0, + storage_change: None, + status: InstructionResult::Continue, + }); + } + + /// Fills the current trace with the output of a step. + /// + /// Invoked on [Inspector::step_end]. + fn fill_step_on_step_end( + &mut self, + interp: &Interpreter<'_>, + data: &EVMData<'_, DB>, + ) { + let StackStep { trace_idx, step_idx } = + self.step_stack.pop().expect("can't fill step without starting a step first"); + let step = &mut self.traces.arena[trace_idx].trace.steps[step_idx]; + + if self.config.record_stack_snapshots.is_pushes() { + let num_pushed = stack_push_count(step.op); + let start = interp.stack.len() - num_pushed; + step.push_stack = Some(interp.stack.data()[start..].to_vec()); + } + + if self.config.record_memory_snapshots { + // resize memory so opcodes that allocated memory is correctly displayed + if interp.shared_memory.len() > step.memory.len() { + step.memory.resize(interp.shared_memory.len()); + } + } + if self.config.record_state_diff { + let op = step.op.get(); + + let journal_entry = data + .journaled_state + .journal + .last() + // This should always work because revm initializes it as `vec![vec![]]` + // See [JournaledState::new](revm::JournaledState) + .expect("exists; initialized with vec") + .last(); + + step.storage_change = match (op, journal_entry) { + ( + opcode::SLOAD | opcode::SSTORE, + Some(JournalEntry::StorageChange { address, key, had_value }), + ) => { + // SAFETY: (Address,key) exists if part if StorageChange + let value = data.journaled_state.state[address].storage[key].present_value(); + let reason = match op { + opcode::SLOAD => StorageChangeReason::SLOAD, + opcode::SSTORE => StorageChangeReason::SSTORE, + _ => unreachable!(), + }; + let change = StorageChange { key: *key, value, had_value: *had_value, reason }; + Some(change) + } + _ => None, + }; + } + + // The gas cost is the difference between the recorded gas remaining at the start of the + // step the remaining gas here, at the end of the step. + step.gas_cost = step.gas_remaining - self.gas_inspector.gas_remaining(); + + // set the status + step.status = interp.instruction_result; + } +} + +impl Inspector for TracingInspector +where + DB: Database, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + self.gas_inspector.initialize_interp(interp, data) + } + + fn step(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + if self.config.record_steps { + self.gas_inspector.step(interp, data); + self.start_step(interp, data); + } + } + + fn log( + &mut self, + evm_data: &mut EVMData<'_, DB>, + address: &Address, + topics: &[B256], + data: &Bytes, + ) { + self.gas_inspector.log(evm_data, address, topics, data); + + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx]; + + if self.config.record_logs { + trace.ordering.push(LogCallOrder::Log(trace.logs.len())); + trace.logs.push(Log::new_unchecked(topics.to_vec(), data.clone())); + } + } + + fn step_end(&mut self, interp: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { + if self.config.record_steps { + self.gas_inspector.step_end(interp, data); + self.fill_step_on_step_end(interp, data); + } + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + ) -> (InstructionResult, Gas, Bytes) { + self.gas_inspector.call(data, inputs); + + // determine correct `from` and `to` based on the call scheme + let (from, to) = match inputs.context.scheme { + CallScheme::DelegateCall | CallScheme::CallCode => { + (inputs.context.address, inputs.context.code_address) + } + _ => (inputs.context.caller, inputs.context.address), + }; + + let value = if matches!(inputs.context.scheme, CallScheme::DelegateCall) { + // for delegate calls we need to use the value of the top trace + if let Some(parent) = self.active_trace() { + parent.trace.value + } else { + inputs.transfer.value + } + } else { + inputs.transfer.value + }; + + // if calls to precompiles should be excluded, check whether this is a call to a precompile + let maybe_precompile = + self.config.exclude_precompile_calls.then(|| self.is_precompile_call(data, &to, value)); + + self.start_trace_on_call( + data, + to, + inputs.input.clone(), + value, + inputs.context.scheme.into(), + from, + inputs.gas_limit, + maybe_precompile, + ); + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + gas: Gas, + ret: InstructionResult, + out: Bytes, + ) -> (InstructionResult, Gas, Bytes) { + self.gas_inspector.call_end(data, inputs, gas, ret, out.clone()); + + self.fill_trace_on_call_end(data, ret, &gas, out.clone(), None); + + (ret, gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option
, Gas, Bytes) { + self.gas_inspector.create(data, inputs); + + let _ = data.journaled_state.load_account(inputs.caller, data.db); + let nonce = data.journaled_state.account(inputs.caller).info.nonce; + self.start_trace_on_call( + data, + inputs.created_address(nonce), + inputs.init_code.clone(), + inputs.value, + inputs.scheme.into(), + inputs.caller, + inputs.gas_limit, + Some(false), + ); + + (InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default()) + } + + /// Called when a contract has been created. + /// + /// InstructionResulting anything other than the values passed to this function (`(ret, + /// remaining_gas, address, out)`) will alter the result of the create. + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + status: InstructionResult, + address: Option
, + gas: Gas, + retdata: Bytes, + ) -> (InstructionResult, Option
, Gas, Bytes) { + self.gas_inspector.create_end(data, inputs, status, address, gas, retdata.clone()); + + // get the code of the created contract + let code = address + .and_then(|address| { + data.journaled_state + .account(address) + .info + .code + .as_ref() + .map(|code| code.bytes()[..code.len()].to_vec()) + }) + .unwrap_or_default(); + + self.fill_trace_on_call_end(data, status, &gas, code.into(), address); + + (status, address, gas, retdata) + } + + fn selfdestruct(&mut self, _contract: Address, target: Address, _value: U256) { + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx].trace; + trace.selfdestruct_refund_target = Some(target) + } +} + +#[derive(Debug, Clone, Copy)] +struct StackStep { + trace_idx: usize, + step_idx: usize, +} diff --git a/src/tracing/opcount.rs b/src/tracing/opcount.rs new file mode 100644 index 00000000..2088f216 --- /dev/null +++ b/src/tracing/opcount.rs @@ -0,0 +1,29 @@ +//! Opcount tracing inspector that simply counts all opcodes. +//! +//! See also + +use revm::{interpreter::Interpreter, Database, EVMData, Inspector}; + +/// An inspector that counts all opcodes. +#[derive(Debug, Clone, Copy, Default)] +pub struct OpcodeCountInspector { + /// opcode counter + count: usize, +} + +impl OpcodeCountInspector { + /// Returns the opcode counter + #[inline] + pub fn count(&self) -> usize { + self.count + } +} + +impl Inspector for OpcodeCountInspector +where + DB: Database, +{ + fn step(&mut self, _interp: &mut Interpreter<'_>, _data: &mut EVMData<'_, DB>) { + self.count += 1; + } +} diff --git a/src/tracing/types.rs b/src/tracing/types.rs new file mode 100644 index 00000000..13274e66 --- /dev/null +++ b/src/tracing/types.rs @@ -0,0 +1,684 @@ +//! Types for representing call trace items. + +use crate::tracing::{config::TraceStyle, utils, utils::convert_memory}; +pub use alloy_primitives::Log; +use alloy_primitives::{Address, Bytes, U256, U64}; + +use alloy_rpc_trace_types::{ + geth::{CallFrame, CallLogFrame, GethDefaultTracingOptions, StructLog}, + parity::{ + Action, ActionType, CallAction, CallOutput, CallType, CreateAction, CreateOutput, + SelfdestructAction, TraceOutput, TransactionTrace, + }, +}; +use revm::interpreter::{opcode, CallContext, CallScheme, CreateScheme, InstructionResult, OpCode}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, VecDeque}; + +/// A trace of a call. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CallTrace { + /// The depth of the call + pub depth: usize, + /// Whether the call was successful + pub success: bool, + /// caller of this call + pub caller: Address, + /// The destination address of the call or the address from the created contract. + /// + /// In other words, this is the callee if the [CallKind::Call] or the address of the created + /// contract if [CallKind::Create]. + pub address: Address, + /// Whether this is a call to a precompile + /// + /// Note: This is an Option because not all tracers make use of this + pub maybe_precompile: Option, + /// Holds the target for the selfdestruct refund target if `status` is + /// [InstructionResult::SelfDestruct] + pub selfdestruct_refund_target: Option
, + /// The kind of call this is + pub kind: CallKind, + /// The value transferred in the call + pub value: U256, + /// The calldata for the call, or the init code for contract creations + pub data: Bytes, + /// The return data of the call if this was not a contract creation, otherwise it is the + /// runtime bytecode of the created contract + pub output: Bytes, + /// The gas cost of the call + pub gas_used: u64, + /// The gas limit of the call + pub gas_limit: u64, + /// The status of the trace's call + pub status: InstructionResult, + /// call context of the runtime + pub call_context: Option>, + /// Opcode-level execution steps + pub steps: Vec, +} + +impl CallTrace { + /// Returns true if the status code is an error or revert, See [InstructionResult::Revert] + #[inline] + pub fn is_error(&self) -> bool { + !self.status.is_ok() + } + + /// Returns true if the status code is a revert + #[inline] + pub fn is_revert(&self) -> bool { + self.status == InstructionResult::Revert + } + + /// Returns the error message if it is an erroneous result. + pub(crate) fn as_error_msg(&self, kind: TraceStyle) -> Option { + // See also + self.is_error().then(|| match self.status { + InstructionResult::Revert => { + if kind.is_parity() { "Reverted" } else { "execution reverted" }.to_string() + } + InstructionResult::OutOfGas | InstructionResult::MemoryOOG => { + if kind.is_parity() { "Out of gas" } else { "out of gas" }.to_string() + } + InstructionResult::OpcodeNotFound => { + if kind.is_parity() { "Bad instruction" } else { "invalid opcode" }.to_string() + } + InstructionResult::StackOverflow => "Out of stack".to_string(), + InstructionResult::InvalidJump => { + if kind.is_parity() { "Bad jump destination" } else { "invalid jump destination" } + .to_string() + } + InstructionResult::PrecompileError => { + if kind.is_parity() { "Built-in failed" } else { "precompiled failed" }.to_string() + } + status => format!("{:?}", status), + }) + } +} + +impl Default for CallTrace { + fn default() -> Self { + Self { + depth: Default::default(), + success: Default::default(), + caller: Default::default(), + address: Default::default(), + selfdestruct_refund_target: None, + kind: Default::default(), + value: Default::default(), + data: Default::default(), + maybe_precompile: None, + output: Default::default(), + gas_used: Default::default(), + gas_limit: Default::default(), + status: InstructionResult::Continue, + call_context: Default::default(), + steps: Default::default(), + } + } +} + +/// A node in the arena +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct CallTraceNode { + /// Parent node index in the arena + pub parent: Option, + /// Children node indexes in the arena + pub children: Vec, + /// This node's index in the arena + pub idx: usize, + /// The call trace + pub trace: CallTrace, + /// Recorded logs, if enabled + pub logs: Vec, + /// Ordering of child calls and logs + pub ordering: Vec, +} + +impl CallTraceNode { + /// Returns the call context's execution address + /// + /// See `Inspector::call` impl of [TracingInspector](crate::tracing::TracingInspector) + pub fn execution_address(&self) -> Address { + if self.trace.kind.is_delegate() { + self.trace.caller + } else { + self.trace.address + } + } + + /// Returns all storage slots touched by this trace and the value this storage. + /// + /// A touched slot is either a slot that was written to or read from. + /// + /// If the slot is accessed more than once, the result only includes the first time it was + /// accessed, in other words in only returns the original value of the slot. + pub fn touched_slots(&self) -> BTreeMap { + let mut touched_slots = BTreeMap::new(); + for change in self.trace.steps.iter().filter_map(|s| s.storage_change.as_ref()) { + match touched_slots.entry(change.key) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(change.value); + } + std::collections::btree_map::Entry::Occupied(_) => { + // already touched + } + } + } + + touched_slots + } + + /// Pushes all steps onto the stack in reverse order + /// so that the first step is on top of the stack + pub(crate) fn push_steps_on_stack<'a>( + &'a self, + stack: &mut VecDeque>, + ) { + stack.extend(self.call_step_stack().into_iter().rev()); + } + + /// Returns a list of all steps in this trace in the order they were executed + /// + /// If the step is a call, the id of the child trace is set. + pub(crate) fn call_step_stack(&self) -> Vec> { + let mut stack = Vec::with_capacity(self.trace.steps.len()); + let mut child_id = 0; + for step in self.trace.steps.iter() { + let mut item = CallTraceStepStackItem { trace_node: self, step, call_child_id: None }; + + // If the opcode is a call, put the child trace on the stack + if step.is_calllike_op() { + // The opcode of this step is a call but it's possible that this step resulted + // in a revert or out of gas error in which case there's no actual child call executed and recorded: + if let Some(call_id) = self.children.get(child_id).copied() { + item.call_child_id = Some(call_id); + child_id += 1; + } + } + stack.push(item); + } + stack + } + + /// Returns true if this is a call to a precompile + #[inline] + pub(crate) fn is_precompile(&self) -> bool { + self.trace.maybe_precompile.unwrap_or(false) + } + + /// Returns the kind of call the trace belongs to + #[inline] + pub(crate) const fn kind(&self) -> CallKind { + self.trace.kind + } + + /// Returns the status of the call + #[inline] + pub(crate) const fn status(&self) -> InstructionResult { + self.trace.status + } + + /// Returns true if the call was a selfdestruct + #[inline] + pub(crate) fn is_selfdestruct(&self) -> bool { + self.status() == InstructionResult::SelfDestruct + } + + /// Converts this node into a parity `TransactionTrace` + pub(crate) fn parity_transaction_trace(&self, trace_address: Vec) -> TransactionTrace { + let action = self.parity_action(); + let result = if self.trace.is_error() && !self.trace.is_revert() { + // if the trace is a selfdestruct or an error that is not a revert, the result is None + None + } else { + Some(self.parity_trace_output()) + }; + let error = self.trace.as_error_msg(TraceStyle::Parity); + TransactionTrace { action, error, result, trace_address, subtraces: self.children.len() } + } + + /// Returns the `Output` for a parity trace + pub(crate) fn parity_trace_output(&self) -> TraceOutput { + match self.kind() { + CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => { + TraceOutput::Call(CallOutput { + gas_used: U64::from(self.trace.gas_used), + output: self.trace.output.clone(), + }) + } + CallKind::Create | CallKind::Create2 => TraceOutput::Create(CreateOutput { + gas_used: U64::from(self.trace.gas_used), + code: self.trace.output.clone(), + address: self.trace.address, + }), + } + } + + /// If the trace is a selfdestruct, returns the `Action` for a parity trace. + pub(crate) fn parity_selfdestruct_action(&self) -> Option { + if self.is_selfdestruct() { + Some(Action::Selfdestruct(SelfdestructAction { + address: self.trace.address, + refund_address: self.trace.selfdestruct_refund_target.unwrap_or_default(), + balance: self.trace.value, + })) + } else { + None + } + } + + /// If the trace is a selfdestruct, returns the `CallFrame` for a geth call trace + pub(crate) fn geth_selfdestruct_call_trace(&self) -> Option { + if self.is_selfdestruct() { + Some(CallFrame { + typ: "SELFDESTRUCT".to_string(), + from: self.trace.caller, + to: self.trace.selfdestruct_refund_target, + value: Some(self.trace.value), + ..Default::default() + }) + } else { + None + } + } + + /// If the trace is a selfdestruct, returns the `TransactionTrace` for a parity trace. + pub(crate) fn parity_selfdestruct_trace( + &self, + trace_address: Vec, + ) -> Option { + let trace = self.parity_selfdestruct_action()?; + Some(TransactionTrace { + action: trace, + error: None, + result: None, + trace_address, + subtraces: 0, + }) + } + + /// Returns the `Action` for a parity trace. + /// + /// Caution: This does not include the selfdestruct action, if the trace is a selfdestruct, + /// since those are handled in addition to the call action. + pub(crate) fn parity_action(&self) -> Action { + match self.kind() { + CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => { + Action::Call(CallAction { + from: self.trace.caller, + to: self.trace.address, + value: self.trace.value, + gas: U64::from(self.trace.gas_limit), + input: self.trace.data.clone(), + call_type: self.kind().into(), + }) + } + CallKind::Create | CallKind::Create2 => Action::Create(CreateAction { + from: self.trace.caller, + value: self.trace.value, + gas: U64::from(self.trace.gas_limit), + init: self.trace.data.clone(), + }), + } + } + + /// Converts this call trace into an _empty_ geth [CallFrame] + pub(crate) fn geth_empty_call_frame(&self, include_logs: bool) -> CallFrame { + let mut call_frame = CallFrame { + typ: self.trace.kind.to_string(), + from: self.trace.caller, + to: Some(self.trace.address), + value: Some(self.trace.value), + gas: U256::from(self.trace.gas_limit), + gas_used: U256::from(self.trace.gas_used), + input: self.trace.data.clone(), + output: (!self.trace.output.is_empty()).then(|| self.trace.output.clone()), + error: None, + revert_reason: None, + calls: Default::default(), + logs: Default::default(), + }; + + if self.trace.kind.is_static_call() { + // STATICCALL frames don't have a value + call_frame.value = None; + } + + // we need to populate error and revert reason + if !self.trace.success { + call_frame.revert_reason = utils::maybe_revert_reason(self.trace.output.as_ref()); + + // Note: the call tracer mimics parity's trace transaction and geth maps errors to parity style error messages, + call_frame.error = self.trace.as_error_msg(TraceStyle::Parity); + } + + if include_logs && !self.logs.is_empty() { + call_frame.logs = self + .logs + .iter() + .map(|log| CallLogFrame { + address: Some(self.execution_address()), + topics: Some(log.topics().to_vec()), + data: Some(log.data.clone()), + }) + .collect(); + } + + call_frame + } +} + +/// A unified representation of a call. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum CallKind { + /// Represents a regular call. + #[default] + Call, + /// Represents a static call. + StaticCall, + /// Represents a call code operation. + CallCode, + /// Represents a delegate call. + DelegateCall, + /// Represents a contract creation operation. + Create, + /// Represents a contract creation operation using the CREATE2 opcode. + Create2, +} + +impl CallKind { + /// Returns true if the call is a create + #[inline] + pub fn is_any_create(&self) -> bool { + matches!(self, CallKind::Create | CallKind::Create2) + } + + /// Returns true if the call is a delegate of some sorts + #[inline] + pub fn is_delegate(&self) -> bool { + matches!(self, CallKind::DelegateCall | CallKind::CallCode) + } + + /// Returns true if the call is [CallKind::StaticCall]. + #[inline] + pub fn is_static_call(&self) -> bool { + matches!(self, CallKind::StaticCall) + } +} + +impl std::fmt::Display for CallKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CallKind::Call => { + write!(f, "CALL") + } + CallKind::StaticCall => { + write!(f, "STATICCALL") + } + CallKind::CallCode => { + write!(f, "CALLCODE") + } + CallKind::DelegateCall => { + write!(f, "DELEGATECALL") + } + CallKind::Create => { + write!(f, "CREATE") + } + CallKind::Create2 => { + write!(f, "CREATE2") + } + } + } +} + +impl From for CallKind { + fn from(scheme: CallScheme) -> Self { + match scheme { + CallScheme::Call => CallKind::Call, + CallScheme::StaticCall => CallKind::StaticCall, + CallScheme::CallCode => CallKind::CallCode, + CallScheme::DelegateCall => CallKind::DelegateCall, + } + } +} + +impl From for CallKind { + fn from(create: CreateScheme) -> Self { + match create { + CreateScheme::Create => CallKind::Create, + CreateScheme::Create2 { .. } => CallKind::Create2, + } + } +} + +impl From for ActionType { + fn from(kind: CallKind) -> Self { + match kind { + CallKind::Call | CallKind::StaticCall | CallKind::DelegateCall | CallKind::CallCode => { + ActionType::Call + } + CallKind::Create => ActionType::Create, + CallKind::Create2 => ActionType::Create, + } + } +} + +impl From for CallType { + fn from(ty: CallKind) -> Self { + match ty { + CallKind::Call => CallType::Call, + CallKind::StaticCall => CallType::StaticCall, + CallKind::CallCode => CallType::CallCode, + CallKind::DelegateCall => CallType::DelegateCall, + CallKind::Create => CallType::None, + CallKind::Create2 => CallType::None, + } + } +} + +pub(crate) struct CallTraceStepStackItem<'a> { + /// The trace node that contains this step + pub(crate) trace_node: &'a CallTraceNode, + /// The step that this stack item represents + pub(crate) step: &'a CallTraceStep, + /// The index of the child call in the CallArena if this step's opcode is a call + pub(crate) call_child_id: Option, +} + +/// Ordering enum for calls and logs +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LogCallOrder { + /// Contains the index of the corresponding log + Log(usize), + /// Contains the index of the corresponding trace node + Call(usize), +} + +/// Represents a tracked call step during execution +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CallTraceStep { + // Fields filled in `step` + /// Call depth + pub depth: u64, + /// Program counter before step execution + pub pc: usize, + /// Opcode to be executed + pub op: OpCode, + /// Current contract address + pub contract: Address, + /// Stack before step execution + pub stack: Option>, + /// The new stack items placed by this step if any + pub push_stack: Option>, + /// All allocated memory in a step + /// + /// This will be empty if memory capture is disabled + pub memory: RecordedMemory, + /// Size of memory at the beginning of the step + pub memory_size: usize, + /// Remaining gas before step execution + pub gas_remaining: u64, + /// Gas refund counter before step execution + pub gas_refund_counter: u64, + // Fields filled in `step_end` + /// Gas cost of step execution + pub gas_cost: u64, + /// Change of the contract state after step execution (effect of the SLOAD/SSTORE instructions) + pub storage_change: Option, + /// Final status of the step + /// + /// This is set after the step was executed. + pub status: InstructionResult, +} + +// === impl CallTraceStep === + +impl CallTraceStep { + /// Converts this step into a geth [StructLog] + /// + /// This sets memory and stack capture based on the `opts` parameter. + pub(crate) fn convert_to_geth_struct_log(&self, opts: &GethDefaultTracingOptions) -> StructLog { + let mut log = StructLog { + depth: self.depth, + error: self.as_error(), + gas: self.gas_remaining, + gas_cost: self.gas_cost, + op: self.op.to_string(), + pc: self.pc as u64, + refund_counter: (self.gas_refund_counter > 0).then_some(self.gas_refund_counter), + // Filled, if not disabled manually + stack: None, + // Filled in `CallTraceArena::geth_trace` as a result of compounding all slot changes + return_data: None, + // Filled via trace object + storage: None, + // Only enabled if `opts.enable_memory` is true + memory: None, + // This is None in the rpc response + memory_size: None, + }; + + if opts.is_stack_enabled() { + log.stack = self.stack.clone(); + } + + if opts.is_memory_enabled() { + log.memory = Some(self.memory.memory_chunks()); + } + + log + } + + /// Returns true if the step is a STOP opcode + #[inline] + pub(crate) fn is_stop(&self) -> bool { + matches!(self.op.get(), opcode::STOP) + } + + /// Returns true if the step is a call operation, any of + /// CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2 + #[inline] + pub(crate) fn is_calllike_op(&self) -> bool { + matches!( + self.op.get(), + opcode::CALL | + opcode::DELEGATECALL | + opcode::STATICCALL | + opcode::CREATE | + opcode::CALLCODE | + opcode::CREATE2 + ) + } + + // Returns true if the status code is an error or revert, See [InstructionResult::Revert] + #[inline] + pub(crate) fn is_error(&self) -> bool { + self.status as u8 >= InstructionResult::Revert as u8 + } + + /// Returns the error message if it is an erroneous result. + #[inline] + pub(crate) fn as_error(&self) -> Option { + self.is_error().then(|| format!("{:?}", self.status)) + } +} + +/// Represents the source of a storage change - e.g., whether it came +/// from an SSTORE or SLOAD instruction. +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageChangeReason { + /// SLOAD opcode + SLOAD, + /// SSTORE opcode + SSTORE, +} + +/// Represents a storage change during execution. +/// +/// This maps to evm internals: +/// [JournalEntry::StorageChange](revm::JournalEntry::StorageChange) +/// +/// It is used to track both storage change and warm load of a storage slot. For warm load in regard +/// to EIP-2929 AccessList had_value will be None. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StorageChange { + /// key of the storage slot + pub key: U256, + /// Current value of the storage slot + pub value: U256, + /// The previous value of the storage slot, if any + pub had_value: Option, + /// How this storage was accessed + pub reason: StorageChangeReason, +} + +/// Represents the memory captured during execution +/// +/// This is a wrapper around the [SharedMemory](revm::interpreter::SharedMemory) context memory. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct RecordedMemory(pub(crate) Vec); + +impl RecordedMemory { + #[inline] + pub(crate) fn new(mem: Vec) -> Self { + Self(mem) + } + + /// Returns the memory as a byte slice + #[inline] + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + #[inline] + pub(crate) fn resize(&mut self, size: usize) { + self.0.resize(size, 0); + } + + /// Returns the size of the memory + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether the memory is empty + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Converts the memory into 32byte hex chunks + #[inline] + pub fn memory_chunks(&self) -> Vec { + convert_memory(self.as_bytes()) + } +} + +impl AsRef<[u8]> for RecordedMemory { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} diff --git a/src/tracing/utils.rs b/src/tracing/utils.rs new file mode 100644 index 00000000..d13c8aa1 --- /dev/null +++ b/src/tracing/utils.rs @@ -0,0 +1,91 @@ +//! Util functions for revm related ops + +use alloy_primitives::{hex, Bytes}; +use alloy_sol_types::{ContractError, GenericRevertReason}; +use revm::{ + primitives::{SpecId, KECCAK_EMPTY}, + DatabaseRef, +}; + +/// creates the memory data in 32byte chunks +/// see +#[inline] +pub(crate) fn convert_memory(data: &[u8]) -> Vec { + let mut memory = Vec::with_capacity((data.len() + 31) / 32); + for idx in (0..data.len()).step_by(32) { + let len = std::cmp::min(idx + 32, data.len()); + memory.push(hex::encode(&data[idx..len])); + } + memory +} + +/// Get the gas used, accounting for refunds +#[inline] +pub(crate) fn gas_used(spec: SpecId, spent: u64, refunded: u64) -> u64 { + let refund_quotient = if SpecId::enabled(spec, SpecId::LONDON) { 5 } else { 2 }; + spent - (refunded).min(spent / refund_quotient) +} + +/// Loads the code for the given account from the account itself or the database +/// +/// Returns None if the code hash is the KECCAK_EMPTY hash +#[inline] +pub(crate) fn load_account_code( + db: DB, + db_acc: &revm::primitives::AccountInfo, +) -> Option { + db_acc + .code + .as_ref() + .map(|code| code.original_bytes()) + .or_else(|| { + if db_acc.code_hash == KECCAK_EMPTY { + None + } else { + db.code_by_hash_ref(db_acc.code_hash).ok().map(|code| code.original_bytes()) + } + }) + .map(Into::into) +} + +/// Returns a non empty revert reason if the output is a revert/error. +#[inline] +pub(crate) fn maybe_revert_reason(output: &[u8]) -> Option { + let reason = match GenericRevertReason::decode(output)? { + GenericRevertReason::ContractError(err) => { + match err { + ContractError::Revert(revert) => { + // return the raw revert reason and don't use the revert's display message + revert.reason + } + err => err.to_string(), + } + } + GenericRevertReason::RawString(err) => err, + }; + if reason.is_empty() { + None + } else { + Some(reason) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_sol_types::{GenericContractError, SolInterface}; + + #[test] + fn decode_empty_revert() { + let reason = GenericRevertReason::decode("".as_bytes()).map(|x| x.to_string()); + assert_eq!(reason, Some("".to_string())); + } + + #[test] + fn decode_revert_reason() { + let err = GenericContractError::Revert("my revert".into()); + let encoded = err.abi_encode(); + let reason = maybe_revert_reason(&encoded).unwrap(); + assert_eq!(reason, "my revert"); + } +}