mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-22 21:57:58 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0cffd24c1 | |||
| f9a63a5641 | |||
| fb009f65c1 | |||
| dff4c25e24 | |||
| e433d93cbf | |||
| 408754e8fb | |||
| 59bfffe5fe | |||
| 380ea693be | |||
| d02152b565 | |||
| 75159229df | |||
| 2af1a62319 | |||
| e09be4f3fa | |||
| 33b5faca45 | |||
| 172fb4700f | |||
| fefea17c8e | |||
| b71445b632 | |||
| f1a911545e | |||
| 48e7d69158 | |||
| 260ac5d98e | |||
| 94f116f843 | |||
| 0d7a87a728 | |||
| 29bf5304ec | |||
| 491c23efb3 | |||
| 3c86cbb7ef | |||
| fde07b7c0d | |||
| ebc24a588b | |||
| 21e25f09e6 | |||
| 8c412dc924 | |||
| 6da3172581 | |||
| c6eb04b04e | |||
| e5114d31dc | |||
| 74fdeb4a2e | |||
| f9dc362c03 | |||
| c2ba2cfed6 | |||
| 3dda739cef | |||
| 97e3f8bbff | |||
| 7189361a58 | |||
| 9b700bfec2 | |||
| 98b62d705f | |||
| 1a894f791a | |||
| c2526e48e7 | |||
| 7878f68c26 | |||
| 8b1afc36a3 | |||
| 60328cd493 | |||
| eb264fcc7b | |||
| 84b139d3b4 |
+110
-13
@@ -15,6 +15,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
POLKADOT_VERSION: polkadot-stable2506-2
|
||||
|
||||
jobs:
|
||||
cache-polkadot:
|
||||
@@ -50,15 +51,15 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/substrate-node
|
||||
~/.cargo/bin/revive-dev-node
|
||||
~/.cargo/bin/eth-rpc
|
||||
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}
|
||||
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}-with-dev-node
|
||||
|
||||
- name: Build substrate-node
|
||||
- name: Build revive-dev-node
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd polkadot-sdk
|
||||
cargo install --locked --force --profile=production --path substrate/bin/node/cli --bin substrate-node --features cli
|
||||
cargo install --locked --force --profile=production --path substrate/frame/revive/dev-node/node --bin revive-dev-node
|
||||
|
||||
- name: Build eth-rpc
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
@@ -66,6 +67,37 @@ jobs:
|
||||
cd polkadot-sdk
|
||||
cargo install --path substrate/frame/revive/rpc --bin eth-rpc
|
||||
|
||||
- name: Cache downloaded Polkadot binaries
|
||||
id: cache-polkadot
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/polkadot-cache/polkadot
|
||||
~/polkadot-cache/polkadot-execute-worker
|
||||
~/polkadot-cache/polkadot-prepare-worker
|
||||
~/polkadot-cache/polkadot-parachain
|
||||
key: polkadot-downloaded-${{ matrix.os }}-${{ env.POLKADOT_VERSION }}
|
||||
|
||||
- name: Download Polkadot binaries on macOS
|
||||
if: matrix.os == 'macos-14' && steps.cache-polkadot.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p ~/polkadot-cache
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-aarch64-apple-darwin -o ~/polkadot-cache/polkadot
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-execute-worker-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-execute-worker
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-prepare-worker-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-prepare-worker
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-parachain-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-parachain
|
||||
chmod +x ~/polkadot-cache/*
|
||||
|
||||
- name: Download Polkadot binaries on Ubuntu
|
||||
if: matrix.os == 'ubuntu-24.04' && steps.cache-polkadot.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p ~/polkadot-cache
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot -o ~/polkadot-cache/polkadot
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-execute-worker -o ~/polkadot-cache/polkadot-execute-worker
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-prepare-worker -o ~/polkadot-cache/polkadot-prepare-worker
|
||||
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-parachain -o ~/polkadot-cache/polkadot-parachain
|
||||
chmod +x ~/polkadot-cache/*
|
||||
|
||||
ci:
|
||||
name: CI on ${{ matrix.os }}
|
||||
needs: cache-polkadot
|
||||
@@ -77,24 +109,44 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore binaries from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/substrate-node
|
||||
~/.cargo/bin/revive-dev-node
|
||||
~/.cargo/bin/eth-rpc
|
||||
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}
|
||||
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}-with-dev-node
|
||||
|
||||
- name: Restore downloaded Polkadot binaries from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/polkadot-cache/polkadot
|
||||
~/polkadot-cache/polkadot-execute-worker
|
||||
~/polkadot-cache/polkadot-prepare-worker
|
||||
~/polkadot-cache/polkadot-parachain
|
||||
key: polkadot-downloaded-${{ matrix.os }}-${{ env.POLKADOT_VERSION }}
|
||||
|
||||
- name: Install Polkadot binaries
|
||||
run: |
|
||||
sudo cp ~/polkadot-cache/polkadot /usr/local/bin/
|
||||
sudo cp ~/polkadot-cache/polkadot-execute-worker /usr/local/bin/
|
||||
sudo cp ~/polkadot-cache/polkadot-prepare-worker /usr/local/bin/
|
||||
sudo cp ~/polkadot-cache/polkadot-parachain /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/polkadot*
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ""
|
||||
|
||||
- name: Add wasm32 target
|
||||
- name: Add wasm32 target and formatting
|
||||
run: |
|
||||
rustup target add wasm32-unknown-unknown
|
||||
rustup component add rust-src
|
||||
rustup component add rust-src rustfmt clippy
|
||||
|
||||
- name: Install Geth on Ubuntu
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
@@ -141,8 +193,24 @@ jobs:
|
||||
chmod +x resolc
|
||||
sudo mv resolc /usr/local/bin
|
||||
|
||||
- name: Install Kurtosis on macOS
|
||||
if: matrix.os == 'macos-14'
|
||||
run: brew install kurtosis-tech/tap/kurtosis-cli
|
||||
|
||||
- name: Install Kurtosis on Ubuntu
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
run: |
|
||||
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
|
||||
sudo apt update
|
||||
sudo apt install kurtosis-cli
|
||||
|
||||
- name: Install cargo-machete
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
with:
|
||||
command: install
|
||||
args: cargo-machete@0.7.0
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@v0.7.1
|
||||
run: cargo machete crates
|
||||
|
||||
- name: Format
|
||||
run: make format
|
||||
@@ -150,8 +218,8 @@ jobs:
|
||||
- name: Clippy
|
||||
run: make clippy
|
||||
|
||||
- name: Check substrate-node version
|
||||
run: substrate-node --version
|
||||
- name: Check revive-dev-node version
|
||||
run: revive-dev-node --version
|
||||
|
||||
- name: Check eth-rpc version
|
||||
run: eth-rpc --version
|
||||
@@ -159,5 +227,34 @@ jobs:
|
||||
- name: Check resolc version
|
||||
run: resolc --version
|
||||
|
||||
- name: Test cargo workspace
|
||||
run: make test
|
||||
- name: Check polkadot version
|
||||
run: polkadot --version
|
||||
|
||||
- name: Check polkadot-parachain version
|
||||
run: polkadot-parachain --version
|
||||
|
||||
- name: Check polkadot-execute-worker version
|
||||
run: polkadot-execute-worker --version
|
||||
|
||||
- name: Check polkadot-prepare-worker version
|
||||
run: polkadot-prepare-worker --version
|
||||
|
||||
- name: Test Formatting
|
||||
run: make format
|
||||
|
||||
- name: Test Clippy
|
||||
run: make clippy
|
||||
|
||||
- name: Test Machete
|
||||
run: make machete
|
||||
|
||||
- name: Unit Tests
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
run: cargo test --workspace -- --nocapture
|
||||
|
||||
# We can't install docker in the MacOS image used in CI and therefore we need to skip the
|
||||
# Kurtosis and lighthouse related tests when running the CI on MacOS.
|
||||
- name: Unit Tests
|
||||
if: matrix.os == 'macos-14'
|
||||
run: |
|
||||
cargo test --workspace -- --nocapture --skip lighthouse_geth::tests::
|
||||
|
||||
@@ -9,3 +9,11 @@ node_modules
|
||||
*.log
|
||||
|
||||
profile.json.gz
|
||||
workdir
|
||||
|
||||
!/schema.json
|
||||
!/dev-genesis.json
|
||||
|
||||
# Ignore all shell scripts except for the `run_tests.sh` script
|
||||
*.sh
|
||||
!run_tests.sh
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "polkadot-sdk"]
|
||||
path = polkadot-sdk
|
||||
url = https://github.com/paritytech/polkadot-sdk.git
|
||||
[submodule "resolc-compiler-tests"]
|
||||
path = resolc-compiler-tests
|
||||
url = https://github.com/paritytech/resolc-compiler-tests
|
||||
|
||||
Generated
+2642
-118
File diff suppressed because it is too large
Load Diff
+15
-5
@@ -22,12 +22,11 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
|
||||
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
||||
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
||||
|
||||
alloy-primitives = "1.2.1"
|
||||
alloy-sol-types = "1.2.1"
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1.0"
|
||||
bson = { version = "2.15.0" }
|
||||
cacache = { version = "13.1.0" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
dashmap = { version = "6.1.0" }
|
||||
foundry-compilers-artifacts = { version = "0.18.0" }
|
||||
futures = { version = "0.3.31" }
|
||||
@@ -37,6 +36,7 @@ moka = "0.12.10"
|
||||
paste = "1.0.15"
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
once_cell = "1.21"
|
||||
schemars = { version = "1.0.4", features = ["semver1"] }
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0", default-features = false, features = [
|
||||
@@ -44,10 +44,13 @@ serde_json = { version = "1.0", default-features = false, features = [
|
||||
"std",
|
||||
"unbounded_depth",
|
||||
] }
|
||||
serde_with = { version = "3.14.0" }
|
||||
serde_with = { version = "3.14.0", features = ["hex"] }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
sha2 = { version = "0.10.9" }
|
||||
sp-core = "36.1.0"
|
||||
sp-runtime = "41.1.0"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
subxt = { version = "0.44.0" }
|
||||
temp-dir = { version = "0.1.16" }
|
||||
tempfile = "3.3"
|
||||
thiserror = "2"
|
||||
@@ -56,6 +59,7 @@ tokio = { version = "1.47.0", default-features = false, features = [
|
||||
"process",
|
||||
"rt",
|
||||
] }
|
||||
tower = { version = "0.5.2", features = ["limit"] }
|
||||
uuid = { version = "1.8", features = ["v4"] }
|
||||
tracing = { version = "0.1.41" }
|
||||
tracing-appender = { version = "0.2.3" }
|
||||
@@ -65,19 +69,24 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
|
||||
"env-filter",
|
||||
] }
|
||||
indexmap = { version = "2.10.0", default-features = false }
|
||||
itertools = { version = "0.14.0" }
|
||||
|
||||
# revive compiler
|
||||
revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||
|
||||
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
|
||||
|
||||
[workspace.dependencies.alloy]
|
||||
version = "1.0.22"
|
||||
version = "1.0.37"
|
||||
default-features = false
|
||||
features = [
|
||||
"json-abi",
|
||||
"providers",
|
||||
"provider-ws",
|
||||
"provider-ipc",
|
||||
"provider-http",
|
||||
"provider-debug-api",
|
||||
"reqwest",
|
||||
"rpc-types",
|
||||
@@ -87,6 +96,7 @@ features = [
|
||||
"serde",
|
||||
"rpc-types-eth",
|
||||
"genesis",
|
||||
"sol-types",
|
||||
]
|
||||
|
||||
[profile.bench]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
This project compiles and executes declarative smart-contract tests against multiple platforms, then compares behavior (status, return data, events, and state diffs). Today it supports:
|
||||
|
||||
- Geth (EVM reference implementation)
|
||||
- Revive Kitchensink (Substrate-based PolkaVM + `eth-rpc` proxy)
|
||||
- Revive Dev Node (Substrate-based PolkaVM + `eth-rpc` proxy)
|
||||
|
||||
Use it to:
|
||||
|
||||
@@ -39,11 +39,12 @@ This repository contains none of the tests and only contains the testing framewo
|
||||
This section describes the required dependencies that this framework requires to run. Compiling this framework is pretty straightforward and no additional dependencies beyond what's specified in the `Cargo.toml` file should be required.
|
||||
|
||||
- Stable Rust
|
||||
- Geth - When doing differential testing against the PVM we submit transactions to a Geth node and to Kitchensink to compare them.
|
||||
- Kitchensink - When doing differential testing against the PVM we submit transactions to a Geth node and to Kitchensink to compare them.
|
||||
- ETH-RPC - All communication with Kitchensink is done through the ETH RPC.
|
||||
- Geth - When doing differential testing against the PVM we submit transactions to a Geth node and to Revive Dev Node to compare them.
|
||||
- Revive Dev Node - When doing differential testing against the PVM we submit transactions to a Geth node and to Revive Dev Node to compare them.
|
||||
- ETH-RPC - All communication with Revive Dev Node is done through the ETH RPC.
|
||||
- Solc - This is actually a transitive dependency, while this tool doesn't require solc as it downloads the versions that it requires, resolc requires that Solc is installed and available in the path.
|
||||
- Resolc - This is required to compile the contracts to PolkaVM bytecode.
|
||||
- Kurtosis - The Kurtosis CLI tool is required for the production Ethereum mainnet-like node configuration with Geth as the execution layer and lighthouse as the consensus layer. Kurtosis also requires docker to be installed since it runs everything inside of docker containers.
|
||||
|
||||
All of the above need to be installed and available in the path in order for the tool to work.
|
||||
|
||||
@@ -51,160 +52,69 @@ All of the above need to be installed and available in the path in order for the
|
||||
|
||||
This tool is being updated quite frequently. Therefore, it's recommended that you don't install the tool and then run it, but rather that you run it from the root of the directory using `cargo run --release`. The help command of the tool gives you all of the information you need to know about each of the options and flags that the tool offers.
|
||||
|
||||
```bash
|
||||
$ cargo run --release -- --help
|
||||
Usage: retester [OPTIONS]
|
||||
|
||||
Options:
|
||||
-s, --solc <SOLC>
|
||||
The `solc` version to use if the test didn't specify it explicitly
|
||||
|
||||
[default: 0.8.29]
|
||||
|
||||
--wasm
|
||||
Use the Wasm compiler versions
|
||||
|
||||
-r, --resolc <RESOLC>
|
||||
The path to the `resolc` executable to be tested.
|
||||
|
||||
By default it uses the `resolc` binary found in `$PATH`.
|
||||
|
||||
If `--wasm` is set, this should point to the resolc Wasm ile.
|
||||
|
||||
[default: resolc]
|
||||
|
||||
-c, --corpus <CORPUS>
|
||||
A list of test corpus JSON files to be tested
|
||||
|
||||
-w, --workdir <WORKING_DIRECTORY>
|
||||
A place to store temporary artifacts during test execution.
|
||||
|
||||
Creates a temporary dir if not specified.
|
||||
|
||||
-g, --geth <GETH>
|
||||
The path to the `geth` executable.
|
||||
|
||||
By default it uses `geth` binary found in `$PATH`.
|
||||
|
||||
[default: geth]
|
||||
|
||||
--geth-start-timeout <GETH_START_TIMEOUT>
|
||||
The maximum time in milliseconds to wait for geth to start
|
||||
|
||||
[default: 5000]
|
||||
|
||||
--genesis <GENESIS_FILE>
|
||||
Configure nodes according to this genesis.json file
|
||||
|
||||
[default: genesis.json]
|
||||
|
||||
-a, --account <ACCOUNT>
|
||||
The signing account private key
|
||||
|
||||
[default: 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d]
|
||||
|
||||
--private-keys-count <PRIVATE_KEYS_TO_ADD>
|
||||
This argument controls which private keys the nodes should have access to and be added to its wallet signers. With a value of N, private keys (0, N] will be added to the signer set of the node
|
||||
|
||||
[default: 100000]
|
||||
|
||||
-l, --leader <LEADER>
|
||||
The differential testing leader node implementation
|
||||
|
||||
[default: geth]
|
||||
|
||||
Possible values:
|
||||
- geth: The go-ethereum reference full node EVM implementation
|
||||
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
|
||||
|
||||
-f, --follower <FOLLOWER>
|
||||
The differential testing follower node implementation
|
||||
|
||||
[default: kitchensink]
|
||||
|
||||
Possible values:
|
||||
- geth: The go-ethereum reference full node EVM implementation
|
||||
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
|
||||
|
||||
--compile-only <COMPILE_ONLY>
|
||||
Only compile against this testing platform (doesn't execute the tests)
|
||||
|
||||
Possible values:
|
||||
- geth: The go-ethereum reference full node EVM implementation
|
||||
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
|
||||
|
||||
--number-of-nodes <NUMBER_OF_NODES>
|
||||
Determines the amount of nodes that will be spawned for each chain
|
||||
|
||||
[default: 1]
|
||||
|
||||
--number-of-threads <NUMBER_OF_THREADS>
|
||||
Determines the amount of tokio worker threads that will will be used
|
||||
|
||||
[default: 16]
|
||||
|
||||
--number-concurrent-tasks <NUMBER_CONCURRENT_TASKS>
|
||||
Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes
|
||||
|
||||
-e, --extract-problems
|
||||
Extract problems back to the test corpus
|
||||
|
||||
-k, --kitchensink <KITCHENSINK>
|
||||
The path to the `kitchensink` executable.
|
||||
|
||||
By default it uses `substrate-node` binary found in `$PATH`.
|
||||
|
||||
[default: substrate-node]
|
||||
|
||||
-p, --eth_proxy <ETH_PROXY>
|
||||
The path to the `eth_proxy` executable.
|
||||
|
||||
By default it uses `eth-rpc` binary found in `$PATH`.
|
||||
|
||||
[default: eth-rpc]
|
||||
|
||||
-i, --invalidate-compilation-cache
|
||||
Controls if the compilation cache should be invalidated or not
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
```
|
||||
|
||||
To run tests with this tool you need a corpus JSON file that defines the tests included in the corpus. The simplest corpus file looks like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
|
||||
"path": "resolc-compiler-tests/fixtures/solidity"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Note that the tests can be found in the [`resolc-compiler-tests`](https://github.com/paritytech/resolc-compiler-tests) repository.
|
||||
|
||||
The above corpus file instructs the tool to look for all of the test cases contained within all of the metadata files of the specified directory.
|
||||
|
||||
The simplest command to run this tool is the following:
|
||||
|
||||
```bash
|
||||
RUST_LOG="info" cargo run --release -- \
|
||||
--corpus path_to_your_corpus_file.json \
|
||||
--workdir path_to_a_temporary_directory_to_cache_things_in \
|
||||
--number-of-nodes 5 \
|
||||
RUST_LOG="info" cargo run --release -- test \
|
||||
--test ./resolc-compiler-tests/fixtures/solidity \
|
||||
--platform geth-evm-solc \
|
||||
--working-directory workdir \
|
||||
> logs.log \
|
||||
2> output.log
|
||||
```
|
||||
|
||||
The above command will run the tool executing every one of the tests discovered in the path specified in the corpus file. All of the logs from the execution will be persisted in the `logs.log` file and all of the output of the tool will be persisted to the `output.log` file. If all that you're looking for is to run the tool and check which tests succeeded and failed, then the `output.log` file is what you need to be looking at. However, if you're contributing the to the tool then the `logs.log` file will be very valuable.
|
||||
The above command will run the tool executing every one of the tests discovered in the path provided to the tool. All of the logs from the execution will be persisted in the `logs.log` file and all of the output of the tool will be persisted to the `output.log` file. If all that you're looking for is to run the tool and check which tests succeeded and failed, then the `output.log` file is what you need to be looking at. However, if you're contributing the to the tool then the `logs.log` file will be very valuable.
|
||||
|
||||
If you only want to run a subset of tests, then you can specify that in your corpus file. The following is an example:
|
||||
<details>
|
||||
<summary>User Managed Nodes</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
|
||||
"paths": [
|
||||
"path/to/a/single/metadata/file/I/want/to/run.json",
|
||||
"path/to/a/directory/to/find/all/metadata/files/within"
|
||||
]
|
||||
}
|
||||
This section describes how the user can make use of nodes that they manage rather than allowing the tool to spawn and manage the nodes on the user's behalf.
|
||||
|
||||
> ⚠️ This is an advanced feature of the tool and could lead test successes or failures to not be reproducible. Please use this feature with caution and only if you understand the implications of running your own node instead of having the framework manage your nodes. ⚠️
|
||||
|
||||
If you're an advanced user and you'd like to manage your own nodes instead of having the tool initialize, spawn, and manage them, then you can choose to run your own nodes and then provide them to the tool to make use of just like the following:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PLATFORM="revive-dev-node-revm-solc"
|
||||
|
||||
retester export-genesis "$PLATFORM" > chainspec.json
|
||||
|
||||
# Start revive-dev-node in a detached tmux session
|
||||
tmux new-session -d -s revive-dev-node \
|
||||
'RUST_LOG="error,evm=debug,sc_rpc_server=info,runtime::revive=debug" revive-dev-node \
|
||||
--dev \
|
||||
--chain chainspec.json \
|
||||
--force-authoring \
|
||||
--rpc-methods Unsafe \
|
||||
--rpc-cors all \
|
||||
--rpc-max-connections 4294967295 \
|
||||
--pool-limit 4294967295 \
|
||||
--pool-kbytes 4294967295'
|
||||
sleep 5
|
||||
|
||||
# Start eth-rpc in a detached tmux session
|
||||
tmux new-session -d -s eth-rpc \
|
||||
'RUST_LOG="info,eth-rpc=debug" eth-rpc \
|
||||
--dev \
|
||||
--node-rpc-url ws://127.0.0.1:9944 \
|
||||
--rpc-max-connections 4294967295'
|
||||
sleep 5
|
||||
|
||||
# Run the tests (logs to files as before)
|
||||
RUST_LOG="info" retester test \
|
||||
--platform "$PLATFORM" \
|
||||
--corpus ./revive-differential-tests/fixtures/solidity \
|
||||
--working-directory ./workdir \
|
||||
--concurrency.number-of-nodes 1 \
|
||||
--concurrency.number-of-concurrent-tasks 5 \
|
||||
--revive-dev-node.existing-rpc-url "http://localhost:8545" \
|
||||
> logs.log
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Binary file not shown.
@@ -9,11 +9,16 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
moka = { workspace = true, features = ["sync"] }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
tokio = { workspace = true, default-features = false, features = ["time"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -3,19 +3,28 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
/// This method clears the passed directory of all of the files and directories contained within
|
||||
/// without deleting the directory.
|
||||
pub fn clear_directory(path: impl AsRef<Path>) -> Result<()> {
|
||||
for entry in read_dir(path.as_ref())? {
|
||||
let entry = entry?;
|
||||
for entry in read_dir(path.as_ref())
|
||||
.with_context(|| format!("Failed to read directory: {}", path.as_ref().display()))?
|
||||
{
|
||||
let entry = entry.with_context(|| {
|
||||
format!(
|
||||
"Failed to read an entry in directory: {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
if entry_path.is_file() {
|
||||
remove_file(entry_path)?
|
||||
remove_file(&entry_path)
|
||||
.with_context(|| format!("Failed to remove file: {}", entry_path.display()))?
|
||||
} else {
|
||||
remove_dir_all(entry_path)?
|
||||
remove_dir_all(&entry_path)
|
||||
.with_context(|| format!("Failed to remove directory: {}", entry_path.display()))?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::ControlFlow;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
||||
const EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION: Duration = Duration::from_secs(60);
|
||||
|
||||
@@ -38,7 +38,10 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
match future().await? {
|
||||
match future()
|
||||
.await
|
||||
.context("Polled future returned an error during polling loop")?
|
||||
{
|
||||
ControlFlow::Continue(()) => {
|
||||
let next_wait_duration = match polling_wait_behavior {
|
||||
PollingWaitBehavior::Constant(duration) => duration,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/// An iterator that could be either of two iterators.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum EitherIter<A, B> {
|
||||
A(A),
|
||||
B(B),
|
||||
}
|
||||
|
||||
impl<A, B, T> Iterator for EitherIter<A, B>
|
||||
where
|
||||
A: Iterator<Item = T>,
|
||||
B: Iterator<Item = T>,
|
||||
{
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
EitherIter::A(iter) => iter.next(),
|
||||
EitherIter::B(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod either_iter;
|
||||
mod files_with_extension_iterator;
|
||||
|
||||
pub use either_iter::*;
|
||||
pub use files_with_extension_iterator::*;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
||||
|
||||
/// An enum of the platform identifiers of all of the platforms supported by this framework. This
|
||||
/// could be thought of like the target triple from Rust and LLVM where it specifies the platform
|
||||
/// completely starting with the node, the vm, and finally the compiler used for this combination.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum PlatformIdentifier {
|
||||
/// The Go-ethereum reference full node EVM implementation with the solc compiler.
|
||||
GethEvmSolc,
|
||||
/// The Lighthouse Go-ethereum reference full node EVM implementation with the solc compiler.
|
||||
LighthouseGethEvmSolc,
|
||||
/// The revive dev node with the PolkaVM backend with the resolc compiler.
|
||||
ReviveDevNodePolkavmResolc,
|
||||
/// The revive dev node with the REVM backend with the solc compiler.
|
||||
ReviveDevNodeRevmSolc,
|
||||
/// A zombienet based Substrate/Polkadot node with the PolkaVM backend with the resolc compiler.
|
||||
ZombienetPolkavmResolc,
|
||||
/// A zombienet based Substrate/Polkadot node with the REVM backend with the solc compiler.
|
||||
ZombienetRevmSolc,
|
||||
}
|
||||
|
||||
/// An enum of the platform identifiers of all of the platforms supported by this framework.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum CompilerIdentifier {
|
||||
/// The solc compiler.
|
||||
Solc,
|
||||
/// The resolc compiler.
|
||||
Resolc,
|
||||
}
|
||||
|
||||
/// An enum representing the identifiers of the supported nodes.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum NodeIdentifier {
|
||||
/// The go-ethereum node implementation.
|
||||
Geth,
|
||||
/// The go-ethereum node implementation.
|
||||
LighthouseGeth,
|
||||
/// The revive dev node implementation.
|
||||
ReviveDevNode,
|
||||
/// A zombienet spawned nodes
|
||||
Zombienet,
|
||||
}
|
||||
|
||||
/// An enum representing the identifiers of the supported VMs.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum VmIdentifier {
|
||||
/// The ethereum virtual machine.
|
||||
Evm,
|
||||
/// The EraVM virtual machine.
|
||||
EraVM,
|
||||
/// Polkadot's PolaVM Risc-v based virtual machine.
|
||||
PolkaVM,
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
mod identifiers;
|
||||
mod mode;
|
||||
mod parsed_test_specifier;
|
||||
mod private_key_allocator;
|
||||
mod round_robin_pool;
|
||||
mod version_or_requirement;
|
||||
|
||||
pub use identifiers::*;
|
||||
pub use mode::*;
|
||||
pub use parsed_test_specifier::*;
|
||||
pub use private_key_allocator::*;
|
||||
pub use round_robin_pool::*;
|
||||
pub use version_or_requirement::*;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use crate::iterators::EitherIter;
|
||||
use crate::types::VersionOrRequirement;
|
||||
use anyhow::{Context as _, bail};
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// This represents a mode that a given test should be run with, if possible.
|
||||
///
|
||||
@@ -32,16 +38,34 @@ impl Display for Mode {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let parsed_mode = ParsedMode::from_str(s)?;
|
||||
let mut iter = parsed_mode.to_modes();
|
||||
let (Some(mode), None) = (iter.next(), iter.next()) else {
|
||||
bail!("Failed to parse the mode")
|
||||
};
|
||||
Ok(mode)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Return all of the available mode combinations.
|
||||
pub fn all() -> impl Iterator<Item = Mode> {
|
||||
ModePipeline::test_cases().flat_map(|pipeline| {
|
||||
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
|
||||
pipeline,
|
||||
optimize_setting,
|
||||
version: None,
|
||||
})
|
||||
})
|
||||
pub fn all() -> impl Iterator<Item = &'static Mode> {
|
||||
static ALL_MODES: LazyLock<Vec<Mode>> = LazyLock::new(|| {
|
||||
ModePipeline::test_cases()
|
||||
.flat_map(|pipeline| {
|
||||
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
|
||||
pipeline,
|
||||
optimize_setting,
|
||||
version: None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
ALL_MODES.iter()
|
||||
}
|
||||
|
||||
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
|
||||
@@ -165,3 +189,250 @@ impl ModeOptimizerSetting {
|
||||
!matches!(self, ModeOptimizerSetting::M0)
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
///
|
||||
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ParsedMode {
|
||||
pub pipeline: Option<ModePipeline>,
|
||||
pub optimize_flag: Option<bool>,
|
||||
pub optimize_setting: Option<ModeOptimizerSetting>,
|
||||
pub version: Option<semver::VersionReq>,
|
||||
}
|
||||
|
||||
impl FromStr for ParsedMode {
|
||||
type Err = anyhow::Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?x)
|
||||
^
|
||||
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
|
||||
\s*
|
||||
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
|
||||
\s*
|
||||
(?P<version>[>=<^]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
|
||||
$
|
||||
").unwrap()
|
||||
});
|
||||
|
||||
let Some(caps) = REGEX.captures(s) else {
|
||||
anyhow::bail!("Cannot parse mode '{s}' from string");
|
||||
};
|
||||
|
||||
let pipeline = match caps.name("pipeline") {
|
||||
Some(m) => Some(
|
||||
ModePipeline::from_str(m.as_str())
|
||||
.context("Failed to parse mode pipeline from string")?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
|
||||
|
||||
let optimize_setting = match caps.name("optimize_setting") {
|
||||
Some(m) => Some(
|
||||
ModeOptimizerSetting::from_str(m.as_str())
|
||||
.context("Failed to parse optimizer setting from string")?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let version = match caps.name("version") {
|
||||
Some(m) => Some(
|
||||
semver::VersionReq::parse(m.as_str())
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Cannot parse the version requirement '{}': {e}",
|
||||
m.as_str()
|
||||
)
|
||||
})
|
||||
.context("Failed to parse semver requirement from mode string")?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(ParsedMode {
|
||||
pipeline,
|
||||
optimize_flag,
|
||||
optimize_setting,
|
||||
version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParsedMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut has_written = false;
|
||||
|
||||
if let Some(pipeline) = self.pipeline {
|
||||
pipeline.fmt(f)?;
|
||||
if let Some(optimize_flag) = self.optimize_flag {
|
||||
f.write_str(if optimize_flag { "+" } else { "-" })?;
|
||||
}
|
||||
has_written = true;
|
||||
}
|
||||
|
||||
if let Some(optimize_setting) = self.optimize_setting {
|
||||
if has_written {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
optimize_setting.fmt(f)?;
|
||||
has_written = true;
|
||||
}
|
||||
|
||||
if let Some(version) = &self.version {
|
||||
if has_written {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
version.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParsedMode> for String {
|
||||
fn from(parsed_mode: ParsedMode) -> Self {
|
||||
parsed_mode.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ParsedMode {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
ParsedMode::from_str(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedMode {
|
||||
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
|
||||
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
|
||||
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|
||||
|| EitherIter::A(ModePipeline::test_cases()),
|
||||
|p| EitherIter::B(std::iter::once(*p)),
|
||||
);
|
||||
|
||||
let optimize_flag_setting = self.optimize_flag.map(|flag| {
|
||||
if flag {
|
||||
ModeOptimizerSetting::M3
|
||||
} else {
|
||||
ModeOptimizerSetting::M0
|
||||
}
|
||||
});
|
||||
|
||||
let optimize_flag_iter = match optimize_flag_setting {
|
||||
Some(setting) => EitherIter::A(std::iter::once(setting)),
|
||||
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
|
||||
};
|
||||
|
||||
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|
||||
|| EitherIter::A(optimize_flag_iter),
|
||||
|s| EitherIter::B(std::iter::once(*s)),
|
||||
);
|
||||
|
||||
pipeline_iter.flat_map(move |pipeline| {
|
||||
optimize_settings_iter
|
||||
.clone()
|
||||
.map(move |optimize_setting| Mode {
|
||||
pipeline,
|
||||
optimize_setting,
|
||||
version: self.version.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
|
||||
/// This avoids any duplicate entries.
|
||||
pub fn many_to_modes<'a>(
|
||||
parsed: impl Iterator<Item = &'a ParsedMode>,
|
||||
) -> impl Iterator<Item = Mode> {
|
||||
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
|
||||
modes.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parsed_mode_from_str() {
|
||||
let strings = vec![
|
||||
("Mz", "Mz"),
|
||||
("Y", "Y"),
|
||||
("Y+", "Y+"),
|
||||
("Y-", "Y-"),
|
||||
("E", "E"),
|
||||
("E+", "E+"),
|
||||
("E-", "E-"),
|
||||
("Y M0", "Y M0"),
|
||||
("Y M1", "Y M1"),
|
||||
("Y M2", "Y M2"),
|
||||
("Y M3", "Y M3"),
|
||||
("Y Ms", "Y Ms"),
|
||||
("Y Mz", "Y Mz"),
|
||||
("E M0", "E M0"),
|
||||
("E M1", "E M1"),
|
||||
("E M2", "E M2"),
|
||||
("E M3", "E M3"),
|
||||
("E Ms", "E Ms"),
|
||||
("E Mz", "E Mz"),
|
||||
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
|
||||
("Y 0.8.0", "Y ^0.8.0"),
|
||||
("E+ 0.8.0", "E+ ^0.8.0"),
|
||||
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
|
||||
("E Mz <0.7.0", "E Mz <0.7.0"),
|
||||
// We can parse +- _and_ M1/M2 but the latter takes priority.
|
||||
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
|
||||
("E- M2 0.7.0", "E- M2 ^0.7.0"),
|
||||
// We don't see this in the wild but it is parsed.
|
||||
("<=0.8", "<=0.8"),
|
||||
];
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
assert_eq!(
|
||||
expected,
|
||||
parsed.to_string(),
|
||||
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parsed_mode_to_test_modes() {
|
||||
let strings = vec![
|
||||
("Mz", vec!["Y Mz", "E Mz"]),
|
||||
("Y", vec!["Y M0", "Y M3"]),
|
||||
("E", vec!["E M0", "E M3"]),
|
||||
("Y+", vec!["Y M3"]),
|
||||
("Y-", vec!["Y M0"]),
|
||||
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
|
||||
(
|
||||
"<=0.8",
|
||||
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
|
||||
),
|
||||
];
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
|
||||
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
|
||||
|
||||
assert_eq!(
|
||||
expected_set, actual_set,
|
||||
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
|
||||
use crate::types::Mode;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ParsedTestSpecifier {
|
||||
/// All of the test cases in the file should be ran across all of the specified modes
|
||||
FileOrDirectory {
|
||||
/// The path of the metadata file containing the test cases.
|
||||
metadata_or_directory_file_path: PathBuf,
|
||||
},
|
||||
/// Only a specific case within the metadata file should be ran across all of the modes in the
|
||||
/// file.
|
||||
Case {
|
||||
/// The path of the metadata file containing the test cases.
|
||||
metadata_file_path: PathBuf,
|
||||
|
||||
/// The index of the specific case to run.
|
||||
case_idx: usize,
|
||||
},
|
||||
/// A specific case and a specific mode should be ran. This is the most specific out of all of
|
||||
/// the specifier types.
|
||||
CaseWithMode {
|
||||
/// The path of the metadata file containing the test cases.
|
||||
metadata_file_path: PathBuf,
|
||||
|
||||
/// The index of the specific case to run.
|
||||
case_idx: usize,
|
||||
|
||||
/// The parsed mode that the test should be run in.
|
||||
mode: Mode,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for ParsedTestSpecifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ParsedTestSpecifier::FileOrDirectory {
|
||||
metadata_or_directory_file_path,
|
||||
} => {
|
||||
write!(f, "{}", metadata_or_directory_file_path.display())
|
||||
}
|
||||
ParsedTestSpecifier::Case {
|
||||
metadata_file_path,
|
||||
case_idx,
|
||||
} => {
|
||||
write!(f, "{}::{}", metadata_file_path.display(), case_idx)
|
||||
}
|
||||
ParsedTestSpecifier::CaseWithMode {
|
||||
metadata_file_path,
|
||||
case_idx,
|
||||
mode,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"{}::{}::{}",
|
||||
metadata_file_path.display(),
|
||||
case_idx,
|
||||
mode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ParsedTestSpecifier {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut split_iter = s.split("::");
|
||||
|
||||
let Some(path_string) = split_iter.next() else {
|
||||
bail!("Could not find the path in the test specifier")
|
||||
};
|
||||
let path = PathBuf::from(path_string)
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize the path of the test")?;
|
||||
|
||||
let Some(case_idx_string) = split_iter.next() else {
|
||||
return Ok(Self::FileOrDirectory {
|
||||
metadata_or_directory_file_path: path,
|
||||
});
|
||||
};
|
||||
let case_idx = usize::from_str(case_idx_string)
|
||||
.context("Failed to parse the case idx of the test specifier from string")?;
|
||||
|
||||
// At this point the provided path must be a file.
|
||||
if !path.is_file() {
|
||||
bail!(
|
||||
"Test specifier with a path and case idx must point to a file and not a directory"
|
||||
)
|
||||
}
|
||||
|
||||
let Some(mode_string) = split_iter.next() else {
|
||||
return Ok(Self::Case {
|
||||
metadata_file_path: path,
|
||||
case_idx,
|
||||
});
|
||||
};
|
||||
let mode = Mode::from_str(mode_string)
|
||||
.context("Failed to parse the mode string in the parsed test specifier")?;
|
||||
|
||||
Ok(Self::CaseWithMode {
|
||||
metadata_file_path: path,
|
||||
case_idx,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParsedTestSpecifier> for String {
|
||||
fn from(value: ParsedTestSpecifier) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ParsedTestSpecifier {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ParsedTestSpecifier {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use alloy::primitives::U256;
|
||||
use alloy::signers::local::PrivateKeySigner;
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
/// This is a sequential private key allocator. When instantiated, it allocated private keys in
|
||||
/// sequentially and in order until the maximum private key specified is reached.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PrivateKeyAllocator {
|
||||
/// The next private key to be returned by the allocator when requested.
|
||||
next_private_key: U256,
|
||||
|
||||
/// The highest private key (exclusive) that can be returned by this allocator.
|
||||
highest_private_key_inclusive: U256,
|
||||
}
|
||||
|
||||
impl PrivateKeyAllocator {
|
||||
/// Creates a new instance of the private key allocator.
|
||||
pub fn new(highest_private_key_inclusive: U256) -> Self {
|
||||
Self {
|
||||
next_private_key: U256::ONE,
|
||||
highest_private_key_inclusive,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates a new private key and errors out if the maximum private key has been reached.
|
||||
pub fn allocate(&mut self) -> Result<PrivateKeySigner> {
|
||||
if self.next_private_key > self.highest_private_key_inclusive {
|
||||
bail!("Attempted to allocate a private key but failed since all have been allocated");
|
||||
};
|
||||
let private_key =
|
||||
PrivateKeySigner::from_slice(self.next_private_key.to_be_bytes::<32>().as_slice())
|
||||
.context("Failed to convert the private key digits into a private key")?;
|
||||
self.next_private_key += U256::ONE;
|
||||
Ok(private_key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
pub struct RoundRobinPool<T> {
|
||||
next_index: AtomicUsize,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> RoundRobinPool<T> {
|
||||
pub fn new(items: Vec<T>) -> Self {
|
||||
Self {
|
||||
next_index: Default::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn round_robin(&self) -> &T {
|
||||
let current = self.next_index.fetch_add(1, Ordering::SeqCst) % self.items.len();
|
||||
self.items.get(current).unwrap()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.items.iter()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ revive-dt-solc-binaries = { workspace = true }
|
||||
revive-common = { workspace = true }
|
||||
|
||||
alloy = { workspace = true }
|
||||
alloy-primitives = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
foundry-compilers-artifacts = { workspace = true }
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
use semver::Version;
|
||||
|
||||
/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON.
|
||||
pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
|
||||
+26
-54
@@ -3,23 +3,21 @@
|
||||
//! - Polkadot revive resolc compiler
|
||||
//! - Polkadot revive Wasm compiler
|
||||
|
||||
mod constants;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::Hash,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy_primitives::Address;
|
||||
use alloy::primitives::Address;
|
||||
use anyhow::{Context as _, Result};
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_common::cached_fs::read_to_string;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::Arguments;
|
||||
|
||||
// Re-export this as it's a part of the compiler interface.
|
||||
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||
@@ -30,35 +28,28 @@ pub mod solc;
|
||||
|
||||
/// A common interface for all supported Solidity compilers.
|
||||
pub trait SolidityCompiler {
|
||||
/// Extra options specific to the compiler.
|
||||
type Options: Default + PartialEq + Eq + Hash;
|
||||
/// Returns the version of the compiler.
|
||||
fn version(&self) -> &Version;
|
||||
|
||||
/// Returns the path of the compiler executable.
|
||||
fn path(&self) -> &Path;
|
||||
|
||||
/// The low-level compiler interface.
|
||||
fn build(
|
||||
&self,
|
||||
input: CompilerInput,
|
||||
additional_options: Self::Options,
|
||||
) -> impl Future<Output = anyhow::Result<CompilerOutput>>;
|
||||
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>>;
|
||||
|
||||
fn new(solc_executable: PathBuf) -> Self;
|
||||
|
||||
fn get_compiler_executable(
|
||||
config: &Arguments,
|
||||
version: impl Into<VersionOrRequirement>,
|
||||
) -> impl Future<Output = anyhow::Result<PathBuf>>;
|
||||
|
||||
fn version(&self) -> impl Future<Output = anyhow::Result<Version>>;
|
||||
|
||||
/// Does the compiler support the provided mode and version settings?
|
||||
/// Does the compiler support the provided mode and version settings.
|
||||
fn supports_mode(
|
||||
compiler_version: &Version,
|
||||
optimize_setting: ModeOptimizerSetting,
|
||||
&self,
|
||||
optimizer_setting: ModeOptimizerSetting,
|
||||
pipeline: ModePipeline,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
/// The generic compilation input configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct CompilerInput {
|
||||
pub pipeline: Option<ModePipeline>,
|
||||
pub optimization: Option<ModeOptimizerSetting>,
|
||||
@@ -73,27 +64,18 @@ pub struct CompilerInput {
|
||||
/// The generic compilation output configuration.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CompilerOutput {
|
||||
/// The compiled contracts. The bytecode of the contract is kept as a string incase linking is
|
||||
/// The compiled contracts. The bytecode of the contract is kept as a string in case linking is
|
||||
/// required and the compiled source has placeholders.
|
||||
pub contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
}
|
||||
|
||||
/// A generic builder style interface for configuring the supported compiler options.
|
||||
pub struct Compiler<T: SolidityCompiler> {
|
||||
#[derive(Default)]
|
||||
pub struct Compiler {
|
||||
input: CompilerInput,
|
||||
additional_options: T::Options,
|
||||
}
|
||||
|
||||
impl Default for Compiler<solc::Solc> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Compiler<T>
|
||||
where
|
||||
T: SolidityCompiler,
|
||||
{
|
||||
impl Compiler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
input: CompilerInput {
|
||||
@@ -106,7 +88,6 @@ where
|
||||
libraries: Default::default(),
|
||||
revert_string_handling: Default::default(),
|
||||
},
|
||||
additional_options: T::Options::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +116,11 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_source(mut self, path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
self.input
|
||||
.sources
|
||||
.insert(path.as_ref().to_path_buf(), read_to_string(path.as_ref())?);
|
||||
pub fn with_source(mut self, path: impl AsRef<Path>) -> Result<Self> {
|
||||
self.input.sources.insert(
|
||||
path.as_ref().to_path_buf(),
|
||||
read_to_string(path.as_ref()).context("Failed to read the contract source")?,
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@@ -164,11 +146,6 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
|
||||
self.additional_options = options.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
|
||||
callback(self)
|
||||
}
|
||||
@@ -177,17 +154,12 @@ where
|
||||
callback(self)
|
||||
}
|
||||
|
||||
pub async fn try_build(
|
||||
self,
|
||||
compiler_path: impl AsRef<Path>,
|
||||
) -> anyhow::Result<CompilerOutput> {
|
||||
T::new(compiler_path.as_ref().to_path_buf())
|
||||
.build(self.input, self.additional_options)
|
||||
.await
|
||||
pub async fn try_build(self, compiler: &dyn SolidityCompiler) -> Result<CompilerOutput> {
|
||||
compiler.build(self.input).await
|
||||
}
|
||||
|
||||
pub fn input(&self) -> CompilerInput {
|
||||
self.input.clone()
|
||||
pub fn input(&self) -> &CompilerInput {
|
||||
&self.input
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,42 +3,95 @@
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
sync::LazyLock,
|
||||
pin::Pin,
|
||||
process::Stdio,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
use revive_solc_json_interface::{
|
||||
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
||||
SolcStandardJsonOutput,
|
||||
};
|
||||
use tracing::{Span, field::display};
|
||||
|
||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
||||
use crate::{
|
||||
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
|
||||
};
|
||||
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context as _, Result};
|
||||
use semver::Version;
|
||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||
|
||||
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
|
||||
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
|
||||
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
|
||||
|
||||
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
|
||||
#[derive(Debug)]
|
||||
pub struct Resolc {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Resolc(Arc<ResolcInner>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
struct ResolcInner {
|
||||
/// The internal solc compiler that the resolc compiler uses as a compiler frontend.
|
||||
solc: Solc,
|
||||
/// Path to the `resolc` executable
|
||||
resolc_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Resolc {
|
||||
pub async fn new(
|
||||
context: impl AsRef<SolcConfiguration>
|
||||
+ AsRef<ResolcConfiguration>
|
||||
+ AsRef<WorkingDirectoryConfiguration>,
|
||||
version: impl Into<Option<VersionOrRequirement>>,
|
||||
) -> Result<Self> {
|
||||
/// This is a cache of all of the resolc compiler objects. Since we do not currently support
|
||||
/// multiple resolc compiler versions, so our cache is just keyed by the solc compiler and
|
||||
/// its version to the resolc compiler.
|
||||
static COMPILERS_CACHE: LazyLock<DashMap<Solc, Resolc>> = LazyLock::new(Default::default);
|
||||
|
||||
let resolc_configuration = AsRef::<ResolcConfiguration>::as_ref(&context);
|
||||
|
||||
let solc = Solc::new(&context, version)
|
||||
.await
|
||||
.context("Failed to create the solc compiler frontend for resolc")?;
|
||||
|
||||
Ok(COMPILERS_CACHE
|
||||
.entry(solc.clone())
|
||||
.or_insert_with(|| {
|
||||
Self(Arc::new(ResolcInner {
|
||||
solc,
|
||||
resolc_path: resolc_configuration.path.clone(),
|
||||
}))
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl SolidityCompiler for Resolc {
|
||||
type Options = Vec<String>;
|
||||
fn version(&self) -> &Version {
|
||||
// We currently return the solc compiler version since we do not support multiple resolc
|
||||
// compiler versions.
|
||||
SolidityCompiler::version(&self.0.solc)
|
||||
}
|
||||
|
||||
fn path(&self) -> &std::path::Path {
|
||||
&self.0.resolc_path
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", ret)]
|
||||
async fn build(
|
||||
#[tracing::instrument(
|
||||
level = "error",
|
||||
skip_all,
|
||||
fields(
|
||||
resolc_version = %self.version(),
|
||||
solc_version = %self.0.solc.version(),
|
||||
json_in = tracing::field::Empty
|
||||
),
|
||||
err(Debug)
|
||||
)]
|
||||
fn build(
|
||||
&self,
|
||||
CompilerInput {
|
||||
pipeline,
|
||||
@@ -52,247 +105,207 @@ impl SolidityCompiler for Resolc {
|
||||
// resolc. So, we need to go back to this later once it's supported.
|
||||
revert_string_handling: _,
|
||||
}: CompilerInput,
|
||||
additional_options: Self::Options,
|
||||
) -> anyhow::Result<CompilerOutput> {
|
||||
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
|
||||
anyhow::bail!(
|
||||
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let input = SolcStandardJsonInput {
|
||||
language: SolcStandardJsonInputLanguage::Solidity,
|
||||
sources: sources
|
||||
.into_iter()
|
||||
.map(|(path, source)| (path.display().to_string(), source.into()))
|
||||
.collect(),
|
||||
settings: SolcStandardJsonInputSettings {
|
||||
evm_version,
|
||||
libraries: Some(
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(|(source_code, libraries_map)| {
|
||||
(
|
||||
source_code.display().to_string(),
|
||||
libraries_map
|
||||
.into_iter()
|
||||
.map(|(library_ident, library_address)| {
|
||||
(library_ident, library_address.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
remappings: None,
|
||||
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
|
||||
via_ir: Some(true),
|
||||
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
||||
optimization
|
||||
.unwrap_or(ModeOptimizerSetting::M0)
|
||||
.optimizations_enabled(),
|
||||
None,
|
||||
&Version::new(0, 0, 0),
|
||||
false,
|
||||
),
|
||||
metadata: None,
|
||||
polkavm: None,
|
||||
},
|
||||
};
|
||||
|
||||
let mut command = AsyncCommand::new(&self.resolc_path);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.arg("--standard-json");
|
||||
|
||||
if let Some(ref base_path) = base_path {
|
||||
command.arg("--base-path").arg(base_path);
|
||||
}
|
||||
if !allow_paths.is_empty() {
|
||||
command.arg("--allow-paths").arg(
|
||||
allow_paths
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
|
||||
let serialized_input = serde_json::to_vec(&input)?;
|
||||
stdin_pipe.write_all(&serialized_input).await?;
|
||||
|
||||
let output = child.wait_with_output().await?;
|
||||
let stdout = output.stdout;
|
||||
let stderr = output.stderr;
|
||||
|
||||
if !output.status.success() {
|
||||
let json_in = serde_json::to_string_pretty(&input)?;
|
||||
let message = String::from_utf8_lossy(&stderr);
|
||||
tracing::error!(
|
||||
status = %output.status,
|
||||
message = %message,
|
||||
json_input = json_in,
|
||||
"Compilation using resolc failed"
|
||||
);
|
||||
anyhow::bail!("Compilation failed with an error: {message}");
|
||||
}
|
||||
|
||||
let parsed = serde_json::from_slice::<SolcStandardJsonOutput>(&stdout).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse resolc JSON output: {e}\nstderr: {}",
|
||||
String::from_utf8_lossy(&stderr)
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::debug!(
|
||||
output = %serde_json::to_string(&parsed).unwrap(),
|
||||
"Compiled successfully"
|
||||
);
|
||||
|
||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||
// errors instead of returning the compiler output that might contain errors.
|
||||
for error in parsed.errors.iter().flatten() {
|
||||
if error.severity == "error" {
|
||||
tracing::error!(
|
||||
?error,
|
||||
?input,
|
||||
output = %serde_json::to_string(&parsed).unwrap(),
|
||||
"Encountered an error in the compilation"
|
||||
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>> {
|
||||
Box::pin(async move {
|
||||
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
|
||||
anyhow::bail!(
|
||||
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
|
||||
);
|
||||
anyhow::bail!("Encountered an error in the compilation: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
let Some(contracts) = parsed.contracts else {
|
||||
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
|
||||
};
|
||||
let input = SolcStandardJsonInput {
|
||||
language: SolcStandardJsonInputLanguage::Solidity,
|
||||
sources: sources
|
||||
.into_iter()
|
||||
.map(|(path, source)| (path.display().to_string(), source.into()))
|
||||
.collect(),
|
||||
settings: SolcStandardJsonInputSettings {
|
||||
evm_version,
|
||||
libraries: Some(
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(|(source_code, libraries_map)| {
|
||||
(
|
||||
source_code.display().to_string(),
|
||||
libraries_map
|
||||
.into_iter()
|
||||
.map(|(library_ident, library_address)| {
|
||||
(library_ident, library_address.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
remappings: None,
|
||||
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
|
||||
via_ir: Some(true),
|
||||
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
||||
optimization
|
||||
.unwrap_or(ModeOptimizerSetting::M0)
|
||||
.optimizations_enabled(),
|
||||
None,
|
||||
&Version::new(0, 0, 0),
|
||||
false,
|
||||
),
|
||||
metadata: None,
|
||||
polkavm: None,
|
||||
},
|
||||
};
|
||||
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
|
||||
|
||||
let mut compiler_output = CompilerOutput::default();
|
||||
for (source_path, contracts) in contracts.into_iter() {
|
||||
let source_path = PathBuf::from(source_path).canonicalize()?;
|
||||
let path = &self.0.resolc_path;
|
||||
let mut command = AsyncCommand::new(path);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.arg("--solc")
|
||||
.arg(self.0.solc.path())
|
||||
.arg("--standard-json");
|
||||
|
||||
let map = compiler_output.contracts.entry(source_path).or_default();
|
||||
for (contract_name, contract_information) in contracts.into_iter() {
|
||||
let bytecode = contract_information
|
||||
.evm
|
||||
.and_then(|evm| evm.bytecode.clone())
|
||||
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
|
||||
let abi = contract_information
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.as_object())
|
||||
.and_then(|metadata| metadata.get("solc_metadata"))
|
||||
.and_then(|solc_metadata| solc_metadata.as_str())
|
||||
.and_then(|metadata| serde_json::from_str::<serde_json::Value>(metadata).ok())
|
||||
.and_then(|metadata| {
|
||||
metadata.get("output").and_then(|output| {
|
||||
output
|
||||
.get("abi")
|
||||
.and_then(|abi| serde_json::from_value::<JsonAbi>(abi.clone()).ok())
|
||||
})
|
||||
})
|
||||
.context(
|
||||
"Unexpected - Failed to get the ABI for a contract compiled with resolc",
|
||||
)?;
|
||||
map.insert(contract_name, (bytecode.object, abi));
|
||||
if let Some(ref base_path) = base_path {
|
||||
command.arg("--base-path").arg(base_path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(compiler_output)
|
||||
}
|
||||
|
||||
fn new(resolc_path: PathBuf) -> Self {
|
||||
Resolc { resolc_path }
|
||||
}
|
||||
|
||||
async fn get_compiler_executable(
|
||||
config: &Arguments,
|
||||
_version: impl Into<VersionOrRequirement>,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
if !config.resolc.as_os_str().is_empty() {
|
||||
return Ok(config.resolc.clone());
|
||||
}
|
||||
|
||||
Ok(PathBuf::from("resolc"))
|
||||
}
|
||||
|
||||
async fn version(&self) -> anyhow::Result<semver::Version> {
|
||||
/// This is a cache of the path of the compiler to the version number of the compiler. We
|
||||
/// choose to cache the version in this way rather than through a field on the struct since
|
||||
/// compiler objects are being created all the time from the path and the compiler object is
|
||||
/// not reused over time.
|
||||
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
|
||||
|
||||
match VERSION_CACHE.entry(self.resolc_path.clone()) {
|
||||
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
|
||||
dashmap::Entry::Vacant(vacant_entry) => {
|
||||
let output = Command::new(self.resolc_path.as_path())
|
||||
.arg("--version")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?
|
||||
.wait_with_output()?
|
||||
.stdout;
|
||||
|
||||
let output = String::from_utf8_lossy(&output);
|
||||
let version_string = output
|
||||
.split("version ")
|
||||
.nth(1)
|
||||
.context("Version parsing failed")?
|
||||
.split("+")
|
||||
.next()
|
||||
.context("Version parsing failed")?;
|
||||
|
||||
let version = Version::parse(version_string)?;
|
||||
|
||||
vacant_entry.insert(version.clone());
|
||||
|
||||
Ok(version)
|
||||
if !allow_paths.is_empty() {
|
||||
command.arg("--allow-paths").arg(
|
||||
allow_paths
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to spawn resolc at {}", path.display()))?;
|
||||
|
||||
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
|
||||
let serialized_input = serde_json::to_vec(&input)
|
||||
.context("Failed to serialize Standard JSON input for resolc")?;
|
||||
stdin_pipe
|
||||
.write_all(&serialized_input)
|
||||
.await
|
||||
.context("Failed to write Standard JSON to resolc stdin")?;
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.context("Failed while waiting for resolc process to finish")?;
|
||||
let stdout = output.stdout;
|
||||
let stderr = output.stderr;
|
||||
|
||||
if !output.status.success() {
|
||||
let json_in = serde_json::to_string_pretty(&input)
|
||||
.context("Failed to pretty-print Standard JSON input for logging")?;
|
||||
let message = String::from_utf8_lossy(&stderr);
|
||||
tracing::error!(
|
||||
status = %output.status,
|
||||
message = %message,
|
||||
json_input = json_in,
|
||||
"Compilation using resolc failed"
|
||||
);
|
||||
anyhow::bail!("Compilation failed with an error: {message}");
|
||||
}
|
||||
|
||||
let parsed = serde_json::from_slice::<SolcStandardJsonOutput>(&stdout)
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse resolc JSON output: {e}\nstderr: {}",
|
||||
String::from_utf8_lossy(&stderr)
|
||||
)
|
||||
})
|
||||
.context("Failed to parse resolc standard JSON output")?;
|
||||
|
||||
tracing::debug!(
|
||||
output = %serde_json::to_string(&parsed).unwrap(),
|
||||
"Compiled successfully"
|
||||
);
|
||||
|
||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||
// errors instead of returning the compiler output that might contain errors.
|
||||
for error in parsed.errors.iter().flatten() {
|
||||
if error.severity == "error" {
|
||||
tracing::error!(
|
||||
?error,
|
||||
?input,
|
||||
output = %serde_json::to_string(&parsed).unwrap(),
|
||||
"Encountered an error in the compilation"
|
||||
);
|
||||
anyhow::bail!("Encountered an error in the compilation: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
let Some(contracts) = parsed.contracts else {
|
||||
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
|
||||
};
|
||||
|
||||
let mut compiler_output = CompilerOutput::default();
|
||||
for (source_path, contracts) in contracts.into_iter() {
|
||||
let src_for_msg = source_path.clone();
|
||||
let source_path = PathBuf::from(source_path)
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize path {src_for_msg}"))?;
|
||||
|
||||
let map = compiler_output.contracts.entry(source_path).or_default();
|
||||
for (contract_name, contract_information) in contracts.into_iter() {
|
||||
let bytecode = contract_information
|
||||
.evm
|
||||
.and_then(|evm| evm.bytecode.clone())
|
||||
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
|
||||
let abi = {
|
||||
let metadata = contract_information
|
||||
.metadata
|
||||
.as_ref()
|
||||
.context("No metadata found for the contract")?;
|
||||
let solc_metadata_str = match metadata {
|
||||
serde_json::Value::String(solc_metadata_str) => {
|
||||
solc_metadata_str.as_str()
|
||||
}
|
||||
serde_json::Value::Object(metadata_object) => {
|
||||
let solc_metadata_value = metadata_object
|
||||
.get("solc_metadata")
|
||||
.context("Contract doesn't have a 'solc_metadata' field")?;
|
||||
solc_metadata_value
|
||||
.as_str()
|
||||
.context("The 'solc_metadata' field is not a string")?
|
||||
}
|
||||
serde_json::Value::Null
|
||||
| serde_json::Value::Bool(_)
|
||||
| serde_json::Value::Number(_)
|
||||
| serde_json::Value::Array(_) => {
|
||||
anyhow::bail!("Unsupported type of metadata {metadata:?}")
|
||||
}
|
||||
};
|
||||
let solc_metadata = serde_json::from_str::<serde_json::Value>(
|
||||
solc_metadata_str,
|
||||
)
|
||||
.context(
|
||||
"Failed to deserialize the solc_metadata as a serde_json generic value",
|
||||
)?;
|
||||
let output_value = solc_metadata
|
||||
.get("output")
|
||||
.context("solc_metadata doesn't have an output field")?;
|
||||
let abi_value = output_value
|
||||
.get("abi")
|
||||
.context("solc_metadata output doesn't contain an abi field")?;
|
||||
serde_json::from_value::<JsonAbi>(abi_value.clone())
|
||||
.context("ABI found in solc_metadata output is not valid ABI")?
|
||||
};
|
||||
map.insert(contract_name, (bytecode.object, abi));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(compiler_output)
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_mode(
|
||||
_compiler_version: &Version,
|
||||
_optimize_setting: ModeOptimizerSetting,
|
||||
&self,
|
||||
optimize_setting: ModeOptimizerSetting,
|
||||
pipeline: ModePipeline,
|
||||
) -> bool {
|
||||
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
|
||||
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
|
||||
// needs this to translate to LLVM IR and then RISCV.
|
||||
|
||||
// Note: the original implementation of this function looked like the following:
|
||||
// ```
|
||||
// pipeline == ModePipeline::ViaYulIR && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
|
||||
// ```
|
||||
// However, that implementation is sadly incorrect since the version that's passed into this
|
||||
// function is not the version of solc but the version of resolc. This is despite the fact
|
||||
// that resolc depends on Solc for the initial Yul codegen. Therefore, we have skipped the
|
||||
// version check until we do a better integrations between resolc and solc.
|
||||
pipeline == ModePipeline::ViaYulIR
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn compiler_version_can_be_obtained() {
|
||||
// Arrange
|
||||
let args = Arguments::default();
|
||||
let path = Resolc::get_compiler_executable(&args, Version::new(0, 7, 6))
|
||||
.await
|
||||
.unwrap();
|
||||
let compiler = Resolc::new(path);
|
||||
|
||||
// Act
|
||||
let version = compiler.version().await;
|
||||
|
||||
// Assert
|
||||
let _ = version.expect("Failed to get version");
|
||||
&& SolidityCompiler::supports_mode(&self.0.solc, optimize_setting, pipeline)
|
||||
}
|
||||
}
|
||||
|
||||
+239
-248
@@ -3,19 +3,20 @@
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
sync::LazyLock,
|
||||
pin::Pin,
|
||||
process::Stdio,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_config::{SolcConfiguration, WorkingDirectoryConfiguration};
|
||||
use revive_dt_solc_binaries::download_solc;
|
||||
use tracing::{Span, field::display, info};
|
||||
|
||||
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
|
||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context as _, Result};
|
||||
use foundry_compilers_artifacts::{
|
||||
output_selection::{
|
||||
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
|
||||
@@ -26,16 +27,78 @@ use foundry_compilers_artifacts::{
|
||||
use semver::Version;
|
||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Solc {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Solc(Arc<SolcInner>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
struct SolcInner {
|
||||
/// The path of the solidity compiler executable that this object uses.
|
||||
solc_path: PathBuf,
|
||||
/// The version of the solidity compiler executable that this object uses.
|
||||
solc_version: Version,
|
||||
}
|
||||
|
||||
impl Solc {
|
||||
pub async fn new(
|
||||
context: impl AsRef<SolcConfiguration> + AsRef<WorkingDirectoryConfiguration>,
|
||||
version: impl Into<Option<VersionOrRequirement>>,
|
||||
) -> Result<Self> {
|
||||
// This is a cache for the compiler objects so that whenever the same compiler version is
|
||||
// requested the same object is returned. We do this as we do not want to keep cloning the
|
||||
// compiler around.
|
||||
static COMPILERS_CACHE: LazyLock<DashMap<(PathBuf, Version), Solc>> =
|
||||
LazyLock::new(Default::default);
|
||||
|
||||
let working_directory_configuration =
|
||||
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let solc_configuration = AsRef::<SolcConfiguration>::as_ref(&context);
|
||||
|
||||
// We attempt to download the solc binary. Note the following: this call does the version
|
||||
// resolution for us. Therefore, even if the download didn't proceed, this function will
|
||||
// resolve the version requirement into a canonical version of the compiler. It's then up
|
||||
// to us to either use the provided path or not.
|
||||
let version = version
|
||||
.into()
|
||||
.unwrap_or_else(|| solc_configuration.version.clone().into());
|
||||
let (version, path) =
|
||||
download_solc(working_directory_configuration.as_path(), version, false)
|
||||
.await
|
||||
.context("Failed to download/get path to solc binary")?;
|
||||
|
||||
Ok(COMPILERS_CACHE
|
||||
.entry((path.clone(), version.clone()))
|
||||
.or_insert_with(|| {
|
||||
info!(
|
||||
solc_path = %path.display(),
|
||||
solc_version = %version,
|
||||
"Created a new solc compiler object"
|
||||
);
|
||||
Self(Arc::new(SolcInner {
|
||||
solc_path: path,
|
||||
solc_version: version,
|
||||
}))
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl SolidityCompiler for Solc {
|
||||
type Options = ();
|
||||
fn version(&self) -> &Version {
|
||||
&self.0.solc_version
|
||||
}
|
||||
|
||||
fn path(&self) -> &std::path::Path {
|
||||
&self.0.solc_path
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", ret)]
|
||||
async fn build(
|
||||
#[tracing::instrument(
|
||||
level = "error",
|
||||
skip_all,
|
||||
fields(json_in = tracing::field::Empty),
|
||||
err(Debug)
|
||||
)]
|
||||
fn build(
|
||||
&self,
|
||||
CompilerInput {
|
||||
pipeline,
|
||||
@@ -47,262 +110,190 @@ impl SolidityCompiler for Solc {
|
||||
libraries,
|
||||
revert_string_handling,
|
||||
}: CompilerInput,
|
||||
_: Self::Options,
|
||||
) -> anyhow::Result<CompilerOutput> {
|
||||
let compiler_supports_via_ir = self.version().await? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
|
||||
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>> {
|
||||
Box::pin(async move {
|
||||
// Be careful to entirely omit the viaIR field if the compiler does not support it,
|
||||
// as it will error if you provide fields it does not know about. Because
|
||||
// `supports_mode` is called prior to instantiating a compiler, we should never
|
||||
// ask for something which is invalid.
|
||||
let via_ir = match (pipeline, self.compiler_supports_yul()) {
|
||||
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
|
||||
(_pipeline, false) => None,
|
||||
};
|
||||
|
||||
// Be careful to entirely omit the viaIR field if the compiler does not support it,
|
||||
// as it will error if you provide fields it does not know about. Because
|
||||
// `supports_mode` is called prior to instantiating a compiler, we should never
|
||||
// ask for something which is invalid.
|
||||
let via_ir = match (pipeline, compiler_supports_via_ir) {
|
||||
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
|
||||
(_pipeline, false) => None,
|
||||
};
|
||||
|
||||
let input = SolcInput {
|
||||
language: SolcLanguage::Solidity,
|
||||
sources: Sources(
|
||||
sources
|
||||
.into_iter()
|
||||
.map(|(source_path, source_code)| (source_path, Source::new(source_code)))
|
||||
.collect(),
|
||||
),
|
||||
settings: Settings {
|
||||
optimizer: Optimizer {
|
||||
enabled: optimization.map(|o| o.optimizations_enabled()),
|
||||
details: Some(Default::default()),
|
||||
let input = SolcInput {
|
||||
language: SolcLanguage::Solidity,
|
||||
sources: Sources(
|
||||
sources
|
||||
.into_iter()
|
||||
.map(|(source_path, source_code)| (source_path, Source::new(source_code)))
|
||||
.collect(),
|
||||
),
|
||||
settings: Settings {
|
||||
optimizer: Optimizer {
|
||||
enabled: optimization.map(|o| o.optimizations_enabled()),
|
||||
details: Some(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
output_selection: OutputSelection::common_output_selection(
|
||||
[
|
||||
ContractOutputSelection::Abi,
|
||||
ContractOutputSelection::Evm(EvmOutputSelection::ByteCode(
|
||||
BytecodeOutputSelection::Object,
|
||||
)),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|item| item.to_string()),
|
||||
),
|
||||
evm_version: evm_version.map(|version| version.to_string().parse().unwrap()),
|
||||
via_ir,
|
||||
libraries: Libraries {
|
||||
libs: libraries
|
||||
.into_iter()
|
||||
.map(|(file_path, libraries)| {
|
||||
(
|
||||
file_path,
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(|(library_name, library_address)| {
|
||||
(library_name, library_address.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
debug: revert_string_handling.map(|revert_string_handling| DebuggingSettings {
|
||||
revert_strings: match revert_string_handling {
|
||||
crate::RevertString::Default => Some(RevertStrings::Default),
|
||||
crate::RevertString::Debug => Some(RevertStrings::Debug),
|
||||
crate::RevertString::Strip => Some(RevertStrings::Strip),
|
||||
crate::RevertString::VerboseDebug => Some(RevertStrings::VerboseDebug),
|
||||
},
|
||||
debug_info: Default::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
output_selection: OutputSelection::common_output_selection(
|
||||
[
|
||||
ContractOutputSelection::Abi,
|
||||
ContractOutputSelection::Evm(EvmOutputSelection::ByteCode(
|
||||
BytecodeOutputSelection::Object,
|
||||
)),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|item| item.to_string()),
|
||||
),
|
||||
evm_version: evm_version.map(|version| version.to_string().parse().unwrap()),
|
||||
via_ir,
|
||||
libraries: Libraries {
|
||||
libs: libraries
|
||||
.into_iter()
|
||||
.map(|(file_path, libraries)| {
|
||||
(
|
||||
file_path,
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(|(library_name, library_address)| {
|
||||
(library_name, library_address.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
|
||||
|
||||
let path = &self.0.solc_path;
|
||||
let mut command = AsyncCommand::new(path);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.arg("--standard-json");
|
||||
|
||||
if let Some(ref base_path) = base_path {
|
||||
command.arg("--base-path").arg(base_path);
|
||||
}
|
||||
if !allow_paths.is_empty() {
|
||||
command.arg("--allow-paths").arg(
|
||||
allow_paths
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to spawn solc at {}", path.display()))?;
|
||||
|
||||
let stdin = child.stdin.as_mut().expect("should be piped");
|
||||
let serialized_input = serde_json::to_vec(&input)
|
||||
.context("Failed to serialize Standard JSON input for solc")?;
|
||||
stdin
|
||||
.write_all(&serialized_input)
|
||||
.await
|
||||
.context("Failed to write Standard JSON to solc stdin")?;
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.context("Failed while waiting for solc process to finish")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let json_in = serde_json::to_string_pretty(&input)
|
||||
.context("Failed to pretty-print Standard JSON input for logging")?;
|
||||
tracing::error!(
|
||||
status = %output.status,
|
||||
json_input = json_in,
|
||||
"Compilation using solc failed"
|
||||
);
|
||||
anyhow::bail!("Compilation failed");
|
||||
}
|
||||
|
||||
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout)
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse resolc JSON output: {e}\nstdout: {}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
})
|
||||
.context("Failed to parse solc standard JSON output")?;
|
||||
|
||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||
// errors instead of returning the compiler output that might contain errors.
|
||||
for error in parsed.errors.iter() {
|
||||
if error.severity == Severity::Error {
|
||||
tracing::error!(?error, ?input, "Encountered an error in the compilation");
|
||||
anyhow::bail!("Encountered an error in the compilation: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
output = %String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
"Compiled successfully"
|
||||
);
|
||||
|
||||
let mut compiler_output = CompilerOutput::default();
|
||||
for (contract_path, contracts) in parsed.contracts {
|
||||
let map = compiler_output
|
||||
.contracts
|
||||
.entry(contract_path.canonicalize().with_context(|| {
|
||||
format!(
|
||||
"Failed to canonicalize contract path {}",
|
||||
contract_path.display()
|
||||
)
|
||||
})?)
|
||||
.or_default();
|
||||
for (contract_name, contract_info) in contracts.into_iter() {
|
||||
let source_code = contract_info
|
||||
.evm
|
||||
.and_then(|evm| evm.bytecode)
|
||||
.map(|bytecode| match bytecode.object {
|
||||
BytecodeObject::Bytecode(bytecode) => bytecode.to_string(),
|
||||
BytecodeObject::Unlinked(unlinked) => unlinked,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
debug: revert_string_handling.map(|revert_string_handling| DebuggingSettings {
|
||||
revert_strings: match revert_string_handling {
|
||||
crate::RevertString::Default => Some(RevertStrings::Default),
|
||||
crate::RevertString::Debug => Some(RevertStrings::Debug),
|
||||
crate::RevertString::Strip => Some(RevertStrings::Strip),
|
||||
crate::RevertString::VerboseDebug => Some(RevertStrings::VerboseDebug),
|
||||
},
|
||||
debug_info: Default::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let mut command = AsyncCommand::new(&self.solc_path);
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.arg("--standard-json");
|
||||
|
||||
if let Some(ref base_path) = base_path {
|
||||
command.arg("--base-path").arg(base_path);
|
||||
}
|
||||
if !allow_paths.is_empty() {
|
||||
command.arg("--allow-paths").arg(
|
||||
allow_paths
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let stdin = child.stdin.as_mut().expect("should be piped");
|
||||
let serialized_input = serde_json::to_vec(&input)?;
|
||||
stdin.write_all(&serialized_input).await?;
|
||||
let output = child.wait_with_output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let json_in = serde_json::to_string_pretty(&input)?;
|
||||
let message = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!(
|
||||
status = %output.status,
|
||||
message = %message,
|
||||
json_input = json_in,
|
||||
"Compilation using solc failed"
|
||||
);
|
||||
anyhow::bail!("Compilation failed with an error: {message}");
|
||||
}
|
||||
|
||||
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse resolc JSON output: {e}\nstderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
})?;
|
||||
|
||||
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||
// errors instead of returning the compiler output that might contain errors.
|
||||
for error in parsed.errors.iter() {
|
||||
if error.severity == Severity::Error {
|
||||
tracing::error!(?error, ?input, "Encountered an error in the compilation");
|
||||
anyhow::bail!("Encountered an error in the compilation: {error}")
|
||||
.context("Unexpected - contract compiled with solc has no source code")?;
|
||||
let abi = contract_info
|
||||
.abi
|
||||
.context("Unexpected - contract compiled with solc as no ABI")?;
|
||||
map.insert(contract_name, (source_code, abi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
output = %String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
"Compiled successfully"
|
||||
);
|
||||
|
||||
let mut compiler_output = CompilerOutput::default();
|
||||
for (contract_path, contracts) in parsed.contracts {
|
||||
let map = compiler_output
|
||||
.contracts
|
||||
.entry(contract_path.canonicalize()?)
|
||||
.or_default();
|
||||
for (contract_name, contract_info) in contracts.into_iter() {
|
||||
let source_code = contract_info
|
||||
.evm
|
||||
.and_then(|evm| evm.bytecode)
|
||||
.map(|bytecode| match bytecode.object {
|
||||
BytecodeObject::Bytecode(bytecode) => bytecode.to_string(),
|
||||
BytecodeObject::Unlinked(unlinked) => unlinked,
|
||||
})
|
||||
.context("Unexpected - contract compiled with solc has no source code")?;
|
||||
let abi = contract_info
|
||||
.abi
|
||||
.context("Unexpected - contract compiled with solc as no ABI")?;
|
||||
map.insert(contract_name, (source_code, abi));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(compiler_output)
|
||||
}
|
||||
|
||||
fn new(solc_path: PathBuf) -> Self {
|
||||
Self { solc_path }
|
||||
}
|
||||
|
||||
async fn get_compiler_executable(
|
||||
config: &Arguments,
|
||||
version: impl Into<VersionOrRequirement>,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
let path = download_solc(config.directory(), version, config.wasm).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn version(&self) -> anyhow::Result<semver::Version> {
|
||||
/// This is a cache of the path of the compiler to the version number of the compiler. We
|
||||
/// choose to cache the version in this way rather than through a field on the struct since
|
||||
/// compiler objects are being created all the time from the path and the compiler object is
|
||||
/// not reused over time.
|
||||
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
|
||||
|
||||
match VERSION_CACHE.entry(self.solc_path.clone()) {
|
||||
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
|
||||
dashmap::Entry::Vacant(vacant_entry) => {
|
||||
// The following is the parsing code for the version from the solc version strings
|
||||
// which look like the following:
|
||||
// ```
|
||||
// solc, the solidity compiler commandline interface
|
||||
// Version: 0.8.30+commit.73712a01.Darwin.appleclang
|
||||
// ```
|
||||
let child = Command::new(self.solc_path.as_path())
|
||||
.arg("--version")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
let output = child.wait_with_output()?;
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
let version_line = output
|
||||
.split("Version: ")
|
||||
.nth(1)
|
||||
.context("Version parsing failed")?;
|
||||
let version_string = version_line
|
||||
.split("+")
|
||||
.next()
|
||||
.context("Version parsing failed")?;
|
||||
|
||||
let version = Version::parse(version_string)?;
|
||||
|
||||
vacant_entry.insert(version.clone());
|
||||
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
Ok(compiler_output)
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_mode(
|
||||
compiler_version: &Version,
|
||||
&self,
|
||||
_optimize_setting: ModeOptimizerSetting,
|
||||
pipeline: ModePipeline,
|
||||
) -> bool {
|
||||
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
|
||||
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
|
||||
pipeline == ModePipeline::ViaEVMAssembly
|
||||
|| (pipeline == ModePipeline::ViaYulIR
|
||||
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
|
||||
|| (pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn compiler_version_can_be_obtained() {
|
||||
// Arrange
|
||||
let args = Arguments::default();
|
||||
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6))
|
||||
.await
|
||||
.unwrap();
|
||||
let compiler = Solc::new(path);
|
||||
|
||||
// Act
|
||||
let version = compiler.version().await;
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
version.expect("Failed to get version"),
|
||||
Version::new(0, 7, 6)
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compiler_version_can_be_obtained1() {
|
||||
// Arrange
|
||||
let args = Arguments::default();
|
||||
let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21))
|
||||
.await
|
||||
.unwrap();
|
||||
let compiler = Solc::new(path);
|
||||
|
||||
// Act
|
||||
let version = compiler.version().await;
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
version.expect("Failed to get version"),
|
||||
Version::new(0, 4, 21)
|
||||
)
|
||||
impl Solc {
|
||||
fn compiler_supports_yul(&self) -> bool {
|
||||
const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
|
||||
SolidityCompiler::version(self) >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ pragma solidity >=0.6.9;
|
||||
import "./callable.sol";
|
||||
|
||||
contract Main {
|
||||
function main(uint[1] calldata p1, Callable callable) public returns(uint) {
|
||||
function main(
|
||||
uint[1] calldata p1,
|
||||
Callable callable
|
||||
) public pure returns (uint) {
|
||||
return callable.f(p1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use revive_dt_compiler::{Compiler, SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use revive_dt_compiler::{Compiler, revive_resolc::Resolc, solc::Solc};
|
||||
use revive_dt_config::TestExecutionContext;
|
||||
use semver::Version;
|
||||
|
||||
#[tokio::test]
|
||||
async fn contracts_can_be_compiled_with_solc() {
|
||||
// Arrange
|
||||
let args = Arguments::default();
|
||||
let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30))
|
||||
let args = TestExecutionContext::default();
|
||||
let solc = Solc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
let output = Compiler::<Solc>::new()
|
||||
let output = Compiler::new()
|
||||
.with_source("./tests/assets/array_one_element/callable.sol")
|
||||
.unwrap()
|
||||
.with_source("./tests/assets/array_one_element/main.sol")
|
||||
.unwrap()
|
||||
.try_build(compiler_path)
|
||||
.try_build(&solc)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
@@ -48,18 +49,18 @@ async fn contracts_can_be_compiled_with_solc() {
|
||||
#[tokio::test]
|
||||
async fn contracts_can_be_compiled_with_resolc() {
|
||||
// Arrange
|
||||
let args = Arguments::default();
|
||||
let compiler_path = Resolc::get_compiler_executable(&args, Version::new(0, 8, 30))
|
||||
let args = TestExecutionContext::default();
|
||||
let resolc = Resolc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
let output = Compiler::<Resolc>::new()
|
||||
let output = Compiler::new()
|
||||
.with_source("./tests/assets/array_one_element/callable.sol")
|
||||
.unwrap()
|
||||
.with_source("./tests/assets/array_one_element/main.sol")
|
||||
.unwrap()
|
||||
.try_build(compiler_path)
|
||||
.try_build(&resolc)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -9,11 +9,17 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
revive-dt-common = { workspace = true }
|
||||
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
temp-dir = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+1095
-139
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ revive-dt-node = { workspace = true }
|
||||
revive-dt-node-interaction = { workspace = true }
|
||||
revive-dt-report = { workspace = true }
|
||||
|
||||
ansi_term = { workspace = true }
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
bson = { workspace = true }
|
||||
@@ -28,16 +29,14 @@ cacache = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
temp-dir = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
//! A wrapper around the compiler which allows for caching of compilation artifacts so that they can
|
||||
//! be reused between runs.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::FutureExt;
|
||||
use revive_dt_common::iterators::FilesWithExtensionIterator;
|
||||
use revive_dt_compiler::{Compiler, CompilerInput, CompilerOutput, Mode, SolidityCompiler};
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
|
||||
|
||||
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
|
||||
use anyhow::{Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{Instrument, debug, debug_span, instrument};
|
||||
|
||||
use crate::Platform;
|
||||
|
||||
pub struct CachedCompiler(ArtifactsCache);
|
||||
|
||||
impl CachedCompiler {
|
||||
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
|
||||
let mut cache = ArtifactsCache::new(path);
|
||||
if invalidate_cache {
|
||||
cache = cache.with_invalidated_cache().await?;
|
||||
}
|
||||
Ok(Self(cache))
|
||||
}
|
||||
|
||||
/// Compiles or gets the compilation artifacts from the cache.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
metadata_file_path = %metadata_file_path.as_ref().display(),
|
||||
%mode,
|
||||
platform = P::config_id().to_string()
|
||||
),
|
||||
err
|
||||
)]
|
||||
pub async fn compile_contracts<P: Platform>(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
metadata_file_path: impl AsRef<Path>,
|
||||
mode: &Mode,
|
||||
config: &Arguments,
|
||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||
compilation_success_report_callback: impl Fn(
|
||||
Version,
|
||||
PathBuf,
|
||||
bool,
|
||||
Option<CompilerInput>,
|
||||
CompilerOutput,
|
||||
) + Clone,
|
||||
compilation_failure_report_callback: impl Fn(
|
||||
Option<Version>,
|
||||
Option<PathBuf>,
|
||||
Option<CompilerInput>,
|
||||
String,
|
||||
),
|
||||
) -> Result<(CompilerOutput, Version)> {
|
||||
static CACHE_KEY_LOCK: Lazy<RwLock<HashMap<CacheKey, Arc<Mutex<()>>>>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
let compiler_version_or_requirement = mode.compiler_version_to_use(config.solc.clone());
|
||||
let compiler_path = <P::Compiler as SolidityCompiler>::get_compiler_executable(
|
||||
config,
|
||||
compiler_version_or_requirement,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
compilation_failure_report_callback(None, None, None, err.to_string())
|
||||
})?;
|
||||
let compiler_version = <P::Compiler as SolidityCompiler>::new(compiler_path.clone())
|
||||
.version()
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
compilation_failure_report_callback(
|
||||
None,
|
||||
Some(compiler_path.clone()),
|
||||
None,
|
||||
err.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cache_key = CacheKey {
|
||||
platform_key: P::config_id().to_string(),
|
||||
compiler_version: compiler_version.clone(),
|
||||
metadata_file_path: metadata_file_path.as_ref().to_path_buf(),
|
||||
solc_mode: mode.clone(),
|
||||
};
|
||||
|
||||
let compilation_callback = || {
|
||||
let compiler_path = compiler_path.clone();
|
||||
let compiler_version = compiler_version.clone();
|
||||
let compilation_success_report_callback = compilation_success_report_callback.clone();
|
||||
async move {
|
||||
compile_contracts::<P>(
|
||||
metadata.directory()?,
|
||||
compiler_path,
|
||||
compiler_version,
|
||||
metadata.files_to_compile()?,
|
||||
mode,
|
||||
deployed_libraries,
|
||||
compilation_success_report_callback,
|
||||
compilation_failure_report_callback,
|
||||
)
|
||||
.map(|compilation_result| compilation_result.map(CacheValue::new))
|
||||
.await
|
||||
}
|
||||
.instrument(debug_span!(
|
||||
"Running compilation for the cache key",
|
||||
cache_key.platform_key = %cache_key.platform_key,
|
||||
cache_key.compiler_version = %cache_key.compiler_version,
|
||||
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
|
||||
cache_key.solc_mode = %cache_key.solc_mode,
|
||||
))
|
||||
};
|
||||
|
||||
let compiled_contracts = match deployed_libraries {
|
||||
// If deployed libraries have been specified then we will re-compile the contract as it
|
||||
// means that linking is required in this case.
|
||||
Some(_) => {
|
||||
debug!("Deployed libraries defined, recompilation must take place");
|
||||
debug!("Cache miss");
|
||||
compilation_callback().await?.compiler_output
|
||||
}
|
||||
// If no deployed libraries are specified then we can follow the cached flow and attempt
|
||||
// to lookup the compilation artifacts in the cache.
|
||||
None => {
|
||||
debug!("Deployed libraries undefined, attempting to make use of cache");
|
||||
|
||||
// Lock this specific cache key such that we do not get inconsistent state. We want
|
||||
// that when multiple cases come in asking for the compilation artifacts then they
|
||||
// don't all trigger a compilation if there's a cache miss. Hence, the lock here.
|
||||
let read_guard = CACHE_KEY_LOCK.read().await;
|
||||
let mutex = match read_guard.get(&cache_key).cloned() {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
drop(read_guard);
|
||||
CACHE_KEY_LOCK
|
||||
.write()
|
||||
.await
|
||||
.entry(cache_key.clone())
|
||||
.or_default()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
match self.0.get(&cache_key).await {
|
||||
Some(cache_value) => {
|
||||
compilation_success_report_callback(
|
||||
compiler_version.clone(),
|
||||
compiler_path,
|
||||
true,
|
||||
None,
|
||||
cache_value.compiler_output.clone(),
|
||||
);
|
||||
cache_value.compiler_output
|
||||
}
|
||||
None => compilation_callback().await?.compiler_output,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((compiled_contracts, compiler_version))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn compile_contracts<P: Platform>(
|
||||
metadata_directory: impl AsRef<Path>,
|
||||
compiler_path: impl AsRef<Path>,
|
||||
compiler_version: Version,
|
||||
mut files_to_compile: impl Iterator<Item = PathBuf>,
|
||||
mode: &Mode,
|
||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||
compilation_success_report_callback: impl Fn(
|
||||
Version,
|
||||
PathBuf,
|
||||
bool,
|
||||
Option<CompilerInput>,
|
||||
CompilerOutput,
|
||||
),
|
||||
compilation_failure_report_callback: impl Fn(
|
||||
Option<Version>,
|
||||
Option<PathBuf>,
|
||||
Option<CompilerInput>,
|
||||
String,
|
||||
),
|
||||
) -> Result<CompilerOutput> {
|
||||
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
|
||||
.with_allowed_extension("sol")
|
||||
.with_use_cached_fs(true)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let compiler = Compiler::<P::Compiler>::new()
|
||||
.with_allow_path(metadata_directory)
|
||||
// Handling the modes
|
||||
.with_optimization(mode.optimize_setting)
|
||||
.with_pipeline(mode.pipeline)
|
||||
// Adding the contract sources to the compiler.
|
||||
.try_then(|compiler| {
|
||||
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
|
||||
})
|
||||
.inspect_err(|err| {
|
||||
compilation_failure_report_callback(
|
||||
Some(compiler_version.clone()),
|
||||
Some(compiler_path.as_ref().to_path_buf()),
|
||||
None,
|
||||
err.to_string(),
|
||||
)
|
||||
})?
|
||||
// Adding the deployed libraries to the compiler.
|
||||
.then(|compiler| {
|
||||
deployed_libraries
|
||||
.iter()
|
||||
.flat_map(|value| value.iter())
|
||||
.map(|(instance, (ident, address, abi))| (instance, ident, address, abi))
|
||||
.flat_map(|(_, ident, address, _)| {
|
||||
all_sources_in_dir
|
||||
.iter()
|
||||
.map(move |path| (ident, address, path))
|
||||
})
|
||||
.fold(compiler, |compiler, (ident, address, path)| {
|
||||
compiler.with_library(path, ident.as_str(), *address)
|
||||
})
|
||||
});
|
||||
|
||||
let compiler_input = compiler.input();
|
||||
let compiler_output = compiler
|
||||
.try_build(compiler_path.as_ref())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
compilation_failure_report_callback(
|
||||
Some(compiler_version.clone()),
|
||||
Some(compiler_path.as_ref().to_path_buf()),
|
||||
Some(compiler_input.clone()),
|
||||
err.to_string(),
|
||||
)
|
||||
})?;
|
||||
compilation_success_report_callback(
|
||||
compiler_version,
|
||||
compiler_path.as_ref().to_path_buf(),
|
||||
false,
|
||||
Some(compiler_input),
|
||||
compiler_output.clone(),
|
||||
);
|
||||
Ok(compiler_output)
|
||||
}
|
||||
|
||||
struct ArtifactsCache {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl ArtifactsCache {
|
||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn with_invalidated_cache(self) -> Result<Self> {
|
||||
cacache::clear(self.path.as_path())
|
||||
.await
|
||||
.map_err(Into::<Error>::into)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn insert(&self, key: &CacheKey, value: &CacheValue) -> Result<()> {
|
||||
let key = bson::to_vec(key)?;
|
||||
let value = bson::to_vec(value)?;
|
||||
cacache::write(self.path.as_path(), key.encode_hex(), value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(&self, key: &CacheKey) -> Option<CacheValue> {
|
||||
let key = bson::to_vec(key).ok()?;
|
||||
let value = cacache::read(self.path.as_path(), key.encode_hex())
|
||||
.await
|
||||
.ok()?;
|
||||
let value = bson::from_slice::<CacheValue>(&value).ok()?;
|
||||
Some(value)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn get_or_insert_with(
|
||||
&self,
|
||||
key: &CacheKey,
|
||||
callback: impl AsyncFnOnce() -> Result<CacheValue>,
|
||||
) -> Result<CacheValue> {
|
||||
match self.get(key).await {
|
||||
Some(value) => {
|
||||
debug!("Cache hit");
|
||||
Ok(value)
|
||||
}
|
||||
None => {
|
||||
debug!("Cache miss");
|
||||
let value = callback().await?;
|
||||
self.insert(key, &value).await?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
struct CacheKey {
|
||||
/// The platform name that this artifact was compiled for. For example, this could be EVM or
|
||||
/// PVM.
|
||||
platform_key: String,
|
||||
|
||||
/// The version of the compiler that was used to compile the artifacts.
|
||||
compiler_version: Version,
|
||||
|
||||
/// The path of the metadata file that the compilation artifacts are for.
|
||||
metadata_file_path: PathBuf,
|
||||
|
||||
/// The mode that the compilation artifacts where compiled with.
|
||||
solc_mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct CacheValue {
|
||||
/// The compiler output from the compilation run.
|
||||
compiler_output: CompilerOutput,
|
||||
}
|
||||
|
||||
impl CacheValue {
|
||||
pub fn new(compiler_output: CompilerOutput) -> Self {
|
||||
Self { compiler_output }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::ControlFlow,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
hex,
|
||||
json_abi::JsonAbi,
|
||||
network::{Ethereum, TransactionBuilder},
|
||||
primitives::{Address, TxHash, U256},
|
||||
rpc::types::{
|
||||
TransactionReceipt, TransactionRequest,
|
||||
trace::geth::{
|
||||
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
|
||||
GethDebugTracingOptions,
|
||||
},
|
||||
},
|
||||
};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use futures::TryFutureExt;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_common::{
|
||||
futures::{PollingWaitBehavior, poll},
|
||||
types::PrivateKeyAllocator,
|
||||
};
|
||||
use revive_dt_format::{
|
||||
metadata::{ContractInstance, ContractPathAndIdent},
|
||||
steps::{
|
||||
AllocateAccountStep, Calldata, EtherValue, FunctionCallStep, Method, RepeatStep, Step,
|
||||
StepIdx, StepPath,
|
||||
},
|
||||
traits::{ResolutionContext, ResolverApi},
|
||||
};
|
||||
use tokio::sync::{Mutex, OnceCell, mpsc::UnboundedSender};
|
||||
use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
differential_benchmarks::{ExecutionState, WatcherEvent},
|
||||
helpers::{CachedCompiler, TestDefinition, TestPlatformInformation},
|
||||
};
|
||||
|
||||
static DRIVER_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// The differential tests driver for a single platform.
|
||||
pub struct Driver<'a, I> {
|
||||
/// The id of the driver.
|
||||
driver_id: usize,
|
||||
|
||||
/// The information of the platform that this driver is for.
|
||||
platform_information: &'a TestPlatformInformation<'a>,
|
||||
|
||||
/// The resolver of the platform.
|
||||
resolver: Arc<dyn ResolverApi + 'a>,
|
||||
|
||||
/// The definition of the test that the driver is instructed to execute.
|
||||
test_definition: &'a TestDefinition<'a>,
|
||||
|
||||
/// The private key allocator used by this driver and other drivers when account allocations are
|
||||
/// needed.
|
||||
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
||||
|
||||
/// The execution state associated with the platform.
|
||||
execution_state: ExecutionState,
|
||||
|
||||
/// The send side of the watcher's unbounded channel associated with this driver.
|
||||
watcher_tx: UnboundedSender<WatcherEvent>,
|
||||
|
||||
/// The number of steps that were executed on the driver.
|
||||
steps_executed: usize,
|
||||
|
||||
/// This is the queue of steps that are to be executed by the driver for this test case. Each
|
||||
/// time `execute_step` is called one of the steps is executed.
|
||||
steps_iterator: I,
|
||||
}
|
||||
|
||||
impl<'a, I> Driver<'a, I>
|
||||
where
|
||||
I: Iterator<Item = (StepPath, Step)>,
|
||||
{
|
||||
// region:Constructors & Initialization
|
||||
pub async fn new(
|
||||
platform_information: &'a TestPlatformInformation<'a>,
|
||||
test_definition: &'a TestDefinition<'a>,
|
||||
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
||||
cached_compiler: &CachedCompiler<'a>,
|
||||
watcher_tx: UnboundedSender<WatcherEvent>,
|
||||
steps: I,
|
||||
) -> Result<Self> {
|
||||
let mut this = Driver {
|
||||
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
|
||||
platform_information,
|
||||
resolver: platform_information
|
||||
.node
|
||||
.resolver()
|
||||
.await
|
||||
.context("Failed to create resolver")?,
|
||||
test_definition,
|
||||
private_key_allocator,
|
||||
execution_state: ExecutionState::empty(),
|
||||
steps_executed: 0,
|
||||
steps_iterator: steps,
|
||||
watcher_tx,
|
||||
};
|
||||
this.init_execution_state(cached_compiler)
|
||||
.await
|
||||
.context("Failed to initialize the execution state of the platform")?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn init_execution_state(&mut self, cached_compiler: &CachedCompiler<'a>) -> Result<()> {
|
||||
let compiler_output = cached_compiler
|
||||
.compile_contracts(
|
||||
self.test_definition.metadata,
|
||||
self.test_definition.metadata_file_path,
|
||||
self.test_definition.mode.clone(),
|
||||
None,
|
||||
self.platform_information.compiler.as_ref(),
|
||||
self.platform_information.platform,
|
||||
&self.platform_information.reporter,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "Pre-linking compilation failed"))
|
||||
.context("Failed to produce the pre-linking compiled contracts")?;
|
||||
|
||||
let mut deployed_libraries = None::<HashMap<_, _>>;
|
||||
let mut contract_sources = self
|
||||
.test_definition
|
||||
.metadata
|
||||
.contract_sources()
|
||||
.inspect_err(|err| error!(?err, "Failed to retrieve contract sources from metadata"))
|
||||
.context("Failed to get the contract instances from the metadata file")?;
|
||||
for library_instance in self
|
||||
.test_definition
|
||||
.metadata
|
||||
.libraries
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|(_, map)| map.values())
|
||||
{
|
||||
debug!(%library_instance, "Deploying Library Instance");
|
||||
|
||||
let ContractPathAndIdent {
|
||||
contract_source_path: library_source_path,
|
||||
contract_ident: library_ident,
|
||||
} = contract_sources
|
||||
.remove(library_instance)
|
||||
.context("Failed to get the contract sources of the contract instance")?;
|
||||
|
||||
let (code, abi) = compiler_output
|
||||
.contracts
|
||||
.get(&library_source_path)
|
||||
.and_then(|contracts| contracts.get(library_ident.as_str()))
|
||||
.context("Failed to get the code and abi for the instance")?;
|
||||
|
||||
let code = alloy::hex::decode(code)?;
|
||||
|
||||
// Getting the deployer address from the cases themselves. This is to ensure
|
||||
// that we're doing the deployments from different accounts and therefore we're
|
||||
// not slowed down by the nonce.
|
||||
let deployer_address = self
|
||||
.test_definition
|
||||
.case
|
||||
.steps
|
||||
.iter()
|
||||
.filter_map(|step| match step {
|
||||
Step::FunctionCall(input) => input.caller.as_address().copied(),
|
||||
Step::BalanceAssertion(..) => None,
|
||||
Step::StorageEmptyAssertion(..) => None,
|
||||
Step::Repeat(..) => None,
|
||||
Step::AllocateAccount(..) => None,
|
||||
})
|
||||
.next()
|
||||
.unwrap_or(FunctionCallStep::default_caller_address());
|
||||
let tx = TransactionBuilder::<Ethereum>::with_deploy_code(
|
||||
TransactionRequest::default().from(deployer_address),
|
||||
code,
|
||||
);
|
||||
let receipt = self
|
||||
.execute_transaction(tx, None)
|
||||
.and_then(|(_, receipt_fut)| receipt_fut)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
%library_instance,
|
||||
"Failed to deploy the library"
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!(?library_instance, "Deployed library");
|
||||
|
||||
let library_address = receipt
|
||||
.contract_address
|
||||
.expect("Failed to deploy the library");
|
||||
|
||||
deployed_libraries.get_or_insert_default().insert(
|
||||
library_instance.clone(),
|
||||
(library_ident.clone(), library_address, abi.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
let compiler_output = cached_compiler
|
||||
.compile_contracts(
|
||||
self.test_definition.metadata,
|
||||
self.test_definition.metadata_file_path,
|
||||
self.test_definition.mode.clone(),
|
||||
deployed_libraries.as_ref(),
|
||||
self.platform_information.compiler.as_ref(),
|
||||
self.platform_information.platform,
|
||||
&self.platform_information.reporter,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "Post-linking compilation failed"))
|
||||
.context("Failed to compile the post-link contracts")?;
|
||||
|
||||
for (contract_path, contract_name_to_info_mapping) in compiler_output.contracts.iter() {
|
||||
for (contract_name, (contract_bytecode, _)) in contract_name_to_info_mapping.iter() {
|
||||
let contract_bytecode = hex::decode(contract_bytecode)
|
||||
.expect("Impossible for us to get an undecodable bytecode after linking");
|
||||
|
||||
self.platform_information
|
||||
.reporter
|
||||
.report_contract_information_event(
|
||||
contract_path.to_path_buf(),
|
||||
contract_name.clone(),
|
||||
contract_bytecode.len(),
|
||||
)
|
||||
.expect("Should not fail");
|
||||
}
|
||||
}
|
||||
|
||||
self.execution_state = ExecutionState::new(
|
||||
compiler_output.contracts,
|
||||
deployed_libraries.unwrap_or_default(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// endregion:Constructors & Initialization
|
||||
|
||||
// region:Step Handling
|
||||
pub async fn execute_all(mut self) -> Result<usize> {
|
||||
while let Some(result) = self.execute_next_step().await {
|
||||
result?
|
||||
}
|
||||
Ok(self.steps_executed)
|
||||
}
|
||||
|
||||
pub async fn execute_next_step(&mut self) -> Option<Result<()>> {
|
||||
let (step_path, step) = self.steps_iterator.next()?;
|
||||
info!(%step_path, "Executing Step");
|
||||
Some(
|
||||
self.execute_step(&step_path, &step)
|
||||
.await
|
||||
.inspect(|_| info!(%step_path, "Step execution succeeded"))
|
||||
.inspect_err(|err| error!(%step_path, ?err, "Step execution failed")),
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
%step_path,
|
||||
),
|
||||
err(Debug),
|
||||
)]
|
||||
async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> {
|
||||
let steps_executed = match step {
|
||||
Step::FunctionCall(step) => self
|
||||
.execute_function_call(step_path, step.as_ref())
|
||||
.await
|
||||
.context("Function call step Failed"),
|
||||
Step::Repeat(step) => self
|
||||
.execute_repeat_step(step_path, step.as_ref())
|
||||
.await
|
||||
.context("Repetition Step Failed"),
|
||||
Step::AllocateAccount(step) => self
|
||||
.execute_account_allocation(step_path, step.as_ref())
|
||||
.await
|
||||
.context("Account Allocation Step Failed"),
|
||||
// The following steps are disabled in the benchmarking driver.
|
||||
Step::BalanceAssertion(..) | Step::StorageEmptyAssertion(..) => Ok(0),
|
||||
}?;
|
||||
self.steps_executed += steps_executed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
|
||||
pub async fn execute_function_call(
|
||||
&mut self,
|
||||
step_path: &StepPath,
|
||||
step: &FunctionCallStep,
|
||||
) -> Result<usize> {
|
||||
let deployment_receipts = self
|
||||
.handle_function_call_contract_deployment(step_path, step)
|
||||
.await
|
||||
.context("Failed to deploy contracts for the function call step")?;
|
||||
let transaction_hash = self
|
||||
.handle_function_call_execution(step_path, step, deployment_receipts)
|
||||
.await
|
||||
.context("Failed to handle the function call execution")?;
|
||||
self.handle_function_call_variable_assignment(step, transaction_hash)
|
||||
.await
|
||||
.context("Failed to handle function call variable assignment")?;
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
async fn handle_function_call_contract_deployment(
|
||||
&mut self,
|
||||
step_path: &StepPath,
|
||||
step: &FunctionCallStep,
|
||||
) -> Result<HashMap<ContractInstance, TransactionReceipt>> {
|
||||
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
|
||||
for instance in step.find_all_contract_instances().into_iter() {
|
||||
if !self
|
||||
.execution_state
|
||||
.deployed_contracts
|
||||
.contains_key(&instance)
|
||||
{
|
||||
instances_we_must_deploy.entry(instance).or_insert(false);
|
||||
}
|
||||
}
|
||||
if let Method::Deployer = step.method {
|
||||
instances_we_must_deploy.swap_remove(&step.instance);
|
||||
instances_we_must_deploy.insert(step.instance.clone(), true);
|
||||
}
|
||||
|
||||
let mut receipts = HashMap::new();
|
||||
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
|
||||
let calldata = deploy_with_constructor_arguments.then_some(&step.calldata);
|
||||
let value = deploy_with_constructor_arguments
|
||||
.then_some(step.value)
|
||||
.flatten();
|
||||
|
||||
let caller = {
|
||||
let context = self.default_resolution_context();
|
||||
step.caller
|
||||
.resolve_address(self.resolver.as_ref(), context)
|
||||
.await?
|
||||
};
|
||||
if let (_, _, Some(receipt)) = self
|
||||
.get_or_deploy_contract_instance(
|
||||
&instance,
|
||||
caller,
|
||||
calldata,
|
||||
value,
|
||||
Some(step_path),
|
||||
)
|
||||
.await
|
||||
.context("Failed to get or deploy contract instance during input execution")?
|
||||
{
|
||||
receipts.insert(instance.clone(), receipt);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(receipts)
|
||||
}
|
||||
|
||||
async fn handle_function_call_execution(
|
||||
&mut self,
|
||||
step_path: &StepPath,
|
||||
step: &FunctionCallStep,
|
||||
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
|
||||
) -> Result<TxHash> {
|
||||
match step.method {
|
||||
// This step was already executed when `handle_step` was called. We just need to
|
||||
// lookup the transaction receipt in this case and continue on.
|
||||
Method::Deployer => deployment_receipts
|
||||
.remove(&step.instance)
|
||||
.context("Failed to find deployment receipt for constructor call")
|
||||
.map(|receipt| receipt.transaction_hash),
|
||||
Method::Fallback | Method::FunctionName(_) => {
|
||||
let tx = step
|
||||
.as_transaction(self.resolver.as_ref(), self.default_resolution_context())
|
||||
.await?;
|
||||
Ok(self.execute_transaction(tx, Some(step_path)).await?.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_function_call_call_frame_tracing(
|
||||
&mut self,
|
||||
tx_hash: TxHash,
|
||||
) -> Result<CallFrame> {
|
||||
self.platform_information
|
||||
.node
|
||||
.trace_transaction(
|
||||
tx_hash,
|
||||
GethDebugTracingOptions {
|
||||
tracer: Some(GethDebugTracerType::BuiltInTracer(
|
||||
GethDebugBuiltInTracerType::CallTracer,
|
||||
)),
|
||||
tracer_config: GethDebugTracerConfig(serde_json::json! {{
|
||||
"onlyTopCall": true,
|
||||
"withLog": false,
|
||||
"withStorage": false,
|
||||
"withMemory": false,
|
||||
"withStack": false,
|
||||
"withReturnData": true
|
||||
}}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|trace| {
|
||||
trace
|
||||
.try_into_call_frame()
|
||||
.expect("Impossible - we requested a callframe trace so we must get it back")
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_function_call_variable_assignment(
|
||||
&mut self,
|
||||
step: &FunctionCallStep,
|
||||
tx_hash: TxHash,
|
||||
) -> Result<()> {
|
||||
let Some(ref assignments) = step.variable_assignments else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Handling the return data variable assignments.
|
||||
let callframe = OnceCell::new();
|
||||
for (variable_name, output_word) in assignments.return_data.iter().zip(
|
||||
callframe
|
||||
.get_or_try_init(|| self.handle_function_call_call_frame_tracing(tx_hash))
|
||||
.await
|
||||
.context("Failed to get the callframe trace for transaction")?
|
||||
.output
|
||||
.as_ref()
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
.chunks(32),
|
||||
) {
|
||||
let value = U256::from_be_slice(output_word);
|
||||
self.execution_state
|
||||
.variables
|
||||
.insert(variable_name.clone(), value);
|
||||
tracing::info!(
|
||||
variable_name,
|
||||
variable_value = hex::encode(value.to_be_bytes::<32>()),
|
||||
"Assigned variable"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
|
||||
async fn execute_repeat_step(
|
||||
&mut self,
|
||||
step_path: &StepPath,
|
||||
step: &RepeatStep,
|
||||
) -> Result<usize> {
|
||||
let tasks = (0..step.repeat)
|
||||
.map(|_| Driver {
|
||||
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
|
||||
platform_information: self.platform_information,
|
||||
resolver: self.resolver.clone(),
|
||||
test_definition: self.test_definition,
|
||||
private_key_allocator: self.private_key_allocator.clone(),
|
||||
execution_state: self.execution_state.clone(),
|
||||
steps_executed: 0,
|
||||
steps_iterator: {
|
||||
let steps = step
|
||||
.steps
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(step_idx, step)| {
|
||||
let step_idx = StepIdx::new(step_idx);
|
||||
let step_path = step_path.append(step_idx);
|
||||
(step_path, step)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
steps.into_iter()
|
||||
},
|
||||
watcher_tx: self.watcher_tx.clone(),
|
||||
})
|
||||
.map(|driver| driver.execute_all());
|
||||
|
||||
// TODO: Determine how we want to know the `ignore_block_before` and if it's through the
|
||||
// receipt and how this would impact the architecture and the possibility of us not waiting
|
||||
// for receipts in the future.
|
||||
self.watcher_tx
|
||||
.send(WatcherEvent::RepetitionStartEvent {
|
||||
ignore_block_before: 0,
|
||||
})
|
||||
.context("Failed to send message on the watcher's tx")?;
|
||||
|
||||
let res = futures::future::try_join_all(tasks)
|
||||
.await
|
||||
.context("Repetition execution failed")?;
|
||||
Ok(res.into_iter().sum())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all, err(Debug))]
|
||||
pub async fn execute_account_allocation(
|
||||
&mut self,
|
||||
_: &StepPath,
|
||||
step: &AllocateAccountStep,
|
||||
) -> Result<usize> {
|
||||
let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else {
|
||||
bail!("Account allocation must start with $VARIABLE:");
|
||||
};
|
||||
|
||||
let private_key = self
|
||||
.private_key_allocator
|
||||
.lock()
|
||||
.await
|
||||
.allocate()
|
||||
.context("Account allocation through the private key allocator failed")?;
|
||||
let account = private_key.address();
|
||||
let variable = U256::from_be_slice(account.0.as_slice());
|
||||
|
||||
self.execution_state
|
||||
.variables
|
||||
.insert(variable_name.to_string(), variable);
|
||||
|
||||
Ok(1)
|
||||
}
|
||||
// endregion:Step Handling
|
||||
|
||||
// region:Contract Deployment
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
%contract_instance,
|
||||
%deployer
|
||||
),
|
||||
err(Debug),
|
||||
)]
|
||||
async fn get_or_deploy_contract_instance(
|
||||
&mut self,
|
||||
contract_instance: &ContractInstance,
|
||||
deployer: Address,
|
||||
calldata: Option<&Calldata>,
|
||||
value: Option<EtherValue>,
|
||||
step_path: Option<&StepPath>,
|
||||
) -> Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
||||
if let Some((_, address, abi)) = self
|
||||
.execution_state
|
||||
.deployed_contracts
|
||||
.get(contract_instance)
|
||||
{
|
||||
info!(
|
||||
|
||||
%address,
|
||||
"Contract instance already deployed."
|
||||
);
|
||||
Ok((*address, abi.clone(), None))
|
||||
} else {
|
||||
info!("Contract instance requires deployment.");
|
||||
let (address, abi, receipt) = self
|
||||
.deploy_contract(contract_instance, deployer, calldata, value, step_path)
|
||||
.await
|
||||
.context("Failed to deploy contract")?;
|
||||
info!(
|
||||
%address,
|
||||
"Contract instance has been deployed."
|
||||
);
|
||||
Ok((address, abi, Some(receipt)))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
%contract_instance,
|
||||
%deployer
|
||||
),
|
||||
err(Debug),
|
||||
)]
|
||||
async fn deploy_contract(
|
||||
&mut self,
|
||||
contract_instance: &ContractInstance,
|
||||
deployer: Address,
|
||||
calldata: Option<&Calldata>,
|
||||
value: Option<EtherValue>,
|
||||
step_path: Option<&StepPath>,
|
||||
) -> Result<(Address, JsonAbi, TransactionReceipt)> {
|
||||
let Some(ContractPathAndIdent {
|
||||
contract_source_path,
|
||||
contract_ident,
|
||||
}) = self
|
||||
.test_definition
|
||||
.metadata
|
||||
.contract_sources()?
|
||||
.remove(contract_instance)
|
||||
else {
|
||||
anyhow::bail!(
|
||||
"Contract source not found for instance {:?}",
|
||||
contract_instance
|
||||
)
|
||||
};
|
||||
|
||||
let Some((code, abi)) = self
|
||||
.execution_state
|
||||
.compiled_contracts
|
||||
.get(&contract_source_path)
|
||||
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
|
||||
.cloned()
|
||||
else {
|
||||
anyhow::bail!(
|
||||
"Failed to find information for contract {:?}",
|
||||
contract_instance
|
||||
)
|
||||
};
|
||||
|
||||
let mut code = match alloy::hex::decode(&code) {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
?error,
|
||||
contract_source_path = contract_source_path.display().to_string(),
|
||||
contract_ident = contract_ident.as_ref(),
|
||||
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
|
||||
);
|
||||
anyhow::bail!("Failed to hex-decode the byte code {}", error)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(calldata) = calldata {
|
||||
let calldata = calldata
|
||||
.calldata(self.resolver.as_ref(), self.default_resolution_context())
|
||||
.await?;
|
||||
code.extend(calldata);
|
||||
}
|
||||
|
||||
let tx = {
|
||||
let tx = TransactionRequest::default().from(deployer);
|
||||
let tx = match value {
|
||||
Some(ref value) => tx.value(value.into_inner()),
|
||||
_ => tx,
|
||||
};
|
||||
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
|
||||
};
|
||||
|
||||
let receipt = match self
|
||||
.execute_transaction(tx, step_path)
|
||||
.and_then(|(_, receipt_fut)| receipt_fut)
|
||||
.await
|
||||
{
|
||||
Ok(receipt) => receipt,
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "Contract deployment transaction failed.");
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let Some(address) = receipt.contract_address else {
|
||||
anyhow::bail!("Contract deployment didn't return an address");
|
||||
};
|
||||
tracing::info!(
|
||||
instance_name = ?contract_instance,
|
||||
instance_address = ?address,
|
||||
"Deployed contract"
|
||||
);
|
||||
self.platform_information
|
||||
.reporter
|
||||
.report_contract_deployed_event(contract_instance.clone(), address)?;
|
||||
|
||||
self.execution_state.deployed_contracts.insert(
|
||||
contract_instance.clone(),
|
||||
(contract_ident, address, abi.clone()),
|
||||
);
|
||||
|
||||
Ok((address, abi, receipt))
|
||||
}
|
||||
// endregion:Contract Deployment
|
||||
|
||||
// region:Resolution & Resolver
|
||||
fn default_resolution_context(&self) -> ResolutionContext<'_> {
|
||||
ResolutionContext::default()
|
||||
.with_deployed_contracts(&self.execution_state.deployed_contracts)
|
||||
.with_variables(&self.execution_state.variables)
|
||||
}
|
||||
// endregion:Resolution & Resolver
|
||||
|
||||
// region:Transaction Execution
|
||||
/// Executes the transaction on the driver's node with some custom waiting logic for the receipt
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(driver_id = self.driver_id, transaction_hash = tracing::field::Empty)
|
||||
)]
|
||||
async fn execute_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
step_path: Option<&StepPath>,
|
||||
) -> anyhow::Result<(TxHash, impl Future<Output = Result<TransactionReceipt>>)> {
|
||||
let node = self.platform_information.node;
|
||||
let transaction_hash = node
|
||||
.submit_transaction(transaction)
|
||||
.await
|
||||
.context("Failed to submit transaction")?;
|
||||
Span::current().record("transaction_hash", display(transaction_hash));
|
||||
|
||||
info!("Submitted transaction");
|
||||
if let Some(step_path) = step_path {
|
||||
self.watcher_tx
|
||||
.send(WatcherEvent::SubmittedTransaction {
|
||||
transaction_hash,
|
||||
step_path: step_path.clone(),
|
||||
})
|
||||
.context("Failed to send the transaction hash to the watcher")?;
|
||||
};
|
||||
|
||||
Ok((transaction_hash, async move {
|
||||
info!("Starting to poll for transaction receipt");
|
||||
poll(
|
||||
Duration::from_secs(30 * 60),
|
||||
PollingWaitBehavior::Constant(Duration::from_secs(1)),
|
||||
|| {
|
||||
async move {
|
||||
match node.get_receipt(transaction_hash).await {
|
||||
Ok(receipt) => {
|
||||
info!("Polling succeeded, receipt found");
|
||||
Ok(ControlFlow::Break(receipt))
|
||||
}
|
||||
Err(_) => Ok(ControlFlow::Continue(())),
|
||||
}
|
||||
}
|
||||
.instrument(info_span!("Polling for receipt"))
|
||||
},
|
||||
)
|
||||
.instrument(info_span!("Polling for receipt", %transaction_hash))
|
||||
.await
|
||||
.inspect(|_| info!("Found the transaction receipt"))
|
||||
}))
|
||||
}
|
||||
// endregion:Transaction Execution
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! The main entry point for differential benchmarking.
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use revive_dt_common::types::PrivateKeyAllocator;
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_format::{
|
||||
corpus::Corpus,
|
||||
steps::{Step, StepIdx, StepPath},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{Instrument, error, info, info_span, instrument, warn};
|
||||
|
||||
use revive_dt_config::{BenchmarkingContext, Context};
|
||||
use revive_dt_report::Reporter;
|
||||
|
||||
use crate::{
|
||||
differential_benchmarks::{Driver, Watcher, WatcherEvent},
|
||||
helpers::{CachedCompiler, NodePool, create_test_definitions_stream},
|
||||
};
|
||||
|
||||
/// Handles the differential testing executing it according to the information defined in the
|
||||
/// context
|
||||
#[instrument(level = "info", err(Debug), skip_all)]
|
||||
pub async fn handle_differential_benchmarks(
|
||||
mut context: BenchmarkingContext,
|
||||
reporter: Reporter,
|
||||
) -> anyhow::Result<()> {
|
||||
// A bit of a hack but we need to override the number of nodes specified through the CLI since
|
||||
// benchmarks can only be run on a single node. Perhaps in the future we'd have a cleaner way to
|
||||
// do this. But, for the time being, we need to override the cli arguments.
|
||||
if context.concurrency_configuration.number_of_nodes != 1 {
|
||||
warn!(
|
||||
specified_number_of_nodes = context.concurrency_configuration.number_of_nodes,
|
||||
updated_number_of_nodes = 1,
|
||||
"Invalid number of nodes specified through the CLI. Benchmarks can only be run on a single node. Updated the arguments."
|
||||
);
|
||||
context.concurrency_configuration.number_of_nodes = 1;
|
||||
};
|
||||
let full_context = Context::Benchmark(Box::new(context.clone()));
|
||||
|
||||
// Discover all of the metadata files that are defined in the context.
|
||||
let corpus = context
|
||||
.corpus_configuration
|
||||
.test_specifiers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.try_fold(Corpus::default(), Corpus::with_test_specifier)
|
||||
.context("Failed to parse the test corpus")?;
|
||||
info!(
|
||||
len = corpus.metadata_file_count(),
|
||||
"Discovered metadata files"
|
||||
);
|
||||
|
||||
// Discover the list of platforms that the tests should run on based on the context.
|
||||
let platforms = context
|
||||
.platforms
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Into::<&dyn Platform>::into)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Starting the nodes of the various platforms specified in the context. Note that we use the
|
||||
// node pool since it contains all of the code needed to spawn nodes from A to Z and therefore
|
||||
// it's the preferred way for us to start nodes even when we're starting just a single node. The
|
||||
// added overhead from it is quite small (performance wise) since it's involved only when we're
|
||||
// creating the test definitions, but it might have other maintenance overhead as it obscures
|
||||
// the fact that only a single node is spawned.
|
||||
let platforms_and_nodes = {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for platform in platforms.iter() {
|
||||
let platform_identifier = platform.platform_identifier();
|
||||
|
||||
let node_pool = NodePool::new(full_context.clone(), *platform)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
%platform_identifier,
|
||||
"Failed to initialize the node pool for the platform."
|
||||
)
|
||||
})
|
||||
.context("Failed to initialize the node pool")?;
|
||||
|
||||
map.insert(platform_identifier, (*platform, node_pool));
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
info!("Spawned the platform nodes");
|
||||
|
||||
// Preparing test definitions for the execution.
|
||||
let test_definitions = create_test_definitions_stream(
|
||||
&full_context,
|
||||
&corpus,
|
||||
&platforms_and_nodes,
|
||||
None,
|
||||
reporter.clone(),
|
||||
)
|
||||
.await
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
info!(len = test_definitions.len(), "Created test definitions");
|
||||
|
||||
// Creating the objects that will be shared between the various runs. The cached compiler is the
|
||||
// only one at the current moment of time that's safe to share between runs.
|
||||
let cached_compiler = CachedCompiler::new(
|
||||
context
|
||||
.working_directory
|
||||
.as_path()
|
||||
.join("compilation_cache"),
|
||||
context
|
||||
.compilation_configuration
|
||||
.invalidate_compilation_cache,
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.context("Failed to initialize cached compiler")?;
|
||||
|
||||
// Note: we do not want to run all of the workloads concurrently on all platforms. Rather, we'd
|
||||
// like to run all of the workloads for one platform, and then the next sequentially as we'd
|
||||
// like for the effect of concurrency to be minimized when we're doing the benchmarking.
|
||||
for platform in platforms.iter() {
|
||||
let platform_identifier = platform.platform_identifier();
|
||||
|
||||
let span = info_span!("Benchmarking for the platform", %platform_identifier);
|
||||
let _guard = span.enter();
|
||||
|
||||
for test_definition in test_definitions.iter() {
|
||||
let platform_information = &test_definition.platforms[&platform_identifier];
|
||||
|
||||
let span = info_span!(
|
||||
"Executing workload",
|
||||
metadata_file_path = %test_definition.metadata_file_path.display(),
|
||||
case_idx = %test_definition.case_idx,
|
||||
mode = %test_definition.mode,
|
||||
);
|
||||
let _guard = span.enter();
|
||||
|
||||
// Initializing all of the components requires to execute this particular workload.
|
||||
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
|
||||
context.wallet_configuration.highest_private_key_exclusive(),
|
||||
)));
|
||||
let (watcher, watcher_tx) = Watcher::new(
|
||||
platform_information
|
||||
.node
|
||||
.subscribe_to_full_blocks_information()
|
||||
.await
|
||||
.context("Failed to subscribe to full blocks information from the node")?,
|
||||
test_definition
|
||||
.reporter
|
||||
.execution_specific_reporter(0usize, platform_identifier),
|
||||
);
|
||||
let driver = Driver::new(
|
||||
platform_information,
|
||||
test_definition,
|
||||
private_key_allocator,
|
||||
cached_compiler.as_ref(),
|
||||
watcher_tx.clone(),
|
||||
test_definition
|
||||
.case
|
||||
.steps_iterator_for_benchmarks(context.default_repetition_count)
|
||||
.enumerate()
|
||||
.map(|(step_idx, step)| -> (StepPath, Step) {
|
||||
(StepPath::new(vec![StepIdx::new(step_idx)]), step)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to create the benchmarks driver")?;
|
||||
|
||||
futures::future::try_join(
|
||||
watcher.run(),
|
||||
driver
|
||||
.execute_all()
|
||||
.instrument(info_span!("Executing Benchmarks", %platform_identifier))
|
||||
.inspect(|_| {
|
||||
info!("All transactions submitted - driver completed execution");
|
||||
watcher_tx
|
||||
.send(WatcherEvent::AllTransactionsSubmitted)
|
||||
.unwrap()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to run the driver and executor")
|
||||
.inspect(|(_, steps_executed)| info!(steps_executed, "Workload Execution Succeeded"))
|
||||
.inspect_err(|err| error!(?err, "Workload Execution Failed"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use alloy::{
|
||||
json_abi::JsonAbi,
|
||||
primitives::{Address, U256},
|
||||
};
|
||||
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
|
||||
|
||||
#[derive(Clone)]
|
||||
/// The state associated with the test execution of one of the workloads.
|
||||
pub struct ExecutionState {
|
||||
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
|
||||
/// against them and therefore they're ready to be deployed on-demand.
|
||||
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
|
||||
/// A map of all of the deployed contracts and information about them.
|
||||
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
|
||||
/// This map stores the variables used for each one of the cases contained in the metadata file.
|
||||
pub variables: HashMap<String, U256>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
pub fn new(
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
compiled_contracts,
|
||||
deployed_contracts,
|
||||
variables: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
compiled_contracts: Default::default(),
|
||||
deployed_contracts: Default::default(),
|
||||
variables: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod driver;
|
||||
mod entry_point;
|
||||
mod execution_state;
|
||||
mod watcher;
|
||||
|
||||
pub use driver::*;
|
||||
pub use entry_point::*;
|
||||
pub use execution_state::*;
|
||||
pub use watcher::*;
|
||||
@@ -0,0 +1,215 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use alloy::primitives::{BlockNumber, TxHash};
|
||||
use anyhow::Result;
|
||||
use futures::{Stream, StreamExt};
|
||||
use revive_dt_format::steps::StepPath;
|
||||
use revive_dt_report::{ExecutionSpecificReporter, MinedBlockInformation, TransactionInformation};
|
||||
use tokio::sync::{
|
||||
RwLock,
|
||||
mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
|
||||
};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// This struct defines the watcher used in the benchmarks. A watcher is only valid for 1 workload
|
||||
/// and MUST NOT be re-used between workloads since it holds important internal state for a given
|
||||
/// workload and is not designed for reuse.
|
||||
pub struct Watcher {
|
||||
/// The receive side of the channel that all of the drivers and various other parts of the code
|
||||
/// send events to the watcher on.
|
||||
rx: UnboundedReceiver<WatcherEvent>,
|
||||
|
||||
/// This is a stream of the blocks that were mined by the node. This is for a single platform
|
||||
/// and a single node from that platform.
|
||||
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
|
||||
|
||||
/// The reporter used to send events to the report aggregator.
|
||||
reporter: ExecutionSpecificReporter,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn new(
|
||||
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
|
||||
reporter: ExecutionSpecificReporter,
|
||||
) -> (Self, UnboundedSender<WatcherEvent>) {
|
||||
let (tx, rx) = unbounded_channel::<WatcherEvent>();
|
||||
(
|
||||
Self {
|
||||
rx,
|
||||
blocks_stream,
|
||||
reporter,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn run(mut self) -> Result<()> {
|
||||
// The first event that the watcher receives must be a `RepetitionStartEvent` that informs
|
||||
// the watcher of the last block number that it should ignore and what the block number is
|
||||
// for the first important block that it should look for.
|
||||
let ignore_block_before = loop {
|
||||
let Some(WatcherEvent::RepetitionStartEvent {
|
||||
ignore_block_before,
|
||||
}) = self.rx.recv().await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
break ignore_block_before;
|
||||
};
|
||||
|
||||
// This is the set of the transaction hashes that the watcher should be looking for and
|
||||
// watch for them in the blocks. The watcher will keep watching for blocks until it sees
|
||||
// that all of the transactions that it was watching for has been seen in the mined blocks.
|
||||
let watch_for_transaction_hashes =
|
||||
Arc::new(RwLock::new(HashMap::<TxHash, (StepPath, SystemTime)>::new()));
|
||||
|
||||
// A boolean that keeps track of whether all of the transactions were submitted or if more
|
||||
// txs are expected to come through the receive side of the channel. We do not want to rely
|
||||
// on the channel closing alone for the watcher to know that all of the transactions were
|
||||
// submitted and for there to be an explicit event sent by the core orchestrator that
|
||||
// informs the watcher that no further transactions are to be expected and that it can
|
||||
// safely ignore the channel.
|
||||
let all_transactions_submitted = Arc::new(RwLock::new(false));
|
||||
|
||||
let watcher_event_watching_task = {
|
||||
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
|
||||
let all_transactions_submitted = all_transactions_submitted.clone();
|
||||
async move {
|
||||
while let Some(watcher_event) = self.rx.recv().await {
|
||||
match watcher_event {
|
||||
// Subsequent repetition starts are ignored since certain workloads can
|
||||
// contain nested repetitions and therefore there's no use in doing any
|
||||
// action if the repetitions are nested.
|
||||
WatcherEvent::RepetitionStartEvent { .. } => {}
|
||||
WatcherEvent::SubmittedTransaction {
|
||||
transaction_hash,
|
||||
step_path,
|
||||
} => {
|
||||
watch_for_transaction_hashes
|
||||
.write()
|
||||
.await
|
||||
.insert(transaction_hash, (step_path, SystemTime::now()));
|
||||
}
|
||||
WatcherEvent::AllTransactionsSubmitted => {
|
||||
*all_transactions_submitted.write().await = true;
|
||||
self.rx.close();
|
||||
info!("Watcher's Events Watching Task Finished");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let reporter = self.reporter.clone();
|
||||
let block_information_watching_task = {
|
||||
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
|
||||
let all_transactions_submitted = all_transactions_submitted.clone();
|
||||
let mut blocks_information_stream = self.blocks_stream;
|
||||
async move {
|
||||
while let Some(mut block) = blocks_information_stream.next().await {
|
||||
// If the block number is equal to or less than the last block before the
|
||||
// repetition then we ignore it and continue on to the next block.
|
||||
if block.ethereum_block_information.block_number <= ignore_block_before {
|
||||
continue;
|
||||
}
|
||||
{
|
||||
let watch_for_transaction_hashes =
|
||||
watch_for_transaction_hashes.read().await;
|
||||
for tx_hash in block.ethereum_block_information.transaction_hashes.iter() {
|
||||
let Some((step_path, _)) = watch_for_transaction_hashes.get(tx_hash)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
*block.tx_counts.entry(step_path.clone()).or_default() += 1
|
||||
}
|
||||
}
|
||||
reporter
|
||||
.report_block_mined_event(block.clone())
|
||||
.expect("Can't fail");
|
||||
|
||||
if *all_transactions_submitted.read().await
|
||||
&& watch_for_transaction_hashes.read().await.is_empty()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
info!(
|
||||
block_number = block.ethereum_block_information.block_number,
|
||||
block_tx_count = block.ethereum_block_information.transaction_hashes.len(),
|
||||
remaining_transactions = watch_for_transaction_hashes.read().await.len(),
|
||||
"Observed a block"
|
||||
);
|
||||
|
||||
// Remove all of the transaction hashes observed in this block from the txs we
|
||||
// are currently watching for.
|
||||
let mut watch_for_transaction_hashes =
|
||||
watch_for_transaction_hashes.write().await;
|
||||
for tx_hash in block.ethereum_block_information.transaction_hashes.iter() {
|
||||
let Some((step_path, submission_time)) =
|
||||
watch_for_transaction_hashes.remove(tx_hash)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let transaction_information = TransactionInformation {
|
||||
transaction_hash: *tx_hash,
|
||||
submission_timestamp: submission_time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Can't fail")
|
||||
.as_secs() as _,
|
||||
block_timestamp: block.ethereum_block_information.block_timestamp,
|
||||
block_number: block.ethereum_block_information.block_number,
|
||||
};
|
||||
reporter
|
||||
.report_step_transaction_information_event(
|
||||
step_path,
|
||||
transaction_information,
|
||||
)
|
||||
.expect("Can't fail")
|
||||
}
|
||||
}
|
||||
|
||||
info!("Watcher's Block Watching Task Finished");
|
||||
}
|
||||
};
|
||||
|
||||
let (_, _) =
|
||||
futures::future::join(watcher_event_watching_task, block_information_watching_task)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum WatcherEvent {
|
||||
/// Informs the watcher that it should begin watching for the blocks mined by the platforms.
|
||||
/// Before the watcher receives this event it will not be watching for the mined blocks. The
|
||||
/// reason behind this is that we do not want the initialization transactions (e.g., contract
|
||||
/// deployments) to be included in the overall TPS and GPS measurements since these blocks will
|
||||
/// most likely only contain a single transaction since they're just being used for
|
||||
/// initialization.
|
||||
RepetitionStartEvent {
|
||||
/// This is the block number of the last block seen before the repetition started. This is
|
||||
/// used to instruct the watcher to ignore all block prior to this block when it starts
|
||||
/// streaming the blocks.
|
||||
ignore_block_before: BlockNumber,
|
||||
},
|
||||
/// Informs the watcher that a transaction was submitted and that the watcher should watch for a
|
||||
/// transaction with this hash in the blocks that it watches.
|
||||
SubmittedTransaction {
|
||||
/// The hash of the submitted transaction.
|
||||
transaction_hash: TxHash,
|
||||
/// The step path of the step that the transaction belongs to.
|
||||
step_path: StepPath,
|
||||
},
|
||||
/// Informs the watcher that all of the transactions of this benchmark have been submitted and
|
||||
/// that it can expect to receive no further transaction hashes and not even watch the channel
|
||||
/// any longer.
|
||||
AllTransactionsSubmitted,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
//! The main entry point into differential testing.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
io::{BufWriter, Write, stderr},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ansi_term::{ANSIStrings, Color};
|
||||
use anyhow::Context as _;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use revive_dt_common::{cached_fs::read_to_string, types::PrivateKeyAllocator};
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_format::corpus::Corpus;
|
||||
use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
use tracing::{Instrument, error, info, info_span, instrument};
|
||||
|
||||
use revive_dt_config::{Context, OutputFormat, TestExecutionContext};
|
||||
use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus};
|
||||
|
||||
use crate::{
|
||||
differential_tests::Driver,
|
||||
helpers::{CachedCompiler, NodePool, create_test_definitions_stream},
|
||||
};
|
||||
|
||||
/// Handles the differential testing executing it according to the information defined in the
|
||||
/// context
|
||||
#[instrument(level = "info", err(Debug), skip_all)]
|
||||
pub async fn handle_differential_tests(
|
||||
context: TestExecutionContext,
|
||||
reporter: Reporter,
|
||||
) -> anyhow::Result<()> {
|
||||
let reporter_clone = reporter.clone();
|
||||
|
||||
// Discover all of the metadata files that are defined in the context.
|
||||
let corpus = context
|
||||
.corpus_configuration
|
||||
.test_specifiers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.try_fold(Corpus::default(), Corpus::with_test_specifier)
|
||||
.context("Failed to parse the test corpus")?;
|
||||
info!(
|
||||
len = corpus.metadata_file_count(),
|
||||
"Discovered metadata files"
|
||||
);
|
||||
|
||||
// Discover the list of platforms that the tests should run on based on the context.
|
||||
let platforms = context
|
||||
.platforms
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Into::<&dyn Platform>::into)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Starting the nodes of the various platforms specified in the context.
|
||||
let platforms_and_nodes = {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for platform in platforms.iter() {
|
||||
let platform_identifier = platform.platform_identifier();
|
||||
|
||||
let context = Context::Test(Box::new(context.clone()));
|
||||
let node_pool = NodePool::new(context, *platform)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
%platform_identifier,
|
||||
"Failed to initialize the node pool for the platform."
|
||||
)
|
||||
})
|
||||
.context("Failed to initialize the node pool")?;
|
||||
|
||||
map.insert(platform_identifier, (*platform, node_pool));
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
info!("Spawned the platform nodes");
|
||||
|
||||
// Preparing test definitions.
|
||||
let only_execute_failed_tests = match context.ignore_success_configuration.path.as_ref() {
|
||||
Some(path) => {
|
||||
let report = read_to_string(path)
|
||||
.context("Failed to read the report file to ignore the succeeding test cases")?;
|
||||
Some(serde_json::from_str(&report).context("Failed to deserialize report")?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let full_context = Context::Test(Box::new(context.clone()));
|
||||
let test_definitions = create_test_definitions_stream(
|
||||
&full_context,
|
||||
&corpus,
|
||||
&platforms_and_nodes,
|
||||
only_execute_failed_tests.as_ref(),
|
||||
reporter.clone(),
|
||||
)
|
||||
.await
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
info!(len = test_definitions.len(), "Created test definitions");
|
||||
|
||||
// Creating everything else required for the driver to run.
|
||||
let cached_compiler = CachedCompiler::new(
|
||||
context
|
||||
.working_directory
|
||||
.as_path()
|
||||
.join("compilation_cache"),
|
||||
context
|
||||
.compilation_configuration
|
||||
.invalidate_compilation_cache,
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.context("Failed to initialize cached compiler")?;
|
||||
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
|
||||
context.wallet_configuration.highest_private_key_exclusive(),
|
||||
)));
|
||||
|
||||
// Creating the driver and executing all of the steps.
|
||||
let semaphore = context
|
||||
.concurrency_configuration
|
||||
.concurrency_limit()
|
||||
.map(Semaphore::new)
|
||||
.map(Arc::new);
|
||||
let running_task_list = Arc::new(RwLock::new(BTreeSet::<usize>::new()));
|
||||
let driver_task = futures::future::join_all(test_definitions.iter().enumerate().map(
|
||||
|(test_id, test_definition)| {
|
||||
let running_task_list = running_task_list.clone();
|
||||
let semaphore = semaphore.clone();
|
||||
|
||||
let private_key_allocator = private_key_allocator.clone();
|
||||
let cached_compiler = cached_compiler.clone();
|
||||
let mode = test_definition.mode.clone();
|
||||
let span = info_span!(
|
||||
"Executing Test Case",
|
||||
test_id,
|
||||
metadata_file_path = %test_definition.metadata_file_path.display(),
|
||||
case_idx = %test_definition.case_idx,
|
||||
mode = %mode,
|
||||
);
|
||||
async move {
|
||||
let permit = match semaphore.as_ref() {
|
||||
Some(semaphore) => Some(semaphore.acquire().await.expect("Can't fail")),
|
||||
None => None,
|
||||
};
|
||||
|
||||
running_task_list.write().await.insert(test_id);
|
||||
let driver = match Driver::new_root(
|
||||
test_definition,
|
||||
private_key_allocator,
|
||||
&cached_compiler,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(driver) => driver,
|
||||
Err(error) => {
|
||||
test_definition
|
||||
.reporter
|
||||
.report_test_failed_event(format!("{error:#}"))
|
||||
.expect("Can't fail");
|
||||
error!("Test Case Failed");
|
||||
drop(permit);
|
||||
running_task_list.write().await.remove(&test_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("Created the driver for the test case");
|
||||
|
||||
match driver.execute_all().await {
|
||||
Ok(steps_executed) => test_definition
|
||||
.reporter
|
||||
.report_test_succeeded_event(steps_executed)
|
||||
.expect("Can't fail"),
|
||||
Err(error) => {
|
||||
test_definition
|
||||
.reporter
|
||||
.report_test_failed_event(format!("{error:#}"))
|
||||
.expect("Can't fail");
|
||||
error!("Test Case Failed");
|
||||
}
|
||||
};
|
||||
info!("Finished the execution of the test case");
|
||||
drop(permit);
|
||||
running_task_list.write().await.remove(&test_id);
|
||||
}
|
||||
.instrument(span)
|
||||
},
|
||||
))
|
||||
.inspect(|_| {
|
||||
info!("Finished executing all test cases");
|
||||
reporter_clone
|
||||
.report_completion_event()
|
||||
.expect("Can't fail")
|
||||
});
|
||||
let cli_reporting_task = start_cli_reporting_task(context.output_format, reporter);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
let remaining_tasks = running_task_list.read().await;
|
||||
info!(
|
||||
count = remaining_tasks.len(),
|
||||
?remaining_tasks,
|
||||
"Remaining Tests"
|
||||
);
|
||||
drop(remaining_tasks);
|
||||
tokio::time::sleep(Duration::from_secs(10)).await
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::join(driver_task, cli_reporting_task).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
|
||||
async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporter) {
|
||||
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
|
||||
drop(reporter);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut global_success_count = 0;
|
||||
let mut global_failure_count = 0;
|
||||
let mut global_ignore_count = 0;
|
||||
|
||||
let mut buf = BufWriter::new(stderr());
|
||||
while let Ok(event) = aggregator_events_rx.recv().await {
|
||||
let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
|
||||
metadata_file_path,
|
||||
mode,
|
||||
case_status,
|
||||
} = event
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match output_format {
|
||||
OutputFormat::Legacy => {
|
||||
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
|
||||
for (case_idx, case_status) in case_status.into_iter() {
|
||||
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
|
||||
let _ = match case_status {
|
||||
TestCaseStatus::Succeeded { steps_executed } => {
|
||||
global_success_count += 1;
|
||||
writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Green.bold().paint("Case Succeeded"),
|
||||
Color::Green
|
||||
.paint(format!(" - Steps Executed: {steps_executed}")),
|
||||
])
|
||||
)
|
||||
}
|
||||
TestCaseStatus::Failed { reason } => {
|
||||
global_failure_count += 1;
|
||||
writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Red.bold().paint("Case Failed"),
|
||||
Color::Red.paint(format!(" - Reason: {}", reason.trim())),
|
||||
])
|
||||
)
|
||||
}
|
||||
TestCaseStatus::Ignored { reason, .. } => {
|
||||
global_ignore_count += 1;
|
||||
writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Yellow.bold().paint("Case Ignored"),
|
||||
Color::Yellow.paint(format!(" - Reason: {}", reason.trim())),
|
||||
])
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
let _ = writeln!(buf);
|
||||
}
|
||||
OutputFormat::CargoTestLike => {
|
||||
writeln!(
|
||||
buf,
|
||||
"\t{} {} - {}\n",
|
||||
Color::Green.paint("Running"),
|
||||
metadata_file_path.display(),
|
||||
mode
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
let mut ignored_count = 0;
|
||||
writeln!(buf, "running {} tests", case_status.len()).unwrap();
|
||||
for (case_idx, case_result) in case_status.iter() {
|
||||
let status = match case_result {
|
||||
TestCaseStatus::Succeeded { .. } => {
|
||||
success_count += 1;
|
||||
global_success_count += 1;
|
||||
Color::Green.paint("ok")
|
||||
}
|
||||
TestCaseStatus::Failed { reason } => {
|
||||
failure_count += 1;
|
||||
global_failure_count += 1;
|
||||
Color::Red.paint(format!("FAILED, {reason}"))
|
||||
}
|
||||
TestCaseStatus::Ignored { reason, .. } => {
|
||||
ignored_count += 1;
|
||||
global_ignore_count += 1;
|
||||
Color::Yellow.paint(format!("ignored, {reason:?}"))
|
||||
}
|
||||
};
|
||||
writeln!(buf, "test case_idx_{} ... {}", case_idx, status).unwrap();
|
||||
}
|
||||
writeln!(buf).unwrap();
|
||||
|
||||
let status = if failure_count > 0 {
|
||||
Color::Red.paint("FAILED")
|
||||
} else {
|
||||
Color::Green.paint("ok")
|
||||
};
|
||||
writeln!(
|
||||
buf,
|
||||
"test result: {}. {} passed; {} failed; {} ignored",
|
||||
status, success_count, failure_count, ignored_count,
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(buf).unwrap();
|
||||
|
||||
if aggregator_events_rx.is_empty() {
|
||||
buf = tokio::task::spawn_blocking(move || {
|
||||
buf.flush().unwrap();
|
||||
buf
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Aggregator Broadcast Channel Closed");
|
||||
|
||||
// Summary at the end.
|
||||
match output_format {
|
||||
OutputFormat::Legacy => {
|
||||
writeln!(
|
||||
buf,
|
||||
"{} cases: {} cases succeeded, {} cases failed in {} seconds",
|
||||
global_success_count + global_failure_count + global_ignore_count,
|
||||
Color::Green.paint(global_success_count.to_string()),
|
||||
Color::Red.paint(global_failure_count.to_string()),
|
||||
start.elapsed().as_secs()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
OutputFormat::CargoTestLike => {
|
||||
writeln!(
|
||||
buf,
|
||||
"run finished. {} passed; {} failed; {} ignored; finished in {}s",
|
||||
global_success_count,
|
||||
global_failure_count,
|
||||
global_ignore_count,
|
||||
start.elapsed().as_secs()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use alloy::{
|
||||
json_abi::JsonAbi,
|
||||
primitives::{Address, U256},
|
||||
};
|
||||
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
|
||||
|
||||
#[derive(Clone)]
|
||||
/// The state associated with the test execution of one of the tests.
|
||||
pub struct ExecutionState {
|
||||
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
|
||||
/// against them and therefore they're ready to be deployed on-demand.
|
||||
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
|
||||
/// A map of all of the deployed contracts and information about them.
|
||||
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
|
||||
/// This map stores the variables used for each one of the cases contained in the metadata file.
|
||||
pub variables: HashMap<String, U256>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
pub fn new(
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
compiled_contracts,
|
||||
deployed_contracts,
|
||||
variables: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! This module contains all of the code responsible for performing differential tests including the
|
||||
//! driver implementation, state implementation, and the core logic that allows for tests to be
|
||||
//! executed.
|
||||
|
||||
mod driver;
|
||||
mod entry_point;
|
||||
mod execution_state;
|
||||
|
||||
pub use driver::*;
|
||||
pub use entry_point::*;
|
||||
pub use execution_state::*;
|
||||
@@ -1,835 +0,0 @@
|
||||
//! The test driver handles the compilation and execution of the test cases.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use alloy::consensus::EMPTY_ROOT_HASH;
|
||||
use alloy::hex;
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy::network::{Ethereum, TransactionBuilder};
|
||||
use alloy::primitives::U256;
|
||||
use alloy::rpc::types::TransactionReceipt;
|
||||
use alloy::rpc::types::trace::geth::{
|
||||
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
|
||||
GethDebugTracingOptions, GethTrace, PreStateConfig,
|
||||
};
|
||||
use alloy::{
|
||||
primitives::Address,
|
||||
rpc::types::{TransactionRequest, trace::geth::DiffMode},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
|
||||
use revive_dt_report::ExecutionSpecificReporter;
|
||||
use semver::Version;
|
||||
|
||||
use revive_dt_format::case::Case;
|
||||
use revive_dt_format::input::{
|
||||
BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, StepIdx,
|
||||
StorageEmptyAssertion,
|
||||
};
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
|
||||
use revive_dt_format::{input::Step, metadata::Metadata};
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use tokio::try_join;
|
||||
use tracing::{Instrument, info, info_span, instrument};
|
||||
|
||||
use crate::Platform;
|
||||
|
||||
pub struct CaseState<T: Platform> {
|
||||
/// A map of all of the compiled contracts for the given metadata file.
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
|
||||
/// This map stores the contracts deployments for this case.
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
|
||||
/// This map stores the variables used for each one of the cases contained in the metadata
|
||||
/// file.
|
||||
variables: HashMap<String, U256>,
|
||||
|
||||
/// Stores the version used for the current case.
|
||||
compiler_version: Version,
|
||||
|
||||
/// The execution reporter.
|
||||
execution_reporter: ExecutionSpecificReporter,
|
||||
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> CaseState<T>
|
||||
where
|
||||
T: Platform,
|
||||
{
|
||||
pub fn new(
|
||||
compiler_version: Version,
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
execution_reporter: ExecutionSpecificReporter,
|
||||
) -> Self {
|
||||
Self {
|
||||
compiled_contracts,
|
||||
deployed_contracts,
|
||||
variables: Default::default(),
|
||||
compiler_version,
|
||||
execution_reporter,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_step(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
step: &Step,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<StepOutput> {
|
||||
match step {
|
||||
Step::FunctionCall(input) => {
|
||||
let (receipt, geth_trace, diff_mode) =
|
||||
self.handle_input(metadata, input, node).await?;
|
||||
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
|
||||
}
|
||||
Step::BalanceAssertion(balance_assertion) => {
|
||||
self.handle_balance_assertion(metadata, balance_assertion, node)
|
||||
.await?;
|
||||
Ok(StepOutput::BalanceAssertion)
|
||||
}
|
||||
Step::StorageEmptyAssertion(storage_empty) => {
|
||||
self.handle_storage_empty(metadata, storage_empty, node)
|
||||
.await?;
|
||||
Ok(StepOutput::StorageEmptyAssertion)
|
||||
}
|
||||
}
|
||||
.inspect(|_| info!("Step Succeeded"))
|
||||
}
|
||||
|
||||
#[instrument(level = "info", name = "Handling Input", skip_all)]
|
||||
pub async fn handle_input(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
input: &Input,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
|
||||
let deployment_receipts = self
|
||||
.handle_input_contract_deployment(metadata, input, node)
|
||||
.await?;
|
||||
let execution_receipt = self
|
||||
.handle_input_execution(input, deployment_receipts, node)
|
||||
.await?;
|
||||
let tracing_result = self
|
||||
.handle_input_call_frame_tracing(&execution_receipt, node)
|
||||
.await?;
|
||||
self.handle_input_variable_assignment(input, &tracing_result)?;
|
||||
let (_, (geth_trace, diff_mode)) = try_join!(
|
||||
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
|
||||
self.handle_input_diff(&execution_receipt, node)
|
||||
)?;
|
||||
Ok((execution_receipt, geth_trace, diff_mode))
|
||||
}
|
||||
|
||||
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
|
||||
pub async fn handle_balance_assertion(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
balance_assertion: &BalanceAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
|
||||
.await?;
|
||||
self.handle_balance_assertion_execution(balance_assertion, node)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
|
||||
pub async fn handle_storage_empty(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
storage_empty: &StorageEmptyAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
|
||||
.await?;
|
||||
self.handle_storage_empty_assertion_execution(storage_empty, node)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles the contract deployment for a given input performing it if it needs to be performed.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_contract_deployment(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
input: &Input,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
|
||||
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
|
||||
for instance in input.find_all_contract_instances().into_iter() {
|
||||
if !self.deployed_contracts.contains_key(&instance) {
|
||||
instances_we_must_deploy.entry(instance).or_insert(false);
|
||||
}
|
||||
}
|
||||
if let Method::Deployer = input.method {
|
||||
instances_we_must_deploy.swap_remove(&input.instance);
|
||||
instances_we_must_deploy.insert(input.instance.clone(), true);
|
||||
}
|
||||
|
||||
let mut receipts = HashMap::new();
|
||||
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
|
||||
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
|
||||
let value = deploy_with_constructor_arguments
|
||||
.then_some(input.value)
|
||||
.flatten();
|
||||
|
||||
if let (_, _, Some(receipt)) = self
|
||||
.get_or_deploy_contract_instance(
|
||||
&instance,
|
||||
metadata,
|
||||
input.caller,
|
||||
calldata,
|
||||
value,
|
||||
node,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
receipts.insert(instance.clone(), receipt);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(receipts)
|
||||
}
|
||||
|
||||
/// Handles the execution of the input in terms of the calls that need to be made.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_execution(
|
||||
&mut self,
|
||||
input: &Input,
|
||||
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<TransactionReceipt> {
|
||||
match input.method {
|
||||
// This input was already executed when `handle_input` was called. We just need to
|
||||
// lookup the transaction receipt in this case and continue on.
|
||||
Method::Deployer => deployment_receipts
|
||||
.remove(&input.instance)
|
||||
.context("Failed to find deployment receipt"),
|
||||
Method::Fallback | Method::FunctionName(_) => {
|
||||
let tx = match input
|
||||
.legacy_transaction(node, self.default_resolution_context())
|
||||
.await
|
||||
{
|
||||
Ok(tx) => tx,
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
match node.execute_transaction(tx).await {
|
||||
Ok(receipt) => Ok(receipt),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_call_frame_tracing(
|
||||
&self,
|
||||
execution_receipt: &TransactionReceipt,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<CallFrame> {
|
||||
node.trace_transaction(
|
||||
execution_receipt,
|
||||
GethDebugTracingOptions {
|
||||
tracer: Some(GethDebugTracerType::BuiltInTracer(
|
||||
GethDebugBuiltInTracerType::CallTracer,
|
||||
)),
|
||||
tracer_config: GethDebugTracerConfig(serde_json::json! {{
|
||||
"onlyTopCall": true,
|
||||
"withLog": false,
|
||||
"withStorage": false,
|
||||
"withMemory": false,
|
||||
"withStack": false,
|
||||
"withReturnData": true
|
||||
}}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|trace| {
|
||||
trace
|
||||
.try_into_call_frame()
|
||||
.expect("Impossible - we requested a callframe trace so we must get it back")
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
fn handle_input_variable_assignment(
|
||||
&mut self,
|
||||
input: &Input,
|
||||
tracing_result: &CallFrame,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(ref assignments) = input.variable_assignments else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Handling the return data variable assignments.
|
||||
for (variable_name, output_word) in assignments.return_data.iter().zip(
|
||||
tracing_result
|
||||
.output
|
||||
.as_ref()
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
.chunks(32),
|
||||
) {
|
||||
let value = U256::from_be_slice(output_word);
|
||||
self.variables.insert(variable_name.clone(), value);
|
||||
tracing::info!(
|
||||
variable_name,
|
||||
variable_value = hex::encode(value.to_be_bytes::<32>()),
|
||||
"Assigned variable"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_expectations(
|
||||
&self,
|
||||
input: &Input,
|
||||
execution_receipt: &TransactionReceipt,
|
||||
resolver: &impl ResolverApi,
|
||||
tracing_result: &CallFrame,
|
||||
) -> anyhow::Result<()> {
|
||||
// Resolving the `input.expected` into a series of expectations that we can then assert on.
|
||||
let mut expectations = match input {
|
||||
Input {
|
||||
expected: Some(Expected::Calldata(calldata)),
|
||||
..
|
||||
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
|
||||
Input {
|
||||
expected: Some(Expected::Expected(expected)),
|
||||
..
|
||||
} => vec![expected.clone()],
|
||||
Input {
|
||||
expected: Some(Expected::ExpectedMany(expected)),
|
||||
..
|
||||
} => expected.clone(),
|
||||
Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
|
||||
};
|
||||
|
||||
// This is a bit of a special case and we have to support it separately on it's own. If it's
|
||||
// a call to the deployer method, then the tests will assert that it "returns" the address
|
||||
// of the contract. Deployments do not return the address of the contract but the runtime
|
||||
// code of the contracts. Therefore, this assertion would always fail. So, we replace it
|
||||
// with an assertion of "check if it succeeded"
|
||||
if let Method::Deployer = &input.method {
|
||||
for expectation in expectations.iter_mut() {
|
||||
expectation.return_data = None;
|
||||
}
|
||||
}
|
||||
|
||||
futures::stream::iter(expectations.into_iter().map(Ok))
|
||||
.try_for_each_concurrent(None, |expectation| async move {
|
||||
self.handle_input_expectation_item(
|
||||
execution_receipt,
|
||||
resolver,
|
||||
expectation,
|
||||
tracing_result,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_expectation_item(
|
||||
&self,
|
||||
execution_receipt: &TransactionReceipt,
|
||||
resolver: &impl ResolverApi,
|
||||
expectation: ExpectedOutput,
|
||||
tracing_result: &CallFrame,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(ref version_requirement) = expectation.compiler_version {
|
||||
if !version_requirement.matches(&self.compiler_version) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let resolution_context = self
|
||||
.default_resolution_context()
|
||||
.with_block_number(execution_receipt.block_number.as_ref())
|
||||
.with_transaction_hash(&execution_receipt.transaction_hash);
|
||||
|
||||
// Handling the receipt state assertion.
|
||||
let expected = !expectation.exception;
|
||||
let actual = execution_receipt.status();
|
||||
if actual != expected {
|
||||
tracing::error!(
|
||||
expected,
|
||||
actual,
|
||||
?execution_receipt,
|
||||
?tracing_result,
|
||||
"Transaction status assertion failed"
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Transaction status assertion failed - Expected {expected} but got {actual}",
|
||||
);
|
||||
}
|
||||
|
||||
// Handling the calldata assertion
|
||||
if let Some(ref expected_calldata) = expectation.return_data {
|
||||
let expected = expected_calldata;
|
||||
let actual = &tracing_result.output.as_ref().unwrap_or_default();
|
||||
if !expected
|
||||
.is_equivalent(actual, resolver, resolution_context)
|
||||
.await?
|
||||
{
|
||||
tracing::error!(
|
||||
?execution_receipt,
|
||||
?expected,
|
||||
%actual,
|
||||
"Calldata assertion failed"
|
||||
);
|
||||
anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",);
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the events assertion
|
||||
if let Some(ref expected_events) = expectation.events {
|
||||
// Handling the events length assertion.
|
||||
let expected = expected_events.len();
|
||||
let actual = execution_receipt.logs().len();
|
||||
if actual != expected {
|
||||
tracing::error!(expected, actual, "Event count assertion failed",);
|
||||
anyhow::bail!(
|
||||
"Event count assertion failed - Expected {expected} but got {actual}",
|
||||
);
|
||||
}
|
||||
|
||||
// Handling the events assertion.
|
||||
for (event_idx, (expected_event, actual_event)) in expected_events
|
||||
.iter()
|
||||
.zip(execution_receipt.logs())
|
||||
.enumerate()
|
||||
{
|
||||
// Handling the emitter assertion.
|
||||
if let Some(ref expected_address) = expected_event.address {
|
||||
let expected = Address::from_slice(
|
||||
Calldata::new_compound([expected_address])
|
||||
.calldata(resolver, resolution_context)
|
||||
.await?
|
||||
.get(12..32)
|
||||
.expect("Can't fail"),
|
||||
);
|
||||
let actual = actual_event.address();
|
||||
if actual != expected {
|
||||
tracing::error!(
|
||||
event_idx,
|
||||
%expected,
|
||||
%actual,
|
||||
"Event emitter assertion failed",
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Event emitter assertion failed - Expected {expected} but got {actual}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the topics assertion.
|
||||
for (expected, actual) in expected_event
|
||||
.topics
|
||||
.as_slice()
|
||||
.iter()
|
||||
.zip(actual_event.topics())
|
||||
{
|
||||
let expected = Calldata::new_compound([expected]);
|
||||
if !expected
|
||||
.is_equivalent(&actual.0, resolver, resolution_context)
|
||||
.await?
|
||||
{
|
||||
tracing::error!(
|
||||
event_idx,
|
||||
?execution_receipt,
|
||||
?expected,
|
||||
?actual,
|
||||
"Event topics assertion failed",
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Event topics assertion failed - Expected {expected:?} but got {actual:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the values assertion.
|
||||
let expected = &expected_event.values;
|
||||
let actual = &actual_event.data().data;
|
||||
if !expected
|
||||
.is_equivalent(&actual.0, resolver, resolution_context)
|
||||
.await?
|
||||
{
|
||||
tracing::error!(
|
||||
event_idx,
|
||||
?execution_receipt,
|
||||
?expected,
|
||||
?actual,
|
||||
"Event value assertion failed",
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Event value assertion failed - Expected {expected:?} but got {actual:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn handle_input_diff(
|
||||
&self,
|
||||
execution_receipt: &TransactionReceipt,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<(GethTrace, DiffMode)> {
|
||||
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
|
||||
diff_mode: Some(true),
|
||||
disable_code: None,
|
||||
disable_storage: None,
|
||||
});
|
||||
|
||||
let trace = node
|
||||
.trace_transaction(execution_receipt, trace_options)
|
||||
.await?;
|
||||
let diff = node.state_diff(execution_receipt).await?;
|
||||
|
||||
Ok((trace, diff))
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn handle_balance_assertion_contract_deployment(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
balance_assertion: &BalanceAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(instance) = balance_assertion
|
||||
.address
|
||||
.strip_suffix(".address")
|
||||
.map(ContractInstance::new)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
self.get_or_deploy_contract_instance(
|
||||
&instance,
|
||||
metadata,
|
||||
Input::default_caller(),
|
||||
None,
|
||||
None,
|
||||
node,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn handle_balance_assertion_execution(
|
||||
&mut self,
|
||||
BalanceAssertion {
|
||||
address: address_string,
|
||||
expected_balance: amount,
|
||||
..
|
||||
}: &BalanceAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
let address = Address::from_slice(
|
||||
Calldata::new_compound([address_string])
|
||||
.calldata(node, self.default_resolution_context())
|
||||
.await?
|
||||
.get(12..32)
|
||||
.expect("Can't fail"),
|
||||
);
|
||||
|
||||
let balance = node.balance_of(address).await?;
|
||||
|
||||
let expected = *amount;
|
||||
let actual = balance;
|
||||
if expected != actual {
|
||||
tracing::error!(%expected, %actual, %address, "Balance assertion failed");
|
||||
anyhow::bail!(
|
||||
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
|
||||
expected,
|
||||
actual,
|
||||
address_string,
|
||||
address,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn handle_storage_empty_assertion_contract_deployment(
|
||||
&mut self,
|
||||
metadata: &Metadata,
|
||||
storage_empty_assertion: &StorageEmptyAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(instance) = storage_empty_assertion
|
||||
.address
|
||||
.strip_suffix(".address")
|
||||
.map(ContractInstance::new)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
self.get_or_deploy_contract_instance(
|
||||
&instance,
|
||||
metadata,
|
||||
Input::default_caller(),
|
||||
None,
|
||||
None,
|
||||
node,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn handle_storage_empty_assertion_execution(
|
||||
&mut self,
|
||||
StorageEmptyAssertion {
|
||||
address: address_string,
|
||||
is_storage_empty,
|
||||
..
|
||||
}: &StorageEmptyAssertion,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<()> {
|
||||
let address = Address::from_slice(
|
||||
Calldata::new_compound([address_string])
|
||||
.calldata(node, self.default_resolution_context())
|
||||
.await?
|
||||
.get(12..32)
|
||||
.expect("Can't fail"),
|
||||
);
|
||||
|
||||
let storage = node.latest_state_proof(address, Default::default()).await?;
|
||||
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
|
||||
|
||||
let expected = is_storage_empty;
|
||||
let actual = is_empty;
|
||||
|
||||
if *expected != actual {
|
||||
tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed");
|
||||
anyhow::bail!(
|
||||
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
|
||||
expected,
|
||||
actual,
|
||||
address_string,
|
||||
address,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the information of a deployed contract or library from the state. If it's found to not
|
||||
/// be deployed then it will be deployed.
|
||||
///
|
||||
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
|
||||
/// cross-case deployed contracts address mapping.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_or_deploy_contract_instance(
|
||||
&mut self,
|
||||
contract_instance: &ContractInstance,
|
||||
metadata: &Metadata,
|
||||
deployer: Address,
|
||||
calldata: Option<&Calldata>,
|
||||
value: Option<EtherValue>,
|
||||
node: &T::Blockchain,
|
||||
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
||||
if let Some((_, address, abi)) = self.deployed_contracts.get(contract_instance) {
|
||||
return Ok((*address, abi.clone(), None));
|
||||
}
|
||||
|
||||
let Some(ContractPathAndIdent {
|
||||
contract_source_path,
|
||||
contract_ident,
|
||||
}) = metadata.contract_sources()?.remove(contract_instance)
|
||||
else {
|
||||
anyhow::bail!(
|
||||
"Contract source not found for instance {:?}",
|
||||
contract_instance
|
||||
)
|
||||
};
|
||||
|
||||
let Some((code, abi)) = self
|
||||
.compiled_contracts
|
||||
.get(&contract_source_path)
|
||||
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
|
||||
.cloned()
|
||||
else {
|
||||
anyhow::bail!(
|
||||
"Failed to find information for contract {:?}",
|
||||
contract_instance
|
||||
)
|
||||
};
|
||||
|
||||
let mut code = match alloy::hex::decode(&code) {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
?error,
|
||||
contract_source_path = contract_source_path.display().to_string(),
|
||||
contract_ident = contract_ident.as_ref(),
|
||||
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
|
||||
);
|
||||
anyhow::bail!("Failed to hex-decode the byte code {}", error)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(calldata) = calldata {
|
||||
let calldata = calldata
|
||||
.calldata(node, self.default_resolution_context())
|
||||
.await?;
|
||||
code.extend(calldata);
|
||||
}
|
||||
|
||||
let tx = {
|
||||
let tx = TransactionRequest::default().from(deployer);
|
||||
let tx = match value {
|
||||
Some(ref value) => tx.value(value.into_inner()),
|
||||
_ => tx,
|
||||
};
|
||||
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
|
||||
};
|
||||
|
||||
let receipt = match node.execute_transaction(tx).await {
|
||||
Ok(receipt) => receipt,
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
node = std::any::type_name::<T>(),
|
||||
?error,
|
||||
"Contract deployment transaction failed."
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let Some(address) = receipt.contract_address else {
|
||||
anyhow::bail!("Contract deployment didn't return an address");
|
||||
};
|
||||
tracing::info!(
|
||||
instance_name = ?contract_instance,
|
||||
instance_address = ?address,
|
||||
"Deployed contract"
|
||||
);
|
||||
self.execution_reporter
|
||||
.report_contract_deployed_event(contract_instance.clone(), address)?;
|
||||
|
||||
self.deployed_contracts.insert(
|
||||
contract_instance.clone(),
|
||||
(contract_ident, address, abi.clone()),
|
||||
);
|
||||
|
||||
Ok((address, abi, Some(receipt)))
|
||||
}
|
||||
|
||||
fn default_resolution_context(&self) -> ResolutionContext<'_> {
|
||||
ResolutionContext::default()
|
||||
.with_deployed_contracts(&self.deployed_contracts)
|
||||
.with_variables(&self.variables)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> {
|
||||
metadata: &'a Metadata,
|
||||
case: &'a Case,
|
||||
leader_node: &'a Leader::Blockchain,
|
||||
follower_node: &'a Follower::Blockchain,
|
||||
leader_state: CaseState<Leader>,
|
||||
follower_state: CaseState<Follower>,
|
||||
}
|
||||
|
||||
impl<'a, L, F> CaseDriver<'a, L, F>
|
||||
where
|
||||
L: Platform,
|
||||
F: Platform,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
metadata: &'a Metadata,
|
||||
case: &'a Case,
|
||||
leader_node: &'a L::Blockchain,
|
||||
follower_node: &'a F::Blockchain,
|
||||
leader_state: CaseState<L>,
|
||||
follower_state: CaseState<F>,
|
||||
) -> CaseDriver<'a, L, F> {
|
||||
Self {
|
||||
metadata,
|
||||
case,
|
||||
leader_node,
|
||||
follower_node,
|
||||
leader_state,
|
||||
follower_state,
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", name = "Executing Case", skip_all)]
|
||||
pub async fn execute(&mut self) -> anyhow::Result<usize> {
|
||||
let mut steps_executed = 0;
|
||||
for (step_idx, step) in self
|
||||
.case
|
||||
.steps_iterator()
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (StepIdx::new(idx), v))
|
||||
{
|
||||
let (leader_step_output, follower_step_output) = try_join!(
|
||||
self.leader_state
|
||||
.handle_step(self.metadata, &step, self.leader_node)
|
||||
.instrument(info_span!(
|
||||
"Handling Step",
|
||||
%step_idx,
|
||||
target = "Leader",
|
||||
)),
|
||||
self.follower_state
|
||||
.handle_step(self.metadata, &step, self.follower_node)
|
||||
.instrument(info_span!(
|
||||
"Handling Step",
|
||||
%step_idx,
|
||||
target = "Follower",
|
||||
))
|
||||
)?;
|
||||
|
||||
match (leader_step_output, follower_step_output) {
|
||||
(StepOutput::FunctionCall(..), StepOutput::FunctionCall(..)) => {
|
||||
// TODO: We need to actually work out how/if we will compare the diff between
|
||||
// the leader and the follower. The diffs are almost guaranteed to be different
|
||||
// from leader and follower and therefore without an actual strategy for this
|
||||
// we have something that's guaranteed to fail. Even a simple call to some
|
||||
// contract will produce two non-equal diffs because on the leader the contract
|
||||
// has address X and on the follower it has address Y. On the leader contract X
|
||||
// contains address A in the state and on the follower it contains address B. So
|
||||
// this isn't exactly a straightforward thing to do and I'm not even sure that
|
||||
// it's possible to do. Once we have an actual strategy for doing the diffs we
|
||||
// will implement it here. Until then, this remains empty.
|
||||
}
|
||||
(StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {}
|
||||
(StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {}
|
||||
_ => unreachable!("The two step outputs can not be of a different kind"),
|
||||
}
|
||||
|
||||
steps_executed += 1;
|
||||
}
|
||||
|
||||
Ok(steps_executed)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum StepOutput {
|
||||
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
|
||||
BalanceAssertion,
|
||||
StorageEmptyAssertion,
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
//! A wrapper around the compiler which allows for caching of compilation artifacts so that they can
|
||||
//! be reused between runs.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use futures::FutureExt;
|
||||
use revive_dt_common::{iterators::FilesWithExtensionIterator, types::CompilerIdentifier};
|
||||
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
|
||||
|
||||
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use revive_dt_report::ExecutionSpecificReporter;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
use tracing::{Instrument, debug, debug_span, instrument};
|
||||
|
||||
pub struct CachedCompiler<'a> {
|
||||
/// The cache that stores the compiled contracts.
|
||||
artifacts_cache: ArtifactsCache,
|
||||
|
||||
/// This is a mechanism that the cached compiler uses so that if multiple compilation requests
|
||||
/// come in for the same contract we never compile all of them and only compile it once and all
|
||||
/// other tasks that request this same compilation concurrently get the cached version.
|
||||
cache_key_lock: RwLock<HashMap<CacheKey<'a>, Arc<Mutex<()>>>>,
|
||||
}
|
||||
|
||||
impl<'a> CachedCompiler<'a> {
|
||||
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
|
||||
let mut cache = ArtifactsCache::new(path);
|
||||
if invalidate_cache {
|
||||
cache = cache
|
||||
.with_invalidated_cache()
|
||||
.await
|
||||
.context("Failed to invalidate compilation cache directory")?;
|
||||
}
|
||||
Ok(Self {
|
||||
artifacts_cache: cache,
|
||||
cache_key_lock: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Compiles or gets the compilation artifacts from the cache.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
%mode,
|
||||
platform = %platform.platform_identifier()
|
||||
),
|
||||
err
|
||||
)]
|
||||
pub async fn compile_contracts(
|
||||
&self,
|
||||
metadata: &'a Metadata,
|
||||
metadata_file_path: &'a Path,
|
||||
mode: Cow<'a, Mode>,
|
||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||
compiler: &dyn SolidityCompiler,
|
||||
platform: &dyn Platform,
|
||||
reporter: &ExecutionSpecificReporter,
|
||||
) -> Result<CompilerOutput> {
|
||||
let cache_key = CacheKey {
|
||||
compiler_identifier: platform.compiler_identifier(),
|
||||
compiler_version: compiler.version().clone(),
|
||||
metadata_file_path,
|
||||
solc_mode: mode.clone(),
|
||||
};
|
||||
|
||||
let compilation_callback = || {
|
||||
async move {
|
||||
compile_contracts(
|
||||
metadata
|
||||
.directory()
|
||||
.context("Failed to get metadata directory while preparing compilation")?,
|
||||
metadata
|
||||
.files_to_compile()
|
||||
.context("Failed to enumerate files to compile from metadata")?,
|
||||
&mode,
|
||||
deployed_libraries,
|
||||
compiler,
|
||||
reporter,
|
||||
)
|
||||
.map(|compilation_result| compilation_result.map(CacheValue::new))
|
||||
.await
|
||||
}
|
||||
.instrument(debug_span!(
|
||||
"Running compilation for the cache key",
|
||||
cache_key.compiler_identifier = %cache_key.compiler_identifier,
|
||||
cache_key.compiler_version = %cache_key.compiler_version,
|
||||
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
|
||||
cache_key.solc_mode = %cache_key.solc_mode,
|
||||
))
|
||||
};
|
||||
|
||||
let compiled_contracts = match deployed_libraries {
|
||||
// If deployed libraries have been specified then we will re-compile the contract as it
|
||||
// means that linking is required in this case.
|
||||
Some(_) => {
|
||||
debug!("Deployed libraries defined, recompilation must take place");
|
||||
debug!("Cache miss");
|
||||
compilation_callback()
|
||||
.await
|
||||
.context("Compilation callback for deployed libraries failed")?
|
||||
.compiler_output
|
||||
}
|
||||
// If no deployed libraries are specified then we can follow the cached flow and attempt
|
||||
// to lookup the compilation artifacts in the cache.
|
||||
None => {
|
||||
debug!("Deployed libraries undefined, attempting to make use of cache");
|
||||
|
||||
// Lock this specific cache key such that we do not get inconsistent state. We want
|
||||
// that when multiple cases come in asking for the compilation artifacts then they
|
||||
// don't all trigger a compilation if there's a cache miss. Hence, the lock here.
|
||||
let read_guard = self.cache_key_lock.read().await;
|
||||
let mutex = match read_guard.get(&cache_key).cloned() {
|
||||
Some(value) => {
|
||||
drop(read_guard);
|
||||
value
|
||||
}
|
||||
None => {
|
||||
drop(read_guard);
|
||||
self.cache_key_lock
|
||||
.write()
|
||||
.await
|
||||
.entry(cache_key.clone())
|
||||
.or_default()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
match self.artifacts_cache.get(&cache_key).await {
|
||||
Some(cache_value) => {
|
||||
if deployed_libraries.is_some() {
|
||||
reporter
|
||||
.report_post_link_contracts_compilation_succeeded_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path(),
|
||||
true,
|
||||
None,
|
||||
cache_value.compiler_output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
} else {
|
||||
reporter
|
||||
.report_pre_link_contracts_compilation_succeeded_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path(),
|
||||
true,
|
||||
None,
|
||||
cache_value.compiler_output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
cache_value.compiler_output
|
||||
}
|
||||
None => {
|
||||
let compiler_output = compilation_callback()
|
||||
.await
|
||||
.context("Compilation callback failed (cache miss path)")?
|
||||
.compiler_output;
|
||||
self.artifacts_cache
|
||||
.insert(
|
||||
&cache_key,
|
||||
&CacheValue {
|
||||
compiler_output: compiler_output.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to write the cached value of the compilation artifacts",
|
||||
)?;
|
||||
compiler_output
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(compiled_contracts)
|
||||
}
|
||||
}
|
||||
|
||||
async fn compile_contracts(
|
||||
metadata_directory: impl AsRef<Path>,
|
||||
mut files_to_compile: impl Iterator<Item = PathBuf>,
|
||||
mode: &Mode,
|
||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||
compiler: &dyn SolidityCompiler,
|
||||
reporter: &ExecutionSpecificReporter,
|
||||
) -> Result<CompilerOutput> {
|
||||
// Puts a limit on how many compilations we can perform at any given instance which helps us
|
||||
// with some of the errors we've been seeing with high concurrency on MacOS (we have not tried
|
||||
// it on Linux so we don't know if these issues also persist there or not.)
|
||||
static SPAWN_GATE: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(5));
|
||||
let _permit = SPAWN_GATE.acquire().await?;
|
||||
|
||||
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
|
||||
.with_allowed_extension("sol")
|
||||
.with_use_cached_fs(true)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let compilation = Compiler::new()
|
||||
.with_allow_path(metadata_directory)
|
||||
// Handling the modes
|
||||
.with_optimization(mode.optimize_setting)
|
||||
.with_pipeline(mode.pipeline)
|
||||
// Adding the contract sources to the compiler.
|
||||
.try_then(|compiler| {
|
||||
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
|
||||
})?
|
||||
// Adding the deployed libraries to the compiler.
|
||||
.then(|compiler| {
|
||||
deployed_libraries
|
||||
.iter()
|
||||
.flat_map(|value| value.iter())
|
||||
.map(|(instance, (ident, address, abi))| (instance, ident, address, abi))
|
||||
.flat_map(|(_, ident, address, _)| {
|
||||
all_sources_in_dir
|
||||
.iter()
|
||||
.map(move |path| (ident, address, path))
|
||||
})
|
||||
.fold(compiler, |compiler, (ident, address, path)| {
|
||||
compiler.with_library(path, ident.as_str(), *address)
|
||||
})
|
||||
});
|
||||
|
||||
let input = compilation.input().clone();
|
||||
let output = compilation.try_build(compiler).await;
|
||||
|
||||
match (output.as_ref(), deployed_libraries.is_some()) {
|
||||
(Ok(output), true) => {
|
||||
reporter
|
||||
.report_post_link_contracts_compilation_succeeded_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path(),
|
||||
false,
|
||||
input,
|
||||
output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
(Ok(output), false) => {
|
||||
reporter
|
||||
.report_pre_link_contracts_compilation_succeeded_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path(),
|
||||
false,
|
||||
input,
|
||||
output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
(Err(err), true) => {
|
||||
reporter
|
||||
.report_post_link_contracts_compilation_failed_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path().to_path_buf(),
|
||||
input,
|
||||
format!("{err:#}"),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
(Err(err), false) => {
|
||||
reporter
|
||||
.report_pre_link_contracts_compilation_failed_event(
|
||||
compiler.version().clone(),
|
||||
compiler.path().to_path_buf(),
|
||||
input,
|
||||
format!("{err:#}"),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
struct ArtifactsCache {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl ArtifactsCache {
|
||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn with_invalidated_cache(self) -> Result<Self> {
|
||||
cacache::clear(self.path.as_path())
|
||||
.await
|
||||
.map_err(Into::<Error>::into)
|
||||
.with_context(|| format!("Failed to clear cache at {}", self.path.display()))?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn insert(&self, key: &CacheKey<'_>, value: &CacheValue) -> Result<()> {
|
||||
let key = bson::to_vec(key).context("Failed to serialize cache key (bson)")?;
|
||||
let value = bson::to_vec(value).context("Failed to serialize cache value (bson)")?;
|
||||
cacache::write(self.path.as_path(), key.encode_hex(), value)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write cache entry under {}", self.path.display())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(&self, key: &CacheKey<'_>) -> Option<CacheValue> {
|
||||
let key = bson::to_vec(key).ok()?;
|
||||
let value = cacache::read(self.path.as_path(), key.encode_hex())
|
||||
.await
|
||||
.ok()?;
|
||||
let value = bson::from_slice::<CacheValue>(&value).ok()?;
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
struct CacheKey<'a> {
|
||||
/// The identifier of the used compiler.
|
||||
compiler_identifier: CompilerIdentifier,
|
||||
|
||||
/// The version of the compiler that was used to compile the artifacts.
|
||||
compiler_version: Version,
|
||||
|
||||
/// The path of the metadata file that the compilation artifacts are for.
|
||||
metadata_file_path: &'a Path,
|
||||
|
||||
/// The mode that the compilation artifacts where compiled with.
|
||||
solc_mode: Cow<'a, Mode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct CacheValue {
|
||||
/// The compiler output from the compilation run.
|
||||
compiler_output: CompilerOutput,
|
||||
}
|
||||
|
||||
impl CacheValue {
|
||||
pub fn new(compiler_output: CompilerOutput) -> Self {
|
||||
Self { compiler_output }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod cached_compiler;
|
||||
mod pool;
|
||||
mod test;
|
||||
|
||||
pub use cached_compiler::*;
|
||||
pub use pool::*;
|
||||
pub use test::*;
|
||||
@@ -0,0 +1,59 @@
|
||||
//! This crate implements concurrent handling of testing node.
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
/// The node pool starts one or more [Node] which then can be accessed
|
||||
/// in a round robbin fashion.
|
||||
pub struct NodePool {
|
||||
next: AtomicUsize,
|
||||
nodes: Vec<Box<dyn EthereumNode + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl NodePool {
|
||||
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
|
||||
pub async fn new(context: Context, platform: &dyn Platform) -> anyhow::Result<Self> {
|
||||
let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
|
||||
let nodes = concurrency_configuration.number_of_nodes;
|
||||
|
||||
let mut handles = Vec::with_capacity(nodes);
|
||||
for _ in 0..nodes {
|
||||
let context = context.clone();
|
||||
handles.push(platform.new_node(context)?);
|
||||
}
|
||||
|
||||
let mut nodes = Vec::with_capacity(nodes);
|
||||
for handle in handles {
|
||||
nodes.push(
|
||||
handle
|
||||
.join()
|
||||
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))
|
||||
.context("Failed to join node spawn thread")?
|
||||
.context("Node failed to spawn")?,
|
||||
);
|
||||
}
|
||||
|
||||
let pre_transactions_tasks = nodes
|
||||
.iter_mut()
|
||||
.map(|node| node.pre_transactions())
|
||||
.collect::<Vec<_>>();
|
||||
futures::future::try_join_all(pre_transactions_tasks)
|
||||
.await
|
||||
.context("Failed to run the pre-transactions task")?;
|
||||
|
||||
Ok(Self {
|
||||
nodes,
|
||||
next: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a handle to the next node.
|
||||
pub fn round_robbin(&self) -> &dyn EthereumNode {
|
||||
let current = self.next.fetch_add(1, Ordering::SeqCst) % self.nodes.len();
|
||||
self.nodes.get(current).unwrap().as_ref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use indexmap::{IndexMap, indexmap};
|
||||
use revive_dt_common::types::PlatformIdentifier;
|
||||
use revive_dt_config::Context;
|
||||
use revive_dt_format::corpus::Corpus;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use revive_dt_compiler::Mode;
|
||||
use revive_dt_compiler::SolidityCompiler;
|
||||
use revive_dt_format::{
|
||||
case::{Case, CaseIdx},
|
||||
metadata::MetadataFile,
|
||||
};
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use revive_dt_report::{ExecutionSpecificReporter, Report, Reporter, TestCaseStatus};
|
||||
use revive_dt_report::{TestSpecificReporter, TestSpecifier};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::Platform;
|
||||
use crate::helpers::NodePool;
|
||||
|
||||
pub async fn create_test_definitions_stream<'a>(
|
||||
// This is only required for creating the compiler objects and is not used anywhere else in the
|
||||
// function.
|
||||
context: &Context,
|
||||
corpus: &'a Corpus,
|
||||
platforms_and_nodes: &'a BTreeMap<PlatformIdentifier, (&dyn Platform, NodePool)>,
|
||||
only_execute_failed_tests: Option<&Report>,
|
||||
reporter: Reporter,
|
||||
) -> impl Stream<Item = TestDefinition<'a>> {
|
||||
let cloned_reporter = reporter.clone();
|
||||
stream::iter(
|
||||
corpus
|
||||
.cases_iterator()
|
||||
.inspect(move |(metadata_file, ..)| {
|
||||
cloned_reporter
|
||||
.report_metadata_file_discovery_event(
|
||||
metadata_file.metadata_file_path.clone(),
|
||||
metadata_file.content.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.map(move |(metadata_file, case_idx, case, mode)| {
|
||||
let reporter = reporter.clone();
|
||||
|
||||
(
|
||||
metadata_file,
|
||||
case_idx,
|
||||
case,
|
||||
mode.clone(),
|
||||
reporter.test_specific_reporter(Arc::new(TestSpecifier {
|
||||
solc_mode: mode.as_ref().clone(),
|
||||
metadata_file_path: metadata_file.metadata_file_path.clone(),
|
||||
case_idx: CaseIdx::new(case_idx),
|
||||
})),
|
||||
)
|
||||
})
|
||||
// Inform the reporter of each one of the test cases that were discovered which we expect to
|
||||
// run.
|
||||
.inspect(|(_, _, _, _, reporter)| {
|
||||
reporter
|
||||
.report_test_case_discovery_event()
|
||||
.expect("Can't fail");
|
||||
}),
|
||||
)
|
||||
// Creating the Test Definition objects from all of the various objects we have and creating
|
||||
// their required dependencies (e.g., compiler).
|
||||
.filter_map(
|
||||
move |(metadata_file, case_idx, case, mode, reporter)| async move {
|
||||
let mut platforms = BTreeMap::new();
|
||||
for (platform, node_pool) in platforms_and_nodes.values() {
|
||||
let node = node_pool.round_robbin();
|
||||
let compiler = platform
|
||||
.new_compiler(context.clone(), mode.version.clone().map(Into::into))
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
platform_identifier = %platform.platform_identifier(),
|
||||
"Failed to instantiate the compiler"
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
reporter
|
||||
.report_node_assigned_event(
|
||||
node.id(),
|
||||
platform.platform_identifier(),
|
||||
node.connection_string(),
|
||||
)
|
||||
.expect("Can't fail");
|
||||
|
||||
let reporter =
|
||||
reporter.execution_specific_reporter(node.id(), platform.platform_identifier());
|
||||
|
||||
platforms.insert(
|
||||
platform.platform_identifier(),
|
||||
TestPlatformInformation {
|
||||
platform: *platform,
|
||||
node,
|
||||
compiler,
|
||||
reporter,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Some(TestDefinition {
|
||||
/* Metadata file information */
|
||||
metadata: metadata_file,
|
||||
metadata_file_path: metadata_file.metadata_file_path.as_path(),
|
||||
|
||||
/* Mode Information */
|
||||
mode: mode.clone(),
|
||||
|
||||
/* Case Information */
|
||||
case_idx: CaseIdx::new(case_idx),
|
||||
case,
|
||||
|
||||
/* Platform and Node Assignment Information */
|
||||
platforms,
|
||||
|
||||
/* Reporter */
|
||||
reporter,
|
||||
})
|
||||
},
|
||||
)
|
||||
// Filter out the test cases which are incompatible or that can't run in the current setup.
|
||||
.filter_map(move |test| async move {
|
||||
match test.check_compatibility(only_execute_failed_tests) {
|
||||
Ok(()) => Some(test),
|
||||
Err((reason, additional_information)) => {
|
||||
debug!(
|
||||
metadata_file_path = %test.metadata.metadata_file_path.display(),
|
||||
case_idx = %test.case_idx,
|
||||
mode = %test.mode,
|
||||
reason,
|
||||
additional_information =
|
||||
serde_json::to_string(&additional_information).unwrap(),
|
||||
"Ignoring Test Case"
|
||||
);
|
||||
test.reporter
|
||||
.report_test_ignored_event(
|
||||
reason.to_string(),
|
||||
additional_information
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into(), v))
|
||||
.collect::<IndexMap<_, _>>(),
|
||||
)
|
||||
.expect("Can't fail");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.inspect(|test| {
|
||||
info!(
|
||||
metadata_file_path = %test.metadata_file_path.display(),
|
||||
case_idx = %test.case_idx,
|
||||
mode = %test.mode,
|
||||
"Created a test case definition"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
/// This is a full description of a differential test to run alongside the full metadata file, the
|
||||
/// specific case to be tested, the platforms that the tests should run on, the specific nodes of
|
||||
/// these platforms that they should run on, the compilers to use, and everything else needed making
|
||||
/// it a complete description.
|
||||
pub struct TestDefinition<'a> {
|
||||
/* Metadata file information */
|
||||
pub metadata: &'a MetadataFile,
|
||||
pub metadata_file_path: &'a Path,
|
||||
|
||||
/* Mode Information */
|
||||
pub mode: Cow<'a, Mode>,
|
||||
|
||||
/* Case Information */
|
||||
pub case_idx: CaseIdx,
|
||||
pub case: &'a Case,
|
||||
|
||||
/* Platform and Node Assignment Information */
|
||||
pub platforms: BTreeMap<PlatformIdentifier, TestPlatformInformation<'a>>,
|
||||
|
||||
/* Reporter */
|
||||
pub reporter: TestSpecificReporter,
|
||||
}
|
||||
|
||||
impl<'a> TestDefinition<'a> {
|
||||
/// Checks if this test can be ran with the current configuration.
|
||||
pub fn check_compatibility(
|
||||
&self,
|
||||
only_execute_failed_tests: Option<&Report>,
|
||||
) -> TestCheckFunctionResult {
|
||||
self.check_metadata_file_ignored()?;
|
||||
self.check_case_file_ignored()?;
|
||||
self.check_target_compatibility()?;
|
||||
self.check_evm_version_compatibility()?;
|
||||
self.check_compiler_compatibility()?;
|
||||
self.check_ignore_succeeded(only_execute_failed_tests)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the metadata file is ignored or not.
|
||||
fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult {
|
||||
if self.metadata.ignore.is_some_and(|ignore| ignore) {
|
||||
Err(("Metadata file is ignored.", indexmap! {}))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the case file is ignored or not.
|
||||
fn check_case_file_ignored(&self) -> TestCheckFunctionResult {
|
||||
if self.case.ignore.is_some_and(|ignore| ignore) {
|
||||
Err(("Case is ignored.", indexmap! {}))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the platforms all support the desired targets in the metadata file.
|
||||
fn check_target_compatibility(&self) -> TestCheckFunctionResult {
|
||||
let mut error_map = indexmap! {
|
||||
"test_desired_targets" => json!(self.metadata.targets.as_ref()),
|
||||
};
|
||||
let mut is_allowed = true;
|
||||
for (_, platform_information) in self.platforms.iter() {
|
||||
let is_allowed_for_platform = match self.metadata.targets.as_ref() {
|
||||
None => true,
|
||||
Some(required_vm_identifiers) => {
|
||||
required_vm_identifiers.contains(&platform_information.platform.vm_identifier())
|
||||
}
|
||||
};
|
||||
is_allowed &= is_allowed_for_platform;
|
||||
error_map.insert(
|
||||
platform_information.platform.platform_identifier().into(),
|
||||
json!(is_allowed_for_platform),
|
||||
);
|
||||
}
|
||||
|
||||
if is_allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((
|
||||
"One of the platforms do do not support the targets allowed by the test.",
|
||||
error_map,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for the compatibility of the EVM version with the platforms specified.
|
||||
fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult {
|
||||
let Some(evm_version_requirement) = self.metadata.required_evm_version else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut error_map = indexmap! {
|
||||
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
|
||||
};
|
||||
let mut is_allowed = true;
|
||||
for (_, platform_information) in self.platforms.iter() {
|
||||
let is_allowed_for_platform =
|
||||
evm_version_requirement.matches(&platform_information.node.evm_version());
|
||||
is_allowed &= is_allowed_for_platform;
|
||||
error_map.insert(
|
||||
platform_information.platform.platform_identifier().into(),
|
||||
json!(is_allowed_for_platform),
|
||||
);
|
||||
}
|
||||
|
||||
if is_allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((
|
||||
"EVM version is incompatible for the platforms specified",
|
||||
error_map,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the platforms compilers support the mode that the test is for.
|
||||
fn check_compiler_compatibility(&self) -> TestCheckFunctionResult {
|
||||
let mut error_map = indexmap! {
|
||||
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
|
||||
};
|
||||
let mut is_allowed = true;
|
||||
for (_, platform_information) in self.platforms.iter() {
|
||||
let is_allowed_for_platform = platform_information
|
||||
.compiler
|
||||
.supports_mode(self.mode.optimize_setting, self.mode.pipeline);
|
||||
is_allowed &= is_allowed_for_platform;
|
||||
error_map.insert(
|
||||
platform_information.platform.platform_identifier().into(),
|
||||
json!(is_allowed_for_platform),
|
||||
);
|
||||
}
|
||||
|
||||
if is_allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((
|
||||
"Compilers do not support this mode either for the provided platforms.",
|
||||
error_map,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the test case should be executed or not based on the passed report and whether the
|
||||
/// user has instructed the tool to ignore the already succeeding test cases.
|
||||
fn check_ignore_succeeded(
|
||||
&self,
|
||||
only_execute_failed_tests: Option<&Report>,
|
||||
) -> TestCheckFunctionResult {
|
||||
let Some(report) = only_execute_failed_tests else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let test_case_status = report
|
||||
.execution_information
|
||||
.get(&(self.metadata_file_path.to_path_buf().into()))
|
||||
.and_then(|obj| obj.case_reports.get(&self.case_idx))
|
||||
.and_then(|obj| obj.mode_execution_reports.get(&self.mode))
|
||||
.and_then(|obj| obj.status.as_ref());
|
||||
|
||||
match test_case_status {
|
||||
Some(TestCaseStatus::Failed { .. }) => Ok(()),
|
||||
Some(TestCaseStatus::Ignored { .. }) => Err((
|
||||
"Ignored since it was ignored in a previous run",
|
||||
indexmap! {},
|
||||
)),
|
||||
Some(TestCaseStatus::Succeeded { .. }) => {
|
||||
Err(("Ignored since it succeeded in a prior run", indexmap! {}))
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPlatformInformation<'a> {
|
||||
pub platform: &'a dyn Platform,
|
||||
pub node: &'a dyn EthereumNode,
|
||||
pub compiler: Box<dyn SolidityCompiler>,
|
||||
pub reporter: ExecutionSpecificReporter,
|
||||
}
|
||||
|
||||
type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>;
|
||||
+464
-27
@@ -3,45 +3,482 @@
|
||||
//! This crate defines the testing configuration and
|
||||
//! provides a helper utility to execute tests.
|
||||
|
||||
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
|
||||
use revive_dt_config::TestingPlatform;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
|
||||
use alloy::genesis::Genesis;
|
||||
use anyhow::Context as _;
|
||||
use revive_dt_common::types::*;
|
||||
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_node::{
|
||||
Node, node_implementations::geth::GethNode,
|
||||
node_implementations::lighthouse_geth::LighthouseGethNode,
|
||||
node_implementations::substrate::SubstrateNode, node_implementations::zombienet::ZombienetNode,
|
||||
};
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use tracing::info;
|
||||
|
||||
pub mod driver;
|
||||
|
||||
/// One platform can be tested differentially against another.
|
||||
///
|
||||
/// For this we need a blockchain node implementation and a compiler.
|
||||
/// A trait that describes the interface for the platforms that are supported by the tool.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub trait Platform {
|
||||
type Blockchain: EthereumNode + Node + ResolverApi;
|
||||
type Compiler: SolidityCompiler;
|
||||
/// Returns the identifier of this platform. This is a combination of the node and the compiler
|
||||
/// used.
|
||||
fn platform_identifier(&self) -> PlatformIdentifier;
|
||||
|
||||
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].
|
||||
fn config_id() -> TestingPlatform;
|
||||
/// Returns a full identifier for the platform.
|
||||
fn full_identifier(&self) -> (NodeIdentifier, VmIdentifier, CompilerIdentifier) {
|
||||
(
|
||||
self.node_identifier(),
|
||||
self.vm_identifier(),
|
||||
self.compiler_identifier(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the identifier of the node used.
|
||||
fn node_identifier(&self) -> NodeIdentifier;
|
||||
|
||||
/// Returns the identifier of the vm used.
|
||||
fn vm_identifier(&self) -> VmIdentifier;
|
||||
|
||||
/// Returns the identifier of the compiler used.
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier;
|
||||
|
||||
/// Creates a new node for the platform by spawning a new thread, creating the node object,
|
||||
/// initializing it, spawning it, and waiting for it to start up.
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>>;
|
||||
|
||||
/// Creates a new compiler for the provided platform
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>>;
|
||||
|
||||
/// Exports the genesis/chainspec for the node.
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Geth;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct GethEvmSolcPlatform;
|
||||
|
||||
impl Platform for Geth {
|
||||
type Blockchain = geth::GethNode;
|
||||
type Compiler = solc::Solc;
|
||||
impl Platform for GethEvmSolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::GethEvmSolc
|
||||
}
|
||||
|
||||
fn config_id() -> TestingPlatform {
|
||||
TestingPlatform::Geth
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::Geth
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::Evm
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Solc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = GethNode::new(context);
|
||||
let node = spawn_node::<GethNode>(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Solc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let genesis = AsRef::<GenesisConfiguration>::as_ref(&context).genesis()?;
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
|
||||
let node_genesis = GethNode::node_genesis(genesis.clone(), &wallet);
|
||||
serde_json::to_value(node_genesis)
|
||||
.context("Failed to convert node genesis to a serde_value")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Kitchensink;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct LighthouseGethEvmSolcPlatform;
|
||||
|
||||
impl Platform for Kitchensink {
|
||||
type Blockchain = KitchensinkNode;
|
||||
type Compiler = revive_resolc::Resolc;
|
||||
impl Platform for LighthouseGethEvmSolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::LighthouseGethEvmSolc
|
||||
}
|
||||
|
||||
fn config_id() -> TestingPlatform {
|
||||
TestingPlatform::Kitchensink
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::LighthouseGeth
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::Evm
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Solc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = LighthouseGethNode::new(context);
|
||||
let node = spawn_node::<LighthouseGethNode>(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Solc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let genesis = AsRef::<GenesisConfiguration>::as_ref(&context).genesis()?;
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
|
||||
let node_genesis = LighthouseGethNode::node_genesis(genesis.clone(), &wallet);
|
||||
serde_json::to_value(node_genesis)
|
||||
.context("Failed to convert node genesis to a serde_value")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct ReviveDevNodePolkavmResolcPlatform;
|
||||
|
||||
impl Platform for ReviveDevNodePolkavmResolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc
|
||||
}
|
||||
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::ReviveDevNode
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::PolkaVM
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Resolc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||
|
||||
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
|
||||
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
|
||||
|
||||
let eth_rpc_connection_strings = revive_dev_node_configuration.existing_rpc_url.clone();
|
||||
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
revive_dev_node_path,
|
||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||
Some(revive_dev_node_consensus),
|
||||
context,
|
||||
ð_rpc_connection_strings,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Resolc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.as_path();
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
let export_chainspec_command = SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND;
|
||||
|
||||
SubstrateNode::node_genesis(revive_dev_node_path, export_chainspec_command, &wallet)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct ReviveDevNodeRevmSolcPlatform;
|
||||
|
||||
impl Platform for ReviveDevNodeRevmSolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc
|
||||
}
|
||||
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::ReviveDevNode
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::Evm
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Solc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||
|
||||
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
|
||||
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
|
||||
|
||||
let eth_rpc_connection_strings = revive_dev_node_configuration.existing_rpc_url.clone();
|
||||
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
revive_dev_node_path,
|
||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||
Some(revive_dev_node_consensus),
|
||||
context,
|
||||
ð_rpc_connection_strings,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Solc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.as_path();
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
let export_chainspec_command = SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND;
|
||||
|
||||
SubstrateNode::node_genesis(revive_dev_node_path, export_chainspec_command, &wallet)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct ZombienetPolkavmResolcPlatform;
|
||||
|
||||
impl Platform for ZombienetPolkavmResolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::ZombienetPolkavmResolc
|
||||
}
|
||||
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::Zombienet
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::PolkaVM
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Resolc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = ZombienetNode::new(polkadot_parachain_path, context);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Resolc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.as_path();
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
|
||||
ZombienetNode::node_genesis(polkadot_parachain_path, &wallet)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||
pub struct ZombienetRevmSolcPlatform;
|
||||
|
||||
impl Platform for ZombienetRevmSolcPlatform {
|
||||
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||
PlatformIdentifier::ZombienetRevmSolc
|
||||
}
|
||||
|
||||
fn node_identifier(&self) -> NodeIdentifier {
|
||||
NodeIdentifier::Zombienet
|
||||
}
|
||||
|
||||
fn vm_identifier(&self) -> VmIdentifier {
|
||||
VmIdentifier::Evm
|
||||
}
|
||||
|
||||
fn compiler_identifier(&self) -> CompilerIdentifier {
|
||||
CompilerIdentifier::Solc
|
||||
}
|
||||
|
||||
fn new_node(
|
||||
&self,
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = ZombienetNode::new(polkadot_parachain_path, context);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_compiler(
|
||||
&self,
|
||||
context: Context,
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Solc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
|
||||
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
|
||||
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.as_path();
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
|
||||
ZombienetNode::node_genesis(polkadot_parachain_path, &wallet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlatformIdentifier> for Box<dyn Platform> {
|
||||
fn from(value: PlatformIdentifier) -> Self {
|
||||
match value {
|
||||
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||
Box::new(LighthouseGethEvmSolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||
Box::new(ReviveDevNodePolkavmResolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ZombienetPolkavmResolc => {
|
||||
Box::new(ZombienetPolkavmResolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ZombienetRevmSolc => Box::new(ZombienetRevmSolcPlatform) as Box<_>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlatformIdentifier> for &dyn Platform {
|
||||
fn from(value: PlatformIdentifier) -> Self {
|
||||
match value {
|
||||
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||
&LighthouseGethEvmSolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||
&ReviveDevNodePolkavmResolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||
&ReviveDevNodeRevmSolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ZombienetPolkavmResolc => {
|
||||
&ZombienetPolkavmResolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ZombienetRevmSolc => &ZombienetRevmSolcPlatform as &dyn Platform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_node<T: Node + EthereumNode + Send + Sync>(
|
||||
mut node: T,
|
||||
genesis: Genesis,
|
||||
) -> anyhow::Result<T> {
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawning node"
|
||||
);
|
||||
node.spawn(genesis)
|
||||
.context("Failed to spawn node process")?;
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawned node"
|
||||
);
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
+61
-954
File diff suppressed because it is too large
Load Diff
@@ -14,15 +14,14 @@ revive-dt-common = { workspace = true }
|
||||
revive-common = { workspace = true }
|
||||
|
||||
alloy = { workspace = true }
|
||||
alloy-primitives = { workspace = true }
|
||||
alloy-sol-types = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -1,38 +1,60 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
||||
|
||||
use crate::{
|
||||
input::{Expected, Step},
|
||||
mode::ParsedMode,
|
||||
use revive_dt_common::{
|
||||
macros::define_wrapper_type,
|
||||
types::{Mode, ParsedMode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
use crate::steps::*;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
|
||||
pub struct Case {
|
||||
/// An optional name of the test case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// An optional comment on the case which has no impact on the execution in any way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
///
|
||||
/// If this is provided then it takes higher priority than the modes specified in the metadata
|
||||
/// file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub modes: Option<Vec<ParsedMode>>,
|
||||
|
||||
/// The set of steps to run as part of this test case.
|
||||
#[serde(rename = "inputs")]
|
||||
pub steps: Vec<Step>,
|
||||
|
||||
/// An optional name of the group of tests that this test belongs to.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
|
||||
/// An optional set of expectations and assertions to make about the transaction after it ran.
|
||||
///
|
||||
/// If this is not specified then the only assertion that will be ran is that the transaction
|
||||
/// was successful.
|
||||
///
|
||||
/// This expectation that's on the case itself will be attached to the final step of the case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expected: Option<Expected>,
|
||||
|
||||
/// An optional boolean which defines if the case as a whole should be ignored. If null then the
|
||||
/// case will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
}
|
||||
|
||||
impl Case {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
pub fn steps_iterator(&self) -> impl Iterator<Item = Step> {
|
||||
let steps_len = self.steps.len();
|
||||
self.steps
|
||||
@@ -61,10 +83,28 @@ impl Case {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn steps_iterator_for_benchmarks(
|
||||
&self,
|
||||
default_repeat_count: usize,
|
||||
) -> Box<dyn Iterator<Item = Step> + '_> {
|
||||
let contains_repeat = self
|
||||
.steps_iterator()
|
||||
.any(|step| matches!(&step, Step::Repeat(..)));
|
||||
if contains_repeat {
|
||||
Box::new(self.steps_iterator()) as Box<_>
|
||||
} else {
|
||||
Box::new(std::iter::once(Step::Repeat(Box::new(RepeatStep {
|
||||
comment: None,
|
||||
repeat: default_repeat_count,
|
||||
steps: self.steps_iterator().collect(),
|
||||
})))) as Box<_>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn solc_modes(&self) -> Vec<Mode> {
|
||||
match &self.modes {
|
||||
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
||||
None => Mode::all().collect(),
|
||||
None => Mode::all().cloned().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+180
-109
@@ -1,129 +1,200 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use revive_dt_common::iterators::FilesWithExtensionIterator;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, info};
|
||||
use itertools::Itertools;
|
||||
use revive_dt_common::{
|
||||
iterators::{EitherIter, FilesWithExtensionIterator},
|
||||
types::{Mode, ParsedMode, ParsedTestSpecifier},
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::metadata::{Metadata, MetadataFile};
|
||||
use crate::{
|
||||
case::{Case, CaseIdx},
|
||||
metadata::{Metadata, MetadataFile},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Corpus {
|
||||
SinglePath { name: String, path: PathBuf },
|
||||
MultiplePaths { name: String, paths: Vec<PathBuf> },
|
||||
#[derive(Default)]
|
||||
pub struct Corpus {
|
||||
test_specifiers: HashMap<ParsedTestSpecifier, Vec<PathBuf>>,
|
||||
metadata_files: HashMap<PathBuf, MetadataFile>,
|
||||
}
|
||||
|
||||
impl Corpus {
|
||||
pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
let mut corpus = File::open(file_path.as_ref())
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?;
|
||||
|
||||
for path in corpus.paths_iter_mut() {
|
||||
*path = file_path
|
||||
.as_ref()
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
|
||||
})?
|
||||
.canonicalize()
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to canonicalize path to corpus '{}': {error}",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
.join(path.as_path())
|
||||
}
|
||||
|
||||
Ok(corpus)
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
|
||||
let mut tests = self
|
||||
.paths_iter()
|
||||
.flat_map(|root_path| {
|
||||
if !root_path.is_dir() {
|
||||
Box::new(std::iter::once(root_path.to_path_buf()))
|
||||
as Box<dyn Iterator<Item = _>>
|
||||
} else {
|
||||
Box::new(
|
||||
FilesWithExtensionIterator::new(root_path)
|
||||
.with_use_cached_fs(true)
|
||||
.with_allowed_extension("sol")
|
||||
.with_allowed_extension("json"),
|
||||
)
|
||||
pub fn with_test_specifier(
|
||||
mut self,
|
||||
test_specifier: ParsedTestSpecifier,
|
||||
) -> anyhow::Result<Self> {
|
||||
match &test_specifier {
|
||||
ParsedTestSpecifier::FileOrDirectory {
|
||||
metadata_or_directory_file_path: metadata_file_path,
|
||||
}
|
||||
| ParsedTestSpecifier::Case {
|
||||
metadata_file_path, ..
|
||||
}
|
||||
| ParsedTestSpecifier::CaseWithMode {
|
||||
metadata_file_path, ..
|
||||
} => {
|
||||
let metadata_files = enumerate_metadata_files(metadata_file_path);
|
||||
self.test_specifiers.insert(
|
||||
test_specifier,
|
||||
metadata_files
|
||||
.iter()
|
||||
.map(|metadata_file| metadata_file.metadata_file_path.clone())
|
||||
.collect(),
|
||||
);
|
||||
for metadata_file in metadata_files.into_iter() {
|
||||
self.metadata_files
|
||||
.insert(metadata_file.metadata_file_path.clone(), metadata_file);
|
||||
}
|
||||
.map(move |metadata_file_path| (root_path, metadata_file_path))
|
||||
})
|
||||
.filter_map(|(root_path, metadata_file_path)| {
|
||||
Metadata::try_from_file(&metadata_file_path)
|
||||
.or_else(|| {
|
||||
debug!(
|
||||
discovered_from = %root_path.display(),
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
"Skipping file since it doesn't contain valid metadata"
|
||||
);
|
||||
None
|
||||
})
|
||||
.map(|metadata| MetadataFile {
|
||||
metadata_file_path,
|
||||
corpus_file_path: root_path.to_path_buf(),
|
||||
content: metadata,
|
||||
})
|
||||
.inspect(|metadata_file| {
|
||||
debug!(
|
||||
metadata_file_path = %metadata_file.relative_path().display(),
|
||||
"Loaded metadata file"
|
||||
}
|
||||
};
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn cases_iterator(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&'_ MetadataFile, CaseIdx, &'_ Case, Cow<'_, Mode>)> + '_ {
|
||||
let mut iterator = Box::new(std::iter::empty())
|
||||
as Box<dyn Iterator<Item = (&'_ MetadataFile, CaseIdx, &'_ Case, Cow<'_, Mode>)> + '_>;
|
||||
|
||||
for (test_specifier, metadata_file_paths) in self.test_specifiers.iter() {
|
||||
for metadata_file_path in metadata_file_paths {
|
||||
let metadata_file = self
|
||||
.metadata_files
|
||||
.get(metadata_file_path)
|
||||
.expect("Must succeed");
|
||||
|
||||
match test_specifier {
|
||||
ParsedTestSpecifier::FileOrDirectory { .. } => {
|
||||
for (case_idx, case) in metadata_file.cases.iter().enumerate() {
|
||||
let case_idx = CaseIdx::new(case_idx);
|
||||
|
||||
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
|
||||
let modes = match modes {
|
||||
Some(modes) => EitherIter::A(
|
||||
ParsedMode::many_to_modes(modes.iter())
|
||||
.map(Cow::<'static, _>::Owned),
|
||||
),
|
||||
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
|
||||
};
|
||||
|
||||
iterator = Box::new(
|
||||
iterator.chain(
|
||||
modes
|
||||
.into_iter()
|
||||
.map(move |mode| (metadata_file, case_idx, case, mode)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ParsedTestSpecifier::Case { case_idx, .. } => {
|
||||
let Some(case) = metadata_file.cases.get(*case_idx) else {
|
||||
warn!(
|
||||
test_specifier = %test_specifier,
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
case_idx = case_idx,
|
||||
case_count = metadata_file.cases.len(),
|
||||
"Specified case not found in metadata file"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let case_idx = CaseIdx::new(*case_idx);
|
||||
|
||||
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
|
||||
let modes = match modes {
|
||||
Some(modes) => EitherIter::A(
|
||||
ParsedMode::many_to_modes(modes.iter())
|
||||
.map(Cow::<'static, Mode>::Owned),
|
||||
),
|
||||
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
|
||||
};
|
||||
|
||||
iterator = Box::new(
|
||||
iterator.chain(
|
||||
modes
|
||||
.into_iter()
|
||||
.map(move |mode| (metadata_file, case_idx, case, mode)),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
|
||||
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
|
||||
info!(
|
||||
len = tests.len(),
|
||||
corpus_name = self.name(),
|
||||
"Found tests in Corpus"
|
||||
);
|
||||
tests
|
||||
}
|
||||
}
|
||||
ParsedTestSpecifier::CaseWithMode { case_idx, mode, .. } => {
|
||||
let Some(case) = metadata_file.cases.get(*case_idx) else {
|
||||
warn!(
|
||||
test_specifier = %test_specifier,
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
case_idx = case_idx,
|
||||
case_count = metadata_file.cases.len(),
|
||||
"Specified case not found in metadata file"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let case_idx = CaseIdx::new(*case_idx);
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Corpus::SinglePath { name, .. } | Corpus::MultiplePaths { name, .. } => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paths_iter(&self) -> impl Iterator<Item = &Path> {
|
||||
match self {
|
||||
Corpus::SinglePath { path, .. } => {
|
||||
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::MultiplePaths { paths, .. } => {
|
||||
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
|
||||
let mode = Cow::Borrowed(mode);
|
||||
iterator = Box::new(iterator.chain(std::iter::once((
|
||||
metadata_file,
|
||||
case_idx,
|
||||
case,
|
||||
mode,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iterator.unique_by(|item| (&item.0.metadata_file_path, item.1, item.3.clone()))
|
||||
}
|
||||
|
||||
pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
|
||||
match self {
|
||||
Corpus::SinglePath { path, .. } => {
|
||||
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::MultiplePaths { paths, .. } => {
|
||||
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_count(&self) -> usize {
|
||||
match self {
|
||||
Corpus::SinglePath { .. } => 1,
|
||||
Corpus::MultiplePaths { paths, .. } => paths.len(),
|
||||
}
|
||||
pub fn metadata_file_count(&self) -> usize {
|
||||
self.metadata_files.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn enumerate_metadata_files(path: impl AsRef<Path>) -> Vec<MetadataFile> {
|
||||
let root_path = path.as_ref();
|
||||
let mut tests = if !root_path.is_dir() {
|
||||
Box::new(std::iter::once(root_path.to_path_buf())) as Box<dyn Iterator<Item = _>>
|
||||
} else {
|
||||
Box::new(
|
||||
FilesWithExtensionIterator::new(root_path)
|
||||
.with_use_cached_fs(true)
|
||||
.with_allowed_extension("sol")
|
||||
.with_allowed_extension("json"),
|
||||
)
|
||||
}
|
||||
.map(move |metadata_file_path| (root_path, metadata_file_path))
|
||||
.filter_map(|(root_path, metadata_file_path)| {
|
||||
Metadata::try_from_file(&metadata_file_path)
|
||||
.or_else(|| {
|
||||
debug!(
|
||||
discovered_from = %root_path.display(),
|
||||
metadata_file_path = %metadata_file_path.display(),
|
||||
"Skipping file since it doesn't contain valid metadata"
|
||||
);
|
||||
None
|
||||
})
|
||||
.map(|metadata| MetadataFile {
|
||||
metadata_file_path,
|
||||
corpus_file_path: root_path.to_path_buf(),
|
||||
content: metadata,
|
||||
})
|
||||
.inspect(|metadata_file| {
|
||||
debug!(
|
||||
metadata_file_path = %metadata_file.relative_path().display(),
|
||||
"Loaded metadata file"
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
|
||||
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
|
||||
tests
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
pub mod case;
|
||||
pub mod corpus;
|
||||
pub mod input;
|
||||
pub mod metadata;
|
||||
pub mod mode;
|
||||
pub mod steps;
|
||||
pub mod traits;
|
||||
|
||||
@@ -8,16 +8,19 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_common::{
|
||||
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
|
||||
types::Mode,
|
||||
cached_fs::read_to_string,
|
||||
iterators::FilesWithExtensionIterator,
|
||||
macros::define_wrapper_type,
|
||||
types::{Mode, ParsedMode, VmIdentifier},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{case::Case, mode::ParsedMode};
|
||||
use crate::case::Case;
|
||||
|
||||
pub const METADATA_FILE_EXTENSION: &str = "json";
|
||||
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
|
||||
@@ -56,30 +59,62 @@ impl Deref for MetadataFile {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
/// A MatterLabs metadata file.
|
||||
///
|
||||
/// This defines the structure that the MatterLabs metadata files follow for defining the tests or
|
||||
/// the workloads.
|
||||
///
|
||||
/// Each metadata file is composed of multiple test cases where each test case is isolated from the
|
||||
/// others and runs in a completely different address space. Each test case is composed of a number
|
||||
/// of steps and assertions that should be performed as part of the test case.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// A comment on the test case that's added for human-readability.
|
||||
/// This is an optional comment on the metadata file which has no impact on the execution in any
|
||||
/// way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// An optional boolean which defines if the metadata file as a whole should be ignored. If null
|
||||
/// then the metadata file will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
|
||||
/// An optional vector of targets that this Metadata file's cases can be executed on. As an
|
||||
/// example, if we wish for the metadata file's cases to only be run on PolkaVM then we'd
|
||||
/// specify a target of "PolkaVM" in here.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub targets: Option<Vec<String>>,
|
||||
pub targets: Option<Vec<VmIdentifier>>,
|
||||
|
||||
/// A vector of the test cases and workloads contained within the metadata file. This is their
|
||||
/// primary description.
|
||||
pub cases: Vec<Case>,
|
||||
|
||||
/// A map of all of the contracts that the test requires to run.
|
||||
///
|
||||
/// This is a map where the key is the name of the contract instance and the value is the
|
||||
/// contract's path and ident in the file.
|
||||
///
|
||||
/// If any contract is to be used by the test then it must be included in here first so that the
|
||||
/// framework is aware of its path, compiles it, and prepares it.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
|
||||
|
||||
/// The set of libraries that this metadata file requires.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub modes: Option<Vec<ParsedMode>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
pub file_path: Option<PathBuf>,
|
||||
|
||||
/// This field specifies an EVM version requirement that the test case has where the test might
|
||||
@@ -87,9 +122,9 @@ pub struct Metadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required_evm_version: Option<EvmVersionRequirement>,
|
||||
|
||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
|
||||
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
|
||||
/// just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts
|
||||
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]
|
||||
/// is just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compiler_directives: Option<CompilationDirectives>,
|
||||
}
|
||||
@@ -99,7 +134,7 @@ impl Metadata {
|
||||
pub fn solc_modes(&self) -> Vec<Mode> {
|
||||
match &self.modes {
|
||||
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
||||
None => Mode::all().collect(),
|
||||
None => Mode::all().cloned().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +167,15 @@ impl Metadata {
|
||||
) in contracts
|
||||
{
|
||||
let alias = alias.clone();
|
||||
let absolute_path = directory.join(contract_source_path).canonicalize()?;
|
||||
let absolute_path = directory
|
||||
.join(contract_source_path)
|
||||
.canonicalize()
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to canonicalize contract source path '{}': {error}",
|
||||
directory.join(contract_source_path).display()
|
||||
)
|
||||
})?;
|
||||
let contract_ident = contract_ident.clone();
|
||||
|
||||
sources.insert(
|
||||
@@ -254,7 +297,7 @@ define_wrapper_type!(
|
||||
///
|
||||
/// Typically, this is used as the key to the "contracts" field of metadata files.
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractInstance(String) impl Display;
|
||||
@@ -265,7 +308,7 @@ define_wrapper_type!(
|
||||
///
|
||||
/// A contract identifier is the name of the contract in the source code.
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContractIdent(String) impl Display;
|
||||
@@ -278,7 +321,9 @@ define_wrapper_type!(
|
||||
/// ```text
|
||||
/// ${path}:${contract_ident}
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ContractPathAndIdent {
|
||||
/// The path of the contract source code relative to the directory containing the metadata file.
|
||||
@@ -355,9 +400,15 @@ impl From<ContractPathAndIdent> for String {
|
||||
}
|
||||
}
|
||||
|
||||
/// An EVM version requirement that the test case has. This gets serialized and
|
||||
/// deserialized from and into [`String`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
/// An EVM version requirement that the test case has. This gets serialized and deserialized from
|
||||
/// and into [`String`]. This follows a simple format of (>=|<=|=|>|<) followed by a string of the
|
||||
/// EVM version.
|
||||
///
|
||||
/// When specified, the framework will only run the test if the node's EVM version matches that
|
||||
/// required by the metadata file.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct EvmVersionRequirement {
|
||||
ordering: Ordering,
|
||||
@@ -485,7 +536,18 @@ impl From<EvmVersionRequirement> for String {
|
||||
/// just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
/// Defines how the compiler should handle revert strings.
|
||||
#[derive(
|
||||
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
|
||||
Clone,
|
||||
Debug,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub struct CompilationDirectives {
|
||||
/// Defines how the revert strings should be handled.
|
||||
@@ -494,14 +556,29 @@ pub struct CompilationDirectives {
|
||||
|
||||
/// Defines how the compiler should handle revert strings.
|
||||
#[derive(
|
||||
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
|
||||
Clone,
|
||||
Debug,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RevertString {
|
||||
/// The default handling of the revert strings.
|
||||
#[default]
|
||||
Default,
|
||||
/// The debug handling of the revert strings.
|
||||
Debug,
|
||||
/// Strip the revert strings.
|
||||
Strip,
|
||||
/// Provide verbose debug strings for the revert string.
|
||||
VerboseDebug,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
use regex::Regex;
|
||||
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
/// Mode strings can take the following form (in pseudo-regex):
|
||||
///
|
||||
/// ```text
|
||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||
/// ```
|
||||
///
|
||||
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ParsedMode {
|
||||
pub pipeline: Option<ModePipeline>,
|
||||
pub optimize_flag: Option<bool>,
|
||||
pub optimize_setting: Option<ModeOptimizerSetting>,
|
||||
pub version: Option<semver::VersionReq>,
|
||||
}
|
||||
|
||||
impl FromStr for ParsedMode {
|
||||
type Err = anyhow::Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?x)
|
||||
^
|
||||
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
|
||||
\s*
|
||||
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
|
||||
\s*
|
||||
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
|
||||
$
|
||||
").unwrap()
|
||||
});
|
||||
|
||||
let Some(caps) = REGEX.captures(s) else {
|
||||
anyhow::bail!("Cannot parse mode '{s}' from string");
|
||||
};
|
||||
|
||||
let pipeline = match caps.name("pipeline") {
|
||||
Some(m) => Some(ModePipeline::from_str(m.as_str())?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
|
||||
|
||||
let optimize_setting = match caps.name("optimize_setting") {
|
||||
Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let version = match caps.name("version") {
|
||||
Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| {
|
||||
anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str())
|
||||
})?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(ParsedMode {
|
||||
pipeline,
|
||||
optimize_flag,
|
||||
optimize_setting,
|
||||
version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParsedMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut has_written = false;
|
||||
|
||||
if let Some(pipeline) = self.pipeline {
|
||||
pipeline.fmt(f)?;
|
||||
if let Some(optimize_flag) = self.optimize_flag {
|
||||
f.write_str(if optimize_flag { "+" } else { "-" })?;
|
||||
}
|
||||
has_written = true;
|
||||
}
|
||||
|
||||
if let Some(optimize_setting) = self.optimize_setting {
|
||||
if has_written {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
optimize_setting.fmt(f)?;
|
||||
has_written = true;
|
||||
}
|
||||
|
||||
if let Some(version) = &self.version {
|
||||
if has_written {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
version.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParsedMode> for String {
|
||||
fn from(parsed_mode: ParsedMode) -> Self {
|
||||
parsed_mode.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ParsedMode {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
ParsedMode::from_str(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedMode {
|
||||
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
|
||||
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
|
||||
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|
||||
|| EitherIter::A(ModePipeline::test_cases()),
|
||||
|p| EitherIter::B(std::iter::once(*p)),
|
||||
);
|
||||
|
||||
let optimize_flag_setting = self.optimize_flag.map(|flag| {
|
||||
if flag {
|
||||
ModeOptimizerSetting::M3
|
||||
} else {
|
||||
ModeOptimizerSetting::M0
|
||||
}
|
||||
});
|
||||
|
||||
let optimize_flag_iter = match optimize_flag_setting {
|
||||
Some(setting) => EitherIter::A(std::iter::once(setting)),
|
||||
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
|
||||
};
|
||||
|
||||
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|
||||
|| EitherIter::A(optimize_flag_iter),
|
||||
|s| EitherIter::B(std::iter::once(*s)),
|
||||
);
|
||||
|
||||
pipeline_iter.flat_map(move |pipeline| {
|
||||
optimize_settings_iter
|
||||
.clone()
|
||||
.map(move |optimize_setting| Mode {
|
||||
pipeline,
|
||||
optimize_setting,
|
||||
version: self.version.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
|
||||
/// This avoids any duplicate entries.
|
||||
pub fn many_to_modes<'a>(
|
||||
parsed: impl Iterator<Item = &'a ParsedMode>,
|
||||
) -> impl Iterator<Item = Mode> {
|
||||
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
|
||||
modes.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator that could be either of two iterators.
|
||||
#[derive(Clone, Debug)]
|
||||
enum EitherIter<A, B> {
|
||||
A(A),
|
||||
B(B),
|
||||
}
|
||||
|
||||
impl<A, B> Iterator for EitherIter<A, B>
|
||||
where
|
||||
A: Iterator,
|
||||
B: Iterator<Item = A::Item>,
|
||||
{
|
||||
type Item = A::Item;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
EitherIter::A(iter) => iter.next(),
|
||||
EitherIter::B(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parsed_mode_from_str() {
|
||||
let strings = vec![
|
||||
("Mz", "Mz"),
|
||||
("Y", "Y"),
|
||||
("Y+", "Y+"),
|
||||
("Y-", "Y-"),
|
||||
("E", "E"),
|
||||
("E+", "E+"),
|
||||
("E-", "E-"),
|
||||
("Y M0", "Y M0"),
|
||||
("Y M1", "Y M1"),
|
||||
("Y M2", "Y M2"),
|
||||
("Y M3", "Y M3"),
|
||||
("Y Ms", "Y Ms"),
|
||||
("Y Mz", "Y Mz"),
|
||||
("E M0", "E M0"),
|
||||
("E M1", "E M1"),
|
||||
("E M2", "E M2"),
|
||||
("E M3", "E M3"),
|
||||
("E Ms", "E Ms"),
|
||||
("E Mz", "E Mz"),
|
||||
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
|
||||
("Y 0.8.0", "Y ^0.8.0"),
|
||||
("E+ 0.8.0", "E+ ^0.8.0"),
|
||||
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
|
||||
("E Mz <0.7.0", "E Mz <0.7.0"),
|
||||
// We can parse +- _and_ M1/M2 but the latter takes priority.
|
||||
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
|
||||
("E- M2 0.7.0", "E- M2 ^0.7.0"),
|
||||
// We don't see this in the wild but it is parsed.
|
||||
("<=0.8", "<=0.8"),
|
||||
];
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
assert_eq!(
|
||||
expected,
|
||||
parsed.to_string(),
|
||||
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parsed_mode_to_test_modes() {
|
||||
let strings = vec![
|
||||
("Mz", vec!["Y Mz", "E Mz"]),
|
||||
("Y", vec!["Y M0", "Y M3"]),
|
||||
("E", vec!["E M0", "E M3"]),
|
||||
("Y+", vec!["Y M3"]),
|
||||
("Y-", vec!["Y M0"]),
|
||||
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
|
||||
(
|
||||
"<=0.8",
|
||||
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
|
||||
),
|
||||
];
|
||||
|
||||
for (actual, expected) in strings {
|
||||
let parsed = ParsedMode::from_str(actual)
|
||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
|
||||
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
|
||||
|
||||
assert_eq!(
|
||||
expected_set, actual_set,
|
||||
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||
|
||||
use alloy::hex::ToHexExt;
|
||||
use alloy::primitives::{FixedBytes, utils::parse_units};
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
json_abi::Function,
|
||||
@@ -7,9 +9,9 @@ use alloy::{
|
||||
primitives::{Address, Bytes, U256},
|
||||
rpc::types::TransactionRequest,
|
||||
};
|
||||
use alloy_primitives::{FixedBytes, utils::parse_units};
|
||||
use anyhow::Context;
|
||||
use anyhow::Context as _;
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
||||
use schemars::JsonSchema;
|
||||
use semver::VersionReq;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -23,53 +25,139 @@ use crate::{metadata::ContractInstance, traits::ResolutionContext};
|
||||
///
|
||||
/// A test step can be anything. It could be an invocation to a function, an assertion, or any other
|
||||
/// action that needs to be run or executed on the nodes used in the tests.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum Step {
|
||||
/// A function call or an invocation to some function on some smart contract.
|
||||
FunctionCall(Box<Input>),
|
||||
FunctionCall(Box<FunctionCallStep>),
|
||||
|
||||
/// A step for performing a balance assertion on some account or contract.
|
||||
BalanceAssertion(Box<BalanceAssertion>),
|
||||
BalanceAssertion(Box<BalanceAssertionStep>),
|
||||
|
||||
/// A step for asserting that the storage of some contract or account is empty.
|
||||
StorageEmptyAssertion(Box<StorageEmptyAssertion>),
|
||||
StorageEmptyAssertion(Box<StorageEmptyAssertionStep>),
|
||||
|
||||
/// A special step for repeating a bunch of steps a certain number of times.
|
||||
Repeat(Box<RepeatStep>),
|
||||
|
||||
/// A step type that allows for a new account address to be allocated and to later on be used
|
||||
/// as the caller in another step.
|
||||
AllocateAccount(Box<AllocateAccountStep>),
|
||||
}
|
||||
|
||||
define_wrapper_type!(
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct StepIdx(usize) impl Display;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct StepIdx(usize) impl Display, FromStr;
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Input {
|
||||
#[serde(default = "Input::default_caller")]
|
||||
pub caller: Address,
|
||||
define_wrapper_type!(
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct StepPath(Vec<StepIdx>);
|
||||
);
|
||||
|
||||
impl StepPath {
|
||||
pub fn from_iterator(path: impl IntoIterator<Item = impl Into<StepIdx>>) -> Self {
|
||||
Self(path.into_iter().map(|value| value.into()).collect())
|
||||
}
|
||||
|
||||
pub fn increment(&self) -> Self {
|
||||
let mut this = self.clone();
|
||||
if let Some(last) = this.last_mut() {
|
||||
last.0 += 1
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub fn append(&self, step_idx: impl Into<StepIdx>) -> Self {
|
||||
let mut this = self.clone();
|
||||
this.0.push(step_idx.into());
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StepPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for StepPath {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.split(".")
|
||||
.map(StepIdx::from_str)
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StepPath> for String {
|
||||
fn from(value: StepPath) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for StepPath {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an input step which is a transaction description that the framework translates into a
|
||||
/// transaction and executes on the nodes.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct FunctionCallStep {
|
||||
/// The address of the account performing the call and paying the fees for it.
|
||||
#[serde(default = "FunctionCallStep::default_caller")]
|
||||
#[schemars(with = "String")]
|
||||
pub caller: StepAddress,
|
||||
|
||||
/// An optional comment on the step which has no impact on the execution in any way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
#[serde(default = "Input::default_instance")]
|
||||
/// The contract instance that's being called in this transaction step.
|
||||
#[serde(default = "FunctionCallStep::default_instance")]
|
||||
pub instance: ContractInstance,
|
||||
|
||||
/// The method that's being called in this step.
|
||||
pub method: Method,
|
||||
|
||||
/// The calldata that the function should be invoked with.
|
||||
#[serde(default)]
|
||||
pub calldata: Calldata,
|
||||
|
||||
/// A set of assertions and expectations to have for the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expected: Option<Expected>,
|
||||
|
||||
/// An optional value to provide as part of the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<EtherValue>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
pub storage: Option<HashMap<String, Calldata>>,
|
||||
|
||||
/// Variable assignment to perform in the framework allowing us to reference them again later on
|
||||
/// during the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variable_assignments: Option<VariableAssignments>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct BalanceAssertion {
|
||||
/// This represents a balance assertion step where the framework needs to query the balance of some
|
||||
/// account or contract and assert that it's some amount.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct BalanceAssertionStep {
|
||||
/// An optional comment on the balance assertion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
@@ -80,14 +168,18 @@ pub struct BalanceAssertion {
|
||||
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
|
||||
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
|
||||
/// followed in the calldata.
|
||||
pub address: String,
|
||||
pub address: StepAddress,
|
||||
|
||||
/// The amount of balance to assert that the account or contract has.
|
||||
/// The amount of balance to assert that the account or contract has. This is a 256 bit string
|
||||
/// that's serialized and deserialized into a decimal string.
|
||||
#[schemars(with = "String")]
|
||||
pub expected_balance: U256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct StorageEmptyAssertion {
|
||||
/// This represents an assertion for the storage of some contract or account and whether it's empty
|
||||
/// or not.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct StorageEmptyAssertionStep {
|
||||
/// An optional comment on the storage empty assertion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
@@ -98,37 +190,86 @@ pub struct StorageEmptyAssertion {
|
||||
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
|
||||
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
|
||||
/// followed in the calldata.
|
||||
pub address: String,
|
||||
pub address: StepAddress,
|
||||
|
||||
/// A boolean of whether the storage of the address is empty or not.
|
||||
pub is_storage_empty: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
/// This represents a repetition step which is a special step type that allows for a sequence of
|
||||
/// steps to be repeated (on different drivers) a certain number of times.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct RepeatStep {
|
||||
/// An optional comment on the repetition step.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// The number of repetitions that the steps should be repeated for.
|
||||
pub repeat: usize,
|
||||
|
||||
/// The sequence of steps to repeat for the above defined number of repetitions.
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct AllocateAccountStep {
|
||||
/// An optional comment on the account allocation step.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// An instruction to allocate a new account with the value being the variable name of that
|
||||
/// account. This must start with `$VARIABLE:` and then be followed by the variable name of the
|
||||
/// account.
|
||||
#[serde(rename = "allocate_account")]
|
||||
pub variable_name: String,
|
||||
}
|
||||
|
||||
/// A set of expectations and assertions to make about the transaction after it ran.
|
||||
///
|
||||
/// If this is not specified then the only assertion that will be ran is that the transaction
|
||||
/// was successful.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Expected {
|
||||
/// An assertion that the transaction succeeded and returned the provided set of data.
|
||||
Calldata(Calldata),
|
||||
/// A more complex assertion.
|
||||
Expected(ExpectedOutput),
|
||||
/// A set of assertions.
|
||||
ExpectedMany(Vec<ExpectedOutput>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
/// A set of assertions to run on the transaction.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
pub struct ExpectedOutput {
|
||||
/// An optional compiler version that's required in order for this assertion to run.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub compiler_version: Option<VersionReq>,
|
||||
|
||||
/// An optional field of the expected returns from the invocation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub return_data: Option<Calldata>,
|
||||
|
||||
/// An optional set of assertions to run on the emitted events from the transaction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub events: Option<Vec<Event>>,
|
||||
|
||||
/// A boolean which defines whether we expect the transaction to succeed or fail.
|
||||
#[serde(default)]
|
||||
pub exception: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
pub struct Event {
|
||||
/// An optional field of the address of the emitter of the event.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
pub address: Option<StepAddress>,
|
||||
|
||||
/// The set of topics to expect the event to have.
|
||||
pub topics: Vec<String>,
|
||||
|
||||
/// The set of values to expect the event to have.
|
||||
pub values: Calldata,
|
||||
}
|
||||
|
||||
@@ -183,16 +324,17 @@ pub struct Event {
|
||||
/// [`Single`]: Calldata::Single
|
||||
/// [`Compound`]: Calldata::Compound
|
||||
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Calldata {
|
||||
Single(Bytes),
|
||||
Single(#[schemars(with = "String")] Bytes),
|
||||
Compound(Vec<CalldataItem>),
|
||||
}
|
||||
|
||||
define_wrapper_type! {
|
||||
/// This represents an item in the [`Calldata::Compound`] variant.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
/// This represents an item in the [`Calldata::Compound`] variant. Each item will be resolved
|
||||
/// according to the resolution rules of the tool.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct CalldataItem(String) impl Display;
|
||||
}
|
||||
@@ -217,7 +359,7 @@ enum Operation {
|
||||
}
|
||||
|
||||
/// Specify how the contract is called.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
|
||||
pub enum Method {
|
||||
/// Initiate a deploy transaction, calling contracts constructor.
|
||||
///
|
||||
@@ -238,11 +380,16 @@ pub enum Method {
|
||||
}
|
||||
|
||||
define_wrapper_type!(
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
/// Defines an Ether value.
|
||||
///
|
||||
/// This is an unsigned 256 bit integer that's followed by some denomination which can either be
|
||||
/// eth, ether, gwei, or wei.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
|
||||
#[schemars(with = "String")]
|
||||
pub struct EtherValue(U256) impl Display;
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct VariableAssignments {
|
||||
/// A vector of the variable names to assign to the return data.
|
||||
///
|
||||
@@ -250,25 +397,90 @@ pub struct VariableAssignments {
|
||||
pub return_data: Vec<String>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub const fn default_caller() -> Address {
|
||||
/// An address type that might either be an address literal or a resolvable address.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
#[schemars(with = "String")]
|
||||
#[serde(untagged)]
|
||||
pub enum StepAddress {
|
||||
Address(Address),
|
||||
ResolvableAddress(String),
|
||||
}
|
||||
|
||||
impl Default for StepAddress {
|
||||
fn default() -> Self {
|
||||
Self::Address(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StepAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StepAddress::Address(address) => Display::fmt(address, f),
|
||||
StepAddress::ResolvableAddress(address) => Display::fmt(address, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StepAddress {
|
||||
pub fn as_address(&self) -> Option<&Address> {
|
||||
match self {
|
||||
StepAddress::Address(address) => Some(address),
|
||||
StepAddress::ResolvableAddress(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_resolvable_address(&self) -> Option<&str> {
|
||||
match self {
|
||||
StepAddress::ResolvableAddress(address) => Some(address),
|
||||
StepAddress::Address(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_address(
|
||||
&self,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<Address> {
|
||||
match self {
|
||||
StepAddress::Address(address) => Ok(*address),
|
||||
StepAddress::ResolvableAddress(address) => Ok(Address::from_slice(
|
||||
Calldata::new_compound([address])
|
||||
.calldata(resolver, context)
|
||||
.await?
|
||||
.get(12..32)
|
||||
.expect("Can't fail"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionCallStep {
|
||||
pub const fn default_caller_address() -> Address {
|
||||
Address(FixedBytes(alloy::hex!(
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
|
||||
)))
|
||||
}
|
||||
|
||||
pub const fn default_caller() -> StepAddress {
|
||||
StepAddress::Address(Self::default_caller_address())
|
||||
}
|
||||
|
||||
fn default_instance() -> ContractInstance {
|
||||
ContractInstance::new("Test")
|
||||
}
|
||||
|
||||
pub async fn encoded_input(
|
||||
&self,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<Bytes> {
|
||||
match self.method {
|
||||
Method::Deployer | Method::Fallback => {
|
||||
let calldata = self.calldata.calldata(resolver, context).await?;
|
||||
let calldata = self
|
||||
.calldata
|
||||
.calldata(resolver, context)
|
||||
.await
|
||||
.context("Failed to produce calldata for deployer/fallback method")?;
|
||||
|
||||
Ok(calldata.into())
|
||||
}
|
||||
@@ -283,14 +495,15 @@ impl Input {
|
||||
// Overloads are handled by providing the full function signature in the "function
|
||||
// name".
|
||||
// https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190
|
||||
let selector = if function_name.contains('(') && function_name.contains(')') {
|
||||
Function::parse(function_name)
|
||||
let selector =
|
||||
if function_name.contains('(') && function_name.contains(')') {
|
||||
Function::parse(function_name)
|
||||
.context(
|
||||
"Failed to parse the provided function name into a function signature",
|
||||
)?
|
||||
.selector()
|
||||
} else {
|
||||
abi.functions()
|
||||
} else {
|
||||
abi.functions()
|
||||
.find(|function| function.signature().starts_with(function_name))
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
@@ -298,9 +511,13 @@ impl Input {
|
||||
function_name,
|
||||
&self.instance
|
||||
)
|
||||
})?
|
||||
})
|
||||
.with_context(|| format!(
|
||||
"Failed to resolve function selector for {:?} on instance {:?}",
|
||||
function_name, &self.instance
|
||||
))?
|
||||
.selector()
|
||||
};
|
||||
};
|
||||
|
||||
// Allocating a vector that we will be using for the calldata. The vector size will be:
|
||||
// 4 bytes for the function selector.
|
||||
@@ -312,7 +529,8 @@ impl Input {
|
||||
calldata.extend(selector.0);
|
||||
self.calldata
|
||||
.calldata_into_slice(&mut calldata, resolver, context)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to append encoded argument to calldata buffer")?;
|
||||
|
||||
Ok(calldata.into())
|
||||
}
|
||||
@@ -320,13 +538,17 @@ impl Input {
|
||||
}
|
||||
|
||||
/// Parse this input into a legacy transaction.
|
||||
pub async fn legacy_transaction(
|
||||
pub async fn as_transaction(
|
||||
&self,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<TransactionRequest> {
|
||||
let input_data = self.encoded_input(resolver, context).await?;
|
||||
let transaction_request = TransactionRequest::default().from(self.caller).value(
|
||||
let input_data = self
|
||||
.encoded_input(resolver, context)
|
||||
.await
|
||||
.context("Failed to encode input bytes for transaction request")?;
|
||||
let caller = self.caller.resolve_address(resolver, context).await?;
|
||||
let transaction_request = TransactionRequest::default().from(caller).value(
|
||||
self.value
|
||||
.map(|value| value.into_inner())
|
||||
.unwrap_or_default(),
|
||||
@@ -408,7 +630,7 @@ impl Calldata {
|
||||
|
||||
pub async fn calldata(
|
||||
&self,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
|
||||
@@ -420,7 +642,7 @@ impl Calldata {
|
||||
pub async fn calldata_into_slice(
|
||||
&self,
|
||||
buffer: &mut Vec<u8>,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self {
|
||||
@@ -437,7 +659,8 @@ impl Calldata {
|
||||
})
|
||||
.buffered(0xFF)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to resolve one or more calldata arguments")?;
|
||||
|
||||
buffer.extend(resolved.into_iter().flatten());
|
||||
}
|
||||
@@ -456,7 +679,7 @@ impl Calldata {
|
||||
pub async fn is_equivalent(
|
||||
&self,
|
||||
other: &[u8],
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<bool> {
|
||||
match self {
|
||||
@@ -464,8 +687,8 @@ impl Calldata {
|
||||
Calldata::Compound(items) => {
|
||||
stream::iter(items.iter().zip(other.chunks(32)))
|
||||
.map(|(this, other)| async move {
|
||||
// The matterlabs format supports wildcards and therefore we
|
||||
// also need to support them.
|
||||
// The MatterLabs format supports wildcards and therefore we also need to
|
||||
// support them.
|
||||
if this.as_ref() == "*" {
|
||||
return Ok::<_, anyhow::Error>(true);
|
||||
}
|
||||
@@ -478,8 +701,12 @@ impl Calldata {
|
||||
std::borrow::Cow::Borrowed(other)
|
||||
};
|
||||
|
||||
let this = this.resolve(resolver, context).await?;
|
||||
let this = this
|
||||
.resolve(resolver, context)
|
||||
.await
|
||||
.context("Failed to resolve calldata item during equivalence check")?;
|
||||
let other = U256::from_be_slice(&other);
|
||||
|
||||
Ok(this == other)
|
||||
})
|
||||
.buffered(0xFF)
|
||||
@@ -492,10 +719,10 @@ impl Calldata {
|
||||
}
|
||||
|
||||
impl CalldataItem {
|
||||
#[instrument(level = "info", skip_all, err)]
|
||||
#[instrument(level = "info", skip_all, err(Debug))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<U256> {
|
||||
let mut stack = Vec::<CalldataToken<U256>>::new();
|
||||
@@ -543,7 +770,14 @@ impl CalldataItem {
|
||||
match stack.as_slice() {
|
||||
// Empty stack means that we got an empty compound calldata which we resolve to zero.
|
||||
[] => Ok(U256::ZERO),
|
||||
[CalldataToken::Item(item)] => Ok(*item),
|
||||
[CalldataToken::Item(item)] => {
|
||||
tracing::debug!(
|
||||
original_item = ?self,
|
||||
resolved_item = item.to_be_bytes::<32>().encode_hex(),
|
||||
"Resolution Done"
|
||||
);
|
||||
Ok(*item)
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid calldata arithmetic operation - Invalid stack"
|
||||
)),
|
||||
@@ -600,7 +834,7 @@ impl<T: AsRef<str>> CalldataToken<T> {
|
||||
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
|
||||
async fn resolve(
|
||||
self,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<CalldataToken<U256>> {
|
||||
match self {
|
||||
@@ -633,7 +867,7 @@ impl<T: AsRef<str>> CalldataToken<T> {
|
||||
context
|
||||
.transaction_hash()
|
||||
.context("No transaction hash provided to get the transaction gas price")
|
||||
.map(|tx_hash| resolver.transaction_gas_price(tx_hash))?
|
||||
.map(|tx_hash| resolver.transaction_gas_price(*tx_hash))?
|
||||
.await
|
||||
.map(U256::from)
|
||||
} else if item == Self::GAS_LIMIT_VARIABLE {
|
||||
@@ -664,17 +898,24 @@ impl<T: AsRef<str>> CalldataToken<T> {
|
||||
|
||||
let current_block_number = match context.tip_block_number() {
|
||||
Some(block_number) => *block_number,
|
||||
None => resolver.last_block_number().await?,
|
||||
None => resolver.last_block_number().await.context(
|
||||
"Failed to query last block number while resolving $BLOCK_HASH",
|
||||
)?,
|
||||
};
|
||||
let desired_block_number = current_block_number.saturating_sub(offset);
|
||||
|
||||
let block_hash = resolver.block_hash(desired_block_number.into()).await?;
|
||||
let block_hash = resolver
|
||||
.block_hash(desired_block_number.into())
|
||||
.await
|
||||
.context(format!("Failed to resolve the block hash of block number {desired_block_number}"))?;
|
||||
|
||||
Ok(U256::from_be_bytes(block_hash.0))
|
||||
} else if item == Self::BLOCK_NUMBER_VARIABLE {
|
||||
let current_block_number = match context.tip_block_number() {
|
||||
Some(block_number) => *block_number,
|
||||
None => resolver.last_block_number().await?,
|
||||
None => resolver.last_block_number().await.context(
|
||||
"Failed to query last block number while resolving $BLOCK_NUMBER",
|
||||
)?,
|
||||
};
|
||||
Ok(U256::from(current_block_number))
|
||||
} else if item == Self::BLOCK_TIMESTAMP_VARIABLE {
|
||||
@@ -727,10 +968,10 @@ impl<'de> Deserialize<'de> for EtherValue {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use alloy::primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
|
||||
use alloy::sol_types::SolValue;
|
||||
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
|
||||
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
|
||||
use alloy_sol_types::SolValue;
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, pin::Pin};
|
||||
|
||||
use super::*;
|
||||
use crate::metadata::ContractIdent;
|
||||
@@ -738,40 +979,63 @@ mod tests {
|
||||
struct MockResolver;
|
||||
|
||||
impl ResolverApi for MockResolver {
|
||||
async fn chain_id(&self) -> anyhow::Result<ChainId> {
|
||||
Ok(0x123)
|
||||
fn chain_id(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<ChainId>> + '_>> {
|
||||
Box::pin(async move { Ok(0x123) })
|
||||
}
|
||||
|
||||
async fn block_gas_limit(&self, _: BlockNumberOrTag) -> anyhow::Result<u128> {
|
||||
Ok(0x1234)
|
||||
fn block_gas_limit(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||
Box::pin(async move { Ok(0x1234) })
|
||||
}
|
||||
|
||||
async fn block_coinbase(&self, _: BlockNumberOrTag) -> anyhow::Result<Address> {
|
||||
Ok(Address::ZERO)
|
||||
fn block_coinbase(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Address>> + '_>> {
|
||||
Box::pin(async move { Ok(Address::ZERO) })
|
||||
}
|
||||
|
||||
async fn block_difficulty(&self, _: BlockNumberOrTag) -> anyhow::Result<U256> {
|
||||
Ok(U256::from(0x12345u128))
|
||||
fn block_difficulty(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
|
||||
Box::pin(async move { Ok(U256::from(0x12345u128)) })
|
||||
}
|
||||
|
||||
async fn block_base_fee(&self, _: BlockNumberOrTag) -> anyhow::Result<u64> {
|
||||
Ok(0x100)
|
||||
fn block_base_fee(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u64>> + '_>> {
|
||||
Box::pin(async move { Ok(0x100) })
|
||||
}
|
||||
|
||||
async fn block_hash(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
|
||||
Ok([0xEE; 32].into())
|
||||
fn block_hash(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockHash>> + '_>> {
|
||||
Box::pin(async move { Ok([0xEE; 32].into()) })
|
||||
}
|
||||
|
||||
async fn block_timestamp(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
|
||||
Ok(0x123456)
|
||||
fn block_timestamp(
|
||||
&self,
|
||||
_: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockTimestamp>> + '_>> {
|
||||
Box::pin(async move { Ok(0x123456) })
|
||||
}
|
||||
|
||||
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
|
||||
Ok(0x1234567)
|
||||
fn last_block_number(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockNumber>> + '_>> {
|
||||
Box::pin(async move { Ok(0x1234567) })
|
||||
}
|
||||
|
||||
async fn transaction_gas_price(&self, _: &TxHash) -> anyhow::Result<u128> {
|
||||
Ok(0x200)
|
||||
fn transaction_gas_price(
|
||||
&self,
|
||||
_: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||
Box::pin(async move { Ok(0x200) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,7 +1062,7 @@ mod tests {
|
||||
.selector()
|
||||
.0;
|
||||
|
||||
let input = Input {
|
||||
let input = FunctionCallStep {
|
||||
instance: ContractInstance::new("Contract"),
|
||||
method: Method::FunctionName("store".to_owned()),
|
||||
calldata: Calldata::new_compound(["42"]),
|
||||
@@ -842,7 +1106,7 @@ mod tests {
|
||||
.selector()
|
||||
.0;
|
||||
|
||||
let input: Input = Input {
|
||||
let input: FunctionCallStep = FunctionCallStep {
|
||||
instance: "Contract".to_owned().into(),
|
||||
method: Method::FunctionName("send(address)".to_owned()),
|
||||
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
||||
@@ -860,7 +1124,7 @@ mod tests {
|
||||
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
||||
assert!(encoded.0.starts_with(&selector));
|
||||
|
||||
type T = (alloy_primitives::Address,);
|
||||
type T = (alloy::primitives::Address,);
|
||||
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.0,
|
||||
@@ -889,7 +1153,7 @@ mod tests {
|
||||
.selector()
|
||||
.0;
|
||||
|
||||
let input: Input = Input {
|
||||
let input: FunctionCallStep = FunctionCallStep {
|
||||
instance: ContractInstance::new("Contract"),
|
||||
method: Method::FunctionName("send".to_owned()),
|
||||
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
||||
@@ -907,7 +1171,7 @@ mod tests {
|
||||
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
||||
assert!(encoded.0.starts_with(&selector));
|
||||
|
||||
type T = (alloy_primitives::Address,);
|
||||
type T = (alloy::primitives::Address,);
|
||||
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.0,
|
||||
@@ -918,7 +1182,7 @@ mod tests {
|
||||
async fn resolve_calldata_item(
|
||||
input: &str,
|
||||
deployed_contracts: &HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
resolver: &impl ResolverApi,
|
||||
resolver: &(impl ResolverApi + ?Sized),
|
||||
) -> anyhow::Result<U256> {
|
||||
let context = ResolutionContext::default().with_deployed_contracts(deployed_contracts);
|
||||
CalldataItem::new(input).resolve(resolver, context).await
|
||||
+30
-11
@@ -1,9 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use alloy::eips::BlockNumberOrTag;
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy::primitives::TxHash;
|
||||
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
|
||||
use alloy_primitives::TxHash;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::metadata::{ContractIdent, ContractInstance};
|
||||
@@ -12,36 +13,54 @@ use crate::metadata::{ContractIdent, ContractInstance};
|
||||
/// crate implements to go from string calldata and into the bytes calldata.
|
||||
pub trait ResolverApi {
|
||||
/// Returns the ID of the chain that the node is on.
|
||||
fn chain_id(&self) -> impl Future<Output = Result<ChainId>>;
|
||||
fn chain_id(&self) -> Pin<Box<dyn Future<Output = Result<ChainId>> + '_>>;
|
||||
|
||||
/// Returns the gas price for the specified transaction.
|
||||
fn transaction_gas_price(&self, tx_hash: &TxHash) -> impl Future<Output = Result<u128>>;
|
||||
fn transaction_gas_price(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = Result<u128>> + '_>>;
|
||||
|
||||
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
|
||||
// TODO: This is currently a u128 due to substrate needing more than 64 bits for its gas limit
|
||||
// when we implement the changes to the gas we need to adjust this to be a u64.
|
||||
/// Returns the gas limit of the specified block.
|
||||
fn block_gas_limit(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<u128>>;
|
||||
fn block_gas_limit(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = Result<u128>> + '_>>;
|
||||
|
||||
/// Returns the coinbase of the specified block.
|
||||
fn block_coinbase(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<Address>>;
|
||||
fn block_coinbase(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Address>> + '_>>;
|
||||
|
||||
/// Returns the difficulty of the specified block.
|
||||
fn block_difficulty(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<U256>>;
|
||||
fn block_difficulty(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = Result<U256>> + '_>>;
|
||||
|
||||
/// Returns the base fee of the specified block.
|
||||
fn block_base_fee(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<u64>>;
|
||||
fn block_base_fee(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = Result<u64>> + '_>>;
|
||||
|
||||
/// Returns the hash of the specified block.
|
||||
fn block_hash(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<BlockHash>>;
|
||||
fn block_hash(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = Result<BlockHash>> + '_>>;
|
||||
|
||||
/// Returns the timestamp of the specified block,
|
||||
fn block_timestamp(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> impl Future<Output = Result<BlockTimestamp>>;
|
||||
) -> Pin<Box<dyn Future<Output = Result<BlockTimestamp>> + '_>>;
|
||||
|
||||
/// Returns the number of the last block.
|
||||
fn last_block_number(&self) -> impl Future<Output = Result<BlockNumber>>;
|
||||
fn last_block_number(&self) -> Pin<Box<dyn Future<Output = Result<BlockNumber>> + '_>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
|
||||
@@ -9,8 +9,14 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
revive-common = { workspace = true }
|
||||
|
||||
revive-dt-format = { workspace = true }
|
||||
revive-dt-report = { workspace = true }
|
||||
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,35 +1,83 @@
|
||||
//! This crate implements all node interactions.
|
||||
|
||||
use alloy::primitives::{Address, StorageKey, U256};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use alloy::network::Ethereum;
|
||||
use alloy::primitives::{Address, StorageKey, TxHash, U256};
|
||||
use alloy::providers::DynProvider;
|
||||
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
||||
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::Stream;
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_report::MinedBlockInformation;
|
||||
|
||||
/// An interface for all interactions with Ethereum compatible nodes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub trait EthereumNode {
|
||||
/// A function to run post spawning the nodes and before any transactions are run on the node.
|
||||
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>>;
|
||||
|
||||
fn id(&self) -> usize;
|
||||
|
||||
/// Returns the nodes connection string.
|
||||
fn connection_string(&self) -> &str;
|
||||
|
||||
fn submit_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> Pin<Box<dyn Future<Output = Result<TxHash>> + '_>>;
|
||||
|
||||
fn get_receipt(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
|
||||
|
||||
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
|
||||
fn execute_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> impl Future<Output = Result<TransactionReceipt>>;
|
||||
) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
|
||||
|
||||
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
|
||||
fn trace_transaction(
|
||||
&self,
|
||||
receipt: &TransactionReceipt,
|
||||
tx_hash: TxHash,
|
||||
trace_options: GethDebugTracingOptions,
|
||||
) -> impl Future<Output = Result<GethTrace>>;
|
||||
) -> Pin<Box<dyn Future<Output = Result<GethTrace>> + '_>>;
|
||||
|
||||
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
|
||||
fn state_diff(&self, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>;
|
||||
fn state_diff(&self, tx_hash: TxHash) -> Pin<Box<dyn Future<Output = Result<DiffMode>> + '_>>;
|
||||
|
||||
/// Returns the balance of the provided [`Address`] back.
|
||||
fn balance_of(&self, address: Address) -> impl Future<Output = Result<U256>>;
|
||||
fn balance_of(&self, address: Address) -> Pin<Box<dyn Future<Output = Result<U256>> + '_>>;
|
||||
|
||||
/// Returns the latest storage proof of the provided [`Address`]
|
||||
fn latest_state_proof(
|
||||
&self,
|
||||
address: Address,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> impl Future<Output = Result<EIP1186AccountProofResponse>>;
|
||||
) -> Pin<Box<dyn Future<Output = Result<EIP1186AccountProofResponse>> + '_>>;
|
||||
|
||||
/// Returns the resolver that is to use with this ethereum node.
|
||||
fn resolver(&self) -> Pin<Box<dyn Future<Output = Result<Arc<dyn ResolverApi + '_>>> + '_>>;
|
||||
|
||||
/// Returns the EVM version of the node.
|
||||
fn evm_version(&self) -> EVMVersion;
|
||||
|
||||
/// Returns a stream of the blocks that were mined by the node.
|
||||
fn subscribe_to_full_blocks_information(
|
||||
&self,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
|
||||
+ '_,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn provider(&self)
|
||||
-> Pin<Box<dyn Future<Output = anyhow::Result<DynProvider<Ethereum>>> + '_>>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
alloy = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
revive-common = { workspace = true }
|
||||
@@ -19,12 +21,17 @@ revive-dt-common = { workspace = true }
|
||||
revive-dt-config = { workspace = true }
|
||||
revive-dt-format = { workspace = true }
|
||||
revive-dt-node-interaction = { workspace = true }
|
||||
revive-dt-report = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
|
||||
sp-core = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
subxt = { workspace = true }
|
||||
zombienet-sdk = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
temp-dir = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use alloy::primitives::ChainId;
|
||||
|
||||
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
|
||||
///
|
||||
/// Note: After changing this number, check that the tests for kitchensink work as we encountered
|
||||
/// some issues with different values of the initial balance on Kitchensink.
|
||||
/// Note: After changing this number, check that the tests for substrate work as we encountered
|
||||
/// some issues with different values of the initial balance on substrate.
|
||||
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
|
||||
|
||||
/// The chain id used for all of the chains spawned by the framework.
|
||||
pub const CHAIN_ID: ChainId = 420420420;
|
||||
|
||||
@@ -1,743 +0,0 @@
|
||||
//! The go-ethereum node implementation.
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
ops::ControlFlow,
|
||||
path::PathBuf,
|
||||
process::{Child, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||
primitives::{
|
||||
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
|
||||
},
|
||||
providers::{
|
||||
Provider, ProviderBuilder,
|
||||
ext::DebugApi,
|
||||
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
|
||||
},
|
||||
rpc::types::{
|
||||
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
||||
},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use revive_common::EVMVersion;
|
||||
use tracing::{Instrument, instrument};
|
||||
|
||||
use revive_dt_common::{
|
||||
fs::clear_directory,
|
||||
futures::{PollingWaitBehavior, poll},
|
||||
};
|
||||
use revive_dt_config::Arguments;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
|
||||
|
||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// The go-ethereum node instance implementation.
|
||||
///
|
||||
/// Implements helpers to initialize, spawn and wait the node.
|
||||
///
|
||||
/// Assumes dev mode and IPC only (`P2P`, `http`` etc. are kept disabled).
|
||||
///
|
||||
/// Prunes the child process and the base directory on drop.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct GethNode {
|
||||
connection_string: String,
|
||||
base_directory: PathBuf,
|
||||
data_directory: PathBuf,
|
||||
logs_directory: PathBuf,
|
||||
geth: PathBuf,
|
||||
id: u32,
|
||||
handle: Option<Child>,
|
||||
start_timeout: u64,
|
||||
wallet: Arc<EthereumWallet>,
|
||||
nonce_manager: CachedNonceManager,
|
||||
chain_id_filler: ChainIdFiller,
|
||||
/// This vector stores [`File`] objects that we use for logging which we want to flush when the
|
||||
/// node object is dropped. We do not store them in a structured fashion at the moment (in
|
||||
/// separate fields) as the logic that we need to apply to them is all the same regardless of
|
||||
/// what it belongs to, we just want to flush them on [`Drop`] of the node.
|
||||
logs_file_to_flush: Vec<File>,
|
||||
}
|
||||
|
||||
impl GethNode {
|
||||
const BASE_DIRECTORY: &str = "geth";
|
||||
const DATA_DIRECTORY: &str = "data";
|
||||
const LOGS_DIRECTORY: &str = "logs";
|
||||
|
||||
const IPC_FILE: &str = "geth.ipc";
|
||||
const GENESIS_JSON_FILE: &str = "genesis.json";
|
||||
|
||||
const READY_MARKER: &str = "IPC endpoint opened";
|
||||
const ERROR_MARKER: &str = "Fatal:";
|
||||
|
||||
const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
|
||||
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
|
||||
|
||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
||||
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Create the node directory and call `geth init` to configure the genesis.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
|
||||
let _ = clear_directory(&self.base_directory);
|
||||
let _ = clear_directory(&self.logs_directory);
|
||||
|
||||
create_dir_all(&self.base_directory)?;
|
||||
create_dir_all(&self.logs_directory)?;
|
||||
|
||||
let mut genesis = serde_json::from_str::<Genesis>(&genesis)?;
|
||||
for signer_address in
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
||||
{
|
||||
// Note, the use of the entry API here means that we only modify the entries for any
|
||||
// account that is not in the `alloc` field of the genesis state.
|
||||
genesis
|
||||
.alloc
|
||||
.entry(signer_address)
|
||||
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
|
||||
}
|
||||
let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE);
|
||||
serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
|
||||
|
||||
let mut child = Command::new(&self.geth)
|
||||
.arg("--state.scheme")
|
||||
.arg("hash")
|
||||
.arg("init")
|
||||
.arg("--datadir")
|
||||
.arg(&self.data_directory)
|
||||
.arg(genesis_path)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
let mut stderr = String::new();
|
||||
child
|
||||
.stderr
|
||||
.take()
|
||||
.expect("should be piped")
|
||||
.read_to_string(&mut stderr)?;
|
||||
|
||||
if !child.wait()?.success() {
|
||||
anyhow::bail!("failed to initialize geth node #{:?}: {stderr}", &self.id);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Spawn the go-ethereum node child process.
|
||||
///
|
||||
/// [Instance::init] must be called prior.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
|
||||
// This is the `OpenOptions` that we wish to use for all of the log files that we will be
|
||||
// opening in this method. We need to construct it in this way to:
|
||||
// 1. Be consistent
|
||||
// 2. Less verbose and more dry
|
||||
// 3. Because the builder pattern uses mutable references so we need to get around that.
|
||||
let open_options = {
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).truncate(true).write(true);
|
||||
options
|
||||
};
|
||||
|
||||
let stdout_logs_file = open_options
|
||||
.clone()
|
||||
.open(self.geth_stdout_log_file_path())?;
|
||||
let stderr_logs_file = open_options.open(self.geth_stderr_log_file_path())?;
|
||||
self.handle = Command::new(&self.geth)
|
||||
.arg("--dev")
|
||||
.arg("--datadir")
|
||||
.arg(&self.data_directory)
|
||||
.arg("--ipcpath")
|
||||
.arg(&self.connection_string)
|
||||
.arg("--nodiscover")
|
||||
.arg("--maxpeers")
|
||||
.arg("0")
|
||||
.arg("--txlookuplimit")
|
||||
.arg("0")
|
||||
.arg("--cache.blocklogs")
|
||||
.arg("512")
|
||||
.arg("--state.scheme")
|
||||
.arg("hash")
|
||||
.arg("--syncmode")
|
||||
.arg("full")
|
||||
.arg("--gcmode")
|
||||
.arg("archive")
|
||||
.stderr(stderr_logs_file.try_clone()?)
|
||||
.stdout(stdout_logs_file.try_clone()?)
|
||||
.spawn()?
|
||||
.into();
|
||||
|
||||
if let Err(error) = self.wait_ready() {
|
||||
tracing::error!(?error, "Failed to start geth, shutting down gracefully");
|
||||
self.shutdown()?;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
self.logs_file_to_flush
|
||||
.extend([stderr_logs_file, stdout_logs_file]);
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Wait for the g-ethereum node child process getting ready.
|
||||
///
|
||||
/// [Instance::spawn_process] must be called priorly.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn wait_ready(&mut self) -> anyhow::Result<&mut Self> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let logs_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.append(false)
|
||||
.truncate(false)
|
||||
.open(self.geth_stderr_log_file_path())?;
|
||||
|
||||
let maximum_wait_time = Duration::from_millis(self.start_timeout);
|
||||
let mut stderr = BufReader::new(logs_file).lines();
|
||||
let mut lines = vec![];
|
||||
loop {
|
||||
if let Some(Ok(line)) = stderr.next() {
|
||||
if line.contains(Self::ERROR_MARKER) {
|
||||
anyhow::bail!("Failed to start geth {line}");
|
||||
}
|
||||
if line.contains(Self::READY_MARKER) {
|
||||
return Ok(self);
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
if Instant::now().duration_since(start_time) > maximum_wait_time {
|
||||
anyhow::bail!(
|
||||
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
|
||||
self.start_timeout,
|
||||
lines.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn geth_stdout_log_file_path(&self) -> PathBuf {
|
||||
self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn geth_stderr_log_file_path(&self) -> PathBuf {
|
||||
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
|
||||
}
|
||||
|
||||
async fn provider(
|
||||
&self,
|
||||
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>>
|
||||
{
|
||||
ProviderBuilder::new()
|
||||
.disable_recommended_fillers()
|
||||
.filler(FallbackGasFiller::new(
|
||||
25_000_000,
|
||||
1_000_000_000,
|
||||
1_000_000_000,
|
||||
))
|
||||
.filler(self.chain_id_filler.clone())
|
||||
.filler(NonceFiller::new(self.nonce_manager.clone()))
|
||||
.wallet(self.wallet.clone())
|
||||
.connect(&self.connection_string)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthereumNode for GethNode {
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
||||
err,
|
||||
)]
|
||||
async fn execute_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
|
||||
let provider = self.provider().await?;
|
||||
|
||||
let pending_transaction = provider.send_transaction(transaction).await.inspect_err(
|
||||
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
|
||||
)?;
|
||||
let transaction_hash = *pending_transaction.tx_hash();
|
||||
|
||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
||||
// to get. You can find more information on this in the following GH issue in geth
|
||||
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
||||
// before we can get the receipt of the transaction it needs to have been indexed by the
|
||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
|
||||
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
|
||||
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
|
||||
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
|
||||
// implement a retry mechanism for the receipt to keep retrying to get it until it
|
||||
// eventually works, but we only do that if the error we get back is the "transaction
|
||||
// indexing is in progress" error or if the receipt is None.
|
||||
//
|
||||
// Getting the transaction indexed and taking a receipt can take a long time especially when
|
||||
// a lot of transactions are being submitted to the node. Thus, while initially we only
|
||||
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
|
||||
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
|
||||
// backoff each time we attempt to get the receipt and find that it's not available.
|
||||
let provider = Arc::new(provider);
|
||||
poll(
|
||||
Self::RECEIPT_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
move || {
|
||||
let provider = provider.clone();
|
||||
async move {
|
||||
match provider.get_transaction_receipt(transaction_hash).await {
|
||||
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
||||
Ok(None) => Ok(ControlFlow::Continue(())),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.instrument(tracing::info_span!(
|
||||
"Awaiting transaction receipt",
|
||||
?transaction_hash
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn trace_transaction(
|
||||
&self,
|
||||
transaction: &TransactionReceipt,
|
||||
trace_options: GethDebugTracingOptions,
|
||||
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
|
||||
let provider = Arc::new(self.provider().await?);
|
||||
poll(
|
||||
Self::TRACE_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
move || {
|
||||
let provider = provider.clone();
|
||||
let trace_options = trace_options.clone();
|
||||
async move {
|
||||
match provider
|
||||
.debug_trace_transaction(transaction.transaction_hash, trace_options)
|
||||
.await
|
||||
{
|
||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
|
||||
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
|
||||
diff_mode: Some(true),
|
||||
disable_code: None,
|
||||
disable_storage: None,
|
||||
});
|
||||
match self
|
||||
.trace_transaction(transaction, trace_options)
|
||||
.await?
|
||||
.try_into_pre_state_frame()?
|
||||
{
|
||||
PreStateFrame::Diff(diff) => Ok(diff),
|
||||
_ => anyhow::bail!("expected a diff mode trace"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_balance(address)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn latest_state_proof(
|
||||
&self,
|
||||
address: Address,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> anyhow::Result<EIP1186AccountProofResponse> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_proof(address, keys)
|
||||
.latest()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolverApi for GethNode {
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_chain_id()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_transaction_receipt(*tx_hash)
|
||||
.await?
|
||||
.context("Failed to get the transaction receipt")
|
||||
.map(|receipt| receipt.effective_gas_price)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.map(|block| block.header.gas_limit as _)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.map(|block| block.header.beneficiary)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.map(|block| U256::from_be_bytes(block.header.mix_hash.0))
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.and_then(|block| {
|
||||
block
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.context("Failed to get the base fee per gas")
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.map(|block| block.header.hash)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_by_number(number)
|
||||
.await?
|
||||
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
|
||||
.map(|block| block.header.timestamp)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
|
||||
self.provider()
|
||||
.await?
|
||||
.get_block_number()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for GethNode {
|
||||
fn new(config: &Arguments) -> Self {
|
||||
let geth_directory = config.directory().join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = geth_directory.join(id.to_string());
|
||||
|
||||
let mut wallet = config.wallet();
|
||||
for signer in (1..=config.private_keys_to_add)
|
||||
.map(|id| U256::from(id))
|
||||
.map(|id| id.to_be_bytes::<32>())
|
||||
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
|
||||
{
|
||||
wallet.register_signer(signer);
|
||||
}
|
||||
|
||||
Self {
|
||||
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
|
||||
data_directory: base_directory.join(Self::DATA_DIRECTORY),
|
||||
logs_directory: base_directory.join(Self::LOGS_DIRECTORY),
|
||||
base_directory,
|
||||
geth: config.geth.clone(),
|
||||
id,
|
||||
handle: None,
|
||||
start_timeout: config.geth_start_timeout,
|
||||
wallet: Arc::new(wallet),
|
||||
chain_id_filler: Default::default(),
|
||||
nonce_manager: Default::default(),
|
||||
// We know that we only need to be storing 2 files so we can specify that when creating
|
||||
// the vector. It's the stdout and stderr of the geth node.
|
||||
logs_file_to_flush: Vec::with_capacity(2),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn id(&self) -> usize {
|
||||
self.id as _
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn connection_string(&self) -> String {
|
||||
self.connection_string.clone()
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn shutdown(&mut self) -> anyhow::Result<()> {
|
||||
// Terminate the processes in a graceful manner to allow for the output to be flushed.
|
||||
if let Some(mut child) = self.handle.take() {
|
||||
child
|
||||
.kill()
|
||||
.map_err(|error| anyhow::anyhow!("Failed to kill the geth process: {error:?}"))?;
|
||||
}
|
||||
|
||||
// Flushing the files that we're using for keeping the logs before shutdown.
|
||||
for file in self.logs_file_to_flush.iter_mut() {
|
||||
file.flush()?
|
||||
}
|
||||
|
||||
// Remove the node's database so that subsequent runs do not run on the same database. We
|
||||
// ignore the error just in case the directory didn't exist in the first place and therefore
|
||||
// there's nothing to be deleted.
|
||||
let _ = remove_dir_all(self.base_directory.join(Self::DATA_DIRECTORY));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
|
||||
self.init(genesis)?.spawn_process()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn version(&self) -> anyhow::Result<String> {
|
||||
let output = Command::new(&self.geth)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?
|
||||
.wait_with_output()?
|
||||
.stdout;
|
||||
Ok(String::from_utf8_lossy(&output).into())
|
||||
}
|
||||
|
||||
fn matches_target(targets: Option<&[String]>) -> bool {
|
||||
match targets {
|
||||
None => true,
|
||||
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
|
||||
}
|
||||
}
|
||||
|
||||
fn evm_version() -> EVMVersion {
|
||||
EVMVersion::Cancun
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GethNode {
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn drop(&mut self) {
|
||||
self.shutdown().expect("Failed to shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use revive_dt_config::Arguments;
|
||||
|
||||
use temp_dir::TempDir;
|
||||
|
||||
use crate::{GENESIS_JSON, Node};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> (Arguments, TempDir) {
|
||||
let mut config = Arguments::default();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
config.working_directory = temp_dir.path().to_path_buf().into();
|
||||
|
||||
(config, temp_dir)
|
||||
}
|
||||
|
||||
fn new_node() -> (GethNode, TempDir) {
|
||||
let (args, temp_dir) = test_config();
|
||||
let mut node = GethNode::new(&args);
|
||||
node.init(GENESIS_JSON.to_owned())
|
||||
.expect("Failed to initialize the node")
|
||||
.spawn_process()
|
||||
.expect("Failed to spawn the node process");
|
||||
(node, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_works() {
|
||||
GethNode::new(&test_config().0)
|
||||
.init(GENESIS_JSON.to_string())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_works() {
|
||||
GethNode::new(&test_config().0)
|
||||
.spawn(GENESIS_JSON.to_string())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_works() {
|
||||
let version = GethNode::new(&test_config().0).version().unwrap();
|
||||
assert!(
|
||||
version.starts_with("geth version"),
|
||||
"expected version string, got: '{version}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_chain_id_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let chain_id = node.chain_id().await;
|
||||
|
||||
// Assert
|
||||
let chain_id = chain_id.expect("Failed to get the chain id");
|
||||
assert_eq!(chain_id, 420_420_420);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_gas_limit_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let gas_limit = gas_limit.expect("Failed to get the gas limit");
|
||||
assert_eq!(gas_limit, u32::MAX as u128)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_coinbase_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let coinbase = coinbase.expect("Failed to get the coinbase");
|
||||
assert_eq!(coinbase, Address::new([0xFF; 20]))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_block_difficulty_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
|
||||
assert_eq!(block_difficulty, U256::ZERO)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_block_hash_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_hash.expect("Failed to get the block hash");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_block_timestamp_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_get_block_number_from_node() {
|
||||
// Arrange
|
||||
let (node, _temp_dir) = new_node();
|
||||
|
||||
// Act
|
||||
let block_number = node.last_block_number().await;
|
||||
|
||||
// Assert
|
||||
let block_number = block_number.expect("Failed to get the block number");
|
||||
assert_eq!(block_number, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod process;
|
||||
|
||||
pub use process::*;
|
||||
@@ -0,0 +1,191 @@
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::Path,
|
||||
process::{Child, Command},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
/// A wrapper around processes which allows for their stdout and stderr to be logged and flushed
|
||||
/// when the process is dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
/// The handle of the child process.
|
||||
child: Child,
|
||||
|
||||
/// The file that stdout is being logged to.
|
||||
stdout_logs_file: File,
|
||||
|
||||
/// The file that stderr is being logged to.
|
||||
stderr_logs_file: File,
|
||||
}
|
||||
|
||||
impl Process {
|
||||
pub fn new(
|
||||
log_file_prefix: impl Into<Option<&'static str>>,
|
||||
logs_directory: impl AsRef<Path>,
|
||||
binary_path: impl AsRef<Path>,
|
||||
command_building_callback: impl FnOnce(&mut Command, File, File),
|
||||
process_readiness_wait_behavior: ProcessReadinessWaitBehavior,
|
||||
) -> Result<Self> {
|
||||
let log_file_prefix = log_file_prefix.into();
|
||||
|
||||
let (stdout_file_name, stderr_file_name) = match log_file_prefix {
|
||||
Some(prefix) => (
|
||||
format!("{prefix}_stdout.log"),
|
||||
format!("{prefix}_stderr.log"),
|
||||
),
|
||||
None => ("stdout.log".to_string(), "stderr.log".to_string()),
|
||||
};
|
||||
|
||||
let stdout_logs_file_path = logs_directory.as_ref().join(stdout_file_name);
|
||||
let stderr_logs_file_path = logs_directory.as_ref().join(stderr_file_name);
|
||||
|
||||
let stdout_logs_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(stdout_logs_file_path.as_path())
|
||||
.context("Failed to open the stdout logs file")?;
|
||||
let stderr_logs_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(stderr_logs_file_path.as_path())
|
||||
.context("Failed to open the stderr logs file")?;
|
||||
|
||||
let mut command = {
|
||||
let stdout_logs_file = stdout_logs_file
|
||||
.try_clone()
|
||||
.context("Failed to clone the stdout logs file")?;
|
||||
let stderr_logs_file = stderr_logs_file
|
||||
.try_clone()
|
||||
.context("Failed to clone the stderr logs file")?;
|
||||
|
||||
let mut command = Command::new(binary_path.as_ref());
|
||||
command_building_callback(&mut command, stdout_logs_file, stderr_logs_file);
|
||||
command
|
||||
};
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.context("Failed to spawn the built command")?;
|
||||
|
||||
match process_readiness_wait_behavior {
|
||||
ProcessReadinessWaitBehavior::NoStartupWait => {}
|
||||
ProcessReadinessWaitBehavior::WaitDuration(duration) => std::thread::sleep(duration),
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration,
|
||||
mut check_function,
|
||||
} => {
|
||||
let spawn_time = Instant::now();
|
||||
|
||||
let stdout_logs_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(stdout_logs_file_path)
|
||||
.context("Failed to open the stdout logs file")?;
|
||||
let stderr_logs_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(stderr_logs_file_path)
|
||||
.context("Failed to open the stderr logs file")?;
|
||||
|
||||
let mut stdout_lines = BufReader::new(stdout_logs_file).lines();
|
||||
let mut stderr_lines = BufReader::new(stderr_logs_file).lines();
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
loop {
|
||||
let stdout_line = stdout_lines.next().and_then(Result::ok);
|
||||
let stderr_line = stderr_lines.next().and_then(Result::ok);
|
||||
|
||||
if let Some(stdout_line) = stdout_line.as_ref() {
|
||||
stdout.push_str(stdout_line);
|
||||
stdout.push('\n');
|
||||
}
|
||||
if let Some(stderr_line) = stderr_line.as_ref() {
|
||||
stderr.push_str(stderr_line);
|
||||
stderr.push('\n');
|
||||
}
|
||||
|
||||
let check_result =
|
||||
check_function(stdout_line.as_deref(), stderr_line.as_deref()).context(
|
||||
format!(
|
||||
"Failed to wait for the process to be ready - {stdout} - {stderr}"
|
||||
),
|
||||
)?;
|
||||
|
||||
if check_result {
|
||||
break;
|
||||
}
|
||||
|
||||
if Instant::now().duration_since(spawn_time) > max_wait_duration {
|
||||
bail!(
|
||||
"Waited for the process to start but it failed to start in time. stderr {stderr} - stdout {stdout}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessReadinessWaitBehavior::WaitForCommandToExit => {
|
||||
if !child
|
||||
.wait()
|
||||
.context("Failed waiting for process to finish")?
|
||||
.success()
|
||||
{
|
||||
anyhow::bail!("Failed to spawn command");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
child,
|
||||
stdout_logs_file,
|
||||
stderr_logs_file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Process {
|
||||
fn drop(&mut self) {
|
||||
self.child.kill().expect("Failed to kill the process");
|
||||
self.stdout_logs_file
|
||||
.flush()
|
||||
.expect("Failed to flush the stdout logs file");
|
||||
self.stderr_logs_file
|
||||
.flush()
|
||||
.expect("Failed to flush the stderr logs file");
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ProcessReadinessWaitBehavior {
|
||||
/// The process does not require any kind of wait after it's been spawned and can be used
|
||||
/// straight away.
|
||||
NoStartupWait,
|
||||
|
||||
/// Waits for the command to exit.
|
||||
WaitForCommandToExit,
|
||||
|
||||
/// The process does require some amount of wait duration after it's been started.
|
||||
WaitDuration(Duration),
|
||||
|
||||
/// The process requires a time bounded wait function which is a function of the lines that
|
||||
/// appear in the log files.
|
||||
TimeBoundedWaitFunction {
|
||||
/// The maximum amount of time to wait for the check function to return true.
|
||||
max_wait_duration: Duration,
|
||||
|
||||
/// The function to use to check if the process spawned is ready to use or not. This
|
||||
/// function should return the following in the following cases:
|
||||
///
|
||||
/// - `Ok(true)`: Returned when the condition the process is waiting for has been fulfilled
|
||||
/// and the wait is completed.
|
||||
/// - `Ok(false)`: The process is not ready yet but it might be ready in the future.
|
||||
/// - `Err`: The process is not ready yet and will not be ready in the future as it appears
|
||||
/// that it has encountered an error when it was being spawned.
|
||||
///
|
||||
/// The first argument is a line from stdout and the second argument is a line from stderr.
|
||||
#[allow(clippy::type_complexity)]
|
||||
check_function: Box<dyn FnMut(Option<&str>, Option<&str>) -> anyhow::Result<bool>>,
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+5
-26
@@ -1,46 +1,25 @@
|
||||
//! This crate implements the testing nodes.
|
||||
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_config::Arguments;
|
||||
use alloy::genesis::Genesis;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
pub mod common;
|
||||
pub mod constants;
|
||||
pub mod geth;
|
||||
pub mod kitchensink;
|
||||
pub mod pool;
|
||||
|
||||
/// The default genesis configuration.
|
||||
pub const GENESIS_JSON: &str = include_str!("../../../genesis.json");
|
||||
pub mod helpers;
|
||||
pub mod node_implementations;
|
||||
pub mod provider_utils;
|
||||
|
||||
/// An abstract interface for testing nodes.
|
||||
pub trait Node: EthereumNode {
|
||||
/// Create a new uninitialized instance.
|
||||
fn new(config: &Arguments) -> Self;
|
||||
|
||||
/// Returns the identifier of the node.
|
||||
fn id(&self) -> usize;
|
||||
|
||||
/// Spawns a node configured according to the genesis json.
|
||||
///
|
||||
/// Blocking until it's ready to accept transactions.
|
||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()>;
|
||||
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()>;
|
||||
|
||||
/// Prune the node instance and related data.
|
||||
///
|
||||
/// Blocking until it's completely stopped.
|
||||
fn shutdown(&mut self) -> anyhow::Result<()>;
|
||||
|
||||
/// Returns the nodes connection string.
|
||||
fn connection_string(&self) -> String;
|
||||
|
||||
/// Returns the node version.
|
||||
fn version(&self) -> anyhow::Result<String>;
|
||||
|
||||
/// Given a list of targets from the metadata file, this function determines if the metadata
|
||||
/// file can be ran on this node or not.
|
||||
fn matches_target(targets: Option<&[String]>) -> bool;
|
||||
|
||||
/// Returns the EVM version of the node.
|
||||
fn evm_version() -> EVMVersion;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,916 @@
|
||||
//! The go-ethereum node implementation.
|
||||
|
||||
use std::{
|
||||
fs::{File, create_dir_all, remove_dir_all},
|
||||
io::Read,
|
||||
ops::ControlFlow,
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||
providers::{
|
||||
Provider,
|
||||
ext::DebugApi,
|
||||
fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
|
||||
},
|
||||
rpc::types::{
|
||||
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||
trace::geth::{
|
||||
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
|
||||
},
|
||||
},
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use revive_common::EVMVersion;
|
||||
use tokio::sync::OnceCell;
|
||||
use tracing::{Instrument, error, instrument};
|
||||
|
||||
use revive_dt_common::{
|
||||
fs::clear_directory,
|
||||
futures::{PollingWaitBehavior, poll},
|
||||
};
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use revive_dt_report::{EthereumMinedBlockInformation, MinedBlockInformation};
|
||||
|
||||
use crate::{
|
||||
Node,
|
||||
constants::{CHAIN_ID, INITIAL_BALANCE},
|
||||
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||
};
|
||||
|
||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// The go-ethereum node instance implementation.
|
||||
///
|
||||
/// Implements helpers to initialize, spawn and wait the node.
|
||||
///
|
||||
/// Assumes dev mode and IPC only (`P2P`, `http`` etc. are kept disabled).
|
||||
///
|
||||
/// Prunes the child process and the base directory on drop.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct GethNode {
|
||||
connection_string: String,
|
||||
base_directory: PathBuf,
|
||||
data_directory: PathBuf,
|
||||
logs_directory: PathBuf,
|
||||
geth: PathBuf,
|
||||
id: u32,
|
||||
handle: Option<Process>,
|
||||
start_timeout: Duration,
|
||||
wallet: Arc<EthereumWallet>,
|
||||
nonce_manager: CachedNonceManager,
|
||||
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
|
||||
}
|
||||
|
||||
impl GethNode {
|
||||
const BASE_DIRECTORY: &str = "geth";
|
||||
const DATA_DIRECTORY: &str = "data";
|
||||
const LOGS_DIRECTORY: &str = "logs";
|
||||
|
||||
const IPC_FILE: &str = "geth.ipc";
|
||||
const GENESIS_JSON_FILE: &str = "genesis.json";
|
||||
|
||||
const READY_MARKER: &str = "IPC endpoint opened";
|
||||
const ERROR_MARKER: &str = "Fatal:";
|
||||
|
||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
||||
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
+ AsRef<WalletConfiguration>
|
||||
+ AsRef<GethConfiguration>
|
||||
+ Clone,
|
||||
) -> Self {
|
||||
let working_directory_configuration =
|
||||
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
|
||||
let geth_configuration = AsRef::<GethConfiguration>::as_ref(&context);
|
||||
|
||||
let geth_directory = working_directory_configuration
|
||||
.as_path()
|
||||
.join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = geth_directory.join(id.to_string());
|
||||
|
||||
let wallet = wallet_configuration.wallet();
|
||||
|
||||
Self {
|
||||
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
|
||||
data_directory: base_directory.join(Self::DATA_DIRECTORY),
|
||||
logs_directory: base_directory.join(Self::LOGS_DIRECTORY),
|
||||
base_directory,
|
||||
geth: geth_configuration.path.clone(),
|
||||
id,
|
||||
handle: None,
|
||||
start_timeout: geth_configuration.start_timeout_ms,
|
||||
wallet: wallet.clone(),
|
||||
nonce_manager: Default::default(),
|
||||
provider: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the node directory and call `geth init` to configure the genesis.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn init(&mut self, genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
let _ = clear_directory(&self.base_directory);
|
||||
let _ = clear_directory(&self.logs_directory);
|
||||
|
||||
create_dir_all(&self.base_directory)
|
||||
.context("Failed to create base directory for geth node")?;
|
||||
create_dir_all(&self.logs_directory)
|
||||
.context("Failed to create logs directory for geth node")?;
|
||||
|
||||
let genesis = Self::node_genesis(genesis, self.wallet.as_ref());
|
||||
let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE);
|
||||
serde_json::to_writer(
|
||||
File::create(&genesis_path).context("Failed to create geth genesis file")?,
|
||||
&genesis,
|
||||
)
|
||||
.context("Failed to serialize geth genesis JSON to file")?;
|
||||
|
||||
let mut child = Command::new(&self.geth)
|
||||
.arg("--state.scheme")
|
||||
.arg("hash")
|
||||
.arg("init")
|
||||
.arg("--datadir")
|
||||
.arg(&self.data_directory)
|
||||
.arg(genesis_path)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed to spawn geth --init process")?;
|
||||
|
||||
let mut stderr = String::new();
|
||||
child
|
||||
.stderr
|
||||
.take()
|
||||
.expect("should be piped")
|
||||
.read_to_string(&mut stderr)
|
||||
.context("Failed to read geth --init stderr")?;
|
||||
|
||||
if !child
|
||||
.wait()
|
||||
.context("Failed waiting for geth --init process to finish")?
|
||||
.success()
|
||||
{
|
||||
anyhow::bail!("failed to initialize geth node #{:?}: {stderr}", &self.id);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Spawn the go-ethereum node child process.
|
||||
///
|
||||
/// [Instance::init] must be called prior.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
|
||||
let process = Process::new(
|
||||
None,
|
||||
self.logs_directory.as_path(),
|
||||
self.geth.as_path(),
|
||||
|command, stdout_file, stderr_file| {
|
||||
command
|
||||
.arg("--dev")
|
||||
.arg("--datadir")
|
||||
.arg(&self.data_directory)
|
||||
.arg("--ipcpath")
|
||||
.arg(&self.connection_string)
|
||||
.arg("--nodiscover")
|
||||
.arg("--maxpeers")
|
||||
.arg("0")
|
||||
.arg("--txlookuplimit")
|
||||
.arg("0")
|
||||
.arg("--cache.blocklogs")
|
||||
.arg("512")
|
||||
.arg("--state.scheme")
|
||||
.arg("hash")
|
||||
.arg("--syncmode")
|
||||
.arg("full")
|
||||
.arg("--gcmode")
|
||||
.arg("archive")
|
||||
.stderr(stderr_file)
|
||||
.stdout(stdout_file);
|
||||
},
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration: self.start_timeout,
|
||||
check_function: Box::new(|_, stderr_line| match stderr_line {
|
||||
Some(line) => {
|
||||
if line.contains(Self::ERROR_MARKER) {
|
||||
anyhow::bail!("Failed to start geth {line}");
|
||||
} else if line.contains(Self::READY_MARKER) {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
None => Ok(false),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
match process {
|
||||
Ok(process) => self.handle = Some(process),
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to start geth, shutting down gracefully");
|
||||
self.shutdown()
|
||||
.context("Failed to gracefully shutdown after geth start error")?;
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
|
||||
self.provider
|
||||
.get_or_try_init(|| async move {
|
||||
construct_concurrency_limited_provider::<Ethereum, _>(
|
||||
self.connection_string.as_str(),
|
||||
FallbackGasFiller::default(),
|
||||
ChainIdFiller::new(Some(CHAIN_ID)),
|
||||
NonceFiller::new(self.nonce_manager.clone()),
|
||||
self.wallet.clone(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to construct the provider")
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn node_genesis(mut genesis: Genesis, wallet: &EthereumWallet) -> Genesis {
|
||||
for signer_address in NetworkWallet::<Ethereum>::signer_addresses(&wallet) {
|
||||
genesis
|
||||
.alloc
|
||||
.entry(signer_address)
|
||||
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
|
||||
}
|
||||
genesis
|
||||
}
|
||||
}
|
||||
|
||||
impl EthereumNode for GethNode {
|
||||
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
|
||||
Box::pin(async move { Ok(()) })
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
self.id as _
|
||||
}
|
||||
|
||||
fn connection_string(&self) -> &str {
|
||||
&self.connection_string
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
||||
err,
|
||||
)]
|
||||
fn submit_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create the provider for transaction submission")?;
|
||||
let pending_transaction = provider
|
||||
.send_transaction(transaction)
|
||||
.await
|
||||
.context("Failed to submit the transaction through the provider")?;
|
||||
Ok(*pending_transaction.tx_hash())
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
||||
err,
|
||||
)]
|
||||
fn get_receipt(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider()
|
||||
.await
|
||||
.context("Failed to create provider for getting the receipt")?
|
||||
.get_transaction_receipt(tx_hash)
|
||||
.await
|
||||
.context("Failed to get the receipt of the transaction")?
|
||||
.context("Failed to get the receipt of the transaction")
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
||||
err,
|
||||
)]
|
||||
fn execute_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create provider for transaction submission")?;
|
||||
|
||||
let pending_transaction = provider
|
||||
.send_transaction(transaction)
|
||||
.await
|
||||
.inspect_err(
|
||||
|err| error!(%err, "Encountered an error when submitting the transaction"),
|
||||
)
|
||||
.context("Failed to submit transaction to geth node")?;
|
||||
let transaction_hash = *pending_transaction.tx_hash();
|
||||
|
||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
||||
// to get. You can find more information on this in the following GH issue in geth
|
||||
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
||||
// before we can get the receipt of the transaction it needs to have been indexed by the
|
||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
|
||||
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
|
||||
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
|
||||
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
|
||||
// implement a retry mechanism for the receipt to keep retrying to get it until it
|
||||
// eventually works, but we only do that if the error we get back is the "transaction
|
||||
// indexing is in progress" error or if the receipt is None.
|
||||
//
|
||||
// Getting the transaction indexed and taking a receipt can take a long time especially when
|
||||
// a lot of transactions are being submitted to the node. Thus, while initially we only
|
||||
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
|
||||
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
|
||||
// backoff each time we attempt to get the receipt and find that it's not available.
|
||||
poll(
|
||||
Self::RECEIPT_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
move || {
|
||||
let provider = provider.clone();
|
||||
async move {
|
||||
match provider.get_transaction_receipt(transaction_hash).await {
|
||||
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
||||
Ok(None) => Ok(ControlFlow::Continue(())),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.instrument(tracing::info_span!(
|
||||
"Awaiting transaction receipt",
|
||||
?transaction_hash
|
||||
))
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn trace_transaction(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
trace_options: GethDebugTracingOptions,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create provider for tracing")?;
|
||||
poll(
|
||||
Self::TRACE_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
move || {
|
||||
let provider = provider.clone();
|
||||
let trace_options = trace_options.clone();
|
||||
async move {
|
||||
match provider
|
||||
.debug_trace_transaction(tx_hash, trace_options)
|
||||
.await
|
||||
{
|
||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn state_diff(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<DiffMode>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
|
||||
diff_mode: Some(true),
|
||||
disable_code: None,
|
||||
disable_storage: None,
|
||||
});
|
||||
match self
|
||||
.trace_transaction(tx_hash, trace_options)
|
||||
.await
|
||||
.context("Failed to trace transaction for prestate diff")?
|
||||
.try_into_pre_state_frame()
|
||||
.context("Failed to convert trace into pre-state frame")?
|
||||
{
|
||||
PreStateFrame::Diff(diff) => Ok(diff),
|
||||
_ => anyhow::bail!("expected a diff mode trace"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn balance_of(
|
||||
&self,
|
||||
address: Address,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider()
|
||||
.await
|
||||
.context("Failed to get the Geth provider")?
|
||||
.get_balance(address)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn latest_state_proof(
|
||||
&self,
|
||||
address: Address,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider()
|
||||
.await
|
||||
.context("Failed to get the Geth provider")?
|
||||
.get_proof(address, keys)
|
||||
.latest()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
// #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn resolver(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Arc<dyn ResolverApi + '_>>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let id = self.id;
|
||||
let provider = self.provider().await?;
|
||||
Ok(Arc::new(GethNodeResolver { id, provider }) as Arc<dyn ResolverApi>)
|
||||
})
|
||||
}
|
||||
|
||||
fn evm_version(&self) -> EVMVersion {
|
||||
EVMVersion::Cancun
|
||||
}
|
||||
|
||||
fn subscribe_to_full_blocks_information(
|
||||
&self,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
|
||||
+ '_,
|
||||
>,
|
||||
> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create the provider for block subscription")?;
|
||||
let block_subscription = provider.subscribe_full_blocks();
|
||||
let block_stream = block_subscription
|
||||
.into_stream()
|
||||
.await
|
||||
.context("Failed to create the block stream")?;
|
||||
|
||||
let mined_block_information_stream = block_stream.filter_map(|block| async {
|
||||
let block = block.ok()?;
|
||||
Some(MinedBlockInformation {
|
||||
ethereum_block_information: EthereumMinedBlockInformation {
|
||||
block_number: block.number(),
|
||||
block_timestamp: block.header.timestamp,
|
||||
mined_gas: block.header.gas_used as _,
|
||||
block_gas_limit: block.header.gas_limit as _,
|
||||
transaction_hashes: block
|
||||
.transactions
|
||||
.into_hashes()
|
||||
.as_hashes()
|
||||
.expect("Must be hashes")
|
||||
.to_vec(),
|
||||
},
|
||||
substrate_block_information: None,
|
||||
tx_counts: Default::default(),
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Box::pin(mined_block_information_stream)
|
||||
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||
})
|
||||
}
|
||||
|
||||
fn provider(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
|
||||
{
|
||||
Box::pin(
|
||||
self.provider()
|
||||
.map(|provider| provider.map(|provider| provider.erased())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GethNodeResolver {
|
||||
id: u32,
|
||||
provider: ConcreteProvider<Ethereum, Arc<EthereumWallet>>,
|
||||
}
|
||||
|
||||
impl ResolverApi for GethNodeResolver {
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn chain_id(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::primitives::ChainId>> + '_>> {
|
||||
Box::pin(async move { self.provider.get_chain_id().await.map_err(Into::into) })
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn transaction_gas_price(
|
||||
&self,
|
||||
tx_hash: TxHash,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_transaction_receipt(tx_hash)
|
||||
.await?
|
||||
.context("Failed to get the transaction receipt")
|
||||
.map(|receipt| receipt.effective_gas_price)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_gas_limit(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.map(|block| block.header.gas_limit as _)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_coinbase(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Address>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.map(|block| block.header.beneficiary)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_difficulty(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.map(|block| U256::from_be_bytes(block.header.mix_hash.0))
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_base_fee(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<u64>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.and_then(|block| {
|
||||
block
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.context("Failed to get the base fee per gas")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_hash(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockHash>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.map(|block| block.header.hash)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn block_timestamp(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockTimestamp>> + '_>> {
|
||||
Box::pin(async move {
|
||||
self.provider
|
||||
.get_block_by_number(number)
|
||||
.await
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.map(|block| block.header.timestamp)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn last_block_number(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockNumber>> + '_>> {
|
||||
Box::pin(async move { self.provider.get_block_number().await.map_err(Into::into) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for GethNode {
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn shutdown(&mut self) -> anyhow::Result<()> {
|
||||
drop(self.handle.take());
|
||||
|
||||
// Remove the node's database so that subsequent runs do not run on the same database. We
|
||||
// ignore the error just in case the directory didn't exist in the first place and therefore
|
||||
// there's nothing to be deleted.
|
||||
let _ = remove_dir_all(self.base_directory.join(Self::DATA_DIRECTORY));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
|
||||
self.init(genesis)?.spawn_process()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn version(&self) -> anyhow::Result<String> {
|
||||
let output = Command::new(&self.geth)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed to spawn geth --version process")?
|
||||
.wait_with_output()
|
||||
.context("Failed to wait for geth --version output")?
|
||||
.stdout;
|
||||
Ok(String::from_utf8_lossy(&output).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GethNode {
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn drop(&mut self) {
|
||||
self.shutdown().expect("Failed to shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> TestExecutionContext {
|
||||
TestExecutionContext::default()
|
||||
}
|
||||
|
||||
fn new_node() -> (TestExecutionContext, GethNode) {
|
||||
let context = test_config();
|
||||
let mut node = GethNode::new(&context);
|
||||
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
||||
.expect("Failed to initialize the node")
|
||||
.spawn_process()
|
||||
.expect("Failed to spawn the node process");
|
||||
(context, node)
|
||||
}
|
||||
|
||||
fn shared_state() -> &'static (TestExecutionContext, GethNode) {
|
||||
static STATE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node);
|
||||
&STATE
|
||||
}
|
||||
|
||||
fn shared_node() -> &'static GethNode {
|
||||
&shared_state().1
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
|
||||
// Arrange
|
||||
let (context, node) = shared_state();
|
||||
|
||||
let account_address = context
|
||||
.wallet_configuration
|
||||
.wallet()
|
||||
.default_signer()
|
||||
.address();
|
||||
let transaction = TransactionRequest::default()
|
||||
.to(account_address)
|
||||
.value(U256::from(100_000_000_000_000u128));
|
||||
|
||||
// Act
|
||||
let receipt = node.execute_transaction(transaction).await;
|
||||
|
||||
// Assert
|
||||
let _ = receipt.expect("Failed to get the receipt for the transfer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
fn version_works() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let version = node.version();
|
||||
|
||||
// Assert
|
||||
let version = version.expect("Failed to get the version");
|
||||
assert!(
|
||||
version.starts_with("geth version"),
|
||||
"expected version string, got: '{version}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_chain_id_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let chain_id = node.resolver().await.unwrap().chain_id().await;
|
||||
|
||||
// Assert
|
||||
let chain_id = chain_id.expect("Failed to get the chain id");
|
||||
assert_eq!(chain_id, 420_420_420);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_gas_limit_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let gas_limit = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_gas_limit(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let _ = gas_limit.expect("Failed to get the gas limit");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_coinbase_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let coinbase = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_coinbase(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let _ = coinbase.expect("Failed to get the coinbase");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_block_difficulty_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_difficulty = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_difficulty(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let _ = block_difficulty.expect("Failed to get the block difficulty");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_block_hash_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_hash = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_hash(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let _ = block_hash.expect("Failed to get the block hash");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_block_timestamp_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_timestamp = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_timestamp(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn can_get_block_number_from_node() {
|
||||
// Arrange
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_number = node.resolver().await.unwrap().last_block_number().await;
|
||||
|
||||
// Assert
|
||||
let _ = block_number.expect("Failed to get the block number");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
pub mod geth;
|
||||
pub mod lighthouse_geth;
|
||||
pub mod substrate;
|
||||
pub mod zombienet;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
||||
//! This crate implements concurrent handling of testing node.
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
thread,
|
||||
};
|
||||
|
||||
use revive_dt_common::cached_fs::read_to_string;
|
||||
|
||||
use anyhow::Context;
|
||||
use revive_dt_config::Arguments;
|
||||
use tracing::info;
|
||||
|
||||
use crate::Node;
|
||||
|
||||
/// The node pool starts one or more [Node] which then can be accessed
|
||||
/// in a round robbin fasion.
|
||||
pub struct NodePool<T> {
|
||||
next: AtomicUsize,
|
||||
nodes: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> NodePool<T>
|
||||
where
|
||||
T: Node + Send + 'static,
|
||||
{
|
||||
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
|
||||
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
|
||||
let nodes = config.number_of_nodes;
|
||||
let genesis = read_to_string(&config.genesis_file).context(format!(
|
||||
"can not read genesis file: {}",
|
||||
config.genesis_file.display()
|
||||
))?;
|
||||
|
||||
let mut handles = Vec::with_capacity(nodes);
|
||||
for _ in 0..nodes {
|
||||
let config = config.clone();
|
||||
let genesis = genesis.clone();
|
||||
handles.push(thread::spawn(move || spawn_node::<T>(&config, genesis)));
|
||||
}
|
||||
|
||||
let mut nodes = Vec::with_capacity(nodes);
|
||||
for handle in handles {
|
||||
nodes.push(
|
||||
handle
|
||||
.join()
|
||||
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))?
|
||||
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
nodes,
|
||||
next: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a handle to the next node.
|
||||
pub fn round_robbin(&self) -> &T {
|
||||
let current = self.next.fetch_add(1, Ordering::SeqCst) % self.nodes.len();
|
||||
self.nodes.get(current).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
|
||||
let mut node = T::new(args);
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawning node"
|
||||
);
|
||||
node.spawn(genesis)?;
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawned node"
|
||||
);
|
||||
Ok(node)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use alloy::transports::BoxFuture;
|
||||
use tokio::sync::Semaphore;
|
||||
use tower::{Layer, Service};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConcurrencyLimiterLayer {
|
||||
semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl ConcurrencyLimiterLayer {
|
||||
pub fn new(permit_count: usize) -> Self {
|
||||
Self {
|
||||
semaphore: Arc::new(Semaphore::new(permit_count)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for ConcurrencyLimiterLayer {
|
||||
type Service = ConcurrencyLimiterService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
ConcurrencyLimiterService {
|
||||
service: inner,
|
||||
semaphore: self.semaphore.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConcurrencyLimiterService<S> {
|
||||
service: S,
|
||||
semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl<S, Request> Service<Request> for ConcurrencyLimiterService<S>
|
||||
where
|
||||
S: Service<Request> + Send,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
let semaphore = self.semaphore.clone();
|
||||
let future = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let _permit = semaphore
|
||||
.acquire()
|
||||
.await
|
||||
.expect("Semaphore has been closed");
|
||||
tracing::debug!(
|
||||
available_permits = semaphore.available_permits(),
|
||||
"Acquired Semaphore Permit"
|
||||
);
|
||||
future.await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ use alloy::{
|
||||
transports::TransportResult,
|
||||
};
|
||||
|
||||
// Percentage padding applied to estimated gas (e.g. 120 = 20% padding)
|
||||
const GAS_ESTIMATE_PADDING_NUMERATOR: u64 = 120;
|
||||
const GAS_ESTIMATE_PADDING_DENOMINATOR: u64 = 100;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FallbackGasFiller {
|
||||
inner: GasFiller,
|
||||
@@ -30,6 +34,12 @@ impl FallbackGasFiller {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FallbackGasFiller {
|
||||
fn default() -> Self {
|
||||
FallbackGasFiller::new(25_000_000, 1_000_000_000, 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> TxFiller<N> for FallbackGasFiller
|
||||
where
|
||||
N: Network,
|
||||
@@ -50,11 +60,12 @@ where
|
||||
provider: &P,
|
||||
tx: &<N as Network>::TransactionRequest,
|
||||
) -> TransportResult<Self::Fillable> {
|
||||
// Try to fetch GasFiller’s “fillable” (gas_price, base_fee, estimate_gas, …)
|
||||
// If it errors (i.e. tx would revert under eth_estimateGas), swallow it.
|
||||
match self.inner.prepare(provider, tx).await {
|
||||
Ok(fill) => Ok(Some(fill)),
|
||||
Err(_) => Ok(None),
|
||||
Err(err) => {
|
||||
tracing::debug!(error = ?err, "Gas Provider Estimation Failed, using fallback");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +75,17 @@ where
|
||||
mut tx: alloy::providers::SendableTx<N>,
|
||||
) -> TransportResult<SendableTx<N>> {
|
||||
if let Some(fill) = fillable {
|
||||
// our inner GasFiller succeeded — use it
|
||||
self.inner.fill(fill, tx).await
|
||||
let mut tx = self.inner.fill(fill, tx).await?;
|
||||
if let Some(builder) = tx.as_mut_builder() {
|
||||
if let Some(estimated) = builder.gas_limit() {
|
||||
let padded = estimated
|
||||
.checked_mul(GAS_ESTIMATE_PADDING_NUMERATOR)
|
||||
.and_then(|v| v.checked_div(GAS_ESTIMATE_PADDING_DENOMINATOR))
|
||||
.unwrap_or(u64::MAX);
|
||||
builder.set_gas_limit(padded);
|
||||
}
|
||||
}
|
||||
Ok(tx)
|
||||
} else {
|
||||
if let Some(builder) = tx.as_mut_builder() {
|
||||
builder.set_gas_limit(self.default_gas_limit);
|
||||
@@ -0,0 +1,7 @@
|
||||
mod concurrency_limiter;
|
||||
mod fallback_gas_filler;
|
||||
mod provider;
|
||||
|
||||
pub use concurrency_limiter::*;
|
||||
pub use fallback_gas_filler::*;
|
||||
pub use provider::*;
|
||||
@@ -0,0 +1,132 @@
|
||||
use std::{ops::ControlFlow, sync::LazyLock, time::Duration};
|
||||
|
||||
use alloy::{
|
||||
network::{Ethereum, Network, NetworkWallet, TransactionBuilder4844},
|
||||
providers::{
|
||||
Identity, PendingTransactionBuilder, Provider, ProviderBuilder, RootProvider,
|
||||
fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller},
|
||||
},
|
||||
rpc::client::ClientBuilder,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use revive_dt_common::futures::{PollingWaitBehavior, poll};
|
||||
use tracing::{Instrument, debug, info, info_span};
|
||||
|
||||
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
|
||||
|
||||
pub type ConcreteProvider<N, W> = FillProvider<
|
||||
JoinFill<
|
||||
JoinFill<JoinFill<JoinFill<Identity, FallbackGasFiller>, ChainIdFiller>, NonceFiller>,
|
||||
WalletFiller<W>,
|
||||
>,
|
||||
RootProvider<N>,
|
||||
N,
|
||||
>;
|
||||
|
||||
pub async fn construct_concurrency_limited_provider<N, W>(
|
||||
rpc_url: &str,
|
||||
fallback_gas_filler: FallbackGasFiller,
|
||||
chain_id_filler: ChainIdFiller,
|
||||
nonce_filler: NonceFiller,
|
||||
wallet: W,
|
||||
) -> Result<ConcreteProvider<N, W>>
|
||||
where
|
||||
N: Network<TransactionRequest: TransactionBuilder4844>,
|
||||
W: NetworkWallet<N>,
|
||||
Identity: TxFiller<N>,
|
||||
FallbackGasFiller: TxFiller<N>,
|
||||
ChainIdFiller: TxFiller<N>,
|
||||
NonceFiller: TxFiller<N>,
|
||||
WalletFiller<W>: TxFiller<N>,
|
||||
{
|
||||
// This is a global limit on the RPC concurrency that applies to all of the providers created
|
||||
// by the framework. With this limit, it means that we can have a maximum of N concurrent
|
||||
// requests at any point of time and no more than that. This is done in an effort to stabilize
|
||||
// the framework from some of the interment issues that we've been seeing related to RPC calls.
|
||||
static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock<ConcurrencyLimiterLayer> =
|
||||
LazyLock::new(|| ConcurrencyLimiterLayer::new(500));
|
||||
|
||||
let client = ClientBuilder::default()
|
||||
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
|
||||
.connect(rpc_url)
|
||||
.await
|
||||
.context("Failed to construct the RPC client")?;
|
||||
|
||||
let provider = ProviderBuilder::new()
|
||||
.disable_recommended_fillers()
|
||||
.network::<N>()
|
||||
.filler(fallback_gas_filler)
|
||||
.filler(chain_id_filler)
|
||||
.filler(nonce_filler)
|
||||
.wallet(wallet)
|
||||
.connect_client(client);
|
||||
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
pub async fn execute_transaction<N, W>(
|
||||
provider: ConcreteProvider<N, W>,
|
||||
transaction: N::TransactionRequest,
|
||||
) -> Result<N::ReceiptResponse>
|
||||
where
|
||||
N: Network<
|
||||
TransactionRequest: TransactionBuilder4844,
|
||||
TxEnvelope = <Ethereum as Network>::TxEnvelope,
|
||||
>,
|
||||
W: NetworkWallet<N>,
|
||||
Identity: TxFiller<N>,
|
||||
FallbackGasFiller: TxFiller<N>,
|
||||
ChainIdFiller: TxFiller<N>,
|
||||
NonceFiller: TxFiller<N>,
|
||||
WalletFiller<W>: TxFiller<N>,
|
||||
{
|
||||
let sendable_transaction = provider
|
||||
.fill(transaction)
|
||||
.await
|
||||
.context("Failed to fill transaction")?;
|
||||
|
||||
let transaction_envelope = sendable_transaction
|
||||
.try_into_envelope()
|
||||
.context("Failed to convert transaction into an envelope")?;
|
||||
let tx_hash = *transaction_envelope.tx_hash();
|
||||
|
||||
let mut pending_transaction = match provider.send_tx_envelope(transaction_envelope).await {
|
||||
Ok(pending_transaction) => pending_transaction,
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
|
||||
if error_string.contains("Transaction Already Imported") {
|
||||
PendingTransactionBuilder::<N>::new(provider.root().clone(), tx_hash)
|
||||
} else {
|
||||
return Err(error).context(format!("Failed to submit transaction {tx_hash}"));
|
||||
}
|
||||
}
|
||||
};
|
||||
debug!(%tx_hash, "Submitted Transaction");
|
||||
|
||||
pending_transaction.set_timeout(Some(Duration::from_secs(120)));
|
||||
let tx_hash = pending_transaction.watch().await.context(format!(
|
||||
"Transaction inclusion watching timeout for {tx_hash}"
|
||||
))?;
|
||||
|
||||
poll(
|
||||
Duration::from_secs(60),
|
||||
PollingWaitBehavior::Constant(Duration::from_secs(3)),
|
||||
|| {
|
||||
let provider = provider.clone();
|
||||
|
||||
async move {
|
||||
match provider.get_transaction_receipt(tx_hash).await {
|
||||
Ok(Some(receipt)) => {
|
||||
info!("Found the transaction receipt");
|
||||
Ok(ControlFlow::Break(receipt))
|
||||
}
|
||||
_ => Ok(ControlFlow::Continue(())),
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.instrument(info_span!("Polling for receipt", %tx_hash))
|
||||
.await
|
||||
.context(format!("Polling for receipt failed for {tx_hash}"))
|
||||
}
|
||||
@@ -13,10 +13,11 @@ revive-dt-config = { workspace = true }
|
||||
revive-dt-format = { workspace = true }
|
||||
revive-dt-compiler = { workspace = true }
|
||||
|
||||
alloy-primitives = { workspace = true }
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
indexmap = { workspace = true, features = ["serde"] }
|
||||
itertools = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
+528
-121
@@ -4,18 +4,21 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fs::OpenOptions,
|
||||
ops::{Add, Div},
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use alloy_primitives::Address;
|
||||
use anyhow::Result;
|
||||
use alloy::primitives::{Address, BlockNumber, BlockTimestamp, TxHash};
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use revive_dt_common::types::PlatformIdentifier;
|
||||
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
|
||||
use revive_dt_config::{Arguments, TestingPlatform};
|
||||
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
||||
use revive_dt_config::Context;
|
||||
use revive_dt_format::{case::CaseIdx, metadata::ContractInstance, steps::StepPath};
|
||||
use semver::Version;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{DisplayFromStr, serde_as};
|
||||
use tokio::sync::{
|
||||
broadcast::{Sender, channel},
|
||||
@@ -36,11 +39,11 @@ pub struct ReportAggregator {
|
||||
}
|
||||
|
||||
impl ReportAggregator {
|
||||
pub fn new(config: Arguments) -> Self {
|
||||
pub fn new(context: Context) -> Self {
|
||||
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
||||
let (listener_tx, _) = channel::<ReporterEvent>(1024);
|
||||
let (listener_tx, _) = channel::<ReporterEvent>(0xFFFF);
|
||||
Self {
|
||||
report: Report::new(config),
|
||||
report: Report::new(context),
|
||||
remaining_cases: Default::default(),
|
||||
runner_tx: Some(runner_tx),
|
||||
runner_rx,
|
||||
@@ -61,14 +64,11 @@ impl ReportAggregator {
|
||||
debug!("Starting to aggregate report");
|
||||
|
||||
while let Some(event) = self.runner_rx.recv().await {
|
||||
debug!(?event, "Received Event");
|
||||
debug!(event = event.variant_name(), "Received Event");
|
||||
match event {
|
||||
RunnerEvent::SubscribeToEvents(event) => {
|
||||
self.handle_subscribe_to_events_event(*event);
|
||||
}
|
||||
RunnerEvent::CorpusFileDiscovery(event) => {
|
||||
self.handle_corpus_file_discovered_event(*event)
|
||||
}
|
||||
RunnerEvent::MetadataFileDiscovery(event) => {
|
||||
self.handle_metadata_file_discovery_event(*event);
|
||||
}
|
||||
@@ -84,11 +84,8 @@ impl ReportAggregator {
|
||||
RunnerEvent::TestIgnored(event) => {
|
||||
self.handle_test_ignored_event(*event);
|
||||
}
|
||||
RunnerEvent::LeaderNodeAssigned(event) => {
|
||||
self.handle_leader_node_assigned_event(*event);
|
||||
}
|
||||
RunnerEvent::FollowerNodeAssigned(event) => {
|
||||
self.handle_follower_node_assigned_event(*event);
|
||||
RunnerEvent::NodeAssigned(event) => {
|
||||
self.handle_node_assigned_event(*event);
|
||||
}
|
||||
RunnerEvent::PreLinkContractsCompilationSucceeded(event) => {
|
||||
self.handle_pre_link_contracts_compilation_succeeded_event(*event)
|
||||
@@ -108,24 +105,52 @@ impl ReportAggregator {
|
||||
RunnerEvent::ContractDeployed(event) => {
|
||||
self.handle_contract_deployed_event(*event);
|
||||
}
|
||||
RunnerEvent::Completion(_) => {
|
||||
break;
|
||||
}
|
||||
/* Benchmarks Events */
|
||||
RunnerEvent::StepTransactionInformation(event) => {
|
||||
self.handle_step_transaction_information(*event)
|
||||
}
|
||||
RunnerEvent::ContractInformation(event) => {
|
||||
self.handle_contract_information(*event);
|
||||
}
|
||||
RunnerEvent::BlockMined(event) => self.handle_block_mined(*event),
|
||||
}
|
||||
}
|
||||
self.handle_completion(CompletionEvent {});
|
||||
debug!("Report aggregation completed");
|
||||
|
||||
let file_name = {
|
||||
let current_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let current_timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("System clock is before UNIX_EPOCH; cannot compute report timestamp")?
|
||||
.as_secs();
|
||||
let mut file_name = current_timestamp.to_string();
|
||||
file_name.push_str(".json");
|
||||
file_name
|
||||
};
|
||||
let file_path = self.report.config.directory().join(file_name);
|
||||
let file_path = self
|
||||
.report
|
||||
.context
|
||||
.working_directory_configuration()
|
||||
.as_path()
|
||||
.join(file_name);
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.read(false)
|
||||
.open(file_path)?;
|
||||
serde_json::to_writer_pretty(file, &self.report)?;
|
||||
.open(&file_path)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to open report file for writing: {}",
|
||||
file_path.display()
|
||||
)
|
||||
})?;
|
||||
serde_json::to_writer_pretty(&file, &self.report).with_context(|| {
|
||||
format!("Failed to serialize report JSON to {}", file_path.display())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -134,10 +159,6 @@ impl ReportAggregator {
|
||||
let _ = event.tx.send(self.listener_tx.subscribe());
|
||||
}
|
||||
|
||||
fn handle_corpus_file_discovered_event(&mut self, event: CorpusFileDiscoveryEvent) {
|
||||
self.report.corpora.push(event.corpus);
|
||||
}
|
||||
|
||||
fn handle_metadata_file_discovery_event(&mut self, event: MetadataFileDiscoveryEvent) {
|
||||
self.report.metadata_files.insert(event.path.clone());
|
||||
}
|
||||
@@ -216,17 +237,19 @@ impl ReportAggregator {
|
||||
|
||||
let case_status = self
|
||||
.report
|
||||
.test_case_information
|
||||
.execution_information
|
||||
.entry(specifier.metadata_file_path.clone().into())
|
||||
.or_default()
|
||||
.entry(specifier.solc_mode.clone())
|
||||
.or_default()
|
||||
.case_reports
|
||||
.iter()
|
||||
.map(|(case_idx, case_report)| {
|
||||
(
|
||||
*case_idx,
|
||||
case_report.status.clone().expect("Can't be uninitialized"),
|
||||
)
|
||||
.flat_map(|(case_idx, mode_to_execution_map)| {
|
||||
let case_status = mode_to_execution_map
|
||||
.mode_execution_reports
|
||||
.get(&specifier.solc_mode)?
|
||||
.status
|
||||
.clone()
|
||||
.expect("Can't be uninitialized");
|
||||
Some((*case_idx, case_status))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let event = ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
|
||||
@@ -241,28 +264,15 @@ impl ReportAggregator {
|
||||
let _ = self.listener_tx.send(event);
|
||||
}
|
||||
|
||||
fn handle_leader_node_assigned_event(&mut self, event: LeaderNodeAssignedEvent) {
|
||||
fn handle_node_assigned_event(&mut self, event: NodeAssignedEvent) {
|
||||
let execution_information = self.execution_information(&ExecutionSpecifier {
|
||||
test_specifier: event.test_specifier,
|
||||
node_id: event.id,
|
||||
node_designation: NodeDesignation::Leader,
|
||||
platform_identifier: event.platform_identifier,
|
||||
});
|
||||
execution_information.node = Some(TestCaseNodeInformation {
|
||||
id: event.id,
|
||||
platform: event.platform,
|
||||
connection_string: event.connection_string,
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_follower_node_assigned_event(&mut self, event: FollowerNodeAssignedEvent) {
|
||||
let execution_information = self.execution_information(&ExecutionSpecifier {
|
||||
test_specifier: event.test_specifier,
|
||||
node_id: event.id,
|
||||
node_designation: NodeDesignation::Follower,
|
||||
});
|
||||
execution_information.node = Some(TestCaseNodeInformation {
|
||||
id: event.id,
|
||||
platform: event.platform,
|
||||
platform_identifier: event.platform_identifier,
|
||||
connection_string: event.connection_string,
|
||||
});
|
||||
}
|
||||
@@ -271,8 +281,16 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PreLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
let include_output = self.report.config.report_include_compiler_output;
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
@@ -300,8 +318,16 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PostLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
let include_output = self.report.config.report_include_compiler_output;
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
@@ -329,21 +355,13 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PreLinkContractsCompilationFailedEvent,
|
||||
) {
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
let compiler_input = if include_input {
|
||||
event.compiler_input
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
execution_information.pre_link_compilation_status = Some(CompilationStatus::Failure {
|
||||
reason: event.reason,
|
||||
compiler_version: event.compiler_version,
|
||||
compiler_path: event.compiler_path,
|
||||
compiler_input,
|
||||
compiler_input: event.compiler_input,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,21 +369,13 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PostLinkContractsCompilationFailedEvent,
|
||||
) {
|
||||
let include_input = self.report.config.report_include_compiler_input;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
let compiler_input = if include_input {
|
||||
event.compiler_input
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
execution_information.post_link_compilation_status = Some(CompilationStatus::Failure {
|
||||
reason: event.reason,
|
||||
compiler_version: event.compiler_version,
|
||||
compiler_path: event.compiler_path,
|
||||
compiler_input,
|
||||
compiler_input: event.compiler_input,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -378,18 +388,157 @@ impl ReportAggregator {
|
||||
self.execution_information(&event.execution_specifier)
|
||||
.deployed_contracts
|
||||
.get_or_insert_default()
|
||||
.insert(event.contract_instance, event.address);
|
||||
.insert(event.contract_instance.clone(), event.address);
|
||||
self.test_case_report(&event.execution_specifier.test_specifier)
|
||||
.contract_addresses
|
||||
.entry(event.contract_instance)
|
||||
.or_default()
|
||||
.entry(event.execution_specifier.platform_identifier)
|
||||
.or_default()
|
||||
.push(event.address);
|
||||
}
|
||||
|
||||
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport {
|
||||
fn handle_completion(&mut self, _: CompletionEvent) {
|
||||
self.runner_rx.close();
|
||||
self.handle_metrics_computation();
|
||||
}
|
||||
|
||||
fn handle_metrics_computation(&mut self) {
|
||||
for report in self.report.execution_information.values_mut() {
|
||||
for report in report.case_reports.values_mut() {
|
||||
for report in report.mode_execution_reports.values_mut() {
|
||||
for (platform_identifier, block_information) in
|
||||
report.mined_block_information.iter_mut()
|
||||
{
|
||||
block_information.sort_by(|a, b| {
|
||||
a.ethereum_block_information
|
||||
.block_number
|
||||
.cmp(&b.ethereum_block_information.block_number)
|
||||
});
|
||||
|
||||
// Computing the TPS.
|
||||
let tps = block_information
|
||||
.iter()
|
||||
.tuple_windows::<(_, _)>()
|
||||
.map(|(block1, block2)| {
|
||||
block2.ethereum_block_information.transaction_hashes.len() as u64
|
||||
/ (block2.ethereum_block_information.block_timestamp
|
||||
- block1.ethereum_block_information.block_timestamp)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
report
|
||||
.metrics
|
||||
.get_or_insert_default()
|
||||
.transaction_per_second
|
||||
.with_list(*platform_identifier, tps);
|
||||
|
||||
// Computing the GPS.
|
||||
let gps = block_information
|
||||
.iter()
|
||||
.tuple_windows::<(_, _)>()
|
||||
.map(|(block1, block2)| {
|
||||
block2.ethereum_block_information.mined_gas as u64
|
||||
/ (block2.ethereum_block_information.block_timestamp
|
||||
- block1.ethereum_block_information.block_timestamp)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
report
|
||||
.metrics
|
||||
.get_or_insert_default()
|
||||
.gas_per_second
|
||||
.with_list(*platform_identifier, gps);
|
||||
|
||||
// Computing the gas block fullness
|
||||
let gas_block_fullness = block_information
|
||||
.iter()
|
||||
.map(|block| block.gas_block_fullness_percentage())
|
||||
.map(|v| v as u64)
|
||||
.collect::<Vec<_>>();
|
||||
report
|
||||
.metrics
|
||||
.get_or_insert_default()
|
||||
.gas_block_fullness
|
||||
.with_list(*platform_identifier, gas_block_fullness);
|
||||
|
||||
// Computing the ref-time block fullness
|
||||
let reftime_block_fullness = block_information
|
||||
.iter()
|
||||
.filter_map(|block| block.ref_time_block_fullness_percentage())
|
||||
.map(|v| v as u64)
|
||||
.collect::<Vec<_>>();
|
||||
if !reftime_block_fullness.is_empty() {
|
||||
report
|
||||
.metrics
|
||||
.get_or_insert_default()
|
||||
.ref_time_block_fullness
|
||||
.get_or_insert_default()
|
||||
.with_list(*platform_identifier, reftime_block_fullness);
|
||||
}
|
||||
|
||||
// Computing the proof size block fullness
|
||||
let proof_size_block_fullness = block_information
|
||||
.iter()
|
||||
.filter_map(|block| block.proof_size_block_fullness_percentage())
|
||||
.map(|v| v as u64)
|
||||
.collect::<Vec<_>>();
|
||||
if !proof_size_block_fullness.is_empty() {
|
||||
report
|
||||
.metrics
|
||||
.get_or_insert_default()
|
||||
.proof_size_block_fullness
|
||||
.get_or_insert_default()
|
||||
.with_list(*platform_identifier, proof_size_block_fullness);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_step_transaction_information(&mut self, event: StepTransactionInformationEvent) {
|
||||
self.test_case_report(&event.execution_specifier.test_specifier)
|
||||
.steps
|
||||
.entry(event.step_path)
|
||||
.or_default()
|
||||
.transactions
|
||||
.entry(event.execution_specifier.platform_identifier)
|
||||
.or_default()
|
||||
.push(event.transaction_information);
|
||||
}
|
||||
|
||||
fn handle_contract_information(&mut self, event: ContractInformationEvent) {
|
||||
self.test_case_report(&event.execution_specifier.test_specifier)
|
||||
.compiled_contracts
|
||||
.entry(event.source_code_path)
|
||||
.or_default()
|
||||
.entry(event.contract_name)
|
||||
.or_default()
|
||||
.contract_size
|
||||
.insert(
|
||||
event.execution_specifier.platform_identifier,
|
||||
event.contract_size,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_block_mined(&mut self, event: BlockMinedEvent) {
|
||||
self.test_case_report(&event.execution_specifier.test_specifier)
|
||||
.mined_block_information
|
||||
.entry(event.execution_specifier.platform_identifier)
|
||||
.or_default()
|
||||
.push(event.mined_block_information);
|
||||
}
|
||||
|
||||
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut ExecutionReport {
|
||||
self.report
|
||||
.test_case_information
|
||||
.execution_information
|
||||
.entry(specifier.metadata_file_path.clone().into())
|
||||
.or_default()
|
||||
.entry(specifier.solc_mode.clone())
|
||||
.or_default()
|
||||
.case_reports
|
||||
.entry(specifier.case_idx)
|
||||
.or_default()
|
||||
.mode_execution_reports
|
||||
.entry(specifier.solc_mode.clone())
|
||||
.or_default()
|
||||
}
|
||||
|
||||
fn execution_information(
|
||||
@@ -397,65 +546,87 @@ impl ReportAggregator {
|
||||
specifier: &ExecutionSpecifier,
|
||||
) -> &mut ExecutionInformation {
|
||||
let test_case_report = self.test_case_report(&specifier.test_specifier);
|
||||
match specifier.node_designation {
|
||||
NodeDesignation::Leader => test_case_report
|
||||
.leader_execution_information
|
||||
.get_or_insert_default(),
|
||||
NodeDesignation::Follower => test_case_report
|
||||
.follower_execution_information
|
||||
.get_or_insert_default(),
|
||||
}
|
||||
test_case_report
|
||||
.platform_execution
|
||||
.entry(specifier.platform_identifier)
|
||||
.or_default()
|
||||
.get_or_insert_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
/// The configuration that the tool was started up with.
|
||||
pub config: Arguments,
|
||||
/// The platform of the leader chain.
|
||||
pub leader_platform: TestingPlatform,
|
||||
/// The platform of the follower chain.
|
||||
pub follower_platform: TestingPlatform,
|
||||
/// The list of corpus files that the tool found.
|
||||
pub corpora: Vec<Corpus>,
|
||||
/// The context that the tool was started up with.
|
||||
pub context: Context,
|
||||
/// The list of metadata files that were found by the tool.
|
||||
pub metadata_files: BTreeSet<MetadataFilePath>,
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// Information relating to each test case.
|
||||
#[serde_as(as = "BTreeMap<_, HashMap<DisplayFromStr, BTreeMap<DisplayFromStr, _>>>")]
|
||||
pub test_case_information:
|
||||
BTreeMap<MetadataFilePath, HashMap<Mode, BTreeMap<CaseIdx, TestCaseReport>>>,
|
||||
pub execution_information: BTreeMap<MetadataFilePath, MetadataFileReport>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn new(config: Arguments) -> Self {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self {
|
||||
leader_platform: config.leader,
|
||||
follower_platform: config.follower,
|
||||
config,
|
||||
corpora: Default::default(),
|
||||
context,
|
||||
metrics: Default::default(),
|
||||
metadata_files: Default::default(),
|
||||
test_case_information: Default::default(),
|
||||
execution_information: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
pub struct TestCaseReport {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct MetadataFileReport {
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// The report of each case keyed by the case idx.
|
||||
pub case_reports: BTreeMap<CaseIdx, CaseReport>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct CaseReport {
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<Metrics>,
|
||||
/// The [`ExecutionReport`] for each one of the [`Mode`]s.
|
||||
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
|
||||
pub mode_execution_reports: HashMap<Mode, ExecutionReport>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ExecutionReport {
|
||||
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<TestCaseStatus>,
|
||||
/// Information related to the execution on the leader.
|
||||
/// Metrics from the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub leader_execution_information: Option<ExecutionInformation>,
|
||||
/// Information related to the execution on the follower.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub follower_execution_information: Option<ExecutionInformation>,
|
||||
pub metrics: Option<Metrics>,
|
||||
/// Information related to the execution on one of the platforms.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub platform_execution: PlatformKeyedInformation<Option<ExecutionInformation>>,
|
||||
/// Information on the compiled contracts.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub compiled_contracts: BTreeMap<PathBuf, BTreeMap<String, ContractInformation>>,
|
||||
/// The addresses of the deployed contracts
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub contract_addresses: BTreeMap<ContractInstance, PlatformKeyedInformation<Vec<Address>>>,
|
||||
/// Information on the mined blocks as part of this execution.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub mined_block_information: PlatformKeyedInformation<Vec<MinedBlockInformation>>,
|
||||
/// Information tracked for each step that was executed.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub steps: BTreeMap<StepPath, StepReport>,
|
||||
}
|
||||
|
||||
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
||||
/// it was ignored.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum TestCaseStatus {
|
||||
/// The test case succeeded.
|
||||
@@ -478,19 +649,19 @@ pub enum TestCaseStatus {
|
||||
},
|
||||
}
|
||||
|
||||
/// Information related to the leader or follower node that's being used to execute the step.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
/// Information related to the platform node that's being used to execute the step.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestCaseNodeInformation {
|
||||
/// The ID of the node that this case is being executed on.
|
||||
pub id: usize,
|
||||
/// The platform of the node.
|
||||
pub platform: TestingPlatform,
|
||||
pub platform_identifier: PlatformIdentifier,
|
||||
/// The connection string of the node.
|
||||
pub connection_string: String,
|
||||
}
|
||||
|
||||
/// Execution information tied to the leader or the follower.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
/// Execution information tied to the platform.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ExecutionInformation {
|
||||
/// Information related to the node assigned to this test case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -510,7 +681,7 @@ pub struct ExecutionInformation {
|
||||
}
|
||||
|
||||
/// Information related to compilation
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum CompilationStatus {
|
||||
/// The compilation was successful.
|
||||
@@ -522,12 +693,12 @@ pub enum CompilationStatus {
|
||||
/// The path of the compiler used to compile the contracts.
|
||||
compiler_path: PathBuf,
|
||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
||||
/// the appropriate flag is set in the CLI configuration and if the contracts were not
|
||||
/// cached and the compiler was invoked.
|
||||
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||
/// the compiler was invoked.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_input: Option<CompilerInput>,
|
||||
/// The output of the compiler. This is only included if the appropriate flag is set in the
|
||||
/// CLI configurations.
|
||||
/// CLI contexts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_output: Option<CompilerOutput>,
|
||||
},
|
||||
@@ -542,9 +713,245 @@ pub enum CompilationStatus {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_path: Option<PathBuf>,
|
||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
||||
/// the appropriate flag is set in the CLI configuration and if the contracts were not
|
||||
/// cached and the compiler was invoked.
|
||||
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||
/// the compiler was invoked.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compiler_input: Option<CompilerInput>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Information on each step in the execution.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct StepReport {
|
||||
/// Information on the transactions submitted as part of this step.
|
||||
transactions: PlatformKeyedInformation<Vec<TransactionInformation>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TransactionInformation {
|
||||
/// The hash of the transaction
|
||||
pub transaction_hash: TxHash,
|
||||
pub submission_timestamp: u64,
|
||||
pub block_timestamp: u64,
|
||||
pub block_number: BlockNumber,
|
||||
}
|
||||
|
||||
/// The metrics we collect for our benchmarks.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Metrics {
|
||||
pub transaction_per_second: Metric<u64>,
|
||||
pub gas_per_second: Metric<u64>,
|
||||
/* Block Fullness */
|
||||
pub gas_block_fullness: Metric<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ref_time_block_fullness: Option<Metric<u64>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proof_size_block_fullness: Option<Metric<u64>>,
|
||||
}
|
||||
|
||||
/// The data that we store for a given metric (e.g., TPS).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Metric<T> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maximum: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mean: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub median: Option<PlatformKeyedInformation<T>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw: Option<PlatformKeyedInformation<Vec<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Metric<T>
|
||||
where
|
||||
T: Default
|
||||
+ Copy
|
||||
+ Ord
|
||||
+ PartialOrd
|
||||
+ Add<Output = T>
|
||||
+ Div<Output = T>
|
||||
+ TryFrom<usize, Error: std::fmt::Debug>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn platform_identifiers(&self) -> BTreeSet<PlatformIdentifier> {
|
||||
self.minimum
|
||||
.as_ref()
|
||||
.map(|m| m.keys())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(
|
||||
self.maximum
|
||||
.as_ref()
|
||||
.map(|m| m.keys())
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.chain(self.mean.as_ref().map(|m| m.keys()).into_iter().flatten())
|
||||
.chain(self.median.as_ref().map(|m| m.keys()).into_iter().flatten())
|
||||
.chain(self.raw.as_ref().map(|m| m.keys()).into_iter().flatten())
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn with_list(
|
||||
&mut self,
|
||||
platform_identifier: PlatformIdentifier,
|
||||
original_list: Vec<T>,
|
||||
) -> &mut Self {
|
||||
let mut list = original_list.clone();
|
||||
list.sort();
|
||||
let Some(min) = list.first().copied() else {
|
||||
return self;
|
||||
};
|
||||
let Some(max) = list.last().copied() else {
|
||||
return self;
|
||||
};
|
||||
let sum = list.iter().fold(T::default(), |acc, num| acc + *num);
|
||||
let mean = sum / TryInto::<T>::try_into(list.len()).unwrap();
|
||||
|
||||
let median = match list.len().is_multiple_of(2) {
|
||||
true => {
|
||||
let idx = list.len() / 2;
|
||||
let val1 = *list.get(idx - 1).unwrap();
|
||||
let val2 = *list.get(idx).unwrap();
|
||||
(val1 + val2) / TryInto::<T>::try_into(2usize).unwrap()
|
||||
}
|
||||
false => {
|
||||
let idx = list.len() / 2;
|
||||
*list.get(idx).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
self.minimum
|
||||
.get_or_insert_default()
|
||||
.insert(platform_identifier, min);
|
||||
self.maximum
|
||||
.get_or_insert_default()
|
||||
.insert(platform_identifier, max);
|
||||
self.mean
|
||||
.get_or_insert_default()
|
||||
.insert(platform_identifier, mean);
|
||||
self.median
|
||||
.get_or_insert_default()
|
||||
.insert(platform_identifier, median);
|
||||
self.raw
|
||||
.get_or_insert_default()
|
||||
.insert(platform_identifier, original_list);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn combine(&self, other: &Self) -> Self {
|
||||
let mut platform_identifiers = self.platform_identifiers();
|
||||
platform_identifiers.extend(other.platform_identifiers());
|
||||
|
||||
let mut this = Self::new();
|
||||
for platform_identifier in platform_identifiers {
|
||||
let mut l1 = self
|
||||
.raw
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&platform_identifier))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let l2 = other
|
||||
.raw
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&platform_identifier))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
l1.extend(l2);
|
||||
this.with_list(platform_identifier, l1);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ContractInformation {
|
||||
/// The size of the contract on the various platforms.
|
||||
pub contract_size: PlatformKeyedInformation<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct MinedBlockInformation {
|
||||
pub ethereum_block_information: EthereumMinedBlockInformation,
|
||||
pub substrate_block_information: Option<SubstrateMinedBlockInformation>,
|
||||
pub tx_counts: BTreeMap<StepPath, usize>,
|
||||
}
|
||||
|
||||
impl MinedBlockInformation {
|
||||
pub fn gas_block_fullness_percentage(&self) -> u8 {
|
||||
self.ethereum_block_information
|
||||
.gas_block_fullness_percentage()
|
||||
}
|
||||
|
||||
pub fn ref_time_block_fullness_percentage(&self) -> Option<u8> {
|
||||
self.substrate_block_information
|
||||
.as_ref()
|
||||
.map(|block| block.ref_time_block_fullness_percentage())
|
||||
}
|
||||
|
||||
pub fn proof_size_block_fullness_percentage(&self) -> Option<u8> {
|
||||
self.substrate_block_information
|
||||
.as_ref()
|
||||
.map(|block| block.proof_size_block_fullness_percentage())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct EthereumMinedBlockInformation {
|
||||
/// The block number.
|
||||
pub block_number: BlockNumber,
|
||||
|
||||
/// The block timestamp.
|
||||
pub block_timestamp: BlockTimestamp,
|
||||
|
||||
/// The amount of gas mined in the block.
|
||||
pub mined_gas: u128,
|
||||
|
||||
/// The gas limit of the block.
|
||||
pub block_gas_limit: u128,
|
||||
|
||||
/// The hashes of the transactions that were mined as part of the block.
|
||||
pub transaction_hashes: Vec<TxHash>,
|
||||
}
|
||||
|
||||
impl EthereumMinedBlockInformation {
|
||||
pub fn gas_block_fullness_percentage(&self) -> u8 {
|
||||
(self.mined_gas * 100 / self.block_gas_limit) as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct SubstrateMinedBlockInformation {
|
||||
/// The ref time for substrate based chains.
|
||||
pub ref_time: u128,
|
||||
|
||||
/// The max ref time for substrate based chains.
|
||||
pub max_ref_time: u64,
|
||||
|
||||
/// The proof size for substrate based chains.
|
||||
pub proof_size: u128,
|
||||
|
||||
/// The max proof size for substrate based chains.
|
||||
pub max_proof_size: u64,
|
||||
}
|
||||
|
||||
impl SubstrateMinedBlockInformation {
|
||||
pub fn ref_time_block_fullness_percentage(&self) -> u8 {
|
||||
(self.ref_time * 100 / self.max_ref_time as u128) as u8
|
||||
}
|
||||
|
||||
pub fn proof_size_block_fullness_percentage(&self) -> u8 {
|
||||
(self.proof_size * 100 / self.max_proof_size as u128) as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Information keyed by the platform identifier.
|
||||
pub type PlatformKeyedInformation<T> = BTreeMap<PlatformIdentifier, T>;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use revive_dt_common::define_wrapper_type;
|
||||
use revive_dt_common::{define_wrapper_type, types::PlatformIdentifier};
|
||||
use revive_dt_compiler::Mode;
|
||||
use revive_dt_format::{case::CaseIdx, input::StepIdx};
|
||||
use revive_dt_format::{case::CaseIdx, steps::StepPath};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
define_wrapper_type!(
|
||||
@@ -22,22 +22,16 @@ pub struct TestSpecifier {
|
||||
}
|
||||
|
||||
/// An absolute path for a test that also includes information about the node that it's assigned to
|
||||
/// and whether it's the leader or follower.
|
||||
/// and what platform it belongs to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ExecutionSpecifier {
|
||||
pub test_specifier: Arc<TestSpecifier>,
|
||||
pub node_id: usize,
|
||||
pub node_designation: NodeDesignation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum NodeDesignation {
|
||||
Leader,
|
||||
Follower,
|
||||
pub platform_identifier: PlatformIdentifier,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StepExecutionSpecifier {
|
||||
pub execution_specifier: Arc<ExecutionSpecifier>,
|
||||
pub step_idx: StepIdx,
|
||||
pub step_idx: StepPath,
|
||||
}
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
|
||||
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
||||
|
||||
use alloy_primitives::Address;
|
||||
use alloy::primitives::Address;
|
||||
use anyhow::Context as _;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_common::types::PlatformIdentifier;
|
||||
use revive_dt_compiler::{CompilerInput, CompilerOutput};
|
||||
use revive_dt_config::TestingPlatform;
|
||||
use revive_dt_format::metadata::ContractInstance;
|
||||
use revive_dt_format::metadata::Metadata;
|
||||
use revive_dt_format::{corpus::Corpus, metadata::ContractInstance};
|
||||
use revive_dt_format::steps::StepPath;
|
||||
use semver::Version;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
|
||||
use crate::MinedBlockInformation;
|
||||
use crate::TransactionInformation;
|
||||
use crate::{ExecutionSpecifier, ReporterEvent, TestSpecifier, common::MetadataFilePath};
|
||||
|
||||
macro_rules! __report_gen_emit_test_specific {
|
||||
@@ -343,6 +347,16 @@ macro_rules! define_event {
|
||||
),*
|
||||
}
|
||||
|
||||
impl $ident {
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant_ident { .. } => stringify!($variant_ident)
|
||||
),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[derive(Debug)]
|
||||
$(#[$variant_meta])*
|
||||
@@ -411,14 +425,14 @@ macro_rules! define_event {
|
||||
pub fn execution_specific_reporter(
|
||||
&self,
|
||||
node_id: impl Into<usize>,
|
||||
node_designation: impl Into<$crate::common::NodeDesignation>
|
||||
platform_identifier: impl Into<PlatformIdentifier>
|
||||
) -> [< $ident ExecutionSpecificReporter >] {
|
||||
[< $ident ExecutionSpecificReporter >] {
|
||||
reporter: self.reporter.clone(),
|
||||
execution_specifier: Arc::new($crate::common::ExecutionSpecifier {
|
||||
test_specifier: self.test_specifier.clone(),
|
||||
node_id: node_id.into(),
|
||||
node_designation: node_designation.into(),
|
||||
platform_identifier: platform_identifier.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -433,7 +447,7 @@ macro_rules! define_event {
|
||||
}
|
||||
|
||||
/// A reporter that's tied to a specific execution of the test case such as execution on
|
||||
/// a specific node like the leader or follower.
|
||||
/// a specific node from a specific platform.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct [< $ident ExecutionSpecificReporter >] {
|
||||
$vis reporter: [< $ident Reporter >],
|
||||
@@ -479,11 +493,6 @@ define_event! {
|
||||
/// The channel that the aggregator is to send the receive side of the channel on.
|
||||
tx: oneshot::Sender<broadcast::Receiver<ReporterEvent>>
|
||||
},
|
||||
/// An event emitted by runners when they've discovered a corpus file.
|
||||
CorpusFileDiscovery {
|
||||
/// The contents of the corpus file.
|
||||
corpus: Corpus
|
||||
},
|
||||
/// An event emitted by runners when they've discovered a metadata file.
|
||||
MetadataFileDiscovery {
|
||||
/// The path of the metadata file discovered.
|
||||
@@ -519,25 +528,14 @@ define_event! {
|
||||
/// A reason for the failure of the test.
|
||||
reason: String,
|
||||
},
|
||||
/// An event emitted when the test case is assigned a leader node.
|
||||
LeaderNodeAssigned {
|
||||
/// An event emitted when the test case is assigned a platform node.
|
||||
NodeAssigned {
|
||||
/// A specifier for the test that the assignment is for.
|
||||
test_specifier: Arc<TestSpecifier>,
|
||||
/// The ID of the node that this case is being executed on.
|
||||
id: usize,
|
||||
/// The platform of the node.
|
||||
platform: TestingPlatform,
|
||||
/// The connection string of the node.
|
||||
connection_string: String,
|
||||
},
|
||||
/// An event emitted when the test case is assigned a follower node.
|
||||
FollowerNodeAssigned {
|
||||
/// A specifier for the test that the assignment is for.
|
||||
test_specifier: Arc<TestSpecifier>,
|
||||
/// The ID of the node that this case is being executed on.
|
||||
id: usize,
|
||||
/// The platform of the node.
|
||||
platform: TestingPlatform,
|
||||
/// The identifier of the platform used.
|
||||
platform_identifier: PlatformIdentifier,
|
||||
/// The connection string of the node.
|
||||
connection_string: String,
|
||||
},
|
||||
@@ -623,6 +621,36 @@ define_event! {
|
||||
/// The address of the contract.
|
||||
address: Address
|
||||
},
|
||||
/// Reports the completion of the run.
|
||||
Completion {},
|
||||
|
||||
/* Benchmarks Events */
|
||||
/// An event emitted with information on a transaction that was submitted for a certain step
|
||||
/// of the execution.
|
||||
StepTransactionInformation {
|
||||
/// A specifier for the execution that's taking place.
|
||||
execution_specifier: Arc<ExecutionSpecifier>,
|
||||
/// The path of the step that this transaction belongs to.
|
||||
step_path: StepPath,
|
||||
/// Information about the transaction
|
||||
transaction_information: TransactionInformation
|
||||
},
|
||||
ContractInformation {
|
||||
/// A specifier for the execution that's taking place.
|
||||
execution_specifier: Arc<ExecutionSpecifier>,
|
||||
/// The path of the solidity source code that contains the contract.
|
||||
source_code_path: PathBuf,
|
||||
/// The name of the contract
|
||||
contract_name: String,
|
||||
/// The size of the contract
|
||||
contract_size: usize
|
||||
},
|
||||
BlockMined {
|
||||
/// A specifier for the execution that's taking place.
|
||||
execution_specifier: Arc<ExecutionSpecifier>,
|
||||
/// Information on the mined block,
|
||||
mined_block_information: MinedBlockInformation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,7 +658,8 @@ define_event! {
|
||||
impl RunnerEventReporter {
|
||||
pub async fn subscribe(&self) -> anyhow::Result<broadcast::Receiver<ReporterEvent>> {
|
||||
let (tx, rx) = oneshot::channel::<broadcast::Receiver<ReporterEvent>>();
|
||||
self.report_subscribe_to_events_event(tx)?;
|
||||
self.report_subscribe_to_events_event(tx)
|
||||
.context("Failed to send subscribe request to reporter task")?;
|
||||
rx.await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ use std::{
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use semver::Version;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::download::SolcDownloader;
|
||||
use anyhow::Context as _;
|
||||
|
||||
pub const SOLC_CACHE_DIRECTORY: &str = "solc";
|
||||
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
|
||||
@@ -19,7 +21,7 @@ pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new
|
||||
pub(crate) async fn get_or_download(
|
||||
working_directory: &Path,
|
||||
downloader: &SolcDownloader,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
) -> anyhow::Result<(Version, PathBuf)> {
|
||||
let target_directory = working_directory
|
||||
.join(SOLC_CACHE_DIRECTORY)
|
||||
.join(downloader.version.to_string());
|
||||
@@ -28,14 +30,26 @@ pub(crate) async fn get_or_download(
|
||||
let mut cache = SOLC_CACHER.lock().await;
|
||||
if cache.contains(&target_file) {
|
||||
tracing::debug!("using cached solc: {}", target_file.display());
|
||||
return Ok(target_file);
|
||||
return Ok((downloader.version.clone(), target_file));
|
||||
}
|
||||
|
||||
create_dir_all(target_directory)?;
|
||||
download_to_file(&target_file, downloader).await?;
|
||||
create_dir_all(&target_directory).with_context(|| {
|
||||
format!(
|
||||
"Failed to create solc cache directory: {}",
|
||||
target_directory.display()
|
||||
)
|
||||
})?;
|
||||
download_to_file(&target_file, downloader)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to write downloaded solc to {}",
|
||||
target_file.display()
|
||||
)
|
||||
})?;
|
||||
cache.insert(target_file.clone());
|
||||
|
||||
Ok(target_file)
|
||||
Ok((downloader.version.clone(), target_file))
|
||||
}
|
||||
|
||||
async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::Result<()> {
|
||||
@@ -45,14 +59,26 @@ async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::R
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut permissions = file.metadata()?.permissions();
|
||||
let mut permissions = file
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read metadata for {}", path.display()))?
|
||||
.permissions();
|
||||
permissions.set_mode(permissions.mode() | 0o111);
|
||||
file.set_permissions(permissions)?;
|
||||
file.set_permissions(permissions).with_context(|| {
|
||||
format!("Failed to set executable permissions on {}", path.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut file = BufWriter::new(file);
|
||||
file.write_all(&downloader.download().await?)?;
|
||||
file.flush()?;
|
||||
file.write_all(
|
||||
&downloader
|
||||
.download()
|
||||
.await
|
||||
.context("Failed to download solc binary bytes")?,
|
||||
)
|
||||
.with_context(|| format!("Failed to write solc binary to {}", path.display()))?;
|
||||
file.flush()
|
||||
.with_context(|| format!("Failed to flush file {}", path.display()))?;
|
||||
drop(file);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -63,8 +89,20 @@ async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::R
|
||||
.stderr(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to spawn xattr to remove quarantine attribute on {}",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
.wait()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed waiting for xattr operation to complete on {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::FromStr,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
|
||||
use semver::Version;
|
||||
use semver::{Version, VersionReq};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::list::List;
|
||||
use anyhow::Context as _;
|
||||
|
||||
pub static LIST_CACHE: LazyLock<Mutex<HashMap<&'static str, List>>> =
|
||||
LazyLock::new(Default::default);
|
||||
@@ -30,7 +32,12 @@ impl List {
|
||||
return Ok(list.clone());
|
||||
}
|
||||
|
||||
let body: List = reqwest::get(url).await?.json().await?;
|
||||
let body: List = reqwest::get(url)
|
||||
.await
|
||||
.with_context(|| format!("Failed to GET solc list from {url}"))?
|
||||
.json()
|
||||
.await
|
||||
.with_context(|| format!("Failed to deserialize solc list JSON from {url}"))?;
|
||||
|
||||
LIST_CACHE.lock().unwrap().insert(url, body.clone());
|
||||
|
||||
@@ -59,6 +66,9 @@ impl SolcDownloader {
|
||||
target: &'static str,
|
||||
list: &'static str,
|
||||
) -> anyhow::Result<Self> {
|
||||
static MAXIMUM_COMPILER_VERSION_REQUIREMENT: LazyLock<VersionReq> =
|
||||
LazyLock::new(|| VersionReq::from_str("<=0.8.30").unwrap());
|
||||
|
||||
let version_or_requirement = version.into();
|
||||
match version_or_requirement {
|
||||
VersionOrRequirement::Version(version) => Ok(Self {
|
||||
@@ -68,11 +78,15 @@ impl SolcDownloader {
|
||||
}),
|
||||
VersionOrRequirement::Requirement(requirement) => {
|
||||
let Some(version) = List::download(list)
|
||||
.await?
|
||||
.await
|
||||
.with_context(|| format!("Failed to download solc builds list from {list}"))?
|
||||
.builds
|
||||
.into_iter()
|
||||
.map(|build| build.version)
|
||||
.filter(|version| requirement.matches(version))
|
||||
.filter(|version| {
|
||||
MAXIMUM_COMPILER_VERSION_REQUIREMENT.matches(version)
|
||||
&& requirement.matches(version)
|
||||
})
|
||||
.max()
|
||||
else {
|
||||
anyhow::bail!("Failed to find a version that satisfies {requirement:?}");
|
||||
@@ -107,11 +121,20 @@ impl SolcDownloader {
|
||||
/// Errors out if the download fails or the digest of the downloaded file
|
||||
/// mismatches the expected digest from the release [List].
|
||||
pub async fn download(&self) -> anyhow::Result<Vec<u8>> {
|
||||
let builds = List::download(self.list).await?.builds;
|
||||
let builds = List::download(self.list)
|
||||
.await
|
||||
.with_context(|| format!("Failed to download solc builds list from {}", self.list))?
|
||||
.builds;
|
||||
let build = builds
|
||||
.iter()
|
||||
.find(|build| build.version == self.version)
|
||||
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Requested solc version {} was not found in builds list fetched from {}",
|
||||
self.version, self.list
|
||||
)
|
||||
})?;
|
||||
|
||||
let path = build.path.clone();
|
||||
let expected_digest = build
|
||||
@@ -121,7 +144,13 @@ impl SolcDownloader {
|
||||
.to_string();
|
||||
let url = format!("{}/{}/{}", Self::BASE_URL, self.target, path.display());
|
||||
|
||||
let file = reqwest::get(url).await?.bytes().await?.to_vec();
|
||||
let file = reqwest::get(&url)
|
||||
.await
|
||||
.with_context(|| format!("Failed to GET solc binary from {url}"))?
|
||||
.bytes()
|
||||
.await
|
||||
.with_context(|| format!("Failed to read solc binary bytes from {url}"))?
|
||||
.to_vec();
|
||||
|
||||
if hex::encode(Sha256::digest(&file)) != expected_digest {
|
||||
anyhow::bail!("sha256 mismatch for solc version {}", self.version);
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use cache::get_or_download;
|
||||
use download::SolcDownloader;
|
||||
|
||||
use revive_dt_common::types::VersionOrRequirement;
|
||||
use semver::Version;
|
||||
|
||||
pub mod cache;
|
||||
pub mod download;
|
||||
@@ -23,7 +25,7 @@ pub async fn download_solc(
|
||||
cache_directory: &Path,
|
||||
version: impl Into<VersionOrRequirement>,
|
||||
wasm: bool,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
) -> anyhow::Result<(Version, PathBuf)> {
|
||||
let downloader = if wasm {
|
||||
SolcDownloader::wasm(version).await
|
||||
} else if cfg!(target_os = "linux") {
|
||||
@@ -34,7 +36,8 @@ pub async fn download_solc(
|
||||
SolcDownloader::windows(version).await
|
||||
} else {
|
||||
unimplemented!()
|
||||
}?;
|
||||
}
|
||||
.context("Failed to initialize the Solc Downloader")?;
|
||||
|
||||
get_or_download(cache_directory, &downloader).await
|
||||
}
|
||||
|
||||
+1
-1
Submodule polkadot-sdk updated: dc3d0e5ab7...45a0ea734f
Submodule
+1
Submodule resolc-compiler-tests added at 40ffa2b839
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Revive Differential Tests - Quick Start Script
|
||||
# This script clones the test repository, and runs the tool
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
TEST_REPO_URL="https://github.com/paritytech/resolc-compiler-tests"
|
||||
TEST_REPO_DIR="resolc-compiler-tests"
|
||||
WORKDIR="workdir"
|
||||
|
||||
# Optional positional argument: path to polkadot-sdk directory
|
||||
POLKADOT_SDK_DIR="${1:-}"
|
||||
|
||||
# Binary paths (default to names in $PATH)
|
||||
REVIVE_DEV_NODE_BIN="revive-dev-node"
|
||||
ETH_RPC_BIN="eth-rpc"
|
||||
|
||||
echo -e "${GREEN}=== Revive Differential Tests Quick Start ===${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if test repo already exists
|
||||
if [ -d "$TEST_REPO_DIR" ]; then
|
||||
echo -e "${YELLOW}Test repository already exists. Pulling latest changes...${NC}"
|
||||
cd "$TEST_REPO_DIR"
|
||||
git pull
|
||||
cd ..
|
||||
else
|
||||
echo -e "${GREEN}Cloning test repository...${NC}"
|
||||
git clone "$TEST_REPO_URL"
|
||||
fi
|
||||
|
||||
# If polkadot-sdk path is provided, verify and use binaries from there; build if needed
|
||||
if [ -n "$POLKADOT_SDK_DIR" ]; then
|
||||
if [ ! -d "$POLKADOT_SDK_DIR" ]; then
|
||||
echo -e "${RED}Provided polkadot-sdk directory does not exist: $POLKADOT_SDK_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLKADOT_SDK_DIR=$(realpath "$POLKADOT_SDK_DIR")
|
||||
echo -e "${GREEN}Using polkadot-sdk at: $POLKADOT_SDK_DIR${NC}"
|
||||
|
||||
REVIVE_DEV_NODE_BIN="$POLKADOT_SDK_DIR/target/release/revive-dev-node"
|
||||
ETH_RPC_BIN="$POLKADOT_SDK_DIR/target/release/eth-rpc"
|
||||
|
||||
if [ ! -x "$REVIVE_DEV_NODE_BIN" ] || [ ! -x "$ETH_RPC_BIN" ]; then
|
||||
echo -e "${YELLOW}Required binaries not found in release target. Building...${NC}"
|
||||
(cd "$POLKADOT_SDK_DIR" && cargo build --release --package staging-node-cli --package pallet-revive-eth-rpc --package revive-dev-node)
|
||||
fi
|
||||
|
||||
for bin in "$REVIVE_DEV_NODE_BIN" "$ETH_RPC_BIN"; do
|
||||
if [ ! -x "$bin" ]; then
|
||||
echo -e "${RED}Expected binary not found after build: $bin${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo -e "${YELLOW}No polkadot-sdk path provided. Using binaries from $PATH.${NC}"
|
||||
fi
|
||||
|
||||
# Create workdir if it doesn't exist
|
||||
mkdir -p "$WORKDIR"
|
||||
|
||||
echo -e "${GREEN}Starting differential tests...${NC}"
|
||||
echo "This may take a while..."
|
||||
echo ""
|
||||
|
||||
# Run the tool
|
||||
cargo build --release;
|
||||
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
|
||||
--platform revive-dev-node-polkavm-resolc \
|
||||
--test $(realpath "$TEST_REPO_DIR/fixtures/solidity") \
|
||||
--working-directory "$WORKDIR" \
|
||||
--concurrency.number-of-nodes 10 \
|
||||
--concurrency.number-of-threads 5 \
|
||||
--concurrency.number-of-concurrent-tasks 500 \
|
||||
--wallet.additional-keys 100000 \
|
||||
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
||||
--eth-rpc.path "$ETH_RPC_BIN" \
|
||||
> logs.log \
|
||||
2> output.log
|
||||
|
||||
echo -e "${GREEN}=== Test run completed! ===${NC}"
|
||||
+583
@@ -0,0 +1,583 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Metadata",
|
||||
"description": "A MatterLabs metadata file.\n\nThis defines the structure that the MatterLabs metadata files follow for defining the tests or\nthe workloads.\n\nEach metadata file is composed of multiple test cases where each test case is isolated from the\nothers and runs in a completely different address space. Each test case is composed of a number\nof steps and assertions that should be performed as part of the test case.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "This is an optional comment on the metadata file which has no impact on the execution in any\nway.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ignore": {
|
||||
"description": "An optional boolean which defines if the metadata file as a whole should be ignored. If null\nthen the metadata file will not be ignored.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"targets": {
|
||||
"description": "An optional vector of targets that this Metadata file's cases can be executed on. As an\nexample, if we wish for the metadata file's cases to only be run on PolkaVM then we'd\nspecify a target of \"PolkaVM\" in here.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/VmIdentifier"
|
||||
}
|
||||
},
|
||||
"cases": {
|
||||
"description": "A vector of the test cases and workloads contained within the metadata file. This is their\nprimary description.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Case"
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"description": "A map of all of the contracts that the test requires to run.\n\nThis is a map where the key is the name of the contract instance and the value is the\ncontract's path and ident in the file.\n\nIf any contract is to be used by the test then it must be included in here first so that the\nframework is aware of its path, compiles it, and prepares it.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/ContractPathAndIdent"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"description": "The set of libraries that this metadata file requires.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/ContractInstance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modes": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/ParsedMode"
|
||||
}
|
||||
},
|
||||
"required_evm_version": {
|
||||
"description": "This field specifies an EVM version requirement that the test case has where the test might\nbe run of the evm version of the nodes match the evm version specified here.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/EvmVersionRequirement"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compiler_directives": {
|
||||
"description": "A set of compilation directives that will be passed to the compiler whenever the contracts\nfor the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]\nis just a filter for when a test can run whereas this is an instruction to the compiler.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CompilationDirectives"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cases"
|
||||
],
|
||||
"$defs": {
|
||||
"VmIdentifier": {
|
||||
"description": "An enum representing the identifiers of the supported VMs.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The ethereum virtual machine.",
|
||||
"type": "string",
|
||||
"const": "evm"
|
||||
},
|
||||
{
|
||||
"description": "The EraVM virtual machine.",
|
||||
"type": "string",
|
||||
"const": "eravm"
|
||||
},
|
||||
{
|
||||
"description": "Polkadot's PolaVM Risc-v based virtual machine.",
|
||||
"type": "string",
|
||||
"const": "polkavm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Case": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "An optional name of the test case.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"comment": {
|
||||
"description": "An optional comment on the case which has no impact on the execution in any way.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modes": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```\n\nIf this is provided then it takes higher priority than the modes specified in the metadata\nfile.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/ParsedMode"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"description": "The set of steps to run as part of this test case.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Step"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"description": "An optional name of the group of tests that this test belongs to.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"description": "An optional set of expectations and assertions to make about the transaction after it ran.\n\nIf this is not specified then the only assertion that will be ran is that the transaction\nwas successful.\n\nThis expectation that's on the case itself will be attached to the final step of the case.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Expected"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignore": {
|
||||
"description": "An optional boolean which defines if the case as a whole should be ignored. If null then the\ncase will not be ignored.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inputs"
|
||||
]
|
||||
},
|
||||
"ParsedMode": {
|
||||
"description": "This represents a mode that has been parsed from test metadata.\n\nMode strings can take the following form (in pseudo-regex):\n\n```text\n[YEILV][+-]? (M[0123sz])? <semver>?\n```\n\nWe can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].",
|
||||
"type": "string"
|
||||
},
|
||||
"Step": {
|
||||
"description": "A test step.\n\nA test step can be anything. It could be an invocation to a function, an assertion, or any other\naction that needs to be run or executed on the nodes used in the tests.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A function call or an invocation to some function on some smart contract.",
|
||||
"$ref": "#/$defs/FunctionCallStep"
|
||||
},
|
||||
{
|
||||
"description": "A step for performing a balance assertion on some account or contract.",
|
||||
"$ref": "#/$defs/BalanceAssertionStep"
|
||||
},
|
||||
{
|
||||
"description": "A step for asserting that the storage of some contract or account is empty.",
|
||||
"$ref": "#/$defs/StorageEmptyAssertionStep"
|
||||
},
|
||||
{
|
||||
"description": "A special step for repeating a bunch of steps a certain number of times.",
|
||||
"$ref": "#/$defs/RepeatStep"
|
||||
},
|
||||
{
|
||||
"description": "A step type that allows for a new account address to be allocated and to later on be used\nas the caller in another step.",
|
||||
"$ref": "#/$defs/AllocateAccountStep"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FunctionCallStep": {
|
||||
"description": "This is an input step which is a transaction description that the framework translates into a\ntransaction and executes on the nodes.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caller": {
|
||||
"description": "The address of the account performing the call and paying the fees for it.",
|
||||
"type": "string",
|
||||
"default": "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1"
|
||||
},
|
||||
"comment": {
|
||||
"description": "An optional comment on the step which has no impact on the execution in any way.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instance": {
|
||||
"description": "The contract instance that's being called in this transaction step.",
|
||||
"$ref": "#/$defs/ContractInstance",
|
||||
"default": "Test"
|
||||
},
|
||||
"method": {
|
||||
"description": "The method that's being called in this step.",
|
||||
"$ref": "#/$defs/Method"
|
||||
},
|
||||
"calldata": {
|
||||
"description": "The calldata that the function should be invoked with.",
|
||||
"$ref": "#/$defs/Calldata",
|
||||
"default": []
|
||||
},
|
||||
"expected": {
|
||||
"description": "A set of assertions and expectations to have for the transaction.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Expected"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"description": "An optional value to provide as part of the transaction.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/EtherValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable_assignments": {
|
||||
"description": "Variable assignment to perform in the framework allowing us to reference them again later on\nduring the execution.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/VariableAssignments"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"ContractInstance": {
|
||||
"description": "Represents a contract instance found a metadata file.\n\nTypically, this is used as the key to the \"contracts\" field of metadata files.",
|
||||
"type": "string"
|
||||
},
|
||||
"Method": {
|
||||
"description": "Specify how the contract is called.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Initiate a deploy transaction, calling contracts constructor.\n\nIndicated by `#deployer`.",
|
||||
"type": "string",
|
||||
"const": "#deployer"
|
||||
},
|
||||
{
|
||||
"description": "Does not calculate and insert a function selector.\n\nIndicated by `#fallback`.",
|
||||
"type": "string",
|
||||
"const": "#fallback"
|
||||
},
|
||||
{
|
||||
"description": "Call the public function with the given name.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Calldata": {
|
||||
"description": "A type definition for the calldata supported by the testing framework.\n\nWe choose to document all of the types used in [`Calldata`] in this one doc comment to elaborate\non why they exist and consolidate all of the documentation for calldata in a single place where\nit can be viewed and understood.\n\nThe [`Single`] variant of this enum is quite simple and straightforward: it's a hex-encoded byte\narray of the calldata.\n\nThe [`Compound`] type is more intricate and allows for capabilities such as resolution and some\nsimple arithmetic operations. It houses a vector of [`CalldataItem`]s which is just a wrapper\naround an owned string.\n\nA [`CalldataItem`] could be a simple hex string of a single calldata argument, but it could also\nbe something that requires resolution such as `MyContract.address` which is a variable that is\nunderstood by the resolution logic to mean \"Lookup the address of this particular contract\ninstance\".\n\nIn addition to the above, the format supports some simple arithmetic operations like add, sub,\ndivide, multiply, bitwise AND, bitwise OR, and bitwise XOR. Our parser understands the [reverse\npolish notation] simply because it's easy to write a calculator for that notation and since we\ndo not have plans to use arithmetic too often in tests. In reverse polish notation a typical\n`2 + 4` would be written as `2 4 +` which makes this notation very simple to implement through\na stack.\n\nCombining the above, a single [`CalldataItem`] could employ both resolution and arithmetic at\nthe same time. For example, a [`CalldataItem`] of `$BLOCK_NUMBER $BLOCK_NUMBER +` means that\nthe block number should be retrieved and then it should be added to itself.\n\nInternally, we split the [`CalldataItem`] by spaces. Therefore, `$BLOCK_NUMBER $BLOCK_NUMBER+`\nis invalid but `$BLOCK_NUMBER $BLOCK_NUMBER +` is valid and can be understood by the parser and\ncalculator. After the split is done, each token is parsed into a [`CalldataToken<&str>`] forming\nan [`Iterator`] over [`CalldataToken<&str>`]. A [`CalldataToken<&str>`] can then be resolved\ninto a [`CalldataToken<U256>`] through the resolution logic. Finally, after resolution is done,\nthis iterator of [`CalldataToken<U256>`] is collapsed into the final result by applying the\narithmetic operations requested.\n\nFor example, supplying a [`Compound`] calldata of `0xdeadbeef` produces an iterator of a single\n[`CalldataToken<&str>`] items of the value [`CalldataToken::Item`] of the string value 12 which\nwe can then resolve into the appropriate [`U256`] value and convert into calldata.\n\nIn summary, the various types used in [`Calldata`] represent the following:\n- [`CalldataItem`]: A calldata string from the metadata files.\n- [`CalldataToken<&str>`]: Typically used in an iterator of items from the space splitted\n [`CalldataItem`] and represents a token that has not yet been resolved into its value.\n- [`CalldataToken<U256>`]: Represents a token that's been resolved from being a string and into\n the word-size calldata argument on which we can perform arithmetic.\n\n[`Single`]: Calldata::Single\n[`Compound`]: Calldata::Compound\n[reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/CalldataItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"CalldataItem": {
|
||||
"description": "This represents an item in the [`Calldata::Compound`] variant. Each item will be resolved\naccording to the resolution rules of the tool.",
|
||||
"type": "string"
|
||||
},
|
||||
"Expected": {
|
||||
"description": "A set of expectations and assertions to make about the transaction after it ran.\n\nIf this is not specified then the only assertion that will be ran is that the transaction\nwas successful.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "An assertion that the transaction succeeded and returned the provided set of data.",
|
||||
"$ref": "#/$defs/Calldata"
|
||||
},
|
||||
{
|
||||
"description": "A more complex assertion.",
|
||||
"$ref": "#/$defs/ExpectedOutput"
|
||||
},
|
||||
{
|
||||
"description": "A set of assertions.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ExpectedOutput"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExpectedOutput": {
|
||||
"description": "A set of assertions to run on the transaction.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"compiler_version": {
|
||||
"description": "An optional compiler version that's required in order for this assertion to run.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"return_data": {
|
||||
"description": "An optional field of the expected returns from the invocation.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Calldata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": {
|
||||
"description": "An optional set of assertions to run on the emitted events from the transaction.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/Event"
|
||||
}
|
||||
},
|
||||
"exception": {
|
||||
"description": "A boolean which defines whether we expect the transaction to succeed or fail.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "An optional field of the address of the emitter of the event.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/StepAddress"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topics": {
|
||||
"description": "The set of topics to expect the event to have.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"description": "The set of values to expect the event to have.",
|
||||
"$ref": "#/$defs/Calldata"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"topics",
|
||||
"values"
|
||||
]
|
||||
},
|
||||
"StepAddress": {
|
||||
"description": "An address type that might either be an address literal or a resolvable address.",
|
||||
"type": "string"
|
||||
},
|
||||
"EtherValue": {
|
||||
"description": "Defines an Ether value.\n\nThis is an unsigned 256 bit integer that's followed by some denomination which can either be\neth, ether, gwei, or wei.",
|
||||
"type": "string"
|
||||
},
|
||||
"VariableAssignments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"return_data": {
|
||||
"description": "A vector of the variable names to assign to the return data.\n\nExample: `UniswapV3PoolAddress`",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"return_data"
|
||||
]
|
||||
},
|
||||
"BalanceAssertionStep": {
|
||||
"description": "This represents a balance assertion step where the framework needs to query the balance of some\naccount or contract and assert that it's some amount.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the balance assertion.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"address": {
|
||||
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
|
||||
"$ref": "#/$defs/StepAddress"
|
||||
},
|
||||
"expected_balance": {
|
||||
"description": "The amount of balance to assert that the account or contract has. This is a 256 bit string\nthat's serialized and deserialized into a decimal string.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"expected_balance"
|
||||
]
|
||||
},
|
||||
"StorageEmptyAssertionStep": {
|
||||
"description": "This represents an assertion for the storage of some contract or account and whether it's empty\nor not.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the storage empty assertion.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"address": {
|
||||
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
|
||||
"$ref": "#/$defs/StepAddress"
|
||||
},
|
||||
"is_storage_empty": {
|
||||
"description": "A boolean of whether the storage of the address is empty or not.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"is_storage_empty"
|
||||
]
|
||||
},
|
||||
"RepeatStep": {
|
||||
"description": "This represents a repetition step which is a special step type that allows for a sequence of\nsteps to be repeated (on different drivers) a certain number of times.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the repetition step.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"repeat": {
|
||||
"description": "The number of repetitions that the steps should be repeated for.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0
|
||||
},
|
||||
"steps": {
|
||||
"description": "The sequence of steps to repeat for the above defined number of repetitions.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Step"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"repeat",
|
||||
"steps"
|
||||
]
|
||||
},
|
||||
"AllocateAccountStep": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "An optional comment on the account allocation step.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allocate_account": {
|
||||
"description": "An instruction to allocate a new account with the value being the variable name of that\naccount. This must start with `$VARIABLE:` and then be followed by the variable name of the\naccount.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allocate_account"
|
||||
]
|
||||
},
|
||||
"ContractPathAndIdent": {
|
||||
"description": "Represents an identifier used for contracts.\n\nThe type supports serialization from and into the following string format:\n\n```text\n${path}:${contract_ident}\n```",
|
||||
"type": "string"
|
||||
},
|
||||
"EvmVersionRequirement": {
|
||||
"description": "An EVM version requirement that the test case has. This gets serialized and deserialized from\nand into [`String`]. This follows a simple format of (>=|<=|=|>|<) followed by a string of the\nEVM version.\n\nWhen specified, the framework will only run the test if the node's EVM version matches that\nrequired by the metadata file.",
|
||||
"type": "string"
|
||||
},
|
||||
"CompilationDirectives": {
|
||||
"description": "A set of compilation directives that will be passed to the compiler whenever the contracts for\nthe test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is\njust a filter for when a test can run whereas this is an instruction to the compiler.\nDefines how the compiler should handle revert strings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revert_string_handling": {
|
||||
"description": "Defines how the revert strings should be handled.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RevertString"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RevertString": {
|
||||
"description": "Defines how the compiler should handle revert strings.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The default handling of the revert strings.",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
},
|
||||
{
|
||||
"description": "The debug handling of the revert strings.",
|
||||
"type": "string",
|
||||
"const": "debug"
|
||||
},
|
||||
{
|
||||
"description": "Strip the revert strings.",
|
||||
"type": "string",
|
||||
"const": "strip"
|
||||
},
|
||||
{
|
||||
"description": "Provide verbose debug strings for the revert string.",
|
||||
"type": "string",
|
||||
"const": "verboseDebug"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Utilities to print benchmark metrics from a report JSON into CSV.
|
||||
|
||||
Usage:
|
||||
python scripts/print_benchmark_metrics_csv.py /absolute/path/to/report.json
|
||||
|
||||
The script prints, for each metadata path, case index, and mode combination,
|
||||
CSV rows aligned to mined blocks with the following columns:
|
||||
- block_number
|
||||
- number_of_txs
|
||||
- tps (transaction_per_second)
|
||||
- gps (gas_per_second)
|
||||
- gas_block_fullness
|
||||
- ref_time (if available)
|
||||
- max_ref_time (if available)
|
||||
- proof_size (if available)
|
||||
- max_proof_size (if available)
|
||||
- ref_time_block_fullness (if available)
|
||||
- proof_size_block_fullness (if available)
|
||||
|
||||
Important nuance: TPS and GPS arrays have (number_of_blocks - 1) items. The
|
||||
first block row has no TPS/GPS; the CSV leaves those cells empty for the first
|
||||
row and aligns subsequent values to their corresponding next block.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import csv
|
||||
from typing import List, Mapping, TypedDict
|
||||
|
||||
|
||||
class EthereumMinedBlockInformation(TypedDict):
|
||||
"""EVM block information extracted from the report.
|
||||
|
||||
Attributes:
|
||||
block_number: The block height.
|
||||
block_timestamp: The UNIX timestamp of the block.
|
||||
mined_gas: Total gas used (mined) in the block.
|
||||
block_gas_limit: The gas limit of the block.
|
||||
transaction_hashes: List of transaction hashes included in the block.
|
||||
"""
|
||||
|
||||
block_number: int
|
||||
block_timestamp: int
|
||||
mined_gas: int
|
||||
block_gas_limit: int
|
||||
transaction_hashes: List[str]
|
||||
|
||||
|
||||
class SubstrateMinedBlockInformation(TypedDict):
|
||||
"""Substrate-specific block resource usage fields.
|
||||
|
||||
Attributes:
|
||||
ref_time: The consumed ref time in the block.
|
||||
max_ref_time: The maximum ref time allowed for the block.
|
||||
proof_size: The consumed proof size in the block.
|
||||
max_proof_size: The maximum proof size allowed for the block.
|
||||
"""
|
||||
|
||||
ref_time: int
|
||||
max_ref_time: int
|
||||
proof_size: int
|
||||
max_proof_size: int
|
||||
|
||||
|
||||
class MinedBlockInformation(TypedDict):
|
||||
"""Block-level information for a mined block with both EVM and optional Substrate fields."""
|
||||
|
||||
ethereum_block_information: EthereumMinedBlockInformation
|
||||
substrate_block_information: SubstrateMinedBlockInformation
|
||||
|
||||
|
||||
class Metric(TypedDict):
|
||||
"""Metric data of integer values keyed by platform identifier.
|
||||
|
||||
Attributes:
|
||||
minimum: Single scalar minimum per platform.
|
||||
maximum: Single scalar maximum per platform.
|
||||
mean: Single scalar mean per platform.
|
||||
median: Single scalar median per platform.
|
||||
raw: Time-series (or list) of values per platform.
|
||||
"""
|
||||
|
||||
minimum: Mapping[str, int]
|
||||
maximum: Mapping[str, int]
|
||||
mean: Mapping[str, int]
|
||||
median: Mapping[str, int]
|
||||
raw: Mapping[str, List[int]]
|
||||
|
||||
|
||||
class Metrics(TypedDict):
|
||||
"""All metrics that may be present for a given execution report.
|
||||
|
||||
Note that some metrics are optional and present only for specific platforms
|
||||
or execution modes.
|
||||
"""
|
||||
|
||||
transaction_per_second: Metric
|
||||
gas_per_second: Metric
|
||||
gas_block_fullness: Metric
|
||||
ref_time_block_fullness: Metric
|
||||
proof_size_block_fullness: Metric
|
||||
|
||||
|
||||
class ExecutionReport(TypedDict):
|
||||
"""Execution report for a mode containing mined blocks and metrics.
|
||||
|
||||
Attributes:
|
||||
mined_block_information: Mapping from platform identifier to the list of
|
||||
mined blocks observed for that platform.
|
||||
metrics: The computed metrics for the execution.
|
||||
"""
|
||||
|
||||
mined_block_information: Mapping[str, List[MinedBlockInformation]]
|
||||
metrics: Metrics
|
||||
|
||||
|
||||
class CaseReport(TypedDict):
|
||||
"""Report for a single case, keyed by mode string."""
|
||||
|
||||
mode_execution_reports: Mapping[str, ExecutionReport]
|
||||
|
||||
|
||||
class MetadataFileReport(TypedDict):
|
||||
"""Report subtree keyed by case indices for a metadata file path."""
|
||||
|
||||
case_reports: Mapping[str, CaseReport]
|
||||
|
||||
|
||||
class ReportRoot(TypedDict):
|
||||
"""Top-level report schema with execution information keyed by metadata path."""
|
||||
|
||||
execution_information: Mapping[str, MetadataFileReport]
|
||||
|
||||
|
||||
BlockInformation = TypedDict(
|
||||
"BlockInformation",
|
||||
{
|
||||
"Block Number": int,
|
||||
"Timestamp": int,
|
||||
"Datetime": None,
|
||||
"Transaction Count": int,
|
||||
"TPS": int | None,
|
||||
"GPS": int | None,
|
||||
"Ref Time": int,
|
||||
"Max Ref Time": int,
|
||||
"Block Fullness Ref Time": int,
|
||||
"Proof Size": int,
|
||||
"Max Proof Size": int,
|
||||
"Block Fullness Proof Size": int,
|
||||
},
|
||||
)
|
||||
"""A typed dictionary used to hold all of the block information"""
|
||||
|
||||
|
||||
def load_report(path: str) -> ReportRoot:
|
||||
"""Load the report JSON from disk.
|
||||
|
||||
Args:
|
||||
path: Absolute or relative filesystem path to the JSON report file.
|
||||
|
||||
Returns:
|
||||
The parsed report as a typed dictionary structure.
|
||||
"""
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data: ReportRoot = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def main() -> None:
|
||||
report_path: str = sys.argv[1]
|
||||
report: ReportRoot = load_report(report_path)
|
||||
|
||||
# TODO: Remove this in the future, but for now, the target is fixed.
|
||||
target: str = "revive-dev-node-revm-solc"
|
||||
|
||||
csv_writer = csv.writer(sys.stdout)
|
||||
|
||||
for _, metadata_file_report in report["execution_information"].items():
|
||||
for _, case_report in metadata_file_report["case_reports"].items():
|
||||
for _, execution_report in case_report["mode_execution_reports"].items():
|
||||
blocks_information: list[MinedBlockInformation] = execution_report[
|
||||
"mined_block_information"
|
||||
][target]
|
||||
|
||||
resolved_blocks: list[BlockInformation] = []
|
||||
for i, block_information in enumerate(blocks_information):
|
||||
resolved_blocks.append(
|
||||
{
|
||||
"Block Number": block_information[
|
||||
"ethereum_block_information"
|
||||
]["block_number"],
|
||||
"Timestamp": block_information[
|
||||
"ethereum_block_information"
|
||||
]["block_timestamp"],
|
||||
"Datetime": None,
|
||||
"Transaction Count": len(
|
||||
block_information["ethereum_block_information"][
|
||||
"transaction_hashes"
|
||||
]
|
||||
),
|
||||
"TPS": (
|
||||
None
|
||||
if i == 0
|
||||
else execution_report["metrics"][
|
||||
"transaction_per_second"
|
||||
]["raw"][target][i - 1]
|
||||
),
|
||||
"GPS": (
|
||||
None
|
||||
if i == 0
|
||||
else execution_report["metrics"]["gas_per_second"][
|
||||
"raw"
|
||||
][target][i - 1]
|
||||
),
|
||||
"Ref Time": block_information[
|
||||
"substrate_block_information"
|
||||
]["ref_time"],
|
||||
"Max Ref Time": block_information[
|
||||
"substrate_block_information"
|
||||
]["max_ref_time"],
|
||||
"Block Fullness Ref Time": execution_report["metrics"][
|
||||
"ref_time_block_fullness"
|
||||
]["raw"][target][i],
|
||||
"Proof Size": block_information[
|
||||
"substrate_block_information"
|
||||
]["proof_size"],
|
||||
"Max Proof Size": block_information[
|
||||
"substrate_block_information"
|
||||
]["max_proof_size"],
|
||||
"Block Fullness Proof Size": execution_report["metrics"][
|
||||
"proof_size_block_fullness"
|
||||
]["raw"][target][i],
|
||||
}
|
||||
)
|
||||
|
||||
csv_writer = csv.DictWriter(sys.stdout, resolved_blocks[0].keys())
|
||||
csv_writer.writeheader()
|
||||
csv_writer.writerows(resolved_blocks)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
This script is used to turn the JSON report produced by the revive differential tests tool into an
|
||||
easy to consume markdown document for the purpose of reporting this information in the Polkadot SDK
|
||||
CI. The full models used in the JSON report can be found in the revive differential tests repo and
|
||||
the models used in this script are just a partial reproduction of the full report models.
|
||||
"""
|
||||
|
||||
from typing import TypedDict, Literal, Union
|
||||
|
||||
import json, io
|
||||
|
||||
|
||||
class Report(TypedDict):
|
||||
context: "Context"
|
||||
execution_information: dict[
|
||||
"MetadataFilePathString",
|
||||
dict["ModeString", dict["CaseIdxString", "CaseReport"]],
|
||||
]
|
||||
|
||||
|
||||
class Context(TypedDict):
|
||||
Test: "TestContext"
|
||||
|
||||
|
||||
class TestContext(TypedDict):
|
||||
corpus_configuration: "CorpusConfiguration"
|
||||
|
||||
|
||||
class CorpusConfiguration(TypedDict):
|
||||
test_specifiers: list["TestSpecifier"]
|
||||
|
||||
|
||||
class CaseReport(TypedDict):
|
||||
status: "CaseStatus"
|
||||
|
||||
|
||||
class CaseStatusSuccess(TypedDict):
|
||||
status: Literal["Succeeded"]
|
||||
steps_executed: int
|
||||
|
||||
|
||||
class CaseStatusFailure(TypedDict):
|
||||
status: Literal["Failed"]
|
||||
reason: str
|
||||
|
||||
|
||||
class CaseStatusIgnored(TypedDict):
|
||||
status: Literal["Ignored"]
|
||||
reason: str
|
||||
|
||||
|
||||
CaseStatus = Union[CaseStatusSuccess, CaseStatusFailure, CaseStatusIgnored]
|
||||
"""A union type of all of the possible statuses that could be reported for a case."""
|
||||
|
||||
TestSpecifier = str
|
||||
"""A test specifier string. For example resolc-compiler-tests/fixtures/solidity/test.json::0::Y+"""
|
||||
|
||||
ModeString = str
|
||||
"""The mode string. For example Y+ >=0.8.13"""
|
||||
|
||||
MetadataFilePathString = str
|
||||
"""The path to a metadata file. For example resolc-compiler-tests/fixtures/solidity/test.json"""
|
||||
|
||||
CaseIdxString = str
|
||||
"""The index of a case as a string. For example '0'"""
|
||||
|
||||
|
||||
def path_relative_to_resolc_compiler_test_directory(path: str) -> str:
|
||||
"""
|
||||
Given a path, this function returns the path relative to the resolc-compiler-test directory. The
|
||||
following is an example of an input and an output:
|
||||
|
||||
Input: ~/polkadot-sdk/revive-differential-tests/resolc-compiler-tests/fixtures/solidity/test.json
|
||||
Output: test.json
|
||||
"""
|
||||
|
||||
return f"{path.split('resolc-compiler-tests/fixtures/solidity')[-1].strip('/')}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with open("report.json", "r") as file:
|
||||
report: Report = json.load(file)
|
||||
|
||||
# Starting the markdown document and adding information to it as we go.
|
||||
markdown_document: io.TextIOWrapper = open("report.md", "w")
|
||||
print("# Differential Tests Results", file=markdown_document)
|
||||
|
||||
# Getting all of the test specifiers from the report and making them relative to the tests dir.
|
||||
test_specifiers: list[str] = list(
|
||||
map(
|
||||
path_relative_to_resolc_compiler_test_directory,
|
||||
report["context"]["Test"]["corpus_configuration"]["test_specifiers"],
|
||||
)
|
||||
)
|
||||
print("## Specified Tests", file=markdown_document)
|
||||
for test_specifier in test_specifiers:
|
||||
print(f"* `{test_specifier}`", file=markdown_document)
|
||||
|
||||
# Counting the total number of test cases, successes, failures, and ignored tests
|
||||
total_number_of_cases: int = 0
|
||||
total_number_of_successes: int = 0
|
||||
total_number_of_failures: int = 0
|
||||
total_number_of_ignores: int = 0
|
||||
for _, mode_to_case_mapping in report["execution_information"].items():
|
||||
for _, case_idx_to_report_mapping in mode_to_case_mapping.items():
|
||||
for _, case_report in case_idx_to_report_mapping.items():
|
||||
status: CaseStatus = case_report["status"]
|
||||
|
||||
total_number_of_cases += 1
|
||||
if status["status"] == "Succeeded":
|
||||
total_number_of_successes += 1
|
||||
elif status["status"] == "Failed":
|
||||
total_number_of_failures += 1
|
||||
elif status["status"] == "Ignored":
|
||||
total_number_of_ignores += 1
|
||||
else:
|
||||
raise Exception(
|
||||
f"Encountered a status that's unknown to the script: {status}"
|
||||
)
|
||||
|
||||
print("## Counts", file=markdown_document)
|
||||
print(
|
||||
f"* **Total Number of Test Cases:** {total_number_of_cases}",
|
||||
file=markdown_document,
|
||||
)
|
||||
print(
|
||||
f"* **Total Number of Successes:** {total_number_of_successes}",
|
||||
file=markdown_document,
|
||||
)
|
||||
print(
|
||||
f"* **Total Number of Failures:** {total_number_of_failures}",
|
||||
file=markdown_document,
|
||||
)
|
||||
print(
|
||||
f"* **Total Number of Ignores:** {total_number_of_ignores}",
|
||||
file=markdown_document,
|
||||
)
|
||||
|
||||
# Grouping the various test cases into dictionaries and groups depending on their status to make
|
||||
# them easier to include in the markdown document later on.
|
||||
successful_cases: dict[
|
||||
MetadataFilePathString, dict[CaseIdxString, set[ModeString]]
|
||||
] = {}
|
||||
for metadata_file_path, mode_to_case_mapping in report[
|
||||
"execution_information"
|
||||
].items():
|
||||
for mode_string, case_idx_to_report_mapping in mode_to_case_mapping.items():
|
||||
for case_idx_string, case_report in case_idx_to_report_mapping.items():
|
||||
status: CaseStatus = case_report["status"]
|
||||
metadata_file_path: str = (
|
||||
path_relative_to_resolc_compiler_test_directory(metadata_file_path)
|
||||
)
|
||||
mode_string: str = mode_string.replace(" M3", "+").replace(" M0", "-")
|
||||
|
||||
if status["status"] == "Succeeded":
|
||||
successful_cases.setdefault(
|
||||
metadata_file_path,
|
||||
{},
|
||||
).setdefault(
|
||||
case_idx_string, set()
|
||||
).add(mode_string)
|
||||
|
||||
print("## Failures", file=markdown_document)
|
||||
print(
|
||||
"The test specifiers seen in this section have the format 'path::case_idx::compilation_mode'\
|
||||
and they're compatible with the revive differential tests framework and can be specified\
|
||||
to it directly in the same way that they're provided through the `--test` argument of the\
|
||||
framework.\n",
|
||||
file=markdown_document,
|
||||
)
|
||||
print(
|
||||
"The failures are provided in an expandable section to ensure that the PR does not get \
|
||||
polluted with information. Please click on the section below for more information",
|
||||
file=markdown_document,
|
||||
)
|
||||
print(
|
||||
"<details><summary>Detailed Differential Tests Failure Information</summary>\n\n",
|
||||
file=markdown_document,
|
||||
)
|
||||
print("| Test Specifier | Failure Reason | Note |", file=markdown_document)
|
||||
print("| -- | -- | -- |", file=markdown_document)
|
||||
|
||||
for metadata_file_path, mode_to_case_mapping in report[
|
||||
"execution_information"
|
||||
].items():
|
||||
for mode_string, case_idx_to_report_mapping in mode_to_case_mapping.items():
|
||||
for case_idx_string, case_report in case_idx_to_report_mapping.items():
|
||||
status: CaseStatus = case_report["status"]
|
||||
metadata_file_path: str = (
|
||||
path_relative_to_resolc_compiler_test_directory(metadata_file_path)
|
||||
)
|
||||
mode_string: str = mode_string.replace(" M3", "+").replace(" M0", "-")
|
||||
|
||||
if status["status"] != "Failed":
|
||||
continue
|
||||
|
||||
failure_reason: str = status["reason"].replace("\n", " ")
|
||||
|
||||
note: str = ""
|
||||
modes_where_this_case_succeeded: set[ModeString] = (
|
||||
successful_cases.setdefault(
|
||||
metadata_file_path,
|
||||
{},
|
||||
).setdefault(case_idx_string, set())
|
||||
)
|
||||
if len(modes_where_this_case_succeeded) != 0:
|
||||
note: str = (
|
||||
f"This test case succeeded with other compilation modes: {modes_where_this_case_succeeded}"
|
||||
)
|
||||
|
||||
test_specifier: str = (
|
||||
f"{metadata_file_path}::{case_idx_string}::{mode_string}"
|
||||
)
|
||||
print(
|
||||
f"| `{test_specifier}` | `{failure_reason}` | {note} |",
|
||||
file=markdown_document,
|
||||
)
|
||||
print("\n\n</details>", file=markdown_document)
|
||||
|
||||
# The primary downside of not using `with`, but I guess it's better since I don't want to over
|
||||
# indent the code.
|
||||
markdown_document.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user