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