diff --git a/Cargo.lock b/Cargo.lock index 7e16f158..9d162211 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e9e31d834fe25fe991b8884e4b9f0e59db4a97d86e05d1464d6899c013cd62" +checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" dependencies = [ "alloy-primitives", "num_enum", @@ -373,13 +373,13 @@ dependencies = [ "derive_more", "foldhash 0.2.0", "hashbrown 0.16.1", - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "k256", "keccak-asm", "paste", "proptest", - "rand 0.9.3", + "rand 0.9.4", "rapidhash", "ruint", "rustc-hash", @@ -581,7 +581,7 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.13.1", + "indexmap 2.14.0", "proc-macro-error2", "proc-macro2", "quote", @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -1326,9 +1326,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -1398,7 +1398,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.3", + "rand 0.9.4", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -1592,9 +1592,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1855,15 +1855,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2187,7 +2178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2527,9 +2518,9 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2128a84f7a3850d54ee343334e3392cca61f9f6aa9441eec481b9394b43c238b" +checksum = "368a4a4e4273b0135111fe9464e35465067766a8f664615b5a86338b73864407" dependencies = [ "alloy-primitives", "ethereum_serde_utils", @@ -2607,7 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", - "rand 0.9.3", + "rand 0.9.4", "web-time", ] @@ -2999,7 +2990,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3054,6 +3045,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.9.1" @@ -3136,7 +3133,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.3", + "rand 0.9.4", "ring", "socket2 0.5.10", "thiserror 2.0.18", @@ -3159,7 +3156,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "resolv-conf", "smallvec", "thiserror 2.0.18", @@ -3284,15 +3281,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3554,7 +3550,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.9.3", + "rand 0.9.4", "tokio", "url", "xmltree", @@ -3593,12 +3589,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -3736,9 +3732,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -3794,9 +3790,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libgit2-sys" @@ -4530,14 +4526,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] @@ -4591,9 +4587,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -4750,11 +4746,11 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" dependencies = [ - "core2", + "no_std_io2", "serde", "unsigned-varint 0.8.0", ] @@ -4856,6 +4852,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -5046,7 +5051,7 @@ dependencies = [ "eventsource-stream", "futures-core", "http", - "indexmap 2.13.1", + "indexmap 2.14.0", "oas3", "prettyplease", "proc-macro2", @@ -5103,9 +5108,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ "bitflags", "cfg-if", @@ -5135,9 +5140,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", @@ -5319,7 +5324,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.1", + "indexmap 2.14.0", ] [[package]] @@ -5366,9 +5371,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5995,7 +6000,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.3", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -6156,7 +6161,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -6236,9 +6241,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6332,9 +6337,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -6374,9 +6379,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -6589,7 +6594,7 @@ dependencies = [ "primitive-types", "proptest", "rand 0.8.5", - "rand 0.9.3", + "rand 0.9.4", "rlp", "ruint-macro", "serde_core", @@ -6657,9 +6662,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", @@ -6722,9 +6727,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -6974,7 +6979,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -7035,7 +7040,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.1", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -7062,7 +7067,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -7628,9 +7633,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.0" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -7735,7 +7740,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -7749,7 +7754,7 @@ version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", @@ -7818,7 +7823,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.1", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -8311,9 +8316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -8324,9 +8329,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -8334,9 +8339,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8344,9 +8349,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -8357,9 +8362,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -8381,7 +8386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.1", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -8407,7 +8412,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.1", + "indexmap 2.14.0", "semver 1.0.28", ] @@ -8427,9 +8432,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -8974,7 +8979,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.1", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -9005,7 +9010,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -9024,7 +9029,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.1", + "indexmap 2.14.0", "log", "semver 1.0.28", "serde", @@ -9129,7 +9134,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.9.3", + "rand 0.9.4", "static_assertions", "web-time", ] diff --git a/crates/cli/src/commands/test/mev.rs b/crates/cli/src/commands/test/mev.rs index dfa930c5..305ed499 100644 --- a/crates/cli/src/commands/test/mev.rs +++ b/crates/cli/src/commands/test/mev.rs @@ -1,9 +1,44 @@ //! MEV relay tests. -use super::{TestConfigArgs, helpers::TestCategoryResult}; -use crate::error::Result; +use std::{collections::HashMap, io::Write, time::Duration}; + +use reqwest::{Method, StatusCode}; +use tokio::{task::JoinSet, time::Instant}; +use tokio_util::sync::CancellationToken; +use tracing::info; + +use super::{ + AllCategoriesResult, TestCategory, TestCategoryResult, TestConfigArgs, TestResult, TestVerdict, + calculate_score, + constants::{SLOT_TIME, SLOTS_IN_EPOCH}, + evaluate_rtt, must_output_to_file_on_quiet, publish_result_to_obol_api, request_rtt, + write_result_to_file, write_result_to_writer, +}; +use crate::{ + commands::test::TestCaseName, + duration::Duration as CliDuration, + error::{CliError, Result}, +}; use clap::Args; -use std::io::Write; + +/// MEV-specific errors. +#[derive(Debug, thiserror::Error)] +enum MevError { + /// Relay returned non-200 for the header request. + #[error("status code not 200 OK")] + StatusCodeNot200, + #[error(transparent)] + Cli(#[from] CliError), +} + +/// Thresholds for MEV ping measure test. +const THRESHOLD_MEV_MEASURE_AVG: Duration = Duration::from_millis(40); +/// Threshold for poor MEV ping measure. +const THRESHOLD_MEV_MEASURE_POOR: Duration = Duration::from_millis(100); +/// Threshold for average MEV block creation RTT. +const THRESHOLD_MEV_BLOCK_AVG: Duration = Duration::from_millis(500); +/// Threshold for poor MEV block creation RTT. +const THRESHOLD_MEV_BLOCK_POOR: Duration = Duration::from_millis(800); /// Arguments for the MEV test command. #[derive(Args, Clone, Debug)] @@ -40,11 +75,683 @@ pub struct TestMevArgs { pub number_of_payloads: u32, } +#[derive(Debug, Clone)] +enum TestCaseMev { + Ping, + PingMeasure, + CreateBlock, +} + +impl TestCaseMev { + fn all() -> Vec { + vec![Self::Ping, Self::PingMeasure, Self::CreateBlock] + } + + fn test_case_name(&self) -> TestCaseName { + match self { + TestCaseMev::Ping => TestCaseName::new("Ping", 1), + TestCaseMev::PingMeasure => TestCaseName::new("PingMeasure", 2), + TestCaseMev::CreateBlock => TestCaseName::new("CreateBlock", 3), + } + } + + async fn run(&self, target: &str, conf: &TestMevArgs) -> TestResult { + match self { + TestCaseMev::Ping => mev_ping_test(target, conf).await, + TestCaseMev::PingMeasure => mev_ping_measure_test(target, conf).await, + TestCaseMev::CreateBlock => mev_create_block_test(target, conf).await, + } + } +} + /// Runs the MEV relay tests. -pub async fn run(_args: TestMevArgs, _writer: &mut dyn Write) -> Result { - // TODO: Implement MEV tests - // - Ping - // - PingMeasure - // - CreateBlock - unimplemented!("mev test not yet implemented") +pub async fn run( + args: TestMevArgs, + writer: &mut dyn Write, + token: CancellationToken, +) -> Result { + must_output_to_file_on_quiet(args.test_config.quiet, &args.test_config.output_json)?; + + // Validate flag combinations. + if args.load_test && args.beacon_node_endpoint.is_none() { + return Err(CliError::Other( + "beacon-node-endpoint required when load-test enabled".to_string(), + )); + } + if !args.load_test && args.beacon_node_endpoint.is_some() { + return Err(CliError::Other( + "beacon-node-endpoint only supported when load-test enabled".to_string(), + )); + } + + info!("Starting MEV relays test"); + + let queued_tests = { + let mut filtered = TestCaseMev::all().to_vec(); + if let Some(filtered_cases) = args.test_config.test_cases.as_ref() { + filtered.retain(|case| { + filtered_cases + .iter() + .any(|s| s == case.test_case_name().name) + }); + } + filtered + }; + if queued_tests.is_empty() { + return Err(CliError::TestCaseNotSupported); + } + + let token = token.child_token(); + tokio::spawn({ + let token = token.clone(); + async move { + tokio::time::sleep(args.test_config.timeout).await; + token.cancel(); + } + }); + + let start_time = Instant::now(); + let test_results = test_all_mevs(&queued_tests, &args, token).await; + let exec_time = CliDuration::new(start_time.elapsed()); + + let score = test_results + .values() + .map(|results| calculate_score(results)) + .min(); + + let res = TestCategoryResult { + category_name: Some(TestCategory::Mev), + targets: test_results, + execution_time: Some(exec_time), + score, + }; + + if !args.test_config.quiet { + write_result_to_writer(&res, writer)?; + } + + if !args.test_config.output_json.is_empty() { + write_result_to_file(&res, args.test_config.output_json.as_ref()).await?; + } + + if args.test_config.publish { + publish_result_to_obol_api( + AllCategoriesResult { + mev: Some(res.clone()), + ..Default::default() + }, + &args.test_config.publish_addr, + &args.test_config.publish_private_key_file, + ) + .await?; + } + + Ok(res) +} + +async fn test_all_mevs( + queued_tests: &[TestCaseMev], + conf: &TestMevArgs, + token: CancellationToken, +) -> HashMap> { + let mut join_set = JoinSet::new(); + + for endpoint in &conf.endpoints { + let queued_tests = queued_tests.to_vec(); + let conf = conf.clone(); + let endpoint = endpoint.clone(); + let token = token.clone(); + + join_set.spawn(async move { + let results = test_single_mev(&queued_tests, &conf, &endpoint, token).await; + let relay_name = format_mev_relay_name(&endpoint); + (relay_name, results) + }); + } + + let all_results = join_set.join_all().await; + all_results.into_iter().collect::>() +} + +async fn test_single_mev( + queued_tests: &[TestCaseMev], + conf: &TestMevArgs, + target: &str, + token: CancellationToken, +) -> Vec { + let mut join_set = JoinSet::new(); + + let queued_tests = queued_tests.to_vec(); + for test_case in queued_tests { + let token = token.clone(); + let conf = conf.clone(); + let target = target.to_string(); + + join_set.spawn(async move { + let tc_name = test_case.test_case_name(); + tokio::select! { + _ = token.cancelled() => { + let tr = TestResult::new(tc_name.name); + tr.fail(CliError::TimeoutInterrupted) + } + r = test_case.run(&target, &conf) => { + r + } + } + }); + } + + join_set.join_all().await +} + +async fn mev_ping_test(target: &str, _conf: &TestMevArgs) -> TestResult { + let test_res = TestResult::new("Ping"); + let url = format!("{target}/eth/v1/builder/status"); + let client = reqwest::Client::new(); + + let resp = match client.get(&url).send().await { + Ok(r) => r, + Err(e) => return test_res.fail(e), + }; + + if resp.status().as_u16() > 399 { + return test_res.fail(CliError::Other(http_status_error(resp.status()))); + } + + test_res.ok() +} + +async fn mev_ping_measure_test(target: &str, _conf: &TestMevArgs) -> TestResult { + let test_res = TestResult::new("PingMeasure"); + let url = format!("{target}/eth/v1/builder/status"); + + let rtt = match request_rtt(&url, Method::GET, None, StatusCode::OK).await { + Ok(r) => r, + Err(e) => return test_res.fail(e), + }; + + evaluate_rtt( + rtt, + test_res, + THRESHOLD_MEV_MEASURE_AVG, + THRESHOLD_MEV_MEASURE_POOR, + ) +} + +async fn mev_create_block_test(target: &str, conf: &TestMevArgs) -> TestResult { + let test_res = TestResult::new("CreateBlock"); + + if !conf.load_test { + return TestResult { + verdict: TestVerdict::Skip, + ..test_res + }; + } + + let beacon_endpoint = match &conf.beacon_node_endpoint { + Some(ep) => ep.as_str(), + None => { + return test_res.fail(CliError::Other("beacon-node-endpoint required".to_string())); + } + }; + + let latest_block = match latest_beacon_block(beacon_endpoint).await { + Ok(b) => b, + Err(e) => return test_res.fail(e), + }; + + let latest_block_ts_unix: i64 = match latest_block.body.execution_payload.timestamp.parse() { + Ok(v) => v, + Err(e) => return test_res.fail(CliError::Other(format!("parse timestamp: {e}"))), + }; + + let latest_block_ts = std::time::UNIX_EPOCH + .checked_add(Duration::from_secs(latest_block_ts_unix.unsigned_abs())) + .unwrap_or(std::time::UNIX_EPOCH); + let next_block_ts = latest_block_ts + .checked_add(SLOT_TIME) + .unwrap_or(latest_block_ts); + + if let Ok(remaining) = next_block_ts.duration_since(std::time::SystemTime::now()) { + tokio::time::sleep(remaining).await; + } + + let latest_slot: i64 = match latest_block.slot.parse() { + Ok(v) => v, + Err(e) => return test_res.fail(CliError::Other(format!("parse slot: {e}"))), + }; + + let mut next_slot = latest_slot.saturating_add(1); + let slots_in_epoch_i64 = i64::try_from(SLOTS_IN_EPOCH.get()).unwrap_or(i64::MAX); + let epoch = next_slot.checked_div(slots_in_epoch_i64).unwrap_or(0); + + let mut proposer_duties = match fetch_proposers_for_epoch(beacon_endpoint, epoch).await { + Ok(d) => d, + Err(e) => return test_res.fail(e), + }; + + let mut all_blocks_rtt: Vec = Vec::new(); + + info!( + mev_relay = target, + blocks = conf.number_of_payloads, + "Starting attempts for block creation" + ); + + let mut latest_block = latest_block; + + loop { + let start_iteration = Instant::now(); + + let rtt = match create_mev_block( + conf, + target, + next_slot, + &mut latest_block, + &mut proposer_duties, + beacon_endpoint, + ) + .await + { + Ok(r) => r, + Err(e) => return test_res.fail(e), + }; + + all_blocks_rtt.push(rtt); + if all_blocks_rtt.len() == usize::try_from(conf.number_of_payloads).unwrap_or(usize::MAX) { + break; + } + + let elapsed = start_iteration.elapsed(); + let elapsed_nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX); + let slot_nanos = u64::try_from(SLOT_TIME.as_nanos()).unwrap_or(1); + let remainder_nanos = elapsed_nanos.checked_rem(slot_nanos).unwrap_or(0); + let slot_remainder = SLOT_TIME + .checked_sub(Duration::from_nanos(remainder_nanos)) + .unwrap_or_default(); + if let Some(sleep_dur) = slot_remainder.checked_sub(Duration::from_secs(1)) { + tokio::time::sleep(sleep_dur).await; + } + + let start_beacon_fetch = Instant::now(); + latest_block = match latest_beacon_block(beacon_endpoint).await { + Ok(b) => b, + Err(e) => return test_res.fail(e), + }; + + let latest_slot_parsed: i64 = match latest_block.slot.parse() { + Ok(v) => v, + Err(e) => return test_res.fail(CliError::Other(format!("parse slot: {e}"))), + }; + + next_slot = latest_slot_parsed.saturating_add(1); + + // Wait 1 second minus how long the fetch took. + if let Some(sleep_dur) = Duration::from_secs(1).checked_sub(start_beacon_fetch.elapsed()) { + tokio::time::sleep(sleep_dur).await; + } + } + + if all_blocks_rtt.is_empty() { + return test_res.fail(CliError::TimeoutInterrupted); + } + + let total_rtt: Duration = all_blocks_rtt.iter().sum(); + let count = u32::try_from(all_blocks_rtt.len().max(1)).unwrap_or(u32::MAX); + let average_rtt = total_rtt.checked_div(count).unwrap_or_default(); + + evaluate_rtt( + average_rtt, + test_res, + THRESHOLD_MEV_BLOCK_AVG, + THRESHOLD_MEV_BLOCK_POOR, + ) +} + +// Helper types +#[derive(Debug, Clone, serde::Deserialize)] +struct BeaconBlock { + data: BeaconBlockData, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct BeaconBlockData { + message: BeaconBlockMessage, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct BeaconBlockMessage { + slot: String, + body: BeaconBlockBody, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct BeaconBlockBody { + execution_payload: BeaconBlockExecPayload, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct BeaconBlockExecPayload { + block_hash: String, + timestamp: String, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct ProposerDuties { + data: Vec, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct ProposerDutiesData { + pubkey: String, + slot: String, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct BuilderBidResponse { + version: String, + data: serde_json::Value, +} + +async fn latest_beacon_block(endpoint: &str) -> Result { + let url = format!("{endpoint}/eth/v2/beacon/blocks/head"); + let resp = reqwest::Client::new().get(&url).send().await?; + let body = resp.bytes().await?; + let block: BeaconBlock = serde_json::from_slice(&body)?; + + Ok(block.data.message) +} + +async fn fetch_proposers_for_epoch( + beacon_endpoint: &str, + epoch: i64, +) -> Result> { + let url = format!("{beacon_endpoint}/eth/v1/validator/duties/proposer/{epoch}"); + let resp = reqwest::Client::new().get(&url).send().await?; + let body = resp.bytes().await?; + let duties: ProposerDuties = serde_json::from_slice(&body)?; + + Ok(duties.data) +} + +fn get_validator_pk_for_slot(proposers: &[ProposerDutiesData], slot: i64) -> Option { + let slot_str = slot.to_string(); + proposers + .iter() + .find(|p| p.slot == slot_str) + .map(|p| p.pubkey.clone()) +} + +async fn get_block_header( + target: &str, + next_slot: i64, + block_hash: &str, + validator_pub_key: &str, +) -> std::result::Result<(BuilderBidResponse, Duration), MevError> { + let url = + format!("{target}/eth/v1/builder/header/{next_slot}/{block_hash}/{validator_pub_key}"); + + let start = Instant::now(); + + let resp = reqwest::Client::new() + .get(&url) + .send() + .await + .map_err(|e| MevError::Cli(e.into()))?; + + let rtt = start.elapsed(); + + if resp.status() != StatusCode::OK { + return Err(MevError::StatusCodeNot200); + } + + let body = resp.bytes().await.map_err(|e| MevError::Cli(e.into()))?; + + let bid: BuilderBidResponse = + serde_json::from_slice(&body).map_err(|e| MevError::Cli(e.into()))?; + + Ok((bid, rtt)) +} + +async fn create_mev_block( + _conf: &TestMevArgs, + target: &str, + mut next_slot: i64, + latest_block: &mut BeaconBlockMessage, + proposer_duties: &mut Vec, + beacon_endpoint: &str, +) -> Result { + let rtt_get_header; + let builder_bid; + + loop { + let start_iteration = Instant::now(); + let slots_in_epoch_i64 = i64::try_from(SLOTS_IN_EPOCH.get()).unwrap_or(i64::MAX); + let epoch = next_slot.checked_div(slots_in_epoch_i64).unwrap_or(0); + + let pk = if let Some(pk) = get_validator_pk_for_slot(proposer_duties, next_slot) { + pk + } else { + *proposer_duties = fetch_proposers_for_epoch(beacon_endpoint, epoch).await?; + get_validator_pk_for_slot(proposer_duties, next_slot) + .ok_or_else(|| CliError::Other("slot not found".to_string()))? + }; + + match get_block_header( + target, + next_slot, + &latest_block.body.execution_payload.block_hash, + &pk, + ) + .await + { + Ok((bid, rtt)) => { + builder_bid = bid; + rtt_get_header = rtt; + + info!( + slot = next_slot, + target = target, + "Created block headers for slot" + ); + break; + } + + Err(MevError::StatusCodeNot200) => { + let elapsed = start_iteration.elapsed(); + if let Some(sleep_dur) = SLOT_TIME.checked_sub(elapsed) + && let Some(sleep_dur) = sleep_dur.checked_sub(Duration::from_secs(1)) + { + tokio::time::sleep(sleep_dur).await; + } + + let start_beacon_fetch = Instant::now(); + *latest_block = latest_beacon_block(beacon_endpoint).await?; + next_slot = next_slot.saturating_add(1); + + if let Some(sleep_dur) = + Duration::from_secs(1).checked_sub(start_beacon_fetch.elapsed()) + { + tokio::time::sleep(sleep_dur).await; + } + + continue; + } + Err(MevError::Cli(e)) => return Err(e), + } + } + + let payload = build_blinded_block_payload(&builder_bid)?; + let payload_json = serde_json::to_vec(&payload).map_err(|e| { + CliError::Other(format!( + "signed blinded beacon block json payload marshal: {e}" + )) + })?; + + let rtt_submit_block = request_rtt( + format!("{target}/eth/v1/builder/blinded_blocks"), + Method::POST, + Some(payload_json), + StatusCode::BAD_REQUEST, + ) + .await?; + + Ok(rtt_get_header + .checked_add(rtt_submit_block) + .unwrap_or(rtt_get_header)) +} + +fn build_blinded_block_payload(bid: &BuilderBidResponse) -> Result { + let sig_hex = "0xb9251a82040d4620b8c5665f328ee6c2eaa02d31d71d153f4abba31a7922a981e541e85283f0ced387d26e86aef9386d18c6982b9b5f8759882fe7f25a328180d86e146994ef19d28bc1432baf29751dec12b5f3d65dbbe224d72cf900c6831a"; + + let header = extract_execution_payload_header(&bid.data, &bid.version)?; + + let zero_hash = "0x0000000000000000000000000000000000000000000000000000000000000000"; + let zero_sig = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + let mut body = serde_json::json!({ + "randao_reveal": zero_sig, + "eth1_data": { + "deposit_root": zero_hash, + "deposit_count": "0", + "block_hash": zero_hash + }, + "graffiti": zero_hash, + "proposer_slashings": [], + "attester_slashings": [], + "attestations": [], + "deposits": [], + "voluntary_exits": [], + "sync_aggregate": { + "sync_committee_bits": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "sync_committee_signature": zero_sig + }, + "execution_payload_header": header + }); + + let version_lower = bid.version.to_lowercase(); + + if matches!( + version_lower.as_str(), + "capella" | "deneb" | "electra" | "fulu" + ) { + body["bls_to_execution_changes"] = serde_json::json!([]); + } + + if matches!(version_lower.as_str(), "deneb" | "electra" | "fulu") { + body["blob_kzg_commitments"] = serde_json::json!([]); + } + + if matches!(version_lower.as_str(), "electra" | "fulu") { + body["execution_requests"] = serde_json::json!({ + "deposits": [], + "withdrawals": [], + "consolidations": [] + }); + } + + Ok(serde_json::json!({ + "message": { + "slot": "0", + "proposer_index": "0", + "parent_root": zero_hash, + "state_root": zero_hash, + "body": body + }, + "signature": sig_hex + })) +} + +fn extract_execution_payload_header( + data: &serde_json::Value, + version: &str, +) -> Result { + data.get("message") + .and_then(|m| m.get("header")) + .cloned() + .ok_or_else(|| { + CliError::Other(format!( + "not supported version or missing header: {version}" + )) + }) +} + +fn format_mev_relay_name(url_string: &str) -> String { + let Some((scheme, rest)) = url_string.split_once("://") else { + return url_string.to_string(); + }; + + let Some((hash, host)) = rest.split_once('@') else { + return url_string.to_string(); + }; + + if !hash.starts_with("0x") || hash.len() < 18 { + return url_string.to_string(); + } + + let hash_short = format!("{}...{}", &hash[..6], &hash[hash.len().saturating_sub(4)..]); + format!("{scheme}://{hash_short}@{host}") +} + +fn http_status_error(status: StatusCode) -> String { + format!("status code {}", status.as_u16()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_mev_relay_name() { + assert_eq!( + format_mev_relay_name( + "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net" + ), + "https://0xac6e...37ae@boost-relay.flashbots.net" + ); + + assert_eq!( + format_mev_relay_name("boost-relay.flashbots.net"), + "boost-relay.flashbots.net" + ); + + assert_eq!( + format_mev_relay_name("https://boost-relay.flashbots.net"), + "https://boost-relay.flashbots.net" + ); + + assert_eq!( + format_mev_relay_name("https://0xshort@boost-relay.flashbots.net"), + "https://0xshort@boost-relay.flashbots.net" + ); + + assert_eq!( + format_mev_relay_name("https://noprefixhashvalue1234567890@boost-relay.flashbots.net"), + "https://noprefixhashvalue1234567890@boost-relay.flashbots.net" + ); + } + + #[test] + fn test_get_validator_pk_for_slot() { + let duties = vec![ + ProposerDutiesData { + pubkey: "0xabc".to_string(), + slot: "100".to_string(), + }, + ProposerDutiesData { + pubkey: "0xdef".to_string(), + slot: "101".to_string(), + }, + ]; + + assert_eq!( + get_validator_pk_for_slot(&duties, 100), + Some("0xabc".to_string()) + ); + assert_eq!( + get_validator_pk_for_slot(&duties, 101), + Some("0xdef".to_string()) + ); + assert_eq!(get_validator_pk_for_slot(&duties, 102), None); + } } diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index d59ac1b9..d7163982 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -64,11 +64,11 @@ pub enum CliError { /// Test timeout or interrupted. #[error("timeout/interrupted")] - _TimeoutInterrupted, + TimeoutInterrupted, /// Test case not supported. #[error("test case not supported")] - _TestCaseNotSupported, + TestCaseNotSupported, /// Relay P2P error. #[error("Relay P2P error: {0}")] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 65551bc0..dfa06ca6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -55,7 +55,7 @@ async fn run() -> std::result::Result<(), CliError> { Commands::Relay(args) => { let config: pluto_relay_server::config::Config = (*args).clone().try_into()?; pluto_tracing::init(&config.log_config).expect("Failed to initialize tracing"); - commands::relay::run(config, ct.clone()).await + commands::relay::run(config, ct).await } Commands::Alpha(args) => match args.command { AlphaCommands::Test(args) => { @@ -67,7 +67,7 @@ async fn run() -> std::result::Result<(), CliError> { TestCommands::Beacon(args) => { pluto_tracing::init(&pluto_tracing::TracingConfig::default()) .expect("Failed to initialize tracing"); - commands::test::beacon::run(args, &mut stdout, ct.clone()) + commands::test::beacon::run(args, &mut stdout, ct) .await .map(|_| ()) } @@ -76,7 +76,7 @@ async fn run() -> std::result::Result<(), CliError> { .await .map(|_| ()) } - TestCommands::Mev(args) => commands::test::mev::run(args, &mut stdout) + TestCommands::Mev(args) => commands::test::mev::run(args, &mut stdout, ct) .await .map(|_| ()), TestCommands::Infra(args) => commands::test::infra::run(args, &mut stdout)