diff --git a/Cargo.lock b/Cargo.lock index 10e9c4c..808e4f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,7 @@ dependencies = [ "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde", + "alloy-serde 0.3.6", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -80,7 +80,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", - "alloy-serde", + "alloy-serde 0.3.6", "c-kzg", "serde", ] @@ -108,21 +108,22 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b095eb0533144b4497e84a9cc3e44a5c2e3754a3983c0376a55a2f9183a53e" +checksum = "b72bf30967a232bec83809bea1623031f6285a013096229330c68c406192a4ca" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-primitives", + "alloy-rlp", "alloy-sol-types", ] [[package]] name = "alloy-dyn-abi" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4004925bff5ba0a11739ae84dbb6601a981ea692f3bd45b626935ee90a6b8471" +checksum = "f5228b189b18b85761340dc9eaac0141148a8503657b36f9bc3a869413d987ca" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -168,7 +169,7 @@ dependencies = [ "alloy-eip7702", "alloy-primitives", "alloy-rlp", - "alloy-serde", + "alloy-serde 0.3.6", "c-kzg", "derive_more", "once_cell", @@ -183,15 +184,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a7a18afb0b318616b6b2b0e2e7ac5529d32a966c673b48091c9919e284e6aca" dependencies = [ "alloy-primitives", - "alloy-serde", + "alloy-serde 0.3.6", "serde", ] [[package]] name = "alloy-json-abi" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9996daf962fd0a90d3c93b388033228865953b92de7bb1959b891d78750a4091" +checksum = "31a0f0d51db8a1a30a4d98a9f90e090a94c8f44cb4d9eafc7e03aa6d00aae984" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -225,7 +226,7 @@ dependencies = [ "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde", + "alloy-serde 0.3.6", "alloy-signer", "alloy-sol-types", "async-trait", @@ -242,7 +243,7 @@ checksum = "94ad40869867ed2d9cd3842b1e800889e5b49e6b92da346e93862b4a741bedf3" dependencies = [ "alloy-eips", "alloy-primitives", - "alloy-serde", + "alloy-serde 0.3.6", "serde", ] @@ -265,23 +266,29 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411aff151f2a73124ee473708e82ed51b2535f68928b6a1caa8bc1246ae6f7cd" +checksum = "8edae627382349b56cd6a7a2106f4fd69b243a9233e560c55c2e03cabb7e1d3c" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", "derive_more", + "foldhash", + "hashbrown 0.15.0", "hex-literal", + "indexmap", "itoa", "k256", "keccak-asm", + "paste", "proptest", "rand", "ruint", + "rustc-hash", "serde", + "sha3", "tiny-keccak", ] @@ -363,7 +370,7 @@ checksum = "4d0f2d905ebd295e7effec65e5f6868d153936130ae718352771de3e7d03c75c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -399,7 +406,8 @@ checksum = "64333d639f2a0cf73491813c629a405744e16343a4bc5640931be707c345ecc5" dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", - "alloy-serde", + "alloy-rpc-types-mev", + "alloy-serde 0.3.6", "serde", ] @@ -410,7 +418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25cb45ad7c0930dd62eecf164d2afe4c3d2dd2c82af85680ad1f118e1e5cb83" dependencies = [ "alloy-primitives", - "alloy-serde", + "alloy-serde 0.3.6", "serde", ] @@ -438,16 +446,29 @@ dependencies = [ "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde", + "alloy-serde 0.3.6", "alloy-sol-types", "cfg-if", "derive_more", - "hashbrown", + "hashbrown 0.14.5", "itertools 0.13.0", "serde", "serde_json", ] +[[package]] +name = "alloy-rpc-types-mev" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922d92389e5022650c4c60ffd2f9b2467c3f853764f0f74ff16a23106f9017d5" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde 0.3.6", + "serde", + "serde_json", +] + [[package]] name = "alloy-serde" version = "0.3.6" @@ -459,6 +480,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloy-serde" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028e72eaa9703e4882344983cfe7636ce06d8cce104a78ea62fd19b46659efc4" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + [[package]] name = "alloy-signer" version = "0.3.6" @@ -491,23 +523,23 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0458ccb02a564228fcd76efb8eb5a520521a8347becde37b402afec9a1b83859" +checksum = "841eabaa4710f719fddbc24c95d386eae313f07e6da4babc25830ee37945be0c" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc65475025fc1e84bf86fc840f04f63fcccdcf3cf12053c99918e4054dfbc69" +checksum = "6672337f19d837b9f7073c45853aeb528ed9f7dd6a4154ce683e9e5cb7794014" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -517,16 +549,16 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed10f0715a0b69fde3236ff3b9ae5f6f7c97db5a387747100070d3016b9266b" +checksum = "0dff37dd20bfb118b777c96eda83b2067f4226d2644c5cfa00187b3bc01770ba" dependencies = [ "alloy-json-abi", "const-hex", @@ -535,15 +567,15 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.77", + "syn 2.0.86", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edae8ea1de519ccba896b6834dec874230f72fe695ff3c9c118e90ec7cff783" +checksum = "5b853d42292dbb159671a3edae3b2750277ff130f32b726fe07dc2b17aa6f2b5" dependencies = [ "serde", "winnow", @@ -551,9 +583,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eb88e4da0a1b697ed6a9f811fdba223cf4d5c21410804fd1707836af73a462b" +checksum = "aa828bb1b9a6dc52208fbb18084fb9ce2c30facc2bfda6a5d922349b4990354f" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -831,7 +863,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -842,7 +874,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -856,6 +888,12 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "auto_impl" version = "1.2.0" @@ -864,7 +902,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -1017,6 +1055,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -1054,7 +1098,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -1069,6 +1113,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-hex" version = "1.12.0" @@ -1093,11 +1147,14 @@ name = "contender" version = "0.1.0" dependencies = [ "alloy", + "alloy-serde 0.5.4", "async-trait", "eyre", "futures", + "jsonrpsee", "rand", "serde", + "serde_json", "tokio", ] @@ -1226,7 +1283,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1276,7 +1333,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", "unicode-xid", ] @@ -1435,6 +1492,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1482,9 +1545,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1492,9 +1555,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1509,38 +1572,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1605,6 +1668,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1616,13 +1698,23 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "foldhash", + "serde", +] + [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1710,6 +1802,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1720,6 +1813,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1799,7 +1910,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1853,6 +1965,26 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.70" @@ -1862,6 +1994,74 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpsee" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c71d8c1a731cc4227c2f698d377e7848ca12c8a48866fc5e6951c43a4db843" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2882f6f8acb9fdaec7cefc4fd607119a9bd709831df7d7672a1d3b644628280" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3638bc4617f96675973253b3a45006933bde93c2fd8a6170b33c777cc389e5b" +dependencies = [ + "async-trait", + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower 0.4.13", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a178c60086f24cc35bb82f57c651d0d25d99c4742b4d335de04e97fa1f08a8a1" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "k256" version = "0.13.3" @@ -1875,6 +2075,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "keccak-asm" version = "0.1.4" @@ -1941,7 +2150,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -2050,7 +2259,7 @@ checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -2091,7 +2300,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -2211,7 +2420,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -2290,7 +2499,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -2374,6 +2583,7 @@ dependencies = [ "libc", "rand_chacha", "rand_core", + "serde", ] [[package]] @@ -2549,6 +2759,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -2592,6 +2808,7 @@ version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2600,6 +2817,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.1.3" @@ -2616,6 +2846,33 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +[[package]] +name = "rustls-platform-verifier" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-roots", + "winapi", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.102.8" @@ -2651,6 +2908,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.24" @@ -2699,6 +2965,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "libc", + "num-bigint", "security-framework-sys", ] @@ -2759,14 +3026,14 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -2817,6 +3084,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sha3-asm" version = "0.1.4" @@ -2835,9 +3112,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" -version = "2.2.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" dependencies = [ "digest 0.10.7", "rand_core", @@ -2915,7 +3192,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -2937,9 +3214,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" dependencies = [ "proc-macro2", "quote", @@ -2948,14 +3225,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.3" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b95156f8b577cb59dc0b1df15c6f29a10afc5f8a7ac9786b0b5c68c19149278" +checksum = "16320d4a2021ba1a32470b3759676114a918885e9800e68ad60f2c67969fba62" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -3009,7 +3286,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -3047,9 +3324,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -3069,7 +3346,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -3181,6 +3458,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3215,6 +3493,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3228,7 +3507,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -3389,6 +3668,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3426,7 +3715,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", "wasm-bindgen-shared", ] @@ -3460,7 +3749,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3496,6 +3785,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-registry" version = "0.2.0" @@ -3663,7 +3983,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] [[package]] @@ -3683,5 +4003,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.86", ] diff --git a/Cargo.toml b/Cargo.toml index 62fdcd0..b87d6e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,18 +9,22 @@ name = "contender_core" path = "src/lib.rs" [dependencies] -alloy = { workspace = true, features = ["full", "node-bindings"] } -eyre = "0.6.12" +alloy = { workspace = true, features = ["full", "node-bindings", "rpc-types-mev"] } +eyre = { workspace = true } rand = "0.8.5" -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } futures = "0.3.30" async-trait = "0.1.82" tokio = { workspace = true } +jsonrpsee = { version = "0.24", features = ["http-client", "client-core"] } +alloy-serde = "0.5.4" +serde_json = "1.0.132" [workspace] members = ["cli", "sqlite_db", "testfile"] [workspace.dependencies] +eyre = "0.6.12" tokio = { version = "1.40.0" } alloy = { version = "0.3.6" } serde = "1.0.209" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index fc2bd94..0687ded 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -25,6 +25,14 @@ pub enum ContenderSubcommand { /// The HTTP JSON-RPC URL to spam with requests. rpc_url: String, + /// HTTP JSON-RPC URL to use for bundle spamming (must support `eth_sendBundle`). + #[arg( + short, + long, + long_help = "HTTP JSON-RPC URL to use for bundle spamming (must support `eth_sendBundle`)" + )] + builder_url: Option, + /// The number of txs to send per second using the timed spammer. This is the default spammer. /// May not be set if `txs_per_block` is set. #[arg(long, long_help = "Number of txs to send per second. Must not be set if --txs-per-block is set.", visible_aliases = &["tps"])] diff --git a/cli/src/main.rs b/cli/src/main.rs index 7458ec6..ec11724 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -14,7 +14,7 @@ use commands::{ContenderCli, ContenderSubcommand}; use contender_core::{ db::{DbOps, RunTx}, generator::{ - types::{AnyProvider, FunctionCallDefinition}, + types::{AnyProvider, FunctionCallDefinition, SpamRequest}, RandSeed, }, spammer::{BlockwiseSpammer, LogCallback, NilCallback, TimedSpammer}, @@ -77,13 +77,17 @@ async fn main() -> Result<(), Box> { .map(|key| PrivateKeySigner::from_str(&key).expect("invalid private key")) .collect::>(); let signers = get_signers_with_defaults(private_keys); - check_private_keys(&testconfig.setup.to_owned().unwrap_or(vec![]), &signers); + check_private_keys( + &testconfig.setup.to_owned().unwrap_or(vec![]), + signers.as_slice(), + ); check_balances(&user_signers, min_balance, &rpc_client).await; let scenario = TestScenario::new( testconfig.to_owned(), Arc::new(DB.clone()), url, + None, RandSeed::new(), &signers, ); @@ -95,6 +99,7 @@ async fn main() -> Result<(), Box> { ContenderSubcommand::Spam { testfile, rpc_url, + builder_url, txs_per_block, txs_per_second, duration, @@ -117,8 +122,22 @@ async fn main() -> Result<(), Box> { .spam .as_ref() .expect("No spam function calls found in testfile"); - check_private_keys(spam, &signers); - check_balances(&signers, min_balance, &rpc_client).await; + + // distill all FunctionCallDefinitions from the spam requests + let mut fn_calls = vec![]; + for s in spam { + match s { + SpamRequest::Tx(fn_call) => { + fn_calls.push(fn_call.to_owned()); + } + SpamRequest::Bundle(bundle) => { + fn_calls.extend(bundle.txs.iter().map(|s| s.to_owned())); + } + } + } + + check_private_keys(&fn_calls, signers.as_slice()); + check_balances(signers.as_slice(), min_balance, &rpc_client).await; if txs_per_block.is_some() && txs_per_second.is_some() { panic!("Cannot set both --txs-per-block and --txs-per-second"); @@ -128,8 +147,14 @@ async fn main() -> Result<(), Box> { } if let Some(txs_per_block) = txs_per_block { - let scenario = - TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers); + let scenario = TestScenario::new( + testconfig, + DB.clone().into(), + url, + builder_url.map(|url| Url::parse(&url).expect("Invalid builder URL")), + rand_seed, + &signers, + ); println!("Blockwise spamming with {} txs per block", txs_per_block); match spam_callback_default(!disable_reports, Arc::new(rpc_client).into()).await { SpamCallbackType::Log(cback) => { @@ -138,14 +163,14 @@ async fn main() -> Result<(), Box> { .expect("Time went backwards") .as_millis(); let run_id = DB.insert_run(timestamp as u64, txs_per_block * duration)?; - let spammer = BlockwiseSpammer::new(scenario, cback); + let mut spammer = BlockwiseSpammer::new(scenario, cback).await; spammer .spam_rpc(txs_per_block, duration, Some(run_id.into())) .await?; println!("Saved run. run_id = {}", run_id); } SpamCallbackType::Nil(cback) => { - let spammer = BlockwiseSpammer::new(scenario, cback); + let mut spammer = BlockwiseSpammer::new(scenario, cback).await; spammer.spam_rpc(txs_per_block, duration, None).await?; } }; @@ -154,8 +179,14 @@ async fn main() -> Result<(), Box> { // NOTE: private keys are not currently used for timed spamming. // Timed spamming only works with unlocked accounts, because it uses the `eth_sendTransaction` RPC method. - let scenario = - TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers); + let scenario = TestScenario::new( + testconfig, + DB.clone().into(), + url, + None, + rand_seed, + &signers, + ); let tps = txs_per_second.unwrap_or(10); println!("Timed spamming with {} txs per second", tps); let spammer = TimedSpammer::new(scenario, NilCallback::new()); diff --git a/cli/testfile.toml b/cli/testfile.toml new file mode 100644 index 0000000..c2cb664 --- /dev/null +++ b/cli/testfile.toml @@ -0,0 +1,46 @@ +[[create]] +bytecode = "0x6080604052348015600f57600080fd5b506105668061001f6000396000f3fe60806040526004361061004a5760003560e01c806369f86ec81461004f5780639402c00414610066578063a329e8de14610086578063c5eeaf17146100a6578063fb0e722b146100ae575b600080fd5b34801561005b57600080fd5b506100646100d9565b005b34801561007257600080fd5b50610064610081366004610218565b6100e4565b34801561009257600080fd5b506100646100a13660046102d1565b610119565b610064610145565b3480156100ba57600080fd5b506100c3610174565b6040516100d0919061030e565b60405180910390f35b5b60325a116100da57565b6000816040516020016100f892919061037b565b60405160208183030381529060405260009081610115919061044f565b5050565b600061012660d98361050e565b905060005b8181101561014057600160008190550161012b565b505050565b60405141903480156108fc02916000818181858888f19350505050158015610171573d6000803e3d6000fd5b50565b6000805461018190610341565b80601f01602080910402602001604051908101604052809291908181526020018280546101ad90610341565b80156101fa5780601f106101cf576101008083540402835291602001916101fa565b820191906000526020600020905b8154815290600101906020018083116101dd57829003601f168201915b505050505081565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561022a57600080fd5b813567ffffffffffffffff81111561024157600080fd5b8201601f8101841361025257600080fd5b803567ffffffffffffffff81111561026c5761026c610202565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561029b5761029b610202565b6040528181528282016020018610156102b357600080fd5b81602084016020830137600091810160200191909152949350505050565b6000602082840312156102e357600080fd5b5035919050565b60005b838110156103055781810151838201526020016102ed565b50506000910152565b602081526000825180602084015261032d8160408501602087016102ea565b601f01601f19169190910160400192915050565b600181811c9082168061035557607f821691505b60208210810361037557634e487b7160e01b600052602260045260246000fd5b50919050565b600080845461038981610341565b6001821680156103a057600181146103b5576103e5565b60ff19831686528115158202860193506103e5565b87600052602060002060005b838110156103dd578154888201526001909101906020016103c1565b505081860193505b50505083516103f88183602088016102ea565b01949350505050565b601f82111561014057806000526020600020601f840160051c810160208510156104285750805b601f840160051c820191505b818110156104485760008155600101610434565b5050505050565b815167ffffffffffffffff81111561046957610469610202565b61047d816104778454610341565b84610401565b6020601f8211600181146104b157600083156104995750848201515b600019600385901b1c1916600184901b178455610448565b600084815260208120601f198516915b828110156104e157878501518255602094850194600190920191016104c1565b50848210156104ff5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b60008261052b57634e487b7160e01b600052601260045260246000fd5b50049056fea264697066735822122043ea7522db98264cdc5157a0f2d3f9fc75e28c6078f917dfe9a946bf9b21af7f64736f6c634300081b0033" +name = "SpamMe" +from = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + + +# spam single tx +[[spam]] + +[spam.tx] +to = "0x90F79bf6EB2c4f870365E785982E1f101E93b906" +from = "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" +signature = "transfer()" +value = "100000000000000" + + +# spam bundle +[[spam]] + +[[spam.bundle.tx]] +to = "{SpamMe}" +from = "0x90F79bf6EB2c4f870365E785982E1f101E93b906" +signature = "consumeGas(uint256)" +args = ["510000"] + + +[[spam.bundle.tx]] +to = "{SpamMe}" +from = "0x90F79bf6EB2c4f870365E785982E1f101E93b906" +signature = "tipCoinbase()" +value = "10000000000000000" + +[[spam]] + +[[spam.bundle.tx]] +to = "{SpamMe}" +from = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" +signature = "consumeGas(uint256)" +args = ["510000"] + + +[[spam.bundle.tx]] +to = "{SpamMe}" +from = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" +signature = "tipCoinbase()" +value = "10000000000000000" \ No newline at end of file diff --git a/src/bundle_provider.rs b/src/bundle_provider.rs new file mode 100644 index 0000000..d38692b --- /dev/null +++ b/src/bundle_provider.rs @@ -0,0 +1,91 @@ +use alloy::primitives::{Bytes, B256}; +use jsonrpsee::http_client::HttpClient; +use jsonrpsee::{core::client::ClientT, rpc_params}; +use serde::{Deserialize, Serialize}; + +pub struct BundleClient { + client: HttpClient, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EthSendBundleResponse { + pub bundle_hash: B256, +} + +impl BundleClient { + pub fn new(url: String) -> Self { + let client = HttpClient::builder() + .build(url) + .expect("failed to connect to RPC provider"); + Self { client } + } + + pub async fn send_bundle(&self, bundle: EthSendBundle) -> Result<(), String> { + // Result contents optional because some endpoints don't return this response + let res: Result, _> = self + .client + .request("eth_sendBundle", rpc_params![bundle]) + .await; + if let Err(e) = res { + return Err(format!("Failed to send bundle: {:?}", e)); + } + + Ok(()) + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EthSendBundle { + /// A list of hex-encoded signed transactions + pub txs: Vec, + /// hex-encoded block number for which this bundle is valid + #[serde(with = "alloy_serde::quantity")] + pub block_number: u64, + /// unix timestamp when this bundle becomes active + #[serde( + default, + with = "alloy_serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub min_timestamp: Option, + /// unix timestamp how long this bundle stays valid + #[serde( + default, + with = "alloy_serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub max_timestamp: Option, + /// list of hashes of possibly reverting txs + #[serde( + default + // this doesn't work on rbuilder: + // , skip_serializing_if = "Vec::is_empty" + )] + pub reverting_tx_hashes: Vec, + /// UUID that can be used to cancel/replace this bundle + #[serde( + default, + rename = "replacementUuid", + skip_serializing_if = "Option::is_none" + )] + pub replacement_uuid: Option, +} + +impl EthSendBundle { + pub fn new_basic(txs: Vec, block_number: u64) -> Self { + Self { + txs, + block_number, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: Vec::new(), + replacement_uuid: None, + } + } + + pub async fn send_to_builder(&self, client: &BundleClient) -> Result<(), String> { + client.send_bundle(self.clone()).await + } +} diff --git a/src/error/mod.rs b/src/error.rs similarity index 100% rename from src/error/mod.rs rename to src/error.rs diff --git a/src/generator/mod.rs b/src/generator/mod.rs index a7aec03..1e62d1e 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -9,100 +9,48 @@ use crate::{ Result, }; use alloy::primitives::U256; -use alloy::rpc::types::TransactionRequest; use async_trait::async_trait; +use named_txs::ExecutionRequest; +pub use named_txs::NamedTxRequestBuilder; pub use seeder::rand_seed::RandSeed; use std::{collections::HashMap, fmt::Debug, hash::Hash}; +use types::SpamRequest; pub use types::{CallbackResult, NamedTxRequest, PlanType}; +/// Defines named tx requests, which are used to store transaction requests with optional names and kinds. +/// Used for tracking transactions in a test scenario. +pub mod named_txs; + /// Generates values for fuzzed parameters. /// Contains the Seeder trait and an implementation. pub mod seeder; + /// Provides templating for transaction requests, etc. /// Contains the Templater trait and an implementation. pub mod templater; + /// Contains types used by the generator module. pub mod types; + /// Utility functions used in the generator module. pub mod util; -/// Syntactical sugar for creating a [`NamedTxRequest`]. -/// -/// This is useful for imperatively assigning optional fields to a tx. -/// It is _not_ useful when you're dynamically assigning these fields (i.e. you have an Option to check first). -/// -/// ### Example: -/// ``` -/// use alloy::rpc::types::TransactionRequest; -/// # use contender_core::generator::NamedTxRequestBuilder; -/// -/// let tx_req = TransactionRequest::default(); -/// let named_tx_req = NamedTxRequestBuilder::new(tx_req) -/// .with_name("unique_tx_name") -/// .with_kind("tx_kind") -/// .build(); -/// assert_eq!(named_tx_req.name, Some("unique_tx_name".to_owned())); -/// assert_eq!(named_tx_req.kind, Some("tx_kind".to_owned())); -/// ``` -pub struct NamedTxRequestBuilder { - name: Option, - kind: Option, - tx: TransactionRequest, -} - -impl NamedTxRequestBuilder { - pub fn new(tx: TransactionRequest) -> Self { - Self { - name: None, - kind: None, - tx, - } - } - - pub fn with_name(&mut self, name: &str) -> &mut Self { - self.name = Some(name.to_owned()); - self - } - - pub fn with_kind(&mut self, kind: &str) -> &mut Self { - self.kind = Some(kind.to_owned()); - self - } - - pub fn build(&self) -> NamedTxRequest { - NamedTxRequest::new( - self.tx.to_owned(), - self.name.to_owned(), - self.kind.to_owned(), - ) - } -} - -impl NamedTxRequest { - pub fn new(tx: TransactionRequest, name: Option, kind: Option) -> Self { - Self { name, kind, tx } - } -} - -impl From for NamedTxRequest { - fn from(tx: TransactionRequest) -> Self { - Self { - name: None, - kind: None, - tx, - } - } -} - pub trait PlanConfig where K: Eq + Hash + Debug + Send + Sync, { + /// Get \[\[env]] variables from the plan configuration. fn get_env(&self) -> Result>; + + /// Get contract-creation steps from the plan configuration. fn get_create_steps(&self) -> Result>; + + /// Get setup transactions from the plan configuration. fn get_setup_steps(&self) -> Result>; - fn get_spam_steps(&self) -> Result>; + + /// Get spam step templates from the plan configuration. + fn get_spam_steps(&self) -> Result>; } #[async_trait] @@ -117,27 +65,26 @@ where fn get_db(&self) -> &D; fn get_fuzz_seeder(&self) -> &impl Seeder; - fn get_fuzz_map( + /// Generates a map of N=`num_values` fuzzed values for each parameter in `fuzz_args`. + fn create_fuzz_map( &self, num_values: usize, fuzz_args: &[FuzzParam], ) -> HashMap> { - let mut fuzz_map = HashMap::>::new(); let seed = self.get_fuzz_seeder(); - for fuzz in fuzz_args { + HashMap::>::from_iter(fuzz_args.iter().map(|fuzz| { let values: Vec = seed .seed_values(num_values, fuzz.min, fuzz.max) .map(|v| v.as_u256()) .collect(); - fuzz_map.insert(fuzz.param.to_owned(), values); - } - fuzz_map + (fuzz.param.to_owned(), values) + })) } async fn load_txs CallbackResult>( &self, plan_type: PlanType, - ) -> Result> { + ) -> Result> { let conf = self.get_plan_conf(); let env = conf.get_env().unwrap_or_default(); let db = self.get_db(); @@ -148,7 +95,8 @@ where placeholder_map.insert(key.to_owned(), value.to_owned()); } - let mut txs = vec![]; + let mut txs: Vec = vec![]; + match plan_type { PlanType::Create(on_create_step) => { let create_steps = conf.get_create_steps()?; @@ -169,7 +117,7 @@ where ContenderError::with_err(e, "join error; callback crashed") })?; } - txs.push(tx); + txs.push(tx.into()); } } PlanType::Setup(on_setup_step) => { @@ -191,7 +139,7 @@ where ContenderError::with_err(e, "join error; callback crashed") })?; } - txs.push(tx); + txs.push(tx.into()); } } PlanType::Spam(num_txs, on_spam_setup) => { @@ -199,82 +147,124 @@ where let num_steps = spam_steps.len(); // round num_txs up to the nearest multiple of num_steps to prevent missed steps let num_txs = num_txs + (num_txs % num_steps); + let mut placeholder_map = HashMap::::new(); + let mut canonical_fuzz_map = HashMap::>::new(); for step in spam_steps.iter() { - // find templates from fn call - templater.find_fncall_placeholders(step, db, &mut placeholder_map)?; - let fn_args = step.args.to_owned().unwrap_or_default(); + // finds fuzzed values for a function call definition and populates `canonical_fuzz_map` with fuzzy values. + let mut find_fuzz = |req: &FunctionCallDefinition| { + let fuzz_args = req.fuzz.to_owned().unwrap_or(vec![]); + let fuzz_map = self.create_fuzz_map(num_txs, &fuzz_args); // this may create more values than needed, but it's fine + canonical_fuzz_map.extend(fuzz_map); + }; - // parse fn signature, used to check for fuzzed args later (to make sure they're named) - let func = alloy::json_abi::Function::parse(&step.signature).map_err(|e| { - ContenderError::with_err(e, "failed to parse function name") - })?; - - // pre-generate fuzzy values for each fuzzed parameter - let fuzz_args = step.fuzz.to_owned().unwrap_or(vec![]); - let fuzz_map = self.get_fuzz_map(num_txs / num_steps, &fuzz_args); - - // generate spam txs; split total amount by number of spam steps - for i in 0..(num_txs / num_steps) { - // check args for fuzz params first - let mut args = Vec::new(); - for j in 0..fn_args.len() { - let maybe_fuzz = || { - let input_def = func.inputs[j].to_string(); - // there's probably a better way to do this, but I haven't found it - let arg_namedefs = - input_def.split_ascii_whitespace().collect::>(); - if arg_namedefs.len() < 2 { - // can't fuzz unnamed params - return None; - } - let arg_name = arg_namedefs[1]; - if fuzz_map.contains_key(arg_name) { - return Some( - fuzz_map.get(arg_name).expect("this should never happen") - [i] - .to_string(), - ); - } - None - }; + // finds placeholders in a function call definition and populates `placeholder_map` and `canonical_fuzz_map` with injectable values. + let mut lookup_tx_placeholders = |tx: &FunctionCallDefinition| { + let res = templater.find_fncall_placeholders(tx, db, &mut placeholder_map); + if let Err(e) = res { + eprintln!("error finding placeholders: {}", e); + return; + } + find_fuzz(tx); + }; - // !!! args with template values will be overwritten by the fuzzer if it's enabled for this arg - let val = maybe_fuzz().unwrap_or(fn_args[j].to_owned()); - args.push(val); + // populate maps for each step + match step { + SpamRequest::Tx(tx) => { + lookup_tx_placeholders(tx); + } + SpamRequest::Bundle(req) => { + for tx in req.txs.iter() { + lookup_tx_placeholders(tx); + } } - let mut step = step.to_owned(); - step.args = Some(args); + }; + } - let tx = NamedTxRequest::new( - templater.template_function_call(&step, &placeholder_map)?, - None, - step.kind, - ); + for i in 0..(num_txs / num_steps) { + for step in spam_steps.iter() { + // converts a FunctionCallDefinition to a NamedTxRequest (filling in fuzzable args), + // returns a callback handle and the processed tx request + let process_tx = |req| { + let args = get_fuzzed_args(req, &canonical_fuzz_map, i); + let mut req = req.to_owned(); + req.args = Some(args); + let tx = NamedTxRequest::new( + templater.template_function_call(&req, &placeholder_map)?, + None, + req.kind.to_owned(), + ); + return Ok((on_spam_setup(tx.to_owned())?, tx)); + }; - let handle = on_spam_setup(tx.to_owned())?; - if let Some(handle) = handle { - handle - .await - .map_err(|e| ContenderError::with_err(e, "error from callback"))?; + match step { + SpamRequest::Tx(req) => { + let (handle, tx) = process_tx(req)?; + if let Some(handle) = handle { + handle.await.map_err(|e| { + ContenderError::with_err(e, "error from callback") + })?; + } + txs.push(tx.into()); + } + SpamRequest::Bundle(req) => { + let mut bundle_txs = vec![]; + for tx in req.txs.iter() { + let (handle, txr) = process_tx(tx)?; + if let Some(handle) = handle { + handle.await.map_err(|e| { + ContenderError::with_err(e, "error from callback") + })?; + } + bundle_txs.push(txr); + } + txs.push(bundle_txs.into()); + } } - txs.push(tx); } } - - // interleave spam txs to evenly distribute various calls - // this may create contention if different senders are specified for each call - let chunksize = txs.len() / num_steps; - let mut new_templates = vec![]; - for i in 0..txs.len() { - let chunk_idx = chunksize * (i % num_steps); - let idx = (i / num_steps) + chunk_idx; - new_templates.push(txs[idx].to_owned()); - } - return Ok(new_templates); } } Ok(txs) } } + +/// For the given function call definition, return the fuzzy arguments for the given fuzz index. +fn get_fuzzed_args( + tx: &FunctionCallDefinition, + fuzz_map: &HashMap>, + fuzz_idx: usize, +) -> Vec { + // let mut args = Vec::new(); + let func = + alloy::json_abi::Function::parse(&tx.signature).expect("failed to parse function name"); + let tx_args = tx.args.as_deref().unwrap_or_default(); + tx_args + .iter() + .enumerate() + .map(|(idx, arg)| { + let maybe_fuzz = || { + let input_def = func.inputs[idx].to_string(); + // there's probably a better way to do this, but I haven't found it + // we're looking for something like "uint256 arg_name" in input_def + let arg_namedefs = input_def.split_ascii_whitespace().collect::>(); + if arg_namedefs.len() < 2 { + // can't fuzz unnamed params + return None; + } + let arg_name = arg_namedefs[1]; + if fuzz_map.contains_key(arg_name) { + return Some( + fuzz_map.get(arg_name).expect("this should never happen")[fuzz_idx] + .to_string(), + ); + } + None + }; + + // !!! args with template values will be overwritten by the fuzzer if it's enabled for this arg + maybe_fuzz().unwrap_or(arg.to_owned()) + }) + .collect() +} diff --git a/src/generator/named_txs.rs b/src/generator/named_txs.rs new file mode 100644 index 0000000..72ad365 --- /dev/null +++ b/src/generator/named_txs.rs @@ -0,0 +1,95 @@ +use alloy::rpc::types::TransactionRequest; + +/// Wrapper for [`TransactionRequest`](alloy::rpc::types::TransactionRequest) that includes optional name and kind fields. +#[derive(Clone, Debug)] +pub struct NamedTxRequest { + pub name: Option, + pub kind: Option, + pub tx: TransactionRequest, +} + +/// Syntactical sugar for creating a [`NamedTxRequest`]. +/// +/// This is useful for imperatively assigning optional fields to a tx. +/// It is _not_ useful when you're dynamically assigning these fields (i.e. you have an Option to check first). +/// +/// ### Example: +/// ``` +/// use alloy::rpc::types::TransactionRequest; +/// # use contender_core::generator::NamedTxRequestBuilder; +/// +/// let tx_req = TransactionRequest::default(); +/// let named_tx_req = NamedTxRequestBuilder::new(tx_req) +/// .with_name("unique_tx_name") +/// .with_kind("tx_kind") +/// .build(); +/// assert_eq!(named_tx_req.name, Some("unique_tx_name".to_owned())); +/// assert_eq!(named_tx_req.kind, Some("tx_kind".to_owned())); +/// ``` +pub struct NamedTxRequestBuilder { + name: Option, + kind: Option, + tx: TransactionRequest, +} + +#[derive(Clone, Debug)] +pub enum ExecutionRequest { + Tx(NamedTxRequest), + Bundle(Vec), +} + +impl From for ExecutionRequest { + fn from(tx: NamedTxRequest) -> Self { + Self::Tx(tx) + } +} + +impl From> for ExecutionRequest { + fn from(txs: Vec) -> Self { + Self::Bundle(txs) + } +} + +impl NamedTxRequestBuilder { + pub fn new(tx: TransactionRequest) -> Self { + Self { + name: None, + kind: None, + tx, + } + } + + pub fn with_name(&mut self, name: &str) -> &mut Self { + self.name = Some(name.to_owned()); + self + } + + pub fn with_kind(&mut self, kind: &str) -> &mut Self { + self.kind = Some(kind.to_owned()); + self + } + + pub fn build(&self) -> NamedTxRequest { + NamedTxRequest::new( + self.tx.to_owned(), + self.name.to_owned(), + self.kind.to_owned(), + ) + } +} + +impl NamedTxRequest { + pub fn new(tx: TransactionRequest, name: Option, kind: Option) -> Self { + Self { name, kind, tx } + } +} + +impl From for NamedTxRequest { + fn from(tx: TransactionRequest) -> Self { + Self { + name: None, + kind: None, + tx, + } + } +} diff --git a/src/generator/types.rs b/src/generator/types.rs index 23bc81e..a2eb1be 100644 --- a/src/generator/types.rs +++ b/src/generator/types.rs @@ -1,24 +1,24 @@ +use super::named_txs::ExecutionRequest; use alloy::{ network::AnyNetwork, primitives::U256, providers::RootProvider, - rpc::types::TransactionRequest, transports::http::{Client, Http}, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tokio::task::JoinHandle; +// -- re-exports +pub use crate::generator::named_txs::NamedTxRequest; + +// -- convenience pub type EthProvider = RootProvider>; pub type AnyProvider = RootProvider, AnyNetwork>; -#[derive(Clone, Debug)] -pub struct NamedTxRequest { - pub name: Option, - pub kind: Option, - pub tx: TransactionRequest, -} +// -- core types for test scenarios +/// User-facing definition of a function call to be executed. #[derive(Clone, Deserialize, Debug, Serialize)] pub struct FunctionCallDefinition { /// Address of the contract to call. @@ -34,7 +34,24 @@ pub struct FunctionCallDefinition { /// Parameters to fuzz during the test. pub fuzz: Option>, /// Optional type of the spam transaction for categorization. - pub kind: Option + pub kind: Option, +} + +/// User-facing definition of a function call to be executed. +#[derive(Clone, Deserialize, Debug, Serialize)] +pub struct BundleCallDefinition { + #[serde(rename = "tx")] + pub txs: Vec, +} + +/// Definition of a spam request template. +/// TestConfig uses this for TOML parsing. +#[derive(Clone, Deserialize, Debug, Serialize)] +pub enum SpamRequest { + #[serde(rename = "tx")] + Tx(FunctionCallDefinition), + #[serde(rename = "bundle")] + Bundle(BundleCallDefinition), } #[derive(Clone, Deserialize, Debug, Serialize)] @@ -62,7 +79,7 @@ pub struct Plan { pub env: HashMap, pub create_steps: Vec, pub setup_steps: Vec, - pub spam_steps: Vec, + pub spam_steps: Vec, } pub type CallbackResult = crate::Result>>; diff --git a/src/lib.rs b/src/lib.rs index c94a68c..d84c061 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod bundle_provider; pub mod db; pub mod error; pub mod generator; diff --git a/src/spammer/blockwise.rs b/src/spammer/blockwise.rs index c4ce362..abd0381 100644 --- a/src/spammer/blockwise.rs +++ b/src/spammer/blockwise.rs @@ -1,15 +1,20 @@ +use crate::bundle_provider::{BundleClient, EthSendBundle}; use crate::db::DbOps; use crate::error::ContenderError; +use crate::generator::named_txs::ExecutionRequest; use crate::generator::seeder::Seeder; use crate::generator::templater::Templater; use crate::generator::types::{AnyProvider, EthProvider, PlanType}; use crate::generator::{Generator, PlanConfig}; +use crate::spammer::ExecutionPayload; use crate::test_scenario::TestScenario; use crate::Result; +use alloy::eips::eip2718::Encodable2718; use alloy::hex::ToHexExt; -use alloy::network::{AnyNetwork, TransactionBuilder}; -use alloy::primitives::FixedBytes; -use alloy::providers::{Provider, ProviderBuilder}; +use alloy::network::{AnyNetwork, EthereumWallet, TransactionBuilder}; +use alloy::primitives::{Address, FixedBytes}; +use alloy::providers::{PendingTransactionConfig, Provider, ProviderBuilder}; +use alloy::rpc::types::TransactionRequest; use futures::StreamExt; use std::collections::HashMap; use std::ops::Deref; @@ -19,6 +24,9 @@ use tokio::task; use super::tx_actor::TxActorHandle; use super::OnTxSent; +/// Defines the number of blocks to target with a single bundle. +const BUNDLE_BLOCK_TOLERANCE: usize = 5; + pub struct BlockwiseSpammer where D: DbOps + Send + Sync + 'static, @@ -30,7 +38,10 @@ where msg_handler: Arc, rpc_client: AnyProvider, eth_client: Arc, + bundle_client: Option>, callback_handler: Arc, + nonces: HashMap, + gas_limits: HashMap, u128>, } impl BlockwiseSpammer @@ -40,11 +51,15 @@ where S: Seeder + Send + Sync, P: PlanConfig + Templater + Send + Sync, { - pub fn new(scenario: TestScenario, callback_handler: F) -> Self { + pub async fn new(scenario: TestScenario, callback_handler: F) -> Self { let rpc_client = ProviderBuilder::new() .network::() .on_http(scenario.rpc_url.to_owned()); let eth_client = Arc::new(ProviderBuilder::new().on_http(scenario.rpc_url.to_owned())); + let bundle_client = scenario + .builder_rpc_url + .to_owned() + .map(|url| Arc::new(BundleClient::new(url.into()))); let msg_handler = Arc::new(TxActorHandle::new( 12, scenario.db.clone(), @@ -52,17 +67,92 @@ where )); let callback_handler = Arc::new(callback_handler); + // get nonce for each signer and put it into a hashmap + let mut nonces = HashMap::new(); + for (addr, _) in scenario.wallet_map.iter() { + let nonce = eth_client + .get_transaction_count(*addr) + .await + .expect("failed to retrieve nonce"); + nonces.insert(*addr, nonce); + } + + // track gas limits for each function signature + let gas_limits = HashMap::, u128>::new(); + Self { scenario, rpc_client, eth_client, + bundle_client, callback_handler, msg_handler, + nonces, + gas_limits, + } + } + + async fn prepare_tx_req( + &mut self, + tx_req: &TransactionRequest, + gas_price: u128, + chain_id: u64, + ) -> Result<(TransactionRequest, EthereumWallet)> { + let from = tx_req.from.expect("missing from address"); + let nonce = self + .nonces + .get(&from) + .expect("failed to get nonce") + .to_owned(); + /* + Increment nonce assuming the tx will succeed. + Note: if any tx fails, txs with higher nonces will also fail. + However, we'll get a fresh nonce next block. + */ + self.nonces.insert(from.to_owned(), nonce + 1); + let fn_sig = FixedBytes::<4>::from_slice( + tx_req + .input + .input + .to_owned() + .map(|b| b.split_at(4).0.to_owned()) + .expect("invalid function call") + .as_slice(), + ); + if !self.gas_limits.contains_key(fn_sig.as_slice()) { + let gas_limit = self + .eth_client + .estimate_gas(&tx_req.to_owned()) + .await + .map_err(|e| ContenderError::with_err(e, "failed to estimate gas"))?; + self.gas_limits.insert(fn_sig, gas_limit); } + // query hashmaps for gaslimit & signer of this tx + let gas_limit = self + .gas_limits + .get(&fn_sig) + .expect("failed to get gas limit") + .to_owned(); + let signer = self + .scenario + .wallet_map + .get(&from) + .expect("failed to create signer") + .to_owned(); + + let full_tx = tx_req + .clone() + .with_nonce(nonce) + .with_max_fee_per_gas(gas_price + (gas_price / 5)) + .with_max_priority_fee_per_gas(gas_price) + .with_chain_id(chain_id) + .with_gas_limit(gas_limit + (gas_limit / 4)); + + Ok((full_tx, signer)) } pub async fn spam_rpc( - &self, + &mut self, txs_per_block: usize, num_blocks: usize, run_id: Option, @@ -98,18 +188,6 @@ where .take(num_blocks); let mut tasks = vec![]; - let mut gas_limits = HashMap::, u128>::new(); - - // get nonce for each signer and put it into a hashmap - let mut nonces = HashMap::new(); - for (addr, _) in self.scenario.wallet_map.iter() { - let nonce = self - .rpc_client - .get_transaction_count(*addr) - .await - .map_err(|_| ContenderError::SpamError("failed to get nonce", None))?; - nonces.insert(*addr, nonce); - } while let Some(block_hash) = stream.next().await { let block_txs = tx_req_chunks[block_offset].clone(); @@ -132,108 +210,152 @@ where for (idx, tx) in block_txs.into_iter().enumerate() { let gas_price = gas_price + (idx as u128 * 1e9 as u128); - let tx_req = tx.tx.to_owned(); - println!( - "sending tx. from={} to={} input={}", - tx_req.from.map(|s| s.encode_hex()).unwrap_or_default(), - tx_req - .to - .map(|s| s.to().map(|s| *s)) - .flatten() - .map(|s| s.encode_hex()) - .unwrap_or_default(), - tx_req - .input - .input - .as_ref() - .map(|s| s.encode_hex()) - .unwrap_or_default(), - ); - - let from = &tx_req.from.expect("missing from address"); - let nonce = nonces.get(from).expect("failed to get nonce").to_owned(); - /* - Increment nonce assuming the tx will succeed. - Note: if any tx fails, txs with higher nonces will also fail. - However, we'll get a fresh nonce next block. - */ - nonces.insert(from.to_owned(), nonce + 1); - - let fn_sig = FixedBytes::<4>::from_slice( - tx_req - .input - .input - .to_owned() - .map(|b| b.split_at(4).0.to_owned()) - .expect("invalid function call") - .as_slice(), - ); - - if !gas_limits.contains_key(fn_sig.as_slice()) { - let gas_limit = self - .eth_client - .estimate_gas(&tx.tx.to_owned()) - .await - .map_err(|e| ContenderError::with_err(e, "failed to estimate gas"))?; - gas_limits.insert(fn_sig, gas_limit); - } - // clone muh Arcs let eth_client = self.eth_client.clone(); let callback_handler = self.callback_handler.clone(); - - // query hashmaps for gaslimit & signer of this tx - let gas_limit = gas_limits - .get(&fn_sig) - .expect("failed to get gas limit") - .to_owned(); - let signer = self - .scenario - .wallet_map - .get(from) - .expect("failed to create signer") - .to_owned(); - - // build, sign, and send tx in a new task (green thread) let tx_handler = self.msg_handler.clone(); - let tx_kind = tx.kind.to_owned(); - tasks.push(task::spawn(async move { - let provider = ProviderBuilder::new() - .wallet(signer) - .on_provider(eth_client); - let full_tx = tx_req - .clone() - .with_nonce(nonce) - .with_gas_price(gas_price) - .with_chain_id(chain_id) - .with_gas_limit(gas_limit); + // prepare tx/bundle with nonce, gas price, signatures, etc + let payload = match tx { + ExecutionRequest::Bundle(reqs) => { + if self.bundle_client.is_none() { + return Err(ContenderError::SpamError( + "Bundle client not found. Add the `--builder-url` flag to send bundles.", + None, + )); + } + + // prepare each tx in the bundle (increment nonce, set gas price, etc) + let mut bundle_txs = vec![]; + for req in reqs.iter() { + let tx_req = req.tx.to_owned(); + let (tx_req, signer) = self + .prepare_tx_req(&tx_req, gas_price, chain_id) + .await + .map_err(|e| ContenderError::with_err(e, "failed to prepare tx"))?; + + // sign tx + let tx_envelope = tx_req.build(&signer).await.map_err(|e| { + ContenderError::with_err(e, "bad request: failed to build tx") + })?; + + bundle_txs.push(tx_envelope); + } + ExecutionPayload::SignedTxBundle(bundle_txs, reqs) + } + ExecutionRequest::Tx(req) => { + let tx_req = req.tx.to_owned(); + println!( + "sending tx. from={} to={} input={}", + tx_req.from.map(|s| s.encode_hex()).unwrap_or_default(), + tx_req + .to + .map(|s| s.to().map(|s| *s)) + .flatten() + .map(|s| s.encode_hex()) + .unwrap_or_default(), + tx_req + .input + .input + .as_ref() + .map(|s| s.encode_hex()) + .unwrap_or_default(), + ); + + let (tx_req, signer) = self + .prepare_tx_req(&tx_req, gas_price, chain_id) + .await + .map_err(|e| ContenderError::with_err(e, "failed to prepare tx"))?; + + // sign tx + let tx_envelope = tx_req.build(&signer).await.map_err(|e| { + ContenderError::with_err(e, "bad request: failed to build tx") + })?; + + ExecutionPayload::SignedTx(tx_envelope, req) + } + }; + + let bundle_client = self.bundle_client.clone(); + // build, sign, and send tx/bundle in a new task (green thread) + tasks.push(task::spawn(async move { + let provider = ProviderBuilder::new().on_provider(eth_client); let start_timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("failed to get timestamp") .as_millis() as usize; - let res = provider - .send_transaction(full_tx) - .await - .expect("failed to send tx"); + let mut extra = HashMap::new(); extra.insert("start_timestamp".to_owned(), start_timestamp.to_string()); - if let Some(kind) = tx_kind { - extra.insert("kind".to_owned(), kind); - } - let maybe_handle = callback_handler.on_tx_sent( - res.into_inner(), - tx, - extra.into(), - Some(tx_handler), - ); - if let Some(handle) = maybe_handle { - handle.await.expect("callback task failed"); + + // triggers & awaits callback for every individual tx (including txs in a bundle) + let handles = match payload { + ExecutionPayload::SignedTx(signed_tx, req) => { + let res = provider + .send_tx_envelope(signed_tx) + .await + .expect("RPC error: failed to send tx"); + let maybe_handle = callback_handler.on_tx_sent( + res.into_inner(), + &req.to_owned(), + extra.clone().into(), + Some(tx_handler), + ); + vec![maybe_handle] + } + ExecutionPayload::SignedTxBundle(signed_txs, reqs) => { + let mut bundle_txs = vec![]; + for tx in &signed_txs { + println!("sending tx: {:?}", tx); + let mut raw_tx = vec![]; + tx.encode_2718(&mut raw_tx); + bundle_txs.push(raw_tx); + } + let rpc_bundle = EthSendBundle { + txs: bundle_txs.into_iter().map(|tx| tx.into()).collect(), + block_number: last_block_number, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: vec![], + replacement_uuid: None, + }; + if let Some(bundle_client) = bundle_client { + println!("spamming bundle: {:?}", rpc_bundle); + // send `num_blocks` bundles at a time, targeting each successive block + for i in 1..(num_blocks + BUNDLE_BLOCK_TOLERANCE) { + let mut rpc_bundle = rpc_bundle.clone(); + rpc_bundle.block_number = last_block_number + i as u64; + let res = rpc_bundle.send_to_builder(&bundle_client).await; + if let Err(e) = res { + eprintln!("failed to send bundle: {:?}", e); + } + } + } else { + panic!("no bundle client provided. Please add the `--builder-url` flag"); + } + + let mut tx_handles = vec![]; + for (tx, req) in signed_txs.into_iter().zip(&reqs) { + let maybe_handle = callback_handler.on_tx_sent( + PendingTransactionConfig::new(tx.tx_hash().to_owned()), + &req, + extra.clone().into(), + Some(tx_handler.clone()), + ); + tx_handles.push(maybe_handle); + } + tx_handles + } + }; + for handle in handles { + if let Some(handle) = handle { + handle.await.expect("callback task failed"); + } } - // ignore None values so we don't attempt to await them })); } + println!("new block: {block_hash}"); if let Some(run_id) = run_id { // write this block's txs to DB @@ -294,11 +416,12 @@ mod tests { MockConfig, MockDb.into(), anvil.endpoint_url(), + None, seed, - &get_test_signers(), + get_test_signers().as_slice(), ); let callback_handler = MockCallback; - let spammer = BlockwiseSpammer::new(scenario, callback_handler); + let mut spammer = BlockwiseSpammer::new(scenario, callback_handler).await; let result = spammer.spam_rpc(10, 3, None).await; println!("{:?}", result); diff --git a/src/spammer/mod.rs b/src/spammer/mod.rs index 3a8b29a..ae2fd01 100644 --- a/src/spammer/mod.rs +++ b/src/spammer/mod.rs @@ -4,7 +4,7 @@ pub mod tx_actor; pub mod util; use crate::generator::{types::AnyProvider, NamedTxRequest}; -use alloy::providers::PendingTransactionConfig; +use alloy::{consensus::TxEnvelope, providers::PendingTransactionConfig}; use std::{collections::HashMap, sync::Arc}; use tokio::task::JoinHandle; use tx_actor::TxActorHandle; @@ -20,7 +20,7 @@ where fn on_tx_sent( &self, tx_response: PendingTransactionConfig, - req: NamedTxRequest, + req: &NamedTxRequest, extra: Option>, tx_handler: Option>, ) -> Option>; @@ -48,7 +48,7 @@ impl OnTxSent for NilCallback { fn on_tx_sent( &self, _tx_res: PendingTransactionConfig, - _req: NamedTxRequest, + _req: &NamedTxRequest, _extra: Option>, _tx_handler: Option>, ) -> Option> { @@ -61,7 +61,7 @@ impl OnTxSent for LogCallback { fn on_tx_sent( &self, tx_response: PendingTransactionConfig, - _req: NamedTxRequest, + _req: &NamedTxRequest, extra: Option>, tx_actor: Option>, ) -> Option> { @@ -85,3 +85,9 @@ impl OnTxSent for LogCallback { Some(handle) } } + +#[derive(Debug)] +pub enum ExecutionPayload { + SignedTx(TxEnvelope, NamedTxRequest), + SignedTxBundle(Vec, Vec), +} diff --git a/src/spammer/timed.rs b/src/spammer/timed.rs index 7e4e135..f8af06e 100644 --- a/src/spammer/timed.rs +++ b/src/spammer/timed.rs @@ -1,6 +1,7 @@ use crate::{ db::DbOps, generator::{ + named_txs::ExecutionRequest, seeder::Seeder, templater::Templater, types::{EthProvider, PlanType}, @@ -60,31 +61,44 @@ where // send tx to the RPC asynchrononsly tasks.push(task::spawn(async move { - let tx_req = &tx.tx; - println!( - "sending tx. from={} to={} input={}", - tx_req.from.map(|s| s.encode_hex()).unwrap_or_default(), - tx_req - .to - .map(|s| s.to().map(|s| *s)) - .flatten() - .map(|s| s.encode_hex()) - .unwrap_or_default(), - tx_req - .input - .input - .as_ref() - .map(|s| s.encode_hex()) - .unwrap_or_default(), - ); - let res = rpc_client - .send_transaction(tx_req.to_owned()) - .await - .expect("failed to send tx"); - let maybe_handle = callback_handler.on_tx_sent(res.into_inner(), tx, None, None); - if let Some(handle) = maybe_handle { - handle.await.expect("failed to join task handle"); - } // ignore None values so we don't attempt to await them + let handles = match tx { + ExecutionRequest::Tx(req) => { + let tx_req = &req.tx; + println!( + "sending tx. from={} to={} input={}", + tx_req.from.map(|s| s.encode_hex()).unwrap_or_default(), + tx_req + .to + .map(|s| s.to().map(|s| *s)) + .flatten() + .map(|s| s.encode_hex()) + .unwrap_or_default(), + tx_req + .input + .input + .as_ref() + .map(|s| s.encode_hex()) + .unwrap_or_default(), + ); + let res = rpc_client + .send_transaction(tx_req.to_owned()) + .await + .expect("failed to send tx"); + let maybe_handle = + callback_handler.on_tx_sent(res.into_inner(), &req, None, None); + vec![maybe_handle] + } + ExecutionRequest::Bundle(_) => { + eprintln!("bundles are not supported in timed spammer. Please try the blockwise spammer (--tpb)"); + vec![] + } + }; + + for handle in handles { + if let Some(handle) = handle { + handle.await.expect("failed to join task handle"); + } // ignore None values so we don't attempt to await them + } })); // sleep for interval diff --git a/src/spammer/util.rs b/src/spammer/util.rs index 5d90ee0..2080406 100644 --- a/src/spammer/util.rs +++ b/src/spammer/util.rs @@ -15,7 +15,7 @@ pub mod test { fn on_tx_sent( &self, _tx_res: PendingTransactionConfig, - _req: NamedTxRequest, + _req: &NamedTxRequest, _extra: Option>, _tx_handler: Option>, ) -> Option> { diff --git a/src/test_scenario.rs b/src/test_scenario.rs index 6086203..6e849fb 100644 --- a/src/test_scenario.rs +++ b/src/test_scenario.rs @@ -22,6 +22,7 @@ where pub config: P, pub db: Arc, pub rpc_url: Url, + pub builder_rpc_url: Option, pub rand_seed: S, pub wallet_map: HashMap, } @@ -36,6 +37,7 @@ where config: P, db: Arc, rpc_url: Url, + builder_rpc_url: Option, rand_seed: S, signers: &[PrivateKeySigner], ) -> Self { @@ -52,6 +54,7 @@ where config, db: db.clone(), rpc_url, + builder_rpc_url, rand_seed, wallet_map, } @@ -256,7 +259,9 @@ where pub mod tests { use crate::db::MockDb; use crate::generator::templater::Templater; - use crate::generator::types::{CreateDefinition, FunctionCallDefinition, FuzzParam}; + use crate::generator::types::{ + CreateDefinition, FunctionCallDefinition, FuzzParam, SpamRequest, + }; use crate::generator::{types::PlanType, util::test::spawn_anvil, RandSeed}; use crate::generator::{Generator, PlanConfig}; use crate::spammer::util::test::get_test_signers; @@ -323,26 +328,28 @@ pub mod tests { ]) } - fn get_spam_steps(&self) -> Result> { - let fn_call = |data: &str, from_addr: &str| FunctionCallDefinition { - to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D".to_owned(), - from: from_addr.to_owned(), - value: None, - signature: "swap(uint256 x, uint256 y, address a, bytes b)".to_owned(), - args: vec![ - "1".to_owned(), - "2".to_owned(), - Address::repeat_byte(0x11).encode_hex(), - data.to_owned(), - ] - .into(), - fuzz: vec![FuzzParam { - param: "x".to_string(), - min: None, - max: None, - }] - .into(), - kind: None, + fn get_spam_steps(&self) -> Result> { + let fn_call = |data: &str, from_addr: &str| { + SpamRequest::Tx(FunctionCallDefinition { + to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D".to_owned(), + from: from_addr.to_owned(), + value: None, + signature: "swap(uint256 x, uint256 y, address a, bytes b)".to_owned(), + args: vec![ + "1".to_owned(), + "2".to_owned(), + Address::repeat_byte(0x11).encode_hex(), + data.to_owned(), + ] + .into(), + fuzz: vec![FuzzParam { + param: "x".to_string(), + min: None, + max: None, + }] + .into(), + kind: None, + }) }; Ok(vec![ fn_call("0xbeef", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), @@ -388,6 +395,7 @@ pub mod tests { MockConfig, MockDb.into(), anvil.endpoint_url(), + None, seed, &signers, ) diff --git a/testfile/src/lib.rs b/testfile/src/lib.rs index 496ab27..2ad7684 100644 --- a/testfile/src/lib.rs +++ b/testfile/src/lib.rs @@ -7,7 +7,7 @@ use contender_core::{ error::ContenderError, generator::{ templater::Templater, - types::{CreateDefinition, FunctionCallDefinition}, + types::{CreateDefinition, FunctionCallDefinition, SpamRequest}, PlanConfig, }, }; @@ -35,7 +35,7 @@ impl TestConfig { } impl PlanConfig for TestConfig { - fn get_spam_steps(&self) -> Result, ContenderError> { + fn get_spam_steps(&self) -> Result, ContenderError> { Ok(self.spam.to_owned().unwrap_or_default()) } @@ -111,7 +111,11 @@ pub mod tests { use contender_core::{ db::MockDb, generator::{ - types::{CreateDefinition, FunctionCallDefinition, FuzzParam, PlanType}, + named_txs::ExecutionRequest, + types::{ + BundleCallDefinition, CreateDefinition, FunctionCallDefinition, FuzzParam, + PlanType, SpamRequest, + }, Generator, RandSeed, }, test_scenario::TestScenario, @@ -137,26 +141,27 @@ pub mod tests { } pub fn get_testconfig() -> TestConfig { + let fncall = FunctionCallDefinition { + to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F248DD".to_owned(), + from: "0x7a250d5630B4cF539739dF2C5dAcb4c659F248DD".to_owned(), + signature: "swap(uint256 x, uint256 y, address a, bytes b)".to_owned(), + args: vec![ + "1".to_owned(), + "2".to_owned(), + Address::repeat_byte(0x11).encode_hex(), + "0xdead".to_owned(), + ] + .into(), + fuzz: None, + value: None, + kind: None, + }; + TestConfig { env: None, create: None, setup: None, - spam: vec![FunctionCallDefinition { - to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F248DD".to_owned(), - from: "0x7a250d5630B4cF539739dF2C5dAcb4c659F248DD".to_owned(), - signature: "swap(uint256 x, uint256 y, address a, bytes b)".to_owned(), - args: vec![ - "1".to_owned(), - "2".to_owned(), - Address::repeat_byte(0x11).encode_hex(), - "0xdead".to_owned(), - ] - .into(), - fuzz: None, - value: None, - kind: None, - }] - .into(), + spam: vec![SpamRequest::Tx(fncall)].into(), } } @@ -186,9 +191,25 @@ pub mod tests { create: None, setup: None, spam: vec![ - fn_call("0xbeef", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - fn_call("0xea75", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), - fn_call("0xf00d", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), + SpamRequest::Tx(fn_call( + "0xbeef", + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + )), + SpamRequest::Tx(fn_call( + "0xea75", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + )), + SpamRequest::Tx(fn_call( + "0xf00d", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + )), + SpamRequest::Bundle(BundleCallDefinition { + txs: vec![ + fn_call("0xbeef", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + fn_call("0xea75", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + fn_call("0xf00d", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), + ], + }), ] .into(), } @@ -274,19 +295,26 @@ pub mod tests { let spam = test_file.spam.unwrap(); assert_eq!(env.get("env1").unwrap(), "env1"); - assert_eq!( - spam[0].from, - "0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_owned() - ); - assert_eq!(setup.len(), 1); - assert_eq!(setup[0].value, Some("1234".to_owned())); - assert_eq!(spam[0].fuzz.as_ref().unwrap()[0].param, "amountIn"); - assert_eq!(spam[0].fuzz.as_ref().unwrap()[0].min, Some(U256::from(1))); - assert_eq!( - spam[0].fuzz.as_ref().unwrap()[0].max, - Some(U256::from(100_000_000_000_000_000_u64)) - ); - assert_eq!(spam[0].kind, Some("test".to_owned())); + match spam[0] { + SpamRequest::Tx(ref fncall) => { + assert_eq!( + fncall.from, + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_owned() + ); + assert_eq!(setup.len(), 1); + assert_eq!(setup[0].value, Some("1234".to_owned())); + assert_eq!(fncall.fuzz.as_ref().unwrap()[0].param, "amountIn"); + assert_eq!(fncall.fuzz.as_ref().unwrap()[0].min, Some(U256::from(1))); + assert_eq!( + fncall.fuzz.as_ref().unwrap()[0].max, + Some(U256::from(100_000_000_000_000_000_u64)) + ); + assert_eq!(fncall.kind, Some("test".to_owned())); + } + _ => { + panic!("expected SpamRequest::Single"); + } + } } fn print_testconfig(cfg: &str) { @@ -303,10 +331,26 @@ pub mod tests { cfg.save_toml("cargotest.toml").unwrap(); let test_file2 = TestConfig::from_file("cargotest.toml").unwrap(); let spam = cfg.clone().spam.unwrap(); - let args = spam[0].args.as_ref().unwrap(); - assert_eq!(spam[0].to, test_file2.spam.unwrap()[0].to); - assert_eq!(args[0], "1"); - assert_eq!(args[1], "2"); + match &spam[0] { + SpamRequest::Tx(req) => { + let args = req.args.as_ref().unwrap(); + match &test_file2.spam.unwrap()[0] { + SpamRequest::Tx(req2) => { + let args2 = req2.args.as_ref().unwrap(); + assert_eq!(req.from, req2.from); + assert_eq!(req.to, req2.to); + assert_eq!(args[0], args2[0]); + assert_eq!(args[1], args2[1]); + } + _ => { + panic!("expected SpamRequest::Single"); + } + } + } + _ => { + panic!("expected SpamRequest::Single"); + } + } fs::remove_file("cargotest.toml").unwrap(); } @@ -319,6 +363,7 @@ pub mod tests { test_file, MockDb.into(), anvil.endpoint_url(), + None, seed, &get_test_signers(), ); @@ -334,8 +379,15 @@ pub mod tests { .await .unwrap(); assert_eq!(spam_txs.len(), 10); - let data = spam_txs[0].tx.input.input.to_owned().unwrap().to_string(); - assert_eq!(data, "0x022c0d9f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002dead000000000000000000000000000000000000000000000000000000000000"); + match &spam_txs[0] { + ExecutionRequest::Tx(req) => { + let data = req.tx.input.input.to_owned().unwrap().to_string(); + assert_eq!(data, "0x022c0d9f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002dead000000000000000000000000000000000000000000000000000000000000"); + } + _ => { + panic!("expected ExecutionRequest::Tx"); + } + } } #[tokio::test] @@ -348,6 +400,7 @@ pub mod tests { test_file.clone(), MockDb.into(), anvil.endpoint_url(), + None, seed.to_owned(), &signers, ); @@ -355,6 +408,7 @@ pub mod tests { test_file, MockDb.into(), anvil.endpoint_url(), + None, seed, &signers, ); @@ -370,9 +424,32 @@ pub mod tests { .unwrap(); assert_eq!(spam_txs_1.len(), spam_txs_2.len()); for i in 0..spam_txs_1.len() { - let data1 = spam_txs_1[i].tx.input.input.to_owned().unwrap().to_string(); - let data2 = spam_txs_2[i].tx.input.input.to_owned().unwrap().to_string(); - assert_eq!(data1, data2); + match &spam_txs_1[i] { + ExecutionRequest::Tx(req) => { + let data1 = req.tx.input.input.to_owned().unwrap().to_string(); + match &spam_txs_2[i] { + ExecutionRequest::Tx(req) => { + let data2 = req.tx.input.input.to_owned().unwrap().to_string(); + assert_eq!(data1, data2); + } + _ => { + panic!("expected ExecutionRequest::Tx"); + } + } + } + ExecutionRequest::Bundle(reqs) => { + let data1 = reqs[0].tx.input.input.to_owned().unwrap().to_string(); + match &spam_txs_2[i] { + ExecutionRequest::Bundle(reqs) => { + let data2 = reqs[0].tx.input.input.to_owned().unwrap().to_string(); + assert_eq!(data1, data2); + } + _ => { + panic!("expected ExecutionRequest::Bundle"); + } + } + } + } } } } diff --git a/testfile/src/types.rs b/testfile/src/types.rs index 3a1309d..becd3de 100644 --- a/testfile/src/types.rs +++ b/testfile/src/types.rs @@ -1,4 +1,4 @@ -use contender_core::generator::types::{CreateDefinition, FunctionCallDefinition}; +use contender_core::generator::types::{CreateDefinition, FunctionCallDefinition, SpamRequest}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -15,5 +15,5 @@ pub struct TestConfig { pub setup: Option>, /// Function to call in spam txs. - pub spam: Option>, + pub spam: Option>, // TODO: figure out how to implement BundleCallDefinition alongside FunctionCallDefinition } diff --git a/testfile/testConfig.toml b/testfile/testConfig.toml index 5371354..4046fe1 100644 --- a/testfile/testConfig.toml +++ b/testfile/testConfig.toml @@ -8,9 +8,11 @@ from = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" signature = "function deposit() public payable" value = "1234" - -### the spam step will be repeated +# the spam step will be repeated [[spam]] + +# specify a single tx to spam +[spam.tx] kind = "test" to = "0xE46CcF40134e7ad524529B25Ce04e39BC2B51cDc" from = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" @@ -20,7 +22,8 @@ args = [ "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", ] -[[spam.fuzz]] +# each tx can have multiple fuzzed params +[[spam.tx.fuzz]] param = "amountIn" min = "1" max = "100000000000000000"