Compare commits

..

3 Commits

Author SHA1 Message Date
Omar Abdulla 9598bdbe35 Print to the stderr and print logs to stdout 2025-08-03 11:03:00 +03:00
Omar Abdulla 1b9eff1205 Add some waiting period to the printing task 2025-08-02 22:07:39 +03:00
Omar Abdulla c0ca716163 Added basic console reporting 2025-08-02 22:01:43 +03:00
46 changed files with 1429 additions and 3749 deletions
-2
View File
@@ -7,5 +7,3 @@ node_modules
# We do not want to commit any log files that we produce from running the code locally so this is # We do not want to commit any log files that we produce from running the code locally so this is
# added to the .gitignore file. # added to the .gitignore file.
*.log *.log
profile.json.gz
Generated
+9 -610
View File
@@ -659,7 +659,7 @@ checksum = "4f317d20f047b3de4d9728c556e2e9a92c9a507702d2016424cd8be13a74ca5e"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
"base64 0.22.1", "base64",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures", "futures",
"futures-utils-wasm", "futures-utils-wasm",
@@ -1189,151 +1189,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-global-executor"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
"async-channel 2.5.0",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
dependencies = [
"async-lock",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "async-lock"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
dependencies = [
"event-listener 5.4.1",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00"
dependencies = [
"async-channel 2.5.0",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.4.1",
"futures-lite",
"rustix",
]
[[package]]
name = "async-signal"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
]
[[package]]
name = "async-std"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
dependencies = [
"async-channel 1.9.0",
"async-global-executor",
"async-io",
"async-lock",
"async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@@ -1356,12 +1211,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.88" version = "0.1.88"
@@ -1417,12 +1266,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -1561,19 +1404,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel 2.5.0",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "blst" name = "blst"
version = "0.3.14" version = "0.3.14"
@@ -1607,29 +1437,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "bson"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e"
dependencies = [
"ahash",
"base64 0.22.1",
"bitvec",
"getrandom 0.2.16",
"getrandom 0.3.3",
"hex",
"indexmap 2.10.0",
"js-sys",
"once_cell",
"rand 0.9.2",
"serde",
"serde_bytes",
"serde_json",
"time",
"uuid",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.17.0" version = "3.17.0"
@@ -1672,32 +1479,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "cacache"
version = "13.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b"
dependencies = [
"async-std",
"digest 0.10.7",
"either",
"futures",
"hex",
"libc",
"memmap2",
"miette",
"reflink-copy",
"serde",
"serde_derive",
"serde_json",
"sha1",
"sha2 0.10.9",
"ssri",
"tempfile",
"thiserror 1.0.69",
"walkdir",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.25" version = "1.2.25"
@@ -1778,15 +1559,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "const-hex" name = "const-hex"
version = "1.14.1" version = "1.14.1"
@@ -1872,15 +1644,6 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@@ -2348,33 +2111,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener 5.4.1",
"pin-project-lite",
]
[[package]] [[package]]
name = "expander" name = "expander"
version = "2.2.1" version = "2.2.1"
@@ -2616,19 +2352,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.31"
@@ -2676,20 +2399,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9"
[[package]]
name = "generator"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -2708,10 +2417,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -2721,11 +2428,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -2750,18 +2455,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "group" name = "group"
version = "0.13.0" version = "0.13.0"
@@ -2856,12 +2549,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@@ -3014,7 +2701,7 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -3384,15 +3071,6 @@ dependencies = [
"sha3-asm", "sha3-asm",
] ]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -3418,7 +3096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"base64 0.22.1", "base64",
"digest 0.9.0", "digest 0.9.0",
"hmac-drbg", "hmac-drbg",
"libsecp256k1-core", "libsecp256k1-core",
@@ -3486,22 +3164,6 @@ name = "log"
version = "0.4.27" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
dependencies = [
"value-bag",
]
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "lru" name = "lru"
@@ -3538,15 +3200,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memory-db" name = "memory-db"
version = "0.32.0" version = "0.32.0"
@@ -3568,29 +3221,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "miette"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
dependencies = [
"miette-derive",
"once_cell",
"thiserror 1.0.69",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -3617,25 +3247,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "moka"
version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version 0.4.1",
"smallvec",
"tagptr",
"thiserror 1.0.69",
"uuid",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.14" version = "0.2.14"
@@ -3720,7 +3331,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [ dependencies = [
"hermit-abi 0.3.9", "hermit-abi",
"libc", "libc",
] ]
@@ -3877,12 +3488,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.4" version = "0.12.4"
@@ -3988,17 +3593,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pkcs8" name = "pkcs8"
version = "0.10.2" version = "0.10.2"
@@ -4052,27 +3646,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "polling"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.5.2",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.2"
@@ -4354,18 +3927,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "reflink-copy"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c81d000a2c524133cc00d2f92f019d399e57906c3b7119271a2495354fe895"
dependencies = [
"cfg-if",
"libc",
"rustix",
"windows",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@@ -4416,7 +3977,7 @@ version = "0.12.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -4468,11 +4029,7 @@ name = "revive-dt-common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"moka",
"once_cell",
"semver 1.0.26", "semver 1.0.26",
"serde",
"tokio",
] ]
[[package]] [[package]]
@@ -4482,7 +4039,6 @@ dependencies = [
"alloy", "alloy",
"alloy-primitives", "alloy-primitives",
"anyhow", "anyhow",
"dashmap",
"foundry-compilers-artifacts", "foundry-compilers-artifacts",
"revive-common", "revive-common",
"revive-dt-common", "revive-dt-common",
@@ -4513,12 +4069,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"bson",
"cacache",
"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",
@@ -4527,13 +4080,9 @@ dependencies = [
"revive-dt-node-interaction", "revive-dt-node-interaction",
"revive-dt-report", "revive-dt-report",
"semver 1.0.26", "semver 1.0.26",
"serde",
"serde_json",
"temp-dir", "temp-dir",
"tempfile",
"tokio", "tokio",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -4545,9 +4094,6 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"anyhow", "anyhow",
"futures",
"regex",
"revive-common",
"revive-dt-common", "revive-dt-common",
"semver 1.0.26", "semver 1.0.26",
"serde", "serde",
@@ -4562,7 +4108,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"revive-common",
"revive-dt-common", "revive-dt-common",
"revive-dt-config", "revive-dt-config",
"revive-dt-format", "revive-dt-format",
@@ -4589,12 +4134,12 @@ name = "revive-dt-report"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"revive-dt-common",
"revive-dt-compiler", "revive-dt-compiler",
"revive-dt-config", "revive-dt-config",
"revive-dt-format", "revive-dt-format",
"serde", "serde",
"serde_json", "serde_json",
"tracing",
] ]
[[package]] [[package]]
@@ -4875,12 +4420,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -5035,7 +4574,6 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"indexmap 2.10.0",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@@ -5079,7 +4617,7 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
@@ -5113,28 +4651,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha-1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.9.9" version = "0.9.9"
@@ -5609,23 +5125,6 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "ssri"
version = "9.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082"
dependencies = [
"base64 0.21.7",
"digest 0.10.7",
"hex",
"miette",
"serde",
"sha-1",
"sha2 0.10.9",
"thiserror 1.0.69",
"xxhash-rust",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@@ -5773,12 +5272,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"
@@ -6110,18 +5603,6 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror 1.0.69",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.28"
@@ -6288,12 +5769,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -6329,30 +5804,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@@ -6608,28 +6065,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -6643,17 +6078,6 @@ dependencies = [
"windows-strings 0.4.2", "windows-strings 0.4.2",
] ]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.0"
@@ -6678,19 +6102,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.3" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
@@ -6780,15 +6194,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.0", "windows_x86_64_msvc 0.53.0",
] ]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -6918,12 +6323,6 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "1.0.1" version = "1.0.1"
+2 -12
View File
@@ -8,7 +8,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
edition = "2024" edition = "2024"
repository = "https://github.com/paritytech/revive-differential-testing.git" repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.87.0" rust-version = "1.85.0"
[workspace.dependencies] [workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" } revive-dt-common = { version = "0.1.0", path = "crates/common" }
@@ -25,15 +25,10 @@ revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
alloy-primitives = "1.2.1" alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1" alloy-sol-types = "1.2.1"
anyhow = "1.0" anyhow = "1.0"
bson = { version = "2.15.0" }
cacache = { version = "13.1.0" }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
dashmap = { version = "6.1.0" }
foundry-compilers-artifacts = { version = "0.18.0" } foundry-compilers-artifacts = { version = "0.18.0" }
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
hex = "0.4.3" hex = "0.4.3"
regex = "1"
moka = "0.12.10"
reqwest = { version = "0.12.15", features = ["json"] } reqwest = { version = "0.12.15", features = ["json"] }
once_cell = "1.21" once_cell = "1.21"
semver = { version = "1.0", features = ["serde"] } semver = { version = "1.0", features = ["serde"] }
@@ -41,22 +36,19 @@ 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 = [
"arbitrary_precision", "arbitrary_precision",
"std", "std",
"unbounded_depth",
] } ] }
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"
temp-dir = { version = "0.1.16" } temp-dir = { version = "0.1.16" }
tempfile = "3.3" tempfile = "3.3"
thiserror = "2"
tokio = { version = "1.47.0", default-features = false, features = [ tokio = { version = "1.47.0", default-features = false, features = [
"rt-multi-thread", "rt-multi-thread",
"process", "process",
"rt", "rt",
] } ] }
uuid = { version = "1.8", features = ["v4"] } uuid = { version = "1.8", features = ["v4"] }
tracing = { version = "0.1.41" } tracing = "0.1.41"
tracing-appender = { version = "0.2.3" }
tracing-subscriber = { version = "0.3.19", default-features = false, features = [ tracing-subscriber = { version = "0.3.19", default-features = false, features = [
"fmt", "fmt",
"json", "json",
@@ -91,5 +83,3 @@ features = [
inherits = "release" inherits = "release"
lto = true lto = true
codegen-units = 1 codegen-units = 1
[workspace.lints.clippy]
+2 -13
View File
@@ -1,24 +1,13 @@
{ {
"modes": [ "modes": [
"Y >=0.8.9", "Y >=0.8.9",
"E" "E",
"I"
], ],
"cases": [ "cases": [
{ {
"name": "first", "name": "first",
"inputs": [ "inputs": [
{
"address": "0xdeadbeef00000000000000000000000000000042",
"expected_balance": "1233"
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": true
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": false
},
{ {
"instance": "WBTC_1", "instance": "WBTC_1",
"method": "#deployer", "method": "#deployer",
-1
View File
@@ -1 +0,0 @@
-7
View File
@@ -10,11 +10,4 @@ rust-version.workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] }
[lints]
workspace = true
-49
View File
@@ -1,49 +0,0 @@
//! This module implements a cached file system allowing for results to be stored in-memory rather
//! rather being queried from the file system again.
use std::fs;
use std::io::{Error, Result};
use std::path::{Path, PathBuf};
use moka::sync::Cache;
use once_cell::sync::Lazy;
pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
static READ_CACHE: Lazy<Cache<PathBuf, Vec<u8>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_CACHE.get(path.as_path()) {
Some(content) => Ok(content),
None => {
let content = fs::read(path.as_path())?;
READ_CACHE.insert(path, content.clone());
Ok(content)
}
}
}
pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
let content = read(path)?;
String::from_utf8(content).map_err(|_| {
Error::new(
std::io::ErrorKind::InvalidData,
"The contents of the file are not valid UTF8",
)
})
}
pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result<PathBuf>>>> {
static READ_DIR_CACHE: Lazy<Cache<PathBuf, Vec<PathBuf>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_DIR_CACHE.get(path.as_path()) {
Some(entries) => Ok(Box::new(entries.into_iter().map(Ok)) as Box<_>),
None => {
let entries = fs::read_dir(path.as_path())?
.flat_map(|maybe_entry| maybe_entry.map(|entry| entry.path()))
.collect();
READ_DIR_CACHE.insert(path.clone(), entries);
Ok(read_dir(path).unwrap())
}
}
}
-3
View File
@@ -1,3 +0,0 @@
mod poll;
pub use poll::*;
-69
View File
@@ -1,69 +0,0 @@
use std::ops::ControlFlow;
use std::time::Duration;
use anyhow::{Result, anyhow};
const EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION: Duration = Duration::from_secs(60);
/// A function that polls for a fallible future for some period of time and errors if it fails to
/// get a result after polling.
///
/// Given a future that returns a [`Result<ControlFlow<O, ()>>`], this function calls the future
/// repeatedly (with some wait period) until the future returns a [`ControlFlow::Break`] or until it
/// returns an [`Err`] in which case the function stops polling and returns the error.
///
/// If the future keeps returning [`ControlFlow::Continue`] and fails to return a [`Break`] within
/// the permitted polling duration then this function returns an [`Err`]
///
/// [`Break`]: ControlFlow::Break
/// [`Continue`]: ControlFlow::Continue
pub async fn poll<F, O>(
polling_duration: Duration,
polling_wait_behavior: PollingWaitBehavior,
mut future: impl FnMut() -> F,
) -> Result<O>
where
F: Future<Output = Result<ControlFlow<O, ()>>>,
{
let mut retries = 0;
let mut total_wait_duration = Duration::ZERO;
let max_allowed_wait_duration = polling_duration;
loop {
if total_wait_duration >= max_allowed_wait_duration {
break Err(anyhow!(
"Polling failed after {} retries and a total of {:?} of wait time",
retries,
total_wait_duration
));
}
match future().await? {
ControlFlow::Continue(()) => {
let next_wait_duration = match polling_wait_behavior {
PollingWaitBehavior::Constant(duration) => duration,
PollingWaitBehavior::ExponentialBackoff => {
Duration::from_secs(2u64.pow(retries))
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION)
}
};
let next_wait_duration =
next_wait_duration.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
ControlFlow::Break(output) => {
break Ok(output);
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum PollingWaitBehavior {
Constant(Duration),
#[default]
ExponentialBackoff,
}
@@ -1,8 +1,4 @@
use std::{ use std::{borrow::Cow, collections::HashSet, path::PathBuf};
borrow::Cow,
collections::HashSet,
path::{Path, PathBuf},
};
/// An iterator that finds files of a certain extension in the provided directory. You can think of /// An iterator that finds files of a certain extension in the provided directory. You can think of
/// this a glob pattern similar to: `${path}/**/*.md` /// this a glob pattern similar to: `${path}/**/*.md`
@@ -19,20 +15,14 @@ pub struct FilesWithExtensionIterator {
/// this vector then they will be returned when the [`Iterator::next`] method is called. If not /// this vector then they will be returned when the [`Iterator::next`] method is called. If not
/// then we visit one of the next directories to visit. /// then we visit one of the next directories to visit.
files_matching_allowed_extensions: Vec<PathBuf>, files_matching_allowed_extensions: Vec<PathBuf>,
/// This option controls if the the cached file system should be used or not. This could be
/// better for certain cases where the entries in the directories do not change and therefore
/// caching can be used.
use_cached_fs: bool,
} }
impl FilesWithExtensionIterator { impl FilesWithExtensionIterator {
pub fn new(root_directory: impl AsRef<Path>) -> Self { pub fn new(root_directory: PathBuf) -> Self {
Self { Self {
allowed_extensions: Default::default(), allowed_extensions: Default::default(),
directories_to_search: vec![root_directory.as_ref().to_path_buf()], directories_to_search: vec![root_directory],
files_matching_allowed_extensions: Default::default(), files_matching_allowed_extensions: Default::default(),
use_cached_fs: Default::default(),
} }
} }
@@ -43,11 +33,6 @@ impl FilesWithExtensionIterator {
self.allowed_extensions.insert(allowed_extension.into()); self.allowed_extensions.insert(allowed_extension.into());
self self
} }
pub fn with_use_cached_fs(mut self, use_cached_fs: bool) -> Self {
self.use_cached_fs = use_cached_fs;
self
}
} }
impl Iterator for FilesWithExtensionIterator { impl Iterator for FilesWithExtensionIterator {
@@ -60,19 +45,16 @@ impl Iterator for FilesWithExtensionIterator {
let directory_to_search = self.directories_to_search.pop()?; let directory_to_search = self.directories_to_search.pop()?;
let iterator = if self.use_cached_fs { // Read all of the entries in the directory. If we failed to read this dir's entires then we
let Ok(dir_entries) = crate::cached_fs::read_dir(directory_to_search.as_path()) else { // elect to just ignore it and look in the next directory, we do that by calling the next
return self.next(); // method again on the iterator, which is an intentional decision that we made here instead
}; // of panicking.
Box::new(dir_entries) as Box<dyn Iterator<Item = std::io::Result<PathBuf>>>
} else {
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else { let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else {
return self.next(); return self.next();
}; };
Box::new(dir_entries.map(|maybe_entry| maybe_entry.map(|entry| entry.path()))) as Box<_>
};
for entry_path in iterator.flatten() { for entry in dir_entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() { if entry_path.is_dir() {
self.directories_to_search.push(entry_path) self.directories_to_search.push(entry_path)
} else if entry_path.is_file() } else if entry_path.is_file()
-2
View File
@@ -1,9 +1,7 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in //! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from. //! the workspace can benefit from.
pub mod cached_fs;
pub mod fs; pub mod fs;
pub mod futures;
pub mod iterators; pub mod iterators;
pub mod macros; pub mod macros;
pub mod types; pub mod types;
@@ -1,14 +1,3 @@
#[macro_export]
macro_rules! impl_for_wrapper {
(Display, $ident: ident) => {
impl std::fmt::Display for $ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
};
}
/// Defines wrappers around types. /// Defines wrappers around types.
/// ///
/// For example, the macro invocation seen below: /// For example, the macro invocation seen below:
@@ -53,13 +42,7 @@ macro_rules! impl_for_wrapper {
macro_rules! define_wrapper_type { macro_rules! define_wrapper_type {
( (
$(#[$meta: meta])* $(#[$meta: meta])*
$vis:vis struct $ident: ident($ty: ty) $vis:vis struct $ident: ident($ty: ty);
$(
impl $($trait_ident: ident),*
)?
;
) => { ) => {
$(#[$meta])* $(#[$meta])*
$vis struct $ident($ty); $vis struct $ident($ty);
@@ -115,15 +98,9 @@ macro_rules! define_wrapper_type {
value.0 value.0
} }
} }
$(
$(
$crate::macros::impl_for_wrapper!($trait_ident, $ident);
)*
)?
}; };
} }
/// Technically not needed but this allows for the macro to be found in the `macros` module of the /// Technically not needed but this allows for the macro to be found in the `macros` module of the
/// crate in addition to being found in the root of the crate. /// crate in addition to being found in the root of the crate.
pub use {define_wrapper_type, impl_for_wrapper}; pub use define_wrapper_type;
-2
View File
@@ -1,5 +1,3 @@
mod mode;
mod version_or_requirement; mod version_or_requirement;
pub use mode::*;
pub use version_or_requirement::*; pub use version_or_requirement::*;
-167
View File
@@ -1,167 +0,0 @@
use crate::types::VersionOrRequirement;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
/// This represents a mode that a given test should be run with, if possible.
///
/// We obtain this by taking a [`ParsedMode`], which may be looser or more strict
/// in its requirements, and then expanding it out into a list of [`Mode`]s.
///
/// Use [`ParsedMode::to_test_modes()`] to do this.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Mode {
pub pipeline: ModePipeline,
pub optimize_setting: ModeOptimizerSetting,
pub version: Option<semver::VersionReq>,
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.pipeline.fmt(f)?;
f.write_str(" ")?;
self.optimize_setting.fmt(f)?;
if let Some(version) = &self.version {
f.write_str(" ")?;
version.fmt(f)?;
}
Ok(())
}
}
impl Mode {
/// Return all of the available mode combinations.
pub fn all() -> impl Iterator<Item = Mode> {
ModePipeline::test_cases().flat_map(|pipeline| {
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: None,
})
})
}
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
/// the requirement is present on the object. Otherwise, the passed default version is used.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
match self.version {
Some(ref requirement) => requirement.clone().into(),
None => default.into(),
}
}
}
/// What do we want the compiler to do?
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModePipeline {
/// Compile Solidity code via Yul IR
ViaYulIR,
/// Compile Solidity direct to assembly
ViaEVMAssembly,
}
impl FromStr for ModePipeline {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
// via Yul IR
"Y" => Ok(ModePipeline::ViaYulIR),
// Don't go via Yul IR
"E" => Ok(ModePipeline::ViaEVMAssembly),
// Anything else that we see isn't a mode at all
_ => Err(anyhow::anyhow!(
"Unsupported pipeline '{s}': expected 'Y' or 'E'"
)),
}
}
}
impl Display for ModePipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModePipeline::ViaYulIR => f.write_str("Y"),
ModePipeline::ViaEVMAssembly => f.write_str("E"),
}
}
}
impl ModePipeline {
/// Should we go via Yul IR?
pub fn via_yul_ir(&self) -> bool {
matches!(self, ModePipeline::ViaYulIR)
}
/// An iterator over the available pipelines that we'd like to test,
/// when an explicit pipeline was not specified.
pub fn test_cases() -> impl Iterator<Item = ModePipeline> + Clone {
[ModePipeline::ViaYulIR, ModePipeline::ViaEVMAssembly].into_iter()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModeOptimizerSetting {
/// 0 / -: Don't apply any optimizations
M0,
/// 1: Apply less than default optimizations
M1,
/// 2: Apply the default optimizations
M2,
/// 3 / +: Apply aggressive optimizations
M3,
/// s: Optimize for size
Ms,
/// z: Aggressively optimize for size
Mz,
}
impl FromStr for ModeOptimizerSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"M0" => Ok(ModeOptimizerSetting::M0),
"M1" => Ok(ModeOptimizerSetting::M1),
"M2" => Ok(ModeOptimizerSetting::M2),
"M3" => Ok(ModeOptimizerSetting::M3),
"Ms" => Ok(ModeOptimizerSetting::Ms),
"Mz" => Ok(ModeOptimizerSetting::Mz),
_ => Err(anyhow::anyhow!(
"Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'"
)),
}
}
}
impl Display for ModeOptimizerSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeOptimizerSetting::M0 => f.write_str("M0"),
ModeOptimizerSetting::M1 => f.write_str("M1"),
ModeOptimizerSetting::M2 => f.write_str("M2"),
ModeOptimizerSetting::M3 => f.write_str("M3"),
ModeOptimizerSetting::Ms => f.write_str("Ms"),
ModeOptimizerSetting::Mz => f.write_str("Mz"),
}
}
}
impl ModeOptimizerSetting {
/// An iterator over the available optimizer settings that we'd like to test,
/// when an explicit optimizer setting was not specified.
pub fn test_cases() -> impl Iterator<Item = ModeOptimizerSetting> + Clone {
[
// No optimizations:
ModeOptimizerSetting::M0,
// Aggressive optimizations:
ModeOptimizerSetting::M3,
]
.into_iter()
}
/// Are any optimizations enabled?
pub fn optimizations_enabled(&self) -> bool {
!matches!(self, ModeOptimizerSetting::M0)
}
}
-4
View File
@@ -18,13 +18,9 @@ revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
dashmap = { workspace = true }
foundry-compilers-artifacts = { workspace = true } foundry-compilers-artifacts = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
-4
View File
@@ -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);
+10 -52
View File
@@ -3,10 +3,9 @@
//! - 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,
fs::read_to_string,
hash::Hash, hash::Hash,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -17,13 +16,9 @@ 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::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
// Re-export this as it's a part of the compiler interface.
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
pub mod revive_js; pub mod revive_js;
pub mod revive_resolc; pub mod revive_resolc;
pub mod solc; pub mod solc;
@@ -47,27 +42,19 @@ pub trait SolidityCompiler {
version: impl Into<VersionOrRequirement>, version: impl Into<VersionOrRequirement>,
) -> impl Future<Output = anyhow::Result<PathBuf>>; ) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> impl Future<Output = anyhow::Result<Version>>; fn version(&self) -> anyhow::Result<Version>;
/// Does the compiler support the provided mode and version settings?
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
} }
/// The generic compilation input configuration. /// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerInput { pub struct CompilerInput {
pub pipeline: Option<ModePipeline>, pub enable_optimization: Option<bool>,
pub optimization: Option<ModeOptimizerSetting>, pub via_ir: Option<bool>,
pub evm_version: Option<EVMVersion>, pub evm_version: Option<EVMVersion>,
pub allow_paths: Vec<PathBuf>, pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
pub sources: HashMap<PathBuf, String>, pub sources: HashMap<PathBuf, String>,
pub libraries: HashMap<PathBuf, HashMap<String, Address>>, pub libraries: HashMap<PathBuf, HashMap<String, Address>>,
pub revert_string_handling: Option<RevertString>,
} }
/// The generic compilation output configuration. /// The generic compilation output configuration.
@@ -97,26 +84,25 @@ where
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
input: CompilerInput { input: CompilerInput {
pipeline: Default::default(), enable_optimization: Default::default(),
optimization: Default::default(), via_ir: Default::default(),
evm_version: Default::default(), evm_version: Default::default(),
allow_paths: Default::default(), allow_paths: Default::default(),
base_path: Default::default(), base_path: Default::default(),
sources: Default::default(), sources: Default::default(),
libraries: Default::default(), libraries: Default::default(),
revert_string_handling: Default::default(),
}, },
additional_options: T::Options::default(), additional_options: T::Options::default(),
} }
} }
pub fn with_optimization(mut self, value: impl Into<Option<ModeOptimizerSetting>>) -> Self { pub fn with_optimization(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.optimization = value.into(); self.input.enable_optimization = value.into();
self self
} }
pub fn with_pipeline(mut self, value: impl Into<Option<ModePipeline>>) -> Self { pub fn with_via_ir(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.pipeline = value.into(); self.input.via_ir = value.into();
self self
} }
@@ -156,27 +142,11 @@ where
self self
} }
pub fn with_revert_string_handling(
mut self,
revert_string_handling: impl Into<Option<RevertString>>,
) -> Self {
self.input.revert_string_handling = revert_string_handling.into();
self
}
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self { pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
self.additional_options = options.into(); self.additional_options = options.into();
self self
} }
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
callback(self)
}
pub fn try_then<E>(self, callback: impl FnOnce(Self) -> Result<Self, E>) -> Result<Self, E> {
callback(self)
}
pub async fn try_build( pub async fn try_build(
self, self,
compiler_path: impl AsRef<Path>, compiler_path: impl AsRef<Path>,
@@ -190,15 +160,3 @@ where
self.input.clone() self.input.clone()
} }
} }
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
+10 -48
View File
@@ -4,10 +4,8 @@
use std::{ use std::{
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
sync::LazyLock,
}; };
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_solc_json_interface::{ use revive_solc_json_interface::{
@@ -16,8 +14,7 @@ use revive_solc_json_interface::{
SolcStandardJsonOutput, SolcStandardJsonOutput,
}; };
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR; use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use anyhow::Context; use anyhow::Context;
@@ -42,25 +39,17 @@ impl SolidityCompiler for Resolc {
async fn build( async fn build(
&self, &self,
CompilerInput { CompilerInput {
pipeline, enable_optimization,
optimization, // Ignored and not honored since this is required for the resolc compilation.
via_ir: _via_ir,
evm_version, evm_version,
allow_paths, allow_paths,
base_path, base_path,
sources, sources,
libraries, libraries,
// TODO: this is currently not being handled since there is no way to pass it into
// resolc. So, we need to go back to this later once it's supported.
revert_string_handling: _,
}: CompilerInput, }: CompilerInput,
additional_options: Self::Options, additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> { ) -> anyhow::Result<CompilerOutput> {
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
anyhow::bail!(
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
);
}
let input = SolcStandardJsonInput { let input = SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity, language: SolcStandardJsonInputLanguage::Solidity,
sources: sources sources: sources
@@ -89,9 +78,7 @@ impl SolidityCompiler for Resolc {
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()), output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
via_ir: Some(true), via_ir: Some(true),
optimizer: SolcStandardJsonInputSettingsOptimizer::new( optimizer: SolcStandardJsonInputSettingsOptimizer::new(
optimization enable_optimization.unwrap_or(false),
.unwrap_or(ModeOptimizerSetting::M0)
.optimizations_enabled(),
None, None,
&Version::new(0, 0, 0), &Version::new(0, 0, 0),
false, false,
@@ -221,23 +208,16 @@ impl SolidityCompiler for Resolc {
Ok(PathBuf::from("resolc")) Ok(PathBuf::from("resolc"))
} }
async fn version(&self) -> anyhow::Result<semver::Version> { 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 // Logic for parsing the resolc version from the following string:
/// choose to cache the version in this way rather than through a field on the struct since // Solidity frontend for the revive compiler version 0.3.0+commit.b238913.llvm-18.1.8
/// 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()) let output = Command::new(self.resolc_path.as_path())
.arg("--version") .arg("--version")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn()? .spawn()?
.wait_with_output()? .wait_with_output()?
.stdout; .stdout;
let output = String::from_utf8_lossy(&output); let output = String::from_utf8_lossy(&output);
let version_string = output let version_string = output
.split("version ") .split("version ")
@@ -247,25 +227,7 @@ impl SolidityCompiler for Resolc {
.next() .next()
.context("Version parsing failed")?; .context("Version parsing failed")?;
let version = Version::parse(version_string)?; Version::parse(version_string).map_err(Into::into)
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
} }
} }
@@ -283,7 +245,7 @@ mod test {
let compiler = Resolc::new(path); let compiler = Resolc::new(path);
// Act // Act
let version = compiler.version().await; let version = compiler.version();
// Assert // Assert
let _ = version.expect("Failed to get version"); let _ = version.expect("Failed to get version");
+12 -79
View File
@@ -4,16 +4,13 @@
use std::{ use std::{
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
sync::LazyLock,
}; };
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
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, SolidityCompiler};
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use anyhow::Context; use anyhow::Context;
use foundry_compilers_artifacts::{ use foundry_compilers_artifacts::{
@@ -38,28 +35,16 @@ impl SolidityCompiler for Solc {
async fn build( async fn build(
&self, &self,
CompilerInput { CompilerInput {
pipeline, enable_optimization,
optimization, via_ir,
evm_version, evm_version,
allow_paths, allow_paths,
base_path, base_path,
sources, sources,
libraries, libraries,
revert_string_handling,
}: CompilerInput, }: CompilerInput,
_: Self::Options, _: Self::Options,
) -> anyhow::Result<CompilerOutput> { ) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version().await? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
// Be careful to entirely omit the viaIR field if the compiler does not support it,
// as it will error if you provide fields it does not know about. Because
// `supports_mode` is called prior to instantiating a compiler, we should never
// ask for something which is invalid.
let via_ir = match (pipeline, compiler_supports_via_ir) {
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
(_pipeline, false) => None,
};
let input = SolcInput { let input = SolcInput {
language: SolcLanguage::Solidity, language: SolcLanguage::Solidity,
sources: Sources( sources: Sources(
@@ -70,7 +55,7 @@ impl SolidityCompiler for Solc {
), ),
settings: Settings { settings: Settings {
optimizer: Optimizer { optimizer: Optimizer {
enabled: optimization.map(|o| o.optimizations_enabled()), enabled: enable_optimization,
details: Some(Default::default()), details: Some(Default::default()),
..Default::default() ..Default::default()
}, },
@@ -102,15 +87,6 @@ impl SolidityCompiler for Solc {
}) })
.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()
}, },
}; };
@@ -211,22 +187,14 @@ impl SolidityCompiler for Solc {
Ok(path) Ok(path)
} }
async fn version(&self) -> anyhow::Result<semver::Version> { 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 // The following is the parsing code for the version from the solc version strings which
/// choose to cache the version in this way rather than through a field on the struct since // look like the following:
/// 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 // solc, the solidity compiler commandline interface
// Version: 0.8.30+commit.73712a01.Darwin.appleclang // Version: 0.8.30+commit.73712a01.Darwin.appleclang
// ``` // ```
let child = Command::new(self.solc_path.as_path()) let child = Command::new(self.solc_path.as_path())
.arg("--version") .arg("--version")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@@ -242,25 +210,7 @@ impl SolidityCompiler for Solc {
.next() .next()
.context("Version parsing failed")?; .context("Version parsing failed")?;
let version = Version::parse(version_string)?; Version::parse(version_string).map_err(Into::into)
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
pipeline == ModePipeline::ViaEVMAssembly
|| (pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
} }
} }
@@ -272,13 +222,15 @@ mod test {
async fn compiler_version_can_be_obtained() { async fn compiler_version_can_be_obtained() {
// Arrange // Arrange
let args = Arguments::default(); let args = Arguments::default();
println!("Getting compiler path");
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6)) let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await .await
.unwrap(); .unwrap();
println!("Got compiler path");
let compiler = Solc::new(path); let compiler = Solc::new(path);
// Act // Act
let version = compiler.version().await; let version = compiler.version();
// Assert // Assert
assert_eq!( assert_eq!(
@@ -286,23 +238,4 @@ mod test {
Version::new(0, 7, 6) 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
View File
@@ -11,6 +11,7 @@ async fn contracts_can_be_compiled_with_solc() {
let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30)) let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30))
.await .await
.unwrap(); .unwrap();
println!("About to assert");
// Act // Act
let output = Compiler::<Solc>::new() let output = Compiler::<Solc>::new()
-2
View File
@@ -15,5 +15,3 @@ semver = { workspace = true }
temp-dir = { workspace = true } temp-dir = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
[lints]
workspace = true
+7 -23
View File
@@ -58,6 +58,10 @@ pub struct Arguments {
#[arg(long = "geth-start-timeout", default_value = "5000")] #[arg(long = "geth-start-timeout", default_value = "5000")]
pub geth_start_timeout: u64, pub geth_start_timeout: u64,
/// The test network chain ID.
#[arg(short, long = "network-id", default_value = "420420420")]
pub network_id: u64,
/// Configure nodes according to this genesis.json file. /// Configure nodes according to this genesis.json file.
#[arg(long = "genesis", default_value = "genesis.json")] #[arg(long = "genesis", default_value = "genesis.json")]
pub genesis_file: PathBuf, pub genesis_file: PathBuf,
@@ -73,7 +77,7 @@ pub struct Arguments {
/// 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)] #[arg(long = "private-keys-count", default_value_t = 15_000)]
pub private_keys_to_add: usize, pub private_keys_to_add: usize,
/// The differential testing leader node implementation. /// The differential testing leader node implementation.
@@ -92,19 +96,10 @@ pub struct Arguments {
#[arg(long, default_value = "1")] #[arg(long, default_value = "1")]
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 threads that will will be used.
#[arg( #[arg(long, default_value = "12")]
long,
default_value_t = std::thread::available_parallelism()
.map(|n| n.get())
.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.
#[arg(long)]
pub number_concurrent_tasks: Option<usize>,
/// Extract problems back to the test corpus. /// Extract problems back to the test corpus.
#[arg(short, long = "extract-problems")] #[arg(short, long = "extract-problems")]
pub extract_problems: bool, pub extract_problems: bool,
@@ -120,10 +115,6 @@ pub struct Arguments {
/// By default it uses `eth-rpc` binary found in `$PATH`. /// By default it uses `eth-rpc` binary found in `$PATH`.
#[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")] #[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")]
pub eth_proxy: PathBuf, pub eth_proxy: PathBuf,
/// Controls if the compilation cache should be invalidated or not.
#[arg(short, long)]
pub invalidate_compilation_cache: bool,
} }
impl Arguments { impl Arguments {
@@ -143,13 +134,6 @@ impl Arguments {
panic!("should have a workdir configured") 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], /// Try to parse `self.account` into a [PrivateKeySigner],
/// panicing on error. /// panicing on error.
pub fn wallet(&self) -> EthereumWallet { pub fn wallet(&self) -> EthereumWallet {
-10
View File
@@ -23,21 +23,11 @@ revive-dt-report = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
bson = { workspace = true }
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-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
temp-dir = { workspace = true } temp-dir = { workspace = true }
tempfile = { workspace = true }
[lints]
workspace = true
-262
View File
@@ -1,262 +0,0 @@
//! A wrapper around the compiler which allows for caching of compilation artifacts so that they can
//! be reused between runs.
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use futures::FutureExt;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
use anyhow::{Error, Result};
use once_cell::sync::Lazy;
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tracing::{Instrument, debug, debug_span, instrument};
use crate::Platform;
pub struct CachedCompiler(ArtifactsCache);
impl CachedCompiler {
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
let mut cache = ArtifactsCache::new(path);
if invalidate_cache {
cache = cache.with_invalidated_cache().await?;
}
Ok(Self(cache))
}
/// Compiles or gets the compilation artifacts from the cache.
#[instrument(
level = "debug",
skip_all,
fields(
metadata_file_path = %metadata_file_path.as_ref().display(),
%mode,
platform = P::config_id().to_string()
),
err
)]
pub async fn compile_contracts<P: Platform>(
&self,
metadata: &Metadata,
metadata_file_path: impl AsRef<Path>,
mode: &Mode,
config: &Arguments,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
) -> 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?;
let compiler_version = <P::Compiler as SolidityCompiler>::new(compiler_path.clone())
.version()
.await?;
let cache_key = CacheKey {
platform_key: P::config_id().to_string(),
compiler_version: compiler_version.clone(),
metadata_file_path: metadata_file_path.as_ref().to_path_buf(),
solc_mode: mode.clone(),
};
let compilation_callback = || {
async move {
compile_contracts::<P>(
metadata.directory()?,
compiler_path,
metadata.files_to_compile()?,
mode,
deployed_libraries,
)
.map(|compilation_result| compilation_result.map(CacheValue::new))
.await
}
.instrument(debug_span!(
"Running compilation for the cache key",
cache_key.platform_key = %cache_key.platform_key,
cache_key.compiler_version = %cache_key.compiler_version,
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
cache_key.solc_mode = %cache_key.solc_mode,
))
};
let compiled_contracts = match deployed_libraries {
// If deployed libraries have been specified then we will re-compile the contract as it
// means that linking is required in this case.
Some(_) => {
debug!("Deployed libraries defined, recompilation must take place");
debug!("Cache miss");
compilation_callback().await?.compiler_output
}
// If no deployed libraries are specified then we can follow the cached flow and attempt
// to lookup the compilation artifacts in the cache.
None => {
debug!("Deployed libraries undefined, attempting to make use of cache");
// Lock this specific cache key such that we do not get inconsistent state. We want
// that when multiple cases come in asking for the compilation artifacts then they
// don't all trigger a compilation if there's a cache miss. Hence, the lock here.
let read_guard = CACHE_KEY_LOCK.read().await;
let mutex = match read_guard.get(&cache_key).cloned() {
Some(value) => value,
None => {
drop(read_guard);
CACHE_KEY_LOCK
.write()
.await
.entry(cache_key.clone())
.or_default()
.clone()
}
};
let _guard = mutex.lock().await;
self.0
.get_or_insert_with(&cache_key, compilation_callback)
.await
.map(|value| value.compiler_output)?
}
};
Ok((compiled_contracts, compiler_version))
}
}
async fn compile_contracts<P: Platform>(
metadata_directory: impl AsRef<Path>,
compiler_path: impl AsRef<Path>,
mut files_to_compile: impl Iterator<Item = PathBuf>,
mode: &Mode,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
) -> Result<CompilerOutput> {
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
.with_allowed_extension("sol")
.with_use_cached_fs(true)
.collect::<Vec<_>>();
Compiler::<P::Compiler>::new()
.with_allow_path(metadata_directory)
// Handling the modes
.with_optimization(mode.optimize_setting)
.with_pipeline(mode.pipeline)
// Adding the contract sources to the compiler.
.try_then(|compiler| {
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
})?
// Adding the deployed libraries to the compiler.
.then(|compiler| {
deployed_libraries
.iter()
.flat_map(|value| value.iter())
.map(|(instance, (ident, address, abi))| (instance, ident, address, abi))
.flat_map(|(_, ident, address, _)| {
all_sources_in_dir
.iter()
.map(move |path| (ident, address, path))
})
.fold(compiler, |compiler, (ident, address, path)| {
compiler.with_library(path, ident.as_str(), *address)
})
})
.try_build(compiler_path)
.await
}
struct ArtifactsCache {
path: PathBuf,
}
impl ArtifactsCache {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
#[instrument(level = "debug", skip_all, err)]
pub async fn with_invalidated_cache(self) -> Result<Self> {
cacache::clear(self.path.as_path())
.await
.map_err(Into::<Error>::into)?;
Ok(self)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn insert(&self, key: &CacheKey, value: &CacheValue) -> Result<()> {
let key = bson::to_vec(key)?;
let value = bson::to_vec(value)?;
cacache::write(self.path.as_path(), key.encode_hex(), value).await?;
Ok(())
}
pub async fn get(&self, key: &CacheKey) -> Option<CacheValue> {
let key = bson::to_vec(key).ok()?;
let value = cacache::read(self.path.as_path(), key.encode_hex())
.await
.ok()?;
let value = bson::from_slice::<CacheValue>(&value).ok()?;
Some(value)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_or_insert_with(
&self,
key: &CacheKey,
callback: impl AsyncFnOnce() -> Result<CacheValue>,
) -> Result<CacheValue> {
match self.get(key).await {
Some(value) => {
debug!("Cache hit");
Ok(value)
}
None => {
debug!("Cache miss");
let value = callback().await?;
self.insert(key, &value).await?;
Ok(value)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct CacheKey {
/// The platform name that this artifact was compiled for. For example, this could be EVM or
/// PVM.
platform_key: String,
/// The version of the compiler that was used to compile the artifacts.
compiler_version: Version,
/// The path of the metadata file that the compilation artifacts are for.
metadata_file_path: PathBuf,
/// The mode that the compilation artifacts where compiled with.
solc_mode: Mode,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CacheValue {
/// The compiler output from the compilation run.
compiler_output: CompilerOutput,
}
impl CacheValue {
pub fn new(compiler_output: CompilerOutput) -> Self {
Self { compiler_output }
}
}
+177 -322
View File
@@ -4,36 +4,31 @@ use std::collections::HashMap;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::PathBuf; use std::path::PathBuf;
use alloy::consensus::EMPTY_ROOT_HASH;
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::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, GethDebugTracerType, GethDebugTracingOptions, GethTrace,
GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateConfig,
}; };
use alloy::{ use alloy::{
primitives::Address, primitives::Address,
rpc::types::{TransactionRequest, trace::geth::DiffMode}, rpc::types::{
TransactionRequest,
trace::geth::{AccountState, DiffMode},
},
}; };
use anyhow::Context; use anyhow::Context;
use futures::TryStreamExt;
use indexmap::IndexMap; use indexmap::IndexMap;
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
use semver::Version; use semver::Version;
use revive_dt_format::case::Case; use revive_dt_format::case::{Case, CaseIdx};
use revive_dt_format::input::{ use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method};
BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, StepIdx, use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent};
StorageEmptyAssertion, use revive_dt_format::{input::Input, metadata::Metadata};
}; use revive_dt_node::Node;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Step, metadata::Metadata};
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tokio::try_join;
use tracing::{Instrument, info, info_span, instrument};
use crate::Platform; use crate::Platform;
@@ -42,7 +37,7 @@ pub struct CaseState<T: Platform> {
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>, compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// This map stores the contracts deployments for this case. /// This map stores the contracts deployments for this case.
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>, deployed_contracts: HashMap<ContractInstance, (Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata /// This map stores the variables used for each one of the cases contained in the metadata
/// file. /// file.
@@ -61,7 +56,7 @@ where
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, (Address, JsonAbi)>,
) -> Self { ) -> Self {
Self { Self {
compiled_contracts, compiled_contracts,
@@ -72,41 +67,15 @@ where
} }
} }
pub async fn handle_step(
&mut self,
metadata: &Metadata,
step: &Step,
node: &T::Blockchain,
) -> anyhow::Result<StepOutput> {
match step {
Step::FunctionCall(input) => {
let (receipt, geth_trace, diff_mode) =
self.handle_input(metadata, input, node).await?;
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
}
Step::BalanceAssertion(balance_assertion) => {
self.handle_balance_assertion(metadata, balance_assertion, node)
.await?;
Ok(StepOutput::BalanceAssertion)
}
Step::StorageEmptyAssertion(storage_empty) => {
self.handle_storage_empty(metadata, storage_empty, node)
.await?;
Ok(StepOutput::StorageEmptyAssertion)
}
}
.inspect(|_| info!("Step Succeeded"))
}
#[instrument(level = "info", name = "Handling Input", skip_all)]
pub async fn handle_input( pub async fn handle_input(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts = self let deployment_receipts = self
.handle_input_contract_deployment(metadata, input, node) .handle_contract_deployment(metadata, case_idx, input, node)
.await?; .await?;
let execution_receipt = self let execution_receipt = self
.handle_input_execution(input, deployment_receipts, node) .handle_input_execution(input, deployment_receipts, node)
@@ -115,49 +84,27 @@ where
.handle_input_call_frame_tracing(&execution_receipt, node) .handle_input_call_frame_tracing(&execution_receipt, node)
.await?; .await?;
self.handle_input_variable_assignment(input, &tracing_result)?; self.handle_input_variable_assignment(input, &tracing_result)?;
let (_, (geth_trace, diff_mode)) = try_join!( self.handle_input_expectations(input, &execution_receipt, node, &tracing_result)
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
self.handle_input_diff(&execution_receipt, node)
)?;
Ok((execution_receipt, geth_trace, diff_mode))
}
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
pub async fn handle_balance_assertion(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
.await?; .await?;
self.handle_balance_assertion_execution(balance_assertion, node) self.handle_input_diff(case_idx, execution_receipt, node)
.await?; .await
Ok(())
}
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
pub async fn handle_storage_empty(
&mut self,
metadata: &Metadata,
storage_empty: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
.await?;
self.handle_storage_empty_assertion_execution(storage_empty, node)
.await?;
Ok(())
} }
/// Handles the contract deployment for a given input performing it if it needs to be performed. /// Handles the contract deployment for a given input performing it if it needs to be performed.
#[instrument(level = "info", skip_all)] async fn handle_contract_deployment(
async fn handle_input_contract_deployment(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> { ) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
let span = tracing::debug_span!(
"Handling contract deployment",
?case_idx,
instance = ?input.instance
);
let _guard = span.enter();
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() {
if !self.deployed_contracts.contains_key(&instance) { if !self.deployed_contracts.contains_key(&instance) {
@@ -169,6 +116,11 @@ where
instances_we_must_deploy.insert(input.instance.clone(), true); instances_we_must_deploy.insert(input.instance.clone(), true);
} }
tracing::debug!(
instances_to_deploy = instances_we_must_deploy.len(),
"Computed the number of required deployments for input"
);
let mut receipts = HashMap::new(); let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata); let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
@@ -195,7 +147,6 @@ where
} }
/// Handles the execution of the input in terms of the calls that need to be made. /// Handles the execution of the input in terms of the calls that need to be made.
#[instrument(level = "info", skip_all)]
async fn handle_input_execution( async fn handle_input_execution(
&mut self, &mut self,
input: &Input, input: &Input,
@@ -210,24 +161,36 @@ where
.context("Failed to find deployment receipt"), .context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => { Method::Fallback | Method::FunctionName(_) => {
let tx = match input let tx = match input
.legacy_transaction(node, self.default_resolution_context()) .legacy_transaction(&self.deployed_contracts, &self.variables, node)
.await .await
{ {
Ok(tx) => tx, Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
}
Err(err) => { Err(err) => {
tracing::error!("Failed to construct legacy transaction: {err:?}");
return Err(err); return Err(err);
} }
}; };
tracing::trace!("Executing transaction for input: {input:?}");
match node.execute_transaction(tx).await { match node.execute_transaction(tx).await {
Ok(receipt) => Ok(receipt), Ok(receipt) => Ok(receipt),
Err(err) => Err(err), Err(err) => {
tracing::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&*input.instance,
err
);
Err(err)
}
} }
} }
} }
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_call_frame_tracing( async fn handle_input_call_frame_tracing(
&self, &self,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
@@ -239,14 +202,6 @@ where
tracer: Some(GethDebugTracerType::BuiltInTracer( tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer, GethDebugBuiltInTracerType::CallTracer,
)), )),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default() ..Default::default()
}, },
) )
@@ -258,7 +213,6 @@ where
}) })
} }
#[instrument(level = "info", skip_all)]
fn handle_input_variable_assignment( fn handle_input_variable_assignment(
&mut self, &mut self,
input: &Input, input: &Input,
@@ -279,24 +233,21 @@ where
) { ) {
let value = U256::from_be_slice(output_word); let value = U256::from_be_slice(output_word);
self.variables.insert(variable_name.clone(), value); self.variables.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
} }
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_expectations( async fn handle_input_expectations(
&self, &mut self,
input: &Input, input: &Input,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
resolver: &impl ResolverApi, node: &T::Blockchain,
tracing_result: &CallFrame, tracing_result: &CallFrame,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
// 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 { Input {
@@ -325,25 +276,24 @@ where
} }
} }
futures::stream::iter(expectations.into_iter().map(Ok)) for expectation in expectations.iter() {
.try_for_each_concurrent(None, |expectation| async move {
self.handle_input_expectation_item( self.handle_input_expectation_item(
execution_receipt, execution_receipt,
resolver, node,
expectation, expectation,
tracing_result, tracing_result,
) )
.await .await?;
}) }
.await
Ok(())
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_expectation_item( async fn handle_input_expectation_item(
&self, &mut self,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
resolver: &impl ResolverApi, node: &T::Blockchain,
expectation: ExpectedOutput, expectation: &ExpectedOutput,
tracing_result: &CallFrame, tracing_result: &CallFrame,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version { if let Some(ref version_requirement) = expectation.compiler_version {
@@ -352,10 +302,9 @@ where
} }
} }
let resolution_context = self let deployed_contracts = &mut self.deployed_contracts;
.default_resolution_context() let variables = &mut self.variables;
.with_block_number(execution_receipt.block_number.as_ref()) let chain_state_provider = node;
.with_transaction_hash(&execution_receipt.transaction_hash);
// Handling the receipt state assertion. // Handling the receipt state assertion.
let expected = !expectation.exception; let expected = !expectation.exception;
@@ -378,7 +327,12 @@ where
let expected = expected_calldata; let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default(); let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected if !expected
.is_equivalent(actual, resolver, resolution_context) .is_equivalent(
actual,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
@@ -404,16 +358,14 @@ where
} }
// Handling the events assertion. // Handling the events assertion.
for (event_idx, (expected_event, actual_event)) in expected_events for (expected_event, actual_event) in
.iter() expected_events.iter().zip(execution_receipt.logs())
.zip(execution_receipt.logs())
.enumerate()
{ {
// 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 = Address::from_slice(
Calldata::new_compound([expected_address]) Calldata::new_compound([expected_address])
.calldata(resolver, resolution_context) .calldata(deployed_contracts, &*variables, node)
.await? .await?
.get(12..32) .get(12..32)
.expect("Can't fail"), .expect("Can't fail"),
@@ -421,7 +373,6 @@ where
let actual = actual_event.address(); let actual = actual_event.address();
if actual != expected { if actual != expected {
tracing::error!( tracing::error!(
event_idx,
%expected, %expected,
%actual, %actual,
"Event emitter assertion failed", "Event emitter assertion failed",
@@ -441,11 +392,15 @@ where
{ {
let expected = Calldata::new_compound([expected]); let expected = Calldata::new_compound([expected]);
if !expected if !expected
.is_equivalent(&actual.0, resolver, resolution_context) .is_equivalent(
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
event_idx,
?execution_receipt, ?execution_receipt,
?expected, ?expected,
?actual, ?actual,
@@ -461,11 +416,15 @@ where
let expected = &expected_event.values; let expected = &expected_event.values;
let actual = &actual_event.data().data; let actual = &actual_event.data().data;
if !expected if !expected
.is_equivalent(&actual.0, resolver, resolution_context) .is_equivalent(
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
event_idx,
?execution_receipt, ?execution_receipt,
?expected, ?expected,
?actual, ?actual,
@@ -481,12 +440,15 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_diff( async fn handle_input_diff(
&self, &mut self,
execution_receipt: &TransactionReceipt, _: CaseIdx,
execution_receipt: TransactionReceipt,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(GethTrace, DiffMode)> { ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let span = tracing::info_span!("Handling input diff");
let _guard = span.enter();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
disable_code: None, disable_code: None,
@@ -494,137 +456,11 @@ where
}); });
let trace = node let trace = node
.trace_transaction(execution_receipt, trace_options) .trace_transaction(&execution_receipt, trace_options)
.await?; .await?;
let diff = node.state_diff(execution_receipt).await?; let diff = node.state_diff(&execution_receipt).await?;
Ok((trace, diff)) Ok((execution_receipt, trace, diff))
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let Some(instance) = balance_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
Input::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_execution(
&mut self,
BalanceAssertion {
address: address_string,
expected_balance: amount,
..
}: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(node, self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let balance = node.balance_of(address).await?;
let expected = *amount;
let actual = balance;
if expected != actual {
tracing::error!(%expected, %actual, %address, "Balance assertion failed");
anyhow::bail!(
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
}
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
storage_empty_assertion: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let Some(instance) = storage_empty_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
Input::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_execution(
&mut self,
StorageEmptyAssertion {
address: address_string,
is_storage_empty,
..
}: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(node, self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let storage = node.latest_state_proof(address, Default::default()).await?;
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
let expected = is_storage_empty;
let actual = is_empty;
if *expected != actual {
tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed");
anyhow::bail!(
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
};
Ok(())
} }
/// Gets the information of a deployed contract or library from the state. If it's found to not /// Gets the information of a deployed contract or library from the state. If it's found to not
@@ -642,7 +478,7 @@ where
value: Option<EtherValue>, value: Option<EtherValue>,
node: &T::Blockchain, node: &T::Blockchain,
) -> 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));
} }
@@ -651,6 +487,7 @@ where
contract_ident, contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance) }) = metadata.contract_sources()?.remove(contract_instance)
else { else {
tracing::error!("Contract source not found for instance");
anyhow::bail!( anyhow::bail!(
"Contract source not found for instance {:?}", "Contract source not found for instance {:?}",
contract_instance contract_instance
@@ -663,6 +500,11 @@ where
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref())) .and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned() .cloned()
else { else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find information for contract"
);
anyhow::bail!( anyhow::bail!(
"Failed to find information for contract {:?}", "Failed to find information for contract {:?}",
contract_instance contract_instance
@@ -684,7 +526,7 @@ where
if let Some(calldata) = calldata { if let Some(calldata) = calldata {
let calldata = calldata let calldata = calldata
.calldata(node, self.default_resolution_context()) .calldata(&self.deployed_contracts, None, node)
.await?; .await?;
code.extend(calldata); code.extend(calldata);
} }
@@ -711,6 +553,7 @@ where
}; };
let Some(address) = receipt.contract_address else { let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address"); anyhow::bail!("Contract deployment didn't return an address");
}; };
tracing::info!( tracing::info!(
@@ -719,24 +562,17 @@ where
"Deployed contract" "Deployed contract"
); );
self.deployed_contracts.insert( self.deployed_contracts
contract_instance.clone(), .insert(contract_instance.clone(), (address, abi.clone()));
(contract_ident, address, abi.clone()),
);
Ok((address, abi, Some(receipt))) Ok((address, abi, Some(receipt)))
} }
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.deployed_contracts)
.with_variables(&self.variables)
}
} }
pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> { pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> {
metadata: &'a Metadata, metadata: &'a Metadata,
case: &'a Case, case: &'a Case,
case_idx: CaseIdx,
leader_node: &'a Leader::Blockchain, leader_node: &'a Leader::Blockchain,
follower_node: &'a Follower::Blockchain, follower_node: &'a Follower::Blockchain,
leader_state: CaseState<Leader>, leader_state: CaseState<Leader>,
@@ -752,6 +588,7 @@ where
pub fn new( pub fn new(
metadata: &'a Metadata, metadata: &'a Metadata,
case: &'a Case, case: &'a Case,
case_idx: impl Into<CaseIdx>,
leader_node: &'a L::Blockchain, leader_node: &'a L::Blockchain,
follower_node: &'a F::Blockchain, follower_node: &'a F::Blockchain,
leader_state: CaseState<L>, leader_state: CaseState<L>,
@@ -760,6 +597,7 @@ where
Self { Self {
metadata, metadata,
case, case,
case_idx: case_idx.into(),
leader_node, leader_node,
follower_node, follower_node,
leader_state, leader_state,
@@ -767,61 +605,78 @@ where
} }
} }
#[instrument(level = "info", name = "Executing Case", skip_all)] pub fn trace_diff_mode(label: &str, diff: &DiffMode) {
tracing::trace!("{label} - PRE STATE:");
for (addr, state) in &diff.pre {
Self::trace_account_state(" [pre]", addr, state);
}
tracing::trace!("{label} - POST STATE:");
for (addr, state) in &diff.post {
Self::trace_account_state(" [post]", addr, state);
}
}
fn trace_account_state(prefix: &str, addr: &Address, state: &AccountState) {
tracing::trace!("{prefix} 0x{addr:x}");
if let Some(balance) = &state.balance {
tracing::trace!("{prefix} balance: {balance}");
}
if let Some(nonce) = &state.nonce {
tracing::trace!("{prefix} nonce: {nonce}");
}
if let Some(code) = &state.code {
tracing::trace!("{prefix} code: {code}");
}
}
pub async fn execute(&mut self) -> anyhow::Result<usize> { pub async fn execute(&mut self) -> anyhow::Result<usize> {
let mut steps_executed = 0; if !self
for (step_idx, step) in self .leader_node
.case .matches_target(self.metadata.targets.as_deref())
.steps_iterator() || !self
.enumerate() .follower_node
.map(|(idx, v)| (StepIdx::new(idx), v)) .matches_target(self.metadata.targets.as_deref())
{ {
let (leader_step_output, follower_step_output) = try_join!( tracing::warn!(
self.leader_state targets = ?self.metadata.targets,
.handle_step(self.metadata, &step, self.leader_node) "Either the leader or follower node do not support the targets of the file"
.instrument(info_span!( );
"Handling Step", return Ok(0);
%step_idx,
target = "Leader",
)),
self.follower_state
.handle_step(self.metadata, &step, self.follower_node)
.instrument(info_span!(
"Handling Step",
%step_idx,
target = "Follower",
))
)?;
match (leader_step_output, follower_step_output) {
(StepOutput::FunctionCall(..), StepOutput::FunctionCall(..)) => {
// TODO: We need to actually work out how/if we will compare the diff between
// the leader and the follower. The diffs are almost guaranteed to be different
// from leader and follower and therefore without an actual strategy for this
// we have something that's guaranteed to fail. Even a simple call to some
// contract will produce two non-equal diffs because on the leader the contract
// has address X and on the follower it has address Y. On the leader contract X
// contains address A in the state and on the follower it contains address B. So
// this isn't exactly a straightforward thing to do and I'm not even sure that
// it's possible to do. Once we have an actual strategy for doing the diffs we
// will implement it here. Until then, this remains empty.
}
(StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {}
(StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {}
_ => unreachable!("The two step outputs can not be of a different kind"),
} }
steps_executed += 1; let mut inputs_executed = 0;
for (input_idx, input) in self.case.inputs_iterator().enumerate() {
let tracing_span = tracing::info_span!("Handling input", input_idx);
let _guard = tracing_span.enter();
let (leader_receipt, _, leader_diff) = self
.leader_state
.handle_input(self.metadata, self.case_idx, &input, self.leader_node)
.await?;
let (follower_receipt, _, follower_diff) = self
.follower_state
.handle_input(self.metadata, self.case_idx, &input, self.follower_node)
.await?;
if leader_diff == follower_diff {
tracing::debug!("State diffs match between leader and follower.");
} else {
tracing::debug!("State diffs mismatch between leader and follower.");
Self::trace_diff_mode("Leader", &leader_diff);
Self::trace_diff_mode("Follower", &follower_diff);
} }
Ok(steps_executed) if leader_receipt.logs() != follower_receipt.logs() {
tracing::debug!("Log/event mismatch between leader and follower.");
tracing::trace!("Leader logs: {:?}", leader_receipt.logs());
tracing::trace!("Follower logs: {:?}", follower_receipt.logs());
}
inputs_executed += 1;
}
Ok(inputs_executed)
} }
} }
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StepOutput {
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
BalanceAssertion,
StorageEmptyAssertion,
}
+415 -496
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -11,14 +11,10 @@ rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-common = { workspace = true } revive-dt-common = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true } alloy-sol-types = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true }
regex = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
@@ -26,6 +22,3 @@ serde_json = { workspace = true }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+15 -41
View File
@@ -1,50 +1,31 @@
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use revive_dt_common::{macros::define_wrapper_type, types::Mode}; use revive_dt_common::macros::define_wrapper_type;
use crate::{ use crate::{
input::{Expected, Step}, input::{Expected, Input},
mode::ParsedMode, mode::Mode,
}; };
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Case { pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
pub modes: Option<Vec<Mode>>,
#[serde(skip_serializing_if = "Option::is_none")] pub inputs: Vec<Input>,
pub modes: Option<Vec<ParsedMode>>,
#[serde(rename = "inputs")]
pub steps: Vec<Step>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>, pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>, pub expected: Option<Expected>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
} }
impl Case { impl Case {
#[allow(irrefutable_let_patterns)] pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> {
pub fn steps_iterator(&self) -> impl Iterator<Item = Step> { let inputs_len = self.inputs.len();
let steps_len = self.steps.len(); self.inputs
self.steps
.clone() .clone()
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(move |(idx, mut step)| { .map(move |(idx, mut input)| {
let Step::FunctionCall(ref mut input) = step else { if idx + 1 == inputs_len {
return step;
};
if idx + 1 == steps_len {
if input.expected.is_none() { if input.expected.is_none() {
input.expected = self.expected.clone(); input.expected = self.expected.clone();
} }
@@ -54,23 +35,16 @@ impl Case {
// the case? What are we supposed to do with that final expected field on the // the case? What are we supposed to do with that final expected field on the
// case? // case?
step input
} else { } else {
step input
} }
}) })
} }
pub fn solc_modes(&self) -> Vec<Mode> {
match &self.modes {
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
None => Mode::all().collect(),
}
}
} }
define_wrapper_type!( define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file. /// A wrapper type for the index of test cases found in metadata file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaseIdx(usize) impl Display; pub struct CaseIdx(usize);
); );
+58 -88
View File
@@ -3,28 +3,25 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use revive_dt_common::iterators::FilesWithExtensionIterator;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::metadata::{Metadata, MetadataFile}; use crate::metadata::MetadataFile;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(untagged)] pub struct Corpus {
pub enum Corpus { pub name: String,
SinglePath { name: String, path: PathBuf }, pub path: PathBuf,
MultiplePaths { name: String, paths: Vec<PathBuf> },
} }
impl Corpus { impl Corpus {
pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> { /// Try to read and parse the corpus definition file at given `path`.
let mut corpus = File::open(file_path.as_ref()) pub fn try_from_path(path: &Path) -> anyhow::Result<Self> {
.map_err(anyhow::Error::from) let file = File::open(path)?;
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?; let mut corpus: Corpus = serde_json::from_reader(file)?;
for path in corpus.paths_iter_mut() { // Ensure that the path mentioned in the corpus is relative to the corpus file.
*path = file_path // Canonicalizing also helps make the path in any errors unambiguous.
.as_ref() corpus.path = path
.parent() .parent()
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display()) anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
@@ -36,94 +33,67 @@ impl Corpus {
path.display() path.display()
) )
})? })?
.join(path.as_path()) .join(corpus.path);
}
Ok(corpus) Ok(corpus)
} }
/// Scan the corpus base directory and return all tests found.
pub fn enumerate_tests(&self) -> Vec<MetadataFile> { pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
let mut tests = self let mut tests = Vec::new();
.paths_iter() collect_metadata(&self.path, &mut tests);
.flat_map(|root_path| {
if !root_path.is_dir() {
Box::new(std::iter::once(root_path.to_path_buf()))
as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(root_path)
.with_use_cached_fs(true)
.with_allowed_extension("sol")
.with_allowed_extension("json"),
)
}
.map(move |metadata_file_path| (root_path, metadata_file_path))
})
.filter_map(|(root_path, metadata_file_path)| {
Metadata::try_from_file(&metadata_file_path)
.or_else(|| {
debug!(
discovered_from = %root_path.display(),
metadata_file_path = %metadata_file_path.display(),
"Skipping file since it doesn't contain valid metadata"
);
None
})
.map(|metadata| MetadataFile {
metadata_file_path,
corpus_file_path: root_path.to_path_buf(),
content: metadata,
})
.inspect(|metadata_file| {
debug!(
metadata_file_path = %metadata_file.relative_path().display(),
"Loaded metadata file"
)
})
})
.collect::<Vec<_>>();
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
info!(
len = tests.len(),
corpus_name = self.name(),
"Found tests in Corpus"
);
tests tests
} }
}
pub fn name(&self) -> &str { /// Recursively walks `path` and parses any JSON or Solidity file into a test
match self { /// definition [Metadata].
Corpus::SinglePath { name, .. } | Corpus::MultiplePaths { name, .. } => name.as_str(), ///
/// Found tests are inserted into `tests`.
///
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
if path.is_dir() {
let dir_entry = match std::fs::read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
} }
};
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
} }
pub fn paths_iter(&self) -> impl Iterator<Item = &Path> { if path.is_file() {
match self { if let Some(metadata) = MetadataFile::try_from_file(&path) {
Corpus::SinglePath { path, .. } => { tests.push(metadata)
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
}
Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
} }
} }
} }
} else {
pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> { let Some(extension) = path.extension() else {
match self { tracing::error!("Failed to get file extension");
Corpus::SinglePath { path, .. } => { return;
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>> };
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
if let Some(metadata) = MetadataFile::try_from_file(path) {
tests.push(metadata)
} }
Corpus::MultiplePaths { paths, .. } => { } else {
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>> tracing::error!(?extension, "Unsupported file extension");
}
}
}
pub fn path_count(&self) -> usize {
match self {
Corpus::SinglePath { .. } => 1,
Corpus::MultiplePaths { paths, .. } => paths.len(),
} }
} }
} }
+214 -269
View File
@@ -2,109 +2,39 @@ use std::collections::HashMap;
use alloy::{ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
json_abi::Function, hex::ToHexExt,
json_abi::JsonAbi,
network::TransactionBuilder, network::TransactionBuilder,
primitives::{Address, Bytes, U256}, primitives::{Address, Bytes, U256},
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;
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
use semver::VersionReq; use semver::VersionReq;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type; use revive_dt_common::macros::define_wrapper_type;
use tracing::{Instrument, info_span, instrument};
use crate::metadata::ContractInstance;
use crate::traits::ResolverApi; use crate::traits::ResolverApi;
use crate::{metadata::ContractInstance, traits::ResolutionContext};
/// A test step. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
///
/// A test step can be anything. It could be an invocation to a function, an assertion, or any other
/// action that needs to be run or executed on the nodes used in the tests.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum Step {
/// A function call or an invocation to some function on some smart contract.
FunctionCall(Box<Input>),
/// A step for performing a balance assertion on some account or contract.
BalanceAssertion(Box<BalanceAssertion>),
/// A step for asserting that the storage of some contract or account is empty.
StorageEmptyAssertion(Box<StorageEmptyAssertion>),
}
define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StepIdx(usize) impl Display;
);
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct Input { pub struct Input {
#[serde(default = "Input::default_caller")] #[serde(default = "Input::default_caller")]
pub caller: Address, pub caller: Address,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
#[serde(default = "Input::default_instance")] #[serde(default = "Input::default_instance")]
pub instance: ContractInstance, pub instance: ContractInstance,
pub method: Method, pub method: Method,
#[serde(default)] #[serde(default)]
pub calldata: Calldata, pub calldata: Calldata,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>, pub expected: Option<Expected>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<EtherValue>, pub value: Option<EtherValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage: Option<HashMap<String, Calldata>>, pub storage: Option<HashMap<String, Calldata>>,
#[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)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct BalanceAssertion {
/// An optional comment on the balance assertion.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// The address that the balance assertion should be done on.
///
/// This is a string which will be resolved into an address when being processed. Therefore,
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
/// The amount of balance to assert that the account or contract has.
pub expected_balance: U256,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct StorageEmptyAssertion {
/// An optional comment on the storage empty assertion.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// The address that the balance assertion should be done on.
///
/// This is a string which will be resolved into an address when being processed. Therefore,
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
/// A boolean of whether the storage of the address is empty or not.
pub is_storage_empty: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum Expected { pub enum Expected {
Calldata(Calldata), Calldata(Calldata),
@@ -112,21 +42,17 @@ pub enum Expected {
ExpectedMany(Vec<ExpectedOutput>), ExpectedMany(Vec<ExpectedOutput>),
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct ExpectedOutput { pub struct ExpectedOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_version: Option<VersionReq>, pub compiler_version: Option<VersionReq>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_data: Option<Calldata>, pub return_data: Option<Calldata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<Event>>, pub events: Option<Vec<Event>>,
#[serde(default)] #[serde(default)]
pub exception: bool, pub exception: bool,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event { pub struct Event {
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>, pub address: Option<String>,
pub topics: Vec<String>, pub topics: Vec<String>,
pub values: Calldata, pub values: Calldata,
@@ -183,7 +109,7 @@ 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, Deserialize, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum Calldata { pub enum Calldata {
Single(Bytes), Single(Bytes),
@@ -194,7 +120,7 @@ define_wrapper_type! {
/// This represents an item in the [`Calldata::Compound`] variant. /// This represents an item in the [`Calldata::Compound`] variant.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct CalldataItem(String) impl Display; pub struct CalldataItem(String);
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
@@ -217,7 +143,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, Deserialize, Clone, Eq, PartialEq)]
pub enum Method { pub enum Method {
/// Initiate a deploy transaction, calling contracts constructor. /// Initiate a deploy transaction, calling contracts constructor.
/// ///
@@ -239,10 +165,10 @@ pub enum Method {
define_wrapper_type!( define_wrapper_type!(
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EtherValue(U256) impl Display; pub struct EtherValue(U256);
); );
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
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.
/// ///
@@ -261,36 +187,50 @@ impl Input {
ContractInstance::new("Test") ContractInstance::new("Test")
} }
pub async fn encoded_input( fn instance_to_address(
&self, &self,
resolver: &impl ResolverApi, instance: &ContractInstance,
context: ResolutionContext<'_>, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
) -> anyhow::Result<Address> {
deployed_contracts
.get(instance)
.map(|(a, _)| *a)
.ok_or_else(|| anyhow::anyhow!("instance {instance:?} not deployed"))
}
pub async fn encoded_input<'a>(
&'a self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Bytes> { ) -> anyhow::Result<Bytes> {
match self.method { match self.method {
Method::Deployer | Method::Fallback => { Method::Deployer | Method::Fallback => {
let calldata = self.calldata.calldata(resolver, context).await?; let calldata = self
.calldata
.calldata(deployed_contracts, variables, chain_state_provider)
.await?;
Ok(calldata.into()) Ok(calldata.into())
} }
Method::FunctionName(ref function_name) => { Method::FunctionName(ref function_name) => {
let Some(abi) = context.deployed_contract_abi(&self.instance) else { let Some(abi) = deployed_contracts.get(&self.instance).map(|(_, a)| a) else {
tracing::error!(
contract_name = self.instance.as_ref(),
available_abis = ?deployed_contracts.keys().collect::<Vec<_>>(),
"Attempted to lookup ABI of contract but it wasn't found"
);
anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref()); anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref());
}; };
tracing::trace!("ABI found for instance: {}", &self.instance.as_ref());
// We follow the same logic that's implemented in the matter-labs-tester where they resolve // We follow the same logic that's implemented in the matter-labs-tester where they resolve
// the function name into a function selector and they assume that he function doesn't have // the function name into a function selector and they assume that he function doesn't have
// any existing overloads. // any existing overloads.
// Overloads are handled by providing the full function signature in the "function
// name".
// https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190 // https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190
let selector = if function_name.contains('(') && function_name.contains(')') { let function = abi
Function::parse(function_name) .functions()
.context(
"Failed to parse the provided function name into a function signature",
)?
.selector()
} else {
abi.functions()
.find(|function| function.signature().starts_with(function_name)) .find(|function| function.signature().starts_with(function_name))
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!( anyhow::anyhow!(
@@ -298,9 +238,14 @@ impl Input {
function_name, function_name,
&self.instance &self.instance
) )
})? })?;
.selector()
}; tracing::trace!("Functions found for instance: {}", self.instance.as_ref());
tracing::trace!(
"Starting encoding ABI's parameters for instance: {}",
self.instance.as_ref()
);
// Allocating a vector that we will be using for the calldata. The vector size will be: // Allocating a vector that we will be using for the calldata. The vector size will be:
// 4 bytes for the function selector. // 4 bytes for the function selector.
@@ -309,9 +254,14 @@ impl Input {
// We're using indices in the following code in order to avoid the need for us to allocate // We're using indices in the following code in order to avoid the need for us to allocate
// a new buffer for each one of the resolved arguments. // a new buffer for each one of the resolved arguments.
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement()); let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
calldata.extend(selector.0); calldata.extend(function.selector().0);
self.calldata self.calldata
.calldata_into_slice(&mut calldata, resolver, context) .calldata_into_slice(
&mut calldata,
deployed_contracts,
variables,
chain_state_provider,
)
.await?; .await?;
Ok(calldata.into()) Ok(calldata.into())
@@ -320,12 +270,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<'a>(
&self, &'a self,
resolver: &impl ResolverApi, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
context: ResolutionContext<'_>, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<TransactionRequest> { ) -> anyhow::Result<TransactionRequest> {
let input_data = self.encoded_input(resolver, context).await?; let input_data = self
.encoded_input(deployed_contracts, variables, chain_state_provider)
.await?;
let transaction_request = TransactionRequest::default().from(self.caller).value( let transaction_request = TransactionRequest::default().from(self.caller).value(
self.value self.value
.map(|value| value.into_inner()) .map(|value| value.into_inner())
@@ -334,10 +287,7 @@ impl Input {
match self.method { match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
_ => Ok(transaction_request _ => Ok(transaction_request
.to(context .to(self.instance_to_address(&self.instance, deployed_contracts)?)
.deployed_contract_address(&self.instance)
.context("Failed to get the contract address")
.copied()?)
.input(input_data.into())), .input(input_data.into())),
} }
} }
@@ -406,40 +356,49 @@ impl Calldata {
} }
} }
pub async fn calldata( pub async fn calldata<'a>(
&self, &'a self,
resolver: &impl ResolverApi, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
context: ResolutionContext<'_>, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> 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());
self.calldata_into_slice(&mut buffer, resolver, context) self.calldata_into_slice(
&mut buffer,
deployed_contracts,
variables,
chain_state_provider,
)
.await?; .await?;
Ok(buffer) Ok(buffer)
} }
pub async fn calldata_into_slice( pub async fn calldata_into_slice<'a>(
&self, &'a self,
buffer: &mut Vec<u8>, buffer: &mut Vec<u8>,
resolver: &impl ResolverApi, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
context: ResolutionContext<'_>, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
match self { match self {
Calldata::Single(bytes) => { Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes); buffer.extend_from_slice(bytes);
} }
Calldata::Compound(items) => { Calldata::Compound(items) => {
let resolved = stream::iter(items.iter().enumerate()) for (arg_idx, arg) in items.iter().enumerate() {
.map(|(arg_idx, arg)| async move { match arg
arg.resolve(resolver, context) .resolve(deployed_contracts, variables.clone(), chain_state_provider)
.instrument(info_span!("Resolving argument", %arg, arg_idx))
.map_ok(|value| value.to_be_bytes::<32>())
.await .await
}) {
.buffered(0xFF) Ok(resolved) => {
.try_collect::<Vec<_>>() buffer.extend(resolved.to_be_bytes::<32>());
.await?; }
Err(error) => {
buffer.extend(resolved.into_iter().flatten()); tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
} }
}; };
Ok(()) Ok(())
@@ -453,21 +412,23 @@ impl Calldata {
} }
/// Checks if this [`Calldata`] is equivalent to the passed calldata bytes. /// Checks if this [`Calldata`] is equivalent to the passed calldata bytes.
pub async fn is_equivalent( pub async fn is_equivalent<'a>(
&self, &'a self,
other: &[u8], other: &[u8],
resolver: &impl ResolverApi, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
context: ResolutionContext<'_>, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
match self { match self {
Calldata::Single(calldata) => Ok(calldata == other), Calldata::Single(calldata) => Ok(calldata == other),
Calldata::Compound(items) => { Calldata::Compound(items) => {
stream::iter(items.iter().zip(other.chunks(32))) // Chunking the "other" calldata into 32 byte chunks since each
.map(|(this, other)| async move { // one of the items in the compound calldata represents 32 bytes
for (this, other) in items.iter().zip(other.chunks(32)) {
// The matterlabs format supports wildcards and therefore we // The matterlabs format supports wildcards and therefore we
// also need to support them. // also need to support them.
if this.as_ref() == "*" { if this.as_ref() == "*" {
return Ok::<_, anyhow::Error>(true); continue;
} }
let other = if other.len() < 32 { let other = if other.len() < 32 {
@@ -478,31 +439,32 @@ impl Calldata {
std::borrow::Cow::Borrowed(other) std::borrow::Cow::Borrowed(other)
}; };
let this = this.resolve(resolver, context).await?; let this = this
.resolve(deployed_contracts, variables.clone(), chain_state_provider)
.await?;
let other = U256::from_be_slice(&other); let other = U256::from_be_slice(&other);
Ok(this == other) if this != other {
}) return Ok(false);
.buffered(0xFF) }
.all(|v| async move { v.is_ok_and(|v| v) }) }
.map(Ok) Ok(true)
.await
} }
} }
} }
} }
impl CalldataItem { impl CalldataItem {
#[instrument(level = "info", skip_all, err)] async fn resolve<'a>(
async fn resolve( &'a self,
&self, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
resolver: &impl ResolverApi, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
context: ResolutionContext<'_>, chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> { ) -> anyhow::Result<U256> {
let mut stack = Vec::<CalldataToken<U256>>::new(); let mut stack = Vec::<CalldataToken<U256>>::new();
for token in self for token in self
.calldata_tokens() .calldata_tokens()
.map(|token| token.resolve(resolver, context)) .map(|token| token.resolve(deployed_contracts, variables.clone(), chain_state_provider))
{ {
let token = token.await?; let token = token.await?;
let new_token = match token { let new_token = match token {
@@ -543,14 +505,21 @@ impl CalldataItem {
match stack.as_slice() { match stack.as_slice() {
// Empty stack means that we got an empty compound calldata which we resolve to zero. // Empty stack means that we got an empty compound calldata which we resolve to zero.
[] => Ok(U256::ZERO), [] => Ok(U256::ZERO),
[CalldataToken::Item(item)] => Ok(*item), [CalldataToken::Item(item)] => {
tracing::debug!(
original = self.0,
resolved = item.to_be_bytes::<32>().encode_hex(),
"Resolved a Calldata item"
);
Ok(*item)
}
_ => Err(anyhow::anyhow!( _ => Err(anyhow::anyhow!(
"Invalid calldata arithmetic operation - Invalid stack" "Invalid calldata arithmetic operation - Invalid stack"
)), )),
} }
} }
fn calldata_tokens(&self) -> impl Iterator<Item = CalldataToken<&str>> { fn calldata_tokens<'a>(&'a self) -> impl Iterator<Item = CalldataToken<&'a str>> + 'a {
self.0.split(' ').map(|item| match item { self.0.split(' ').map(|item| match item {
"+" => CalldataToken::Operation(Operation::Addition), "+" => CalldataToken::Operation(Operation::Addition),
"-" => CalldataToken::Operation(Operation::Subtraction), "-" => CalldataToken::Operation(Operation::Subtraction),
@@ -574,11 +543,9 @@ impl<T> CalldataToken<T> {
const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT"; const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT";
const COINBASE_VARIABLE: &str = "$COINBASE"; const COINBASE_VARIABLE: &str = "$COINBASE";
const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY"; const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY";
const BLOCK_BASE_FEE_VARIABLE: &str = "$BASE_FEE";
const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH"; const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH";
const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER"; const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER";
const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP"; const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP";
const TRANSACTION_GAS_PRICE: &str = "$TRANSACTION_GAS_PRICE";
const VARIABLE_PREFIX: &str = "$VARIABLE:"; const VARIABLE_PREFIX: &str = "$VARIABLE:";
fn into_item(self) -> Option<T> { fn into_item(self) -> Option<T> {
@@ -598,21 +565,24 @@ impl<T: AsRef<str>> CalldataToken<T> {
/// This piece of code is taken from the matter-labs-tester repository which is licensed under /// This piece of code is taken from the matter-labs-tester repository which is licensed under
/// MIT or Apache. The original source code can be found here: /// MIT or Apache. The original source code can be found here:
/// 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<'a>(
self, self,
resolver: &impl ResolverApi, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
context: ResolutionContext<'_>, variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<CalldataToken<U256>> { ) -> anyhow::Result<CalldataToken<U256>> {
match self { match self {
Self::Item(item) => { Self::Item(item) => {
let item = item.as_ref(); let item = item.as_ref();
let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX) let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX)
{ {
context Ok(U256::from_be_slice(
.deployed_contract_address(&ContractInstance::new(instance)) deployed_contracts
.ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance)) .get(&ContractInstance::new(instance))
.map(AsRef::as_ref) .map(|(a, _)| *a)
.map(U256::from_be_slice) .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))?
.as_ref(),
))
} else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) { } else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) {
let value = U256::from_str_radix(value, 10).map_err(|error| { let value = U256::from_str_radix(value, 10).map_err(|error| {
anyhow::anyhow!("Invalid decimal literal after `-`: {}", error) anyhow::anyhow!("Invalid decimal literal after `-`: {}", error)
@@ -625,36 +595,27 @@ impl<T: AsRef<str>> CalldataToken<T> {
.ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?;
Ok(U256::MAX.checked_sub(value).expect("Always valid")) Ok(U256::MAX.checked_sub(value).expect("Always valid"))
} else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) { } else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) {
U256::from_str_radix(value, 16) Ok(U256::from_str_radix(value, 16).map_err(|error| {
.map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error)) anyhow::anyhow!("Invalid hexadecimal literal: {}", error)
})?)
} else if item == Self::CHAIN_VARIABLE { } else if item == Self::CHAIN_VARIABLE {
resolver.chain_id().await.map(U256::from) let chain_id = chain_state_provider.chain_id().await?;
} else if item == Self::TRANSACTION_GAS_PRICE { Ok(U256::from(chain_id))
context
.transaction_hash()
.context("No transaction hash provided to get the transaction gas price")
.map(|tx_hash| resolver.transaction_gas_price(tx_hash))?
.await
.map(U256::from)
} else if item == Self::GAS_LIMIT_VARIABLE { } else if item == Self::GAS_LIMIT_VARIABLE {
resolver let gas_limit = chain_state_provider
.block_gas_limit(context.resolve_block_number(BlockNumberOrTag::Latest)) .block_gas_limit(BlockNumberOrTag::Latest)
.await .await?;
.map(U256::from) Ok(U256::from(gas_limit))
} else if item == Self::COINBASE_VARIABLE { } else if item == Self::COINBASE_VARIABLE {
resolver let coinbase = chain_state_provider
.block_coinbase(context.resolve_block_number(BlockNumberOrTag::Latest)) .block_coinbase(BlockNumberOrTag::Latest)
.await .await?;
.map(|address| U256::from_be_slice(address.as_ref())) Ok(U256::from_be_slice(coinbase.as_ref()))
} else if item == Self::DIFFICULTY_VARIABLE { } else if item == Self::DIFFICULTY_VARIABLE {
resolver let block_difficulty = chain_state_provider
.block_difficulty(context.resolve_block_number(BlockNumberOrTag::Latest)) .block_difficulty(BlockNumberOrTag::Latest)
.await .await?;
} else if item == Self::BLOCK_BASE_FEE_VARIABLE { Ok(block_difficulty)
resolver
.block_base_fee(context.resolve_block_number(BlockNumberOrTag::Latest))
.await
.map(U256::from)
} else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) { } else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) {
let offset: u64 = item let offset: u64 = item
.split(':') .split(':')
@@ -662,34 +623,35 @@ impl<T: AsRef<str>> CalldataToken<T> {
.and_then(|value| value.parse().ok()) .and_then(|value| value.parse().ok())
.unwrap_or_default(); .unwrap_or_default();
let current_block_number = match context.tip_block_number() { let current_block_number = chain_state_provider.last_block_number().await?;
Some(block_number) => *block_number, let desired_block_number = current_block_number - offset;
None => resolver.last_block_number().await?,
};
let desired_block_number = current_block_number.saturating_sub(offset);
let block_hash = resolver.block_hash(desired_block_number.into()).await?; let block_hash = chain_state_provider
.block_hash(desired_block_number.into())
.await?;
Ok(U256::from_be_bytes(block_hash.0)) Ok(U256::from_be_bytes(block_hash.0))
} else if item == Self::BLOCK_NUMBER_VARIABLE { } else if item == Self::BLOCK_NUMBER_VARIABLE {
let current_block_number = match context.tip_block_number() { let current_block_number = chain_state_provider.last_block_number().await?;
Some(block_number) => *block_number,
None => resolver.last_block_number().await?,
};
Ok(U256::from(current_block_number)) Ok(U256::from(current_block_number))
} else if item == Self::BLOCK_TIMESTAMP_VARIABLE { } else if item == Self::BLOCK_TIMESTAMP_VARIABLE {
resolver let timestamp = chain_state_provider
.block_timestamp(context.resolve_block_number(BlockNumberOrTag::Latest)) .block_timestamp(BlockNumberOrTag::Latest)
.await .await?;
.map(U256::from) Ok(U256::from(timestamp))
} else if let Some(variable_name) = item.strip_prefix(Self::VARIABLE_PREFIX) { } else if let Some(variable_name) = item.strip_prefix(Self::VARIABLE_PREFIX) {
context let Some(variables) = variables.into() else {
.variable(variable_name) anyhow::bail!(
.context("Variable lookup failed") "Variable resolution required but no variables were passed in"
.copied() );
};
let Some(variable) = variables.get(variable_name) else {
anyhow::bail!("No variable found with the name {}", variable_name)
};
Ok(*variable)
} else { } else {
U256::from_str_radix(item, 10) Ok(U256::from_str_radix(item, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error)) .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
}; };
value.map(CalldataToken::Item) value.map(CalldataToken::Item)
} }
@@ -727,52 +689,51 @@ impl<'de> Deserialize<'de> for EtherValue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi}; use super::*;
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address}; use alloy::json_abi::JsonAbi;
use alloy_primitives::address;
use alloy_sol_types::SolValue; use alloy_sol_types::SolValue;
use std::collections::HashMap; use std::collections::HashMap;
use super::*;
use crate::metadata::ContractIdent;
struct MockResolver; struct MockResolver;
impl ResolverApi for MockResolver { impl ResolverApi for MockResolver {
async fn chain_id(&self) -> anyhow::Result<ChainId> { async fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
Ok(0x123) Ok(0x123)
} }
async fn block_gas_limit(&self, _: BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<u128> {
Ok(0x1234) Ok(0x1234)
} }
async fn block_coinbase(&self, _: BlockNumberOrTag) -> anyhow::Result<Address> { async fn block_coinbase(
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<Address> {
Ok(Address::ZERO) Ok(Address::ZERO)
} }
async fn block_difficulty(&self, _: BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<U256> {
Ok(U256::from(0x12345u128)) Ok(U256::from(0x12345u128))
} }
async fn block_base_fee(&self, _: BlockNumberOrTag) -> anyhow::Result<u64> { async fn block_hash(
Ok(0x100) &self,
} _: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<alloy_primitives::BlockHash> {
async fn block_hash(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
Ok([0xEE; 32].into()) Ok([0xEE; 32].into())
} }
async fn block_timestamp(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> { async fn block_timestamp(
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<alloy_primitives::BlockTimestamp> {
Ok(0x123456) Ok(0x123456)
} }
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<alloy_primitives::BlockNumber> {
Ok(0x1234567) Ok(0x1234567)
} }
async fn transaction_gas_price(&self, _: &TxHash) -> anyhow::Result<u128> {
Ok(0x200)
}
} }
#[tokio::test] #[tokio::test]
@@ -808,12 +769,13 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(ContractIdent::new("Contract"), Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let resolver = MockResolver; let encoded = input
let context = ResolutionContext::default().with_deployed_contracts(&contracts); .encoded_input(&contracts, None, &MockResolver)
let encoded = input.encoded_input(&resolver, context).await.unwrap(); .await
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (u64,); type T = (u64,);
@@ -852,12 +814,13 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(ContractIdent::new("Contract"), Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let resolver = MockResolver; let encoded = input
let context = ResolutionContext::default().with_deployed_contracts(&contracts); .encoded_input(&contracts, None, &MockResolver)
let encoded = input.encoded_input(&resolver, context).await.unwrap(); .await
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -899,12 +862,13 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(ContractIdent::new("Contract"), Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let resolver = MockResolver; let encoded = input
let context = ResolutionContext::default().with_deployed_contracts(&contracts); .encoded_input(&contracts, None, &MockResolver)
let encoded = input.encoded_input(&resolver, context).await.unwrap(); .await
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -917,11 +881,12 @@ 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, (Address, JsonAbi)>,
resolver: &impl ResolverApi, chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> { ) -> anyhow::Result<U256> {
let context = ResolutionContext::default().with_deployed_contracts(deployed_contracts); CalldataItem::new(input)
CalldataItem::new(input).resolve(resolver, context).await .resolve(deployed_contracts, None, chain_state_provider)
.await
} }
#[tokio::test] #[tokio::test]
@@ -999,26 +964,6 @@ mod tests {
) )
} }
#[tokio::test]
async fn resolver_can_resolve_block_base_fee_variable() {
// Arrange
let input = "$BASE_FEE";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver).await;
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
MockResolver
.block_base_fee(Default::default())
.await
.map(U256::from)
.unwrap()
)
}
#[tokio::test] #[tokio::test]
async fn resolver_can_resolve_block_hash_variable() { async fn resolver_can_resolve_block_hash_variable() {
// Arrange // Arrange
+60 -222
View File
@@ -1,8 +1,7 @@
use std::{ use std::{
cmp::Ordering,
collections::BTreeMap, collections::BTreeMap,
fmt::Display, fmt::Display,
fs::File, fs::{File, read_to_string},
ops::Deref, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
@@ -10,14 +9,12 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_common::EVMVersion; use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type};
use revive_dt_common::{
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
types::Mode,
};
use tracing::error;
use crate::{case::Case, mode::ParsedMode}; use crate::{
case::Case,
mode::{Mode, SolcMode},
};
pub const METADATA_FILE_EXTENSION: &str = "json"; pub const METADATA_FILE_EXTENSION: &str = "json";
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol"; pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
@@ -25,26 +22,16 @@ pub const SOLIDITY_CASE_COMMENT_MARKER: &str = "//!";
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct MetadataFile { pub struct MetadataFile {
/// The path of the metadata file. This will either be a JSON or solidity file. pub path: PathBuf,
pub metadata_file_path: PathBuf,
/// This is the path contained within the corpus file. This could either be the path of some dir
/// or could be the actual metadata file path.
pub corpus_file_path: PathBuf,
/// The metadata contained within the file.
pub content: Metadata, pub content: Metadata,
} }
impl MetadataFile { impl MetadataFile {
pub fn relative_path(&self) -> &Path { pub fn try_from_file(path: &Path) -> Option<Self> {
if self.corpus_file_path.is_file() { Metadata::try_from_file(path).map(|metadata| Self {
&self.corpus_file_path path: path.to_owned(),
} else { content: metadata,
self.metadata_file_path })
.strip_prefix(&self.corpus_file_path)
.unwrap()
}
} }
} }
@@ -56,51 +43,34 @@ impl Deref for MetadataFile {
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Metadata { pub struct Metadata {
/// A comment on the test case that's added for human-readability.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<String>>, pub targets: Option<Vec<String>>,
pub cases: Vec<Case>, pub cases: Vec<Case>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>, pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
// TODO: Convert into wrapper types for clarity.
#[serde(skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>, pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
pub ignore: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] pub modes: Option<Vec<Mode>>,
pub modes: Option<Vec<ParsedMode>>,
#[serde(skip_serializing_if = "Option::is_none")]
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
/// be run of the evm version of the nodes match the evm version specified here.
#[serde(skip_serializing_if = "Option::is_none")]
pub required_evm_version: Option<EvmVersionRequirement>,
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
/// just a filter for when a test can run whereas this is an instruction to the compiler.
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_directives: Option<CompilationDirectives>,
} }
impl Metadata { impl Metadata {
/// Returns the modes that we should test from this metadata. /// Returns the solc modes of this metadata, inserting a default mode if not present.
pub fn solc_modes(&self) -> Vec<Mode> { pub fn solc_modes(&self) -> Vec<SolcMode> {
match &self.modes { self.modes
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(), .to_owned()
None => Mode::all().collect(), .unwrap_or_else(|| vec![Mode::Solidity(Default::default())])
.iter()
.filter_map(|mode| match mode {
Mode::Solidity(solc_mode) => Some(solc_mode),
Mode::Unknown(mode) => {
tracing::debug!("compiler: ignoring unknown mode '{mode}'");
None
} }
})
.cloned()
.collect()
} }
/// Returns the base directory of this metadata. /// Returns the base directory of this metadata.
@@ -156,7 +126,10 @@ impl Metadata {
pub fn try_from_file(path: &Path) -> Option<Self> { pub fn try_from_file(path: &Path) -> Option<Self> {
assert!(path.is_file(), "not a file: {}", path.display()); assert!(path.is_file(), "not a file: {}", path.display());
let file_extension = path.extension()?; let Some(file_extension) = path.extension() else {
tracing::debug!("skipping corpus file: {}", path.display());
return None;
};
if file_extension == METADATA_FILE_EXTENSION { if file_extension == METADATA_FILE_EXTENSION {
return Self::try_from_json(path); return Self::try_from_json(path);
@@ -166,12 +139,18 @@ impl Metadata {
return Self::try_from_solidity(path); return Self::try_from_solidity(path);
} }
tracing::debug!("ignoring invalid corpus file: {}", path.display());
None None
} }
fn try_from_json(path: &Path) -> Option<Self> { fn try_from_json(path: &Path) -> Option<Self> {
let file = File::open(path) let file = File::open(path)
.inspect_err(|err| error!(path = %path.display(), %err, "Failed to open file")) .inspect_err(|error| {
tracing::error!(
"opening JSON test metadata file '{}' error: {error}",
path.display()
);
})
.ok()?; .ok()?;
match serde_json::from_reader::<_, Metadata>(file) { match serde_json::from_reader::<_, Metadata>(file) {
@@ -179,8 +158,11 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf()); metadata.file_path = Some(path.to_path_buf());
Some(metadata) Some(metadata)
} }
Err(err) => { Err(error) => {
error!(path = %path.display(), %err, "Deserialization of metadata failed"); tracing::error!(
"parsing JSON test metadata file '{}' error: {error}",
path.display()
);
None None
} }
} }
@@ -188,7 +170,12 @@ impl Metadata {
fn try_from_solidity(path: &Path) -> Option<Self> { fn try_from_solidity(path: &Path) -> Option<Self> {
let spec = read_to_string(path) let spec = read_to_string(path)
.inspect_err(|err| error!(path = %path.display(), %err, "Failed to read file content")) .inspect_err(|error| {
tracing::error!(
"opening JSON test metadata file '{}' error: {error}",
path.display()
);
})
.ok()? .ok()?
.lines() .lines()
.filter_map(|line| line.strip_prefix(SOLIDITY_CASE_COMMENT_MARKER)) .filter_map(|line| line.strip_prefix(SOLIDITY_CASE_COMMENT_MARKER))
@@ -216,8 +203,11 @@ impl Metadata {
); );
Some(metadata) Some(metadata)
} }
Err(err) => { Err(error) => {
error!(path = %path.display(), %err, "Failed to deserialize metadata"); tracing::error!(
"parsing Solidity test metadata file '{}' error: '{error}' from data: {spec}",
path.display()
);
None None
} }
} }
@@ -241,9 +231,7 @@ impl Metadata {
Ok(Box::new(std::iter::once(metadata_file_path.clone()))) Ok(Box::new(std::iter::once(metadata_file_path.clone())))
} else { } else {
Ok(Box::new( Ok(Box::new(
FilesWithExtensionIterator::new(self.directory()?) FilesWithExtensionIterator::new(self.directory()?).with_allowed_extension("sol"),
.with_allowed_extension("sol")
.with_use_cached_fs(true),
)) ))
} }
} }
@@ -257,7 +245,7 @@ define_wrapper_type!(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)] )]
#[serde(transparent)] #[serde(transparent)]
pub struct ContractInstance(String) impl Display; pub struct ContractInstance(String);
); );
define_wrapper_type!( define_wrapper_type!(
@@ -268,7 +256,7 @@ define_wrapper_type!(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)] )]
#[serde(transparent)] #[serde(transparent)]
pub struct ContractIdent(String) impl Display; pub struct ContractIdent(String);
); );
/// Represents an identifier used for contracts. /// Represents an identifier used for contracts.
@@ -355,156 +343,6 @@ impl From<ContractPathAndIdent> for String {
} }
} }
/// An EVM version requirement that the test case has. This gets serialized and
/// deserialized from and into [`String`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EvmVersionRequirement {
ordering: Ordering,
or_equal: bool,
evm_version: EVMVersion,
}
impl EvmVersionRequirement {
pub fn new_greater_than_or_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Greater,
or_equal: true,
evm_version: version,
}
}
pub fn new_greater_than(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Greater,
or_equal: false,
evm_version: version,
}
}
pub fn new_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Equal,
or_equal: false,
evm_version: version,
}
}
pub fn new_less_than(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Less,
or_equal: false,
evm_version: version,
}
}
pub fn new_less_than_or_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: version,
}
}
pub fn matches(&self, other: &EVMVersion) -> bool {
let ordering = other.cmp(&self.evm_version);
ordering == self.ordering || (self.or_equal && matches!(ordering, Ordering::Equal))
}
}
impl Display for EvmVersionRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ordering,
or_equal,
evm_version,
} = self;
match ordering {
Ordering::Less => write!(f, "<")?,
Ordering::Equal => write!(f, "=")?,
Ordering::Greater => write!(f, ">")?,
}
if *or_equal && !matches!(ordering, Ordering::Equal) {
write!(f, "=")?;
}
write!(f, "{evm_version}")
}
}
impl FromStr for EvmVersionRequirement {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.as_bytes() {
[b'>', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'>', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Equal,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
_ => anyhow::bail!("Invalid EVM version requirement {s}"),
}
}
}
impl TryFrom<String> for EvmVersionRequirement {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<EvmVersionRequirement> for String {
fn from(value: EvmVersionRequirement) -> Self {
value.to_string()
}
}
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
/// just a filter for when a test can run whereas this is an instruction to the compiler.
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub struct CompilationDirectives {
/// Defines how the revert strings should be handled.
pub revert_string_handling: Option<RevertString>,
}
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+85 -241
View File
@@ -1,262 +1,106 @@
use regex::Regex; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline}; use semver::Version;
use serde::de::Deserializer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
/// This represents a mode that has been parsed from test metadata. /// Specifies the compilation mode of the test artifact.
/// #[derive(Hash, Debug, Clone, Eq, PartialEq)]
/// Mode strings can take the following form (in pseudo-regex): pub enum Mode {
/// Solidity(SolcMode),
/// ```text Unknown(String),
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
} }
impl FromStr for ParsedMode { /// Specify Solidity specific compiler options.
type Err = anyhow::Error; #[derive(Hash, Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
fn from_str(s: &str) -> Result<Self, Self::Err> { pub struct SolcMode {
static REGEX: LazyLock<Regex> = LazyLock::new(|| { pub solc_version: Option<semver::VersionReq>,
Regex::new(r"(?x) solc_optimize: Option<bool>,
^ pub llvm_optimizer_settings: Vec<String>,
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E- }
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
let Some(caps) = REGEX.captures(s) else { impl SolcMode {
anyhow::bail!("Cannot parse mode '{s}' from string"); /// Try to parse a mode string into a solc mode.
/// Returns `None` if the string wasn't a solc YUL mode string.
///
/// The mode string is expected to start with the `Y` ID (YUL ID),
/// optionally followed by `+` or `-` for the solc optimizer settings.
///
/// Options can be separated by a whitespace contain the following
/// - A solc `SemVer version requirement` string
/// - One or more `-OX` where X is a supposed to be an LLVM opt mode
pub fn parse_from_mode_string(mode_string: &str) -> Option<Self> {
let mut result = Self::default();
let mut parts = mode_string.trim().split(" ");
match parts.next()? {
"Y" => {}
"Y+" => result.solc_optimize = Some(true),
"Y-" => result.solc_optimize = Some(false),
_ => return None,
}
for part in parts {
if let Ok(solc_version) = semver::VersionReq::parse(part) {
result.solc_version = Some(solc_version);
continue;
}
if let Some(level) = part.strip_prefix("-O") {
result.llvm_optimizer_settings.push(level.to_string());
continue;
}
panic!("the YUL mode string {mode_string} failed to parse, invalid part: {part}")
}
Some(result)
}
/// Returns whether to enable the solc optimizer.
pub fn solc_optimize(&self) -> bool {
self.solc_optimize.unwrap_or(true)
}
/// Calculate the latest matching solc patch version. Returns:
/// - `latest_supported` if no version request was specified.
/// - A matching version with the same minor version as `latest_supported`, if any.
/// - `None` if no minor version of the `latest_supported` version matches.
pub fn last_patch_version(&self, latest_supported: &Version) -> Option<Version> {
let Some(version_req) = self.solc_version.as_ref() else {
return Some(latest_supported.to_owned());
}; };
let pipeline = match caps.name("pipeline") { // lgtm
Some(m) => Some(ModePipeline::from_str(m.as_str())?), for patch in (0..latest_supported.patch + 1).rev() {
None => None, let version = Version::new(0, latest_supported.minor, patch);
}; if version_req.matches(&version) {
return Some(version);
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| {
anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str())
})?),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
} }
}
impl Display for ParsedMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
}
has_written = true;
} }
if let Some(optimize_setting) = self.optimize_setting { None
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
} }
if let Some(version) = &self.version { /// Resolves the [`SolcMode`]'s solidity version requirement into a [`VersionOrRequirement`] if
if has_written { /// the requirement is present on the object. Otherwise, the passed default version is used.
f.write_str(" ")?; pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
} match self.solc_version {
version.fmt(f)?; Some(ref requirement) => requirement.clone().into(),
} None => default.into(),
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
}
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// This avoids any duplicate entries.
pub fn many_to_modes<'a>(
parsed: impl Iterator<Item = &'a ParsedMode>,
) -> impl Iterator<Item = Mode> {
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
/// An iterator that could be either of two iterators.
#[derive(Clone, Debug)]
enum EitherIter<A, B> {
A(A),
B(B),
}
impl<A, B> Iterator for EitherIter<A, B>
where
A: Iterator,
B: Iterator<Item = A::Item>,
{
type Item = A::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
EitherIter::A(iter) => iter.next(),
EitherIter::B(iter) => iter.next(),
} }
} }
} }
#[cfg(test)] impl<'de> Deserialize<'de> for Mode {
mod tests { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
use super::*; where
D: Deserializer<'de>,
{
let mode_string = String::deserialize(deserializer)?;
#[test] if let Some(solc_mode) = SolcMode::parse_from_mode_string(&mode_string) {
fn test_parsed_mode_from_str() { return Ok(Self::Solidity(solc_mode));
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
}
} }
#[test] Ok(Self::Unknown(mode_string))
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
);
}
} }
} }
-124
View File
@@ -1,22 +1,13 @@
use std::collections::HashMap;
use alloy::eips::BlockNumberOrTag; use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy_primitives::TxHash;
use anyhow::Result; use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance};
/// A trait of the interface are required to implement to be used by the resolution logic that this /// A trait of the interface are required to implement to be used by the resolution logic that this
/// 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) -> impl Future<Output = Result<ChainId>>;
/// Returns the gas price for the specified transaction.
fn transaction_gas_price(&self, tx_hash: &TxHash) -> impl Future<Output = Result<u128>>;
// 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 Kitchensink 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.
@@ -28,9 +19,6 @@ pub trait ResolverApi {
/// 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) -> impl Future<Output = Result<U256>>;
/// Returns the base fee of the specified block.
fn block_base_fee(&self, number: BlockNumberOrTag) -> impl 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) -> impl Future<Output = Result<BlockHash>>;
@@ -43,115 +31,3 @@ pub trait ResolverApi {
/// 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) -> impl Future<Output = Result<BlockNumber>>;
} }
#[derive(Clone, Copy, Debug, Default)]
/// Contextual information required by the code that's performing the resolution.
pub struct ResolutionContext<'a> {
/// When provided the contracts provided here will be used for resolutions.
deployed_contracts: Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
/// When provided the variables in here will be used for performing resolutions.
variables: Option<&'a HashMap<String, U256>>,
/// When provided this block number will be treated as the tip of the chain.
block_number: Option<&'a BlockNumber>,
/// When provided the resolver will use this transaction hash for all of its resolutions.
transaction_hash: Option<&'a TxHash>,
}
impl<'a> ResolutionContext<'a> {
pub fn new() -> Self {
Default::default()
}
pub fn new_from_parts(
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
block_number: impl Into<Option<&'a BlockNumber>>,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
Self {
deployed_contracts: deployed_contracts.into(),
variables: variables.into(),
block_number: block_number.into(),
transaction_hash: transaction_hash.into(),
}
}
pub fn with_deployed_contracts(
mut self,
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
) -> Self {
self.deployed_contracts = deployed_contracts.into();
self
}
pub fn with_variables(
mut self,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
) -> Self {
self.variables = variables.into();
self
}
pub fn with_block_number(mut self, block_number: impl Into<Option<&'a BlockNumber>>) -> Self {
self.block_number = block_number.into();
self
}
pub fn with_transaction_hash(
mut self,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
self.transaction_hash = transaction_hash.into();
self
}
pub fn resolve_block_number(&self, number: BlockNumberOrTag) -> BlockNumberOrTag {
match self.block_number {
Some(block_number) => match number {
BlockNumberOrTag::Latest => BlockNumberOrTag::Number(*block_number),
n @ (BlockNumberOrTag::Finalized
| BlockNumberOrTag::Safe
| BlockNumberOrTag::Earliest
| BlockNumberOrTag::Pending
| BlockNumberOrTag::Number(_)) => n,
},
None => number,
}
}
pub fn deployed_contract(
&self,
instance: &ContractInstance,
) -> Option<&(ContractIdent, Address, JsonAbi)> {
self.deployed_contracts
.and_then(|deployed_contracts| deployed_contracts.get(instance))
}
pub fn deployed_contract_address(&self, instance: &ContractInstance) -> Option<&Address> {
self.deployed_contract(instance).map(|(_, a, _)| a)
}
pub fn deployed_contract_abi(&self, instance: &ContractInstance) -> Option<&JsonAbi> {
self.deployed_contract(instance).map(|(_, _, a)| a)
}
pub fn variable(&self, name: impl AsRef<str>) -> Option<&U256> {
self.variables
.and_then(|variables| variables.get(name.as_ref()))
}
pub fn tip_block_number(&self) -> Option<&'a BlockNumber> {
self.block_number
}
pub fn transaction_hash(&self) -> Option<&'a TxHash> {
self.transaction_hash
}
}
-3
View File
@@ -11,6 +11,3 @@ rust-version.workspace = true
[dependencies] [dependencies]
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
[lints]
workspace = true
+1 -12
View File
@@ -1,8 +1,7 @@
//! This crate implements all node interactions. //! This crate implements all node interactions.
use alloy::primitives::{Address, StorageKey, 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::{TransactionReceipt, TransactionRequest};
use anyhow::Result; use anyhow::Result;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
@@ -22,14 +21,4 @@ pub trait EthereumNode {
/// 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, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>;
/// Returns the balance of the provided [`Address`] back.
fn balance_of(&self, address: Address) -> impl Future<Output = Result<U256>>;
/// Returns the latest storage proof of the provided [`Address`]
fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> impl Future<Output = Result<EIP1186AccountProofResponse>>;
} }
-4
View File
@@ -14,7 +14,6 @@ alloy = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
revive-common = { workspace = true }
revive-dt-common = { workspace = true } revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
@@ -29,6 +28,3 @@ sp-runtime = { workspace = true }
[dev-dependencies] [dev-dependencies]
temp-dir = { workspace = true } temp-dir = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+123 -206
View File
@@ -3,13 +3,9 @@
use std::{ use std::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all}, fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
ops::ControlFlow,
path::PathBuf, path::PathBuf,
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::{ sync::atomic::{AtomicU32, Ordering},
Arc,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -17,31 +13,23 @@ 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, FixedBytes, 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, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner, signers::local::PrivateKeySigner,
}; };
use anyhow::Context; use revive_dt_common::fs::clear_directory;
use revive_common::EVMVersion;
use tracing::{Instrument, instrument};
use revive_dt_common::{
fs::clear_directory,
futures::{PollingWaitBehavior, poll},
};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tracing::Level;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE}; use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -55,7 +43,6 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// ///
/// Prunes the child process and the base directory on drop. /// Prunes the child process and the base directory on drop.
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::type_complexity)]
pub struct GethNode { pub struct GethNode {
connection_string: String, connection_string: String,
base_directory: PathBuf, base_directory: PathBuf,
@@ -64,10 +51,10 @@ pub struct GethNode {
geth: PathBuf, geth: PathBuf,
id: u32, id: u32,
handle: Option<Child>, handle: Option<Child>,
network_id: u64,
start_timeout: u64, start_timeout: u64,
wallet: Arc<EthereumWallet>, wallet: EthereumWallet,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -90,13 +77,9 @@ impl GethNode {
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log"; const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress"; const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
/// Create the node directory and call `geth init` to configure the genesis. /// Create the node directory and call `geth init` to configure the genesis.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> { fn init(&mut self, genesis: String) -> 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);
@@ -119,8 +102,6 @@ impl GethNode {
serde_json::to_writer(File::create(&genesis_path)?, &genesis)?; serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
let mut child = Command::new(&self.geth) let mut child = Command::new(&self.geth)
.arg("--state.scheme")
.arg("hash")
.arg("init") .arg("init")
.arg("--datadir") .arg("--datadir")
.arg(&self.data_directory) .arg(&self.data_directory)
@@ -146,7 +127,7 @@ impl GethNode {
/// Spawn the go-ethereum node child process. /// Spawn the go-ethereum node child process.
/// ///
/// [Instance::init] must be called prior. /// [Instance::init] must be called prior.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> { fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
// This is the `OpenOptions` that we wish to use for all of the log files that we will be // This is the `OpenOptions` that we wish to use for all of the log files that we will be
// opening in this method. We need to construct it in this way to: // opening in this method. We need to construct it in this way to:
@@ -169,6 +150,8 @@ impl GethNode {
.arg(&self.data_directory) .arg(&self.data_directory)
.arg("--ipcpath") .arg("--ipcpath")
.arg(&self.connection_string) .arg(&self.connection_string)
.arg("--networkid")
.arg(self.network_id.to_string())
.arg("--nodiscover") .arg("--nodiscover")
.arg("--maxpeers") .arg("--maxpeers")
.arg("0") .arg("0")
@@ -176,12 +159,6 @@ impl GethNode {
.arg("0") .arg("0")
.arg("--cache.blocklogs") .arg("--cache.blocklogs")
.arg("512") .arg("512")
.arg("--state.scheme")
.arg("hash")
.arg("--syncmode")
.arg("full")
.arg("--gcmode")
.arg("archive")
.stderr(stderr_logs_file.try_clone()?) .stderr(stderr_logs_file.try_clone()?)
.stdout(stdout_logs_file.try_clone()?) .stdout(stdout_logs_file.try_clone()?)
.spawn()? .spawn()?
@@ -202,7 +179,7 @@ impl GethNode {
/// Wait for the g-ethereum node child process getting ready. /// Wait for the g-ethereum node child process getting ready.
/// ///
/// [Instance::spawn_process] must be called priorly. /// [Instance::spawn_process] must be called priorly.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn wait_ready(&mut self) -> anyhow::Result<&mut Self> { fn wait_ready(&mut self) -> anyhow::Result<&mut Self> {
let start_time = Instant::now(); let start_time = Instant::now();
@@ -215,7 +192,6 @@ impl GethNode {
let maximum_wait_time = Duration::from_millis(self.start_timeout); let maximum_wait_time = Duration::from_millis(self.start_timeout);
let mut stderr = BufReader::new(logs_file).lines(); let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop { loop {
if let Some(Ok(line)) = stderr.next() { if let Some(Ok(line)) = stderr.next() {
if line.contains(Self::ERROR_MARKER) { if line.contains(Self::ERROR_MARKER) {
@@ -224,145 +200,142 @@ impl GethNode {
if line.contains(Self::READY_MARKER) { if line.contains(Self::READY_MARKER) {
return Ok(self); return Ok(self);
} }
lines.push(line);
} }
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");
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout,
lines.join("\n")
);
} }
} }
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)]
fn geth_stdout_log_file_path(&self) -> PathBuf { fn geth_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME) self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)]
fn geth_stderr_log_file_path(&self) -> PathBuf { fn geth_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
} }
async fn provider( fn provider(
&self, &self,
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>> ) -> impl Future<
{ Output = anyhow::Result<
FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>,
>,
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers() .disable_recommended_fillers()
.filler(FallbackGasFiller::new( .filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1))
25_000_000, .filler(ChainIdFiller::default())
1_000_000_000, .filler(NonceFiller::new(nonce_manager))
1_000_000_000, .wallet(wallet)
)) .connect(&connection_string)
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.connection_string)
.await .await
.map_err(Into::into) .map_err(Into::into)
})
} }
} }
impl EthereumNode for GethNode { impl EthereumNode for GethNode {
#[instrument( #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
async fn execute_transaction( async fn execute_transaction(
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> { ) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction);
let _outer_guard = outer_span.enter();
let provider = self.provider().await?; let provider = self.provider().await?;
let pending_transaction = provider.send_transaction(transaction).await.inspect_err( let pending_transaction = provider.send_transaction(transaction).await?;
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"), 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 let span = tracing::info_span!("Awaiting transaction receipt", ?transaction_hash);
// to get. You can find more information on this in the following GH issue in geth let _guard = span.enter();
// The following is a fix for the "transaction indexing is in progress" error that we
// used to get. You can find more information on this in the following GH issue in geth
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on, // 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
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was // it has been indexed. When we call alloy's `get_receipt` it checks if the transaction
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which // was confirmed. If it has been, then it will call `eth_getTransactionReceipt` method
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to // which _might_ return the above error if the tx has not yet been indexed yet. So, we
// implement a retry mechanism for the receipt to keep retrying to get it until it // need to implement a retry mechanism for the receipt to keep retrying to get it until
// eventually works, but we only do that if the error we get back is the "transaction // it eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None. // 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
// a lot of transactions are being submitted to the node. Thus, while initially we only // when a lot of transactions are being submitted to the node. Thus, while initially we
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for // only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential // allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// backoff each time we attempt to get the receipt and find that it's not available. // with exponential backoff each time we attempt to get the receipt and find that it's
let provider = Arc::new(provider); // not available.
poll( let mut retries = 0;
Self::RECEIPT_POLLING_DURATION, let mut total_wait_duration = Duration::from_secs(0);
PollingWaitBehavior::Constant(Duration::from_millis(200)), let max_allowed_wait_duration = Duration::from_secs(5 * 60);
move || { loop {
let provider = provider.clone(); if total_wait_duration >= max_allowed_wait_duration {
async move { tracing::error!(
match provider.get_transaction_receipt(transaction_hash).await { ?total_wait_duration,
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)), ?max_allowed_wait_duration,
Ok(None) => Ok(ControlFlow::Continue(())), retry_count = retries,
Err(error) => { "Failed to get receipt after polling for it"
let error_string = error.to_string(); );
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) { anyhow::bail!(
true => Ok(ControlFlow::Continue(())), "Polled for receipt for {total_wait_duration:?} but failed to get it"
false => Err(error.into()), );
}
}
}
}
},
)
.instrument(tracing::info_span!(
"Awaiting transaction receipt",
?transaction_hash
))
.await
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => {
tracing::info!(?total_wait_duration, "Found receipt");
break Ok(receipt);
}
Ok(None) => {}
Err(error) => {
let error_string = error.to_string();
if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
break Err(error.into());
}
}
};
let next_wait_duration = Duration::from_secs(2u64.pow(retries))
.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn trace_transaction( async fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let provider = Arc::new(self.provider().await?); let tx_hash = transaction.transaction_hash;
poll( Ok(self
Self::TRACE_POLLING_DURATION, .provider()
PollingWaitBehavior::Constant(Duration::from_millis(200)), .await?
move || { .debug_trace_transaction(tx_hash, trace_options)
let provider = provider.clone(); .await?)
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))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
@@ -378,33 +351,10 @@ impl EthereumNode for GethNode {
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
} }
impl ResolverApi for GethNode { impl ResolverApi for GethNode {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider() self.provider()
.await? .await?
@@ -413,17 +363,7 @@ impl ResolverApi for GethNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -433,7 +373,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.gas_limit as _) .map(|block| block.header.gas_limit as _)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> { async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider() self.provider()
.await? .await?
@@ -443,32 +383,17 @@ impl ResolverApi for GethNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
.get_block_by_number(number) .get_block_by_number(number)
.await? .await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks")) .ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| U256::from_be_bytes(block.header.mix_hash.0)) .map(|block| block.header.difficulty)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> { async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider() self.provider()
.await? .await?
@@ -478,7 +403,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.hash) .map(|block| block.header.hash)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> { async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider() self.provider()
.await? .await?
@@ -488,7 +413,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.timestamp) .map(|block| block.header.timestamp)
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider() self.provider()
.await? .await?
@@ -521,27 +446,22 @@ impl Node for GethNode {
geth: config.geth.clone(), geth: config.geth.clone(),
id, id,
handle: None, handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout, start_timeout: config.geth_start_timeout,
wallet: Arc::new(wallet), 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 // 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. // the vector. It's the stdout and stderr of the geth node.
logs_file_to_flush: Vec::with_capacity(2), logs_file_to_flush: Vec::with_capacity(2),
nonce_manager: Default::default(),
} }
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(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 { fn connection_string(&self) -> String {
self.connection_string.clone() self.connection_string.clone()
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(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.
if let Some(mut child) = self.handle.take() { if let Some(mut child) = self.handle.take() {
@@ -563,13 +483,13 @@ impl Node for GethNode {
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> { fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()?; self.init(genesis)?.spawn_process()?;
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn version(&self) -> anyhow::Result<String> { fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.geth) let output = Command::new(&self.geth)
.arg("--version") .arg("--version")
@@ -582,20 +502,17 @@ impl Node for GethNode {
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
fn matches_target(targets: Option<&[String]>) -> bool { #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets { match targets {
None => true, None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"), Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
} }
} }
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
} }
impl Drop for GethNode { impl Drop for GethNode {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown") self.shutdown().expect("Failed to shutdown")
} }
+64 -79
View File
@@ -3,10 +3,7 @@ use std::{
io::{BufRead, Write}, io::{BufRead, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::{ sync::atomic::{AtomicU32, Ordering},
Arc,
atomic::{AtomicU32, Ordering},
},
time::Duration, time::Duration,
}; };
@@ -19,8 +16,7 @@ use alloy::{
TransactionBuilderError, UnbuiltTransactionError, TransactionBuilderError, UnbuiltTransactionError,
}, },
primitives::{ primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256,
StorageKey, TxHash, U256,
}, },
providers::{ providers::{
Provider, ProviderBuilder, Provider, ProviderBuilder,
@@ -28,20 +24,19 @@ use alloy::{
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
EIP1186AccountProofResponse, TransactionReceipt, TransactionReceipt,
eth::{Block, Header, Transaction}, eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner, signers::local::PrivateKeySigner,
}; };
use anyhow::Context;
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory; use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json}; use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec; use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32; use sp_runtime::AccountId32;
use tracing::Level;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
@@ -56,13 +51,12 @@ pub struct KitchensinkNode {
substrate_binary: PathBuf, substrate_binary: PathBuf,
eth_proxy_binary: PathBuf, eth_proxy_binary: PathBuf,
rpc_url: String, rpc_url: String,
wallet: EthereumWallet,
base_directory: PathBuf, base_directory: PathBuf,
logs_directory: PathBuf, logs_directory: PathBuf,
process_substrate: Option<Child>, process_substrate: Option<Child>,
process_proxy: Option<Child>, process_proxy: Option<Child>,
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -90,6 +84,7 @@ impl KitchensinkNode {
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log"; const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log"; const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> { fn init(&mut self, genesis: &str) -> 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);
@@ -162,6 +157,7 @@ impl KitchensinkNode {
Ok(self) Ok(self)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<()> { fn spawn_process(&mut self) -> anyhow::Result<()> {
let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16; let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16;
let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16; let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16;
@@ -213,8 +209,12 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready( if let Err(error) = Self::wait_ready(
self.kitchensink_stderr_log_file_path().as_path(), self.kitchensink_stderr_log_file_path().as_path(),
Self::SUBSTRATE_READY_MARKER, Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(60), Duration::from_secs(30),
) { ) {
tracing::error!(
?error,
"Failed to start substrate, shutting down gracefully"
);
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -238,8 +238,9 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready( if let Err(error) = Self::wait_ready(
self.proxy_stderr_log_file_path().as_path(), self.proxy_stderr_log_file_path().as_path(),
Self::ETH_PROXY_READY_MARKER, Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(60), Duration::from_secs(30),
) { ) {
tracing::error!(?error, "Failed to start proxy, shutting down gracefully");
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -254,6 +255,7 @@ impl KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file( fn extract_balance_from_genesis_file(
&self, &self,
genesis: &Genesis, genesis: &Genesis,
@@ -302,6 +304,7 @@ impl KitchensinkNode {
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
pub fn eth_rpc_version(&self) -> anyhow::Result<String> { pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.eth_proxy_binary) let output = Command::new(&self.eth_proxy_binary)
.arg("--version") .arg("--version")
@@ -314,55 +317,74 @@ impl KitchensinkNode {
Ok(String::from_utf8_lossy(&output).trim().to_string()) Ok(String::from_utf8_lossy(&output).trim().to_string())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn kitchensink_stdout_log_file_path(&self) -> PathBuf { fn kitchensink_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory self.logs_directory
.join(Self::KITCHENSINK_STDOUT_LOG_FILE_NAME) .join(Self::KITCHENSINK_STDOUT_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn kitchensink_stderr_log_file_path(&self) -> PathBuf { fn kitchensink_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory self.logs_directory
.join(Self::KITCHENSINK_STDERR_LOG_FILE_NAME) .join(Self::KITCHENSINK_STDERR_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn proxy_stdout_log_file_path(&self) -> PathBuf { fn proxy_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDOUT_LOG_FILE_NAME) self.logs_directory.join(Self::PROXY_STDOUT_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn proxy_stderr_log_file_path(&self) -> PathBuf { fn proxy_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME)
} }
async fn provider( fn provider(
&self, &self,
) -> anyhow::Result< ) -> impl Future<
Output = anyhow::Result<
FillProvider< FillProvider<
impl TxFiller<KitchenSinkNetwork>, impl TxFiller<KitchenSinkNetwork>,
impl Provider<KitchenSinkNetwork>, impl Provider<KitchenSinkNetwork>,
KitchenSinkNetwork, KitchenSinkNetwork,
>, >,
> { >,
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers() .disable_recommended_fillers()
.network::<KitchenSinkNetwork>() .network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new( .filler(FallbackGasFiller::new(
25_000_000, 30_000_000,
1_000_000_000, 200_000_000_000,
1_000_000_000, 3_000_000_000,
)) ))
.filler(self.chain_id_filler.clone()) .filler(ChainIdFiller::default())
.filler(NonceFiller::new(self.nonce_manager.clone())) .filler(NonceFiller::new(nonce_manager))
.wallet(self.wallet.clone()) .wallet(wallet)
.connect(&self.rpc_url) .connect(&connection_string)
.await .await
.map_err(Into::into) .map_err(Into::into)
})
} }
} }
impl EthereumNode for KitchensinkNode { impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn execute_transaction( async fn execute_transaction(
&self, &self,
transaction: alloy::rpc::types::TransactionRequest, transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> { ) -> anyhow::Result<TransactionReceipt> {
tracing::debug!(?transaction, "Submitting transaction");
let receipt = self let receipt = self
.provider() .provider()
.await? .await?
@@ -370,9 +392,11 @@ impl EthereumNode for KitchensinkNode {
.await? .await?
.get_receipt() .get_receipt()
.await?; .await?;
tracing::info!(?receipt, "Submitted tx to kitchensink");
Ok(receipt) Ok(receipt)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn trace_transaction( async fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, transaction: &TransactionReceipt,
@@ -386,6 +410,7 @@ impl EthereumNode for KitchensinkNode {
.await?) .await?)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
@@ -401,30 +426,10 @@ impl EthereumNode for KitchensinkNode {
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
} }
impl ResolverApi for KitchensinkNode { impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider() self.provider()
.await? .await?
@@ -433,15 +438,7 @@ impl ResolverApi for KitchensinkNode {
.map_err(Into::into) .map_err(Into::into)
} }
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> { #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -451,6 +448,7 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.gas_limit as _) .map(|block| block.header.gas_limit as _)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> { async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider() self.provider()
.await? .await?
@@ -460,29 +458,17 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
.get_block_by_number(number) .get_block_by_number(number)
.await? .await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks")) .ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| U256::from_be_bytes(block.header.mix_hash.0)) .map(|block| block.header.difficulty)
}
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> { async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider() self.provider()
.await? .await?
@@ -492,6 +478,7 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.hash) .map(|block| block.header.hash)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> { async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider() self.provider()
.await? .await?
@@ -501,6 +488,7 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.timestamp) .map(|block| block.header.timestamp)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider() self.provider()
.await? .await?
@@ -531,12 +519,11 @@ impl Node for KitchensinkNode {
substrate_binary: config.kitchensink.clone(), substrate_binary: config.kitchensink.clone(),
eth_proxy_binary: config.eth_proxy.clone(), eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(), rpc_url: String::new(),
wallet,
base_directory, base_directory,
logs_directory, logs_directory,
process_substrate: None, process_substrate: None,
process_proxy: None, process_proxy: None,
wallet: Arc::new(wallet),
chain_id_filler: Default::default(),
nonce_manager: Default::default(), nonce_manager: Default::default(),
// We know that we only need to be storing 4 files so we can specify that when creating // We know that we only need to be storing 4 files so we can specify that when creating
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc. // the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
@@ -544,14 +531,12 @@ impl Node for KitchensinkNode {
} }
} }
fn id(&self) -> usize { #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
self.id as _
}
fn connection_string(&self) -> String { fn connection_string(&self) -> String {
self.rpc_url.clone() self.rpc_url.clone()
} }
#[tracing::instrument(skip_all, fields(kitchensink_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.
if let Some(mut child) = self.process_proxy.take() { if let Some(mut child) = self.process_proxy.take() {
@@ -578,10 +563,12 @@ impl Node for KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> { fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(&genesis)?.spawn_process() self.init(&genesis)?.spawn_process()
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn version(&self) -> anyhow::Result<String> { fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.substrate_binary) let output = Command::new(&self.substrate_binary)
.arg("--version") .arg("--version")
@@ -594,19 +581,17 @@ impl Node for KitchensinkNode {
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
fn matches_target(targets: Option<&[String]>) -> bool { #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets { match targets {
None => true, None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"), Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
} }
} }
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
} }
impl Drop for KitchensinkNode { impl Drop for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown") self.shutdown().expect("Failed to shutdown")
} }
+1 -8
View File
@@ -1,6 +1,5 @@
//! This crate implements the testing nodes. //! This crate implements the testing nodes.
use revive_common::EVMVersion;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
@@ -18,9 +17,6 @@ pub trait Node: EthereumNode {
/// Create a new uninitialized instance. /// Create a new uninitialized instance.
fn new(config: &Arguments) -> Self; 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.
@@ -39,8 +35,5 @@ pub trait Node: EthereumNode {
/// Given a list of targets from the metadata file, this function determines if the metadata /// Given a list of targets from the metadata file, this function determines if the metadata
/// file can be ran on this node or not. /// file can be ran on this node or not.
fn matches_target(targets: Option<&[String]>) -> bool; fn matches_target(&self, targets: Option<&[String]>) -> bool;
/// Returns the EVM version of the node.
fn evm_version() -> EVMVersion;
} }
+2 -2
View File
@@ -1,12 +1,11 @@
//! This crate implements concurrent handling of testing node. //! This crate implements concurrent handling of testing node.
use std::{ use std::{
fs::read_to_string,
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
thread, thread,
}; };
use revive_dt_common::cached_fs::read_to_string;
use anyhow::Context; use anyhow::Context;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
@@ -63,6 +62,7 @@ where
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> { fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
let mut node = T::new(args); let mut node = T::new(args);
tracing::info!("starting node: {}", node.connection_string());
node.spawn(genesis)?; node.spawn(genesis)?;
Ok(node) Ok(node)
} }
+1 -4
View File
@@ -8,14 +8,11 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true } revive-dt-compiler = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true
+10 -9
View File
@@ -12,19 +12,18 @@ use std::{
}; };
use anyhow::Context; use anyhow::Context;
use serde::Serialize;
use revive_dt_common::types::Mode;
use revive_dt_compiler::{CompilerInput, CompilerOutput}; use revive_dt_compiler::{CompilerInput, CompilerOutput};
use serde::{Deserialize, Serialize};
use revive_dt_config::{Arguments, TestingPlatform}; use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::corpus::Corpus; use revive_dt_format::{corpus::Corpus, mode::SolcMode};
use crate::analyzer::CompilerStatistics; use crate::analyzer::CompilerStatistics;
pub(crate) static REPORTER: OnceLock<Mutex<Report>> = OnceLock::new(); pub(crate) static REPORTER: OnceLock<Mutex<Report>> = OnceLock::new();
/// The `Report` datastructure stores all relevant inforamtion required for generating reports. /// The `Report` datastructure stores all relevant inforamtion required for generating reports.
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Report { pub struct Report {
/// The configuration used during the test. /// The configuration used during the test.
pub config: Arguments, pub config: Arguments,
@@ -42,14 +41,14 @@ pub struct Report {
} }
/// Contains a compiled contract. /// Contains a compiled contract.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompilationTask { pub struct CompilationTask {
/// The observed compiler input. /// The observed compiler input.
pub json_input: CompilerInput, pub json_input: CompilerInput,
/// The observed compiler output. /// The observed compiler output.
pub json_output: Option<CompilerOutput>, pub json_output: Option<CompilerOutput>,
/// The observed compiler mode. /// The observed compiler mode.
pub mode: Mode, pub mode: SolcMode,
/// The observed compiler version. /// The observed compiler version.
pub compiler_version: String, pub compiler_version: String,
/// The observed error, if any. /// The observed error, if any.
@@ -57,7 +56,7 @@ pub struct CompilationTask {
} }
/// Represents a report about a compilation task. /// Represents a report about a compilation task.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompilationResult { pub struct CompilationResult {
/// The observed compilation task. /// The observed compilation task.
pub compilation_task: CompilationTask, pub compilation_task: CompilationTask,
@@ -66,7 +65,7 @@ pub struct CompilationResult {
} }
/// The [Span] struct indicates the context of what is being reported. /// The [Span] struct indicates the context of what is being reported.
#[derive(Clone, Copy, Debug, Serialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Span { pub struct Span {
/// The corpus index this belongs to. /// The corpus index this belongs to.
corpus: usize, corpus: usize,
@@ -185,6 +184,8 @@ impl Report {
let file = File::create(&path).context(path.display().to_string())?; let file = File::create(&path).context(path.display().to_string())?;
serde_json::to_writer_pretty(file, &self)?; serde_json::to_writer_pretty(file, &self)?;
tracing::info!("report written to: {}", path.display());
Ok(()) Ok(())
} }
} }
-3
View File
@@ -19,6 +19,3 @@ reqwest = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
[lints]
workspace = true
+6 -3
View File
@@ -11,14 +11,14 @@ use std::{
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::download::SolcDownloader; use crate::download::GHDownloader;
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);
pub(crate) async fn get_or_download( pub(crate) async fn get_or_download(
working_directory: &Path, working_directory: &Path,
downloader: &SolcDownloader, downloader: &GHDownloader,
) -> anyhow::Result<PathBuf> { ) -> anyhow::Result<PathBuf> {
let target_directory = working_directory let target_directory = working_directory
.join(SOLC_CACHE_DIRECTORY) .join(SOLC_CACHE_DIRECTORY)
@@ -38,8 +38,11 @@ pub(crate) async fn get_or_download(
Ok(target_file) Ok(target_file)
} }
async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::Result<()> { async fn download_to_file(path: &Path, downloader: &GHDownloader) -> anyhow::Result<()> {
tracing::info!("caching file: {}", path.display());
let Ok(file) = File::create_new(path) else { let Ok(file) = File::create_new(path) else {
tracing::debug!("cache file already exists: {}", path.display());
return Ok(()); return Ok(());
}; };
+25 -25
View File
@@ -38,21 +38,21 @@ impl List {
} }
} }
/// Download solc binaries from the official SolidityLang site /// Download solc binaries from GitHub releases (IPFS links aren't reliable).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SolcDownloader { pub struct GHDownloader {
pub version: Version, pub version: Version,
pub target: &'static str, pub target: &'static str,
pub list: &'static str, pub list: &'static str,
} }
impl SolcDownloader { impl GHDownloader {
pub const BASE_URL: &str = "https://binaries.soliditylang.org"; pub const BASE_URL: &str = "https://github.com/ethereum/solidity/releases/download";
pub const LINUX_NAME: &str = "linux-amd64"; pub const LINUX_NAME: &str = "solc-static-linux";
pub const MACOSX_NAME: &str = "macosx-amd64"; pub const MACOSX_NAME: &str = "solc-macos";
pub const WINDOWS_NAME: &str = "windows-amd64"; pub const WINDOWS_NAME: &str = "solc-windows.exe";
pub const WASM_NAME: &str = "wasm"; pub const WASM_NAME: &str = "soljson.js";
async fn new( async fn new(
version: impl Into<VersionOrRequirement>, version: impl Into<VersionOrRequirement>,
@@ -102,26 +102,26 @@ impl SolcDownloader {
Self::new(version, Self::WASM_NAME, List::WASM_URL).await Self::new(version, Self::WASM_NAME, List::WASM_URL).await
} }
/// Returns the download link.
pub fn url(&self) -> String {
format!("{}/v{}/{}", Self::BASE_URL, &self.version, &self.target)
}
/// Download the solc binary. /// Download the solc binary.
/// ///
/// Errors out if the download fails or the digest of the downloaded file /// Errors out if the download fails or the digest of the downloaded file
/// mismatches the expected digest from the release [List]. /// mismatches the expected digest from the release [List].
pub async fn download(&self) -> anyhow::Result<Vec<u8>> { pub async fn download(&self) -> anyhow::Result<Vec<u8>> {
let builds = List::download(self.list).await?.builds; tracing::info!("downloading solc: {self:?}");
let build = builds let expected_digest = List::download(self.list)
.await?
.builds
.iter() .iter()
.find(|build| build.version == self.version) .find(|build| build.version == self.version)
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))?; .ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))
.map(|b| b.sha256.strip_prefix("0x").unwrap_or(&b.sha256).to_string())?;
let path = build.path.clone(); let file = reqwest::get(self.url()).await?.bytes().await?.to_vec();
let expected_digest = build
.sha256
.strip_prefix("0x")
.unwrap_or(&build.sha256)
.to_string();
let url = format!("{}/{}/{}", Self::BASE_URL, self.target, path.display());
let file = reqwest::get(url).await?.bytes().await?.to_vec();
if hex::encode(Sha256::digest(&file)) != expected_digest { if hex::encode(Sha256::digest(&file)) != expected_digest {
anyhow::bail!("sha256 mismatch for solc version {}", self.version); anyhow::bail!("sha256 mismatch for solc version {}", self.version);
@@ -133,7 +133,7 @@ impl SolcDownloader {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{download::SolcDownloader, list::List}; use crate::{download::GHDownloader, list::List};
#[tokio::test] #[tokio::test]
async fn try_get_windows() { async fn try_get_windows() {
@@ -141,7 +141,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
SolcDownloader::windows(version) GHDownloader::windows(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -155,7 +155,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
SolcDownloader::macosx(version) GHDownloader::macosx(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -169,7 +169,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
SolcDownloader::linux(version) GHDownloader::linux(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -180,7 +180,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn try_get_wasm() { async fn try_get_wasm() {
let version = List::download(List::WASM_URL).await.unwrap().latest_release; let version = List::download(List::WASM_URL).await.unwrap().latest_release;
SolcDownloader::wasm(version) GHDownloader::wasm(version)
.await .await
.unwrap() .unwrap()
.download() .download()
+5 -5
View File
@@ -6,7 +6,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use cache::get_or_download; use cache::get_or_download;
use download::SolcDownloader; use download::GHDownloader;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
@@ -25,13 +25,13 @@ pub async fn download_solc(
wasm: bool, wasm: bool,
) -> anyhow::Result<PathBuf> { ) -> anyhow::Result<PathBuf> {
let downloader = if wasm { let downloader = if wasm {
SolcDownloader::wasm(version).await GHDownloader::wasm(version).await
} else if cfg!(target_os = "linux") { } else if cfg!(target_os = "linux") {
SolcDownloader::linux(version).await GHDownloader::linux(version).await
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
SolcDownloader::macosx(version).await GHDownloader::macosx(version).await
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
SolcDownloader::windows(version).await GHDownloader::windows(version).await
} else { } else {
unimplemented!() unimplemented!()
}?; }?;