mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-22 21:57:58 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e69f17a798 | |||
| 9b700bfec2 | |||
| 736c50a8f0 | |||
| e2fb7a4322 | |||
| 0edfb3a36e | |||
| 98b62d705f | |||
| 1a894f791a | |||
| c2526e48e7 | |||
| 7878f68c26 |
@@ -11,3 +11,5 @@ node_modules
|
|||||||
profile.json.gz
|
profile.json.gz
|
||||||
resolc-compiler-tests
|
resolc-compiler-tests
|
||||||
workdir
|
workdir
|
||||||
|
|
||||||
|
!/schema.json
|
||||||
Generated
+40
-5
@@ -4467,11 +4467,16 @@ dependencies = [
|
|||||||
name = "revive-dt-common"
|
name = "revive-dt-common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"alloy",
|
||||||
|
"alloy-primitives",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"clap",
|
||||||
"moka",
|
"moka",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"schemars 1.0.4",
|
||||||
"semver 1.0.26",
|
"semver 1.0.26",
|
||||||
"serde",
|
"serde",
|
||||||
|
"strum",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4501,9 +4506,13 @@ name = "revive-dt-config"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"revive-dt-common",
|
||||||
"semver 1.0.26",
|
"semver 1.0.26",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
"temp-dir",
|
"temp-dir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4518,7 +4527,6 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"indexmap 2.10.0",
|
"indexmap 2.10.0",
|
||||||
"once_cell",
|
|
||||||
"revive-dt-common",
|
"revive-dt-common",
|
||||||
"revive-dt-compiler",
|
"revive-dt-compiler",
|
||||||
"revive-dt-config",
|
"revive-dt-config",
|
||||||
@@ -4526,11 +4534,10 @@ dependencies = [
|
|||||||
"revive-dt-node",
|
"revive-dt-node",
|
||||||
"revive-dt-node-interaction",
|
"revive-dt-node-interaction",
|
||||||
"revive-dt-report",
|
"revive-dt-report",
|
||||||
|
"schemars 1.0.4",
|
||||||
"semver 1.0.26",
|
"semver 1.0.26",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"temp-dir",
|
|
||||||
"tempfile",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
@@ -4549,6 +4556,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"revive-common",
|
"revive-common",
|
||||||
"revive-dt-common",
|
"revive-dt-common",
|
||||||
|
"schemars 1.0.4",
|
||||||
"semver 1.0.26",
|
"semver 1.0.26",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4582,6 +4590,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"revive-common",
|
||||||
|
"revive-dt-format",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4872,10 +4882,24 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
|
"schemars_derive",
|
||||||
|
"semver 1.0.26",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars_derive"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schnellru"
|
name = "schnellru"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -5060,6 +5084,17 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive_internals"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.140"
|
version = "1.0.140"
|
||||||
@@ -5692,9 +5727,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.27.1"
|
version = "0.27.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ moka = "0.12.10"
|
|||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
reqwest = { version = "0.12.15", features = ["json"] }
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
|
schemars = { version = "1.0.4", features = ["semver1"] }
|
||||||
semver = { version = "1.0", features = ["serde"] }
|
semver = { version = "1.0", features = ["serde"] }
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||||
serde_json = { version = "1.0", default-features = false, features = [
|
serde_json = { version = "1.0", default-features = false, features = [
|
||||||
@@ -48,6 +49,7 @@ serde_with = { version = "3.14.0" }
|
|||||||
sha2 = { version = "0.10.9" }
|
sha2 = { version = "0.10.9" }
|
||||||
sp-core = "36.1.0"
|
sp-core = "36.1.0"
|
||||||
sp-runtime = "41.1.0"
|
sp-runtime = "41.1.0"
|
||||||
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
temp-dir = { version = "0.1.16" }
|
temp-dir = { version = "0.1.16" }
|
||||||
tempfile = "3.3"
|
tempfile = "3.3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|||||||
@@ -52,122 +52,152 @@ 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.
|
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
|
```bash
|
||||||
$ cargo run --release -- --help
|
$ cargo run --release -- execute-tests --help
|
||||||
Usage: retester [OPTIONS]
|
Error: Executes tests in the MatterLabs format differentially on multiple targets concurrently
|
||||||
|
|
||||||
|
Usage: retester execute-tests [OPTIONS]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-s, --solc <SOLC>
|
-w, --working-directory <WORKING_DIRECTORY>
|
||||||
The `solc` version to use if the test didn't specify it explicitly
|
The working directory that the program will use for all of the temporary artifacts needed at runtime.
|
||||||
|
|
||||||
[default: 0.8.29]
|
If not specified, then a temporary directory will be created and used by the program for all temporary artifacts.
|
||||||
|
|
||||||
--wasm
|
[default: ]
|
||||||
Use the Wasm compiler versions
|
|
||||||
|
|
||||||
-r, --resolc <RESOLC>
|
-p, --platform <PLATFORMS>
|
||||||
The path to the `resolc` executable to be tested.
|
The set of platforms that the differential tests should run on
|
||||||
|
|
||||||
By default it uses the `resolc` binary found in `$PATH`.
|
[default: geth-evm-solc,revive-dev-node-polkavm-resolc]
|
||||||
|
|
||||||
If `--wasm` is set, this should point to the resolc Wasm ile.
|
Possible values:
|
||||||
|
- geth-evm-solc: The Go-ethereum reference full node EVM implementation with the solc compiler
|
||||||
[default: resolc]
|
- kitchensink-polkavm-resolc: The kitchensink node with the PolkaVM backend with the resolc compiler
|
||||||
|
- kitchensink-revm-solc: The kitchensink node with the REVM backend with the solc compiler
|
||||||
|
- revive-dev-node-polkavm-resolc: The revive dev node with the PolkaVM backend with the resolc compiler
|
||||||
|
- revive-dev-node-revm-solc: The revive dev node with the REVM backend with the solc compiler
|
||||||
|
|
||||||
-c, --corpus <CORPUS>
|
-c, --corpus <CORPUS>
|
||||||
A list of test corpus JSON files to be tested
|
A list of test corpus JSON files to be tested
|
||||||
|
|
||||||
-w, --workdir <WORKING_DIRECTORY>
|
-h, --help
|
||||||
A place to store temporary artifacts during test execution.
|
Print help (see a summary with '-h')
|
||||||
|
|
||||||
Creates a temporary dir if not specified.
|
Solc Configuration:
|
||||||
|
--solc.version <VERSION>
|
||||||
|
Specifies the default version of the Solc compiler that should be used if there is no override specified by one of the test cases
|
||||||
|
|
||||||
-g, --geth <GETH>
|
[default: 0.8.29]
|
||||||
The path to the `geth` executable.
|
|
||||||
|
|
||||||
By default it uses `geth` binary found in `$PATH`.
|
Resolc Configuration:
|
||||||
|
--resolc.path <resolc.path>
|
||||||
|
Specifies the path of the resolc compiler to be used by the tool.
|
||||||
|
|
||||||
|
If this is not specified, then the tool assumes that it should use the resolc binary that's provided in the user's $PATH.
|
||||||
|
|
||||||
|
[default: resolc]
|
||||||
|
|
||||||
|
Geth Configuration:
|
||||||
|
--geth.path <geth.path>
|
||||||
|
Specifies the path of the geth node to be used by the tool.
|
||||||
|
|
||||||
|
If this is not specified, then the tool assumes that it should use the geth binary that's provided in the user's $PATH.
|
||||||
|
|
||||||
[default: geth]
|
[default: geth]
|
||||||
|
|
||||||
--geth-start-timeout <GETH_START_TIMEOUT>
|
--geth.start-timeout-ms <geth.start-timeout-ms>
|
||||||
The maximum time in milliseconds to wait for geth to start
|
The amount of time to wait upon startup before considering that the node timed out
|
||||||
|
|
||||||
[default: 5000]
|
[default: 5000]
|
||||||
|
|
||||||
--genesis <GENESIS_FILE>
|
Kitchensink Configuration:
|
||||||
Configure nodes according to this genesis.json file
|
--kitchensink.path <kitchensink.path>
|
||||||
|
Specifies the path of the kitchensink node to be used by the tool.
|
||||||
|
|
||||||
[default: genesis.json]
|
If this is not specified, then the tool assumes that it should use the kitchensink binary that's provided in the user's $PATH.
|
||||||
|
|
||||||
-a, --account <ACCOUNT>
|
[default: substrate-node]
|
||||||
The signing account private key
|
|
||||||
|
--kitchensink.start-timeout-ms <kitchensink.start-timeout-ms>
|
||||||
|
The amount of time to wait upon startup before considering that the node timed out
|
||||||
|
|
||||||
|
[default: 5000]
|
||||||
|
|
||||||
|
--kitchensink.dont-use-dev-node
|
||||||
|
This configures the tool to use Kitchensink instead of using the revive-dev-node
|
||||||
|
|
||||||
|
Revive Dev Node Configuration:
|
||||||
|
--revive-dev-node.path <revive-dev-node.path>
|
||||||
|
Specifies the path of the revive dev node to be used by the tool.
|
||||||
|
|
||||||
|
If this is not specified, then the tool assumes that it should use the revive dev node binary that's provided in the user's $PATH.
|
||||||
|
|
||||||
|
[default: revive-dev-node]
|
||||||
|
|
||||||
|
--revive-dev-node.start-timeout-ms <revive-dev-node.start-timeout-ms>
|
||||||
|
The amount of time to wait upon startup before considering that the node timed out
|
||||||
|
|
||||||
|
[default: 5000]
|
||||||
|
|
||||||
|
Eth RPC Configuration:
|
||||||
|
--eth-rpc.path <eth-rpc.path>
|
||||||
|
Specifies the path of the ETH RPC to be used by the tool.
|
||||||
|
|
||||||
|
If this is not specified, then the tool assumes that it should use the ETH RPC binary that's provided in the user's $PATH.
|
||||||
|
|
||||||
|
[default: eth-rpc]
|
||||||
|
|
||||||
|
--eth-rpc.start-timeout-ms <eth-rpc.start-timeout-ms>
|
||||||
|
The amount of time to wait upon startup before considering that the node timed out
|
||||||
|
|
||||||
|
[default: 5000]
|
||||||
|
|
||||||
|
Genesis Configuration:
|
||||||
|
--genesis.path <genesis.path>
|
||||||
|
Specifies the path of the genesis file to use for the nodes that are started.
|
||||||
|
|
||||||
|
This is expected to be the path of a JSON geth genesis file.
|
||||||
|
|
||||||
|
Wallet Configuration:
|
||||||
|
--wallet.default-private-key <DEFAULT_KEY>
|
||||||
|
The private key of the default signer
|
||||||
|
|
||||||
[default: 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d]
|
[default: 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d]
|
||||||
|
|
||||||
--private-keys-count <PRIVATE_KEYS_TO_ADD>
|
--wallet.additional-keys <ADDITIONAL_KEYS>
|
||||||
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
|
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]
|
[default: 100000]
|
||||||
|
|
||||||
-l, --leader <LEADER>
|
Concurrency Configuration:
|
||||||
The differential testing leader node implementation
|
--concurrency.number-of-nodes <NUMBER_OF_NODES>
|
||||||
|
|
||||||
[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
|
Determines the amount of nodes that will be spawned for each chain
|
||||||
|
|
||||||
[default: 1]
|
[default: 5]
|
||||||
|
|
||||||
--number-of-threads <NUMBER_OF_THREADS>
|
--concurrency.number-of-threads <NUMBER_OF_THREADS>
|
||||||
Determines the amount of tokio worker threads that will will be used
|
Determines the amount of tokio worker threads that will will be used
|
||||||
|
|
||||||
[default: 16]
|
[default: 16]
|
||||||
|
|
||||||
--number-concurrent-tasks <NUMBER_CONCURRENT_TASKS>
|
--concurrency.number-of-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
|
Determines the amount of concurrent tasks that will be spawned to run tests.
|
||||||
|
|
||||||
-e, --extract-problems
|
Defaults to 10 x the number of nodes.
|
||||||
Extract problems back to the test corpus
|
|
||||||
|
|
||||||
-k, --kitchensink <KITCHENSINK>
|
--concurrency.ignore-concurrency-limit
|
||||||
The path to the `kitchensink` executable.
|
Determines if the concurrency limit should be ignored or not
|
||||||
|
|
||||||
By default it uses `substrate-node` binary found in `$PATH`.
|
Compilation Configuration:
|
||||||
|
--compilation.invalidate-cache
|
||||||
[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
|
Controls if the compilation cache should be invalidated or not
|
||||||
|
|
||||||
-h, --help
|
Report Configuration:
|
||||||
Print help (see a summary with '-h')
|
--report.include-compiler-input
|
||||||
|
Controls if the compiler input is included in the final report
|
||||||
|
|
||||||
|
--report.include-compiler-output
|
||||||
|
Controls if the compiler output is included in the final report
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
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:
|
||||||
@@ -187,10 +217,12 @@ The above corpus file instructs the tool to look for all of the test cases conta
|
|||||||
The simplest command to run this tool is the following:
|
The simplest command to run this tool is the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
RUST_LOG="info" cargo run --release -- \
|
RUST_LOG="info" cargo run --release -- execute-tests \
|
||||||
--corpus path_to_your_corpus_file.json \
|
--platform geth-evm-solc \
|
||||||
--workdir path_to_a_temporary_directory_to_cache_things_in \
|
--corpus corp.json \
|
||||||
--number-of-nodes 5 \
|
--working-directory workdir \
|
||||||
|
--concurrency.number-of-nodes 5 \
|
||||||
|
--concurrency.ignore-concurrency-limit \
|
||||||
> logs.log \
|
> logs.log \
|
||||||
2> output.log
|
2> output.log
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
alloy = { workspace = true }
|
||||||
|
alloy-primitives = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
moka = { workspace = true, features = ["sync"] }
|
moka = { workspace = true, features = ["sync"] }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
schemars = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
tokio = { workspace = true, default-features = false, features = ["time"] }
|
tokio = { workspace = true, default-features = false, features = ["time"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -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;
|
mod files_with_extension_iterator;
|
||||||
|
|
||||||
|
pub use either_iter::*;
|
||||||
pub use files_with_extension_iterator::*;
|
pub use files_with_extension_iterator::*;
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
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 kitchensink node with the PolkaVM backend with the resolc compiler.
|
||||||
|
KitchensinkPolkavmResolc,
|
||||||
|
/// The kitchensink node with the REVM backend with the solc compiler.
|
||||||
|
KitchensinkRevmSolc,
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 Kitchensink node implementation.
|
||||||
|
Kitchensink,
|
||||||
|
/// The revive dev node implementation.
|
||||||
|
ReviveDevNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,9 @@
|
|||||||
|
mod identifiers;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
mod private_key_allocator;
|
||||||
mod version_or_requirement;
|
mod version_or_requirement;
|
||||||
|
|
||||||
|
pub use identifiers::*;
|
||||||
pub use mode::*;
|
pub use mode::*;
|
||||||
|
pub use private_key_allocator::*;
|
||||||
pub use version_or_requirement::*;
|
pub use version_or_requirement::*;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use semver::Version;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
/// This represents a mode that a given test should be run with, if possible.
|
/// This represents a mode that a given test should be run with, if possible.
|
||||||
///
|
///
|
||||||
@@ -34,14 +35,19 @@ impl Display for Mode {
|
|||||||
|
|
||||||
impl Mode {
|
impl Mode {
|
||||||
/// Return all of the available mode combinations.
|
/// Return all of the available mode combinations.
|
||||||
pub fn all() -> impl Iterator<Item = Mode> {
|
pub fn all() -> impl Iterator<Item = &'static Mode> {
|
||||||
ModePipeline::test_cases().flat_map(|pipeline| {
|
static ALL_MODES: LazyLock<Vec<Mode>> = LazyLock::new(|| {
|
||||||
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
|
ModePipeline::test_cases()
|
||||||
pipeline,
|
.flat_map(|pipeline| {
|
||||||
optimize_setting,
|
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
|
||||||
version: None,
|
pipeline,
|
||||||
})
|
optimize_setting,
|
||||||
})
|
version: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
ALL_MODES.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
|
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use alloy::signers::local::PrivateKeySigner;
|
||||||
|
use alloy_primitives::U256;
|
||||||
|
use anyhow::{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_exclusive: U256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivateKeyAllocator {
|
||||||
|
/// Creates a new instance of the private key allocator.
|
||||||
|
pub fn new(highest_private_key_exclusive: U256) -> Self {
|
||||||
|
Self {
|
||||||
|
next_private_key: U256::ZERO,
|
||||||
|
highest_private_key_exclusive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_exclusive {
|
||||||
|
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())?;
|
||||||
|
self.next_private_key += U256::ONE;
|
||||||
|
Ok(private_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
+21
-51
@@ -3,24 +3,21 @@
|
|||||||
//! - Polkadot revive resolc compiler
|
//! - Polkadot revive resolc compiler
|
||||||
//! - Polkadot revive Wasm compiler
|
//! - Polkadot revive Wasm compiler
|
||||||
|
|
||||||
mod constants;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
};
|
};
|
||||||
|
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
use alloy_primitives::Address;
|
use alloy_primitives::Address;
|
||||||
use anyhow::Context;
|
use anyhow::{Context as _, Result};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use revive_dt_common::cached_fs::read_to_string;
|
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.
|
// Re-export this as it's a part of the compiler interface.
|
||||||
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||||
@@ -31,35 +28,28 @@ pub mod solc;
|
|||||||
|
|
||||||
/// A common interface for all supported Solidity compilers.
|
/// A common interface for all supported Solidity compilers.
|
||||||
pub trait SolidityCompiler {
|
pub trait SolidityCompiler {
|
||||||
/// Extra options specific to the compiler.
|
/// Returns the version of the compiler.
|
||||||
type Options: Default + PartialEq + Eq + Hash;
|
fn version(&self) -> &Version;
|
||||||
|
|
||||||
|
/// Returns the path of the compiler executable.
|
||||||
|
fn path(&self) -> &Path;
|
||||||
|
|
||||||
/// The low-level compiler interface.
|
/// The low-level compiler interface.
|
||||||
fn build(
|
fn build(
|
||||||
&self,
|
&self,
|
||||||
input: CompilerInput,
|
input: CompilerInput,
|
||||||
additional_options: Self::Options,
|
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>>;
|
||||||
) -> impl Future<Output = anyhow::Result<CompilerOutput>>;
|
|
||||||
|
|
||||||
fn new(solc_executable: PathBuf) -> Self;
|
/// Does the compiler support the provided mode and version settings.
|
||||||
|
|
||||||
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?
|
|
||||||
fn supports_mode(
|
fn supports_mode(
|
||||||
compiler_version: &Version,
|
&self,
|
||||||
optimize_setting: ModeOptimizerSetting,
|
optimizer_setting: ModeOptimizerSetting,
|
||||||
pipeline: ModePipeline,
|
pipeline: ModePipeline,
|
||||||
) -> bool;
|
) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The generic compilation input configuration.
|
/// The generic compilation input configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct CompilerInput {
|
pub struct CompilerInput {
|
||||||
pub pipeline: Option<ModePipeline>,
|
pub pipeline: Option<ModePipeline>,
|
||||||
pub optimization: Option<ModeOptimizerSetting>,
|
pub optimization: Option<ModeOptimizerSetting>,
|
||||||
@@ -74,27 +64,18 @@ pub struct CompilerInput {
|
|||||||
/// The generic compilation output configuration.
|
/// The generic compilation output configuration.
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct CompilerOutput {
|
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.
|
/// required and the compiled source has placeholders.
|
||||||
pub contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
pub contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A generic builder style interface for configuring the supported compiler options.
|
/// A generic builder style interface for configuring the supported compiler options.
|
||||||
pub struct Compiler<T: SolidityCompiler> {
|
#[derive(Default)]
|
||||||
|
pub struct Compiler {
|
||||||
input: CompilerInput,
|
input: CompilerInput,
|
||||||
additional_options: T::Options,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Compiler<solc::Solc> {
|
impl Compiler {
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Compiler<T>
|
|
||||||
where
|
|
||||||
T: SolidityCompiler,
|
|
||||||
{
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
input: CompilerInput {
|
input: CompilerInput {
|
||||||
@@ -107,7 +88,6 @@ where
|
|||||||
libraries: Default::default(),
|
libraries: Default::default(),
|
||||||
revert_string_handling: Default::default(),
|
revert_string_handling: Default::default(),
|
||||||
},
|
},
|
||||||
additional_options: T::Options::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +116,7 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_source(mut self, path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
pub fn with_source(mut self, path: impl AsRef<Path>) -> Result<Self> {
|
||||||
self.input.sources.insert(
|
self.input.sources.insert(
|
||||||
path.as_ref().to_path_buf(),
|
path.as_ref().to_path_buf(),
|
||||||
read_to_string(path.as_ref()).context("Failed to read the contract source")?,
|
read_to_string(path.as_ref()).context("Failed to read the contract source")?,
|
||||||
@@ -166,11 +146,6 @@ where
|
|||||||
self
|
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 {
|
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
|
||||||
callback(self)
|
callback(self)
|
||||||
}
|
}
|
||||||
@@ -179,17 +154,12 @@ where
|
|||||||
callback(self)
|
callback(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn try_build(
|
pub async fn try_build(self, compiler: &dyn SolidityCompiler) -> Result<CompilerOutput> {
|
||||||
self,
|
compiler.build(self.input).await
|
||||||
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 fn input(&self) -> CompilerInput {
|
pub fn input(&self) -> &CompilerInput {
|
||||||
self.input.clone()
|
&self.input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,42 +3,84 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Command, Stdio},
|
pin::Pin,
|
||||||
sync::LazyLock,
|
process::Stdio,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use revive_dt_common::types::VersionOrRequirement;
|
use revive_dt_common::types::VersionOrRequirement;
|
||||||
use revive_dt_config::Arguments;
|
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||||
use revive_solc_json_interface::{
|
use revive_solc_json_interface::{
|
||||||
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
|
||||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
||||||
SolcStandardJsonOutput,
|
SolcStandardJsonOutput,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
use crate::{
|
||||||
|
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
|
||||||
|
};
|
||||||
|
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
use anyhow::Context;
|
use anyhow::{Context as _, Result};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
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.
|
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Resolc {
|
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
|
/// Path to the `resolc` executable
|
||||||
resolc_path: PathBuf,
|
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 {
|
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)]
|
#[tracing::instrument(level = "debug", ret)]
|
||||||
async fn build(
|
fn build(
|
||||||
&self,
|
&self,
|
||||||
CompilerInput {
|
CompilerInput {
|
||||||
pipeline,
|
pipeline,
|
||||||
@@ -52,294 +94,204 @@ impl SolidityCompiler for Resolc {
|
|||||||
// resolc. So, we need to go back to this later once it's supported.
|
// resolc. So, we need to go back to this later once it's supported.
|
||||||
revert_string_handling: _,
|
revert_string_handling: _,
|
||||||
}: CompilerInput,
|
}: CompilerInput,
|
||||||
additional_options: Self::Options,
|
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>> {
|
||||||
) -> anyhow::Result<CompilerOutput> {
|
Box::pin(async move {
|
||||||
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
|
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
|
"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()
|
|
||||||
.with_context(|| format!("Failed to spawn resolc at {}", self.resolc_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 {
|
let input = SolcStandardJsonInput {
|
||||||
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
|
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 compiler_output = CompilerOutput::default();
|
let path = &self.0.resolc_path;
|
||||||
for (source_path, contracts) in contracts.into_iter() {
|
let mut command = AsyncCommand::new(path);
|
||||||
let src_for_msg = source_path.clone();
|
command
|
||||||
let source_path = PathBuf::from(source_path)
|
.stdin(Stdio::piped())
|
||||||
.canonicalize()
|
.stdout(Stdio::piped())
|
||||||
.with_context(|| format!("Failed to canonicalize path {src_for_msg}"))?;
|
.stderr(Stdio::piped())
|
||||||
|
.arg("--standard-json");
|
||||||
|
|
||||||
let map = compiler_output.contracts.entry(source_path).or_default();
|
if let Some(ref base_path) = base_path {
|
||||||
for (contract_name, contract_information) in contracts.into_iter() {
|
command.arg("--base-path").arg(base_path);
|
||||||
let bytecode = contract_information
|
}
|
||||||
.evm
|
if !allow_paths.is_empty() {
|
||||||
.and_then(|evm| evm.bytecode.clone())
|
command.arg("--allow-paths").arg(
|
||||||
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
|
allow_paths
|
||||||
let abi = {
|
.iter()
|
||||||
let metadata = contract_information
|
.map(|path| path.display().to_string())
|
||||||
.metadata
|
.collect::<Vec<_>>()
|
||||||
.as_ref()
|
.join(","),
|
||||||
.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(),
|
let mut child = command
|
||||||
serde_json::Value::Object(metadata_object) => {
|
.spawn()
|
||||||
let solc_metadata_value = metadata_object
|
.with_context(|| format!("Failed to spawn resolc at {}", path.display()))?;
|
||||||
.get("solc_metadata")
|
|
||||||
.context("Contract doesn't have a 'solc_metadata' field")?;
|
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
|
||||||
solc_metadata_value
|
let serialized_input = serde_json::to_vec(&input)
|
||||||
.as_str()
|
.context("Failed to serialize Standard JSON input for resolc")?;
|
||||||
.context("The 'solc_metadata' field is not a string")?
|
stdin_pipe
|
||||||
}
|
.write_all(&serialized_input)
|
||||||
serde_json::Value::Null
|
.await
|
||||||
| serde_json::Value::Bool(_)
|
.context("Failed to write Standard JSON to resolc stdin")?;
|
||||||
| serde_json::Value::Number(_)
|
|
||||||
| serde_json::Value::Array(_) => {
|
let output = child
|
||||||
anyhow::bail!("Unsupported type of metadata {metadata:?}")
|
.wait_with_output()
|
||||||
}
|
.await
|
||||||
};
|
.context("Failed while waiting for resolc process to finish")?;
|
||||||
let solc_metadata =
|
let stdout = output.stdout;
|
||||||
serde_json::from_str::<serde_json::Value>(solc_metadata_str).context(
|
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",
|
"Failed to deserialize the solc_metadata as a serde_json generic value",
|
||||||
)?;
|
)?;
|
||||||
let output_value = solc_metadata
|
let output_value = solc_metadata
|
||||||
.get("output")
|
.get("output")
|
||||||
.context("solc_metadata doesn't have an output field")?;
|
.context("solc_metadata doesn't have an output field")?;
|
||||||
let abi_value = output_value
|
let abi_value = output_value
|
||||||
.get("abi")
|
.get("abi")
|
||||||
.context("solc_metadata output doesn't contain an abi field")?;
|
.context("solc_metadata output doesn't contain an abi field")?;
|
||||||
serde_json::from_value::<JsonAbi>(abi_value.clone())
|
serde_json::from_value::<JsonAbi>(abi_value.clone())
|
||||||
.context("ABI found in solc_metadata output is not valid ABI")?
|
.context("ABI found in solc_metadata output is not valid ABI")?
|
||||||
};
|
};
|
||||||
map.insert(contract_name, (bytecode.object, abi));
|
map.insert(contract_name, (bytecode.object, abi));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(compiler_output)
|
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()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed to spawn resolc at {} to get version",
|
|
||||||
self.resolc_path.display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.wait_with_output()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed waiting for resolc at {} to finish --version",
|
|
||||||
self.resolc_path.display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.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).with_context(|| {
|
|
||||||
format!("Failed to parse resolc semver from '{version_string}'")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
vacant_entry.insert(version.clone());
|
|
||||||
|
|
||||||
Ok(version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_mode(
|
fn supports_mode(
|
||||||
_compiler_version: &Version,
|
&self,
|
||||||
_optimize_setting: ModeOptimizerSetting,
|
optimize_setting: ModeOptimizerSetting,
|
||||||
pipeline: ModePipeline,
|
pipeline: ModePipeline,
|
||||||
) -> bool {
|
) -> 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
|
pipeline == ModePipeline::ViaYulIR
|
||||||
}
|
&& SolidityCompiler::supports_mode(&self.0.solc, optimize_setting, pipeline)
|
||||||
}
|
|
||||||
|
|
||||||
#[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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+220
-275
@@ -3,19 +3,19 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Command, Stdio},
|
pin::Pin,
|
||||||
sync::LazyLock,
|
process::Stdio,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use revive_dt_common::types::VersionOrRequirement;
|
use revive_dt_common::types::VersionOrRequirement;
|
||||||
use revive_dt_config::Arguments;
|
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
|
||||||
use revive_dt_solc_binaries::download_solc;
|
use revive_dt_solc_binaries::download_solc;
|
||||||
|
|
||||||
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
|
|
||||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::{Context as _, Result};
|
||||||
use foundry_compilers_artifacts::{
|
use foundry_compilers_artifacts::{
|
||||||
output_selection::{
|
output_selection::{
|
||||||
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
|
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
|
||||||
@@ -26,16 +26,69 @@ use foundry_compilers_artifacts::{
|
|||||||
use semver::Version;
|
use semver::Version;
|
||||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Solc {
|
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,
|
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<ResolcConfiguration>
|
||||||
|
+ 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(|| {
|
||||||
|
Self(Arc::new(SolcInner {
|
||||||
|
solc_path: path,
|
||||||
|
solc_version: version,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SolidityCompiler for Solc {
|
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)]
|
#[tracing::instrument(level = "debug", ret)]
|
||||||
async fn build(
|
fn build(
|
||||||
&self,
|
&self,
|
||||||
CompilerInput {
|
CompilerInput {
|
||||||
pipeline,
|
pipeline,
|
||||||
@@ -47,298 +100,190 @@ impl SolidityCompiler for Solc {
|
|||||||
libraries,
|
libraries,
|
||||||
revert_string_handling,
|
revert_string_handling,
|
||||||
}: CompilerInput,
|
}: CompilerInput,
|
||||||
_: Self::Options,
|
) -> Pin<Box<dyn Future<Output = Result<CompilerOutput>> + '_>> {
|
||||||
) -> anyhow::Result<CompilerOutput> {
|
Box::pin(async move {
|
||||||
let compiler_supports_via_ir = self
|
// Be careful to entirely omit the viaIR field if the compiler does not support it,
|
||||||
.version()
|
// as it will error if you provide fields it does not know about. Because
|
||||||
.await
|
// `supports_mode` is called prior to instantiating a compiler, we should never
|
||||||
.context("Failed to query solc version to determine via-ir support")?
|
// ask for something which is invalid.
|
||||||
>= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
|
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,
|
let input = SolcInput {
|
||||||
// as it will error if you provide fields it does not know about. Because
|
language: SolcLanguage::Solidity,
|
||||||
// `supports_mode` is called prior to instantiating a compiler, we should never
|
sources: Sources(
|
||||||
// ask for something which is invalid.
|
sources
|
||||||
let via_ir = match (pipeline, compiler_supports_via_ir) {
|
.into_iter()
|
||||||
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
|
.map(|(source_path, source_code)| (source_path, Source::new(source_code)))
|
||||||
(_pipeline, false) => None,
|
.collect(),
|
||||||
};
|
),
|
||||||
|
settings: Settings {
|
||||||
let input = SolcInput {
|
optimizer: Optimizer {
|
||||||
language: SolcLanguage::Solidity,
|
enabled: optimization.map(|o| o.optimizations_enabled()),
|
||||||
sources: Sources(
|
details: Some(Default::default()),
|
||||||
sources
|
..Default::default()
|
||||||
.into_iter()
|
},
|
||||||
.map(|(source_path, source_code)| (source_path, Source::new(source_code)))
|
output_selection: OutputSelection::common_output_selection(
|
||||||
.collect(),
|
[
|
||||||
),
|
ContractOutputSelection::Abi,
|
||||||
settings: Settings {
|
ContractOutputSelection::Evm(EvmOutputSelection::ByteCode(
|
||||||
optimizer: Optimizer {
|
BytecodeOutputSelection::Object,
|
||||||
enabled: optimization.map(|o| o.optimizations_enabled()),
|
)),
|
||||||
details: Some(Default::default()),
|
]
|
||||||
|
.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()
|
..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()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut command = AsyncCommand::new(&self.solc_path);
|
let path = &self.0.solc_path;
|
||||||
command
|
let mut command = AsyncCommand::new(path);
|
||||||
.stdin(Stdio::piped())
|
command
|
||||||
.stdout(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.arg("--standard-json");
|
.stderr(Stdio::piped())
|
||||||
|
.arg("--standard-json");
|
||||||
|
|
||||||
if let Some(ref base_path) = base_path {
|
if let Some(ref base_path) = base_path {
|
||||||
command.arg("--base-path").arg(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 {}", self.solc_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")?;
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.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}")
|
|
||||||
}
|
}
|
||||||
}
|
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()))?;
|
||||||
|
|
||||||
tracing::debug!(
|
let stdin = child.stdin.as_mut().expect("should be piped");
|
||||||
output = %String::from_utf8_lossy(&output.stdout).to_string(),
|
let serialized_input = serde_json::to_vec(&input)
|
||||||
"Compiled successfully"
|
.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")?;
|
||||||
|
|
||||||
let mut compiler_output = CompilerOutput::default();
|
if !output.status.success() {
|
||||||
for (contract_path, contracts) in parsed.contracts {
|
let json_in = serde_json::to_string_pretty(&input)
|
||||||
let map = compiler_output
|
.context("Failed to pretty-print Standard JSON input for logging")?;
|
||||||
.contracts
|
let message = String::from_utf8_lossy(&output.stderr);
|
||||||
.entry(contract_path.canonicalize().with_context(|| {
|
tracing::error!(
|
||||||
format!(
|
status = %output.status,
|
||||||
"Failed to canonicalize contract path {}",
|
message = %message,
|
||||||
contract_path.display()
|
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)
|
||||||
)
|
)
|
||||||
})?)
|
})
|
||||||
.or_default();
|
.context("Failed to parse solc standard JSON output")?;
|
||||||
for (contract_name, contract_info) in contracts.into_iter() {
|
|
||||||
let source_code = contract_info
|
// Detecting if the compiler output contained errors and reporting them through logs and
|
||||||
.evm
|
// errors instead of returning the compiler output that might contain errors.
|
||||||
.and_then(|evm| evm.bytecode)
|
for error in parsed.errors.iter() {
|
||||||
.map(|bytecode| match bytecode.object {
|
if error.severity == Severity::Error {
|
||||||
BytecodeObject::Bytecode(bytecode) => bytecode.to_string(),
|
tracing::error!(?error, ?input, "Encountered an error in the compilation");
|
||||||
BytecodeObject::Unlinked(unlinked) => unlinked,
|
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));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(compiler_output)
|
tracing::debug!(
|
||||||
}
|
output = %String::from_utf8_lossy(&output.stdout).to_string(),
|
||||||
|
"Compiled successfully"
|
||||||
|
);
|
||||||
|
|
||||||
fn new(solc_path: PathBuf) -> Self {
|
let mut compiler_output = CompilerOutput::default();
|
||||||
Self { solc_path }
|
for (contract_path, contracts) in parsed.contracts {
|
||||||
}
|
let map = compiler_output
|
||||||
|
.contracts
|
||||||
async fn get_compiler_executable(
|
.entry(contract_path.canonicalize().with_context(|| {
|
||||||
config: &Arguments,
|
|
||||||
version: impl Into<VersionOrRequirement>,
|
|
||||||
) -> anyhow::Result<PathBuf> {
|
|
||||||
let path = download_solc(config.directory(), version, config.wasm)
|
|
||||||
.await
|
|
||||||
.context("Failed to download/get path to solc binary")?;
|
|
||||||
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()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"Failed to spawn solc at {} to get version",
|
"Failed to canonicalize contract path {}",
|
||||||
self.solc_path.display()
|
contract_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?)
|
||||||
let output = child.wait_with_output().with_context(|| {
|
.or_default();
|
||||||
format!(
|
for (contract_name, contract_info) in contracts.into_iter() {
|
||||||
"Failed waiting for solc at {} to finish --version",
|
let source_code = contract_info
|
||||||
self.solc_path.display()
|
.evm
|
||||||
)
|
.and_then(|evm| evm.bytecode)
|
||||||
})?;
|
.map(|bytecode| match bytecode.object {
|
||||||
let output = String::from_utf8_lossy(&output.stdout);
|
BytecodeObject::Bytecode(bytecode) => bytecode.to_string(),
|
||||||
let version_line = output
|
BytecodeObject::Unlinked(unlinked) => unlinked,
|
||||||
.split("Version: ")
|
})
|
||||||
.nth(1)
|
.context("Unexpected - contract compiled with solc has no source code")?;
|
||||||
.context("Version parsing failed")?;
|
let abi = contract_info
|
||||||
let version_string = version_line
|
.abi
|
||||||
.split("+")
|
.context("Unexpected - contract compiled with solc as no ABI")?;
|
||||||
.next()
|
map.insert(contract_name, (source_code, abi));
|
||||||
.context("Version parsing failed")?;
|
}
|
||||||
|
|
||||||
let version = Version::parse(version_string).with_context(|| {
|
|
||||||
format!("Failed to parse solc semver from '{version_string}'")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
vacant_entry.insert(version.clone());
|
|
||||||
|
|
||||||
Ok(version)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Ok(compiler_output)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_mode(
|
fn supports_mode(
|
||||||
compiler_version: &Version,
|
&self,
|
||||||
_optimize_setting: ModeOptimizerSetting,
|
_optimize_setting: ModeOptimizerSetting,
|
||||||
pipeline: ModePipeline,
|
pipeline: ModePipeline,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
|
// 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.
|
// (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::ViaEVMAssembly
|
||||||
|| (pipeline == ModePipeline::ViaYulIR
|
|| (pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
|
||||||
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
impl Solc {
|
||||||
mod test {
|
fn compiler_supports_yul(&self) -> bool {
|
||||||
use super::*;
|
const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
|
||||||
|
SolidityCompiler::version(self) >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
|
||||||
#[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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use revive_dt_compiler::{Compiler, SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
use revive_dt_common::types::VersionOrRequirement;
|
||||||
use revive_dt_config::Arguments;
|
use revive_dt_compiler::{Compiler, revive_resolc::Resolc, solc::Solc};
|
||||||
|
use revive_dt_config::TestExecutionContext;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn contracts_can_be_compiled_with_solc() {
|
async fn contracts_can_be_compiled_with_solc() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let args = Arguments::default();
|
let args = TestExecutionContext::default();
|
||||||
let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30))
|
let solc = Solc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let output = Compiler::<Solc>::new()
|
let output = Compiler::new()
|
||||||
.with_source("./tests/assets/array_one_element/callable.sol")
|
.with_source("./tests/assets/array_one_element/callable.sol")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_source("./tests/assets/array_one_element/main.sol")
|
.with_source("./tests/assets/array_one_element/main.sol")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.try_build(compiler_path)
|
.try_build(&solc)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -48,18 +49,18 @@ async fn contracts_can_be_compiled_with_solc() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn contracts_can_be_compiled_with_resolc() {
|
async fn contracts_can_be_compiled_with_resolc() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let args = Arguments::default();
|
let args = TestExecutionContext::default();
|
||||||
let compiler_path = Resolc::get_compiler_executable(&args, Version::new(0, 8, 30))
|
let resolc = Resolc::new(&args, VersionOrRequirement::Version(Version::new(0, 8, 30)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let output = Compiler::<Resolc>::new()
|
let output = Compiler::new()
|
||||||
.with_source("./tests/assets/array_one_element/callable.sol")
|
.with_source("./tests/assets/array_one_element/callable.sol")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_source("./tests/assets/array_one_element/main.sol")
|
.with_source("./tests/assets/array_one_element/main.sol")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.try_build(compiler_path)
|
.try_build(&resolc)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
revive-dt-common = { workspace = true }
|
||||||
|
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
temp-dir = { workspace = true }
|
temp-dir = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
+579
-138
@@ -2,219 +2,660 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
fs::read_to_string,
|
||||||
|
ops::Deref,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::LazyLock,
|
str::FromStr,
|
||||||
|
sync::{Arc, LazyLock, OnceLock},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
|
use alloy::{
|
||||||
use clap::{Parser, ValueEnum};
|
genesis::Genesis,
|
||||||
|
hex::ToHexExt,
|
||||||
|
network::EthereumWallet,
|
||||||
|
primitives::{FixedBytes, U256},
|
||||||
|
signers::local::PrivateKeySigner,
|
||||||
|
};
|
||||||
|
use clap::{Parser, ValueEnum, ValueHint};
|
||||||
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Serialize, Serializer};
|
||||||
|
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
||||||
use temp_dir::TempDir;
|
use temp_dir::TempDir;
|
||||||
|
|
||||||
#[derive(Debug, Parser, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
#[command(name = "retester")]
|
#[command(name = "retester")]
|
||||||
pub struct Arguments {
|
pub enum Context {
|
||||||
/// The `solc` version to use if the test didn't specify it explicitly.
|
/// Executes tests in the MatterLabs format differentially on multiple targets concurrently.
|
||||||
#[arg(long = "solc", short, default_value = "0.8.29")]
|
ExecuteTests(Box<TestExecutionContext>),
|
||||||
pub solc: Version,
|
/// Exports the JSON schema of the MatterLabs test format used by the tool.
|
||||||
|
ExportJsonSchema,
|
||||||
|
}
|
||||||
|
|
||||||
/// Use the Wasm compiler versions.
|
impl Context {
|
||||||
#[arg(long = "wasm")]
|
pub fn working_directory_configuration(&self) -> &WorkingDirectoryConfiguration {
|
||||||
pub wasm: bool,
|
self.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// The path to the `resolc` executable to be tested.
|
pub fn report_configuration(&self) -> &ReportConfiguration {
|
||||||
|
self.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<WorkingDirectoryConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<SolcConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &SolcConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ResolcConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &ResolcConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<GethConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &GethConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<KitchensinkConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &KitchensinkConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ReviveDevNodeConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<EthRpcConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &EthRpcConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<GenesisConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &GenesisConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<WalletConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &WalletConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ConcurrencyConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &ConcurrencyConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<CompilationConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &CompilationConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ReportConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &ReportConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::ExecuteTests(context) => context.as_ref().as_ref(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct TestExecutionContext {
|
||||||
|
/// The working directory that the program will use for all of the temporary artifacts needed at
|
||||||
|
/// runtime.
|
||||||
///
|
///
|
||||||
/// By default it uses the `resolc` binary found in `$PATH`.
|
/// If not specified, then a temporary directory will be created and used by the program for all
|
||||||
///
|
/// temporary artifacts.
|
||||||
/// If `--wasm` is set, this should point to the resolc Wasm ile.
|
#[clap(
|
||||||
#[arg(long = "resolc", short, default_value = "resolc")]
|
short,
|
||||||
pub resolc: PathBuf,
|
long,
|
||||||
|
default_value = "",
|
||||||
|
value_hint = ValueHint::DirPath,
|
||||||
|
)]
|
||||||
|
pub working_directory: WorkingDirectoryConfiguration,
|
||||||
|
|
||||||
|
/// The set of platforms that the differential tests should run on.
|
||||||
|
#[arg(
|
||||||
|
short = 'p',
|
||||||
|
long = "platform",
|
||||||
|
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
|
||||||
|
)]
|
||||||
|
pub platforms: Vec<PlatformIdentifier>,
|
||||||
|
|
||||||
/// A list of test corpus JSON files to be tested.
|
/// A list of test corpus JSON files to be tested.
|
||||||
#[arg(long = "corpus", short)]
|
#[arg(long = "corpus", short)]
|
||||||
pub corpus: Vec<PathBuf>,
|
pub corpus: Vec<PathBuf>,
|
||||||
|
|
||||||
/// A place to store temporary artifacts during test execution.
|
/// Configuration parameters for the solc compiler.
|
||||||
///
|
#[clap(flatten, next_help_heading = "Solc Configuration")]
|
||||||
/// Creates a temporary dir if not specified.
|
pub solc_configuration: SolcConfiguration,
|
||||||
#[arg(long = "workdir", short)]
|
|
||||||
pub working_directory: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Add a tempdir manually if `working_directory` was not given.
|
/// Configuration parameters for the resolc compiler.
|
||||||
|
#[clap(flatten, next_help_heading = "Resolc Configuration")]
|
||||||
|
pub resolc_configuration: ResolcConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the geth node.
|
||||||
|
#[clap(flatten, next_help_heading = "Geth Configuration")]
|
||||||
|
pub geth_configuration: GethConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the Kitchensink.
|
||||||
|
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
|
||||||
|
pub kitchensink_configuration: KitchensinkConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the Revive Dev Node.
|
||||||
|
#[clap(flatten, next_help_heading = "Revive Dev Node Configuration")]
|
||||||
|
pub revive_dev_node_configuration: ReviveDevNodeConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the Eth Rpc.
|
||||||
|
#[clap(flatten, next_help_heading = "Eth RPC Configuration")]
|
||||||
|
pub eth_rpc_configuration: EthRpcConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the genesis.
|
||||||
|
#[clap(flatten, next_help_heading = "Genesis Configuration")]
|
||||||
|
pub genesis_configuration: GenesisConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the wallet.
|
||||||
|
#[clap(flatten, next_help_heading = "Wallet Configuration")]
|
||||||
|
pub wallet_configuration: WalletConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for concurrency.
|
||||||
|
#[clap(flatten, next_help_heading = "Concurrency Configuration")]
|
||||||
|
pub concurrency_configuration: ConcurrencyConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the compilers and compilation.
|
||||||
|
#[clap(flatten, next_help_heading = "Compilation Configuration")]
|
||||||
|
pub compilation_configuration: CompilationConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for the report.
|
||||||
|
#[clap(flatten, next_help_heading = "Report Configuration")]
|
||||||
|
pub report_configuration: ReportConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestExecutionContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::parse_from(["execution-context"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<WorkingDirectoryConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
||||||
|
&self.working_directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<SolcConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &SolcConfiguration {
|
||||||
|
&self.solc_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ResolcConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &ResolcConfiguration {
|
||||||
|
&self.resolc_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<GethConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &GethConfiguration {
|
||||||
|
&self.geth_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<KitchensinkConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &KitchensinkConfiguration {
|
||||||
|
&self.kitchensink_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ReviveDevNodeConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
|
||||||
|
&self.revive_dev_node_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<EthRpcConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &EthRpcConfiguration {
|
||||||
|
&self.eth_rpc_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<GenesisConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &GenesisConfiguration {
|
||||||
|
&self.genesis_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<WalletConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &WalletConfiguration {
|
||||||
|
&self.wallet_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ConcurrencyConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &ConcurrencyConfiguration {
|
||||||
|
&self.concurrency_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<CompilationConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &CompilationConfiguration {
|
||||||
|
&self.compilation_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ReportConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &ReportConfiguration {
|
||||||
|
&self.report_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for Solc.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct SolcConfiguration {
|
||||||
|
/// Specifies the default version of the Solc compiler that should be used if there is no
|
||||||
|
/// override specified by one of the test cases.
|
||||||
|
#[clap(long = "solc.version", default_value = "0.8.29")]
|
||||||
|
pub version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for Resolc.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct ResolcConfiguration {
|
||||||
|
/// Specifies the path of the resolc compiler to be used by the tool.
|
||||||
///
|
///
|
||||||
/// We attach it here because [TempDir] prunes itself on drop.
|
/// If this is not specified, then the tool assumes that it should use the resolc binary that's
|
||||||
|
/// provided in the user's $PATH.
|
||||||
|
#[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")]
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for Geth.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct GethConfiguration {
|
||||||
|
/// Specifies the path of the geth node to be used by the tool.
|
||||||
|
///
|
||||||
|
/// If this is not specified, then the tool assumes that it should use the geth binary that's
|
||||||
|
/// provided in the user's $PATH.
|
||||||
|
#[clap(id = "geth.path", long = "geth.path", default_value = "geth")]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||||
|
#[clap(
|
||||||
|
id = "geth.start-timeout-ms",
|
||||||
|
long = "geth.start-timeout-ms",
|
||||||
|
default_value = "5000",
|
||||||
|
value_parser = parse_duration
|
||||||
|
)]
|
||||||
|
pub start_timeout_ms: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for Kitchensink.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct KitchensinkConfiguration {
|
||||||
|
/// Specifies the path of the kitchensink node to be used by the tool.
|
||||||
|
///
|
||||||
|
/// If this is not specified, then the tool assumes that it should use the kitchensink binary
|
||||||
|
/// that's provided in the user's $PATH.
|
||||||
|
#[clap(
|
||||||
|
id = "kitchensink.path",
|
||||||
|
long = "kitchensink.path",
|
||||||
|
default_value = "substrate-node"
|
||||||
|
)]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||||
|
#[clap(
|
||||||
|
id = "kitchensink.start-timeout-ms",
|
||||||
|
long = "kitchensink.start-timeout-ms",
|
||||||
|
default_value = "5000",
|
||||||
|
value_parser = parse_duration
|
||||||
|
)]
|
||||||
|
pub start_timeout_ms: Duration,
|
||||||
|
|
||||||
|
/// This configures the tool to use Kitchensink instead of using the revive-dev-node.
|
||||||
|
#[clap(long = "kitchensink.dont-use-dev-node")]
|
||||||
|
pub use_kitchensink: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for the revive dev node.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct ReviveDevNodeConfiguration {
|
||||||
|
/// Specifies the path of the revive dev node to be used by the tool.
|
||||||
|
///
|
||||||
|
/// If this is not specified, then the tool assumes that it should use the revive dev node binary
|
||||||
|
/// that's provided in the user's $PATH.
|
||||||
|
#[clap(
|
||||||
|
id = "revive-dev-node.path",
|
||||||
|
long = "revive-dev-node.path",
|
||||||
|
default_value = "revive-dev-node"
|
||||||
|
)]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||||
|
#[clap(
|
||||||
|
id = "revive-dev-node.start-timeout-ms",
|
||||||
|
long = "revive-dev-node.start-timeout-ms",
|
||||||
|
default_value = "5000",
|
||||||
|
value_parser = parse_duration
|
||||||
|
)]
|
||||||
|
pub start_timeout_ms: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for the ETH RPC.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct EthRpcConfiguration {
|
||||||
|
/// Specifies the path of the ETH RPC to be used by the tool.
|
||||||
|
///
|
||||||
|
/// If this is not specified, then the tool assumes that it should use the ETH RPC binary
|
||||||
|
/// that's provided in the user's $PATH.
|
||||||
|
#[clap(id = "eth-rpc.path", long = "eth-rpc.path", default_value = "eth-rpc")]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||||
|
#[clap(
|
||||||
|
id = "eth-rpc.start-timeout-ms",
|
||||||
|
long = "eth-rpc.start-timeout-ms",
|
||||||
|
default_value = "5000",
|
||||||
|
value_parser = parse_duration
|
||||||
|
)]
|
||||||
|
pub start_timeout_ms: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for the genesis.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct GenesisConfiguration {
|
||||||
|
/// Specifies the path of the genesis file to use for the nodes that are started.
|
||||||
|
///
|
||||||
|
/// This is expected to be the path of a JSON geth genesis file.
|
||||||
|
#[clap(id = "genesis.path", long = "genesis.path")]
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// The genesis object found at the provided path.
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub temp_dir: Option<&'static TempDir>,
|
genesis: OnceLock<Genesis>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The path to the `geth` executable.
|
impl GenesisConfiguration {
|
||||||
///
|
pub fn genesis(&self) -> anyhow::Result<&Genesis> {
|
||||||
/// By default it uses `geth` binary found in `$PATH`.
|
static DEFAULT_GENESIS: LazyLock<Genesis> = LazyLock::new(|| {
|
||||||
#[arg(short, long = "geth", default_value = "geth")]
|
let genesis = include_str!("../../../genesis.json");
|
||||||
pub geth: PathBuf,
|
serde_json::from_str(genesis).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
/// The maximum time in milliseconds to wait for geth to start.
|
match self.genesis.get() {
|
||||||
#[arg(long = "geth-start-timeout", default_value = "5000")]
|
Some(genesis) => Ok(genesis),
|
||||||
pub geth_start_timeout: u64,
|
None => {
|
||||||
|
let genesis = match self.path.as_ref() {
|
||||||
|
Some(genesis_path) => {
|
||||||
|
let genesis_content = read_to_string(genesis_path)?;
|
||||||
|
serde_json::from_str(genesis_content.as_str())?
|
||||||
|
}
|
||||||
|
None => DEFAULT_GENESIS.clone(),
|
||||||
|
};
|
||||||
|
Ok(self.genesis.get_or_init(|| genesis))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configure nodes according to this genesis.json file.
|
/// A set of configuration parameters for the wallet.
|
||||||
#[arg(long = "genesis", default_value = "genesis.json")]
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
pub genesis_file: PathBuf,
|
pub struct WalletConfiguration {
|
||||||
|
/// The private key of the default signer.
|
||||||
/// The signing account private key.
|
#[clap(
|
||||||
#[arg(
|
long = "wallet.default-private-key",
|
||||||
short,
|
|
||||||
long = "account",
|
|
||||||
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||||
)]
|
)]
|
||||||
pub account: String,
|
#[serde(serialize_with = "serialize_private_key")]
|
||||||
|
default_key: PrivateKeySigner,
|
||||||
|
|
||||||
/// This argument controls which private keys the nodes should have access to and be added to
|
/// 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
|
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
|
||||||
/// of the node.
|
/// of the node.
|
||||||
#[arg(long = "private-keys-count", default_value_t = 100_000)]
|
#[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
|
||||||
pub private_keys_to_add: usize,
|
additional_keys: usize,
|
||||||
|
|
||||||
/// The differential testing leader node implementation.
|
/// The wallet object that will be used.
|
||||||
#[arg(short, long = "leader", default_value = "geth")]
|
#[clap(skip)]
|
||||||
pub leader: TestingPlatform,
|
#[serde(skip)]
|
||||||
|
wallet: OnceLock<Arc<EthereumWallet>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The differential testing follower node implementation.
|
impl WalletConfiguration {
|
||||||
#[arg(short, long = "follower", default_value = "kitchensink")]
|
pub fn wallet(&self) -> Arc<EthereumWallet> {
|
||||||
pub follower: TestingPlatform,
|
self.wallet
|
||||||
|
.get_or_init(|| {
|
||||||
|
let mut wallet = EthereumWallet::new(self.default_key.clone());
|
||||||
|
for signer in (1..=self.additional_keys)
|
||||||
|
.map(|id| U256::from(id))
|
||||||
|
.map(|id| id.to_be_bytes::<32>())
|
||||||
|
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
|
||||||
|
{
|
||||||
|
wallet.register_signer(signer);
|
||||||
|
}
|
||||||
|
Arc::new(wallet)
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Only compile against this testing platform (doesn't execute the tests).
|
pub fn highest_private_key_exclusive(&self) -> U256 {
|
||||||
#[arg(long = "compile-only")]
|
U256::try_from(self.additional_keys).unwrap()
|
||||||
pub compile_only: Option<TestingPlatform>,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_private_key<S>(value: &PrivateKeySigner, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
value.to_bytes().encode_hex().serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of configuration for concurrency.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct ConcurrencyConfiguration {
|
||||||
/// Determines the amount of nodes that will be spawned for each chain.
|
/// Determines the amount of nodes that will be spawned for each chain.
|
||||||
#[arg(long, default_value = "1")]
|
#[clap(long = "concurrency.number-of-nodes", default_value_t = 5)]
|
||||||
pub number_of_nodes: usize,
|
pub number_of_nodes: usize,
|
||||||
|
|
||||||
/// Determines the amount of tokio worker threads that will will be used.
|
/// Determines the amount of tokio worker threads that will will be used.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long = "concurrency.number-of-threads",
|
||||||
default_value_t = std::thread::available_parallelism()
|
default_value_t = std::thread::available_parallelism()
|
||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
)]
|
)]
|
||||||
pub number_of_threads: usize,
|
pub number_of_threads: usize,
|
||||||
|
|
||||||
/// Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes.
|
/// Determines the amount of concurrent tasks that will be spawned to run tests.
|
||||||
#[arg(long)]
|
|
||||||
pub number_concurrent_tasks: Option<usize>,
|
|
||||||
|
|
||||||
/// Extract problems back to the test corpus.
|
|
||||||
#[arg(short, long = "extract-problems")]
|
|
||||||
pub extract_problems: bool,
|
|
||||||
|
|
||||||
/// The path to the `kitchensink` executable.
|
|
||||||
///
|
///
|
||||||
/// By default it uses `substrate-node` binary found in `$PATH`.
|
/// Defaults to 10 x the number of nodes.
|
||||||
#[arg(short, long = "kitchensink", default_value = "substrate-node")]
|
#[arg(long = "concurrency.number-of-concurrent-tasks")]
|
||||||
pub kitchensink: PathBuf,
|
number_concurrent_tasks: Option<usize>,
|
||||||
|
|
||||||
/// The path to the `revive-dev-node` executable.
|
/// Determines if the concurrency limit should be ignored or not.
|
||||||
///
|
#[arg(long = "concurrency.ignore-concurrency-limit")]
|
||||||
/// By default it uses `revive-dev-node` binary found in `$PATH`.
|
ignore_concurrency_limit: bool,
|
||||||
#[arg(long = "revive-dev-node", default_value = "revive-dev-node")]
|
}
|
||||||
pub revive_dev_node: PathBuf,
|
|
||||||
|
|
||||||
/// By default the tool uses the revive-dev-node when it's running differential tests against
|
impl ConcurrencyConfiguration {
|
||||||
/// PolkaVM since the dev-node is much faster than kitchensink. This flag allows the caller to
|
pub fn concurrency_limit(&self) -> Option<usize> {
|
||||||
/// configure the tool to use kitchensink rather than the dev-node.
|
match self.ignore_concurrency_limit {
|
||||||
#[arg(long)]
|
true => None,
|
||||||
pub use_kitchensink_not_dev_node: bool,
|
false => Some(
|
||||||
|
self.number_concurrent_tasks
|
||||||
/// The path to the `eth_proxy` executable.
|
.unwrap_or(20 * self.number_of_nodes),
|
||||||
///
|
),
|
||||||
/// By default it uses `eth-rpc` binary found in `$PATH`.
|
}
|
||||||
#[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")]
|
}
|
||||||
pub eth_proxy: PathBuf,
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct CompilationConfiguration {
|
||||||
/// Controls if the compilation cache should be invalidated or not.
|
/// Controls if the compilation cache should be invalidated or not.
|
||||||
#[arg(short, long)]
|
#[arg(long = "compilation.invalidate-cache")]
|
||||||
pub invalidate_compilation_cache: bool,
|
pub invalidate_compilation_cache: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize)]
|
||||||
|
pub struct ReportConfiguration {
|
||||||
/// Controls if the compiler input is included in the final report.
|
/// Controls if the compiler input is included in the final report.
|
||||||
#[clap(long = "report.include-compiler-input")]
|
#[clap(long = "report.include-compiler-input")]
|
||||||
pub report_include_compiler_input: bool,
|
pub include_compiler_input: bool,
|
||||||
|
|
||||||
/// Controls if the compiler output is included in the final report.
|
/// Controls if the compiler output is included in the final report.
|
||||||
#[clap(long = "report.include-compiler-output")]
|
#[clap(long = "report.include-compiler-output")]
|
||||||
pub report_include_compiler_output: bool,
|
pub include_compiler_output: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arguments {
|
/// Represents the working directory that the program uses.
|
||||||
/// Return the configured working directory with the following precedence:
|
#[derive(Debug, Clone)]
|
||||||
/// 1. `self.working_directory` if it was provided.
|
pub enum WorkingDirectoryConfiguration {
|
||||||
/// 2. `self.temp_dir` if it it was provided
|
/// A temporary directory is used as the working directory. This will be removed when dropped.
|
||||||
/// 3. Panic.
|
TemporaryDirectory(Arc<TempDir>),
|
||||||
pub fn directory(&self) -> &Path {
|
/// A directory with a path is used as the working directory.
|
||||||
if let Some(path) = &self.working_directory {
|
Path(PathBuf),
|
||||||
return path.as_path();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(temp_dir) = &self.temp_dir {
|
impl WorkingDirectoryConfiguration {
|
||||||
return temp_dir.path();
|
pub fn as_path(&self) -> &Path {
|
||||||
}
|
self.as_ref()
|
||||||
|
|
||||||
panic!("should have a workdir configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of concurrent tasks to run. This is provided via the
|
|
||||||
/// `--number-concurrent-tasks` argument, and otherwise defaults to --number-of-nodes * 20.
|
|
||||||
pub fn number_of_concurrent_tasks(&self) -> usize {
|
|
||||||
self.number_concurrent_tasks
|
|
||||||
.unwrap_or(20 * self.number_of_nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to parse `self.account` into a [PrivateKeySigner],
|
|
||||||
/// panicing on error.
|
|
||||||
pub fn wallet(&self) -> EthereumWallet {
|
|
||||||
let signer = self
|
|
||||||
.account
|
|
||||||
.parse::<PrivateKeySigner>()
|
|
||||||
.unwrap_or_else(|error| {
|
|
||||||
panic!("private key '{}' parsing error: {error}", self.account);
|
|
||||||
});
|
|
||||||
EthereumWallet::new(signer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Arguments {
|
impl Deref for WorkingDirectoryConfiguration {
|
||||||
|
type Target = Path;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.as_path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for WorkingDirectoryConfiguration {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
WorkingDirectoryConfiguration::TemporaryDirectory(temp_dir) => temp_dir.path(),
|
||||||
|
WorkingDirectoryConfiguration::Path(path) => path.as_path(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WorkingDirectoryConfiguration {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
|
TempDir::new()
|
||||||
|
.map(Arc::new)
|
||||||
|
.map(Self::TemporaryDirectory)
|
||||||
|
.expect("Failed to create the temporary directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let default = Arguments::parse_from(["retester"]);
|
impl FromStr for WorkingDirectoryConfiguration {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
Arguments {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
temp_dir: Some(&TEMP_DIR),
|
match s {
|
||||||
..default
|
"" => Ok(Default::default()),
|
||||||
|
_ => Ok(Self::Path(PathBuf::from(s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for WorkingDirectoryConfiguration {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.as_path().display(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for WorkingDirectoryConfiguration {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.as_path().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
|
||||||
|
u64::from_str(s)
|
||||||
|
.map(Duration::from_millis)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
/// The Solidity compatible node implementation.
|
/// The Solidity compatible node implementation.
|
||||||
///
|
///
|
||||||
/// This describes the solutions to be tested against on a high level.
|
/// This describes the solutions to be tested against on a high level.
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum, Serialize, Deserialize,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Hash,
|
||||||
|
Serialize,
|
||||||
|
ValueEnum,
|
||||||
|
EnumString,
|
||||||
|
Display,
|
||||||
|
AsRefStr,
|
||||||
|
IntoStaticStr,
|
||||||
)]
|
)]
|
||||||
#[clap(rename_all = "lower")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
pub enum TestingPlatform {
|
pub enum TestingPlatform {
|
||||||
/// The go-ethereum reference full node EVM implementation.
|
/// The go-ethereum reference full node EVM implementation.
|
||||||
Geth,
|
Geth,
|
||||||
/// The kitchensink runtime provides the PolkaVM (PVM) based node implentation.
|
/// The kitchensink runtime provides the PolkaVM (PVM) based node implementation.
|
||||||
Kitchensink,
|
Kitchensink,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for TestingPlatform {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Geth => f.write_str("geth"),
|
|
||||||
Self::Kitchensink => f.write_str("revive"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,16 +28,14 @@ cacache = { workspace = true }
|
|||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-appender = { workspace = true }
|
tracing-appender = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
|
schemars = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
temp-dir = { workspace = true }
|
|
||||||
tempfile = { workspace = true }
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
+127
-137
@@ -2,30 +2,37 @@
|
|||||||
//! be reused between runs.
|
//! be reused between runs.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use revive_dt_common::iterators::FilesWithExtensionIterator;
|
use revive_dt_common::{iterators::FilesWithExtensionIterator, types::CompilerIdentifier};
|
||||||
use revive_dt_compiler::{Compiler, CompilerInput, CompilerOutput, Mode, SolidityCompiler};
|
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
|
||||||
use revive_dt_config::Arguments;
|
use revive_dt_core::Platform;
|
||||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
|
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
|
||||||
|
|
||||||
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
|
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
|
||||||
use anyhow::{Context as _, Error, Result};
|
use anyhow::{Context as _, Error, Result};
|
||||||
use once_cell::sync::Lazy;
|
use revive_dt_report::ExecutionSpecificReporter;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tracing::{Instrument, debug, debug_span, instrument};
|
use tracing::{Instrument, debug, debug_span, instrument};
|
||||||
|
|
||||||
use crate::Platform;
|
pub struct CachedCompiler<'a> {
|
||||||
|
/// The cache that stores the compiled contracts.
|
||||||
|
artifacts_cache: ArtifactsCache,
|
||||||
|
|
||||||
pub struct CachedCompiler(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 CachedCompiler {
|
impl<'a> CachedCompiler<'a> {
|
||||||
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
|
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
|
||||||
let mut cache = ArtifactsCache::new(path);
|
let mut cache = ArtifactsCache::new(path);
|
||||||
if invalidate_cache {
|
if invalidate_cache {
|
||||||
@@ -34,7 +41,10 @@ impl CachedCompiler {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to invalidate compilation cache directory")?;
|
.context("Failed to invalidate compilation cache directory")?;
|
||||||
}
|
}
|
||||||
Ok(Self(cache))
|
Ok(Self {
|
||||||
|
artifacts_cache: cache,
|
||||||
|
cache_key_lock: Default::default(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compiles or gets the compilation artifacts from the cache.
|
/// Compiles or gets the compilation artifacts from the cache.
|
||||||
@@ -43,91 +53,49 @@ impl CachedCompiler {
|
|||||||
level = "debug",
|
level = "debug",
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
metadata_file_path = %metadata_file_path.as_ref().display(),
|
metadata_file_path = %metadata_file_path.display(),
|
||||||
%mode,
|
%mode,
|
||||||
platform = P::config_id().to_string()
|
platform = %platform.platform_identifier()
|
||||||
),
|
),
|
||||||
err
|
err
|
||||||
)]
|
)]
|
||||||
pub async fn compile_contracts<P: Platform>(
|
pub async fn compile_contracts(
|
||||||
&self,
|
&self,
|
||||||
metadata: &Metadata,
|
metadata: &'a Metadata,
|
||||||
metadata_file_path: impl AsRef<Path>,
|
metadata_file_path: &'a Path,
|
||||||
mode: &Mode,
|
mode: Cow<'a, Mode>,
|
||||||
config: &Arguments,
|
|
||||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||||
compilation_success_report_callback: impl Fn(
|
compiler: &dyn SolidityCompiler,
|
||||||
Version,
|
platform: &dyn Platform,
|
||||||
PathBuf,
|
reporter: &ExecutionSpecificReporter,
|
||||||
bool,
|
) -> Result<CompilerOutput> {
|
||||||
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, format!("{err:#}"))
|
|
||||||
})
|
|
||||||
.context("Failed to obtain compiler executable path")?;
|
|
||||||
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,
|
|
||||||
format!("{err:#}"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.context("Failed to query compiler version")?;
|
|
||||||
|
|
||||||
let cache_key = CacheKey {
|
let cache_key = CacheKey {
|
||||||
platform_key: P::config_id().to_string(),
|
compiler_identifier: platform.compiler_identifier(),
|
||||||
compiler_version: compiler_version.clone(),
|
compiler_version: compiler.version().clone(),
|
||||||
metadata_file_path: metadata_file_path.as_ref().to_path_buf(),
|
metadata_file_path,
|
||||||
solc_mode: mode.clone(),
|
solc_mode: mode.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let compilation_callback = || {
|
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 {
|
async move {
|
||||||
compile_contracts::<P>(
|
compile_contracts(
|
||||||
metadata
|
metadata
|
||||||
.directory()
|
.directory()
|
||||||
.context("Failed to get metadata directory while preparing compilation")?,
|
.context("Failed to get metadata directory while preparing compilation")?,
|
||||||
compiler_path,
|
|
||||||
compiler_version,
|
|
||||||
metadata
|
metadata
|
||||||
.files_to_compile()
|
.files_to_compile()
|
||||||
.context("Failed to enumerate files to compile from metadata")?,
|
.context("Failed to enumerate files to compile from metadata")?,
|
||||||
mode,
|
&mode,
|
||||||
deployed_libraries,
|
deployed_libraries,
|
||||||
compilation_success_report_callback,
|
compiler,
|
||||||
compilation_failure_report_callback,
|
reporter,
|
||||||
)
|
)
|
||||||
.map(|compilation_result| compilation_result.map(CacheValue::new))
|
.map(|compilation_result| compilation_result.map(CacheValue::new))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
.instrument(debug_span!(
|
.instrument(debug_span!(
|
||||||
"Running compilation for the cache key",
|
"Running compilation for the cache key",
|
||||||
cache_key.platform_key = %cache_key.platform_key,
|
cache_key.compiler_identifier = %cache_key.compiler_identifier,
|
||||||
cache_key.compiler_version = %cache_key.compiler_version,
|
cache_key.compiler_version = %cache_key.compiler_version,
|
||||||
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
|
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
|
||||||
cache_key.solc_mode = %cache_key.solc_mode,
|
cache_key.solc_mode = %cache_key.solc_mode,
|
||||||
@@ -153,12 +121,15 @@ impl CachedCompiler {
|
|||||||
// Lock this specific cache key such that we do not get inconsistent state. We want
|
// 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
|
// 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.
|
// 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 read_guard = self.cache_key_lock.read().await;
|
||||||
let mutex = match read_guard.get(&cache_key).cloned() {
|
let mutex = match read_guard.get(&cache_key).cloned() {
|
||||||
Some(value) => value,
|
Some(value) => {
|
||||||
|
drop(read_guard);
|
||||||
|
value
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
drop(read_guard);
|
drop(read_guard);
|
||||||
CACHE_KEY_LOCK
|
self.cache_key_lock
|
||||||
.write()
|
.write()
|
||||||
.await
|
.await
|
||||||
.entry(cache_key.clone())
|
.entry(cache_key.clone())
|
||||||
@@ -168,15 +139,29 @@ impl CachedCompiler {
|
|||||||
};
|
};
|
||||||
let _guard = mutex.lock().await;
|
let _guard = mutex.lock().await;
|
||||||
|
|
||||||
match self.0.get(&cache_key).await {
|
match self.artifacts_cache.get(&cache_key).await {
|
||||||
Some(cache_value) => {
|
Some(cache_value) => {
|
||||||
compilation_success_report_callback(
|
if deployed_libraries.is_some() {
|
||||||
compiler_version.clone(),
|
reporter
|
||||||
compiler_path,
|
.report_post_link_contracts_compilation_succeeded_event(
|
||||||
true,
|
compiler.version().clone(),
|
||||||
None,
|
compiler.path(),
|
||||||
cache_value.compiler_output.clone(),
|
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
|
cache_value.compiler_output
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -189,38 +174,24 @@ impl CachedCompiler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((compiled_contracts, compiler_version))
|
Ok(compiled_contracts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
async fn compile_contracts(
|
||||||
async fn compile_contracts<P: Platform>(
|
|
||||||
metadata_directory: impl AsRef<Path>,
|
metadata_directory: impl AsRef<Path>,
|
||||||
compiler_path: impl AsRef<Path>,
|
|
||||||
compiler_version: Version,
|
|
||||||
mut files_to_compile: impl Iterator<Item = PathBuf>,
|
mut files_to_compile: impl Iterator<Item = PathBuf>,
|
||||||
mode: &Mode,
|
mode: &Mode,
|
||||||
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
|
||||||
compilation_success_report_callback: impl Fn(
|
compiler: &dyn SolidityCompiler,
|
||||||
Version,
|
reporter: &ExecutionSpecificReporter,
|
||||||
PathBuf,
|
|
||||||
bool,
|
|
||||||
Option<CompilerInput>,
|
|
||||||
CompilerOutput,
|
|
||||||
),
|
|
||||||
compilation_failure_report_callback: impl Fn(
|
|
||||||
Option<Version>,
|
|
||||||
Option<PathBuf>,
|
|
||||||
Option<CompilerInput>,
|
|
||||||
String,
|
|
||||||
),
|
|
||||||
) -> Result<CompilerOutput> {
|
) -> Result<CompilerOutput> {
|
||||||
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
|
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
|
||||||
.with_allowed_extension("sol")
|
.with_allowed_extension("sol")
|
||||||
.with_use_cached_fs(true)
|
.with_use_cached_fs(true)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let compiler = Compiler::<P::Compiler>::new()
|
let compilation = Compiler::new()
|
||||||
.with_allow_path(metadata_directory)
|
.with_allow_path(metadata_directory)
|
||||||
// Handling the modes
|
// Handling the modes
|
||||||
.with_optimization(mode.optimize_setting)
|
.with_optimization(mode.optimize_setting)
|
||||||
@@ -228,14 +199,6 @@ async fn compile_contracts<P: Platform>(
|
|||||||
// Adding the contract sources to the compiler.
|
// Adding the contract sources to the compiler.
|
||||||
.try_then(|compiler| {
|
.try_then(|compiler| {
|
||||||
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
|
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,
|
|
||||||
format!("{err:#}"),
|
|
||||||
)
|
|
||||||
})?
|
})?
|
||||||
// Adding the deployed libraries to the compiler.
|
// Adding the deployed libraries to the compiler.
|
||||||
.then(|compiler| {
|
.then(|compiler| {
|
||||||
@@ -253,27 +216,55 @@ async fn compile_contracts<P: Platform>(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let compiler_input = compiler.input();
|
let input = compilation.input().clone();
|
||||||
let compiler_output = compiler
|
let output = compilation.try_build(compiler).await;
|
||||||
.try_build(compiler_path.as_ref())
|
|
||||||
.await
|
match (output.as_ref(), deployed_libraries.is_some()) {
|
||||||
.inspect_err(|err| {
|
(Ok(output), true) => {
|
||||||
compilation_failure_report_callback(
|
reporter
|
||||||
Some(compiler_version.clone()),
|
.report_post_link_contracts_compilation_succeeded_event(
|
||||||
Some(compiler_path.as_ref().to_path_buf()),
|
compiler.version().clone(),
|
||||||
Some(compiler_input.clone()),
|
compiler.path(),
|
||||||
format!("{err:#}"),
|
false,
|
||||||
)
|
input,
|
||||||
})
|
output.clone(),
|
||||||
.context("Failed to configure compiler with sources and options")?;
|
)
|
||||||
compilation_success_report_callback(
|
.expect("Can't happen");
|
||||||
compiler_version,
|
}
|
||||||
compiler_path.as_ref().to_path_buf(),
|
(Ok(output), false) => {
|
||||||
false,
|
reporter
|
||||||
Some(compiler_input),
|
.report_pre_link_contracts_compilation_succeeded_event(
|
||||||
compiler_output.clone(),
|
compiler.version().clone(),
|
||||||
);
|
compiler.path(),
|
||||||
Ok(compiler_output)
|
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 {
|
struct ArtifactsCache {
|
||||||
@@ -297,7 +288,7 @@ impl ArtifactsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn insert(&self, key: &CacheKey, value: &CacheValue) -> Result<()> {
|
pub async fn insert(&self, key: &CacheKey<'_>, value: &CacheValue) -> Result<()> {
|
||||||
let key = bson::to_vec(key).context("Failed to serialize cache key (bson)")?;
|
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)")?;
|
let value = bson::to_vec(value).context("Failed to serialize cache value (bson)")?;
|
||||||
cacache::write(self.path.as_path(), key.encode_hex(), value)
|
cacache::write(self.path.as_path(), key.encode_hex(), value)
|
||||||
@@ -308,7 +299,7 @@ impl ArtifactsCache {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&self, key: &CacheKey) -> Option<CacheValue> {
|
pub async fn get(&self, key: &CacheKey<'_>) -> Option<CacheValue> {
|
||||||
let key = bson::to_vec(key).ok()?;
|
let key = bson::to_vec(key).ok()?;
|
||||||
let value = cacache::read(self.path.as_path(), key.encode_hex())
|
let value = cacache::read(self.path.as_path(), key.encode_hex())
|
||||||
.await
|
.await
|
||||||
@@ -320,7 +311,7 @@ impl ArtifactsCache {
|
|||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn get_or_insert_with(
|
pub async fn get_or_insert_with(
|
||||||
&self,
|
&self,
|
||||||
key: &CacheKey,
|
key: &CacheKey<'_>,
|
||||||
callback: impl AsyncFnOnce() -> Result<CacheValue>,
|
callback: impl AsyncFnOnce() -> Result<CacheValue>,
|
||||||
) -> Result<CacheValue> {
|
) -> Result<CacheValue> {
|
||||||
match self.get(key).await {
|
match self.get(key).await {
|
||||||
@@ -338,20 +329,19 @@ impl ArtifactsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||||
struct CacheKey {
|
struct CacheKey<'a> {
|
||||||
/// The platform name that this artifact was compiled for. For example, this could be EVM or
|
/// The identifier of the used compiler.
|
||||||
/// PVM.
|
compiler_identifier: CompilerIdentifier,
|
||||||
platform_key: String,
|
|
||||||
|
|
||||||
/// The version of the compiler that was used to compile the artifacts.
|
/// The version of the compiler that was used to compile the artifacts.
|
||||||
compiler_version: Version,
|
compiler_version: Version,
|
||||||
|
|
||||||
/// The path of the metadata file that the compilation artifacts are for.
|
/// The path of the metadata file that the compilation artifacts are for.
|
||||||
metadata_file_path: PathBuf,
|
metadata_file_path: &'a Path,
|
||||||
|
|
||||||
/// The mode that the compilation artifacts where compiled with.
|
/// The mode that the compilation artifacts where compiled with.
|
||||||
solc_mode: Mode,
|
solc_mode: Cow<'a, Mode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
|||||||
+203
-162
@@ -1,14 +1,14 @@
|
|||||||
//! The test driver handles the compilation and execution of the test cases.
|
//! The test driver handles the compilation and execution of the test cases.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use alloy::consensus::EMPTY_ROOT_HASH;
|
use alloy::consensus::EMPTY_ROOT_HASH;
|
||||||
use alloy::hex;
|
use alloy::hex;
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
use alloy::network::{Ethereum, TransactionBuilder};
|
use alloy::network::{Ethereum, TransactionBuilder};
|
||||||
use alloy::primitives::U256;
|
use alloy::primitives::{TxHash, U256};
|
||||||
use alloy::rpc::types::TransactionReceipt;
|
use alloy::rpc::types::TransactionReceipt;
|
||||||
use alloy::rpc::types::trace::geth::{
|
use alloy::rpc::types::trace::geth::{
|
||||||
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
|
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
|
||||||
@@ -18,27 +18,28 @@ use alloy::{
|
|||||||
primitives::Address,
|
primitives::Address,
|
||||||
rpc::types::{TransactionRequest, trace::geth::DiffMode},
|
rpc::types::{TransactionRequest, trace::geth::DiffMode},
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::{Context as _, bail};
|
||||||
use futures::TryStreamExt;
|
use futures::{TryStreamExt, future::try_join_all};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator};
|
||||||
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
|
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
|
||||||
use revive_dt_report::ExecutionSpecificReporter;
|
use revive_dt_report::ExecutionSpecificReporter;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
|
||||||
use revive_dt_format::case::Case;
|
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::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
|
||||||
use revive_dt_format::{input::Step, metadata::Metadata};
|
use revive_dt_format::steps::{
|
||||||
|
BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method,
|
||||||
|
StepIdx, StorageEmptyAssertionStep,
|
||||||
|
};
|
||||||
|
use revive_dt_format::{metadata::Metadata, steps::Step};
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
use tracing::{Instrument, info, info_span, instrument};
|
use tracing::{Instrument, info, info_span, instrument};
|
||||||
|
|
||||||
use crate::Platform;
|
#[derive(Clone)]
|
||||||
|
pub struct CaseState {
|
||||||
pub struct CaseState<T: Platform> {
|
|
||||||
/// A map of all of the compiled contracts for the given metadata file.
|
/// A map of all of the compiled contracts for the given metadata file.
|
||||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||||
|
|
||||||
@@ -55,18 +56,18 @@ pub struct CaseState<T: Platform> {
|
|||||||
/// The execution reporter.
|
/// The execution reporter.
|
||||||
execution_reporter: ExecutionSpecificReporter,
|
execution_reporter: ExecutionSpecificReporter,
|
||||||
|
|
||||||
phantom: PhantomData<T>,
|
/// The private key allocator used for this case state. This is an Arc Mutex to allow for the
|
||||||
|
/// state to be cloned and for all of the clones to refer to the same allocator.
|
||||||
|
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> CaseState<T>
|
impl CaseState {
|
||||||
where
|
|
||||||
T: Platform,
|
|
||||||
{
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
compiler_version: Version,
|
compiler_version: Version,
|
||||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||||
execution_reporter: ExecutionSpecificReporter,
|
execution_reporter: ExecutionSpecificReporter,
|
||||||
|
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
compiled_contracts,
|
compiled_contracts,
|
||||||
@@ -74,7 +75,7 @@ where
|
|||||||
variables: Default::default(),
|
variables: Default::default(),
|
||||||
compiler_version,
|
compiler_version,
|
||||||
execution_reporter,
|
execution_reporter,
|
||||||
phantom: PhantomData,
|
private_key_allocator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
step: &Step,
|
step: &Step,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<StepOutput> {
|
) -> anyhow::Result<StepOutput> {
|
||||||
match step {
|
match step {
|
||||||
Step::FunctionCall(input) => {
|
Step::FunctionCall(input) => {
|
||||||
@@ -104,6 +105,23 @@ where
|
|||||||
.context("Failed to handle storage empty assertion step")?;
|
.context("Failed to handle storage empty assertion step")?;
|
||||||
Ok(StepOutput::StorageEmptyAssertion)
|
Ok(StepOutput::StorageEmptyAssertion)
|
||||||
}
|
}
|
||||||
|
Step::Repeat(repetition_step) => {
|
||||||
|
self.handle_repeat(
|
||||||
|
metadata,
|
||||||
|
repetition_step.repeat,
|
||||||
|
&repetition_step.steps,
|
||||||
|
node,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to handle the repetition step")?;
|
||||||
|
Ok(StepOutput::Repetition)
|
||||||
|
}
|
||||||
|
Step::AllocateAccount(account_allocation) => {
|
||||||
|
self.handle_account_allocation(account_allocation.variable_name.as_str())
|
||||||
|
.await
|
||||||
|
.context("Failed to allocate account")?;
|
||||||
|
Ok(StepOutput::AccountAllocation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.inspect(|_| info!("Step Succeeded"))
|
.inspect(|_| info!("Step Succeeded"))
|
||||||
}
|
}
|
||||||
@@ -112,9 +130,11 @@ where
|
|||||||
pub async fn handle_input(
|
pub async fn handle_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
input: &Input,
|
input: &FunctionCallStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
|
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
|
||||||
|
let resolver = node.resolver().await?;
|
||||||
|
|
||||||
let deployment_receipts = self
|
let deployment_receipts = self
|
||||||
.handle_input_contract_deployment(metadata, input, node)
|
.handle_input_contract_deployment(metadata, input, node)
|
||||||
.await
|
.await
|
||||||
@@ -124,14 +144,19 @@ where
|
|||||||
.await
|
.await
|
||||||
.context("Failed during transaction execution phase of input handling")?;
|
.context("Failed during transaction execution phase of input handling")?;
|
||||||
let tracing_result = self
|
let tracing_result = self
|
||||||
.handle_input_call_frame_tracing(&execution_receipt, node)
|
.handle_input_call_frame_tracing(execution_receipt.transaction_hash, node)
|
||||||
.await
|
.await
|
||||||
.context("Failed during callframe tracing phase of input handling")?;
|
.context("Failed during callframe tracing phase of input handling")?;
|
||||||
self.handle_input_variable_assignment(input, &tracing_result)
|
self.handle_input_variable_assignment(input, &tracing_result)
|
||||||
.context("Failed to assign variables from callframe output")?;
|
.context("Failed to assign variables from callframe output")?;
|
||||||
let (_, (geth_trace, diff_mode)) = try_join!(
|
let (_, (geth_trace, diff_mode)) = try_join!(
|
||||||
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
|
self.handle_input_expectations(
|
||||||
self.handle_input_diff(&execution_receipt, node)
|
input,
|
||||||
|
&execution_receipt,
|
||||||
|
resolver.as_ref(),
|
||||||
|
&tracing_result
|
||||||
|
),
|
||||||
|
self.handle_input_diff(execution_receipt.transaction_hash, node)
|
||||||
)
|
)
|
||||||
.context("Failed while evaluating expectations and diffs in parallel")?;
|
.context("Failed while evaluating expectations and diffs in parallel")?;
|
||||||
Ok((execution_receipt, geth_trace, diff_mode))
|
Ok((execution_receipt, geth_trace, diff_mode))
|
||||||
@@ -141,8 +166,8 @@ where
|
|||||||
pub async fn handle_balance_assertion(
|
pub async fn handle_balance_assertion(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
balance_assertion: &BalanceAssertion,
|
balance_assertion: &BalanceAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
|
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
|
||||||
.await
|
.await
|
||||||
@@ -157,8 +182,8 @@ where
|
|||||||
pub async fn handle_storage_empty(
|
pub async fn handle_storage_empty(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
storage_empty: &StorageEmptyAssertion,
|
storage_empty: &StorageEmptyAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
|
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
|
||||||
.await
|
.await
|
||||||
@@ -169,13 +194,49 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", name = "Handling Repetition", skip_all)]
|
||||||
|
pub async fn handle_repeat(
|
||||||
|
&mut self,
|
||||||
|
metadata: &Metadata,
|
||||||
|
repetitions: usize,
|
||||||
|
steps: &[Step],
|
||||||
|
node: &dyn EthereumNode,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let tasks = (0..repetitions).map(|_| {
|
||||||
|
let mut state = self.clone();
|
||||||
|
async move {
|
||||||
|
for step in steps {
|
||||||
|
state.handle_step(metadata, step, node).await?;
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try_join_all(tasks).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", name = "Handling Account Allocation", skip_all)]
|
||||||
|
pub async fn handle_account_allocation(&mut self, variable_name: &str) -> anyhow::Result<()> {
|
||||||
|
let Some(variable_name) = variable_name.strip_prefix("$VARIABLE:") else {
|
||||||
|
bail!("Account allocation must start with $VARIABLE:");
|
||||||
|
};
|
||||||
|
|
||||||
|
let private_key = self.private_key_allocator.lock().await.allocate()?;
|
||||||
|
let account = private_key.address();
|
||||||
|
let variable = U256::from_be_slice(account.0.as_slice());
|
||||||
|
|
||||||
|
self.variables.insert(variable_name.to_string(), variable);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles the contract deployment for a given input performing it if it needs to be performed.
|
/// Handles the contract deployment for a given input performing it if it needs to be performed.
|
||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
async fn handle_input_contract_deployment(
|
async fn handle_input_contract_deployment(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
input: &Input,
|
input: &FunctionCallStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
|
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
|
||||||
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
|
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
|
||||||
for instance in input.find_all_contract_instances().into_iter() {
|
for instance in input.find_all_contract_instances().into_iter() {
|
||||||
@@ -195,15 +256,16 @@ where
|
|||||||
.then_some(input.value)
|
.then_some(input.value)
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
|
let caller = {
|
||||||
|
let context = self.default_resolution_context();
|
||||||
|
let resolver = node.resolver().await?;
|
||||||
|
input
|
||||||
|
.caller
|
||||||
|
.resolve_address(resolver.as_ref(), context)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
if let (_, _, Some(receipt)) = self
|
if let (_, _, Some(receipt)) = self
|
||||||
.get_or_deploy_contract_instance(
|
.get_or_deploy_contract_instance(&instance, metadata, caller, calldata, value, node)
|
||||||
&instance,
|
|
||||||
metadata,
|
|
||||||
input.caller,
|
|
||||||
calldata,
|
|
||||||
value,
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to get or deploy contract instance during input execution")?
|
.context("Failed to get or deploy contract instance during input execution")?
|
||||||
{
|
{
|
||||||
@@ -218,9 +280,9 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
async fn handle_input_execution(
|
async fn handle_input_execution(
|
||||||
&mut self,
|
&mut self,
|
||||||
input: &Input,
|
input: &FunctionCallStep,
|
||||||
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
|
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<TransactionReceipt> {
|
) -> anyhow::Result<TransactionReceipt> {
|
||||||
match input.method {
|
match input.method {
|
||||||
// This input was already executed when `handle_input` was called. We just need to
|
// This input was already executed when `handle_input` was called. We just need to
|
||||||
@@ -229,8 +291,9 @@ where
|
|||||||
.remove(&input.instance)
|
.remove(&input.instance)
|
||||||
.context("Failed to find deployment receipt for constructor call"),
|
.context("Failed to find deployment receipt for constructor call"),
|
||||||
Method::Fallback | Method::FunctionName(_) => {
|
Method::Fallback | Method::FunctionName(_) => {
|
||||||
|
let resolver = node.resolver().await?;
|
||||||
let tx = match input
|
let tx = match input
|
||||||
.legacy_transaction(node, self.default_resolution_context())
|
.legacy_transaction(resolver.as_ref(), self.default_resolution_context())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(tx) => tx,
|
Ok(tx) => tx,
|
||||||
@@ -250,11 +313,11 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
async fn handle_input_call_frame_tracing(
|
async fn handle_input_call_frame_tracing(
|
||||||
&self,
|
&self,
|
||||||
execution_receipt: &TransactionReceipt,
|
tx_hash: TxHash,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<CallFrame> {
|
) -> anyhow::Result<CallFrame> {
|
||||||
node.trace_transaction(
|
node.trace_transaction(
|
||||||
execution_receipt,
|
tx_hash,
|
||||||
GethDebugTracingOptions {
|
GethDebugTracingOptions {
|
||||||
tracer: Some(GethDebugTracerType::BuiltInTracer(
|
tracer: Some(GethDebugTracerType::BuiltInTracer(
|
||||||
GethDebugBuiltInTracerType::CallTracer,
|
GethDebugBuiltInTracerType::CallTracer,
|
||||||
@@ -281,7 +344,7 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
fn handle_input_variable_assignment(
|
fn handle_input_variable_assignment(
|
||||||
&mut self,
|
&mut self,
|
||||||
input: &Input,
|
input: &FunctionCallStep,
|
||||||
tracing_result: &CallFrame,
|
tracing_result: &CallFrame,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(ref assignments) = input.variable_assignments else {
|
let Some(ref assignments) = input.variable_assignments else {
|
||||||
@@ -312,26 +375,26 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
async fn handle_input_expectations(
|
async fn handle_input_expectations(
|
||||||
&self,
|
&self,
|
||||||
input: &Input,
|
input: &FunctionCallStep,
|
||||||
execution_receipt: &TransactionReceipt,
|
execution_receipt: &TransactionReceipt,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
tracing_result: &CallFrame,
|
tracing_result: &CallFrame,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Resolving the `input.expected` into a series of expectations that we can then assert on.
|
// Resolving the `input.expected` into a series of expectations that we can then assert on.
|
||||||
let mut expectations = match input {
|
let mut expectations = match input {
|
||||||
Input {
|
FunctionCallStep {
|
||||||
expected: Some(Expected::Calldata(calldata)),
|
expected: Some(Expected::Calldata(calldata)),
|
||||||
..
|
..
|
||||||
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
|
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
|
||||||
Input {
|
FunctionCallStep {
|
||||||
expected: Some(Expected::Expected(expected)),
|
expected: Some(Expected::Expected(expected)),
|
||||||
..
|
..
|
||||||
} => vec![expected.clone()],
|
} => vec![expected.clone()],
|
||||||
Input {
|
FunctionCallStep {
|
||||||
expected: Some(Expected::ExpectedMany(expected)),
|
expected: Some(Expected::ExpectedMany(expected)),
|
||||||
..
|
..
|
||||||
} => expected.clone(),
|
} => expected.clone(),
|
||||||
Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
|
FunctionCallStep { 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
|
// This is a bit of a special case and we have to support it separately on it's own. If it's
|
||||||
@@ -362,7 +425,7 @@ where
|
|||||||
async fn handle_input_expectation_item(
|
async fn handle_input_expectation_item(
|
||||||
&self,
|
&self,
|
||||||
execution_receipt: &TransactionReceipt,
|
execution_receipt: &TransactionReceipt,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
expectation: ExpectedOutput,
|
expectation: ExpectedOutput,
|
||||||
tracing_result: &CallFrame,
|
tracing_result: &CallFrame,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -432,13 +495,9 @@ where
|
|||||||
{
|
{
|
||||||
// Handling the emitter assertion.
|
// Handling the emitter assertion.
|
||||||
if let Some(ref expected_address) = expected_event.address {
|
if let Some(ref expected_address) = expected_event.address {
|
||||||
let expected = Address::from_slice(
|
let expected = expected_address
|
||||||
Calldata::new_compound([expected_address])
|
.resolve_address(resolver, resolution_context)
|
||||||
.calldata(resolver, resolution_context)
|
.await?;
|
||||||
.await?
|
|
||||||
.get(12..32)
|
|
||||||
.expect("Can't fail"),
|
|
||||||
);
|
|
||||||
let actual = actual_event.address();
|
let actual = actual_event.address();
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
@@ -507,8 +566,8 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
async fn handle_input_diff(
|
async fn handle_input_diff(
|
||||||
&self,
|
&self,
|
||||||
execution_receipt: &TransactionReceipt,
|
tx_hash: TxHash,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<(GethTrace, DiffMode)> {
|
) -> anyhow::Result<(GethTrace, DiffMode)> {
|
||||||
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
|
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
|
||||||
diff_mode: Some(true),
|
diff_mode: Some(true),
|
||||||
@@ -517,11 +576,11 @@ where
|
|||||||
});
|
});
|
||||||
|
|
||||||
let trace = node
|
let trace = node
|
||||||
.trace_transaction(execution_receipt, trace_options)
|
.trace_transaction(tx_hash, trace_options)
|
||||||
.await
|
.await
|
||||||
.context("Failed to obtain geth prestate tracer output")?;
|
.context("Failed to obtain geth prestate tracer output")?;
|
||||||
let diff = node
|
let diff = node
|
||||||
.state_diff(execution_receipt)
|
.state_diff(tx_hash)
|
||||||
.await
|
.await
|
||||||
.context("Failed to obtain state diff for transaction")?;
|
.context("Failed to obtain state diff for transaction")?;
|
||||||
|
|
||||||
@@ -532,20 +591,20 @@ where
|
|||||||
pub async fn handle_balance_assertion_contract_deployment(
|
pub async fn handle_balance_assertion_contract_deployment(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
balance_assertion: &BalanceAssertion,
|
balance_assertion: &BalanceAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(instance) = balance_assertion
|
let Some(address) = balance_assertion.address.as_resolvable_address() else {
|
||||||
.address
|
|
||||||
.strip_suffix(".address")
|
|
||||||
.map(ContractInstance::new)
|
|
||||||
else {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
self.get_or_deploy_contract_instance(
|
self.get_or_deploy_contract_instance(
|
||||||
&instance,
|
&instance,
|
||||||
metadata,
|
metadata,
|
||||||
Input::default_caller(),
|
FunctionCallStep::default_caller_address(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
node,
|
node,
|
||||||
@@ -557,20 +616,17 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
pub async fn handle_balance_assertion_execution(
|
pub async fn handle_balance_assertion_execution(
|
||||||
&mut self,
|
&mut self,
|
||||||
BalanceAssertion {
|
BalanceAssertionStep {
|
||||||
address: address_string,
|
address,
|
||||||
expected_balance: amount,
|
expected_balance: amount,
|
||||||
..
|
..
|
||||||
}: &BalanceAssertion,
|
}: &BalanceAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let address = Address::from_slice(
|
let resolver = node.resolver().await?;
|
||||||
Calldata::new_compound([address_string])
|
let address = address
|
||||||
.calldata(node, self.default_resolution_context())
|
.resolve_address(resolver.as_ref(), self.default_resolution_context())
|
||||||
.await?
|
.await?;
|
||||||
.get(12..32)
|
|
||||||
.expect("Can't fail"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let balance = node.balance_of(address).await?;
|
let balance = node.balance_of(address).await?;
|
||||||
|
|
||||||
@@ -582,7 +638,7 @@ where
|
|||||||
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
|
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
|
||||||
expected,
|
expected,
|
||||||
actual,
|
actual,
|
||||||
address_string,
|
address,
|
||||||
address,
|
address,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -594,20 +650,20 @@ where
|
|||||||
pub async fn handle_storage_empty_assertion_contract_deployment(
|
pub async fn handle_storage_empty_assertion_contract_deployment(
|
||||||
&mut self,
|
&mut self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
storage_empty_assertion: &StorageEmptyAssertion,
|
storage_empty_assertion: &StorageEmptyAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(instance) = storage_empty_assertion
|
let Some(address) = storage_empty_assertion.address.as_resolvable_address() else {
|
||||||
.address
|
|
||||||
.strip_suffix(".address")
|
|
||||||
.map(ContractInstance::new)
|
|
||||||
else {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
self.get_or_deploy_contract_instance(
|
self.get_or_deploy_contract_instance(
|
||||||
&instance,
|
&instance,
|
||||||
metadata,
|
metadata,
|
||||||
Input::default_caller(),
|
FunctionCallStep::default_caller_address(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
node,
|
node,
|
||||||
@@ -619,20 +675,17 @@ where
|
|||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
pub async fn handle_storage_empty_assertion_execution(
|
pub async fn handle_storage_empty_assertion_execution(
|
||||||
&mut self,
|
&mut self,
|
||||||
StorageEmptyAssertion {
|
StorageEmptyAssertionStep {
|
||||||
address: address_string,
|
address,
|
||||||
is_storage_empty,
|
is_storage_empty,
|
||||||
..
|
..
|
||||||
}: &StorageEmptyAssertion,
|
}: &StorageEmptyAssertionStep,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let address = Address::from_slice(
|
let resolver = node.resolver().await?;
|
||||||
Calldata::new_compound([address_string])
|
let address = address
|
||||||
.calldata(node, self.default_resolution_context())
|
.resolve_address(resolver.as_ref(), self.default_resolution_context())
|
||||||
.await?
|
.await?;
|
||||||
.get(12..32)
|
|
||||||
.expect("Can't fail"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let storage = node.latest_state_proof(address, Default::default()).await?;
|
let storage = node.latest_state_proof(address, Default::default()).await?;
|
||||||
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
|
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
|
||||||
@@ -646,7 +699,7 @@ where
|
|||||||
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
|
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
|
||||||
expected,
|
expected,
|
||||||
actual,
|
actual,
|
||||||
address_string,
|
address,
|
||||||
address,
|
address,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -667,7 +720,7 @@ where
|
|||||||
deployer: Address,
|
deployer: Address,
|
||||||
calldata: Option<&Calldata>,
|
calldata: Option<&Calldata>,
|
||||||
value: Option<EtherValue>,
|
value: Option<EtherValue>,
|
||||||
node: &T::Blockchain,
|
node: &dyn EthereumNode,
|
||||||
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
||||||
if let Some((_, address, abi)) = self.deployed_contracts.get(contract_instance) {
|
if let Some((_, address, abi)) = self.deployed_contracts.get(contract_instance) {
|
||||||
return Ok((*address, abi.clone(), None));
|
return Ok((*address, abi.clone(), None));
|
||||||
@@ -710,8 +763,9 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(calldata) = calldata {
|
if let Some(calldata) = calldata {
|
||||||
|
let resolver = node.resolver().await?;
|
||||||
let calldata = calldata
|
let calldata = calldata
|
||||||
.calldata(node, self.default_resolution_context())
|
.calldata(resolver.as_ref(), self.default_resolution_context())
|
||||||
.await?;
|
.await?;
|
||||||
code.extend(calldata);
|
code.extend(calldata);
|
||||||
}
|
}
|
||||||
@@ -728,11 +782,7 @@ where
|
|||||||
let receipt = match node.execute_transaction(tx).await {
|
let receipt = match node.execute_transaction(tx).await {
|
||||||
Ok(receipt) => receipt,
|
Ok(receipt) => receipt,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(
|
tracing::error!(?error, "Contract deployment transaction failed.");
|
||||||
node = std::any::type_name::<T>(),
|
|
||||||
?error,
|
|
||||||
"Contract deployment transaction failed."
|
|
||||||
);
|
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -763,36 +813,23 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> {
|
pub struct CaseDriver<'a> {
|
||||||
metadata: &'a Metadata,
|
metadata: &'a Metadata,
|
||||||
case: &'a Case,
|
case: &'a Case,
|
||||||
leader_node: &'a Leader::Blockchain,
|
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
|
||||||
follower_node: &'a Follower::Blockchain,
|
|
||||||
leader_state: CaseState<Leader>,
|
|
||||||
follower_state: CaseState<Follower>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, L, F> CaseDriver<'a, L, F>
|
impl<'a> CaseDriver<'a> {
|
||||||
where
|
|
||||||
L: Platform,
|
|
||||||
F: Platform,
|
|
||||||
{
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
metadata: &'a Metadata,
|
metadata: &'a Metadata,
|
||||||
case: &'a Case,
|
case: &'a Case,
|
||||||
leader_node: &'a L::Blockchain,
|
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
|
||||||
follower_node: &'a F::Blockchain,
|
) -> CaseDriver<'a> {
|
||||||
leader_state: CaseState<L>,
|
|
||||||
follower_state: CaseState<F>,
|
|
||||||
) -> CaseDriver<'a, L, F> {
|
|
||||||
Self {
|
Self {
|
||||||
metadata,
|
metadata,
|
||||||
case,
|
case,
|
||||||
leader_node,
|
platform_state,
|
||||||
follower_node,
|
|
||||||
leader_state,
|
|
||||||
follower_state,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,42 +842,44 @@ where
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, v)| (StepIdx::new(idx), v))
|
.map(|(idx, v)| (StepIdx::new(idx), v))
|
||||||
{
|
{
|
||||||
let (leader_step_output, follower_step_output) = try_join!(
|
// Run this step concurrently across all platforms; short-circuit on first failure
|
||||||
self.leader_state
|
let metadata = self.metadata;
|
||||||
.handle_step(self.metadata, &step, self.leader_node)
|
let step_futs =
|
||||||
.instrument(info_span!(
|
self.platform_state
|
||||||
"Handling Step",
|
.iter_mut()
|
||||||
%step_idx,
|
.map(|(node, platform_id, case_state)| {
|
||||||
target = "Leader",
|
let platform_id = *platform_id;
|
||||||
)),
|
let node_ref = *node;
|
||||||
self.follower_state
|
let step_clone = step.clone();
|
||||||
.handle_step(self.metadata, &step, self.follower_node)
|
let span = info_span!(
|
||||||
.instrument(info_span!(
|
"Handling Step",
|
||||||
"Handling Step",
|
%step_idx,
|
||||||
%step_idx,
|
platform = %platform_id,
|
||||||
target = "Follower",
|
);
|
||||||
))
|
async move {
|
||||||
)?;
|
case_state
|
||||||
|
.handle_step(metadata, &step_clone, node_ref)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (platform_id, e))
|
||||||
|
}
|
||||||
|
.instrument(span)
|
||||||
|
});
|
||||||
|
|
||||||
match (leader_step_output, follower_step_output) {
|
match try_join_all(step_futs).await {
|
||||||
(StepOutput::FunctionCall(..), StepOutput::FunctionCall(..)) => {
|
Ok(_outputs) => {
|
||||||
// TODO: We need to actually work out how/if we will compare the diff between
|
// All platforms succeeded for this step
|
||||||
// the leader and the follower. The diffs are almost guaranteed to be different
|
steps_executed += 1;
|
||||||
// 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
|
Err((platform_id, error)) => {
|
||||||
// contract will produce two non-equal diffs because on the leader the contract
|
tracing::error!(
|
||||||
// has address X and on the follower it has address Y. On the leader contract X
|
%step_idx,
|
||||||
// contains address A in the state and on the follower it contains address B. So
|
platform = %platform_id,
|
||||||
// this isn't exactly a straightforward thing to do and I'm not even sure that
|
?error,
|
||||||
// it's possible to do. Once we have an actual strategy for doing the diffs we
|
"Step failed on platform",
|
||||||
// will implement it here. Until then, this remains empty.
|
);
|
||||||
|
return Err(error);
|
||||||
}
|
}
|
||||||
(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)
|
Ok(steps_executed)
|
||||||
@@ -853,4 +892,6 @@ pub enum StepOutput {
|
|||||||
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
|
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
|
||||||
BalanceAssertion,
|
BalanceAssertion,
|
||||||
StorageEmptyAssertion,
|
StorageEmptyAssertion,
|
||||||
|
Repetition,
|
||||||
|
AccountAllocation,
|
||||||
}
|
}
|
||||||
|
|||||||
+350
-25
@@ -3,45 +3,370 @@
|
|||||||
//! This crate defines the testing configuration and
|
//! This crate defines the testing configuration and
|
||||||
//! provides a helper utility to execute tests.
|
//! provides a helper utility to execute tests.
|
||||||
|
|
||||||
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
|
use std::{
|
||||||
use revive_dt_config::TestingPlatform;
|
pin::Pin,
|
||||||
use revive_dt_format::traits::ResolverApi;
|
thread::{self, JoinHandle},
|
||||||
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
|
};
|
||||||
|
|
||||||
|
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, geth::GethNode, substrate::SubstrateNode};
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub mod driver;
|
pub mod driver;
|
||||||
|
|
||||||
/// One platform can be tested differentially against another.
|
/// A trait that describes the interface for the platforms that are supported by the tool.
|
||||||
///
|
#[allow(clippy::type_complexity)]
|
||||||
/// For this we need a blockchain node implementation and a compiler.
|
|
||||||
pub trait Platform {
|
pub trait Platform {
|
||||||
type Blockchain: EthereumNode + Node + ResolverApi;
|
/// Returns the identifier of this platform. This is a combination of the node and the compiler
|
||||||
type Compiler: SolidityCompiler;
|
/// used.
|
||||||
|
fn platform_identifier(&self) -> PlatformIdentifier;
|
||||||
|
|
||||||
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].
|
/// Returns a full identifier for the platform.
|
||||||
fn config_id() -> TestingPlatform;
|
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>>>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||||
pub struct Geth;
|
pub struct GethEvmSolcPlatform;
|
||||||
|
|
||||||
impl Platform for Geth {
|
impl Platform for GethEvmSolcPlatform {
|
||||||
type Blockchain = geth::GethNode;
|
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||||
type Compiler = solc::Solc;
|
PlatformIdentifier::GethEvmSolc
|
||||||
|
}
|
||||||
|
|
||||||
fn config_id() -> TestingPlatform {
|
fn node_identifier(&self) -> NodeIdentifier {
|
||||||
TestingPlatform::Geth
|
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>)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||||
pub struct Kitchensink;
|
pub struct KitchensinkPolkavmResolcPlatform;
|
||||||
|
|
||||||
impl Platform for Kitchensink {
|
impl Platform for KitchensinkPolkavmResolcPlatform {
|
||||||
type Blockchain = KitchensinkNode;
|
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||||
type Compiler = revive_resolc::Resolc;
|
PlatformIdentifier::KitchensinkPolkavmResolc
|
||||||
|
}
|
||||||
|
|
||||||
fn config_id() -> TestingPlatform {
|
fn node_identifier(&self) -> NodeIdentifier {
|
||||||
TestingPlatform::Kitchensink
|
NodeIdentifier::Kitchensink
|
||||||
|
}
|
||||||
|
|
||||||
|
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 kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
|
||||||
|
.path
|
||||||
|
.clone();
|
||||||
|
let genesis = genesis_configuration.genesis()?.clone();
|
||||||
|
Ok(thread::spawn(move || {
|
||||||
|
let node = SubstrateNode::new(
|
||||||
|
kitchensink_path,
|
||||||
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
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>)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||||
|
pub struct KitchensinkRevmSolcPlatform;
|
||||||
|
|
||||||
|
impl Platform for KitchensinkRevmSolcPlatform {
|
||||||
|
fn platform_identifier(&self) -> PlatformIdentifier {
|
||||||
|
PlatformIdentifier::KitchensinkRevmSolc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_identifier(&self) -> NodeIdentifier {
|
||||||
|
NodeIdentifier::Kitchensink
|
||||||
|
}
|
||||||
|
|
||||||
|
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 kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
|
||||||
|
.path
|
||||||
|
.clone();
|
||||||
|
let genesis = genesis_configuration.genesis()?.clone();
|
||||||
|
Ok(thread::spawn(move || {
|
||||||
|
let node = SubstrateNode::new(
|
||||||
|
kitchensink_path,
|
||||||
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
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>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
||||||
|
.path
|
||||||
|
.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,
|
||||||
|
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>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
||||||
|
.path
|
||||||
|
.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,
|
||||||
|
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>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlatformIdentifier> for Box<dyn Platform> {
|
||||||
|
fn from(value: PlatformIdentifier) -> Self {
|
||||||
|
match value {
|
||||||
|
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
|
||||||
|
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||||
|
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>
|
||||||
|
}
|
||||||
|
PlatformIdentifier::KitchensinkRevmSolc => {
|
||||||
|
Box::new(KitchensinkRevmSolcPlatform) as Box<_>
|
||||||
|
}
|
||||||
|
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||||
|
Box::new(ReviveDevNodePolkavmResolcPlatform) as Box<_>
|
||||||
|
}
|
||||||
|
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||||
|
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlatformIdentifier> for &dyn Platform {
|
||||||
|
fn from(value: PlatformIdentifier) -> Self {
|
||||||
|
match value {
|
||||||
|
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
|
||||||
|
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||||
|
&KitchensinkPolkavmResolcPlatform as &dyn Platform
|
||||||
|
}
|
||||||
|
PlatformIdentifier::KitchensinkRevmSolc => {
|
||||||
|
&KitchensinkRevmSolcPlatform as &dyn Platform
|
||||||
|
}
|
||||||
|
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||||
|
&ReviveDevNodePolkavmResolcPlatform as &dyn Platform
|
||||||
|
}
|
||||||
|
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||||
|
&ReviveDevNodeRevmSolcPlatform 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)
|
||||||
|
}
|
||||||
|
|||||||
+519
-752
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
//! 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 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")?
|
||||||
|
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))
|
||||||
|
.context("Node failed to spawn")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ anyhow = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
schemars = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -1,32 +1,55 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{Expected, Step},
|
|
||||||
mode::ParsedMode,
|
mode::ParsedMode,
|
||||||
|
steps::{Expected, Step},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
|
||||||
pub struct Case {
|
pub struct Case {
|
||||||
|
/// An optional name of the test case.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub modes: Option<Vec<ParsedMode>>,
|
pub modes: Option<Vec<ParsedMode>>,
|
||||||
|
|
||||||
|
/// The set of steps to run as part of this test case.
|
||||||
#[serde(rename = "inputs")]
|
#[serde(rename = "inputs")]
|
||||||
pub steps: Vec<Step>,
|
pub steps: Vec<Step>,
|
||||||
|
|
||||||
|
/// An optional name of the group of tests that this test belongs to.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub group: Option<String>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub expected: Option<Expected>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ignore: Option<bool>,
|
pub ignore: Option<bool>,
|
||||||
}
|
}
|
||||||
@@ -64,7 +87,7 @@ impl Case {
|
|||||||
pub fn solc_modes(&self) -> Vec<Mode> {
|
pub fn solc_modes(&self) -> Vec<Mode> {
|
||||||
match &self.modes {
|
match &self.modes {
|
||||||
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
||||||
None => Mode::all().collect(),
|
None => Mode::all().cloned().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pub mod case;
|
pub mod case;
|
||||||
pub mod corpus;
|
pub mod corpus;
|
||||||
pub mod input;
|
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod mode;
|
pub mod mode;
|
||||||
|
pub mod steps;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use revive_dt_common::{
|
use revive_dt_common::{
|
||||||
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
|
cached_fs::read_to_string,
|
||||||
types::Mode,
|
iterators::FilesWithExtensionIterator,
|
||||||
|
macros::define_wrapper_type,
|
||||||
|
types::{Mode, VmIdentifier},
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -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 {
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ignore: Option<bool>,
|
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")]
|
#[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>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
|
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
|
||||||
|
|
||||||
|
/// The set of libraries that this metadata file requires.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub modes: Option<Vec<ParsedMode>>,
|
pub modes: Option<Vec<ParsedMode>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(skip)]
|
||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
|
|
||||||
/// This field specifies an EVM version requirement that the test case has where the test might
|
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub required_evm_version: Option<EvmVersionRequirement>,
|
pub required_evm_version: Option<EvmVersionRequirement>,
|
||||||
|
|
||||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
|
/// A set of compilation directives that will be passed to the compiler whenever the contracts
|
||||||
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
|
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]
|
||||||
/// just a filter for when a test can run whereas this is an instruction to the compiler.
|
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub compiler_directives: Option<CompilationDirectives>,
|
pub compiler_directives: Option<CompilationDirectives>,
|
||||||
}
|
}
|
||||||
@@ -99,7 +134,7 @@ impl Metadata {
|
|||||||
pub fn solc_modes(&self) -> Vec<Mode> {
|
pub fn solc_modes(&self) -> Vec<Mode> {
|
||||||
match &self.modes {
|
match &self.modes {
|
||||||
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
|
||||||
None => Mode::all().collect(),
|
None => Mode::all().cloned().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +297,7 @@ define_wrapper_type!(
|
|||||||
///
|
///
|
||||||
/// Typically, this is used as the key to the "contracts" field of metadata files.
|
/// Typically, this is used as the key to the "contracts" field of metadata files.
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||||
)]
|
)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ContractInstance(String) impl Display;
|
pub struct ContractInstance(String) impl Display;
|
||||||
@@ -273,7 +308,7 @@ define_wrapper_type!(
|
|||||||
///
|
///
|
||||||
/// A contract identifier is the name of the contract in the source code.
|
/// A contract identifier is the name of the contract in the source code.
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema
|
||||||
)]
|
)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ContractIdent(String) impl Display;
|
pub struct ContractIdent(String) impl Display;
|
||||||
@@ -286,7 +321,9 @@ define_wrapper_type!(
|
|||||||
/// ```text
|
/// ```text
|
||||||
/// ${path}:${contract_ident}
|
/// ${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")]
|
#[serde(try_from = "String", into = "String")]
|
||||||
pub struct ContractPathAndIdent {
|
pub struct ContractPathAndIdent {
|
||||||
/// The path of the contract source code relative to the directory containing the metadata file.
|
/// The path of the contract source code relative to the directory containing the metadata file.
|
||||||
@@ -363,9 +400,15 @@ impl From<ContractPathAndIdent> for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An EVM version requirement that the test case has. This gets serialized and
|
/// An EVM version requirement that the test case has. This gets serialized and deserialized from
|
||||||
/// deserialized from and into [`String`].
|
/// and into [`String`]. This follows a simple format of (>=|<=|=|>|<) followed by a string of the
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
/// 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")]
|
#[serde(try_from = "String", into = "String")]
|
||||||
pub struct EvmVersionRequirement {
|
pub struct EvmVersionRequirement {
|
||||||
ordering: Ordering,
|
ordering: Ordering,
|
||||||
@@ -493,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.
|
/// 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.
|
/// Defines how the compiler should handle revert strings.
|
||||||
#[derive(
|
#[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 {
|
pub struct CompilationDirectives {
|
||||||
/// Defines how the revert strings should be handled.
|
/// Defines how the revert strings should be handled.
|
||||||
@@ -502,14 +556,29 @@ pub struct CompilationDirectives {
|
|||||||
|
|
||||||
/// Defines how the compiler should handle revert strings.
|
/// Defines how the compiler should handle revert strings.
|
||||||
#[derive(
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum RevertString {
|
pub enum RevertString {
|
||||||
|
/// The default handling of the revert strings.
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
|
/// The debug handling of the revert strings.
|
||||||
Debug,
|
Debug,
|
||||||
|
/// Strip the revert strings.
|
||||||
Strip,
|
Strip,
|
||||||
|
/// Provide verbose debug strings for the revert string.
|
||||||
VerboseDebug,
|
VerboseDebug,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use revive_dt_common::iterators::EitherIter;
|
||||||
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -16,7 +18,7 @@ use std::sync::LazyLock;
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
||||||
#[serde(try_from = "String", into = "String")]
|
#[serde(try_from = "String", into = "String")]
|
||||||
pub struct ParsedMode {
|
pub struct ParsedMode {
|
||||||
pub pipeline: Option<ModePipeline>,
|
pub pipeline: Option<ModePipeline>,
|
||||||
@@ -176,27 +178,6 @@ impl ParsedMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
eips::BlockNumberOrTag,
|
eips::BlockNumberOrTag,
|
||||||
@@ -8,8 +8,9 @@ use alloy::{
|
|||||||
rpc::types::TransactionRequest,
|
rpc::types::TransactionRequest,
|
||||||
};
|
};
|
||||||
use alloy_primitives::{FixedBytes, utils::parse_units};
|
use alloy_primitives::{FixedBytes, utils::parse_units};
|
||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
||||||
|
use schemars::JsonSchema;
|
||||||
use semver::VersionReq;
|
use semver::VersionReq;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -23,15 +24,24 @@ 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
|
/// 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.
|
/// 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)]
|
#[serde(untagged)]
|
||||||
pub enum Step {
|
pub enum Step {
|
||||||
/// A function call or an invocation to some function on some smart contract.
|
/// 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.
|
/// 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.
|
/// 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!(
|
define_wrapper_type!(
|
||||||
@@ -39,37 +49,52 @@ define_wrapper_type!(
|
|||||||
pub struct StepIdx(usize) impl Display;
|
pub struct StepIdx(usize) impl Display;
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
/// This is an input step which is a transaction description that the framework translates into a
|
||||||
pub struct Input {
|
/// transaction and executes on the nodes.
|
||||||
#[serde(default = "Input::default_caller")]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||||
pub caller: Address,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
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,
|
pub instance: ContractInstance,
|
||||||
|
|
||||||
|
/// The method that's being called in this step.
|
||||||
pub method: Method,
|
pub method: Method,
|
||||||
|
|
||||||
|
/// The calldata that the function should be invoked with.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub calldata: Calldata,
|
pub calldata: Calldata,
|
||||||
|
|
||||||
|
/// A set of assertions and expectations to have for the transaction.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub expected: Option<Expected>,
|
pub expected: Option<Expected>,
|
||||||
|
|
||||||
|
/// An optional value to provide as part of the transaction.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub value: Option<EtherValue>,
|
pub value: Option<EtherValue>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(skip)]
|
||||||
pub storage: Option<HashMap<String, Calldata>>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub variable_assignments: Option<VariableAssignments>,
|
pub variable_assignments: Option<VariableAssignments>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
/// This represents a balance assertion step where the framework needs to query the balance of some
|
||||||
pub struct BalanceAssertion {
|
/// 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.
|
/// An optional comment on the balance assertion.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
@@ -80,14 +105,18 @@ pub struct BalanceAssertion {
|
|||||||
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
|
/// 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
|
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
|
||||||
/// followed in the calldata.
|
/// 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,
|
pub expected_balance: U256,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
/// This represents an assertion for the storage of some contract or account and whether it's empty
|
||||||
pub struct StorageEmptyAssertion {
|
/// or not.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||||
|
pub struct StorageEmptyAssertionStep {
|
||||||
/// An optional comment on the storage empty assertion.
|
/// An optional comment on the storage empty assertion.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
@@ -98,37 +127,86 @@ pub struct StorageEmptyAssertion {
|
|||||||
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
|
/// 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
|
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
|
||||||
/// followed in the calldata.
|
/// followed in the calldata.
|
||||||
pub address: String,
|
pub address: StepAddress,
|
||||||
|
|
||||||
/// A boolean of whether the storage of the address is empty or not.
|
/// A boolean of whether the storage of the address is empty or not.
|
||||||
pub is_storage_empty: bool,
|
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)]
|
#[serde(untagged)]
|
||||||
pub enum Expected {
|
pub enum Expected {
|
||||||
|
/// An assertion that the transaction succeeded and returned the provided set of data.
|
||||||
Calldata(Calldata),
|
Calldata(Calldata),
|
||||||
|
/// A more complex assertion.
|
||||||
Expected(ExpectedOutput),
|
Expected(ExpectedOutput),
|
||||||
|
/// A set of assertions.
|
||||||
ExpectedMany(Vec<ExpectedOutput>),
|
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 {
|
pub struct ExpectedOutput {
|
||||||
|
/// An optional compiler version that's required in order for this assertion to run.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(with = "Option<String>")]
|
||||||
pub compiler_version: Option<VersionReq>,
|
pub compiler_version: Option<VersionReq>,
|
||||||
|
|
||||||
|
/// An optional field of the expected returns from the invocation.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub return_data: Option<Calldata>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub events: Option<Vec<Event>>,
|
pub events: Option<Vec<Event>>,
|
||||||
|
|
||||||
|
/// A boolean which defines whether we expect the transaction to succeed or fail.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub exception: bool,
|
pub exception: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Eq, PartialEq)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
|
/// An optional field of the address of the emitter of the event.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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>,
|
pub topics: Vec<String>,
|
||||||
|
|
||||||
|
/// The set of values to expect the event to have.
|
||||||
pub values: Calldata,
|
pub values: Calldata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,16 +261,17 @@ pub struct Event {
|
|||||||
/// [`Single`]: Calldata::Single
|
/// [`Single`]: Calldata::Single
|
||||||
/// [`Compound`]: Calldata::Compound
|
/// [`Compound`]: Calldata::Compound
|
||||||
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
|
/// [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)]
|
#[serde(untagged)]
|
||||||
pub enum Calldata {
|
pub enum Calldata {
|
||||||
Single(Bytes),
|
Single(#[schemars(with = "String")] Bytes),
|
||||||
Compound(Vec<CalldataItem>),
|
Compound(Vec<CalldataItem>),
|
||||||
}
|
}
|
||||||
|
|
||||||
define_wrapper_type! {
|
define_wrapper_type! {
|
||||||
/// This represents an item in the [`Calldata::Compound`] variant.
|
/// This represents an item in the [`Calldata::Compound`] variant. Each item will be resolved
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
/// according to the resolution rules of the tool.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct CalldataItem(String) impl Display;
|
pub struct CalldataItem(String) impl Display;
|
||||||
}
|
}
|
||||||
@@ -217,7 +296,7 @@ enum Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Specify how the contract is called.
|
/// 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 {
|
pub enum Method {
|
||||||
/// Initiate a deploy transaction, calling contracts constructor.
|
/// Initiate a deploy transaction, calling contracts constructor.
|
||||||
///
|
///
|
||||||
@@ -238,11 +317,16 @@ pub enum Method {
|
|||||||
}
|
}
|
||||||
|
|
||||||
define_wrapper_type!(
|
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;
|
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 {
|
pub struct VariableAssignments {
|
||||||
/// A vector of the variable names to assign to the return data.
|
/// A vector of the variable names to assign to the return data.
|
||||||
///
|
///
|
||||||
@@ -250,20 +334,81 @@ pub struct VariableAssignments {
|
|||||||
pub return_data: Vec<String>,
|
pub return_data: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
/// An address type that might either be an address literal or a resolvable address.
|
||||||
pub const fn default_caller() -> 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!(
|
Address(FixedBytes(alloy::hex!(
|
||||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
|
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn default_caller() -> StepAddress {
|
||||||
|
StepAddress::Address(Self::default_caller_address())
|
||||||
|
}
|
||||||
|
|
||||||
fn default_instance() -> ContractInstance {
|
fn default_instance() -> ContractInstance {
|
||||||
ContractInstance::new("Test")
|
ContractInstance::new("Test")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn encoded_input(
|
pub async fn encoded_input(
|
||||||
&self,
|
&self,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<Bytes> {
|
) -> anyhow::Result<Bytes> {
|
||||||
match self.method {
|
match self.method {
|
||||||
@@ -332,14 +477,15 @@ impl Input {
|
|||||||
/// Parse this input into a legacy transaction.
|
/// Parse this input into a legacy transaction.
|
||||||
pub async fn legacy_transaction(
|
pub async fn legacy_transaction(
|
||||||
&self,
|
&self,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<TransactionRequest> {
|
) -> anyhow::Result<TransactionRequest> {
|
||||||
let input_data = self
|
let input_data = self
|
||||||
.encoded_input(resolver, context)
|
.encoded_input(resolver, context)
|
||||||
.await
|
.await
|
||||||
.context("Failed to encode input bytes for transaction request")?;
|
.context("Failed to encode input bytes for transaction request")?;
|
||||||
let transaction_request = TransactionRequest::default().from(self.caller).value(
|
let caller = self.caller.resolve_address(resolver, context).await?;
|
||||||
|
let transaction_request = TransactionRequest::default().from(caller).value(
|
||||||
self.value
|
self.value
|
||||||
.map(|value| value.into_inner())
|
.map(|value| value.into_inner())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
@@ -421,7 +567,7 @@ impl Calldata {
|
|||||||
|
|
||||||
pub async fn calldata(
|
pub async fn calldata(
|
||||||
&self,
|
&self,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<Vec<u8>> {
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
|
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
|
||||||
@@ -433,7 +579,7 @@ impl Calldata {
|
|||||||
pub async fn calldata_into_slice(
|
pub async fn calldata_into_slice(
|
||||||
&self,
|
&self,
|
||||||
buffer: &mut Vec<u8>,
|
buffer: &mut Vec<u8>,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
@@ -470,7 +616,7 @@ impl Calldata {
|
|||||||
pub async fn is_equivalent(
|
pub async fn is_equivalent(
|
||||||
&self,
|
&self,
|
||||||
other: &[u8],
|
other: &[u8],
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
match self {
|
match self {
|
||||||
@@ -512,7 +658,7 @@ impl CalldataItem {
|
|||||||
#[instrument(level = "info", skip_all, err)]
|
#[instrument(level = "info", skip_all, err)]
|
||||||
async fn resolve(
|
async fn resolve(
|
||||||
&self,
|
&self,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<U256> {
|
) -> anyhow::Result<U256> {
|
||||||
let mut stack = Vec::<CalldataToken<U256>>::new();
|
let mut stack = Vec::<CalldataToken<U256>>::new();
|
||||||
@@ -617,7 +763,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
|
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
|
||||||
async fn resolve(
|
async fn resolve(
|
||||||
self,
|
self,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
) -> anyhow::Result<CalldataToken<U256>> {
|
) -> anyhow::Result<CalldataToken<U256>> {
|
||||||
match self {
|
match self {
|
||||||
@@ -650,7 +796,7 @@ impl<T: AsRef<str>> CalldataToken<T> {
|
|||||||
context
|
context
|
||||||
.transaction_hash()
|
.transaction_hash()
|
||||||
.context("No transaction hash provided to get the transaction gas price")
|
.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
|
.await
|
||||||
.map(U256::from)
|
.map(U256::from)
|
||||||
} else if item == Self::GAS_LIMIT_VARIABLE {
|
} else if item == Self::GAS_LIMIT_VARIABLE {
|
||||||
@@ -754,7 +900,7 @@ mod tests {
|
|||||||
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
|
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
|
||||||
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
|
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
|
||||||
use alloy_sol_types::SolValue;
|
use alloy_sol_types::SolValue;
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, pin::Pin};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::metadata::ContractIdent;
|
use crate::metadata::ContractIdent;
|
||||||
@@ -762,40 +908,63 @@ mod tests {
|
|||||||
struct MockResolver;
|
struct MockResolver;
|
||||||
|
|
||||||
impl ResolverApi for MockResolver {
|
impl ResolverApi for MockResolver {
|
||||||
async fn chain_id(&self) -> anyhow::Result<ChainId> {
|
fn chain_id(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<ChainId>> + '_>> {
|
||||||
Ok(0x123)
|
Box::pin(async move { Ok(0x123) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn block_gas_limit(&self, _: BlockNumberOrTag) -> anyhow::Result<u128> {
|
fn block_gas_limit(
|
||||||
Ok(0x1234)
|
&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> {
|
fn block_coinbase(
|
||||||
Ok(Address::ZERO)
|
&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> {
|
fn block_difficulty(
|
||||||
Ok(U256::from(0x12345u128))
|
&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> {
|
fn block_base_fee(
|
||||||
Ok(0x100)
|
&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> {
|
fn block_hash(
|
||||||
Ok([0xEE; 32].into())
|
&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> {
|
fn block_timestamp(
|
||||||
Ok(0x123456)
|
&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> {
|
fn last_block_number(
|
||||||
Ok(0x1234567)
|
&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> {
|
fn transaction_gas_price(
|
||||||
Ok(0x200)
|
&self,
|
||||||
|
_: TxHash,
|
||||||
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||||
|
Box::pin(async move { Ok(0x200) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,7 +991,7 @@ mod tests {
|
|||||||
.selector()
|
.selector()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let input = Input {
|
let input = FunctionCallStep {
|
||||||
instance: ContractInstance::new("Contract"),
|
instance: ContractInstance::new("Contract"),
|
||||||
method: Method::FunctionName("store".to_owned()),
|
method: Method::FunctionName("store".to_owned()),
|
||||||
calldata: Calldata::new_compound(["42"]),
|
calldata: Calldata::new_compound(["42"]),
|
||||||
@@ -866,7 +1035,7 @@ mod tests {
|
|||||||
.selector()
|
.selector()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let input: Input = Input {
|
let input: FunctionCallStep = FunctionCallStep {
|
||||||
instance: "Contract".to_owned().into(),
|
instance: "Contract".to_owned().into(),
|
||||||
method: Method::FunctionName("send(address)".to_owned()),
|
method: Method::FunctionName("send(address)".to_owned()),
|
||||||
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
||||||
@@ -913,7 +1082,7 @@ mod tests {
|
|||||||
.selector()
|
.selector()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let input: Input = Input {
|
let input: FunctionCallStep = FunctionCallStep {
|
||||||
instance: ContractInstance::new("Contract"),
|
instance: ContractInstance::new("Contract"),
|
||||||
method: Method::FunctionName("send".to_owned()),
|
method: Method::FunctionName("send".to_owned()),
|
||||||
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
|
||||||
@@ -942,7 +1111,7 @@ mod tests {
|
|||||||
async fn resolve_calldata_item(
|
async fn resolve_calldata_item(
|
||||||
input: &str,
|
input: &str,
|
||||||
deployed_contracts: &HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
deployed_contracts: &HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||||
resolver: &impl ResolverApi,
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
) -> anyhow::Result<U256> {
|
) -> anyhow::Result<U256> {
|
||||||
let context = ResolutionContext::default().with_deployed_contracts(deployed_contracts);
|
let context = ResolutionContext::default().with_deployed_contracts(deployed_contracts);
|
||||||
CalldataItem::new(input).resolve(resolver, context).await
|
CalldataItem::new(input).resolve(resolver, context).await
|
||||||
+29
-10
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
use alloy::eips::BlockNumberOrTag;
|
use alloy::eips::BlockNumberOrTag;
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
@@ -12,36 +13,54 @@ use crate::metadata::{ContractIdent, ContractInstance};
|
|||||||
/// crate implements to go from string calldata and into the bytes calldata.
|
/// crate implements to go from string calldata and into the bytes calldata.
|
||||||
pub trait ResolverApi {
|
pub trait ResolverApi {
|
||||||
/// Returns the ID of the chain that the node is on.
|
/// 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.
|
/// 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.
|
// 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.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// 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,
|
/// Returns the timestamp of the specified block,
|
||||||
fn block_timestamp(
|
fn block_timestamp(
|
||||||
&self,
|
&self,
|
||||||
number: BlockNumberOrTag,
|
number: BlockNumberOrTag,
|
||||||
) -> impl Future<Output = Result<BlockTimestamp>>;
|
) -> Pin<Box<dyn Future<Output = Result<BlockTimestamp>> + '_>>;
|
||||||
|
|
||||||
/// Returns the number of the last block.
|
/// 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)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
revive-common = { workspace = true }
|
||||||
|
|
||||||
|
revive-dt-format = { workspace = true }
|
||||||
|
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,53 @@
|
|||||||
//! This crate implements all node interactions.
|
//! This crate implements all node interactions.
|
||||||
|
|
||||||
use alloy::primitives::{Address, StorageKey, U256};
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use alloy::primitives::{Address, StorageKey, TxHash, U256};
|
||||||
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
||||||
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use revive_common::EVMVersion;
|
||||||
|
use revive_dt_format::traits::ResolverApi;
|
||||||
|
|
||||||
/// An interface for all interactions with Ethereum compatible nodes.
|
/// An interface for all interactions with Ethereum compatible nodes.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub trait EthereumNode {
|
pub trait EthereumNode {
|
||||||
|
fn id(&self) -> usize;
|
||||||
|
|
||||||
|
/// Returns the nodes connection string.
|
||||||
|
fn connection_string(&self) -> &str;
|
||||||
|
|
||||||
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
|
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
|
||||||
fn execute_transaction(
|
fn execute_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> impl Future<Output = Result<TransactionReceipt>>;
|
) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
|
||||||
|
|
||||||
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
|
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
|
||||||
fn trace_transaction(
|
fn trace_transaction(
|
||||||
&self,
|
&self,
|
||||||
receipt: &TransactionReceipt,
|
tx_hash: TxHash,
|
||||||
trace_options: GethDebugTracingOptions,
|
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].
|
/// 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.
|
/// 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`]
|
/// Returns the latest storage proof of the provided [`Address`]
|
||||||
fn latest_state_proof(
|
fn latest_state_proof(
|
||||||
&self,
|
&self,
|
||||||
address: Address,
|
address: Address,
|
||||||
keys: Vec<StorageKey>,
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
|
/// 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
|
/// 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 Kitchensink.
|
/// some issues with different values of the initial balance on substrate.
|
||||||
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
|
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
|
||||||
|
|||||||
+351
-311
@@ -5,6 +5,7 @@ use std::{
|
|||||||
io::{BufRead, BufReader, Read, Write},
|
io::{BufRead, BufReader, Read, Write},
|
||||||
ops::ControlFlow,
|
ops::ControlFlow,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
pin::Pin,
|
||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, Stdio},
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
@@ -17,21 +18,18 @@ use alloy::{
|
|||||||
eips::BlockNumberOrTag,
|
eips::BlockNumberOrTag,
|
||||||
genesis::{Genesis, GenesisAccount},
|
genesis::{Genesis, GenesisAccount},
|
||||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||||
primitives::{
|
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||||
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
|
|
||||||
},
|
|
||||||
providers::{
|
providers::{
|
||||||
Provider, ProviderBuilder,
|
Provider, ProviderBuilder,
|
||||||
ext::DebugApi,
|
ext::DebugApi,
|
||||||
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
|
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
|
||||||
},
|
},
|
||||||
rpc::types::{
|
rpc::types::{
|
||||||
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
EIP1186AccountProofResponse, TransactionRequest,
|
||||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
||||||
},
|
},
|
||||||
signers::local::PrivateKeySigner,
|
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use tracing::{Instrument, instrument};
|
use tracing::{Instrument, instrument};
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ use revive_dt_common::{
|
|||||||
fs::clear_directory,
|
fs::clear_directory,
|
||||||
futures::{PollingWaitBehavior, poll},
|
futures::{PollingWaitBehavior, poll},
|
||||||
};
|
};
|
||||||
use revive_dt_config::Arguments;
|
use revive_dt_config::*;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
|
|
||||||
@@ -64,7 +62,7 @@ pub struct GethNode {
|
|||||||
geth: PathBuf,
|
geth: PathBuf,
|
||||||
id: u32,
|
id: u32,
|
||||||
handle: Option<Child>,
|
handle: Option<Child>,
|
||||||
start_timeout: u64,
|
start_timeout: Duration,
|
||||||
wallet: Arc<EthereumWallet>,
|
wallet: Arc<EthereumWallet>,
|
||||||
nonce_manager: CachedNonceManager,
|
nonce_manager: CachedNonceManager,
|
||||||
chain_id_filler: ChainIdFiller,
|
chain_id_filler: ChainIdFiller,
|
||||||
@@ -95,9 +93,46 @@ impl GethNode {
|
|||||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
||||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(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(),
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create the node directory and call `geth init` to configure the genesis.
|
/// Create the node directory and call `geth init` to configure the genesis.
|
||||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
|
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||||
let _ = clear_directory(&self.base_directory);
|
let _ = clear_directory(&self.base_directory);
|
||||||
let _ = clear_directory(&self.logs_directory);
|
let _ = clear_directory(&self.logs_directory);
|
||||||
|
|
||||||
@@ -106,8 +141,6 @@ impl GethNode {
|
|||||||
create_dir_all(&self.logs_directory)
|
create_dir_all(&self.logs_directory)
|
||||||
.context("Failed to create logs directory for geth node")?;
|
.context("Failed to create logs directory for geth node")?;
|
||||||
|
|
||||||
let mut genesis = serde_json::from_str::<Genesis>(&genesis)
|
|
||||||
.context("Failed to deserialize geth genesis JSON")?;
|
|
||||||
for signer_address in
|
for signer_address in
|
||||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
||||||
{
|
{
|
||||||
@@ -240,7 +273,7 @@ impl GethNode {
|
|||||||
.open(self.geth_stderr_log_file_path())
|
.open(self.geth_stderr_log_file_path())
|
||||||
.context("Failed to open geth stderr logs file for readiness check")?;
|
.context("Failed to open geth stderr logs file for readiness check")?;
|
||||||
|
|
||||||
let maximum_wait_time = Duration::from_millis(self.start_timeout);
|
let maximum_wait_time = self.start_timeout;
|
||||||
let mut stderr = BufReader::new(logs_file).lines();
|
let mut stderr = BufReader::new(logs_file).lines();
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
loop {
|
loop {
|
||||||
@@ -256,7 +289,7 @@ impl GethNode {
|
|||||||
if Instant::now().duration_since(start_time) > maximum_wait_time {
|
if Instant::now().duration_since(start_time) > maximum_wait_time {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
|
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
|
||||||
self.start_timeout,
|
self.start_timeout.as_millis(),
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -294,310 +327,327 @@ impl GethNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EthereumNode for GethNode {
|
impl EthereumNode for GethNode {
|
||||||
|
fn id(&self) -> usize {
|
||||||
|
self.id as _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_string(&self) -> &str {
|
||||||
|
&self.connection_string
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
fields(geth_node_id = self.id, connection_string = self.connection_string),
|
||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
async fn execute_transaction(
|
fn execute_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::TransactionReceipt>> + '_>>
|
||||||
let provider = self
|
{
|
||||||
.provider()
|
Box::pin(async move {
|
||||||
.await
|
let provider = self
|
||||||
.context("Failed to create provider for transaction submission")?;
|
.provider()
|
||||||
|
.await
|
||||||
|
.context("Failed to create provider for transaction submission")?;
|
||||||
|
|
||||||
let pending_transaction = provider
|
let pending_transaction = provider
|
||||||
.send_transaction(transaction)
|
.send_transaction(transaction)
|
||||||
.await
|
.await
|
||||||
.inspect_err(
|
.inspect_err(
|
||||||
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
|
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
|
||||||
)
|
)
|
||||||
.context("Failed to submit transaction to geth node")?;
|
.context("Failed to submit transaction to geth node")?;
|
||||||
let transaction_hash = *pending_transaction.tx_hash();
|
let transaction_hash = *pending_transaction.tx_hash();
|
||||||
|
|
||||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
// 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
|
// 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,
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// _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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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.
|
// backoff each time we attempt to get the receipt and find that it's not available.
|
||||||
let provider = Arc::new(provider);
|
let provider = Arc::new(provider);
|
||||||
poll(
|
poll(
|
||||||
Self::RECEIPT_POLLING_DURATION,
|
Self::RECEIPT_POLLING_DURATION,
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||||
move || {
|
move || {
|
||||||
let provider = provider.clone();
|
let provider = provider.clone();
|
||||||
async move {
|
async move {
|
||||||
match provider.get_transaction_receipt(transaction_hash).await {
|
match provider.get_transaction_receipt(transaction_hash).await {
|
||||||
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
|
||||||
Ok(None) => Ok(ControlFlow::Continue(())),
|
Ok(None) => Ok(ControlFlow::Continue(())),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let error_string = error.to_string();
|
let error_string = error.to_string();
|
||||||
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
|
||||||
true => Ok(ControlFlow::Continue(())),
|
true => Ok(ControlFlow::Continue(())),
|
||||||
false => Err(error.into()),
|
false => Err(error.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
.instrument(tracing::info_span!(
|
||||||
.instrument(tracing::info_span!(
|
"Awaiting transaction receipt",
|
||||||
"Awaiting transaction receipt",
|
?transaction_hash
|
||||||
?transaction_hash
|
))
|
||||||
))
|
.await
|
||||||
.await
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn trace_transaction(
|
fn trace_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: &TransactionReceipt,
|
tx_hash: TxHash,
|
||||||
trace_options: GethDebugTracingOptions,
|
trace_options: GethDebugTracingOptions,
|
||||||
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>>
|
||||||
let provider = Arc::new(
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let provider = Arc::new(
|
||||||
|
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()
|
self.provider()
|
||||||
.await
|
.await
|
||||||
.context("Failed to create provider for tracing")?,
|
.context("Failed to get the Geth provider")?
|
||||||
);
|
.get_balance(address)
|
||||||
poll(
|
.await
|
||||||
Self::TRACE_POLLING_DURATION,
|
.map_err(Into::into)
|
||||||
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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
|
fn latest_state_proof(
|
||||||
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
|
|
||||||
.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))]
|
|
||||||
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
|
|
||||||
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))]
|
|
||||||
async fn latest_state_proof(
|
|
||||||
&self,
|
&self,
|
||||||
address: Address,
|
address: Address,
|
||||||
keys: Vec<StorageKey>,
|
keys: Vec<StorageKey>,
|
||||||
) -> anyhow::Result<EIP1186AccountProofResponse> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> {
|
||||||
self.provider()
|
Box::pin(async move {
|
||||||
.await
|
self.provider()
|
||||||
.context("Failed to get the Geth provider")?
|
.await
|
||||||
.get_proof(address, keys)
|
.context("Failed to get the Geth provider")?
|
||||||
.latest()
|
.get_proof(address, keys)
|
||||||
.await
|
.latest()
|
||||||
.map_err(Into::into)
|
.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolverApi for GethNode {
|
pub struct GethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
|
||||||
|
id: u32,
|
||||||
|
provider: FillProvider<F, P, Ethereum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for GethNodeResolver<F, P> {
|
||||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
|
fn chain_id(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::primitives::ChainId>> + '_>> {
|
||||||
.context("Failed to get the Geth provider")?
|
Box::pin(async move { self.provider.get_chain_id().await.map_err(Into::into) })
|
||||||
.get_chain_id()
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
|
fn transaction_gas_price(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
tx_hash: TxHash,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||||
.get_transaction_receipt(*tx_hash)
|
Box::pin(async move {
|
||||||
.await?
|
self.provider
|
||||||
.context("Failed to get the transaction receipt")
|
.get_transaction_receipt(tx_hash)
|
||||||
.map(|receipt| receipt.effective_gas_price)
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
|
fn block_gas_limit(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<u128>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.map(|block| block.header.gas_limit as _)
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
|
fn block_coinbase(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<Address>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.map(|block| block.header.beneficiary)
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
|
fn block_difficulty(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.map(|block| U256::from_be_bytes(block.header.mix_hash.0))
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
|
fn block_base_fee(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<u64>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.and_then(|block| {
|
.context("Failed to get the geth block")?
|
||||||
block
|
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||||
.header
|
.and_then(|block| {
|
||||||
.base_fee_per_gas
|
block
|
||||||
.context("Failed to get the base fee per gas")
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
|
fn block_hash(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockHash>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.map(|block| block.header.hash)
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
|
fn block_timestamp(
|
||||||
self.provider()
|
&self,
|
||||||
.await
|
number: BlockNumberOrTag,
|
||||||
.context("Failed to get the Geth provider")?
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockTimestamp>> + '_>> {
|
||||||
.get_block_by_number(number)
|
Box::pin(async move {
|
||||||
.await
|
self.provider
|
||||||
.context("Failed to get the geth block")?
|
.get_block_by_number(number)
|
||||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
.await
|
||||||
.map(|block| block.header.timestamp)
|
.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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
|
fn last_block_number(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockNumber>> + '_>> {
|
||||||
self.provider()
|
Box::pin(async move { self.provider.get_block_number().await.map_err(Into::into) })
|
||||||
.await
|
|
||||||
.context("Failed to get the Geth provider")?
|
|
||||||
.get_block_number()
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for GethNode {
|
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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
fn shutdown(&mut self) -> anyhow::Result<()> {
|
fn shutdown(&mut self) -> anyhow::Result<()> {
|
||||||
// Terminate the processes in a graceful manner to allow for the output to be flushed.
|
// Terminate the processes in a graceful manner to allow for the output to be flushed.
|
||||||
@@ -621,7 +671,7 @@ impl Node for GethNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
|
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
|
||||||
self.init(genesis)?.spawn_process()?;
|
self.init(genesis)?.spawn_process()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -640,17 +690,6 @@ impl Node for GethNode {
|
|||||||
.stdout;
|
.stdout;
|
||||||
Ok(String::from_utf8_lossy(&output).into())
|
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 {
|
impl Drop for GethNode {
|
||||||
@@ -662,49 +701,25 @@ impl Drop for GethNode {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use revive_dt_config::Arguments;
|
|
||||||
|
|
||||||
use temp_dir::TempDir;
|
|
||||||
|
|
||||||
use crate::{GENESIS_JSON, Node};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn test_config() -> (Arguments, TempDir) {
|
fn test_config() -> TestExecutionContext {
|
||||||
let mut config = Arguments::default();
|
TestExecutionContext::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) {
|
fn new_node() -> (TestExecutionContext, GethNode) {
|
||||||
let (args, temp_dir) = test_config();
|
let context = test_config();
|
||||||
let mut node = GethNode::new(&args);
|
let mut node = GethNode::new(&context);
|
||||||
node.init(GENESIS_JSON.to_owned())
|
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
||||||
.expect("Failed to initialize the node")
|
.expect("Failed to initialize the node")
|
||||||
.spawn_process()
|
.spawn_process()
|
||||||
.expect("Failed to spawn the node process");
|
.expect("Failed to spawn the node process");
|
||||||
(node, temp_dir)
|
(context, node)
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
#[test]
|
||||||
fn version_works() {
|
fn version_works() {
|
||||||
let version = GethNode::new(&test_config().0).version().unwrap();
|
let version = GethNode::new(&test_config()).version().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
version.starts_with("geth version"),
|
version.starts_with("geth version"),
|
||||||
"expected version string, got: '{version}'"
|
"expected version string, got: '{version}'"
|
||||||
@@ -714,10 +729,10 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_chain_id_from_node() {
|
async fn can_get_chain_id_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let chain_id = node.chain_id().await;
|
let chain_id = node.resolver().await.unwrap().chain_id().await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let chain_id = chain_id.expect("Failed to get the chain id");
|
let chain_id = chain_id.expect("Failed to get the chain id");
|
||||||
@@ -727,10 +742,15 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_gas_limit_from_node() {
|
async fn can_get_gas_limit_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
|
let gas_limit = node
|
||||||
|
.resolver()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.block_gas_limit(BlockNumberOrTag::Latest)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let gas_limit = gas_limit.expect("Failed to get the gas limit");
|
let gas_limit = gas_limit.expect("Failed to get the gas limit");
|
||||||
@@ -740,10 +760,15 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_coinbase_from_node() {
|
async fn can_get_coinbase_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
|
let coinbase = node
|
||||||
|
.resolver()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.block_coinbase(BlockNumberOrTag::Latest)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let coinbase = coinbase.expect("Failed to get the coinbase");
|
let coinbase = coinbase.expect("Failed to get the coinbase");
|
||||||
@@ -753,10 +778,15 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_block_difficulty_from_node() {
|
async fn can_get_block_difficulty_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
|
let block_difficulty = node
|
||||||
|
.resolver()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.block_difficulty(BlockNumberOrTag::Latest)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
|
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
|
||||||
@@ -766,10 +796,15 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_block_hash_from_node() {
|
async fn can_get_block_hash_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
|
let block_hash = node
|
||||||
|
.resolver()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.block_hash(BlockNumberOrTag::Latest)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let _ = block_hash.expect("Failed to get the block hash");
|
let _ = block_hash.expect("Failed to get the block hash");
|
||||||
@@ -778,10 +813,15 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_block_timestamp_from_node() {
|
async fn can_get_block_timestamp_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
|
let block_timestamp = node
|
||||||
|
.resolver()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.block_timestamp(BlockNumberOrTag::Latest)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||||
@@ -790,10 +830,10 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn can_get_block_number_from_node() {
|
async fn can_get_block_number_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let (node, _temp_dir) = new_node();
|
let (_context, node) = new_node();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
let block_number = node.last_block_number().await;
|
let block_number = node.resolver().await.unwrap().last_block_number().await;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
let block_number = block_number.expect("Failed to get the block number");
|
let block_number = block_number.expect("Failed to get the block number");
|
||||||
|
|||||||
+3
-24
@@ -1,46 +1,25 @@
|
|||||||
//! This crate implements the testing nodes.
|
//! This crate implements the testing nodes.
|
||||||
|
|
||||||
use revive_common::EVMVersion;
|
use alloy::genesis::Genesis;
|
||||||
use revive_dt_config::Arguments;
|
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod geth;
|
pub mod geth;
|
||||||
pub mod kitchensink;
|
pub mod substrate;
|
||||||
pub mod pool;
|
|
||||||
|
|
||||||
/// The default genesis configuration.
|
|
||||||
pub const GENESIS_JSON: &str = include_str!("../../../genesis.json");
|
|
||||||
|
|
||||||
/// An abstract interface for testing nodes.
|
/// An abstract interface for testing nodes.
|
||||||
pub trait Node: EthereumNode {
|
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.
|
/// Spawns a node configured according to the genesis json.
|
||||||
///
|
///
|
||||||
/// Blocking until it's ready to accept transactions.
|
/// 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.
|
/// Prune the node instance and related data.
|
||||||
///
|
///
|
||||||
/// Blocking until it's completely stopped.
|
/// Blocking until it's completely stopped.
|
||||||
fn shutdown(&mut self) -> anyhow::Result<()>;
|
fn shutdown(&mut self) -> anyhow::Result<()>;
|
||||||
|
|
||||||
/// Returns the nodes connection string.
|
|
||||||
fn connection_string(&self) -> String;
|
|
||||||
|
|
||||||
/// Returns the node version.
|
/// Returns the node version.
|
||||||
fn version(&self) -> anyhow::Result<String>;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +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))
|
|
||||||
.context("Failed to join node spawn thread")?
|
|
||||||
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))
|
|
||||||
.context("Node failed to spawn")?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
.context("Failed to spawn node process")?;
|
|
||||||
info!(
|
|
||||||
id = node.id(),
|
|
||||||
connection_string = node.connection_string(),
|
|
||||||
"Spawned node"
|
|
||||||
);
|
|
||||||
Ok(node)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,9 @@ use std::{
|
|||||||
use alloy_primitives::Address;
|
use alloy_primitives::Address;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
|
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
|
||||||
use revive_dt_config::{Arguments, TestingPlatform};
|
use revive_dt_config::Context;
|
||||||
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -36,11 +37,11 @@ pub struct ReportAggregator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ReportAggregator {
|
impl ReportAggregator {
|
||||||
pub fn new(config: Arguments) -> Self {
|
pub fn new(context: Context) -> Self {
|
||||||
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
|
||||||
let (listener_tx, _) = channel::<ReporterEvent>(1024);
|
let (listener_tx, _) = channel::<ReporterEvent>(1024);
|
||||||
Self {
|
Self {
|
||||||
report: Report::new(config),
|
report: Report::new(context),
|
||||||
remaining_cases: Default::default(),
|
remaining_cases: Default::default(),
|
||||||
runner_tx: Some(runner_tx),
|
runner_tx: Some(runner_tx),
|
||||||
runner_rx,
|
runner_rx,
|
||||||
@@ -84,11 +85,8 @@ impl ReportAggregator {
|
|||||||
RunnerEvent::TestIgnored(event) => {
|
RunnerEvent::TestIgnored(event) => {
|
||||||
self.handle_test_ignored_event(*event);
|
self.handle_test_ignored_event(*event);
|
||||||
}
|
}
|
||||||
RunnerEvent::LeaderNodeAssigned(event) => {
|
RunnerEvent::NodeAssigned(event) => {
|
||||||
self.handle_leader_node_assigned_event(*event);
|
self.handle_node_assigned_event(*event);
|
||||||
}
|
|
||||||
RunnerEvent::FollowerNodeAssigned(event) => {
|
|
||||||
self.handle_follower_node_assigned_event(*event);
|
|
||||||
}
|
}
|
||||||
RunnerEvent::PreLinkContractsCompilationSucceeded(event) => {
|
RunnerEvent::PreLinkContractsCompilationSucceeded(event) => {
|
||||||
self.handle_pre_link_contracts_compilation_succeeded_event(*event)
|
self.handle_pre_link_contracts_compilation_succeeded_event(*event)
|
||||||
@@ -121,7 +119,12 @@ impl ReportAggregator {
|
|||||||
file_name.push_str(".json");
|
file_name.push_str(".json");
|
||||||
file_name
|
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()
|
let file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -252,28 +255,15 @@ impl ReportAggregator {
|
|||||||
let _ = self.listener_tx.send(event);
|
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 {
|
let execution_information = self.execution_information(&ExecutionSpecifier {
|
||||||
test_specifier: event.test_specifier,
|
test_specifier: event.test_specifier,
|
||||||
node_id: event.id,
|
node_id: event.id,
|
||||||
node_designation: NodeDesignation::Leader,
|
platform_identifier: event.platform_identifier,
|
||||||
});
|
});
|
||||||
execution_information.node = Some(TestCaseNodeInformation {
|
execution_information.node = Some(TestCaseNodeInformation {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
platform: event.platform,
|
platform_identifier: event.platform_identifier,
|
||||||
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,
|
|
||||||
connection_string: event.connection_string,
|
connection_string: event.connection_string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,8 +272,16 @@ impl ReportAggregator {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: PreLinkContractsCompilationSucceededEvent,
|
event: PreLinkContractsCompilationSucceededEvent,
|
||||||
) {
|
) {
|
||||||
let include_input = self.report.config.report_include_compiler_input;
|
let include_input = self
|
||||||
let include_output = self.report.config.report_include_compiler_output;
|
.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);
|
let execution_information = self.execution_information(&event.execution_specifier);
|
||||||
|
|
||||||
@@ -311,8 +309,16 @@ impl ReportAggregator {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: PostLinkContractsCompilationSucceededEvent,
|
event: PostLinkContractsCompilationSucceededEvent,
|
||||||
) {
|
) {
|
||||||
let include_input = self.report.config.report_include_compiler_input;
|
let include_input = self
|
||||||
let include_output = self.report.config.report_include_compiler_output;
|
.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);
|
let execution_information = self.execution_information(&event.execution_specifier);
|
||||||
|
|
||||||
@@ -340,21 +346,13 @@ impl ReportAggregator {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: PreLinkContractsCompilationFailedEvent,
|
event: PreLinkContractsCompilationFailedEvent,
|
||||||
) {
|
) {
|
||||||
let include_input = self.report.config.report_include_compiler_input;
|
|
||||||
|
|
||||||
let execution_information = self.execution_information(&event.execution_specifier);
|
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 {
|
execution_information.pre_link_compilation_status = Some(CompilationStatus::Failure {
|
||||||
reason: event.reason,
|
reason: event.reason,
|
||||||
compiler_version: event.compiler_version,
|
compiler_version: event.compiler_version,
|
||||||
compiler_path: event.compiler_path,
|
compiler_path: event.compiler_path,
|
||||||
compiler_input,
|
compiler_input: event.compiler_input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,21 +360,13 @@ impl ReportAggregator {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: PostLinkContractsCompilationFailedEvent,
|
event: PostLinkContractsCompilationFailedEvent,
|
||||||
) {
|
) {
|
||||||
let include_input = self.report.config.report_include_compiler_input;
|
|
||||||
|
|
||||||
let execution_information = self.execution_information(&event.execution_specifier);
|
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 {
|
execution_information.post_link_compilation_status = Some(CompilationStatus::Failure {
|
||||||
reason: event.reason,
|
reason: event.reason,
|
||||||
compiler_version: event.compiler_version,
|
compiler_version: event.compiler_version,
|
||||||
compiler_path: event.compiler_path,
|
compiler_path: event.compiler_path,
|
||||||
compiler_input,
|
compiler_input: event.compiler_input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,26 +398,19 @@ impl ReportAggregator {
|
|||||||
specifier: &ExecutionSpecifier,
|
specifier: &ExecutionSpecifier,
|
||||||
) -> &mut ExecutionInformation {
|
) -> &mut ExecutionInformation {
|
||||||
let test_case_report = self.test_case_report(&specifier.test_specifier);
|
let test_case_report = self.test_case_report(&specifier.test_specifier);
|
||||||
match specifier.node_designation {
|
test_case_report
|
||||||
NodeDesignation::Leader => test_case_report
|
.platform_execution
|
||||||
.leader_execution_information
|
.entry(specifier.platform_identifier)
|
||||||
.get_or_insert_default(),
|
.or_default()
|
||||||
NodeDesignation::Follower => test_case_report
|
.get_or_insert_default()
|
||||||
.follower_execution_information
|
|
||||||
.get_or_insert_default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
/// The configuration that the tool was started up with.
|
/// The context that the tool was started up with.
|
||||||
pub config: Arguments,
|
pub context: Context,
|
||||||
/// 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.
|
/// The list of corpus files that the tool found.
|
||||||
pub corpora: Vec<Corpus>,
|
pub corpora: Vec<Corpus>,
|
||||||
/// The list of metadata files that were found by the tool.
|
/// The list of metadata files that were found by the tool.
|
||||||
@@ -439,11 +422,9 @@ pub struct Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Report {
|
impl Report {
|
||||||
pub fn new(config: Arguments) -> Self {
|
pub fn new(context: Context) -> Self {
|
||||||
Self {
|
Self {
|
||||||
leader_platform: config.leader,
|
context,
|
||||||
follower_platform: config.follower,
|
|
||||||
config,
|
|
||||||
corpora: Default::default(),
|
corpora: Default::default(),
|
||||||
metadata_files: Default::default(),
|
metadata_files: Default::default(),
|
||||||
test_case_information: Default::default(),
|
test_case_information: Default::default(),
|
||||||
@@ -456,12 +437,8 @@ pub struct TestCaseReport {
|
|||||||
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub status: Option<TestCaseStatus>,
|
pub status: Option<TestCaseStatus>,
|
||||||
/// Information related to the execution on the leader.
|
/// Information related to the execution on one of the platforms.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub platform_execution: BTreeMap<PlatformIdentifier, Option<ExecutionInformation>>,
|
||||||
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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
||||||
@@ -489,18 +466,18 @@ pub enum TestCaseStatus {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information related to the leader or follower node that's being used to execute the step.
|
/// Information related to the platform node that's being used to execute the step.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct TestCaseNodeInformation {
|
pub struct TestCaseNodeInformation {
|
||||||
/// The ID of the node that this case is being executed on.
|
/// The ID of the node that this case is being executed on.
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
/// The platform of the node.
|
/// The platform of the node.
|
||||||
pub platform: TestingPlatform,
|
pub platform_identifier: PlatformIdentifier,
|
||||||
/// The connection string of the node.
|
/// The connection string of the node.
|
||||||
pub connection_string: String,
|
pub connection_string: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execution information tied to the leader or the follower.
|
/// Execution information tied to the platform.
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
pub struct ExecutionInformation {
|
pub struct ExecutionInformation {
|
||||||
/// Information related to the node assigned to this test case.
|
/// Information related to the node assigned to this test case.
|
||||||
@@ -533,12 +510,12 @@ pub enum CompilationStatus {
|
|||||||
/// The path of the compiler used to compile the contracts.
|
/// The path of the compiler used to compile the contracts.
|
||||||
compiler_path: PathBuf,
|
compiler_path: PathBuf,
|
||||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
/// 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
|
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||||
/// cached and the compiler was invoked.
|
/// the compiler was invoked.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compiler_input: Option<CompilerInput>,
|
compiler_input: Option<CompilerInput>,
|
||||||
/// The output of the compiler. This is only included if the appropriate flag is set in the
|
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compiler_output: Option<CompilerOutput>,
|
compiler_output: Option<CompilerOutput>,
|
||||||
},
|
},
|
||||||
@@ -553,8 +530,8 @@ pub enum CompilationStatus {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compiler_path: Option<PathBuf>,
|
compiler_path: Option<PathBuf>,
|
||||||
/// The input provided to the compiler to compile the contracts. This is only included if
|
/// 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
|
/// the appropriate flag is set in the CLI context and if the contracts were not cached and
|
||||||
/// cached and the compiler was invoked.
|
/// the compiler was invoked.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compiler_input: Option<CompilerInput>,
|
compiler_input: Option<CompilerInput>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
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_compiler::Mode;
|
||||||
use revive_dt_format::{case::CaseIdx, input::StepIdx};
|
use revive_dt_format::{case::CaseIdx, steps::StepIdx};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
define_wrapper_type!(
|
define_wrapper_type!(
|
||||||
@@ -22,18 +22,12 @@ pub struct TestSpecifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An absolute path for a test that also includes information about the node that it's assigned to
|
/// 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)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct ExecutionSpecifier {
|
pub struct ExecutionSpecifier {
|
||||||
pub test_specifier: Arc<TestSpecifier>,
|
pub test_specifier: Arc<TestSpecifier>,
|
||||||
pub node_id: usize,
|
pub node_id: usize,
|
||||||
pub node_designation: NodeDesignation,
|
pub platform_identifier: PlatformIdentifier,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum NodeDesignation {
|
|
||||||
Leader,
|
|
||||||
Follower,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
|||||||
use alloy_primitives::Address;
|
use alloy_primitives::Address;
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
use revive_dt_compiler::{CompilerInput, CompilerOutput};
|
use revive_dt_compiler::{CompilerInput, CompilerOutput};
|
||||||
use revive_dt_config::TestingPlatform;
|
|
||||||
use revive_dt_format::metadata::Metadata;
|
use revive_dt_format::metadata::Metadata;
|
||||||
use revive_dt_format::{corpus::Corpus, metadata::ContractInstance};
|
use revive_dt_format::{corpus::Corpus, metadata::ContractInstance};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
@@ -412,14 +412,14 @@ macro_rules! define_event {
|
|||||||
pub fn execution_specific_reporter(
|
pub fn execution_specific_reporter(
|
||||||
&self,
|
&self,
|
||||||
node_id: impl Into<usize>,
|
node_id: impl Into<usize>,
|
||||||
node_designation: impl Into<$crate::common::NodeDesignation>
|
platform_identifier: impl Into<PlatformIdentifier>
|
||||||
) -> [< $ident ExecutionSpecificReporter >] {
|
) -> [< $ident ExecutionSpecificReporter >] {
|
||||||
[< $ident ExecutionSpecificReporter >] {
|
[< $ident ExecutionSpecificReporter >] {
|
||||||
reporter: self.reporter.clone(),
|
reporter: self.reporter.clone(),
|
||||||
execution_specifier: Arc::new($crate::common::ExecutionSpecifier {
|
execution_specifier: Arc::new($crate::common::ExecutionSpecifier {
|
||||||
test_specifier: self.test_specifier.clone(),
|
test_specifier: self.test_specifier.clone(),
|
||||||
node_id: node_id.into(),
|
node_id: node_id.into(),
|
||||||
node_designation: node_designation.into(),
|
platform_identifier: platform_identifier.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,7 +434,7 @@ macro_rules! define_event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A reporter that's tied to a specific execution of the test case such as execution on
|
/// 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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct [< $ident ExecutionSpecificReporter >] {
|
pub struct [< $ident ExecutionSpecificReporter >] {
|
||||||
$vis reporter: [< $ident Reporter >],
|
$vis reporter: [< $ident Reporter >],
|
||||||
@@ -520,25 +520,14 @@ define_event! {
|
|||||||
/// A reason for the failure of the test.
|
/// A reason for the failure of the test.
|
||||||
reason: String,
|
reason: String,
|
||||||
},
|
},
|
||||||
/// An event emitted when the test case is assigned a leader node.
|
/// An event emitted when the test case is assigned a platform node.
|
||||||
LeaderNodeAssigned {
|
NodeAssigned {
|
||||||
/// A specifier for the test that the assignment is for.
|
/// A specifier for the test that the assignment is for.
|
||||||
test_specifier: Arc<TestSpecifier>,
|
test_specifier: Arc<TestSpecifier>,
|
||||||
/// The ID of the node that this case is being executed on.
|
/// The ID of the node that this case is being executed on.
|
||||||
id: usize,
|
id: usize,
|
||||||
/// The platform of the node.
|
/// The identifier of the platform used.
|
||||||
platform: TestingPlatform,
|
platform_identifier: PlatformIdentifier,
|
||||||
/// 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 connection string of the node.
|
/// The connection string of the node.
|
||||||
connection_string: String,
|
connection_string: String,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ use std::{
|
|||||||
sync::LazyLock,
|
sync::LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use semver::Version;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::download::SolcDownloader;
|
use crate::download::SolcDownloader;
|
||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
pub const SOLC_CACHE_DIRECTORY: &str = "solc";
|
pub const SOLC_CACHE_DIRECTORY: &str = "solc";
|
||||||
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
|
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
|
||||||
@@ -20,7 +21,7 @@ pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new
|
|||||||
pub(crate) async fn get_or_download(
|
pub(crate) async fn get_or_download(
|
||||||
working_directory: &Path,
|
working_directory: &Path,
|
||||||
downloader: &SolcDownloader,
|
downloader: &SolcDownloader,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<(Version, PathBuf)> {
|
||||||
let target_directory = working_directory
|
let target_directory = working_directory
|
||||||
.join(SOLC_CACHE_DIRECTORY)
|
.join(SOLC_CACHE_DIRECTORY)
|
||||||
.join(downloader.version.to_string());
|
.join(downloader.version.to_string());
|
||||||
@@ -29,7 +30,7 @@ pub(crate) async fn get_or_download(
|
|||||||
let mut cache = SOLC_CACHER.lock().await;
|
let mut cache = SOLC_CACHER.lock().await;
|
||||||
if cache.contains(&target_file) {
|
if cache.contains(&target_file) {
|
||||||
tracing::debug!("using cached solc: {}", target_file.display());
|
tracing::debug!("using cached solc: {}", target_file.display());
|
||||||
return Ok(target_file);
|
return Ok((downloader.version.clone(), target_file));
|
||||||
}
|
}
|
||||||
|
|
||||||
create_dir_all(&target_directory).with_context(|| {
|
create_dir_all(&target_directory).with_context(|| {
|
||||||
@@ -48,7 +49,7 @@ pub(crate) async fn get_or_download(
|
|||||||
})?;
|
})?;
|
||||||
cache.insert(target_file.clone());
|
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<()> {
|
async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use semver::Version;
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::list::List;
|
use crate::list::List;
|
||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
pub static LIST_CACHE: LazyLock<Mutex<HashMap<&'static str, List>>> =
|
pub static LIST_CACHE: LazyLock<Mutex<HashMap<&'static str, List>>> =
|
||||||
LazyLock::new(Default::default);
|
LazyLock::new(Default::default);
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context as _;
|
||||||
use cache::get_or_download;
|
use cache::get_or_download;
|
||||||
use download::SolcDownloader;
|
use download::SolcDownloader;
|
||||||
|
|
||||||
use revive_dt_common::types::VersionOrRequirement;
|
use revive_dt_common::types::VersionOrRequirement;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
@@ -24,7 +25,7 @@ pub async fn download_solc(
|
|||||||
cache_directory: &Path,
|
cache_directory: &Path,
|
||||||
version: impl Into<VersionOrRequirement>,
|
version: impl Into<VersionOrRequirement>,
|
||||||
wasm: bool,
|
wasm: bool,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<(Version, PathBuf)> {
|
||||||
let downloader = if wasm {
|
let downloader = if wasm {
|
||||||
SolcDownloader::wasm(version).await
|
SolcDownloader::wasm(version).await
|
||||||
} else if cfg!(target_os = "linux") {
|
} else if cfg!(target_os = "linux") {
|
||||||
|
|||||||
+8
-6
@@ -89,13 +89,15 @@ echo "This may take a while..."
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Run the tool
|
# Run the tool
|
||||||
RUST_LOG="error" cargo run --release -- \
|
RUST_LOG="info" cargo run --release -- execute-tests \
|
||||||
|
--platform geth-evm-solc \
|
||||||
|
--platform revive-dev-node-polkavm-resolc \
|
||||||
--corpus "$CORPUS_FILE" \
|
--corpus "$CORPUS_FILE" \
|
||||||
--workdir "$WORKDIR" \
|
--working-directory "$WORKDIR" \
|
||||||
--number-of-nodes 5 \
|
--concurrency.number-of-nodes 5 \
|
||||||
--kitchensink "$SUBSTRATE_NODE_BIN" \
|
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
|
||||||
--revive-dev-node "$REVIVE_DEV_NODE_BIN" \
|
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
||||||
--eth_proxy "$ETH_RPC_BIN" \
|
--eth-rpc.path "$ETH_RPC_BIN" \
|
||||||
> logs.log \
|
> logs.log \
|
||||||
2> output.log
|
2> output.log
|
||||||
|
|
||||||
|
|||||||
+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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user