diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0bd83c3..96180292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,3 +159,18 @@ jobs: with: version: stable - run: just check-udeps + + crate-deps: + name: Crate Dependencies + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: dtolnay/rust-toolchain@4305c38b25d97ef35a8ad1f985ccf2d2242004f2 # stable + - name: Install just + uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + - run: just check-crate-deps diff --git a/.gitignore b/.gitignore index 959253b2..036d404a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ target/ .idea/ -integration_logs/ .DS_Store diff --git a/.gitmodules b/.gitmodules index f9644fe7..4508bf7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ -[submodule "crates/client/test-utils/contracts/lib/forge-std"] - path = crates/client/test-utils/contracts/lib/forge-std +[submodule "crates/shared/primitives/contracts/lib/forge-std"] + path = crates/shared/primitives/contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "crates/client/test-utils/contracts/lib/solmate"] - path = crates/client/test-utils/contracts/lib/solmate +[submodule "crates/shared/primitives/contracts/lib/solmate"] + path = crates/shared/primitives/contracts/lib/solmate url = https://github.com/transmissions11/solmate -[submodule "crates/client/test-utils/contracts/lib/openzeppelin-contracts"] - path = crates/client/test-utils/contracts/lib/openzeppelin-contracts +[submodule "crates/shared/primitives/contracts/lib/openzeppelin-contracts"] + path = crates/shared/primitives/contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/Cargo.lock b/Cargo.lock index 31d5857b..1b53991d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e46a465e50a339a817070ec23f06eb3fc9fbb8af71612868367b875a9d49e3" +checksum = "8e30ab0d3e3c32976f67fc1a96179989e45a69594af42003a6663332f9b0bb9d" dependencies = [ "alloy-eips", "alloy-primitives", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07001b1693af794c7526aab400b42e38075f986ef8fef78841e5ebc745473e56" +checksum = "c20736b1f9d927d875d8777ef0c2250d4c57ea828529a9dbfa2c628db57b911e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef1b07c3ff5bf4fab5b8e6c46190cd40b2f2fd2cd72b5b02527a38125d0bff4" +checksum = "008aba161fce2a0d94956ae09d7d7a09f8fbdf18acbef921809ef126d6cdaf97" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707337efeb051ddbaece17a73eaec5150945a5a5541112f4146508248edc2e40" +checksum = "15b85157b7be31fc4adf6acfefcb0d4308cba5dbd7a8d8e62bcc02ff37d6131a" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -299,9 +299,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ba7afffa225272cf50c62ff04ac574adc7bfa73af2370db556340f26fcff5c" +checksum = "a838301c4e2546c96db1848f18ffe9f722f2fccd9715b83d4bf269a2cf00b5a1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48562f9b4c4e1514cab54af16feaffc18194a38216bbd0c23004ec4667ad696b" +checksum = "60f045b69b5e80b8944b25afe74ae6b974f3044d84b4a7a113da04745b2524cc" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364a5eaa598437d7a57bcbcb4b7fcb0518e192cf809a19b09b2b5cf73b9ba1cd" +checksum = "2b314ed5bdc7f449c53853125af2db5ac4d3954a9f4b205e7d694f02fc1932d1" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21af5255bd276e528ee625d97033884916e879a1c6edcd5b70a043bd440c0710" +checksum = "5e9762ac5cca67b0f6ab614f7f8314942eead1c8eeef61511ea43a6ff048dbe0" dependencies = [ "alloy-consensus", "alloy-eips", @@ -455,9 +455,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc919fe241f9dd28c4c7f7dcff9e66e550c280bafe3545e1019622e1239db38" +checksum = "ea8f7ca47514e7f552aa9f3f141ab17351332c6637e3bf00462d8e7c5f10f51f" dependencies = [ "alloy-chains", "alloy-consensus", @@ -482,7 +482,7 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru 0.13.0", + "lru 0.16.3", "parking_lot", "pin-project", "reqwest", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a0778833917a71a9e0065e0409bfc00cddef55ca962b3453472be38ebe7035" +checksum = "4082778c908aa801a1f9fdc85d758812842ab4b2aaba58e9dbe7626d708ab7e1" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -541,9 +541,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b587e63d8c4af437b0a7830dc12d24cb495e956cc8ecbf93e96d62c9cb55b13" +checksum = "26dd083153d2cb73cce1516f5a3f9c3af74764a2761d901581a355777468bd8f" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -567,9 +567,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3000edc72a300048cf461df94bfa29fc5d7760ddd88ca7d56ea6fc8b28729" +checksum = "8c998214325cfee1fbe61e5abaed3a435f4ca746ac7399b46feb57c364552452" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", @@ -580,9 +580,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-admin" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb98103316e6f4a1ebc6e71328c2d18426cdd79fc999c44afd9f0f4e9f5edd6" +checksum = "730a38742dc0753f25b8ce7330c2fa88d79f165c5fc2f19f3d35291739c42e83" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -592,9 +592,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1207e852f30297d6918f91df3e76f758fa7b519ea1e49fbd7d961ce796663f9" +checksum = "a2b03d65fcf579fbf17d3aac32271f99e2b562be04097436cd6e766b3e06613b" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -604,9 +604,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebc96cf29095c10a183fb7106a097fe12ca8dd46733895582da255407f54b29" +checksum = "4b4a6f49d161ef83354d5ba3c8bc83c8ee464cb90182b215551d5c4b846579be" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -615,9 +615,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cea7c1c22628b13b25d31fd63fa5dfa7fac0b0b78f1c89a5068102b653ff65c" +checksum = "3b6654644613f33fd2e6f333f4ce8ad0a26f036c0513699d7bc168bba18d412d" dependencies = [ "alloy-eips", "alloy-primitives", @@ -635,9 +635,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1a6b13b6f95b80d3ff770998f81e61811264eb1d18b88dfa11c80180acdc1b" +checksum = "467025b916f32645f322a085d0017f2996d0200ac89dd82a4fc2bf0f17b9afa3" dependencies = [ "alloy-primitives", "derive_more", @@ -647,9 +647,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f35af673cc14e89813ab33671d79b6e73fe38788c5f3a8ec3a75476b58225f53" +checksum = "933aaaace9faa6d7efda89472add89a8bfd15270318c47a2be8bb76192c951e2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -667,9 +667,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc3f354a5079480acca0a6533d1d3838177a03ea494ef0ae8d1679efea88274" +checksum = "11920b16ab7c86052f990dcb4d25312fb2889faf506c4ee13dc946b450536989" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -689,9 +689,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-mev" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fbd905c35f780926ff0c4c2a74d3ce7d50576cb0e9997dc783ac99c6fd7afb" +checksum = "1826454c2890af6d642bf052909e0162ad7f261d172e56ef2e936d479960699c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -704,9 +704,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d782d80221dfaa5a2f8a7bf277370bdec10e4e8119f5a60d2e2b1adb2e806ca" +checksum = "498375e6a13b6edd04422a13d2b1a6187183e5a3aa14c5907b4c566551248bab" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3076c226bb4365f9c3ac0cd4082ba86208aaa1485cbf664383a90aba7c36b26" +checksum = "6d9123d321ecd70925646eb2c60b1d9b7a965f860fbd717643e2c20fcf85d48d" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -730,9 +730,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a438ce4cd49ec4bc213868c1fe94f2fe103d4c3f22f6a42073db974f9c0962da" +checksum = "d1a0d2d5c64881f3723232eaaf6c2d9f4f88b061c63e87194b2db785ff3aa31f" dependencies = [ "alloy-primitives", "arbitrary", @@ -742,9 +742,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389372d6ae4d62b88c8dca8238e4f7d0a7727b66029eb8a5516a908a03161450" +checksum = "5ea4ac9765e5a7582877ca53688e041fe184880fe75f16edf0945b24a319c710" dependencies = [ "alloy-primitives", "async-trait", @@ -757,9 +757,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69c260e78b9c104c444f8a202f283d5e8c6637e6fa52a83f649ad6aaa0b91fd0" +checksum = "3c9d85b9f7105ab5ce7dae7b0da33cd9d977601a48f759e1c82958978dd1a905" dependencies = [ "alloy-consensus", "alloy-network", @@ -849,9 +849,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f01c27edb3c0926919586a231d99e06284f9239da6044b5682033ef781e1cc62" +checksum = "4e72f5c4ba505ebead6a71144d72f21a70beadfb2d84e0a560a985491ecb71de" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -872,9 +872,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc57657fd3249fc8324cbbc8edbb7d5114af5fbc7c6c32dff944d6b5922f400" +checksum = "400dc298aaabdbd48be05448c4a19eaa38416c446043f3e54561249149269c32" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -887,9 +887,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a5a36d4ca1261a29dd1d791cd89c21b71d7465211910e43b0862d1c067a211" +checksum = "ba22ff961cf99495ee4fdbaf4623f8d5483d408ca2c6e1b1a54ef438ca87f8dd" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -907,9 +907,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e81effa6a2db6b2152eefb244b4aa6334b1c42819d0eca8d5a91826ec7a9fdba" +checksum = "c38b4472f2bbd96a27f393de9e2f12adca0dc1075fb4d0f7c8f3557c5c600392" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -944,9 +944,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99dac443033e83b14f68fac56e8c27e76421f1253729574197ceccd06598f3ef" +checksum = "e2183706e24173309b0ab0e34d3e53cf3163b71a419803b2b3b0c1fb7ff7a941" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -1380,13 +1380,12 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ "compression-codecs", "compression-core", - "futures-core", "pin-project-lite", "tokio", ] @@ -1514,8 +1513,8 @@ dependencies = [ "alloy-eip7928", "alloy-primitives", "alloy-rlp", - "alloy-sol-macro", "alloy-sol-types", + "base-primitives", "eyre", "op-revm", "reth-evm", @@ -1523,7 +1522,6 @@ dependencies = [ "reth-optimism-evm", "revm", "serde", - "serde_json", "tracing", ] @@ -1556,33 +1554,46 @@ dependencies = [ ] [[package]] -name = "base-flashtypes" +name = "base-client-node" version = "0.2.1" dependencies = [ + "alloy-eips", + "alloy-genesis", "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-serde", - "brotli", - "bytes", + "alloy-signer", + "base-primitives", + "chrono", "derive_more", - "rstest", - "serde", - "serde_json", -] - -[[package]] -name = "base-primitives" -version = "0.2.1" -dependencies = [ "eyre", + "futures-util", + "jsonrpsee", + "op-alloy-network", + "op-alloy-rpc-types-engine", "reth", "reth-db", + "reth-ipc", + "reth-node-core", + "reth-optimism-chainspec", "reth-optimism-node", + "reth-optimism-primitives", + "reth-optimism-rpc", + "reth-primitives-traits", + "reth-provider", + "reth-rpc-layer", + "reth-tracing", + "tokio", + "tower", + "tracing", + "tracing-subscriber 0.3.22", + "url", ] [[package]] -name = "base-reth-flashblocks" +name = "base-flashblocks" version = "0.2.1" dependencies = [ "alloy-consensus", @@ -1599,10 +1610,11 @@ dependencies = [ "alloy-sol-macro", "alloy-sol-types", "arc-swap", + "base-client-node", + "base-flashblocks", "base-flashtypes", - "base-primitives", - "base-reth-test-utils", "criterion", + "derive_more", "eyre", "futures-util", "jsonrpsee", @@ -1618,7 +1630,6 @@ dependencies = [ "reth", "reth-db", "reth-db-common", - "reth-e2e-test-utils", "reth-evm", "reth-exex", "reth-optimism-chainspec", @@ -1632,7 +1643,6 @@ dependencies = [ "reth-rpc", "reth-rpc-convert", "reth-rpc-eth-api", - "reth-testing-utils", "reth-tracing", "reth-transaction-pool", "rstest", @@ -1648,7 +1658,23 @@ dependencies = [ ] [[package]] -name = "base-reth-metering" +name = "base-flashtypes" +version = "0.2.1" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "brotli", + "bytes", + "derive_more", + "rstest", + "serde", + "serde_json", +] + +[[package]] +name = "base-metering" version = "0.2.1" dependencies = [ "alloy-consensus", @@ -1657,8 +1683,7 @@ dependencies = [ "alloy-primitives", "alloy-rpc-client", "base-bundles", - "base-primitives", - "base-reth-test-utils", + "base-client-node", "eyre", "jsonrpsee", "op-alloy-consensus", @@ -1673,25 +1698,41 @@ dependencies = [ "reth-optimism-primitives", "reth-primitives-traits", "reth-provider", - "reth-testing-utils", "reth-transaction-pool", "serde", "tokio", "tracing", ] +[[package]] +name = "base-primitives" +version = "0.2.1" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-signer", + "alloy-signer-local", + "alloy-sol-macro", + "alloy-sol-types", + "eyre", + "op-alloy-network", + "op-alloy-rpc-types", + "serde_json", +] + [[package]] name = "base-reth-node" version = "0.2.1" dependencies = [ "base-cli-utils", - "base-primitives", - "base-reth-flashblocks", - "base-reth-metering", - "base-reth-runner", + "base-client-node", + "base-flashblocks", + "base-metering", "base-txpool", "clap", - "once_cell", "reth-cli-util", "reth-optimism-cli", "reth-optimism-node", @@ -1705,80 +1746,12 @@ dependencies = [ "reth-rpc-eth-types", ] -[[package]] -name = "base-reth-runner" -version = "0.2.1" -dependencies = [ - "base-primitives", - "base-reth-flashblocks", - "base-reth-metering", - "base-txpool", - "derive_more", - "eyre", - "futures-util", - "reth", - "reth-db", - "reth-optimism-chainspec", - "reth-optimism-node", - "tracing", -] - -[[package]] -name = "base-reth-test-utils" -version = "0.2.1" -dependencies = [ - "alloy-consensus", - "alloy-contract", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-client", - "alloy-rpc-types", - "alloy-rpc-types-engine", - "alloy-signer", - "alloy-signer-local", - "alloy-sol-macro", - "alloy-sol-types", - "base-flashtypes", - "base-reth-flashblocks", - "chrono", - "derive_more", - "eyre", - "futures-util", - "jsonrpsee", - "once_cell", - "op-alloy-network", - "op-alloy-rpc-types", - "op-alloy-rpc-types-engine", - "reth", - "reth-db", - "reth-e2e-test-utils", - "reth-exex", - "reth-ipc", - "reth-node-core", - "reth-optimism-chainspec", - "reth-optimism-node", - "reth-optimism-primitives", - "reth-optimism-rpc", - "reth-primitives-traits", - "reth-provider", - "reth-rpc-layer", - "reth-tracing", - "serde_json", - "tokio", - "tokio-stream", - "tower", - "tracing-subscriber 0.3.22", - "url", -] - [[package]] name = "base-txpool" version = "0.2.1" dependencies = [ "alloy-primitives", - "base-primitives", + "base-client-node", "chrono", "derive_more", "eyre", @@ -2556,9 +2529,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.35" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" dependencies = [ "brotli", "compression-core", @@ -3045,15 +3018,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -3061,9 +3034,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", "syn 2.0.114", @@ -5327,15 +5300,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru" version = "0.16.3" @@ -8010,6 +7974,7 @@ dependencies = [ "alloy-eip2124", "alloy-hardforks", "alloy-primitives", + "arbitrary", "auto_impl", "once_cell", "rustc-hash", @@ -8918,6 +8883,7 @@ version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" dependencies = [ "alloy-consensus", + "alloy-genesis", "alloy-primitives", "alloy-rpc-types-engine", "alloy-rpc-types-eth", @@ -8928,6 +8894,7 @@ dependencies = [ "op-revm", "reth-chainspec", "reth-consensus", + "reth-e2e-test-utils", "reth-engine-local", "reth-evm", "reth-network", @@ -8949,11 +8916,13 @@ dependencies = [ "reth-rpc-api", "reth-rpc-engine-api", "reth-rpc-server-types", + "reth-tasks", "reth-tracing", "reth-transaction-pool", "reth-trie-common", "revm", "serde", + "serde_json", "tokio", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 71d3c104..ad905eda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,16 @@ codegen-units = 1 [workspace.dependencies] # Shared base-access-lists = { path = "crates/shared/access-lists" } -base-flashtypes = { path = "crates/shared/flashtypes" } +base-bundles = { path = "crates/shared/bundles" } base-cli-utils = { path = "crates/shared/cli-utils" } +base-flashtypes = { path = "crates/shared/flashtypes" } +base-primitives = { path = "crates/shared/primitives" } +base-reth-rpc-types = { path = "crates/shared/reth-rpc-types" } # Client -base-primitives = { path = "crates/client/primitives" } -base-bundles = { path = "crates/shared/bundles" } -base-reth-metering = { path = "crates/client/metering" } -base-reth-rpc-types = { path = "crates/client/reth-rpc-types" } +base-client-node = { path = "crates/client/node" } +base-metering = { path = "crates/client/metering" } base-txpool = { path = "crates/client/txpool" } -base-reth-runner = { path = "crates/client/runner" } -base-reth-test-utils = { path = "crates/client/test-utils" } -base-reth-flashblocks = { path = "crates/client/flashblocks" } +base-flashblocks = { path = "crates/client/flashblocks" } # reth reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } @@ -105,6 +104,7 @@ alloy-eips = "1.0.41" alloy-serde = "1.0.41" alloy-eip7928 = "0.3.0" alloy-genesis = "1.0.41" +alloy-signer = "1.0.41" alloy-signer-local = "1.0.41" alloy-hardforks = "0.4.4" alloy-provider = "1.0.41" diff --git a/Justfile b/Justfile index 5ff85189..b231c87e 100644 --- a/Justfile +++ b/Justfile @@ -83,7 +83,7 @@ build-node: # Build the contracts used for tests build-contracts: - cd crates/client/test-utils/contracts && forge build + cd crates/shared/primitives/contracts && forge build # Cleans the workspace clean: @@ -94,6 +94,10 @@ check-udeps: build-contracts @command -v cargo-udeps >/dev/null 2>&1 || cargo install cargo-udeps cargo +nightly udeps --workspace --all-features --all-targets +# Checks that shared crates don't depend on client crates +check-crate-deps: + ./scripts/check-crate-deps.sh + # Watches tests watch-test: build-contracts cargo watch -x test @@ -108,4 +112,4 @@ benches: # Runs flashblocks pending state benchmarks bench-flashblocks: - cargo bench -p base-reth-flashblocks --bench pending_state + cargo bench -p base-flashblocks --bench pending_state diff --git a/README.md b/README.md index b057bd79..73d5c1fa 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,6 @@ Base Reth Node is a Reth-based Ethereum node implementation, specifically tailor > [node-reth image](https://github.com/base/node/pkgs/container/node-reth). This image bundles vanilla Reth and Base Reth and can be toggled with > `NODE_TYPE=base` or `NODE_TYPE=vanilla` -## Repository Structure - -``` -. -├── Cargo.toml # Rust workspace and package definitions -├── Cargo.lock # Dependency lockfile -├── Dockerfile # For building the Docker image -├── LICENSE # MIT License -├── README.md # This file -├── crates/ -│ ├── client/ # Node client crates (cli, rpc, flashblocks, txpool, etc.) -│ └── shared/ # Shared library crates (access-lists, flashtypes) -├── justfile # Command runner for development tasks -└── .github/ - └── workflows/ - └── ci.yml # GitHub Actions CI configuration -``` - ## Prerequisites - **Rust:** Version 1.85 or later (as specified in `Cargo.toml`). You can install Rust using [rustup](https://rustup.rs/). diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index 3f22f493..43f2ce23 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -15,10 +15,9 @@ workspace = true [dependencies] # internal base-cli-utils.workspace = true -base-primitives.workspace = true -base-reth-flashblocks.workspace = true -base-reth-metering.workspace = true -base-reth-runner.workspace = true +base-client-node.workspace = true +base-flashblocks.workspace = true +base-metering.workspace = true base-txpool.workspace = true # reth @@ -28,7 +27,6 @@ reth-cli-util.workspace = true # misc clap.workspace = true -once_cell.workspace = true [features] default = [] diff --git a/bin/node/src/cli.rs b/bin/node/src/cli.rs index 08a0e1a0..36aaf70c 100644 --- a/bin/node/src/cli.rs +++ b/bin/node/src/cli.rs @@ -1,10 +1,7 @@ //! Contains the CLI arguments -use std::sync::Arc; - -use base_primitives::{FlashblocksConfig, TracingConfig}; -use base_reth_runner::{BaseNodeConfig, RunnerFlashblocksCell}; -use once_cell::sync::OnceCell; +use base_flashblocks::FlashblocksConfig; +use base_txpool::TxpoolConfig; use reth_optimism_node::args::RollupArgs; /// CLI Arguments @@ -51,23 +48,21 @@ impl Args { } } -impl From for BaseNodeConfig { +impl From for Option { fn from(args: Args) -> Self { - let flashblocks_cell: RunnerFlashblocksCell = Arc::new(OnceCell::new()); - let flashblocks = args.websocket_url.map(|websocket_url| FlashblocksConfig { - websocket_url, + args.websocket_url.map(|url| FlashblocksConfig { + websocket_url: url, max_pending_blocks_depth: args.max_pending_blocks_depth, - }); + }) + } +} +impl From for TxpoolConfig { + fn from(args: Args) -> Self { Self { - rollup_args: args.rollup_args, - flashblocks, - tracing: TracingConfig { - enabled: args.enable_transaction_tracing, - logs_enabled: args.enable_transaction_tracing_logs, - }, - metering_enabled: args.enable_metering, - flashblocks_cell, + tracing_enabled: args.enable_transaction_tracing, + tracing_logs_enabled: args.enable_transaction_tracing_logs, + sequencer_rpc: args.rollup_args.sequencer, } } } diff --git a/bin/node/src/main.rs b/bin/node/src/main.rs index f7a247c9..10b591ee 100644 --- a/bin/node/src/main.rs +++ b/bin/node/src/main.rs @@ -5,10 +5,10 @@ pub mod cli; -use base_reth_flashblocks::{FlashblocksCanonExtension, FlashblocksRpcExtension}; -use base_reth_metering::MeteringRpcExtension; -use base_reth_runner::BaseNodeRunner; -use base_txpool::{TransactionStatusRpcExtension, TransactionTracingExtension}; +use base_client_node::BaseNodeRunner; +use base_flashblocks::FlashblocksExtension; +use base_metering::MeteringExtension; +use base_txpool::TxPoolExtension; #[global_allocator] static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator(); @@ -24,16 +24,12 @@ fn main() { // Step 3: Hand the parsed CLI to the node runner so it can build and launch the Base node. cli.run(|builder, args| async move { - let mut runner = BaseNodeRunner::new(args); + let mut runner = BaseNodeRunner::new(args.rollup_args.clone()); - // ExEx extensions - runner.install_ext::()?; - runner.install_ext::()?; - - // RPC extensions (FlashblocksRpcExtension must be last - uses replace_configured) - runner.install_ext::()?; - runner.install_ext::()?; - runner.install_ext::()?; + // Feature extensions (FlashblocksExtension must be last - uses replace_configured) + runner.install_ext::(args.clone().into()); + runner.install_ext::(args.enable_metering); + runner.install_ext::(args.into()); let handle = runner.run(builder); handle.await diff --git a/crates/client/flashblocks/Cargo.toml b/crates/client/flashblocks/Cargo.toml index 82166911..7a0604c4 100644 --- a/crates/client/flashblocks/Cargo.toml +++ b/crates/client/flashblocks/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "base-reth-flashblocks" +name = "base-flashblocks" description = "Flashblocks State Management" version.workspace = true edition.workspace = true @@ -11,10 +11,19 @@ repository.workspace = true [lints] workspace = true +[features] +test-utils = [ + "base-client-node/test-utils", + "dep:derive_more", + "dep:eyre", + "reth-evm/test-utils", + "reth-primitives/test-utils", +] + [dependencies] # workspace base-flashtypes.workspace = true -base-primitives.workspace = true +base-client-node.workspace = true # reth reth.workspace = true @@ -58,7 +67,6 @@ jsonrpsee-types.workspace = true serde.workspace = true # misc -eyre.workspace = true once_cell.workspace = true url.workspace = true thiserror.workspace = true @@ -68,7 +76,12 @@ arc-swap.workspace = true metrics-derive.workspace = true rayon.workspace = true +# Optional test-utils dependencies +derive_more = { workspace = true, features = ["deref"], optional = true } +eyre = { workspace = true, optional = true } + [dev-dependencies] +base-flashblocks = { path = ".", features = ["test-utils"] } rstest.workspace = true rand.workspace = true eyre.workspace = true @@ -83,14 +96,11 @@ alloy-sol-macro = { workspace = true, features = ["json"] } alloy-sol-types.workspace = true alloy-contract.workspace = true op-alloy-consensus.workspace = true -reth-testing-utils.workspace = true reth-tracing.workspace = true reth-optimism-node.workspace = true reth-transaction-pool = { workspace = true, features = ["test-utils"] } tokio-tungstenite.workspace = true tracing-subscriber.workspace = true -reth-e2e-test-utils.workspace = true -base-reth-test-utils.workspace = true serde_json.workspace = true futures-util.workspace = true criterion = { version = "0.5", features = ["async_tokio"] } diff --git a/crates/client/flashblocks/benches/pending_state.rs b/crates/client/flashblocks/benches/pending_state.rs index 7b201576..182b190a 100644 --- a/crates/client/flashblocks/benches/pending_state.rs +++ b/crates/client/flashblocks/benches/pending_state.rs @@ -8,11 +8,11 @@ use std::{ use alloy_eips::{BlockHashOrNumber, Encodable2718}; use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, bytes, hex::FromHex}; use alloy_rpc_types_engine::PayloadId; +use base_client_node::test_utils::{Account, LocalNodeProvider, TestHarness}; +use base_flashblocks::{FlashblocksAPI, FlashblocksReceiver, FlashblocksState}; use base_flashtypes::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, Flashblock, Metadata, }; -use base_reth_flashblocks::{FlashblocksAPI, FlashblocksReceiver, FlashblocksState}; -use base_reth_test_utils::{LocalNodeProvider, TestAccounts, TestHarness}; use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; use reth::{ chainspec::{ChainSpecProvider, EthChainSpec}, @@ -56,7 +56,7 @@ impl BenchSetup { let flashblocks = tx_counts .iter() .map(|count| { - let txs = sample_transactions(&provider, harness.accounts(), *count); + let txs = sample_transactions(&provider, *count); let blocks = build_flashblocks(&canonical_block, &txs); (format!("pending_state_{}_txs", count), blocks) }) @@ -260,12 +260,8 @@ fn transaction_flashblock( } } -fn sample_transactions( - provider: &LocalNodeProvider, - accounts: &TestAccounts, - count: usize, -) -> Vec { - let signer = B256::from_hex(accounts.alice.private_key).expect("valid private key hex"); +fn sample_transactions(provider: &LocalNodeProvider, count: usize) -> Vec { + let signer = B256::from_hex(Account::Alice.private_key()).expect("valid private key hex"); let chain_id = provider.chain_spec().chain_id(); (0..count as u64) @@ -273,7 +269,7 @@ fn sample_transactions( let txn = TransactionBuilder::default() .signer(signer) .chain_id(chain_id) - .to(accounts.bob.address) + .to(Account::Bob.address()) .nonce(nonce) .value(1_000_000_000u128) .gas_limit(TX_GAS_USED) diff --git a/crates/client/flashblocks/src/extension.rs b/crates/client/flashblocks/src/extension.rs index 0f1ea1d3..a9f68d40 100644 --- a/crates/client/flashblocks/src/extension.rs +++ b/crates/client/flashblocks/src/extension.rs @@ -1,31 +1,43 @@ -//! Contains the [`FlashblocksCanonExtension`] which wires up the `flashblocks-canon` -//! execution extension on the Base node builder. +//! Contains the [`FlashblocksExtension`] which wires up the flashblocks feature +//! (both the canon ExEx and RPC surface) on the Base node builder. use std::sync::Arc; -use base_primitives::{ - BaseNodeExtension, ConfigurableBaseNodeExtension, FlashblocksConfig, OpBuilder, OpProvider, -}; +use base_client_node::{BaseNodeExtension, FromExtensionConfig, OpBuilder, OpProvider}; use futures_util::TryStreamExt; use once_cell::sync::OnceCell; use reth_exex::ExExEvent; +use tracing::info; +use url::Url; -use crate::FlashblocksState; +use crate::{ + EthApiExt, EthApiOverrideServer, EthPubSub, EthPubSubApiServer, FlashblocksState, + FlashblocksSubscriber, +}; /// The flashblocks cell holds a shared state reference. pub type FlashblocksCell = Arc>>; -/// Helper struct that wires the Flashblocks canon ExEx into the node builder. +/// Flashblocks-specific configuration knobs. #[derive(Debug, Clone)] -pub struct FlashblocksCanonExtension { +pub struct FlashblocksConfig { + /// The websocket endpoint that streams flashblock updates. + pub websocket_url: String, + /// Maximum number of pending flashblocks to retain in memory. + pub max_pending_blocks_depth: u64, +} + +/// Helper struct that wires the Flashblocks feature (canon ExEx and RPC) into the node builder. +#[derive(Debug, Clone)] +pub struct FlashblocksExtension { /// Shared Flashblocks state cache. - pub cell: FlashblocksCell>, + cell: FlashblocksCell>, /// Optional Flashblocks configuration. - pub config: Option, + config: Option, } -impl FlashblocksCanonExtension { - /// Create a new Flashblocks canon extension helper. +impl FlashblocksExtension { + /// Create a new Flashblocks extension helper. pub const fn new( cell: FlashblocksCell>, config: Option, @@ -34,23 +46,27 @@ impl FlashblocksCanonExtension { } } -impl BaseNodeExtension for FlashblocksCanonExtension { +impl BaseNodeExtension for FlashblocksExtension { /// Applies the extension to the supplied builder. fn apply(self: Box, builder: OpBuilder) -> OpBuilder { - let flashblocks = self.config; - let flashblocks_enabled = flashblocks.is_some(); + let Some(cfg) = self.config else { + info!(message = "flashblocks integration is disabled"); + return builder; + }; + let flashblocks_cell = self.cell; + let cfg_for_rpc = cfg.clone(); + let flashblocks_cell_for_rpc = flashblocks_cell.clone(); - builder.install_exex_if(flashblocks_enabled, "flashblocks-canon", move |mut ctx| { + // Install the canon ExEx + let builder = builder.install_exex("flashblocks-canon", move |mut ctx| { let flashblocks_cell = flashblocks_cell.clone(); async move { - let fb_config = - flashblocks.as_ref().expect("flashblocks config checked above").clone(); let fb = flashblocks_cell .get_or_init(|| { Arc::new(FlashblocksState::new( ctx.provider().clone(), - fb_config.max_pending_blocks_depth, + cfg.max_pending_blocks_depth, )) }) .clone(); @@ -69,22 +85,48 @@ impl BaseNodeExtension for FlashblocksCanonExtension { Ok(()) }) } + }); + + // Extend with RPC modules + builder.extend_rpc_modules(move |ctx| { + info!(message = "Starting Flashblocks RPC"); + + let ws_url = Url::parse(cfg_for_rpc.websocket_url.as_str())?; + let fb = flashblocks_cell_for_rpc + .get_or_init(|| { + Arc::new(FlashblocksState::new( + ctx.provider().clone(), + cfg_for_rpc.max_pending_blocks_depth, + )) + }) + .clone(); + fb.start(); + + let mut flashblocks_client = FlashblocksSubscriber::new(fb.clone(), ws_url); + flashblocks_client.start(); + + let api_ext = EthApiExt::new( + ctx.registry.eth_api().clone(), + ctx.registry.eth_handlers().filter.clone(), + fb.clone(), + ); + ctx.modules.replace_configured(api_ext.into_rpc())?; + + // Register the eth_subscribe subscription endpoint for flashblocks + // Uses replace_configured since eth_subscribe already exists from reth's standard module + // Pass eth_api to enable proxying standard subscription types to reth's implementation + let eth_pubsub = EthPubSub::new(ctx.registry.eth_api().clone(), fb); + ctx.modules.replace_configured(eth_pubsub.into_rpc())?; + + Ok(()) }) } } -/// Configuration trait for [`FlashblocksCanonExtension`]. -/// -/// Types implementing this trait can be used to construct a [`FlashblocksCanonExtension`]. -pub trait FlashblocksCanonConfig { - /// Returns the shared flashblocks cell. - fn flashblocks_cell(&self) -> &FlashblocksCell>; - /// Returns the flashblocks configuration if enabled. - fn flashblocks(&self) -> Option<&FlashblocksConfig>; -} +impl FromExtensionConfig for FlashblocksExtension { + type Config = Option; -impl ConfigurableBaseNodeExtension for FlashblocksCanonExtension { - fn build(config: &C) -> eyre::Result { - Ok(Self::new(config.flashblocks_cell().clone(), config.flashblocks().cloned())) + fn from_config(config: Self::Config) -> Self { + Self::new(Arc::new(OnceCell::new()), config) } } diff --git a/crates/client/flashblocks/src/lib.rs b/crates/client/flashblocks/src/lib.rs index 1f2f1ced..06f1f2b0 100644 --- a/crates/client/flashblocks/src/lib.rs +++ b/crates/client/flashblocks/src/lib.rs @@ -45,7 +45,7 @@ pub use rpc::{ }; mod extension; -pub use extension::{FlashblocksCanonConfig, FlashblocksCanonExtension, FlashblocksCell}; +pub use extension::{FlashblocksCell, FlashblocksConfig, FlashblocksExtension}; -mod rpc_extension; -pub use rpc_extension::FlashblocksRpcExtension; +#[cfg(any(test, feature = "test-utils"))] +pub mod test_harness; diff --git a/crates/client/flashblocks/src/rpc_extension.rs b/crates/client/flashblocks/src/rpc_extension.rs deleted file mode 100644 index e1163e11..00000000 --- a/crates/client/flashblocks/src/rpc_extension.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Contains the [`FlashblocksRpcExtension`] which wires up the flashblocks RPC surface -//! on the Base node builder. - -use std::sync::Arc; - -use base_primitives::{ - BaseNodeExtension, ConfigurableBaseNodeExtension, FlashblocksConfig, OpBuilder, OpProvider, -}; -use tracing::info; -use url::Url; - -use crate::{ - EthApiExt, EthApiOverrideServer, EthPubSub, EthPubSubApiServer, FlashblocksCell, - FlashblocksState, FlashblocksSubscriber, -}; - -/// Helper struct that wires the flashblocks RPC into the node builder. -#[derive(Debug, Clone)] -pub struct FlashblocksRpcExtension { - /// Shared Flashblocks state cache. - pub cell: FlashblocksCell>, - /// Optional Flashblocks configuration. - pub config: Option, -} - -impl FlashblocksRpcExtension { - /// Creates a new flashblocks RPC extension. - pub const fn new( - cell: FlashblocksCell>, - config: Option, - ) -> Self { - Self { cell, config } - } -} - -impl BaseNodeExtension for FlashblocksRpcExtension { - /// Applies the extension to the supplied builder. - fn apply(self: Box, builder: OpBuilder) -> OpBuilder { - let Some(cfg) = self.config else { - info!(message = "flashblocks RPC integration is disabled"); - return builder; - }; - - let flashblocks_cell = self.cell; - - builder.extend_rpc_modules(move |ctx| { - info!(message = "Starting Flashblocks RPC"); - - let ws_url = Url::parse(cfg.websocket_url.as_str())?; - let fb = flashblocks_cell - .get_or_init(|| { - Arc::new(FlashblocksState::new( - ctx.provider().clone(), - cfg.max_pending_blocks_depth, - )) - }) - .clone(); - fb.start(); - - let mut flashblocks_client = FlashblocksSubscriber::new(fb.clone(), ws_url); - flashblocks_client.start(); - - let api_ext = EthApiExt::new( - ctx.registry.eth_api().clone(), - ctx.registry.eth_handlers().filter.clone(), - fb.clone(), - ); - ctx.modules.replace_configured(api_ext.into_rpc())?; - - // Register the eth_subscribe subscription endpoint for flashblocks - // Uses replace_configured since eth_subscribe already exists from reth's standard module - // Pass eth_api to enable proxying standard subscription types to reth's implementation - let eth_pubsub = EthPubSub::new(ctx.registry.eth_api().clone(), fb); - ctx.modules.replace_configured(eth_pubsub.into_rpc())?; - - Ok(()) - }) - } -} - -// Reuse FlashblocksCanonConfig since the methods are identical. -// Any type implementing FlashblocksCanonConfig can construct a FlashblocksRpcExtension. -impl ConfigurableBaseNodeExtension - for FlashblocksRpcExtension -{ - fn build(config: &C) -> eyre::Result { - Ok(Self::new(config.flashblocks_cell().clone(), config.flashblocks().cloned())) - } -} diff --git a/crates/client/flashblocks/src/test_harness.rs b/crates/client/flashblocks/src/test_harness.rs new file mode 100644 index 00000000..cb9e1e62 --- /dev/null +++ b/crates/client/flashblocks/src/test_harness.rs @@ -0,0 +1,317 @@ +//! Flashblocks test harness module. +//! +//! Provides test utilities for flashblocks including: +//! - [`FlashblocksHarness`] - High-level test harness wrapping [`TestHarness`] +//! - [`FlashblocksParts`] - Components for interacting with flashblocks worker tasks +//! - [`FlashblocksTestExtension`] - Node extension for wiring up flashblocks in tests +//! - [`FlashblocksLocalNode`] - Local node wrapper with flashblocks helpers + +use std::{ + fmt, + sync::{Arc, Mutex}, + time::Duration, +}; + +use base_client_node::{ + BaseNodeExtension, + test_utils::{ + LocalNode, LocalNodeProvider, NODE_STARTUP_DELAY_MS, TestHarness, build_test_genesis, + init_silenced_tracing, + }, +}; +use base_flashtypes::Flashblock; +use derive_more::Deref; +use eyre::Result; +use once_cell::sync::OnceCell; +use reth::providers::CanonStateSubscriptions; +use reth_optimism_chainspec::OpChainSpec; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; + +use crate::{ + EthApiExt, EthApiOverrideServer, EthPubSub, EthPubSubApiServer, FlashblocksReceiver, + FlashblocksState, +}; + +/// Convenience alias for the Flashblocks state backing the local node. +pub type LocalFlashblocksState = FlashblocksState; + +/// Components that allow tests to interact with the Flashblocks worker tasks. +#[derive(Clone)] +pub struct FlashblocksParts { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + state: Arc, +} + +impl fmt::Debug for FlashblocksParts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FlashblocksParts").finish_non_exhaustive() + } +} + +impl FlashblocksParts { + /// Clone the shared [`FlashblocksState`] handle. + pub fn state(&self) -> Arc { + self.state.clone() + } + + /// Send a flashblock to the background processor and wait until it is handled. + pub async fn send(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; + rx.await.map_err(|err| eyre::eyre!(err))?; + Ok(()) + } +} + +/// Test extension for flashblocks functionality. +/// +/// This extension wires up the flashblocks ExEx and RPC modules for testing, +/// with optional control over canonical block processing. +#[derive(Clone, Debug)] +pub struct FlashblocksTestExtension { + inner: Arc, +} + +struct FlashblocksTestExtensionInner { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + #[allow(clippy::type_complexity)] + receiver: Arc)>>>>, + fb_cell: Arc>>, + process_canonical: bool, +} + +impl fmt::Debug for FlashblocksTestExtensionInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FlashblocksTestExtensionInner") + .field("process_canonical", &self.process_canonical) + .finish_non_exhaustive() + } +} + +impl FlashblocksTestExtension { + /// Create a new flashblocks test extension. + /// + /// If `process_canonical` is true, canonical blocks are automatically processed. + /// Set to false for tests that need manual control over canonical block timing. + pub fn new(process_canonical: bool) -> Self { + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let inner = FlashblocksTestExtensionInner { + sender, + receiver: Arc::new(Mutex::new(Some(receiver))), + fb_cell: Arc::new(OnceCell::new()), + process_canonical, + }; + Self { inner: Arc::new(inner) } + } + + /// Get the flashblocks parts after the node has been launched. + pub fn parts(&self) -> Result { + let state = self.inner.fb_cell.get().ok_or_else(|| { + eyre::eyre!("FlashblocksState should be initialized during node launch") + })?; + Ok(FlashblocksParts { sender: self.inner.sender.clone(), state: state.clone() }) + } +} + +impl BaseNodeExtension for FlashblocksTestExtension { + fn apply(self: Box, builder: base_client_node::OpBuilder) -> base_client_node::OpBuilder { + let fb_cell = self.inner.fb_cell.clone(); + let receiver = self.inner.receiver.clone(); + let process_canonical = self.inner.process_canonical; + + let fb_cell_for_exex = fb_cell.clone(); + + builder + .install_exex("flashblocks-canon", move |mut ctx| { + let fb_cell = fb_cell_for_exex.clone(); + async move { + let provider = ctx.provider().clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + Ok(async move { + use reth_exex::ExExEvent; + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + let hash = committed.tip().num_hash(); + if process_canonical { + // Many suites drive canonical updates manually to reproduce race conditions, so + // allowing this to be disabled keeps canonical replay deterministic. + let chain = Arc::unwrap_or_clone(committed); + for (_, block) in chain.into_blocks() { + fb.on_canonical_block_received(block); + } + } + let _ = ctx.events.send(ExExEvent::FinishedHeight(hash)); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb_cell = fb_cell.clone(); + let provider = ctx.provider().clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + + let mut canon_stream = tokio_stream::wrappers::BroadcastStream::new( + ctx.provider().subscribe_to_canonical_state(), + ); + tokio::spawn(async move { + use tokio_stream::StreamExt; + while let Some(Ok(notification)) = canon_stream.next().await { + provider.canonical_in_memory_state().notify_canon_state(notification); + } + }); + let api_ext = EthApiExt::new( + ctx.registry.eth_api().clone(), + ctx.registry.eth_handlers().filter.clone(), + fb.clone(), + ); + ctx.modules.replace_configured(api_ext.into_rpc())?; + + // Register eth_subscribe subscription endpoint for flashblocks + // Uses replace_configured since eth_subscribe already exists from reth's standard module + // Pass eth_api to enable proxying standard subscription types to reth's implementation + let eth_pubsub = EthPubSub::new(ctx.registry.eth_api().clone(), fb.clone()); + ctx.modules.replace_configured(eth_pubsub.into_rpc())?; + + let fb_for_task = fb.clone(); + let mut receiver = receiver + .lock() + .expect("flashblock receiver mutex poisoned") + .take() + .expect("flashblock receiver should only be initialized once"); + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb_for_task.on_flashblock_received(payload); + let _ = tx.send(()); + } + }); + + Ok(()) + }) + } +} + +fn init_flashblocks_state( + cell: &Arc>>, + provider: &LocalNodeProvider, +) -> Arc { + cell.get_or_init(|| { + let fb = Arc::new(FlashblocksState::new(provider.clone(), 5)); + fb.start(); + fb + }) + .clone() +} + +/// Local node wrapper that exposes helpers specific to Flashblocks tests. +pub struct FlashblocksLocalNode { + node: LocalNode, + parts: FlashblocksParts, +} + +impl fmt::Debug for FlashblocksLocalNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FlashblocksLocalNode") + .field("node", &self.node) + .field("parts", &self.parts) + .finish() + } +} + +impl FlashblocksLocalNode { + /// Launch a flashblocks-enabled node using the default configuration. + pub async fn new() -> Result { + Self::with_options(true).await + } + + /// Builds a flashblocks-enabled node with canonical block streaming disabled so tests can call + /// `FlashblocksState::on_canonical_block_received` at precise points. + pub async fn manual_canonical() -> Result { + Self::with_options(false).await + } + + async fn with_options(process_canonical: bool) -> Result { + // Build default chain spec programmatically + let genesis = build_test_genesis(); + let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); + + let extension = FlashblocksTestExtension::new(process_canonical); + let parts_source = extension.clone(); + let node = LocalNode::new(vec![Box::new(extension)], chain_spec).await?; + let parts = parts_source.parts()?; + Ok(Self { node, parts }) + } + + /// Access the shared Flashblocks state for assertions or manual driving. + pub fn flashblocks_state(&self) -> Arc { + self.parts.state() + } + + /// Send a flashblock through the background processor and await completion. + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.parts.send(flashblock).await + } + + /// Split the wrapper into the underlying node plus flashblocks parts. + pub fn into_parts(self) -> (LocalNode, FlashblocksParts) { + (self.node, self.parts) + } +} + +/// Helper that exposes [`TestHarness`] conveniences plus Flashblocks helpers. +#[derive(Debug, Deref)] +pub struct FlashblocksHarness { + #[deref] + inner: TestHarness, + parts: FlashblocksParts, +} + +impl FlashblocksHarness { + /// Launch a flashblocks-enabled harness with automatic canonical processing. + pub async fn new() -> Result { + Self::with_options(true).await + } + + /// Launch the harness configured for manual canonical progression. + pub async fn manual_canonical() -> Result { + Self::with_options(false).await + } + + /// Get a handle to the in-memory Flashblocks state backing the harness. + pub fn flashblocks_state(&self) -> Arc { + self.parts.state() + } + + /// Send a single flashblock through the harness. + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.parts.send(flashblock).await + } + + async fn with_options(process_canonical: bool) -> Result { + init_silenced_tracing(); + + // Build default chain spec programmatically + let genesis = build_test_genesis(); + let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); + + // Create the extension and keep a reference to get parts after launch + let extension = FlashblocksTestExtension::new(process_canonical); + let parts_source = extension.clone(); + + // Launch the node with the flashblocks extension + let node = LocalNode::new(vec![Box::new(extension)], chain_spec).await?; + let engine = node.engine_api()?; + + tokio::time::sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; + + // Get the parts from the extension after node launch + let parts = parts_source.parts()?; + + // Create harness by building it directly (avoiding TestHarnessBuilder since we already have node) + let inner = TestHarness::from_parts(node, engine); + + Ok(Self { inner, parts }) + } +} diff --git a/crates/client/flashblocks/tests/README.md b/crates/client/flashblocks/tests/README.md index d7152a24..3a822c9a 100644 --- a/crates/client/flashblocks/tests/README.md +++ b/crates/client/flashblocks/tests/README.md @@ -1,6 +1,6 @@ ## Flashblocks RPC Integration Tests -The suites under this directory exercise `base-reth-flashblocks-rpc` the same way external +The suites under this directory exercise `base-flashblocks-rpc` the same way external consumers do — by linking against the published library instead of the crate's `cfg(test)` build. Running them from `tests/` ensures: diff --git a/crates/client/flashblocks/tests/eip7702_tests.rs b/crates/client/flashblocks/tests/eip7702_tests.rs index e4ab15c9..b1e17f46 100644 --- a/crates/client/flashblocks/tests/eip7702_tests.rs +++ b/crates/client/flashblocks/tests/eip7702_tests.rs @@ -8,12 +8,13 @@ use alloy_eips::{eip2718::Encodable2718, eip7702::Authorization}; use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_provider::Provider; use alloy_sol_types::SolCall; +use base_client_node::test_utils::{ + Account, L1_BLOCK_INFO_DEPOSIT_TX, Minimal7702Account, SignerSync, +}; +use base_flashblocks::test_harness::FlashblocksHarness; use base_flashtypes::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, Flashblock, Metadata, }; -use base_reth_test_utils::{ - Account, FlashblocksHarness, L1_BLOCK_INFO_DEPOSIT_TX, Minimal7702Account, SignerSync, -}; use eyre::Result; use op_alloy_network::ReceiptResponse; @@ -31,7 +32,7 @@ struct TestSetup { impl TestSetup { async fn new() -> Result { let harness = FlashblocksHarness::new().await?; - let deployer = &harness.accounts().deployer; + let deployer = Account::Deployer; // Deploy Minimal7702Account contract let deploy_data = Minimal7702Account::BYTECODE.to_vec(); @@ -48,18 +49,6 @@ impl TestSetup { fn provider(&self) -> alloy_provider::RootProvider { self.harness.provider() } - - fn chain_id(&self) -> u64 { - 84532 // Base Sepolia chain ID (matches test harness) - } - - fn alice(&self) -> &Account { - &self.harness.accounts().alice - } - - fn bob(&self) -> &Account { - &self.harness.accounts().bob - } } /// Build an EIP-7702 authorization for delegating to a contract @@ -67,7 +56,7 @@ fn build_authorization( chain_id: u64, contract_address: Address, nonce: u64, - account: &Account, + account: Account, ) -> alloy_eips::eip7702::SignedAuthorization { let auth = Authorization { chain_id: U256::from(chain_id), address: contract_address, nonce }; @@ -83,7 +72,7 @@ fn build_eip7702_tx( value: U256, input: Bytes, authorization_list: Vec, - account: &Account, + account: Account, ) -> Bytes { let tx = TxEip7702 { chain_id, @@ -111,7 +100,7 @@ fn build_eip1559_tx( to: Address, value: U256, input: Bytes, - account: &Account, + account: Account, ) -> Bytes { let tx = TxEip1559 { chain_id, @@ -180,14 +169,14 @@ fn create_eip7702_flashblock(eip7702_tx: Bytes, cumulative_gas: u64) -> Flashblo #[tokio::test] async fn test_eip7702_delegation_in_pending_flashblock() -> Result<()> { let setup = TestSetup::new().await?; - let chain_id = setup.chain_id(); + let chain_id = setup.harness.chain_id(); // Send base flashblock with contract deployment let base_payload = create_base_flashblock(&setup); setup.send_flashblock(base_payload).await?; // Create authorization for Alice to delegate to the Minimal7702Account contract - let auth = build_authorization(chain_id, setup.account_contract_address, 0, setup.alice()); + let auth = build_authorization(chain_id, setup.account_contract_address, 0, Account::Alice); // Build EIP-7702 transaction with authorization // This delegates Alice's EOA to execute code from Minimal7702Account @@ -195,11 +184,11 @@ async fn test_eip7702_delegation_in_pending_flashblock() -> Result<()> { let eip7702_tx = build_eip7702_tx( chain_id, 0, - setup.alice().address, + Account::Alice.address(), U256::ZERO, Bytes::from(increment_call.abi_encode()), vec![auth], - setup.alice(), + Account::Alice, ); let tx_hash = alloy_primitives::keccak256(&eip7702_tx); @@ -222,7 +211,7 @@ async fn test_eip7702_delegation_in_pending_flashblock() -> Result<()> { #[tokio::test] async fn test_eip7702_multiple_delegations_same_flashblock() -> Result<()> { let setup = TestSetup::new().await?; - let chain_id = setup.chain_id(); + let chain_id = setup.harness.chain_id(); // Send base flashblock with contract deployment let base_payload = create_base_flashblock(&setup); @@ -230,29 +219,29 @@ async fn test_eip7702_multiple_delegations_same_flashblock() -> Result<()> { // Create authorizations for both Alice and Bob let auth_alice = - build_authorization(chain_id, setup.account_contract_address, 0, setup.alice()); - let auth_bob = build_authorization(chain_id, setup.account_contract_address, 0, setup.bob()); + build_authorization(chain_id, setup.account_contract_address, 0, Account::Alice); + let auth_bob = build_authorization(chain_id, setup.account_contract_address, 0, Account::Bob); // Build EIP-7702 transactions let increment_call = Minimal7702Account::incrementCall {}; let tx_alice = build_eip7702_tx( chain_id, 0, - setup.alice().address, + Account::Alice.address(), U256::ZERO, Bytes::from(increment_call.abi_encode()), vec![auth_alice], - setup.alice(), + Account::Alice, ); let tx_bob = build_eip7702_tx( chain_id, 0, - setup.bob().address, + Account::Bob.address(), U256::ZERO, Bytes::from(increment_call.abi_encode()), vec![auth_bob], - setup.bob(), + Account::Bob, ); let tx_hash_alice = alloy_primitives::keccak256(&tx_alice); @@ -296,23 +285,23 @@ async fn test_eip7702_multiple_delegations_same_flashblock() -> Result<()> { #[tokio::test] async fn test_eip7702_pending_receipt() -> Result<()> { let setup = TestSetup::new().await?; - let chain_id = setup.chain_id(); + let chain_id = setup.harness.chain_id(); // Send base flashblock with contract deployment let base_payload = create_base_flashblock(&setup); setup.send_flashblock(base_payload).await?; // Create and send EIP-7702 transaction - let auth = build_authorization(chain_id, setup.account_contract_address, 0, setup.alice()); + let auth = build_authorization(chain_id, setup.account_contract_address, 0, Account::Alice); let increment_call = Minimal7702Account::incrementCall {}; let eip7702_tx = build_eip7702_tx( chain_id, 0, - setup.alice().address, + Account::Alice.address(), U256::ZERO, Bytes::from(increment_call.abi_encode()), vec![auth], - setup.alice(), + Account::Alice, ); let tx_hash = alloy_primitives::keccak256(&eip7702_tx); @@ -335,7 +324,7 @@ async fn test_eip7702_pending_receipt() -> Result<()> { #[tokio::test] async fn test_eip7702_delegation_then_execution() -> Result<()> { let setup = TestSetup::new().await?; - let chain_id = setup.chain_id(); + let chain_id = setup.harness.chain_id(); // Send base flashblock with contract deployment let base_payload = create_base_flashblock(&setup); @@ -346,15 +335,15 @@ async fn test_eip7702_delegation_then_execution() -> Result<()> { let delegation_gas = 30000; let delegation_cumulative = BASE_CUMULATIVE_GAS + delegation_gas; - let auth = build_authorization(chain_id, setup.account_contract_address, 0, setup.alice()); + let auth = build_authorization(chain_id, setup.account_contract_address, 0, Account::Alice); let delegation_tx = build_eip7702_tx( chain_id, 0, - setup.alice().address, + Account::Alice.address(), U256::ZERO, Bytes::new(), // Empty input - just setting up delegation vec![auth], - setup.alice(), + Account::Alice, ); let delegation_hash = alloy_primitives::keccak256(&delegation_tx); @@ -372,10 +361,10 @@ async fn test_eip7702_delegation_then_execution() -> Result<()> { let execution_tx = build_eip1559_tx( chain_id, 1, // incremented nonce - setup.alice().address, + Account::Alice.address(), U256::ZERO, Bytes::from(increment_call.abi_encode()), - setup.alice(), + Account::Alice, ); let execution_hash = alloy_primitives::keccak256(&execution_tx); diff --git a/crates/client/flashblocks/tests/eth_call_erc20.rs b/crates/client/flashblocks/tests/eth_call_erc20.rs index de846fc4..531ddd0b 100644 --- a/crates/client/flashblocks/tests/eth_call_erc20.rs +++ b/crates/client/flashblocks/tests/eth_call_erc20.rs @@ -16,12 +16,13 @@ use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_provider::Provider; use alloy_rpc_types_engine::PayloadId; use alloy_sol_types::{SolConstructor, SolValue}; +use base_client_node::test_utils::{ + Account, L1_BLOCK_INFO_DEPOSIT_TX, MockERC20, TransparentUpgradeableProxy, +}; +use base_flashblocks::test_harness::FlashblocksHarness; use base_flashtypes::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, Flashblock, Metadata, }; -use base_reth_test_utils::{ - FlashblocksHarness, L1_BLOCK_INFO_DEPOSIT_TX, MockERC20, TransparentUpgradeableProxy, -}; use eyre::Result; struct Erc20TestSetup { harness: FlashblocksHarness, @@ -34,7 +35,7 @@ struct Erc20TestSetup { impl Erc20TestSetup { async fn new(with_proxy: bool) -> Result { let harness = FlashblocksHarness::new().await?; - let deployer = &harness.accounts().deployer; + let deployer = Account::Deployer; // Deploy MockERC20 from solmate with constructor args (name, symbol, decimals) let token_constructor = MockERC20::constructorCall { @@ -52,7 +53,7 @@ impl Erc20TestSetup { // Constructor: (implementation, initialOwner, data) let proxy_constructor = TransparentUpgradeableProxy::constructorCall { _logic: token_address, - initialOwner: deployer.address, + initialOwner: deployer.address(), _data: Bytes::new(), }; let proxy_deploy_data = @@ -212,14 +213,13 @@ async fn test_proxy_erc20_deployment() -> Result<()> { async fn test_erc20_mint() -> Result<()> { let setup = Erc20TestSetup::new(false).await?; let provider = setup.harness.provider(); - let accounts = setup.harness.accounts(); // Deploy contracts first setup.send_base_and_deploy().await?; // Check initial balance is zero let token = MockERC20::MockERC20Instance::new(setup.token_address, provider.clone()); - let balance_call = token.balanceOf(accounts.alice.address).into_transaction_request(); + let balance_call = token.balanceOf(Account::Alice.address()).into_transaction_request(); let result = provider.call(balance_call.clone()).block(BlockNumberOrTag::Pending.into()).await?; let initial_balance = U256::abi_decode(&result)?; @@ -228,8 +228,8 @@ async fn test_erc20_mint() -> Result<()> { // Create mint transaction let mint_amount = U256::from(1000u64); let mint_tx_request = - token.mint(accounts.alice.address, mint_amount).into_transaction_request(); - let (mint_tx, _) = accounts.deployer.sign_txn_request(mint_tx_request.nonce(1))?; + token.mint(Account::Alice.address(), mint_amount).into_transaction_request(); + let (mint_tx, _) = Account::Deployer.sign_txn_request(mint_tx_request.nonce(1))?; // Send mint flashblock let mint_payload = setup.create_mint_payload(mint_tx); @@ -248,7 +248,6 @@ async fn test_erc20_mint() -> Result<()> { async fn test_proxy_erc20_mint() -> Result<()> { let setup = Erc20TestSetup::new(true).await?; let provider = setup.harness.provider(); - let accounts = setup.harness.accounts(); // Deploy contracts first setup.send_base_and_deploy().await?; @@ -256,7 +255,8 @@ async fn test_proxy_erc20_mint() -> Result<()> { // Check initial balance is zero through proxy let proxy_address = setup.proxy_address.unwrap(); let token_via_proxy = MockERC20::MockERC20Instance::new(proxy_address, provider.clone()); - let balance_call = token_via_proxy.balanceOf(accounts.alice.address).into_transaction_request(); + let balance_call = + token_via_proxy.balanceOf(Account::Alice.address()).into_transaction_request(); let result = provider.call(balance_call.clone()).block(BlockNumberOrTag::Pending.into()).await?; let initial_balance = U256::abi_decode(&result)?; @@ -265,8 +265,8 @@ async fn test_proxy_erc20_mint() -> Result<()> { // Create mint transaction through proxy let mint_amount = U256::from(5000u64); let mint_tx_request = - token_via_proxy.mint(accounts.alice.address, mint_amount).into_transaction_request(); - let (mint_tx, _) = accounts.deployer.sign_txn_request(mint_tx_request.nonce(2))?; + token_via_proxy.mint(Account::Alice.address(), mint_amount).into_transaction_request(); + let (mint_tx, _) = Account::Deployer.sign_txn_request(mint_tx_request.nonce(2))?; // Send mint flashblock (note: interaction_address returns proxy) let mint_payload = setup.create_mint_payload(mint_tx); diff --git a/crates/client/flashblocks/tests/flashblocks_rpc.rs b/crates/client/flashblocks/tests/flashblocks_rpc.rs index fc8fa418..3b46f047 100644 --- a/crates/client/flashblocks/tests/flashblocks_rpc.rs +++ b/crates/client/flashblocks/tests/flashblocks_rpc.rs @@ -11,10 +11,11 @@ use alloy_rpc_client::RpcClient; use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; +use base_client_node::test_utils::{Account, DoubleCounter, L1_BLOCK_INFO_DEPOSIT_TX}; +use base_flashblocks::test_harness::FlashblocksHarness; use base_flashtypes::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, Flashblock, Metadata, }; -use base_reth_test_utils::{DoubleCounter, FlashblocksHarness, L1_BLOCK_INFO_DEPOSIT_TX}; use eyre::Result; use futures_util::{SinkExt, StreamExt}; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; @@ -173,9 +174,9 @@ impl TestSetup { let harness = FlashblocksHarness::new().await?; let provider = harness.provider(); - let deployer = &harness.accounts().deployer; - let alice = &harness.accounts().alice; - let bob = &harness.accounts().bob; + let deployer = Account::Deployer; + let alice = Account::Alice; + let bob = Account::Bob; // DoubleCounter deployment at nonce 0 let (counter_deployment_tx, counter_address, _) = deployer @@ -193,11 +194,11 @@ impl TestSetup { let (eth_transfer_tx, eth_transfer_hash) = alice .sign_txn_request( OpTransactionRequest::default() - .from(alice.address) + .from(alice.address()) .transaction_type(TransactionType::Eip1559.into()) .gas_limit(100_000) .nonce(0) - .to(bob.address) + .to(bob.address()) .value(U256::from_str("999999999000000000000000").unwrap()) .into(), ) @@ -205,14 +206,14 @@ impl TestSetup { // Log-emitting contracts: // Deploy LogEmitterB at deployer nonce 3 - let log_emitter_b_address = deployer.address.create(3); + let log_emitter_b_address = deployer.address().create(3); let log_emitter_b_bytecode = wrap_in_init_code(LOG_EMITTER_B_RUNTIME); let (log_emitter_b_deployment_tx, _, _) = deployer .create_deployment_tx(log_emitter_b_bytecode, 3) .expect("should be able to sign LogEmitterB deployment txn"); // Deploy LogEmitterA at deployer nonce 4 (knows LogEmitterB's address) - let log_emitter_a_address = deployer.address.create(4); + let log_emitter_a_address = deployer.address().create(4); let log_emitter_a_runtime = log_emitter_a_runtime(log_emitter_b_address); let log_emitter_a_bytecode = wrap_in_init_code(&log_emitter_a_runtime); let (log_emitter_a_deployment_tx, _, _) = deployer @@ -223,7 +224,7 @@ impl TestSetup { let (log_trigger_tx, log_trigger_hash) = deployer .sign_txn_request( OpTransactionRequest::default() - .from(deployer.address) + .from(deployer.address()) .transaction_type(TransactionType::Eip1559.into()) .gas_limit(100_000) .nonce(5) @@ -236,7 +237,7 @@ impl TestSetup { let (balance_transfer_tx, _) = alice .sign_txn_request( OpTransactionRequest::default() - .from(alice.address) + .from(alice.address()) .transaction_type(TransactionType::Eip1559.into()) .gas_limit(21_000) .nonce(1) @@ -471,11 +472,8 @@ async fn test_get_transaction_by_hash_pending() -> Result<()> { .await? .expect("tx2 expected"); assert_eq!(tx2.tx_hash(), setup.txn_details.alice_eth_transfer_hash); - assert_eq!(tx2.from(), setup.harness.accounts().alice.address); - assert_eq!( - tx2.inner.inner.as_eip1559().unwrap().to().unwrap(), - setup.harness.accounts().bob.address - ); + assert_eq!(tx2.from(), Account::Alice.address()); + assert_eq!(tx2.inner.inner.as_eip1559().unwrap().to().unwrap(), Account::Bob.address()); Ok(()) } @@ -508,8 +506,8 @@ async fn test_get_transaction_count() -> Result<()> { let setup = TestSetup::new().await?; let provider = setup.harness.provider(); - let deployer_addr = setup.harness.accounts().deployer.address; - let alice_addr = setup.harness.accounts().alice.address; + let deployer_addr = Account::Deployer.address(); + let alice_addr = Account::Alice.address(); assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).pending().await?, 0); assert_eq!(provider.get_transaction_count(deployer_addr).pending().await?, 0); @@ -532,15 +530,13 @@ async fn test_eth_call() -> Result<()> { let setup = TestSetup::new().await?; let provider = setup.harness.provider(); - let accounts = setup.harness.accounts(); - // Initially, the big spend will succeed because we haven't sent the test payloads yet let big_spend = OpTransactionRequest::default() - .from(accounts.alice.address) + .from(Account::Alice.address()) .transaction_type(0) .gas_limit(200000) .nonce(0) - .to(setup.harness.accounts().bob.address) + .to(Account::Bob.address()) .value(U256::from(9999999999849942300000u128)); let res = provider.call(big_spend.clone()).block(BlockNumberOrTag::Pending.into()).await; @@ -577,11 +573,11 @@ async fn test_eth_estimate_gas() -> Result<()> { // We ensure that eth_estimate_gas will succeed because we are on plain state let send_estimate_gas = OpTransactionRequest::default() - .from(setup.harness.accounts().alice.address) + .from(Account::Alice.address()) .transaction_type(0) .gas_limit(200000) .nonce(0) - .to(setup.harness.accounts().bob.address) + .to(Account::Bob.address()) .value(U256::from(9999999999849942300000u128)) .input(TransactionInput::new(bytes!("0x"))); diff --git a/crates/client/flashblocks/tests/state.rs b/crates/client/flashblocks/tests/state.rs index 8f998125..a0a85b80 100644 --- a/crates/client/flashblocks/tests/state.rs +++ b/crates/client/flashblocks/tests/state.rs @@ -4,18 +4,17 @@ use std::{sync::Arc, time::Duration}; use alloy_consensus::{Receipt, Transaction}; use alloy_eips::{BlockHashOrNumber, Encodable2718}; -use alloy_primitives::{ - Address, B256, BlockNumber, Bytes, U256, hex::FromHex, map::foldhash::HashMap, -}; +use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex::FromHex, map::HashMap}; use alloy_rpc_types_engine::PayloadId; +use base_client_node::test_utils::{ + Account, L1_BLOCK_INFO_DEPOSIT_TX, L1_BLOCK_INFO_DEPOSIT_TX_HASH, LocalNodeProvider, +}; +use base_flashblocks::{ + FlashblocksAPI, FlashblocksState, PendingBlocksAPI, test_harness::FlashblocksHarness, +}; use base_flashtypes::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, Flashblock, Metadata, }; -use base_reth_flashblocks::{FlashblocksAPI, FlashblocksState, PendingBlocksAPI}; -use base_reth_test_utils::{ - FlashblocksHarness, L1_BLOCK_INFO_DEPOSIT_TX, L1_BLOCK_INFO_DEPOSIT_TX_HASH, LocalNodeProvider, - TestAccounts, -}; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; use reth::{ @@ -24,26 +23,17 @@ use reth::{ transaction_pool::test_utils::TransactionBuilder, }; use reth_optimism_primitives::{OpBlock, OpReceipt, OpTransactionSigned}; -use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; +use reth_primitives_traits::{Account as RethAccount, Block as BlockT, RecoveredBlock}; use reth_provider::{ChainSpecProvider, StateProviderFactory}; use tokio::time::sleep; // The amount of time to wait (in milliseconds) after sending a new flashblock or canonical block // so it can be processed by the state processor const SLEEP_TIME: u64 = 10; -#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] -enum User { - Alice, - Bob, - Charlie, -} - struct TestHarness { node: FlashblocksHarness, flashblocks: Arc>, provider: LocalNodeProvider, - user_to_address: HashMap, - user_to_private_key: HashMap, } impl TestHarness { @@ -64,67 +54,43 @@ impl TestHarness { .expect("able to recover block"); flashblocks.on_canonical_block_received(genesis_block); - let accounts: TestAccounts = node.accounts().clone(); - - let mut user_to_address = HashMap::default(); - user_to_address.insert(User::Alice, accounts.alice.address); - user_to_address.insert(User::Bob, accounts.bob.address); - user_to_address.insert(User::Charlie, accounts.charlie.address); - - let mut user_to_private_key = HashMap::default(); - user_to_private_key - .insert(User::Alice, Self::decode_private_key(accounts.alice.private_key)); - user_to_private_key.insert(User::Bob, Self::decode_private_key(accounts.bob.private_key)); - user_to_private_key - .insert(User::Charlie, Self::decode_private_key(accounts.charlie.private_key)); - - Self { node, flashblocks, provider, user_to_address, user_to_private_key } + Self { node, flashblocks, provider } } - fn decode_private_key(key: &str) -> B256 { - B256::from_hex(key).expect("valid hex-encoded key") + fn decode_private_key(account: Account) -> B256 { + B256::from_hex(account.private_key()).expect("valid hex-encoded key") } - fn address(&self, u: User) -> Address { - assert!(self.user_to_address.contains_key(&u)); - self.user_to_address[&u] - } - - fn signer(&self, u: User) -> B256 { - assert!(self.user_to_private_key.contains_key(&u)); - self.user_to_private_key[&u] - } - - fn canonical_account(&self, u: User) -> Account { + fn canonical_account(&self, account: Account) -> RethAccount { self.provider - .basic_account(&self.address(u)) + .basic_account(&account.address()) .expect("can lookup account state") .expect("should be existing account state") } - fn canonical_balance(&self, u: User) -> U256 { - self.canonical_account(u).balance + fn canonical_balance(&self, account: Account) -> U256 { + self.canonical_account(account).balance } - fn expected_pending_balance(&self, u: User, delta: u128) -> U256 { - self.canonical_balance(u) + U256::from(delta) + fn expected_pending_balance(&self, account: Account, delta: u128) -> U256 { + self.canonical_balance(account) + U256::from(delta) } - fn account_state(&self, u: User) -> Account { - let basic_account = self.canonical_account(u); + fn account_state(&self, account: Account) -> RethAccount { + let basic_account = self.canonical_account(account); let nonce = self .flashblocks .get_pending_blocks() - .get_transaction_count(self.address(u)) + .get_transaction_count(account.address()) .to::(); let balance = self .flashblocks .get_pending_blocks() - .get_balance(self.address(u)) + .get_balance(account.address()) .unwrap_or(basic_account.balance); - Account { + RethAccount { nonce: nonce + basic_account.nonce, balance, bytecode_hash: basic_account.bytecode_hash, @@ -133,14 +99,14 @@ impl TestHarness { fn build_transaction_to_send_eth( &self, - from: User, - to: User, + from: Account, + to: Account, amount: u128, ) -> OpTransactionSigned { let txn = TransactionBuilder::default() - .signer(self.signer(from)) + .signer(Self::decode_private_key(from)) .chain_id(self.provider.chain_spec().chain_id()) - .to(self.address(to)) + .to(to.address()) .nonce(self.account_state(from).nonce) .value(amount) .gas_limit(21_000) @@ -156,15 +122,15 @@ impl TestHarness { fn build_transaction_to_send_eth_with_nonce( &self, - from: User, - to: User, + from: Account, + to: Account, amount: u128, nonce: u64, ) -> OpTransactionSigned { let txn = TransactionBuilder::default() - .signer(self.signer(from)) + .signer(Self::decode_private_key(from)) .chain_id(self.provider.chain_spec().chain_id()) - .to(self.address(to)) + .to(to.address()) .nonce(nonce) .value(amount) .gas_limit(21_000) @@ -351,14 +317,14 @@ async fn test_state_overrides_persisted_across_flashblocks() { .get_pending_blocks() .get_state_overrides() .unwrap() - .contains_key(&test.address(User::Alice)) + .contains_key(&Account::Alice.address()) ); test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(), @@ -376,14 +342,14 @@ async fn test_state_overrides_persisted_across_flashblocks() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) + test.expected_pending_balance(Account::Bob, 100_000) ); test.send_flashblock(FlashblockBuilder::new(&test, 2).build()).await; @@ -394,14 +360,14 @@ async fn test_state_overrides_persisted_across_flashblocks() { .get_state_overrides() .expect("should be set from txn execution in flashblock index 1"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) + test.expected_pending_balance(Account::Bob, 100_000) ); } @@ -429,14 +395,14 @@ async fn test_state_overrides_persisted_across_blocks() { .get_pending_blocks() .get_state_overrides() .unwrap() - .contains_key(&test.address(User::Alice)) + .contains_key(&Account::Alice.address()) ); test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(), @@ -454,14 +420,14 @@ async fn test_state_overrides_persisted_across_blocks() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) + test.expected_pending_balance(Account::Bob, 100_000) ); test.send_flashblock( @@ -496,15 +462,15 @@ async fn test_state_overrides_persisted_across_blocks() { .get_pending_blocks() .get_state_overrides() .unwrap() - .contains_key(&test.address(User::Alice)) + .contains_key(&Account::Alice.address()) ); test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_canonical_block_number(initial_block_number) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(), @@ -517,14 +483,14 @@ async fn test_state_overrides_persisted_across_blocks() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 200_000) + test.expected_pending_balance(Account::Bob, 200_000) ); } @@ -549,14 +515,14 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { .get_pending_blocks() .get_state_overrides() .unwrap() - .contains_key(&test.address(User::Alice)) + .contains_key(&Account::Alice.address()) ); test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(), @@ -573,14 +539,14 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) + test.expected_pending_balance(Account::Bob, 100_000) ); test.send_flashblock(FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build()) @@ -589,8 +555,8 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { FlashblockBuilder::new(&test, 1) .with_canonical_block_number(1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(), @@ -607,19 +573,19 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 200_000) + test.expected_pending_balance(Account::Bob, 200_000) ); test.new_canonical_block(vec![test.build_transaction_to_send_eth_with_nonce( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100, 0, )]) @@ -636,14 +602,14 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { .get_state_overrides() .expect("should be set from txn execution"); - assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert!(overrides.get(&Account::Alice.address()).is_some()); assert_eq!( overrides - .get(&test.address(User::Bob)) + .get(&Account::Bob.address()) .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) + test.expected_pending_balance(Account::Bob, 100_000) ); } @@ -660,8 +626,8 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100, )]) .build(), @@ -669,25 +635,25 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { .await; let pending_nonce = - test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce + test.provider.basic_account(&Account::Alice.address()).unwrap().unwrap().nonce + test .flashblocks .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) + .get_transaction_count(Account::Alice.address()) .to::(); assert_eq!(pending_nonce, 1); test.new_canonical_block_without_processing(vec![ - test.build_transaction_to_send_eth_with_nonce(User::Alice, User::Bob, 100, 0), + test.build_transaction_to_send_eth_with_nonce(Account::Alice, Account::Bob, 100, 0), ]) .await; let pending_nonce = - test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce + test.provider.basic_account(&Account::Alice.address()).unwrap().unwrap().nonce + test .flashblocks .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) + .get_transaction_count(Account::Alice.address()) .to::(); // This is 2, because canon block has reached the underlying chain @@ -702,12 +668,12 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { let canon_block = test.flashblocks.get_pending_blocks().get_canonical_block_number(); let canon_state_provider = test.provider.state_by_block_number_or_tag(canon_block).unwrap(); let canon_nonce = - canon_state_provider.account_nonce(&test.address(User::Alice)).unwrap().unwrap(); + canon_state_provider.account_nonce(&Account::Alice.address()).unwrap().unwrap(); let pending_nonce = canon_nonce + test .flashblocks .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) + .get_transaction_count(Account::Alice.address()) .to::(); assert_eq!(pending_nonce, 1); } @@ -798,8 +764,8 @@ async fn test_non_sequential_payload_clears_pending_state() { test.send_flashblock( FlashblockBuilder::new(&test, 3) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100, )]) .build(), @@ -817,8 +783,8 @@ async fn test_duplicate_flashblock_ignored() { let fb = FlashblockBuilder::new(&test, 1) .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, + Account::Alice, + Account::Bob, 100_000, )]) .build(); @@ -841,8 +807,12 @@ async fn test_progress_canonical_blocks_without_flashblocks() { assert_eq!(genesis_block.transaction_count(), 0); assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); - test.new_canonical_block(vec![test.build_transaction_to_send_eth(User::Alice, User::Bob, 100)]) - .await; + test.new_canonical_block(vec![test.build_transaction_to_send_eth( + Account::Alice, + Account::Bob, + 100, + )]) + .await; let block_one = test.node.latest_block(); assert_eq!(block_one.number, 1); @@ -850,8 +820,8 @@ async fn test_progress_canonical_blocks_without_flashblocks() { assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); test.new_canonical_block(vec![ - test.build_transaction_to_send_eth(User::Bob, User::Charlie, 100), - test.build_transaction_to_send_eth(User::Charlie, User::Alice, 1000), + test.build_transaction_to_send_eth(Account::Bob, Account::Charlie, 100), + test.build_transaction_to_send_eth(Account::Charlie, Account::Alice, 1000), ]) .await; diff --git a/crates/client/metering/Cargo.toml b/crates/client/metering/Cargo.toml index 0c975fca..c45ee302 100644 --- a/crates/client/metering/Cargo.toml +++ b/crates/client/metering/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "base-reth-metering" +name = "base-metering" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -14,7 +14,7 @@ workspace = true [dependencies] # workspace base-bundles.workspace = true -base-primitives.workspace = true +base-client-node.workspace = true # reth reth.workspace = true @@ -39,11 +39,10 @@ eyre.workspace = true serde.workspace = true [dev-dependencies] -base-reth-test-utils.workspace = true +base-client-node = { workspace = true, features = ["test-utils"] } reth-db = { workspace = true, features = ["test-utils"] } reth-db-common.workspace = true reth-optimism-node.workspace = true -reth-testing-utils.workspace = true reth-transaction-pool = { workspace = true, features = ["test-utils"] } alloy-genesis.workspace = true alloy-rpc-client.workspace = true diff --git a/crates/client/metering/README.md b/crates/client/metering/README.md index e79d4da8..3d1c679a 100644 --- a/crates/client/metering/README.md +++ b/crates/client/metering/README.md @@ -1,4 +1,4 @@ -# base-reth-metering +# base-metering Metering RPC for Base node. Provides RPC methods for measuring transaction and block execution timing. diff --git a/crates/client/metering/src/extension.rs b/crates/client/metering/src/extension.rs index 578523d1..c089954c 100644 --- a/crates/client/metering/src/extension.rs +++ b/crates/client/metering/src/extension.rs @@ -1,26 +1,26 @@ -//! Contains the [`MeteringRpcExtension`] which wires up the metering RPC surface +//! Contains the [`MeteringExtension`] which wires up the metering RPC surface //! on the Base node builder. -use base_primitives::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder}; +use base_client_node::{BaseNodeExtension, FromExtensionConfig, OpBuilder}; use tracing::info; use crate::{MeteringApiImpl, MeteringApiServer}; /// Helper struct that wires the metering RPC into the node builder. #[derive(Debug, Clone, Copy)] -pub struct MeteringRpcExtension { +pub struct MeteringExtension { /// Whether metering is enabled. pub enabled: bool, } -impl MeteringRpcExtension { - /// Creates a new metering RPC extension. +impl MeteringExtension { + /// Creates a new metering extension. pub const fn new(enabled: bool) -> Self { Self { enabled } } } -impl BaseNodeExtension for MeteringRpcExtension { +impl BaseNodeExtension for MeteringExtension { /// Applies the extension to the supplied builder. fn apply(self: Box, builder: OpBuilder) -> OpBuilder { if !self.enabled { @@ -36,16 +36,10 @@ impl BaseNodeExtension for MeteringRpcExtension { } } -/// Configuration trait for [`MeteringRpcExtension`]. -/// -/// Types implementing this trait can be used to construct a [`MeteringRpcExtension`]. -pub trait MeteringRpcConfig { - /// Returns whether metering is enabled. - fn metering_enabled(&self) -> bool; -} +impl FromExtensionConfig for MeteringExtension { + type Config = bool; -impl ConfigurableBaseNodeExtension for MeteringRpcExtension { - fn build(config: &C) -> eyre::Result { - Ok(Self::new(config.metering_enabled())) + fn from_config(enabled: Self::Config) -> Self { + Self::new(enabled) } } diff --git a/crates/client/metering/src/lib.rs b/crates/client/metering/src/lib.rs index 16a09587..41883a8d 100644 --- a/crates/client/metering/src/lib.rs +++ b/crates/client/metering/src/lib.rs @@ -7,7 +7,7 @@ mod block; pub use block::meter_block; mod extension; -pub use extension::{MeteringRpcConfig, MeteringRpcExtension}; +pub use extension::MeteringExtension; mod meter; pub use meter::meter_bundle; diff --git a/crates/client/metering/tests/meter.rs b/crates/client/metering/tests/meter.rs index 371be97a..3ab38191 100644 --- a/crates/client/metering/tests/meter.rs +++ b/crates/client/metering/tests/meter.rs @@ -1,106 +1,24 @@ //! Integration tests covering the Metering logic surface area. -use std::sync::Arc; - -use alloy_consensus::crypto::secp256k1::public_key_to_address; use alloy_eips::Encodable2718; -use alloy_genesis::GenesisAccount; -use alloy_primitives::{Address, B256, Bytes, U256, keccak256}; +use alloy_primitives::{Address, Bytes, U256, keccak256}; use base_bundles::{Bundle, ParsedBundle}; -use base_reth_metering::meter_bundle; -use base_reth_test_utils::create_provider_factory; +use base_client_node::test_utils::{Account, TestHarness, load_chain_spec}; +use base_metering::{MeteringExtension, meter_bundle}; use eyre::Context; use op_alloy_consensus::OpTxEnvelope; -use rand::{SeedableRng, rngs::StdRng}; -use reth::{api::NodeTypesWithDBAdapter, chainspec::EthChainSpec}; -use reth_db::{DatabaseEnv, test_utils::TempDatabase}; -use reth_optimism_chainspec::{BASE_MAINNET, OpChainSpec, OpChainSpecBuilder}; -use reth_optimism_node::OpNode; +use reth::chainspec::EthChainSpec; use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives_traits::SealedHeader; -use reth_provider::{HeaderProvider, StateProviderFactory, providers::BlockchainProvider}; -use reth_testing_utils::generators::generate_keys; +use reth_provider::{HeaderProvider, StateProviderFactory}; use reth_transaction_pool::test_utils::TransactionBuilder; -type NodeTypes = NodeTypesWithDBAdapter>>; - -#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] -enum User { - Alice, - Bob, -} - -#[derive(Debug, Clone)] -struct TestHarness { - provider: BlockchainProvider, - header: SealedHeader, - chain_spec: Arc, - user_to_address: std::collections::HashMap, - user_to_private_key: std::collections::HashMap, -} - -impl TestHarness { - fn address(&self, u: User) -> Address { - self.user_to_address[&u] - } - - fn signer(&self, u: User) -> B256 { - self.user_to_private_key[&u] - } -} - -fn create_chain_spec( - seed: u64, -) -> ( - Arc, - std::collections::HashMap, - std::collections::HashMap, -) { - let keys = generate_keys(&mut StdRng::seed_from_u64(seed), 2); - - let mut addresses = std::collections::HashMap::new(); - let mut private_keys = std::collections::HashMap::new(); - - let alice_key = keys[0]; - let alice_address = public_key_to_address(alice_key.public_key()); - let alice_secret = B256::from(alice_key.secret_bytes()); - addresses.insert(User::Alice, alice_address); - private_keys.insert(User::Alice, alice_secret); - - let bob_key = keys[1]; - let bob_address = public_key_to_address(bob_key.public_key()); - let bob_secret = B256::from(bob_key.secret_bytes()); - addresses.insert(User::Bob, bob_address); - private_keys.insert(User::Bob, bob_secret); - - let genesis = BASE_MAINNET - .genesis - .clone() - .extend_accounts(vec![ - (alice_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), - (bob_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), - ]) - .with_gas_limit(100_000_000); - - let spec = - Arc::new(OpChainSpecBuilder::base_mainnet().genesis(genesis).isthmus_activated().build()); - - (spec, addresses, private_keys) -} - -fn setup_harness() -> eyre::Result { - let (chain_spec, user_to_address, user_to_private_key) = create_chain_spec(1337); - let factory = create_provider_factory::(chain_spec.clone()); - - reth_db_common::init::init_genesis(&factory).context("initializing genesis state")?; - - let provider = BlockchainProvider::new(factory.clone()).context("creating provider")?; - let header = provider - .sealed_header(0) - .context("fetching genesis header")? - .expect("genesis header exists"); - - Ok(TestHarness { provider, header, chain_spec, user_to_address, user_to_private_key }) +async fn setup() -> eyre::Result { + let chain_spec = load_chain_spec(); + TestHarness::builder() + .with_chain_spec(chain_spec) + .with_ext::(true) + .build() + .await } fn envelope_from_signed(tx: &OpTransactionSigned) -> eyre::Result { @@ -125,19 +43,21 @@ fn create_parsed_bundle(envelopes: Vec) -> eyre::Result eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_bundle_empty_transactions() -> eyre::Result<()> { + let harness = setup().await?; + + let provider = harness.blockchain_provider(); + let header = provider.sealed_header(0)?.expect("genesis header exists"); + let chain_spec = harness.chain_spec(); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; + let state_provider = + provider.state_by_block_hash(header.hash()).context("getting state provider")?; let parsed_bundle = create_parsed_bundle(Vec::new())?; let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec.clone(), parsed_bundle, &harness.header)?; + meter_bundle(state_provider, chain_spec, parsed_bundle, &header)?; assert!(results.is_empty()); assert_eq!(total_gas_used, 0); @@ -149,14 +69,18 @@ fn meter_bundle_empty_transactions() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_bundle_single_transaction() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_bundle_single_transaction() -> eyre::Result<()> { + let harness = setup().await?; + + let provider = harness.blockchain_provider(); + let header = provider.sealed_header(0)?.expect("genesis header exists"); + let chain_spec = harness.chain_spec(); let to = Address::random(); let signed_tx = TransactionBuilder::default() - .signer(harness.signer(User::Alice)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Alice.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to) .value(1_000) @@ -171,21 +95,19 @@ fn meter_bundle_single_transaction() -> eyre::Result<()> { let envelope = envelope_from_signed(&tx)?; let tx_hash = envelope.tx_hash(); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; + let state_provider = + provider.state_by_block_hash(header.hash()).context("getting state provider")?; let parsed_bundle = create_parsed_bundle(vec![envelope.clone()])?; let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec.clone(), parsed_bundle, &harness.header)?; + meter_bundle(state_provider, chain_spec, parsed_bundle, &header)?; assert_eq!(results.len(), 1); let result = &results[0]; assert!(total_execution_time > 0); - assert_eq!(result.from_address, harness.address(User::Alice)); + assert_eq!(result.from_address, Account::Alice.address()); assert_eq!(result.to_address, Some(to)); assert_eq!(result.tx_hash, tx_hash); assert_eq!(result.gas_price, U256::from(10)); @@ -204,17 +126,21 @@ fn meter_bundle_single_transaction() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_bundle_multiple_transactions() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_bundle_multiple_transactions() -> eyre::Result<()> { + let harness = setup().await?; + + let provider = harness.blockchain_provider(); + let header = provider.sealed_header(0)?.expect("genesis header exists"); + let chain_spec = harness.chain_spec(); let to_1 = Address::random(); let to_2 = Address::random(); // Create first transaction let signed_tx_1 = TransactionBuilder::default() - .signer(harness.signer(User::Alice)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Alice.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to_1) .value(1_000) @@ -229,8 +155,8 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> { // Create second transaction let signed_tx_2 = TransactionBuilder::default() - .signer(harness.signer(User::Bob)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Bob.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to_2) .value(2_000) @@ -248,22 +174,20 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> { let tx_hash_1 = envelope_1.tx_hash(); let tx_hash_2 = envelope_2.tx_hash(); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; + let state_provider = + provider.state_by_block_hash(header.hash()).context("getting state provider")?; let parsed_bundle = create_parsed_bundle(vec![envelope_1.clone(), envelope_2.clone()])?; let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec.clone(), parsed_bundle, &harness.header)?; + meter_bundle(state_provider, chain_spec, parsed_bundle, &header)?; assert_eq!(results.len(), 2); assert!(total_execution_time > 0); // Check first transaction let result_1 = &results[0]; - assert_eq!(result_1.from_address, harness.address(User::Alice)); + assert_eq!(result_1.from_address, Account::Alice.address()); assert_eq!(result_1.to_address, Some(to_1)); assert_eq!(result_1.tx_hash, tx_hash_1); assert_eq!(result_1.gas_price, U256::from(10)); @@ -272,7 +196,7 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> { // Check second transaction let result_2 = &results[1]; - assert_eq!(result_2.from_address, harness.address(User::Bob)); + assert_eq!(result_2.from_address, Account::Bob.address()); assert_eq!(result_2.to_address, Some(to_2)); assert_eq!(result_2.tx_hash, tx_hash_2); assert_eq!(result_2.gas_price, U256::from(15)); diff --git a/crates/client/metering/tests/meter_block.rs b/crates/client/metering/tests/meter_block.rs index 42af07b3..704e3b77 100644 --- a/crates/client/metering/tests/meter_block.rs +++ b/crates/client/metering/tests/meter_block.rs @@ -1,108 +1,35 @@ //! Integration tests for block metering functionality. -use std::sync::Arc; - -use alloy_consensus::{BlockHeader, Header, crypto::secp256k1::public_key_to_address}; -use alloy_genesis::GenesisAccount; -use alloy_primitives::{Address, B256, U256}; -use base_reth_metering::meter_block; -use base_reth_test_utils::create_provider_factory; -use eyre::Context; -use rand::{SeedableRng, rngs::StdRng}; -use reth::{api::NodeTypesWithDBAdapter, chainspec::EthChainSpec}; -use reth_db::{DatabaseEnv, test_utils::TempDatabase}; -use reth_optimism_chainspec::{BASE_MAINNET, OpChainSpec, OpChainSpecBuilder}; -use reth_optimism_node::OpNode; +use alloy_consensus::{BlockHeader, Header}; +use alloy_primitives::{Address, B256}; +use base_client_node::test_utils::{Account, TestHarness, load_chain_spec}; +use base_metering::{MeteringExtension, meter_block}; +use reth::chainspec::EthChainSpec; use reth_optimism_primitives::{OpBlock, OpBlockBody, OpTransactionSigned}; use reth_primitives_traits::Block as BlockT; -use reth_provider::{HeaderProvider, providers::BlockchainProvider}; -use reth_testing_utils::generators::generate_keys; +use reth_provider::HeaderProvider; use reth_transaction_pool::test_utils::TransactionBuilder; -type NodeTypes = NodeTypesWithDBAdapter>>; - -#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] -enum User { - Alice, - Bob, -} - -#[derive(Debug, Clone)] -struct TestHarness { - provider: BlockchainProvider, - genesis_header_hash: B256, - genesis_header_number: u64, - genesis_header_timestamp: u64, - chain_spec: Arc, - user_to_private_key: std::collections::HashMap, -} - -impl TestHarness { - fn signer(&self, u: User) -> B256 { - self.user_to_private_key[&u] - } -} - -fn create_chain_spec(seed: u64) -> (Arc, std::collections::HashMap) { - let keys = generate_keys(&mut StdRng::seed_from_u64(seed), 2); - - let mut private_keys = std::collections::HashMap::new(); - - let alice_key = keys[0]; - let alice_address = public_key_to_address(alice_key.public_key()); - let alice_secret = B256::from(alice_key.secret_bytes()); - private_keys.insert(User::Alice, alice_secret); - - let bob_key = keys[1]; - let bob_address = public_key_to_address(bob_key.public_key()); - let bob_secret = B256::from(bob_key.secret_bytes()); - private_keys.insert(User::Bob, bob_secret); - - let genesis = BASE_MAINNET - .genesis - .clone() - .extend_accounts(vec![ - (alice_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), - (bob_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), - ]) - .with_gas_limit(100_000_000); - - let spec = - Arc::new(OpChainSpecBuilder::base_mainnet().genesis(genesis).isthmus_activated().build()); - - (spec, private_keys) -} - -fn setup_harness() -> eyre::Result { - let (chain_spec, user_to_private_key) = create_chain_spec(1337); - let factory = create_provider_factory::(chain_spec.clone()); - - reth_db_common::init::init_genesis(&factory).context("initializing genesis state")?; - - let provider = BlockchainProvider::new(factory.clone()).context("creating provider")?; - let header = provider - .sealed_header(0) - .context("fetching genesis header")? - .expect("genesis header exists"); - - Ok(TestHarness { - provider, - genesis_header_hash: header.hash(), - genesis_header_number: header.number(), - genesis_header_timestamp: header.timestamp(), - chain_spec, - user_to_private_key, - }) +async fn setup() -> eyre::Result { + let chain_spec = load_chain_spec(); + TestHarness::builder() + .with_chain_spec(chain_spec) + .with_ext::(true) + .build() + .await } fn create_block_with_transactions( harness: &TestHarness, transactions: Vec, -) -> OpBlock { +) -> eyre::Result { + let provider = harness.blockchain_provider(); + let genesis = provider.sealed_header(0)?.expect("genesis header exists"); + let header = Header { - parent_hash: harness.genesis_header_hash, - number: harness.genesis_header_number + 1, - timestamp: harness.genesis_header_timestamp + 2, + parent_hash: genesis.hash(), + number: genesis.number() + 1, + timestamp: genesis.timestamp() + 2, gas_limit: 30_000_000, beneficiary: Address::random(), base_fee_per_gas: Some(1), @@ -113,16 +40,16 @@ fn create_block_with_transactions( let body = OpBlockBody { transactions, ommers: vec![], withdrawals: None }; - OpBlock::new(header, body) + Ok(OpBlock::new(header, body)) } -#[test] -fn meter_block_empty_transactions() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_block_empty_transactions() -> eyre::Result<()> { + let harness = setup().await?; - let block = create_block_with_transactions(&harness, vec![]); + let block = create_block_with_transactions(&harness, vec![])?; - let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; + let response = meter_block(harness.blockchain_provider(), harness.chain_spec(), &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -138,14 +65,15 @@ fn meter_block_empty_transactions() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_block_single_transaction() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_block_single_transaction() -> eyre::Result<()> { + let harness = setup().await?; + let chain_spec = harness.chain_spec(); let to = Address::random(); let signed_tx = TransactionBuilder::default() - .signer(harness.signer(User::Alice)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Alice.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to) .value(1_000) @@ -158,9 +86,9 @@ fn meter_block_single_transaction() -> eyre::Result<()> { OpTransactionSigned::Eip1559(signed_tx.as_eip1559().expect("eip1559 transaction").clone()); let tx_hash = tx.tx_hash(); - let block = create_block_with_transactions(&harness, vec![tx]); + let block = create_block_with_transactions(&harness, vec![tx])?; - let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; + let response = meter_block(harness.blockchain_provider(), chain_spec, &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -182,17 +110,18 @@ fn meter_block_single_transaction() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_block_multiple_transactions() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_block_multiple_transactions() -> eyre::Result<()> { + let harness = setup().await?; + let chain_spec = harness.chain_spec(); let to_1 = Address::random(); let to_2 = Address::random(); // Create first transaction from Alice let signed_tx_1 = TransactionBuilder::default() - .signer(harness.signer(User::Alice)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Alice.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to_1) .value(1_000) @@ -208,8 +137,8 @@ fn meter_block_multiple_transactions() -> eyre::Result<()> { // Create second transaction from Bob let signed_tx_2 = TransactionBuilder::default() - .signer(harness.signer(User::Bob)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Bob.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(to_2) .value(2_000) @@ -223,9 +152,9 @@ fn meter_block_multiple_transactions() -> eyre::Result<()> { ); let tx_hash_2 = tx_2.tx_hash(); - let block = create_block_with_transactions(&harness, vec![tx_1, tx_2]); + let block = create_block_with_transactions(&harness, vec![tx_1, tx_2])?; - let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; + let response = meter_block(harness.blockchain_provider(), chain_spec, &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -262,14 +191,15 @@ fn meter_block_multiple_transactions() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_block_timing_consistency() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_block_timing_consistency() -> eyre::Result<()> { + let harness = setup().await?; + let chain_spec = harness.chain_spec(); // Create a block with one transaction let signed_tx = TransactionBuilder::default() - .signer(harness.signer(User::Alice)) - .chain_id(harness.chain_spec.chain_id()) + .signer(Account::Alice.signer_b256()) + .chain_id(chain_spec.chain_id()) .nonce(0) .to(Address::random()) .value(1_000) @@ -281,9 +211,9 @@ fn meter_block_timing_consistency() -> eyre::Result<()> { let tx = OpTransactionSigned::Eip1559(signed_tx.as_eip1559().expect("eip1559 transaction").clone()); - let block = create_block_with_transactions(&harness, vec![tx]); + let block = create_block_with_transactions(&harness, vec![tx])?; - let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; + let response = meter_block(harness.blockchain_provider(), chain_spec, &block)?; // Verify timing invariants assert!(response.signer_recovery_time_us > 0, "signer recovery time must be positive"); @@ -302,16 +232,19 @@ fn meter_block_timing_consistency() -> eyre::Result<()> { // Error Path Tests // ============================================================================ -#[test] -fn meter_block_parent_header_not_found() -> eyre::Result<()> { - let harness = setup_harness()?; +#[tokio::test] +async fn meter_block_parent_header_not_found() -> eyre::Result<()> { + let harness = setup().await?; + let chain_spec = harness.chain_spec(); + let provider = harness.blockchain_provider(); + let genesis = provider.sealed_header(0)?.expect("genesis header exists"); // Create a block that references a non-existent parent let fake_parent_hash = B256::random(); let header = Header { parent_hash: fake_parent_hash, // This parent doesn't exist number: 999, - timestamp: harness.genesis_header_timestamp + 2, + timestamp: genesis.timestamp() + 2, gas_limit: 30_000_000, beneficiary: Address::random(), base_fee_per_gas: Some(1), @@ -322,7 +255,7 @@ fn meter_block_parent_header_not_found() -> eyre::Result<()> { let body = OpBlockBody { transactions: vec![], ommers: vec![], withdrawals: None }; let block = OpBlock::new(header, body); - let result = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block); + let result = meter_block(provider, chain_spec, &block); assert!(result.is_err(), "should fail when parent header is not found"); let err = result.unwrap_err(); @@ -336,16 +269,17 @@ fn meter_block_parent_header_not_found() -> eyre::Result<()> { Ok(()) } -#[test] -fn meter_block_invalid_transaction_signature() -> eyre::Result<()> { +#[tokio::test] +async fn meter_block_invalid_transaction_signature() -> eyre::Result<()> { use alloy_consensus::TxEip1559; use alloy_primitives::Signature; - let harness = setup_harness()?; + let harness = setup().await?; + let chain_spec = harness.chain_spec(); // Create a transaction with an invalid signature let tx = TxEip1559 { - chain_id: harness.chain_spec.chain_id(), + chain_id: chain_spec.chain_id(), nonce: 0, gas_limit: 21_000, max_fee_per_gas: 10, @@ -363,9 +297,9 @@ fn meter_block_invalid_transaction_signature() -> eyre::Result<()> { let signed_tx = alloy_consensus::Signed::new_unchecked(tx, invalid_signature, B256::random()); let op_tx = OpTransactionSigned::Eip1559(signed_tx); - let block = create_block_with_transactions(&harness, vec![op_tx]); + let block = create_block_with_transactions(&harness, vec![op_tx])?; - let result = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block); + let result = meter_block(harness.blockchain_provider(), chain_spec, &block); assert!(result.is_err(), "should fail when transaction has invalid signature"); let err = result.unwrap_err(); diff --git a/crates/client/metering/tests/meter_rpc.rs b/crates/client/metering/tests/meter_rpc.rs index 5ffe598c..3fc528eb 100644 --- a/crates/client/metering/tests/meter_rpc.rs +++ b/crates/client/metering/tests/meter_rpc.rs @@ -1,34 +1,16 @@ //! Integration tests covering the Metering RPC surface area. -use std::{any::Any, net::SocketAddr, sync::Arc}; - use alloy_eips::Encodable2718; -use alloy_primitives::{Bytes, U256, address, b256, bytes}; +use alloy_primitives::{Bytes, U256, address, bytes}; use alloy_rpc_client::RpcClient; use base_bundles::{Bundle, MeterBundleResponse}; -use base_reth_metering::{MeteringApiImpl, MeteringApiServer}; -use base_reth_test_utils::{init_silenced_tracing, load_genesis}; +use base_client_node::test_utils::{Account, TestHarness}; +use base_metering::MeteringExtension; use op_alloy_consensus::OpTxEnvelope; -use reth::{ - args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, - builder::{Node, NodeBuilder, NodeConfig, NodeHandle}, - chainspec::Chain, - core::exit::NodeExitFuture, - tasks::TaskManager, -}; -use reth_optimism_chainspec::OpChainSpecBuilder; -use reth_optimism_node::{OpNode, args::RollupArgs}; use reth_optimism_primitives::OpTransactionSigned; -use reth_provider::providers::BlockchainProvider; use reth_transaction_pool::test_utils::TransactionBuilder; -struct NodeContext { - http_api_addr: SocketAddr, - _node_exit_future: NodeExitFuture, - _node: Box, -} - -// Helper function to create a Bundle with default fields +/// Helper function to create a Bundle with default fields. fn create_bundle(txs: Vec, block_number: u64, min_timestamp: Option) -> Bundle { Bundle { txs, @@ -43,66 +25,17 @@ fn create_bundle(txs: Vec, block_number: u64, min_timestamp: Option) } } -impl NodeContext { - async fn rpc_client(&self) -> eyre::Result { - let url = format!("http://{}", self.http_api_addr); - let client = RpcClient::new_http(url.parse()?); - Ok(client) - } -} - -async fn setup_node() -> eyre::Result { - init_silenced_tracing(); - let tasks = TaskManager::current(); - let exec = tasks.executor(); - const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; - - let genesis = load_genesis(); - let chain_spec = Arc::new( - OpChainSpecBuilder::base_mainnet() - .genesis(genesis) - .ecotone_activated() - .chain(Chain::from(BASE_SEPOLIA_CHAIN_ID)) - .build(), - ); +/// Set up a test harness with the metering extension and return an RPC client. +async fn setup() -> eyre::Result<(TestHarness, RpcClient)> { + let harness = TestHarness::builder().with_ext::(true).build().await?; - let network_config = NetworkArgs { - discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, - ..NetworkArgs::default() - }; - - let node_config = NodeConfig::new(chain_spec.clone()) - .with_network(network_config.clone()) - .with_rpc(RpcServerArgs::default().with_unused_ports().with_http()) - .with_unused_ports(); - - let node = OpNode::new(RollupArgs::default()); - - let NodeHandle { node, node_exit_future } = NodeBuilder::new(node_config.clone()) - .testing_node(exec.clone()) - .with_types_and_provider::>() - .with_components(node.components_builder()) - .with_add_ons(node.add_ons()) - .extend_rpc_modules(move |ctx| { - let metering_api = MeteringApiImpl::new(ctx.provider().clone()); - ctx.modules.merge_configured(metering_api.into_rpc())?; - Ok(()) - }) - .launch() - .await?; - - let http_api_addr = node - .rpc_server_handle() - .http_local_addr() - .ok_or_else(|| eyre::eyre!("Failed to get http api address"))?; - - Ok(NodeContext { http_api_addr, _node_exit_future: node_exit_future, _node: Box::new(node) }) + let client = harness.rpc_client()?; + Ok((harness, client)) } #[tokio::test] async fn test_meter_bundle_empty() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; let bundle = create_bundle(vec![], 0, None); @@ -118,19 +51,15 @@ async fn test_meter_bundle_empty() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_single_transaction() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (harness, client) = setup().await?; - // Use a funded account from genesis.json - // Account: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - // Private key from common test accounts (Hardhat account #0) - let sender_address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); - let sender_secret = b256!("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); + let sender_address = Account::Alice.address(); + let sender_secret = Account::Alice.signer_b256(); // Build a transaction let tx = TransactionBuilder::default() .signer(sender_secret) - .chain_id(84532) + .chain_id(harness.chain_id()) .nonce(0) .to(address!("0x1111111111111111111111111111111111111111")) .value(1000) @@ -166,17 +95,14 @@ async fn test_meter_bundle_single_transaction() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_multiple_transactions() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (harness, client) = setup().await?; - // Use funded accounts from genesis.json - // Hardhat account #0 and #1 - let address1 = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); - let secret1 = b256!("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); + let address1 = Account::Alice.address(); + let secret1 = Account::Alice.signer_b256(); let tx1_inner = TransactionBuilder::default() .signer(secret1) - .chain_id(84532) + .chain_id(harness.chain_id()) .nonce(0) .to(address!("0x1111111111111111111111111111111111111111")) .value(1000) @@ -191,12 +117,12 @@ async fn test_meter_bundle_multiple_transactions() -> eyre::Result<()> { let tx1_bytes = Bytes::from(tx1_envelope.encoded_2718()); // Second transaction from second account - let address2 = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - let secret2 = b256!("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"); + let address2 = Account::Bob.address(); + let secret2 = Account::Bob.signer_b256(); let tx2_inner = TransactionBuilder::default() .signer(secret2) - .chain_id(84532) + .chain_id(harness.chain_id()) .nonce(0) .to(address!("0x2222222222222222222222222222222222222222")) .value(2000) @@ -235,8 +161,7 @@ async fn test_meter_bundle_multiple_transactions() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_invalid_transaction() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; let bundle = create_bundle( vec![bytes!("0xdeadbeef")], // Invalid transaction data @@ -254,8 +179,7 @@ async fn test_meter_bundle_invalid_transaction() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_uses_latest_block() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; // Metering always uses the latest block state, regardless of bundle.block_number let bundle = create_bundle(vec![], 0, None); @@ -270,8 +194,7 @@ async fn test_meter_bundle_uses_latest_block() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_ignores_bundle_block_number() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; // Even if bundle.block_number is different, it should use the latest block // In this test, we specify block_number=0 in the bundle @@ -293,8 +216,7 @@ async fn test_meter_bundle_ignores_bundle_block_number() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_custom_timestamp() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; // Test that bundle.min_timestamp is used for simulation. // The timestamp affects block.timestamp in the EVM during simulation but is not @@ -313,8 +235,7 @@ async fn test_meter_bundle_custom_timestamp() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_arbitrary_block_number() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (_harness, client) = setup().await?; // Since we now ignore bundle.block_number and always use the latest block, // any block_number value should work (it's only used for bundle validity in TIPS) @@ -330,17 +251,15 @@ async fn test_meter_bundle_arbitrary_block_number() -> eyre::Result<()> { #[tokio::test] async fn test_meter_bundle_gas_calculations() -> eyre::Result<()> { - let node = setup_node().await?; - let client = node.rpc_client().await?; + let (harness, client) = setup().await?; - // Use two funded accounts from genesis.json with different gas prices - let secret1 = b256!("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - let secret2 = b256!("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"); + let secret1 = Account::Alice.signer_b256(); + let secret2 = Account::Bob.signer_b256(); // First transaction with 3 gwei gas price let tx1_inner = TransactionBuilder::default() .signer(secret1) - .chain_id(84532) + .chain_id(harness.chain_id()) .nonce(0) .to(address!("0x1111111111111111111111111111111111111111")) .value(1000) @@ -357,7 +276,7 @@ async fn test_meter_bundle_gas_calculations() -> eyre::Result<()> { // Second transaction with 7 gwei gas price let tx2_inner = TransactionBuilder::default() .signer(secret2) - .chain_id(84532) + .chain_id(harness.chain_id()) .nonce(0) .to(address!("0x2222222222222222222222222222222222222222")) .value(2000) diff --git a/crates/client/node/Cargo.toml b/crates/client/node/Cargo.toml new file mode 100644 index 00000000..5ac4c765 --- /dev/null +++ b/crates/client/node/Cargo.toml @@ -0,0 +1,96 @@ +[package] +name = "base-client-node" +description = "Primitive types and traits for Base node runner extensions" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[features] +test-utils = [ + "base-primitives/test-utils", + "dep:alloy-eips", + "dep:alloy-genesis", + "dep:alloy-primitives", + "dep:alloy-provider", + "dep:alloy-rpc-client", + "dep:alloy-rpc-types", + "dep:alloy-rpc-types-engine", + "dep:alloy-signer", + "dep:base-primitives", + "dep:chrono", + "dep:jsonrpsee", + "dep:op-alloy-network", + "dep:op-alloy-rpc-types-engine", + "dep:reth-ipc", + "dep:reth-node-core", + "dep:reth-optimism-primitives", + "dep:reth-optimism-rpc", + "dep:reth-primitives-traits", + "dep:reth-provider", + "dep:reth-rpc-layer", + "dep:reth-tracing", + "dep:tokio", + "dep:tower", + "dep:tracing-subscriber", + "dep:url", + "reth-db/test-utils", + "reth-optimism-node/test-utils", + "reth-primitives-traits?/test-utils", + "reth-provider?/test-utils", +] + +[dependencies] +# Project +base-primitives = { workspace = true, optional = true } + +# reth +reth.workspace = true +reth-db.workspace = true +reth-optimism-node.workspace = true +reth-optimism-chainspec.workspace = true +reth-ipc = { workspace = true, optional = true } +reth-tracing = { workspace = true, optional = true } +reth-rpc-layer = { workspace = true, optional = true } +reth-provider = { workspace = true, optional = true } +reth-node-core = { workspace = true, optional = true } +reth-primitives-traits = { workspace = true, optional = true } +reth-optimism-rpc = { workspace = true, features = ["client"], optional = true } +reth-optimism-primitives = { workspace = true, optional = true } + +# alloy +alloy-primitives = { workspace = true, optional = true } +alloy-genesis = { workspace = true, optional = true } +alloy-eips = { workspace = true, optional = true } +alloy-rpc-types = { workspace = true, optional = true } +alloy-rpc-types-engine = { workspace = true, optional = true } +alloy-provider = { workspace = true, optional = true } +alloy-rpc-client = { workspace = true, optional = true } +alloy-signer = { workspace = true, optional = true } + +# op-alloy +op-alloy-rpc-types-engine = { workspace = true, optional = true } +op-alloy-network = { workspace = true, optional = true } + +# tokio +tokio = { workspace = true, optional = true } + +# rpc +jsonrpsee = { workspace = true, optional = true } + +# misc +eyre.workspace = true +futures-util.workspace = true +tracing.workspace = true +derive_more = { workspace = true, features = ["debug"] } +tracing-subscriber = { workspace = true, optional = true } +url = { workspace = true, optional = true } +chrono = { workspace = true, optional = true } + +# tower for middleware +tower = { version = "0.5", optional = true } diff --git a/crates/client/primitives/README.md b/crates/client/node/README.md similarity index 61% rename from crates/client/primitives/README.md rename to crates/client/node/README.md index aadf5ece..b8fa338e 100644 --- a/crates/client/primitives/README.md +++ b/crates/client/node/README.md @@ -1,32 +1,34 @@ -# `base-primitives` +# `base-client-node` CI MIT License -Primitive types and traits for Base node runner extensions. Provides configuration types, extension traits, and type aliases for building modular node extensions. +Primitive types and traits for Base node runner extensions. Provides extension traits and type aliases for building modular node extensions. ## Overview -- **`FlashblocksConfig`**: Configuration for flashblocks streaming, including websocket endpoint and pending block depth. -- **`TracingConfig`**: Configuration for the transaction tracing ExEx, with toggles for enabling tracing and logs. - **`BaseNodeExtension`**: Trait for node builder extensions that can apply additional wiring to the builder. - **`ConfigurableBaseNodeExtension`**: Trait for extensions that can be constructed from a configuration type. - **`OpBuilder`**: Type alias for the OP node builder with launch context. - **`OpProvider`**: Type alias for the blockchain provider instance. +Configuration types are located in their respective feature crates: +- **`FlashblocksConfig`**: in `base-flashblocks` crate +- **`TxpoolConfig`**: in `base-txpool` crate + ## Usage Add the dependency to your `Cargo.toml`: ```toml [dependencies] -base-primitives = { git = "https://github.com/base/node-reth" } +base-client-node = { git = "https://github.com/base/node-reth" } ``` Implement a custom node extension: ```rust,ignore -use base_primitives::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder}; +use base_client_node::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder}; use eyre::Result; #[derive(Debug)] @@ -47,19 +49,3 @@ impl ConfigurableBaseNodeExtension for MyExtension { } } ``` - -Use configuration types: - -```rust,ignore -use base_primitives::{FlashblocksConfig, TracingConfig}; - -let flashblocks_config = FlashblocksConfig { - websocket_url: "ws://localhost:8545".to_string(), - max_pending_blocks_depth: 10, -}; - -let tracing_config = TracingConfig { - enabled: true, - logs_enabled: false, -}; -``` diff --git a/crates/client/node/src/extension.rs b/crates/client/node/src/extension.rs new file mode 100644 index 00000000..beec0801 --- /dev/null +++ b/crates/client/node/src/extension.rs @@ -0,0 +1,22 @@ +//! Traits describing node builder extensions. + +use std::fmt::Debug; + +use crate::OpBuilder; + +/// Customizes the node builder before launch. +/// +/// Register extensions via [`BaseNodeRunner::install_ext`]. +pub trait BaseNodeExtension: Send + Sync + Debug { + /// Applies the extension to the supplied builder. + fn apply(self: Box, builder: OpBuilder) -> OpBuilder; +} + +/// An extension that can be built from a config. +pub trait FromExtensionConfig: BaseNodeExtension + Sized { + /// Configuration type used to construct this extension. + type Config; + + /// Creates a new extension from the provided configuration. + fn from_config(config: Self::Config) -> Self; +} diff --git a/crates/client/runner/src/handle.rs b/crates/client/node/src/handle.rs similarity index 96% rename from crates/client/runner/src/handle.rs rename to crates/client/node/src/handle.rs index 0332ba2c..9ffc1ca1 100644 --- a/crates/client/runner/src/handle.rs +++ b/crates/client/node/src/handle.rs @@ -1,3 +1,5 @@ +//! Contains the [`BaseNodeHandle`], an awaitable handle to a launched Base node. + use std::{ future::Future, pin::Pin, diff --git a/crates/client/runner/src/lib.rs b/crates/client/node/src/lib.rs similarity index 61% rename from crates/client/runner/src/lib.rs rename to crates/client/node/src/lib.rs index 1e902da1..eeeb27f4 100644 --- a/crates/client/runner/src/lib.rs +++ b/crates/client/node/src/lib.rs @@ -3,8 +3,8 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] -mod context; -pub use context::BaseNodeBuilder; +mod extension; +pub use extension::{BaseNodeExtension, FromExtensionConfig}; mod handle; pub use handle::BaseNodeHandle; @@ -12,5 +12,8 @@ pub use handle::BaseNodeHandle; mod runner; pub use runner::BaseNodeRunner; -mod config; -pub use config::{BaseNodeConfig, RunnerFlashblocksCell}; +mod types; +pub use types::{BaseNodeBuilder, OpBuilder, OpProvider}; + +#[cfg(feature = "test-utils")] +pub mod test_utils; diff --git a/crates/client/runner/src/runner.rs b/crates/client/node/src/runner.rs similarity index 61% rename from crates/client/runner/src/runner.rs rename to crates/client/node/src/runner.rs index a7a9d35f..dcca0009 100644 --- a/crates/client/runner/src/runner.rs +++ b/crates/client/node/src/runner.rs @@ -1,60 +1,50 @@ //! Contains the [`BaseNodeRunner`], which is responsible for configuring and launching a Base node. -use base_primitives::{BaseNodeExtension, ConfigurableBaseNodeExtension}; use eyre::Result; use reth::{ builder::{EngineNodeLauncher, Node, NodeHandleFor, TreeConfig}, providers::providers::BlockchainProvider, }; -use reth_optimism_node::OpNode; +use reth_optimism_node::{OpNode, args::RollupArgs}; use tracing::info; -use crate::{BaseNodeBuilder, BaseNodeConfig, BaseNodeHandle}; +use crate::{BaseNodeBuilder, BaseNodeExtension, BaseNodeHandle, FromExtensionConfig}; /// Wraps the Base node configuration and orchestrates builder wiring. #[derive(Debug)] pub struct BaseNodeRunner { - /// Contains the configuration for the Base node. - config: BaseNodeConfig, + /// Rollup-specific arguments forwarded to the Optimism node implementation. + rollup_args: RollupArgs, /// Registered builder extensions. extensions: Vec>, } impl BaseNodeRunner { - /// Creates a new launcher using the provided configuration. - pub fn new(config: impl Into) -> Self { - Self { config: config.into(), extensions: Vec::new() } + /// Creates a new launcher using the provided rollup arguments. + pub fn new(rollup_args: RollupArgs) -> Self { + Self { rollup_args, extensions: Vec::new() } } - /// Returns the underlying configuration, primarily for testing. - pub const fn config(&self) -> &BaseNodeConfig { - &self.config + /// Registers a new builder extension. + pub fn install_ext(&mut self, config: T::Config) { + self.extensions.push(Box::new(T::from_config(config))); } - /// Registers a new builder extension constructed from the node configuration. - pub fn install_ext(&mut self) -> Result<()> - where - E: ConfigurableBaseNodeExtension, - { - let extension = E::build(&self.config)?; - self.extensions.push(Box::new(extension)); - Ok(()) - } - - /// Applies all Base-specific wiring to the supplied builder, launches the node, and returns a handle that can be awaited. + /// Applies all Base-specific wiring to the supplied builder, launches the node, and returns a + /// handle that can be awaited. pub fn run(self, builder: BaseNodeBuilder) -> BaseNodeHandle { - let Self { config, extensions } = self; - BaseNodeHandle::new(Self::launch_node(config, extensions, builder)) + let Self { rollup_args, extensions } = self; + BaseNodeHandle::new(Self::launch_node(rollup_args, extensions, builder)) } async fn launch_node( - config: BaseNodeConfig, + rollup_args: RollupArgs, extensions: Vec>, builder: BaseNodeBuilder, ) -> Result> { info!(target: "base-runner", "starting custom Base node"); - let op_node = OpNode::new(config.rollup_args.clone()); + let op_node = OpNode::new(rollup_args); let builder = builder .with_types_and_provider::>() diff --git a/crates/client/node/src/test_utils/constants.rs b/crates/client/node/src/test_utils/constants.rs new file mode 100644 index 00000000..7ceb6c7f --- /dev/null +++ b/crates/client/node/src/test_utils/constants.rs @@ -0,0 +1,39 @@ +//! Shared constants used across integration tests. + +use alloy_primitives::{B256, Bytes, b256, bytes}; +pub use reth::chainspec::NamedChain; + +// Block Building + +/// Block time in seconds for test node configuration. +pub const BLOCK_TIME_SECONDS: u64 = 2; +/// Gas limit for test blocks. +pub const GAS_LIMIT: u64 = 200_000_000; + +// Test Account Balances + +/// Balance in ETH for test accounts in fixtures. +pub const TEST_ACCOUNT_BALANCE_ETH: u64 = 100; + +// Timing / Delays + +/// Delay in milliseconds to wait for node startup. +pub const NODE_STARTUP_DELAY_MS: u64 = 500; +/// Delay in milliseconds to wait for block building. +pub const BLOCK_BUILD_DELAY_MS: u64 = 100; + +// Engine API + +/// All-zeros secret for local testing only. +pub const DEFAULT_JWT_SECRET: B256 = B256::ZERO; + +// L1 Block Info (OP Stack) + +/// Sample L1 block info deposit transaction for Base Sepolia tests. +pub const L1_BLOCK_INFO_DEPOSIT_TX: Bytes = bytes!( + "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" +); + +/// Hash of the sample L1 block info deposit transaction. +pub const L1_BLOCK_INFO_DEPOSIT_TX_HASH: B256 = + b256!("0xba56c8b0deb460ff070f8fca8e2ee01e51a3db27841cc862fdd94cc1a47662b6"); diff --git a/crates/client/test-utils/src/engine.rs b/crates/client/node/src/test_utils/engine.rs similarity index 96% rename from crates/client/test-utils/src/engine.rs rename to crates/client/node/src/test_utils/engine.rs index 8e8de2a2..53ba6711 100644 --- a/crates/client/test-utils/src/engine.rs +++ b/crates/client/node/src/test_utils/engine.rs @@ -21,7 +21,7 @@ use reth_rpc_layer::{AuthClientLayer, JwtSecret}; use reth_tracing::tracing::debug; use url::Url; -use crate::DEFAULT_JWT_SECRET; +use crate::test_utils::DEFAULT_JWT_SECRET; /// Describes how to reach the Engine API endpoint. #[derive(Clone, Debug)] @@ -97,7 +97,7 @@ impl EngineApi { /// Build a new HTTP-backed Engine API client from the provided URL. pub fn new(engine_url: String) -> Result { let url: Url = engine_url.parse()?; - let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + let jwt_secret = JwtSecret::from_hex(DEFAULT_JWT_SECRET.to_string())?; Ok(Self { address: EngineAddress::Http(url), jwt_secret, _phantom: PhantomData }) } @@ -106,7 +106,7 @@ impl EngineApi { impl EngineApi { /// Build a new IPC-backed Engine API client using the IPC socket path. pub fn new(path: String) -> Result { - let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + let jwt_secret = JwtSecret::from_hex(DEFAULT_JWT_SECRET.to_string())?; Ok(Self { address: EngineAddress::Ipc(path), jwt_secret, _phantom: PhantomData }) } diff --git a/crates/client/test-utils/src/fixtures.rs b/crates/client/node/src/test_utils/fixtures.rs similarity index 63% rename from crates/client/test-utils/src/fixtures.rs rename to crates/client/node/src/test_utils/fixtures.rs index 382a01b5..eff8a258 100644 --- a/crates/client/test-utils/src/fixtures.rs +++ b/crates/client/node/src/test_utils/fixtures.rs @@ -2,18 +2,37 @@ use std::sync::Arc; -use alloy_genesis::Genesis; +use alloy_genesis::GenesisAccount; +use alloy_primitives::{U256, utils::Unit}; +use base_primitives::{Account, build_test_genesis}; use reth::api::{NodeTypes, NodeTypesWithDBAdapter}; use reth_db::{ ClientVersion, DatabaseEnv, init_db, mdbx::{DatabaseArguments, KILOBYTE, MEGABYTE, MaxReadTransactionDuration}, test_utils::{ERROR_DB_CREATION, TempDatabase, create_test_static_files_dir, tempdir_path}, }; +use reth_optimism_chainspec::OpChainSpec; use reth_provider::{ProviderFactory, providers::StaticFileProvider}; -/// Loads the shared test genesis configuration. -pub fn load_genesis() -> Genesis { - serde_json::from_str(include_str!("../assets/genesis.json")).unwrap() +use crate::test_utils::{GENESIS_GAS_LIMIT, TEST_ACCOUNT_BALANCE_ETH}; + +/// Creates a test chain spec with pre-funded test accounts. +pub fn load_chain_spec() -> Arc { + let test_account_balance: U256 = + Unit::ETHER.wei().saturating_mul(U256::from(TEST_ACCOUNT_BALANCE_ETH)); + + let genesis = build_test_genesis() + .extend_accounts( + Account::all() + .into_iter() + .map(|a| { + (a.address(), GenesisAccount::default().with_balance(test_account_balance)) + }) + .collect::>(), + ) + .with_gas_limit(GENESIS_GAS_LIMIT); + + Arc::new(OpChainSpec::from_genesis(genesis)) } /// Creates a provider factory for tests with the given chain spec. diff --git a/crates/client/test-utils/src/harness.rs b/crates/client/node/src/test_utils/harness.rs similarity index 65% rename from crates/client/test-utils/src/harness.rs rename to crates/client/node/src/test_utils/harness.rs index 7a1a7e3c..76ddc3f3 100644 --- a/crates/client/test-utils/src/harness.rs +++ b/crates/client/node/src/test_utils/harness.rs @@ -1,67 +1,112 @@ //! Unified test harness combining node and engine helpers, plus optional flashblocks adapter. -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; use alloy_primitives::{B64, B256, Bytes}; use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_client::RpcClient; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; use eyre::{Result, eyre}; -use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; -use reth::{ - builder::NodeHandle, - providers::{BlockNumReader, BlockReader, ChainSpecProvider}, -}; -use reth_e2e_test_utils::Adapter; -use reth_optimism_node::OpNode; +use reth::providers::{BlockNumReader, BlockReader, ChainSpecProvider}; +use reth_optimism_chainspec::OpChainSpec; use reth_optimism_primitives::OpBlock; use reth_primitives_traits::{Block as BlockT, RecoveredBlock}; use tokio::time::sleep; use crate::{ - BLOCK_BUILD_DELAY_MS, BLOCK_TIME_SECONDS, GAS_LIMIT, L1_BLOCK_INFO_DEPOSIT_TX, - NODE_STARTUP_DELAY_MS, TestAccounts, - engine::{EngineApi, IpcEngine}, - node::{LocalNode, LocalNodeProvider, OpAddOns, OpBuilder, default_launcher}, - tracing::init_silenced_tracing, + BaseNodeExtension, FromExtensionConfig, + test_utils::{ + BLOCK_BUILD_DELAY_MS, BLOCK_TIME_SECONDS, GAS_LIMIT, L1_BLOCK_INFO_DEPOSIT_TX, + NODE_STARTUP_DELAY_MS, + engine::{EngineApi, IpcEngine}, + node::{LocalNode, LocalNodeProvider}, + tracing::init_silenced_tracing, + }, }; +/// Builder for configuring and launching a test harness. +#[derive(Debug, Default)] +pub struct TestHarnessBuilder { + extensions: Vec>, + chain_spec: Option>, +} + +impl TestHarnessBuilder { + /// Create a new builder with no extensions. + pub fn new() -> Self { + Self::default() + } + + /// Add an extension to be applied during node launch using its config type. + pub fn with_ext(mut self, config: T::Config) -> Self { + self.extensions.push(Box::new(T::from_config(config))); + self + } + + /// Add a pre-constructed extension to be applied during node launch. + /// + /// Prefer [`with_ext`](Self::with_ext) for simpler configuration. + pub fn with_extension(mut self, ext: impl BaseNodeExtension + 'static) -> Self { + self.extensions.push(Box::new(ext)); + self + } + + /// Set a custom chain spec for the test harness. + /// + /// If not provided, the default genesis is built programmatically. + pub fn with_chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); + self + } + + /// Build and launch the test harness. + pub async fn build(self) -> Result { + init_silenced_tracing(); + + let chain_spec = match self.chain_spec { + Some(spec) => spec, + None => { + let genesis = crate::test_utils::build_test_genesis(); + Arc::new(OpChainSpec::from_genesis(genesis)) + } + }; + + let node = LocalNode::new(self.extensions, chain_spec).await?; + let engine = node.engine_api()?; + + sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; + + Ok(TestHarness { node, engine }) + } +} + /// High-level façade that bundles a local node, engine API client, and common helpers. #[derive(Debug)] pub struct TestHarness { node: LocalNode, engine: EngineApi, - accounts: TestAccounts, } impl TestHarness { - /// Launch a new harness using the default launcher configuration. + /// Launch a new harness using the default configuration (no extensions). pub async fn new() -> Result { - Self::with_launcher(default_launcher).await + TestHarnessBuilder::new().build().await } - /// Launch the harness with a custom node launcher (e.g. to tweak components). - pub async fn with_launcher(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - init_silenced_tracing(); - let node = LocalNode::new(launcher).await?; - Self::from_node(node).await + /// Create a builder for configuring the test harness with extensions. + pub fn builder() -> TestHarnessBuilder { + TestHarnessBuilder::new() } - /// Build a harness from an already-running [`LocalNode`]. - pub(crate) async fn from_node(node: LocalNode) -> Result { - let engine = node.engine_api()?; - let accounts = TestAccounts::new(); - - sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; - - Ok(Self { node, engine, accounts }) + /// Create a harness from pre-built parts. + /// + /// This is useful when you need to capture extension state before building the harness. + pub fn from_parts(node: LocalNode, engine: EngineApi) -> Self { + Self { node, engine } } /// Return an Optimism JSON-RPC provider connected to the harness node. @@ -69,11 +114,6 @@ impl TestHarness { self.node.provider().expect("provider should always be available after node initialization") } - /// Access the deterministic test accounts backing the harness. - pub fn accounts(&self) -> &TestAccounts { - &self.accounts - } - /// Access the low-level blockchain provider for direct database queries. pub fn blockchain_provider(&self) -> LocalNodeProvider { self.node.blockchain_provider() @@ -89,6 +129,12 @@ impl TestHarness { format!("ws://{}", self.node.ws_api_addr) } + /// Return a JSON-RPC client connected to the harness node. + pub fn rpc_client(&self) -> Result { + let url = self.rpc_url().parse()?; + Ok(RpcClient::new_http(url)) + } + /// Build a block using the provided transactions and push it through the engine. pub async fn build_block_from_transactions(&self, mut transactions: Vec) -> Result<()> { // Ensure the block always starts with the required L1 block info deposit. @@ -187,6 +233,16 @@ impl TestHarness { .expect("canonical block exists"); BlockT::try_into_recovered(block).expect("able to recover canonical block") } + + /// Return the chain specification used by the harness. + pub fn chain_spec(&self) -> Arc { + self.node.blockchain_provider().chain_spec() + } + + /// Return the chain ID used by the harness. + pub fn chain_id(&self) -> u64 { + self.chain_spec().chain().id() + } } #[cfg(test)] @@ -195,18 +251,17 @@ mod tests { use alloy_provider::Provider; use super::*; + use crate::test_utils::Account; + #[tokio::test] async fn test_harness_setup() -> Result<()> { let harness = TestHarness::new().await?; - assert_eq!(harness.accounts().alice.name, "Alice"); - assert_eq!(harness.accounts().bob.name, "Bob"); - let provider = harness.provider(); let chain_id = provider.get_chain_id().await?; - assert_eq!(chain_id, crate::BASE_CHAIN_ID); + assert_eq!(chain_id, crate::test_utils::DEVNET_CHAIN_ID); - let alice_balance = provider.get_balance(harness.accounts().alice.address).await?; + let alice_balance = provider.get_balance(Account::Alice.address()).await?; assert!(alice_balance > U256::ZERO); let block_number = provider.get_block_number().await?; diff --git a/crates/client/node/src/test_utils/mod.rs b/crates/client/node/src/test_utils/mod.rs new file mode 100644 index 00000000..5aff3eb1 --- /dev/null +++ b/crates/client/node/src/test_utils/mod.rs @@ -0,0 +1,38 @@ +//! Test utilities for integration testing. +//! +//! This module provides testing infrastructure including: +//! - [`TestHarness`] and [`TestHarnessBuilder`] - Unified test harness for node and engine. +//! - [`LocalNode`] and [`LocalNodeProvider`] - Local node setup. +//! - [`EngineApi`] with [`HttpEngine`] and [`IpcEngine`] - Engine API client. +//! - Test constants and fixtures. + +// Re-export from base-primitives for backwards compatibility +pub use base_primitives::{ + AccessListContract, Account, ContractFactory, DEVNET_CHAIN_ID, DoubleCounter, + GENESIS_GAS_LIMIT, Logic, Logic2, Minimal7702Account, MockERC20, Proxy, SimpleStorage, + TransparentUpgradeableProxy, build_test_genesis, +}; + +mod constants; +pub use constants::{ + BLOCK_BUILD_DELAY_MS, BLOCK_TIME_SECONDS, DEFAULT_JWT_SECRET, GAS_LIMIT, + L1_BLOCK_INFO_DEPOSIT_TX, L1_BLOCK_INFO_DEPOSIT_TX_HASH, NODE_STARTUP_DELAY_MS, NamedChain, + TEST_ACCOUNT_BALANCE_ETH, +}; + +mod engine; +pub use engine::{EngineAddress, EngineApi, EngineProtocol, HttpEngine, IpcEngine}; + +mod fixtures; +pub use fixtures::{create_provider_factory, load_chain_spec}; + +mod harness; +pub use harness::{TestHarness, TestHarnessBuilder}; + +mod node; +pub use node::{LocalNode, LocalNodeProvider}; + +mod tracing; +// Re-export signer traits for use in tests +pub use alloy_signer::SignerSync; +pub use tracing::init_silenced_tracing; diff --git a/crates/client/node/src/test_utils/node.rs b/crates/client/node/src/test_utils/node.rs new file mode 100644 index 00000000..d8ce15c9 --- /dev/null +++ b/crates/client/node/src/test_utils/node.rs @@ -0,0 +1,184 @@ +//! Local node setup with Base Sepolia chainspec + +use std::{any::Any, fmt, net::SocketAddr, path::PathBuf, sync::Arc}; + +use alloy_provider::RootProvider; +use alloy_rpc_client::RpcClient; +use eyre::Result; +use op_alloy_network::Optimism; +use reth::{ + args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, + builder::{EngineNodeLauncher, Node, NodeBuilder, NodeConfig, NodeHandle, TreeConfig}, + core::exit::NodeExitFuture, + providers::providers::BlockchainProvider, + tasks::TaskManager, +}; +use reth_db::{ + ClientVersion, DatabaseEnv, init_db, mdbx::DatabaseArguments, test_utils::tempdir_path, +}; +use reth_node_core::{ + args::DatadirArgs, + dirs::{DataDirPath, MaybePlatformPath}, +}; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_node::{OpNode, args::RollupArgs}; + +use crate::{BaseNodeExtension, OpProvider, test_utils::engine::EngineApi}; + +/// Convenience alias for the local blockchain provider type. +pub type LocalNodeProvider = OpProvider; + +/// Handle to a launched local node along with the resources required to keep it alive. +pub struct LocalNode { + pub(crate) http_api_addr: SocketAddr, + engine_ipc_path: String, + pub(crate) ws_api_addr: SocketAddr, + provider: LocalNodeProvider, + _node_exit_future: NodeExitFuture, + _node: Box, + _task_manager: TaskManager, + _db_path: PathBuf, +} + +impl Drop for LocalNode { + fn drop(&mut self) { + // Clean up the temporary database directory + let _ = std::fs::remove_dir_all(&self._db_path); + } +} + +impl fmt::Debug for LocalNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LocalNode") + .field("http_api_addr", &self.http_api_addr) + .field("ws_api_addr", &self.ws_api_addr) + .field("engine_ipc_path", &self.engine_ipc_path) + .finish_non_exhaustive() + } +} + +impl LocalNode { + /// Launch a new local node with the provided extensions and chain spec. + pub async fn new( + extensions: Vec>, + chain_spec: Arc, + ) -> Result { + let tasks = TaskManager::current(); + let exec = tasks.executor(); + + let network_config = NetworkArgs { + discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, + ..NetworkArgs::default() + }; + + let unique_ipc_path = format!( + "/tmp/reth_engine_api_{}_{}_{:?}.ipc", + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(), + std::process::id(), + std::thread::current().id() + ); + + let mut rpc_args = + RpcServerArgs::default().with_unused_ports().with_http().with_auth_ipc().with_ws(); + rpc_args.auth_ipc_path = unique_ipc_path; + + let op_node = OpNode::new(RollupArgs::default()); + + let (db, db_path) = Self::create_test_database()?; + + let mut node_config = NodeConfig::new(chain_spec.clone()) + .with_network(network_config) + .with_rpc(rpc_args) + .with_unused_ports(); + + let datadir_path = MaybePlatformPath::::from(db_path.clone()); + node_config = node_config + .with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); + + let builder = NodeBuilder::new(node_config.clone()) + .with_database(db) + .with_launch_context(exec.clone()) + .with_types_and_provider::>() + .with_components(op_node.components()) + .with_add_ons(op_node.add_ons()) + .on_component_initialized(move |_ctx| Ok(())); + + // Apply all extensions + let builder = + extensions.into_iter().fold(builder, |builder, extension| extension.apply(builder)); + + // Launch with EngineNodeLauncher + let NodeHandle { node: node_handle, node_exit_future } = builder + .launch_with_fn(|builder| { + let engine_tree_config = TreeConfig::default() + .with_persistence_threshold(builder.config().engine.persistence_threshold) + .with_memory_block_buffer_target( + builder.config().engine.memory_block_buffer_target, + ); + + let launcher = EngineNodeLauncher::new( + builder.task_executor().clone(), + builder.config().datadir(), + engine_tree_config, + ); + + builder.launch_with(launcher) + }) + .await?; + + let http_api_addr = node_handle + .rpc_server_handle() + .http_local_addr() + .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; + + let ws_api_addr = node_handle + .rpc_server_handle() + .ws_local_addr() + .ok_or_else(|| eyre::eyre!("Failed to get websocket api address"))?; + + let engine_ipc_path = node_config.rpc.auth_ipc_path; + let provider = node_handle.provider().clone(); + + Ok(Self { + http_api_addr, + ws_api_addr, + engine_ipc_path, + provider, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + _db_path: db_path, + }) + } + + /// Creates a test database with a 100 MB map size (vs reth's default 8 TB). + fn create_test_database() -> Result<(Arc, PathBuf)> { + let path = tempdir_path(); + let args = DatabaseArguments::new(ClientVersion::default()) + .with_geometry_max_size(Some(100 * 1024 * 1024)); + let db = init_db(&path, args).expect("Failed to create test database"); + Ok((Arc::new(db), path)) + } + + /// Create an HTTP provider pointed at the node's public RPC endpoint. + pub fn provider(&self) -> Result> { + let url = format!("http://{}", self.http_api_addr); + let client = RpcClient::builder().http(url.parse()?); + Ok(RootProvider::::new(client)) + } + + /// Build an Engine API client that talks to the node's IPC endpoint. + pub fn engine_api(&self) -> Result> { + EngineApi::::new(self.engine_ipc_path.clone()) + } + + /// Clone the underlying blockchain provider so callers can inspect chain state. + pub fn blockchain_provider(&self) -> LocalNodeProvider { + self.provider.clone() + } + + /// Websocket URL for the local node. + pub fn ws_url(&self) -> String { + format!("ws://{}", self.ws_api_addr) + } +} diff --git a/crates/client/test-utils/src/tracing.rs b/crates/client/node/src/test_utils/tracing.rs similarity index 100% rename from crates/client/test-utils/src/tracing.rs rename to crates/client/node/src/test_utils/tracing.rs diff --git a/crates/client/primitives/src/types.rs b/crates/client/node/src/types.rs similarity index 74% rename from crates/client/primitives/src/types.rs rename to crates/client/node/src/types.rs index fee2d759..b25f0c46 100644 --- a/crates/client/primitives/src/types.rs +++ b/crates/client/node/src/types.rs @@ -4,10 +4,11 @@ use std::sync::Arc; use reth::{ api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}, - builder::{Node, NodeBuilderWithComponents, WithLaunchContext}, + builder::{Node, NodeBuilder, NodeBuilderWithComponents, WithLaunchContext}, providers::providers::BlockchainProvider, }; use reth_db::DatabaseEnv; +use reth_optimism_chainspec::OpChainSpec; use reth_optimism_node::OpNode; type OpNodeTypes = FullNodeTypesAdapter, OpProvider>; @@ -20,3 +21,6 @@ pub type OpProvider = BlockchainProvider>; + +/// Convenience alias for the Base node builder type. +pub type BaseNodeBuilder = WithLaunchContext, OpChainSpec>>; diff --git a/crates/client/primitives/Cargo.toml b/crates/client/primitives/Cargo.toml deleted file mode 100644 index ed12581e..00000000 --- a/crates/client/primitives/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "base-primitives" -description = "Primitive types and traits for Base node runner extensions" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[lints] -workspace = true - -[dependencies] -# reth -reth.workspace = true -reth-db.workspace = true -reth-optimism-node.workspace = true - -# misc -eyre.workspace = true diff --git a/crates/client/primitives/src/config.rs b/crates/client/primitives/src/config.rs deleted file mode 100644 index 67a102d5..00000000 --- a/crates/client/primitives/src/config.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Configuration types for extensions. - -/// Flashblocks-specific configuration knobs. -#[derive(Debug, Clone)] -pub struct FlashblocksConfig { - /// The websocket endpoint that streams flashblock updates. - pub websocket_url: String, - /// Maximum number of pending flashblocks to retain in memory. - pub max_pending_blocks_depth: u64, -} - -/// Transaction tracing toggles. -#[derive(Debug, Clone, Copy)] -pub struct TracingConfig { - /// Enables the transaction tracing ExEx. - pub enabled: bool, - /// Emits `info`-level logs for the tracing ExEx when enabled. - pub logs_enabled: bool, -} diff --git a/crates/client/primitives/src/extension.rs b/crates/client/primitives/src/extension.rs deleted file mode 100644 index 605978bf..00000000 --- a/crates/client/primitives/src/extension.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Traits describing configurable node builder extensions. - -use std::fmt::Debug; - -use eyre::Result; - -use crate::OpBuilder; - -/// A node builder extension that can apply additional wiring to the builder. -pub trait BaseNodeExtension: Send + Sync + Debug { - /// Applies the extension to the supplied builder. - fn apply(self: Box, builder: OpBuilder) -> OpBuilder; -} - -/// An extension that can be constructed from a configuration type. -pub trait ConfigurableBaseNodeExtension: BaseNodeExtension + Sized + 'static { - /// Builds the extension from the node config. - fn build(config: &C) -> Result; -} diff --git a/crates/client/runner/Cargo.toml b/crates/client/runner/Cargo.toml deleted file mode 100644 index c367a166..00000000 --- a/crates/client/runner/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "base-reth-runner" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -description = "Base-specific node launcher wiring" - -[lints] -workspace = true - -[dependencies] -# internal -base-primitives.workspace = true -base-reth-flashblocks.workspace = true -base-reth-metering.workspace = true -base-txpool.workspace = true - -# reth -reth.workspace = true -reth-db.workspace = true -reth-optimism-node.workspace = true -reth-optimism-chainspec.workspace = true - -# misc -eyre.workspace = true -futures-util.workspace = true -tracing.workspace = true -derive_more = { workspace = true, features = ["debug"] } diff --git a/crates/client/runner/README.md b/crates/client/runner/README.md deleted file mode 100644 index 2bd4f7fa..00000000 --- a/crates/client/runner/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# `base-reth-runner` - -CI -MIT License - -Base-specific node launcher that wires together the Optimism node components, execution extensions, and RPC add-ons for the Base node binary. Exposes the types that the CLI uses to build a node and pass them to Optimism's `Cli` runner. - -## Overview - -- **`BaseNodeBuilder`**: Builder for constructing a Base node with custom extensions and configuration. -- **`BaseNodeRunner`**: Runs the Base node with all configured extensions. -- **`BaseNodeHandle`**: Handle to the running node, providing access to providers and state. -- **`BaseNodeConfig`**: Configuration options for the Base node. -- **`FlashblocksConfig`**: Configuration for flashblocks WebSocket subscription. -- **`TracingConfig`**: Configuration for transaction tracing extension. - -## Extensions - -- **`BaseNodeExtension`**: Core extension trait for adding functionality to the node. -- **`BaseRpcExtension`**: Extension for adding custom RPC methods. -- **`FlashblocksCanonExtension`**: Extension for canonical block reconciliation with flashblocks. -- **`TransactionTracingExtension`**: Extension for transaction lifecycle tracing. - -## Usage - -Add the dependency to your `Cargo.toml`: - -```toml -[dependencies] -base-reth-runner = { git = "https://github.com/base/node-reth" } -``` - -Build and run a Base node: - -```rust,ignore -use base_reth_runner::{BaseNodeBuilder, BaseNodeConfig, FlashblocksConfig}; - -let config = BaseNodeConfig { - flashblocks: FlashblocksConfig { - enabled: true, - url: "wss://flashblocks.base.org".to_string(), - }, - ..Default::default() -}; - -let node = BaseNodeBuilder::new(config) - .with_flashblocks() - .with_transaction_tracing() - .build() - .await?; -``` - -## License - -Licensed under the [MIT License](https://github.com/base/node-reth/blob/main/LICENSE). diff --git a/crates/client/runner/src/config.rs b/crates/client/runner/src/config.rs deleted file mode 100644 index 42f522ae..00000000 --- a/crates/client/runner/src/config.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Contains the Base node configuration structures. - -use base_primitives::{FlashblocksConfig, OpProvider, TracingConfig}; -use base_reth_flashblocks::{FlashblocksCanonConfig, FlashblocksCell, FlashblocksState}; -use base_reth_metering::MeteringRpcConfig; -use base_txpool::{TransactionStatusRpcConfig, TransactionTracingConfig}; -use reth_optimism_node::args::RollupArgs; - -/// Concrete type alias for the flashblocks cell used in the runner. -pub type RunnerFlashblocksCell = FlashblocksCell>; - -/// Captures the pieces of CLI configuration that the node logic cares about. -#[derive(Debug, Clone)] -pub struct BaseNodeConfig { - /// Rollup-specific arguments forwarded to the Optimism node implementation. - pub rollup_args: RollupArgs, - /// Optional flashblocks configuration if the websocket URL was provided. - pub flashblocks: Option, - /// Execution extension tracing toggles. - pub tracing: TracingConfig, - /// Indicates whether the metering RPC surface should be installed. - pub metering_enabled: bool, - /// Shared Flashblocks state cache. - pub flashblocks_cell: RunnerFlashblocksCell, -} - -impl BaseNodeConfig { - /// Returns `true` if flashblocks support should be wired up. - pub const fn flashblocks_enabled(&self) -> bool { - self.flashblocks.is_some() - } -} - -// Implement configuration traits for BaseNodeConfig so it can be used -// with ConfigurableBaseNodeExtension - -impl FlashblocksCanonConfig for BaseNodeConfig { - fn flashblocks_cell(&self) -> &FlashblocksCell> { - &self.flashblocks_cell - } - - fn flashblocks(&self) -> Option<&FlashblocksConfig> { - self.flashblocks.as_ref() - } -} - -impl TransactionTracingConfig for BaseNodeConfig { - fn tracing(&self) -> &TracingConfig { - &self.tracing - } -} - -impl MeteringRpcConfig for BaseNodeConfig { - fn metering_enabled(&self) -> bool { - self.metering_enabled - } -} - -impl TransactionStatusRpcConfig for BaseNodeConfig { - fn sequencer_rpc(&self) -> Option<&str> { - self.rollup_args.sequencer.as_deref() - } -} diff --git a/crates/client/runner/src/context.rs b/crates/client/runner/src/context.rs deleted file mode 100644 index cf28cbb6..00000000 --- a/crates/client/runner/src/context.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Contains a type alias for the base node builder used in the runner. - -use std::sync::Arc; - -use reth::builder::{NodeBuilder, WithLaunchContext}; -use reth_db::DatabaseEnv; -use reth_optimism_chainspec::OpChainSpec; - -/// Convenience alias for the Base node builder type. -pub type BaseNodeBuilder = WithLaunchContext, OpChainSpec>>; diff --git a/crates/client/test-utils/Cargo.toml b/crates/client/test-utils/Cargo.toml deleted file mode 100644 index d66222f9..00000000 --- a/crates/client/test-utils/Cargo.toml +++ /dev/null @@ -1,77 +0,0 @@ -[package] -name = "base-reth-test-utils" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -description = "Common integration test utilities for node-reth crates" - -[lints] -workspace = true - -[dependencies] -# Project -base-flashtypes.workspace = true -base-reth-flashblocks.workspace = true - -# reth -reth.workspace = true -reth-optimism-node.workspace = true -reth-optimism-chainspec.workspace = true -reth-optimism-primitives.workspace = true -reth-optimism-rpc = { workspace = true, features = ["client"] } -reth-provider.workspace = true -reth-primitives-traits.workspace = true -reth-db.workspace = true -reth-e2e-test-utils.workspace = true -reth-node-core.workspace = true -reth-exex.workspace = true -reth-tracing.workspace = true -reth-rpc-layer.workspace = true -reth-ipc.workspace = true - -# alloy -alloy-primitives.workspace = true -alloy-genesis.workspace = true -alloy-sol-macro = { workspace = true, features = ["json"] } -alloy-sol-types.workspace = true -alloy-contract.workspace = true -alloy-eips.workspace = true -alloy-rpc-types.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-consensus.workspace = true -alloy-provider.workspace = true -alloy-rpc-client.workspace = true -alloy-signer = "1.0" -alloy-signer-local = "1.1.0" - -# op-alloy -op-alloy-rpc-types.workspace = true -op-alloy-rpc-types-engine.workspace = true -op-alloy-network.workspace = true - -# tokio -tokio.workspace = true -tokio-stream.workspace = true - -# async -futures-util.workspace = true - -# rpc -jsonrpsee.workspace = true - -# misc -derive_more = { workspace = true, features = ["deref"] } -tracing-subscriber.workspace = true -serde_json.workspace = true -eyre.workspace = true -once_cell.workspace = true -url.workspace = true -chrono.workspace = true - -# tower for middleware -tower = "0.5" - -[dev-dependencies] diff --git a/crates/client/test-utils/README.md b/crates/client/test-utils/README.md deleted file mode 100644 index 2b735f2c..00000000 --- a/crates/client/test-utils/README.md +++ /dev/null @@ -1,328 +0,0 @@ -# Test Utils - -A comprehensive integration test framework for node-reth crates. - -## Overview - -This crate provides reusable testing utilities for integration tests across the node-reth workspace. It includes: - -- **LocalNode**: Isolated in-process node with Base Sepolia chainspec -- **TestHarness**: Unified orchestration layer combining node, Engine API, and flashblocks -- **EngineApi**: Type-safe Engine API client for CL operations -- **Test Accounts**: Pre-funded hardcoded accounts (Alice, Bob, Charlie, Deployer) -- **Flashblocks Support**: Testing pending state with flashblocks delivery - -## Quick Start - -```rust,ignore -use base_reth_test_utils::TestHarness; - -#[tokio::test] -async fn test_example() -> eyre::Result<()> { - let harness = TestHarness::new().await?; - - // Advance the chain - harness.advance_chain(5).await?; - - // Access accounts - let alice = &harness.accounts().alice; - - // Get balance via provider - let balance = harness.provider().get_balance(alice.address).await?; - - Ok(()) -} -``` - -## Architecture - -The framework follows a three-layer architecture: - -```text -┌─────────────────────────────────────┐ -│ TestHarness │ ← Orchestration layer (tests use this) -│ - Coordinates node + engine │ -│ - Builds blocks from transactions │ -│ - Manages test accounts │ -│ - Manages flashblocks │ -└─────────────────────────────────────┘ - │ │ - ┌──────┘ └──────┐ - ▼ ▼ -┌─────────┐ ┌──────────┐ -│LocalNode│ │EngineApi │ ← Raw API wrappers -│ (EL) │ │ (CL) │ -└─────────┘ └──────────┘ -``` - -### Component Responsibilities - -- **LocalNode** (EL wrapper): In-process Optimism node with HTTP RPC + Engine API IPC -- **EngineApi** (CL wrapper): Raw Engine API calls (forkchoice, payloads) -- **TestHarness**: Orchestrates block building by fetching latest block headers and calling Engine API - -## Components - -### 1. TestHarness - -The main entry point for integration tests that only need canonical chain control. Combines node, engine, and accounts into a single interface. - -```rust,ignore -use base_reth_test_utils::TestHarness; -use alloy_primitives::Bytes; - -#[tokio::test] -async fn test_harness() -> eyre::Result<()> { - let harness = TestHarness::new().await?; - - // Access provider - let provider = harness.provider(); - let chain_id = provider.get_chain_id().await?; - - // Access accounts - let alice = &harness.accounts().alice; - let bob = &harness.accounts().bob; - - // Build empty blocks - harness.advance_chain(10).await?; - - // Build block with transactions - let txs: Vec = vec![/* signed transaction bytes */]; - harness.build_block_from_transactions(txs).await?; - - Ok(()) -} -``` - -> Need pending-state testing? Use `FlashblocksHarness` (see Flashblocks section below) to gain `send_flashblock` helpers. - -**Key Methods:** -- `new()` - Create new harness with node, engine, and accounts -- `provider()` - Get Alloy RootProvider for RPC calls -- `accounts()` - Access test accounts -- `advance_chain(n)` - Build N empty blocks -- `build_block_from_transactions(txs)` - Build block with specific transactions (auto-prepends the L1 block info deposit) - -**Block Building Process:** -1. Fetches latest block header from provider (no local state tracking) -2. Calculates next timestamp (parent + 2 seconds for Base) -3. Calls `engine.update_forkchoice()` with payload attributes -4. Waits for block construction -5. Calls `engine.get_payload()` to retrieve built payload -6. Calls `engine.new_payload()` to validate and submit -7. Calls `engine.update_forkchoice()` again to finalize - -### 2. LocalNode - -In-process Optimism node with Base Sepolia configuration. - -```rust,ignore -use base_reth_test_utils::{LocalNode, default_launcher}; - -#[tokio::test] -async fn test_node() -> eyre::Result<()> { - let node = LocalNode::new(default_launcher).await?; - - let provider = node.provider()?; - let engine = node.engine_api()?; - - Ok(()) -} -``` - -**Features (base):** -- Base Sepolia chain configuration -- Disabled P2P discovery (isolated testing) -- Random unused ports (parallel test safety) -- HTTP RPC server at `node.http_api_addr` -- Engine API IPC at `node.engine_ipc_path` - -For flashblocks-enabled nodes, use `FlashblocksLocalNode`: - -```rust,ignore -use base_reth_test_utils::FlashblocksLocalNode; - -let node = FlashblocksLocalNode::new().await?; -let pending_state = node.flashblocks_state(); -node.send_flashblock(flashblock).await?; -``` - -**Note:** Most tests should use `TestHarness` instead of `LocalNode` directly. - -### 3. EngineApi - -Type-safe Engine API client wrapping raw CL operations. - -```rust,ignore -use base_reth_test_utils::EngineApi; -use alloy_primitives::B256; -use op_alloy_rpc_types_engine::OpPayloadAttributes; - -// Usually accessed via TestHarness, but can be used directly -let engine = node.engine_api()?; - -// Raw Engine API calls -let fcu = engine.update_forkchoice(current_head, new_head, Some(attrs)).await?; -let payload = engine.get_payload(payload_id).await?; -let status = engine.new_payload(payload, vec![], parent_root, requests).await?; -``` - -**Methods:** -- `get_payload(payload_id)` - Retrieve built payload by ID -- `new_payload(payload, hashes, root, requests)` - Submit new payload -- `update_forkchoice(current, new, attrs)` - Update forkchoice state - -**Note:** EngineApi is stateless. Block building logic lives in `TestHarness`. - -### 4. Test Accounts - -Hardcoded test accounts with deterministic addresses (Anvil-compatible). - -```rust,ignore -use base_reth_test_utils::{TestAccounts, TestHarness}; - -let accounts = TestAccounts::new(); - -let alice = &accounts.alice; -let bob = &accounts.bob; -let charlie = &accounts.charlie; -let deployer = &accounts.deployer; - -// Access via harness -let harness = TestHarness::new().await?; -let alice = &harness.accounts().alice; -``` - -**Account Details:** -- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH -- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH -- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH -- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH - -Each account includes: -- `name` - Account identifier -- `address` - Ethereum address -- `private_key` - Private key (hex string) -- `initial_balance_eth` - Starting balance in ETH - -### 5. Flashblocks Support - -Use `FlashblocksHarness` when you need `send_flashblock` and access to the in-memory pending state. - -```rust,ignore -use base_reth_test_utils::FlashblocksHarness; - -#[tokio::test] -async fn test_flashblocks() -> eyre::Result<()> { - let harness = FlashblocksHarness::new().await?; - - harness.send_flashblock(flashblock).await?; - - let pending = harness.flashblocks_state(); - // assertions... - - Ok(()) -} -``` - -`FlashblocksHarness` derefs to the base `TestHarness`, so you can keep using methods like `provider()`, `build_block_from_transactions`, etc. - -Test flashblocks delivery without WebSocket connections by constructing payloads and sending them through `FlashblocksHarness` (or the lower-level `FlashblocksLocalNode`). - -## Configuration Constants - -Key constants are exported from the crate root: - -```rust,ignore -use base_reth_test_utils::{ - BASE_CHAIN_ID, // Chain ID for Base Sepolia (84532) - BLOCK_TIME_SECONDS, // Base L2 block time (2 seconds) - GAS_LIMIT, // Default gas limit (200M) - NODE_STARTUP_DELAY_MS, // IPC endpoint initialization (500ms) - BLOCK_BUILD_DELAY_MS, // Payload construction wait (100ms) - L1_BLOCK_INFO_DEPOSIT_TX, // Pre-captured L1 block info deposit - L1_BLOCK_INFO_DEPOSIT_TX_HASH, - DEFAULT_JWT_SECRET, // All-zeros JWT for local testing -}; -``` - -## File Structure - -```text -test-utils/ -├── src/ -│ ├── lib.rs # Public API and re-exports -│ ├── accounts.rs # Test account definitions -│ ├── constants.rs # Shared constants (chain ID, timing, etc.) -│ ├── contracts.rs # Solidity contract bindings -│ ├── engine.rs # EngineApi (CL wrapper) -│ ├── fixtures.rs # Genesis loading, provider factories -│ ├── flashblocks_harness.rs # FlashblocksHarness + helpers -│ ├── harness.rs # TestHarness (orchestration) -│ ├── node.rs # LocalNode (EL wrapper) -│ └── tracing.rs # Tracing initialization helpers -├── assets/ -│ └── genesis.json # Base Sepolia genesis -├── contracts/ # Solidity sources + compiled artifacts -└── Cargo.toml -``` - -## Usage in Other Crates - -Add to `dev-dependencies`: - -```toml -[dev-dependencies] -base-reth-test-utils.workspace = true -``` - -Import in tests: - -```rust,ignore -use base_reth_test_utils::TestHarness; - -#[tokio::test] -async fn my_test() -> eyre::Result<()> { - let harness = TestHarness::new().await?; - // Your test logic - - Ok(()) -} -``` - -## Design Principles - -1. **Separation of Concerns**: LocalNode (EL), EngineApi (CL), TestHarness (orchestration) -2. **Stateless Components**: No local state tracking; always fetch from provider -3. **Type Safety**: Use reth's `OpEngineApiClient` trait instead of raw RPC strings -4. **Parallel Testing**: Random ports + isolated nodes enable concurrent tests -5. **Anvil Compatibility**: Same mnemonic as Anvil for tooling compatibility - -## Testing - -Run the test suite: - -```bash -cargo test -p base-reth-test-utils -``` - -Run specific test: - -```bash -cargo test -p base-reth-test-utils test_harness_setup -``` - -## Future Enhancements - -- Transaction builders for common operations -- Smart contract deployment helpers (Foundry integration planned) -- Snapshot/restore functionality -- Multi-node network simulation -- Performance benchmarking utilities -- Helper builder for Flashblocks - -## References - -Inspired by: -- [op-rbuilder test framework](https://github.com/flashbots/op-rbuilder/tree/main/crates/op-rbuilder/src/tests/framework) -- [reth e2e-test-utils](https://github.com/paradigmxyz/reth/tree/main/crates/e2e-test-utils) diff --git a/crates/client/test-utils/assets/genesis.json b/crates/client/test-utils/assets/genesis.json deleted file mode 100644 index dde20c5e..00000000 --- a/crates/client/test-utils/assets/genesis.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "config": { - "chainId": 84532, - "homesteadBlock": 0, - "eip150Block": 0, - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "mergeNetsplitBlock": 0, - "bedrockBlock": 0, - "regolithTime": 0, - "canyonTime": 0, - "ecotoneTime": 0, - "fjordTime": 0, - "graniteTime": 0, - "isthmusTime": 0, - "jovianTime": 0, - "pragueTime": 0, - "terminalTotalDifficulty": 0, - "terminalTotalDifficultyPassed": true, - "optimism": { - "eip1559Elasticity": 6, - "eip1559Denominator": 50 - } - }, - "nonce": "0x0", - "timestamp": "0x0", - "extraData": "0x00", - "gasLimit": "0x1c9c380", - "difficulty": "0x0", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x0000000000000000000000000000000000000000", - "alloc": { - "0x14dc79964da2c08b23698b3d3cc7ca32193d9955": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x1cbd3b2770909d4e10f157cabc84c7264073c9ec": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x2546bcd3c84621e976d8185a91a922ae77ecec30": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x71be63f3384f5fb98995898a86b02fb2426c5788": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x90f79bf6eb2c4f870365e785982e1f101e93b906": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x976ea74026e726554db657fa54763abd0c3a0aa9": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x9c41de96b2088cdc640c6182dfcf5491dc574a57": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xa0ee7a142d267c1f36714e4a8f75612f20a79720": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xbcd4042de499d14e55001ccbb24a551f3b954096": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xbda5747bfd65f08deb54cb465eb87d40e51b197e": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xcd3b766ccdd6ae721141f452c550ca635964ce71": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xdd2fd4581271e230360230f9337d5c0430bf44c0": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xfabb0ac9d68b0b445fb7357272ff202c5651694a": { - "balance": "0xd3c21bcecceda1000000" - } - }, - "number": "0x0" -} diff --git a/crates/client/test-utils/src/accounts.rs b/crates/client/test-utils/src/accounts.rs deleted file mode 100644 index 331bbded..00000000 --- a/crates/client/test-utils/src/accounts.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Test accounts with pre-funded balances for integration testing. - -use alloy_consensus::{SignableTransaction, Transaction}; -use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{Address, Bytes, FixedBytes, TxHash, address, hex}; -use alloy_signer::SignerSync; -use alloy_signer_local::PrivateKeySigner; -use eyre::Result; -use op_alloy_network::TransactionBuilder; -use op_alloy_rpc_types::OpTransactionRequest; -use reth::{revm::context::TransactionType, rpc::compat::SignTxRequestError}; - -use crate::BASE_CHAIN_ID; - -/// Hardcoded test account with a fixed private key -#[derive(Debug, Clone)] -pub struct Account { - /// Account name for easy identification - pub name: &'static str, - /// Ethereum address - pub address: Address, - /// Private key (hex string without 0x prefix) - pub private_key: &'static str, -} - -impl Account { - /// Constructs a signed CREATE transaction with a given nonce and - /// returns the signed bytes, contract address, and transaction hash - pub fn create_deployment_tx( - &self, - bytecode: Bytes, - nonce: u64, - ) -> Result<(Bytes, Address, TxHash)> { - let tx_request = OpTransactionRequest::default() - .from(self.address) - .transaction_type(TransactionType::Eip1559.into()) - .with_gas_limit(3_000_000) // Increased for larger contracts like ERC-20 - .with_max_fee_per_gas(1_000_000_000) - .with_max_priority_fee_per_gas(0) - .with_chain_id(BASE_CHAIN_ID) - .with_deploy_code(bytecode) - .with_nonce(nonce); - - let tx = tx_request - .build_typed_tx() - .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; - let signature = self.signer().sign_hash_sync(&tx.signature_hash())?; - let signed_tx = tx.into_signed(signature); - let signed_tx_bytes = signed_tx.encoded_2718().into(); - - let contract_address = self.address.create(signed_tx.nonce()); - Ok((signed_tx_bytes, contract_address, signed_tx.hash().clone())) - } - - /// Sign a TransactionRequest and return the signed bytes - pub fn sign_txn_request(&self, tx_request: OpTransactionRequest) -> Result<(Bytes, TxHash)> { - let tx_request = tx_request - .from(self.address) - .transaction_type(TransactionType::Eip1559.into()) - .with_gas_limit(500_000) - .with_chain_id(BASE_CHAIN_ID) - .with_max_fee_per_gas(1_000_000_000) - .with_max_priority_fee_per_gas(0); - - let tx = tx_request - .build_typed_tx() - .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; - let signature = self.signer().sign_hash_sync(&tx.signature_hash())?; - let signed_tx = tx.into_signed(signature); - let signed_tx_bytes = signed_tx.encoded_2718().into(); - let tx_hash = signed_tx.hash(); - Ok((signed_tx_bytes, tx_hash.clone())) - } - - /// Constructs and returns a PrivateKeySigner for the TestAccount - pub fn signer(&self) -> PrivateKeySigner { - let key_bytes = - hex::decode(self.private_key).expect("should be able to decode private key"); - let key_fixed: FixedBytes<32> = FixedBytes::from_slice(&key_bytes); - PrivateKeySigner::from_bytes(&key_fixed) - .expect("should be able to build the PrivateKeySigner") - .into() - } -} - -/// Handy alias used throughout tests to refer to the deterministic `Account`. -pub type TestAccount = Account; - -/// Collection of all test accounts -#[derive(Debug, Clone)] -pub struct TestAccounts { - /// Alice (Anvil account #0) with a large starting balance. - pub alice: TestAccount, - /// Bob (Anvil account #1) handy for bilateral tests. - pub bob: TestAccount, - /// Charlie (Anvil account #2) used when three participants are required. - pub charlie: TestAccount, - /// Deterministic account intended for contract deployments. - pub deployer: TestAccount, -} - -impl TestAccounts { - /// Create a new instance with all test accounts - pub fn new() -> Self { - Self { alice: ALICE, bob: BOB, charlie: CHARLIE, deployer: DEPLOYER } - } - - /// Get all accounts as a vector - pub fn all(&self) -> Vec<&TestAccount> { - vec![&self.alice, &self.bob, &self.charlie, &self.deployer] - } - - /// Get account by name - pub fn get(&self, name: &str) -> Option<&TestAccount> { - match name { - "alice" => Some(&self.alice), - "bob" => Some(&self.bob), - "charlie" => Some(&self.charlie), - "deployer" => Some(&self.deployer), - _ => None, - } - } -} - -impl Default for TestAccounts { - fn default() -> Self { - Self::new() - } -} - -// Hardcoded test accounts using Anvil's deterministic keys -// These are derived from the test mnemonic: "test test test test test test test test test test test junk" - -/// Alice - First test account (Anvil account #0) -pub const ALICE: TestAccount = TestAccount { - name: "Alice", - address: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - private_key: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", -}; - -/// Bob - Second test account (Anvil account #1) -pub const BOB: TestAccount = TestAccount { - name: "Bob", - address: address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"), - private_key: "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", -}; - -/// Charlie - Third test account (Anvil account #2) -pub const CHARLIE: TestAccount = TestAccount { - name: "Charlie", - address: address!("3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), - private_key: "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", -}; - -/// Deployer - Account for deploying smart contracts (Anvil account #3) -pub const DEPLOYER: TestAccount = TestAccount { - name: "Deployer", - address: address!("90F79bf6EB2c4f870365E785982E1f101E93b906"), - private_key: "7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", -}; diff --git a/crates/client/test-utils/src/constants.rs b/crates/client/test-utils/src/constants.rs deleted file mode 100644 index c2855e65..00000000 --- a/crates/client/test-utils/src/constants.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Shared constants used across integration tests. -//! -//! This module centralizes configuration values and magic constants to avoid -//! duplication and make them easy to discover. - -use alloy_primitives::{B256, Bytes, b256, bytes}; -// Re-export NamedChain for convenient access to chain IDs. -pub use reth::chainspec::NamedChain; - -// ============================================================================= -// Chain Configuration -// ============================================================================= - -/// Chain ID for the Base Sepolia environment spun up by the harness. -/// -/// This is equivalent to `NamedChain::BaseSepolia as u64`. -pub const BASE_CHAIN_ID: u64 = NamedChain::BaseSepolia as u64; - -// ============================================================================= -// Block Building -// ============================================================================= - -/// Time between blocks in seconds. -pub const BLOCK_TIME_SECONDS: u64 = 2; - -/// Gas limit for blocks built by the harness. -pub const GAS_LIMIT: u64 = 200_000_000; - -// ============================================================================= -// Timing / Delays -// ============================================================================= - -/// Delay after node startup before the harness is ready. -pub const NODE_STARTUP_DELAY_MS: u64 = 500; - -/// Delay between requesting and fetching a payload during block building. -pub const BLOCK_BUILD_DELAY_MS: u64 = 100; - -// ============================================================================= -// Engine API -// ============================================================================= - -/// Default JWT secret for Engine API authentication in tests. -/// -/// This is an all-zeros secret used only for local testing. -pub const DEFAULT_JWT_SECRET: &str = - "0x0000000000000000000000000000000000000000000000000000000000000000"; - -// ============================================================================= -// L1 Block Info (OP Stack) -// ============================================================================= - -/// Pre-captured L1 block info deposit transaction required by OP Stack. -/// -/// Every OP Stack block must start with an L1 block info deposit. This is a -/// sample transaction suitable for Base Sepolia tests. -pub const L1_BLOCK_INFO_DEPOSIT_TX: Bytes = bytes!( - "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" -); - -/// Hash of the L1 block info deposit transaction. -pub const L1_BLOCK_INFO_DEPOSIT_TX_HASH: B256 = - b256!("0xba56c8b0deb460ff070f8fca8e2ee01e51a3db27841cc862fdd94cc1a47662b6"); diff --git a/crates/client/test-utils/src/flashblocks_harness.rs b/crates/client/test-utils/src/flashblocks_harness.rs deleted file mode 100644 index a1acaa2c..00000000 --- a/crates/client/test-utils/src/flashblocks_harness.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Flashblocks-aware wrapper around [`TestHarness`] that wires in the custom RPC modules. - -use std::sync::Arc; - -use base_flashtypes::Flashblock; -use derive_more::Deref; -use eyre::Result; -use futures_util::Future; -use reth::builder::NodeHandle; -use reth_e2e_test_utils::Adapter; -use reth_optimism_node::OpNode; - -use crate::{ - harness::TestHarness, - init_silenced_tracing, - node::{ - FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, OpAddOns, OpBuilder, - default_launcher, - }, -}; - -/// Helper that exposes [`TestHarness`] conveniences plus Flashblocks helpers. -#[derive(Debug, Deref)] -pub struct FlashblocksHarness { - #[deref] - inner: TestHarness, - parts: FlashblocksParts, -} - -impl FlashblocksHarness { - /// Launch a flashblocks-enabled harness with the default launcher. - pub async fn new() -> Result { - Self::with_launcher(default_launcher).await - } - - /// Launch the harness configured for manual canonical progression. - pub async fn manual_canonical() -> Result { - Self::manual_canonical_with_launcher(default_launcher).await - } - - /// Launch the harness using a custom node launcher. - pub async fn with_launcher(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - init_silenced_tracing(); - let flash_node = FlashblocksLocalNode::with_launcher(launcher).await?; - Self::from_flashblocks_node(flash_node).await - } - - /// Launch the harness with a custom launcher while disabling automatic canonical processing. - pub async fn manual_canonical_with_launcher(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - init_silenced_tracing(); - let flash_node = FlashblocksLocalNode::with_manual_canonical_launcher(launcher).await?; - Self::from_flashblocks_node(flash_node).await - } - - /// Get a handle to the in-memory Flashblocks state backing the harness. - pub fn flashblocks_state(&self) -> Arc { - self.parts.state() - } - - /// Send a single flashblock through the harness. - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.parts.send(flashblock).await - } - - /// Send a batch of flashblocks sequentially, awaiting each confirmation. - pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> - where - I: IntoIterator, - { - for flashblock in flashblocks { - self.send_flashblock(flashblock).await?; - } - Ok(()) - } - - /// Consume the flashblocks harness and return the underlying [`TestHarness`]. - pub fn into_inner(self) -> TestHarness { - self.inner - } - - async fn from_flashblocks_node(flash_node: FlashblocksLocalNode) -> Result { - let (node, parts) = flash_node.into_parts(); - let inner = TestHarness::from_node(node).await?; - Ok(Self { inner, parts }) - } -} diff --git a/crates/client/test-utils/src/lib.rs b/crates/client/test-utils/src/lib.rs deleted file mode 100644 index df8fc534..00000000 --- a/crates/client/test-utils/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![doc = include_str!("../README.md")] -#![doc(issue_tracker_base_url = "https://github.com/base/node-reth/issues/")] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] - -mod accounts; -pub use accounts::{ALICE, Account, BOB, CHARLIE, DEPLOYER, TestAccount, TestAccounts}; - -mod constants; -pub use constants::{ - BASE_CHAIN_ID, BLOCK_BUILD_DELAY_MS, BLOCK_TIME_SECONDS, DEFAULT_JWT_SECRET, GAS_LIMIT, - L1_BLOCK_INFO_DEPOSIT_TX, L1_BLOCK_INFO_DEPOSIT_TX_HASH, NODE_STARTUP_DELAY_MS, NamedChain, -}; - -mod contracts; -pub use contracts::{DoubleCounter, Minimal7702Account, MockERC20, TransparentUpgradeableProxy}; - -mod engine; -pub use engine::{EngineAddress, EngineApi, EngineProtocol, HttpEngine, IpcEngine}; - -mod fixtures; -pub use fixtures::{create_provider_factory, load_genesis}; - -mod flashblocks_harness; -pub use flashblocks_harness::FlashblocksHarness; - -mod harness; -pub use harness::TestHarness; - -mod node; -pub use node::{ - FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, LocalNode, LocalNodeProvider, - OpAddOns, OpBuilder, OpComponentsBuilder, OpTypes, default_launcher, -}; - -mod tracing; -// Re-export signer traits for use in tests -pub use alloy_signer::SignerSync; -pub use tracing::init_silenced_tracing; diff --git a/crates/client/test-utils/src/node.rs b/crates/client/test-utils/src/node.rs deleted file mode 100644 index b943abe2..00000000 --- a/crates/client/test-utils/src/node.rs +++ /dev/null @@ -1,467 +0,0 @@ -//! Local node setup with Base Sepolia chainspec - -use std::{ - any::Any, - fmt, - net::SocketAddr, - sync::{Arc, Mutex}, -}; - -use alloy_genesis::Genesis; -use alloy_provider::RootProvider; -use alloy_rpc_client::RpcClient; -use base_flashtypes::Flashblock; -use base_reth_flashblocks::{ - EthApiExt, EthApiOverrideServer, EthPubSub, EthPubSubApiServer, FlashblocksReceiver, - FlashblocksState, -}; -use eyre::Result; -use futures_util::Future; -use once_cell::sync::OnceCell; -use op_alloy_network::Optimism; -use reth::{ - api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}, - args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, - builder::{ - Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, - }, - core::exit::NodeExitFuture, - tasks::TaskManager, -}; -use reth_db::{ - ClientVersion, DatabaseEnv, init_db, - mdbx::DatabaseArguments, - test_utils::{ERROR_DB_CREATION, TempDatabase, tempdir_path}, -}; -use reth_e2e_test_utils::{Adapter, TmpDB}; -use reth_exex::ExExEvent; -use reth_node_core::{ - args::DatadirArgs, - dirs::{DataDirPath, MaybePlatformPath}, -}; -use reth_optimism_chainspec::OpChainSpec; -use reth_optimism_node::{OpNode, args::RollupArgs}; -use reth_provider::{CanonStateSubscriptions, providers::BlockchainProvider}; -use tokio::sync::{mpsc, oneshot}; -use tokio_stream::StreamExt; - -use crate::engine::EngineApi; - -/// Convenience alias for the local blockchain provider type. -pub type LocalNodeProvider = BlockchainProvider>; -/// Convenience alias for the Flashblocks state backing the local node. -pub type LocalFlashblocksState = FlashblocksState; - -/// Handle to a launched local node along with the resources required to keep it alive. -pub struct LocalNode { - pub(crate) http_api_addr: SocketAddr, - engine_ipc_path: String, - pub(crate) ws_api_addr: SocketAddr, - provider: LocalNodeProvider, - _node_exit_future: NodeExitFuture, - _node: Box, - _task_manager: TaskManager, -} - -impl fmt::Debug for LocalNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LocalNode") - .field("http_api_addr", &self.http_api_addr) - .field("ws_api_addr", &self.ws_api_addr) - .field("engine_ipc_path", &self.engine_ipc_path) - .finish_non_exhaustive() - } -} - -/// Components that allow tests to interact with the Flashblocks worker tasks. -#[derive(Clone)] -pub struct FlashblocksParts { - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - state: Arc, -} - -impl fmt::Debug for FlashblocksParts { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FlashblocksParts").finish_non_exhaustive() - } -} - -impl FlashblocksParts { - /// Clone the shared [`FlashblocksState`] handle. - pub fn state(&self) -> Arc { - self.state.clone() - } - - /// Send a flashblock to the background processor and wait until it is handled. - pub async fn send(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; - rx.await.map_err(|err| eyre::eyre!(err))?; - Ok(()) - } -} - -#[derive(Clone)] -struct FlashblocksNodeExtensions { - inner: Arc, -} - -struct FlashblocksNodeExtensionsInner { - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - #[allow(clippy::type_complexity)] - receiver: Arc)>>>>, - fb_cell: Arc>>, - process_canonical: bool, -} - -impl FlashblocksNodeExtensions { - fn new(process_canonical: bool) -> Self { - let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let inner = FlashblocksNodeExtensionsInner { - sender, - receiver: Arc::new(Mutex::new(Some(receiver))), - fb_cell: Arc::new(OnceCell::new()), - process_canonical, - }; - Self { inner: Arc::new(inner) } - } - - fn apply(&self, builder: OpBuilder) -> OpBuilder { - let fb_cell = self.inner.fb_cell.clone(); - let receiver = self.inner.receiver.clone(); - let process_canonical = self.inner.process_canonical; - - let fb_cell_for_exex = fb_cell.clone(); - - builder - .install_exex("flashblocks-canon", move |mut ctx| { - let fb_cell = fb_cell_for_exex.clone(); - let process_canonical = process_canonical; - async move { - let provider = ctx.provider().clone(); - let fb = init_flashblocks_state(&fb_cell, &provider); - Ok(async move { - while let Some(note) = ctx.notifications.try_next().await? { - if let Some(committed) = note.committed_chain() { - let hash = committed.tip().num_hash(); - if process_canonical { - // Many suites drive canonical updates manually to reproduce race conditions, so - // allowing this to be disabled keeps canonical replay deterministic. - let chain = Arc::unwrap_or_clone(committed); - for (_, block) in chain.into_blocks() { - fb.on_canonical_block_received(block); - } - } - let _ = ctx.events.send(ExExEvent::FinishedHeight(hash)); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules(move |ctx| { - let fb_cell = fb_cell.clone(); - let provider = ctx.provider().clone(); - let fb = init_flashblocks_state(&fb_cell, &provider); - - let mut canon_stream = tokio_stream::wrappers::BroadcastStream::new( - ctx.provider().subscribe_to_canonical_state(), - ); - tokio::spawn(async move { - use tokio_stream::StreamExt; - while let Some(Ok(notification)) = canon_stream.next().await { - provider.canonical_in_memory_state().notify_canon_state(notification); - } - }); - let api_ext = EthApiExt::new( - ctx.registry.eth_api().clone(), - ctx.registry.eth_handlers().filter.clone(), - fb.clone(), - ); - ctx.modules.replace_configured(api_ext.into_rpc())?; - - // Register eth_subscribe subscription endpoint for flashblocks - // Uses replace_configured since eth_subscribe already exists from reth's standard module - // Pass eth_api to enable proxying standard subscription types to reth's implementation - let eth_pubsub = EthPubSub::new(ctx.registry.eth_api().clone(), fb.clone()); - ctx.modules.replace_configured(eth_pubsub.into_rpc())?; - - let fb_for_task = fb.clone(); - let mut receiver = receiver - .lock() - .expect("flashblock receiver mutex poisoned") - .take() - .expect("flashblock receiver should only be initialized once"); - tokio::spawn(async move { - while let Some((payload, tx)) = receiver.recv().await { - fb_for_task.on_flashblock_received(payload); - let _ = tx.send(()); - } - }); - - Ok(()) - }) - } - - fn wrap_launcher(&self, launcher: L) -> impl FnOnce(OpBuilder) -> LRet - where - L: FnOnce(OpBuilder) -> LRet, - { - let extensions = self.clone(); - move |builder| { - let builder = extensions.apply(builder); - launcher(builder) - } - } - - fn parts(&self) -> Result { - let state = self.inner.fb_cell.get().ok_or_else(|| { - eyre::eyre!("FlashblocksState should be initialized during node launch") - })?; - Ok(FlashblocksParts { sender: self.inner.sender.clone(), state: state.clone() }) - } -} - -/// Optimism node types used for the local harness. -pub type OpTypes = - FullNodeTypesAdapter>>; -/// Builder that wires up the concrete node components. -pub type OpComponentsBuilder = >::ComponentsBuilder; -/// Additional services attached to the node builder. -pub type OpAddOns = >::AddOns; -/// Launcher builder used by the harness to customize node startup. -pub type OpBuilder = - WithLaunchContext>; - -/// Default launcher that is reused across the harness and integration tests. -pub async fn default_launcher( - builder: OpBuilder, -) -> eyre::Result, OpAddOns>> { - let launcher = builder.engine_api_launcher(); - builder.launch_with(launcher).await -} - -impl LocalNode { - /// Launch a new local node using the provided launcher function. - pub async fn new(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - build_node(launcher).await - } - - /// Creates a test database with a smaller map size to reduce memory usage. - /// - /// Unlike `NodeBuilder::testing_node()` which hardcodes an 8 TB map size, - /// this method configures the database with a 100 MB map size. This prevents - /// `ENOMEM` errors when running parallel tests with `cargo test`, as the - /// default 8 TB size can cause memory exhaustion when multiple test processes - /// run concurrently. - fn create_test_database() -> Result>> { - let default_size = 100 * 1024 * 1024; // 100 MB - Self::create_test_database_with_size(default_size) - } - - /// Creates a test database with a configurable map size to reduce memory usage. - /// - /// # Arguments - /// - /// * `max_size` - Maximum map size in bytes. - fn create_test_database_with_size(max_size: usize) -> Result>> { - let path = tempdir_path(); - let emsg = format!("{ERROR_DB_CREATION}: {path:?}"); - let args = - DatabaseArguments::new(ClientVersion::default()).with_geometry_max_size(Some(max_size)); - let db = init_db(&path, args).expect(&emsg); - Ok(Arc::new(TempDatabase::new(db, path))) - } - - /// Create an HTTP provider pointed at the node's public RPC endpoint. - pub fn provider(&self) -> Result> { - let url = format!("http://{}", self.http_api_addr); - let client = RpcClient::builder().http(url.parse()?); - Ok(RootProvider::::new(client)) - } - - /// Build an Engine API client that talks to the node's IPC endpoint. - pub fn engine_api(&self) -> Result> { - EngineApi::::new(self.engine_ipc_path.clone()) - } - - /// Clone the underlying blockchain provider so callers can inspect chain state. - pub fn blockchain_provider(&self) -> LocalNodeProvider { - self.provider.clone() - } - - /// Websocket URL for the local node. - pub fn ws_url(&self) -> String { - format!("ws://{}", self.ws_api_addr) - } -} - -async fn build_node(launcher: L) -> Result -where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, -{ - let tasks = TaskManager::current(); - let exec = tasks.executor(); - - let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json"))?; - let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); - - let network_config = NetworkArgs { - discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, - ..NetworkArgs::default() - }; - - let unique_ipc_path = format!( - "/tmp/reth_engine_api_{}_{}_{:?}.ipc", - std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(), - std::process::id(), - std::thread::current().id() - ); - - let mut rpc_args = - RpcServerArgs::default().with_unused_ports().with_http().with_auth_ipc().with_ws(); - rpc_args.auth_ipc_path = unique_ipc_path; - - let node = OpNode::new(RollupArgs::default()); - - let temp_db = LocalNode::create_test_database()?; - let db_path = temp_db.path().to_path_buf(); - - let mut node_config = NodeConfig::new(chain_spec.clone()) - .with_network(network_config) - .with_rpc(rpc_args) - .with_unused_ports(); - - let datadir_path = MaybePlatformPath::::from(db_path.clone()); - node_config = - node_config.with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); - - let builder = NodeBuilder::new(node_config.clone()) - .with_database(temp_db) - .with_launch_context(exec.clone()) - .with_types_and_provider::>() - .with_components(node.components_builder()) - .with_add_ons(node.add_ons()); - - let NodeHandle { node: node_handle, node_exit_future } = - builder.launch_with_fn(launcher).await?; - - let http_api_addr = node_handle - .rpc_server_handle() - .http_local_addr() - .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; - - let ws_api_addr = node_handle - .rpc_server_handle() - .ws_local_addr() - .ok_or_else(|| eyre::eyre!("Failed to get websocket api address"))?; - - let engine_ipc_path = node_config.rpc.auth_ipc_path; - let provider = node_handle.provider().clone(); - - Ok(LocalNode { - http_api_addr, - ws_api_addr, - engine_ipc_path, - provider, - _node_exit_future: node_exit_future, - _node: Box::new(node_handle), - _task_manager: tasks, - }) -} - -fn init_flashblocks_state( - cell: &Arc>>, - provider: &LocalNodeProvider, -) -> Arc { - cell.get_or_init(|| { - let fb = Arc::new(FlashblocksState::new(provider.clone(), 5)); - fb.start(); - fb - }) - .clone() -} - -/// Local node wrapper that exposes helpers specific to Flashblocks tests. -pub struct FlashblocksLocalNode { - node: LocalNode, - parts: FlashblocksParts, -} - -impl fmt::Debug for FlashblocksLocalNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FlashblocksLocalNode") - .field("node", &self.node) - .field("parts", &self.parts) - .finish() - } -} - -impl FlashblocksLocalNode { - /// Launch a flashblocks-enabled node using the default launcher. - pub async fn new() -> Result { - Self::with_launcher(default_launcher).await - } - - /// Builds a flashblocks-enabled node with canonical block streaming disabled so tests can call - /// `FlashblocksState::on_canonical_block_received` at precise points. - pub async fn manual_canonical() -> Result { - Self::with_manual_canonical_launcher(default_launcher).await - } - - /// Launch a flashblocks-enabled node with a custom launcher and canonical processing enabled. - pub async fn with_launcher(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - Self::with_launcher_inner(launcher, true).await - } - - /// Same as [`Self::with_launcher`] but leaves canonical processing to the caller. - pub async fn with_manual_canonical_launcher(launcher: L) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - Self::with_launcher_inner(launcher, false).await - } - - async fn with_launcher_inner(launcher: L, process_canonical: bool) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - let extensions = FlashblocksNodeExtensions::new(process_canonical); - let wrapped_launcher = extensions.wrap_launcher(launcher); - let node = LocalNode::new(wrapped_launcher).await?; - - let parts = extensions.parts()?; - Ok(Self { node, parts }) - } - - /// Access the shared Flashblocks state for assertions or manual driving. - pub fn flashblocks_state(&self) -> Arc { - self.parts.state() - } - - /// Send a flashblock through the background processor and await completion. - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.parts.send(flashblock).await - } - - /// Split the wrapper into the underlying node plus flashblocks parts. - pub fn into_parts(self) -> (LocalNode, FlashblocksParts) { - (self.node, self.parts) - } - - /// Borrow the underlying [`LocalNode`]. - pub fn as_node(&self) -> &LocalNode { - &self.node - } -} diff --git a/crates/client/txpool/Cargo.toml b/crates/client/txpool/Cargo.toml index 5fe839b5..8a031068 100644 --- a/crates/client/txpool/Cargo.toml +++ b/crates/client/txpool/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] # workspace -base-primitives.workspace = true +base-client-node.workspace = true # reth reth.workspace = true diff --git a/crates/client/txpool/README.md b/crates/client/txpool/README.md index 3dbc4aa0..2ae97087 100644 --- a/crates/client/txpool/README.md +++ b/crates/client/txpool/README.md @@ -24,13 +24,18 @@ cargo run -p node --release -- \ --enable-transaction-tracing-logs # optional: emit per-tx lifecycle logs ``` -From code, wire the ExEx into the node builder: +From code, wire the extension into the node builder: ```rust,ignore -use base_reth_runner::{TracingConfig, extensions::TransactionTracingExtension}; - -let tracing = TracingConfig { enabled: true, logs_enabled: true }; -let builder = TransactionTracingExtension::new(tracing).apply(builder); +use base_txpool::{TxpoolConfig, TxPoolExtension}; + +let config = TxpoolConfig { + tracing_enabled: true, + tracing_logs_enabled: true, + sequencer_rpc: None, +}; +let ext = TxPoolExtension::new(config); +let builder = Box::new(ext).apply(builder); ``` ## Metrics diff --git a/crates/client/txpool/src/extension.rs b/crates/client/txpool/src/extension.rs index f8bdce28..3800db71 100644 --- a/crates/client/txpool/src/extension.rs +++ b/crates/client/txpool/src/extension.rs @@ -1,44 +1,64 @@ -//! Contains the [`TransactionTracingExtension`] which wires up the `tracex` -//! execution extension on the Base node builder. +//! Contains the [`TxPoolExtension`] which wires up the transaction pool features +//! (tracing ExEx and status RPC) on the Base node builder. -use base_primitives::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder, TracingConfig}; +use base_client_node::{BaseNodeExtension, FromExtensionConfig, OpBuilder}; +use tracing::info; -use crate::tracex_exex; +use crate::{TransactionStatusApiImpl, TransactionStatusApiServer, tracex_exex}; -/// Helper struct that wires the transaction tracing ExEx into the node builder. -#[derive(Debug, Clone, Copy)] -pub struct TransactionTracingExtension { - /// Transaction tracing configuration flags. - pub config: TracingConfig, +/// Transaction pool configuration. +#[derive(Debug, Clone)] +pub struct TxpoolConfig { + /// Enables the transaction tracing ExEx. + pub tracing_enabled: bool, + /// Emits `info`-level logs for the tracing ExEx when enabled. + pub tracing_logs_enabled: bool, + /// Sequencer RPC endpoint for transaction status proxying. + pub sequencer_rpc: Option, } -impl TransactionTracingExtension { - /// Creates a new transaction tracing extension helper. - pub const fn new(config: TracingConfig) -> Self { +/// Helper struct that wires the transaction pool features into the node builder. +#[derive(Debug, Clone)] +pub struct TxPoolExtension { + /// Transaction pool configuration. + config: TxpoolConfig, +} + +impl TxPoolExtension { + /// Creates a new transaction pool extension helper. + pub const fn new(config: TxpoolConfig) -> Self { Self { config } } } -impl BaseNodeExtension for TransactionTracingExtension { +impl BaseNodeExtension for TxPoolExtension { /// Applies the extension to the supplied builder. fn apply(self: Box, builder: OpBuilder) -> OpBuilder { - let tracing = self.config; - builder.install_exex_if(tracing.enabled, "tracex", move |ctx| async move { - Ok(tracex_exex(ctx, tracing.logs_enabled)) + let config = self.config; + + // Install the tracing ExEx if enabled + let logs_enabled = config.tracing_logs_enabled; + let builder = + builder.install_exex_if(config.tracing_enabled, "tracex", move |ctx| async move { + Ok(tracex_exex(ctx, logs_enabled)) + }); + + // Extend with RPC modules + let sequencer_rpc = config.sequencer_rpc; + builder.extend_rpc_modules(move |ctx| { + info!(message = "Starting Transaction Status RPC"); + let proxy_api = TransactionStatusApiImpl::new(sequencer_rpc, ctx.pool().clone()) + .expect("Failed to create transaction status proxy"); + ctx.modules.merge_configured(proxy_api.into_rpc())?; + Ok(()) }) } } -/// Configuration trait for [`TransactionTracingExtension`]. -/// -/// Types implementing this trait can be used to construct a [`TransactionTracingExtension`]. -pub trait TransactionTracingConfig { - /// Returns the tracing configuration. - fn tracing(&self) -> &TracingConfig; -} +impl FromExtensionConfig for TxPoolExtension { + type Config = TxpoolConfig; -impl ConfigurableBaseNodeExtension for TransactionTracingExtension { - fn build(config: &C) -> eyre::Result { - Ok(Self::new(*config.tracing())) + fn from_config(config: Self::Config) -> Self { + Self::new(config) } } diff --git a/crates/client/txpool/src/lib.rs b/crates/client/txpool/src/lib.rs index ab3577d3..3757caa4 100644 --- a/crates/client/txpool/src/lib.rs +++ b/crates/client/txpool/src/lib.rs @@ -14,11 +14,8 @@ pub use rpc::{ Status, TransactionStatusApiImpl, TransactionStatusApiServer, TransactionStatusResponse, }; -mod rpc_extension; -pub use rpc_extension::{TransactionStatusRpcConfig, TransactionStatusRpcExtension}; - mod tracker; pub use tracker::Tracker; mod extension; -pub use extension::{TransactionTracingConfig, TransactionTracingExtension}; +pub use extension::{TxPoolExtension, TxpoolConfig}; diff --git a/crates/client/txpool/src/rpc_extension.rs b/crates/client/txpool/src/rpc_extension.rs deleted file mode 100644 index 3c3270e2..00000000 --- a/crates/client/txpool/src/rpc_extension.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Contains the [`TransactionStatusRpcExtension`] which wires up the transaction status -//! RPC surface on the Base node builder. - -use base_primitives::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder}; - -use crate::{TransactionStatusApiImpl, TransactionStatusApiServer}; - -/// Helper struct that wires the transaction status RPC into the node builder. -#[derive(Debug, Clone)] -pub struct TransactionStatusRpcExtension { - /// Sequencer RPC endpoint for transaction status proxying. - pub sequencer_rpc: Option, -} - -impl TransactionStatusRpcExtension { - /// Creates a new transaction status RPC extension. - pub const fn new(sequencer_rpc: Option) -> Self { - Self { sequencer_rpc } - } -} - -impl BaseNodeExtension for TransactionStatusRpcExtension { - /// Applies the extension to the supplied builder. - fn apply(self: Box, builder: OpBuilder) -> OpBuilder { - let sequencer_rpc = self.sequencer_rpc; - - builder.extend_rpc_modules(move |ctx| { - let proxy_api = TransactionStatusApiImpl::new(sequencer_rpc, ctx.pool().clone()) - .expect("Failed to create transaction status proxy"); - ctx.modules.merge_configured(proxy_api.into_rpc())?; - Ok(()) - }) - } -} - -/// Configuration trait for [`TransactionStatusRpcExtension`]. -/// -/// Types implementing this trait can be used to construct a [`TransactionStatusRpcExtension`]. -pub trait TransactionStatusRpcConfig { - /// Returns the sequencer RPC URL if configured. - fn sequencer_rpc(&self) -> Option<&str>; -} - -impl ConfigurableBaseNodeExtension - for TransactionStatusRpcExtension -{ - fn build(config: &C) -> eyre::Result { - Ok(Self::new(config.sequencer_rpc().map(String::from))) - } -} diff --git a/crates/shared/access-lists/Cargo.toml b/crates/shared/access-lists/Cargo.toml index 2a0828d6..a392db97 100644 --- a/crates/shared/access-lists/Cargo.toml +++ b/crates/shared/access-lists/Cargo.toml @@ -22,14 +22,13 @@ serde.workspace = true [dev-dependencies] op-revm.workspace = true eyre.workspace = true -reth-optimism-chainspec.workspace = true reth-optimism-evm.workspace = true +reth-optimism-chainspec.workspace = true alloy-consensus.workspace = true alloy-contract.workspace = true -alloy-sol-macro = { workspace = true, features = ["json"] } alloy-sol-types.workspace = true reth-evm.workspace = true -serde_json.workspace = true +base-primitives = { workspace = true, features = ["test-utils"] } [[test]] name = "builder" diff --git a/crates/shared/access-lists/tests/builder/deployment.rs b/crates/shared/access-lists/tests/builder/deployment.rs index ea25add6..b4f0b923 100644 --- a/crates/shared/access-lists/tests/builder/deployment.rs +++ b/crates/shared/access-lists/tests/builder/deployment.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use super::{ - AccountInfo, B256, BASE_SEPOLIA_CHAIN_ID, Bytecode, ContractFactory, IntoAddress, ONE_ETHER, + AccountInfo, B256, Bytecode, ContractFactory, DEVNET_CHAIN_ID, IntoAddress, ONE_ETHER, OpTransaction, SimpleStorage, SolCall, TxEnv, TxKind, U256, execute_txns_build_access_list, }; @@ -30,7 +30,7 @@ fn test_create_deployment_tracked() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(factory)) .data( ContractFactory::deployWithCreateCall { @@ -96,7 +96,7 @@ fn test_create2_deployment_tracked() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(factory)) .data( ContractFactory::deployWithCreate2Call { @@ -160,7 +160,7 @@ fn test_create_and_immediate_call() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(factory)) .data( ContractFactory::deployAndCallCall { diff --git a/crates/shared/access-lists/tests/builder/main.rs b/crates/shared/access-lists/tests/builder/main.rs index 04d94e6d..fbcc9b21 100644 --- a/crates/shared/access-lists/tests/builder/main.rs +++ b/crates/shared/access-lists/tests/builder/main.rs @@ -4,10 +4,12 @@ use std::{collections::HashMap, sync::Arc}; use alloy_consensus::Header; pub use alloy_primitives::{Address, B256, TxKind, U256}; -use alloy_sol_macro::sol; pub use alloy_sol_types::SolCall; use base_access_lists::FBALBuilderDb; pub use base_access_lists::FlashblockAccessList; +use base_primitives::{ + AccessListContract, ContractFactory, DEVNET_CHAIN_ID, SimpleStorage, build_test_genesis, +}; pub use eyre::Result; pub use op_revm::OpTransaction; use reth_evm::{ConfigureEvm, Evm}; @@ -26,62 +28,10 @@ mod deployment; mod storage; mod transfers; -sol!( - #[sol(rpc)] - AccessListContract, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/AccessList.sol/AccessList.json" - ) -); - -sol!( - #[sol(rpc)] - ContractFactory, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/ContractFactory.sol/ContractFactory.json" - ) -); - -sol!( - #[sol(rpc)] - SimpleStorage, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/ContractFactory.sol/SimpleStorage.json" - ) -); - -sol!( - #[sol(rpc)] - Proxy, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/Proxy.sol/Proxy.json" - ) -); - -sol!( - #[sol(rpc)] - Logic, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/Proxy.sol/Logic.json" - ) -); - -sol!( - #[sol(rpc)] - Logic2, - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../client/test-utils/contracts/out/Proxy.sol/Logic2.json" - ) -); - -/// Chain ID for Base Sepolia -pub const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; +/// Loads the test chain spec from the genesis configuration. +fn load_chain_spec() -> Arc { + Arc::new(OpChainSpec::from_genesis(build_test_genesis())) +} /// Executes a list of transactions and builds a FlashblockAccessList tracking all /// account and storage changes across all transactions. @@ -93,10 +43,7 @@ pub fn execute_txns_build_access_list( acc_overrides: Option>, storage_overrides: Option>>, ) -> Result { - let chain_spec = Arc::new(OpChainSpec::from_genesis( - serde_json::from_str(include_str!("../../../../client/test-utils/assets/genesis.json")) - .unwrap(), - )); + let chain_spec = load_chain_spec(); let evm_config = OpEvmConfig::optimism(chain_spec.clone()); let header = Header { base_fee_per_gas: Some(0), ..chain_spec.genesis_header().clone() }; diff --git a/crates/shared/access-lists/tests/builder/storage.rs b/crates/shared/access-lists/tests/builder/storage.rs index 98ba221b..8f40bb11 100644 --- a/crates/shared/access-lists/tests/builder/storage.rs +++ b/crates/shared/access-lists/tests/builder/storage.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use super::{ - AccessListContract, AccountInfo, BASE_SEPOLIA_CHAIN_ID, Bytecode, IntoAddress, ONE_ETHER, + AccessListContract, AccountInfo, Bytecode, DEVNET_CHAIN_ID, IntoAddress, ONE_ETHER, OpTransaction, SolCall, TxEnv, TxKind, U256, execute_txns_build_access_list, }; @@ -24,7 +24,7 @@ fn test_sload_zero_value() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(contract)) .data(AccessListContract::valueCall {}.abi_encode().into()) .gas_price(0) @@ -67,7 +67,7 @@ fn test_update_one_value() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(contract)) .data( AccessListContract::updateValueCall { newValue: U256::from(42) } @@ -87,7 +87,7 @@ fn test_update_one_value() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(contract)) .data(AccessListContract::valueCall {}.abi_encode().into()) .nonce(1) @@ -147,7 +147,7 @@ fn test_multi_sload_same_slot() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(contract)) .data(AccessListContract::getABCall {}.abi_encode().into()) .nonce(0) @@ -193,7 +193,7 @@ fn test_multi_sstore() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(contract)) .data( AccessListContract::insertMultipleCall { diff --git a/crates/shared/access-lists/tests/builder/transfers.rs b/crates/shared/access-lists/tests/builder/transfers.rs index 87157f5f..68306b10 100644 --- a/crates/shared/access-lists/tests/builder/transfers.rs +++ b/crates/shared/access-lists/tests/builder/transfers.rs @@ -3,15 +3,14 @@ use std::collections::HashMap; use super::{ - AccountInfo, BASE_SEPOLIA_CHAIN_ID, IntoAddress, ONE_ETHER, OpTransaction, TxEnv, TxKind, U256, + AccountInfo, DEVNET_CHAIN_ID, IntoAddress, ONE_ETHER, OpTransaction, TxEnv, TxKind, U256, execute_txns_build_access_list, }; #[test] /// Tests that the system precompiles get included in the access list fn test_precompiles() { - let base_tx = - TxEnv::builder().chain_id(Some(BASE_SEPOLIA_CHAIN_ID)).gas_limit(50_000).gas_price(0); + let base_tx = TxEnv::builder().chain_id(Some(DEVNET_CHAIN_ID)).gas_limit(50_000).gas_price(0); let tx = OpTransaction::builder().base(base_tx).build_fill(); let access_list = execute_txns_build_access_list(vec![tx], None, None) .expect("access list build should succeed"); @@ -32,7 +31,7 @@ fn test_single_transfer() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(recipient)) .value(U256::from(1_000_000)) .gas_price(0) @@ -73,7 +72,7 @@ fn test_gas_included_in_balance_change() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .kind(TxKind::Call(recipient)) .value(U256::from(1_000_000)) .gas_price(1000) @@ -118,7 +117,7 @@ fn test_multiple_transfers() { .base( TxEnv::builder() .caller(sender) - .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .chain_id(Some(DEVNET_CHAIN_ID)) .nonce(i) .kind(TxKind::Call(recipient)) .value(U256::from(1_000_000)) diff --git a/crates/shared/primitives/Cargo.toml b/crates/shared/primitives/Cargo.toml new file mode 100644 index 00000000..2a38b5a7 --- /dev/null +++ b/crates/shared/primitives/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "base-primitives" +description = "Shared primitives and test utilities for node-reth" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[features] +default = [] +test-utils = [ + "dep:alloy-consensus", + "dep:alloy-contract", + "dep:alloy-eips", + "dep:alloy-genesis", + "dep:alloy-signer", + "dep:alloy-signer-local", + "dep:alloy-sol-macro", + "dep:alloy-sol-types", + "dep:eyre", + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "dep:serde_json", +] + +[dependencies] +alloy-primitives = { workspace = true, features = ["serde"] } + +# test-utils (optional) +alloy-consensus = { workspace = true, features = ["std"], optional = true } +alloy-contract = { workspace = true, optional = true } +alloy-eips = { workspace = true, optional = true } +alloy-genesis = { workspace = true, optional = true } +alloy-signer = { workspace = true, optional = true } +alloy-signer-local = { workspace = true, optional = true } +alloy-sol-macro = { workspace = true, features = ["json"], optional = true } +alloy-sol-types = { workspace = true, optional = true } +eyre = { workspace = true, optional = true } +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } diff --git a/crates/shared/primitives/README.md b/crates/shared/primitives/README.md new file mode 100644 index 00000000..d6e37ce3 --- /dev/null +++ b/crates/shared/primitives/README.md @@ -0,0 +1,26 @@ +# `base-primitives` + +CI +MIT License + +Shared primitives and test utilities for node-reth crates. + +## Usage + +Add the dependency to your `Cargo.toml`: + +```toml +[dependencies] +base-primitives = { git = "https://github.com/base/node-reth" } +``` + +For test utilities: + +```toml +[dev-dependencies] +base-primitives = { git = "https://github.com/base/node-reth", features = ["test-utils"] } +``` + +## License + +Licensed under the [MIT License](https://github.com/base/node-reth/blob/main/LICENSE). diff --git a/crates/client/test-utils/contracts/.gitignore b/crates/shared/primitives/contracts/.gitignore similarity index 100% rename from crates/client/test-utils/contracts/.gitignore rename to crates/shared/primitives/contracts/.gitignore diff --git a/crates/client/test-utils/contracts/README.md b/crates/shared/primitives/contracts/README.md similarity index 100% rename from crates/client/test-utils/contracts/README.md rename to crates/shared/primitives/contracts/README.md diff --git a/crates/client/test-utils/contracts/foundry.lock b/crates/shared/primitives/contracts/foundry.lock similarity index 100% rename from crates/client/test-utils/contracts/foundry.lock rename to crates/shared/primitives/contracts/foundry.lock diff --git a/crates/client/test-utils/contracts/foundry.toml b/crates/shared/primitives/contracts/foundry.toml similarity index 100% rename from crates/client/test-utils/contracts/foundry.toml rename to crates/shared/primitives/contracts/foundry.toml diff --git a/crates/client/test-utils/contracts/lib/forge-std b/crates/shared/primitives/contracts/lib/forge-std similarity index 100% rename from crates/client/test-utils/contracts/lib/forge-std rename to crates/shared/primitives/contracts/lib/forge-std diff --git a/crates/client/test-utils/contracts/lib/openzeppelin-contracts b/crates/shared/primitives/contracts/lib/openzeppelin-contracts similarity index 100% rename from crates/client/test-utils/contracts/lib/openzeppelin-contracts rename to crates/shared/primitives/contracts/lib/openzeppelin-contracts diff --git a/crates/client/test-utils/contracts/lib/solmate b/crates/shared/primitives/contracts/lib/solmate similarity index 100% rename from crates/client/test-utils/contracts/lib/solmate rename to crates/shared/primitives/contracts/lib/solmate diff --git a/crates/client/test-utils/contracts/script/DeployDoubleCounter.s.sol b/crates/shared/primitives/contracts/script/DeployDoubleCounter.s.sol similarity index 100% rename from crates/client/test-utils/contracts/script/DeployDoubleCounter.s.sol rename to crates/shared/primitives/contracts/script/DeployDoubleCounter.s.sol diff --git a/crates/client/test-utils/contracts/script/DeployERC20.s.sol b/crates/shared/primitives/contracts/script/DeployERC20.s.sol similarity index 100% rename from crates/client/test-utils/contracts/script/DeployERC20.s.sol rename to crates/shared/primitives/contracts/script/DeployERC20.s.sol diff --git a/crates/client/test-utils/contracts/src/AccessList.sol b/crates/shared/primitives/contracts/src/AccessList.sol similarity index 100% rename from crates/client/test-utils/contracts/src/AccessList.sol rename to crates/shared/primitives/contracts/src/AccessList.sol diff --git a/crates/client/test-utils/contracts/src/ContractFactory.sol b/crates/shared/primitives/contracts/src/ContractFactory.sol similarity index 100% rename from crates/client/test-utils/contracts/src/ContractFactory.sol rename to crates/shared/primitives/contracts/src/ContractFactory.sol diff --git a/crates/client/test-utils/contracts/src/DoubleCounter.sol b/crates/shared/primitives/contracts/src/DoubleCounter.sol similarity index 100% rename from crates/client/test-utils/contracts/src/DoubleCounter.sol rename to crates/shared/primitives/contracts/src/DoubleCounter.sol diff --git a/crates/client/test-utils/contracts/src/Minimal7702Account.sol b/crates/shared/primitives/contracts/src/Minimal7702Account.sol similarity index 100% rename from crates/client/test-utils/contracts/src/Minimal7702Account.sol rename to crates/shared/primitives/contracts/src/Minimal7702Account.sol diff --git a/crates/client/test-utils/contracts/src/Proxy.sol b/crates/shared/primitives/contracts/src/Proxy.sol similarity index 100% rename from crates/client/test-utils/contracts/src/Proxy.sol rename to crates/shared/primitives/contracts/src/Proxy.sol diff --git a/crates/client/primitives/src/lib.rs b/crates/shared/primitives/src/lib.rs similarity index 53% rename from crates/client/primitives/src/lib.rs rename to crates/shared/primitives/src/lib.rs index 7fcd87c3..273a68db 100644 --- a/crates/client/primitives/src/lib.rs +++ b/crates/shared/primitives/src/lib.rs @@ -3,11 +3,8 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] -mod config; -pub use config::{FlashblocksConfig, TracingConfig}; +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; -mod extension; -pub use extension::{BaseNodeExtension, ConfigurableBaseNodeExtension}; - -mod types; -pub use types::{OpBuilder, OpProvider}; +#[cfg(any(test, feature = "test-utils"))] +pub use test_utils::*; diff --git a/crates/shared/primitives/src/test_utils/accounts.rs b/crates/shared/primitives/src/test_utils/accounts.rs new file mode 100644 index 00000000..a67024e7 --- /dev/null +++ b/crates/shared/primitives/src/test_utils/accounts.rs @@ -0,0 +1,116 @@ +//! Test accounts with pre-funded balances for integration testing. + +use alloy_consensus::{SignableTransaction, Transaction}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, FixedBytes, TxHash, address, hex}; +use alloy_signer::SignerSync; +use alloy_signer_local::PrivateKeySigner; +use eyre::{Result, eyre}; +use op_alloy_network::TransactionBuilder; +use op_alloy_rpc_types::OpTransactionRequest; + +use super::DEVNET_CHAIN_ID; + +/// EIP-1559 transaction type constant. +const EIP1559_TX_TYPE: u8 = 2; + +/// Hardcoded test accounts using Anvil's deterministic keys. +/// Derived from the test mnemonic: "test test test test test test test test test test test junk" +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Account { + /// Alice (Anvil account #0) + Alice, + /// Bob (Anvil account #1) + Bob, + /// Charlie (Anvil account #2) + Charlie, + /// Deployer (Anvil account #3) + Deployer, +} + +impl Account { + /// Returns the Ethereum address for this account. + pub const fn address(&self) -> Address { + match self { + Self::Alice => address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + Self::Bob => address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"), + Self::Charlie => address!("3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), + Self::Deployer => address!("90F79bf6EB2c4f870365E785982E1f101E93b906"), + } + } + + /// Returns the private key (hex string without 0x prefix). + pub const fn private_key(&self) -> &'static str { + match self { + Self::Alice => "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + Self::Bob => "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + Self::Charlie => "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", + Self::Deployer => "7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + } + } + + /// Returns all available test accounts. + pub const fn all() -> [Self; 4] { + [Self::Alice, Self::Bob, Self::Charlie, Self::Deployer] + } + + /// Constructs and returns a PrivateKeySigner for this account. + pub fn signer(&self) -> PrivateKeySigner { + let key_bytes = + hex::decode(self.private_key()).expect("should be able to decode private key"); + let key_fixed: FixedBytes<32> = FixedBytes::from_slice(&key_bytes); + PrivateKeySigner::from_bytes(&key_fixed) + .expect("should be able to build the PrivateKeySigner") + } + + /// Returns the private key as a B256 for use with TransactionBuilder. + pub fn signer_b256(&self) -> B256 { + let key_bytes = + hex::decode(self.private_key()).expect("should be able to decode private key"); + B256::from_slice(&key_bytes) + } + + /// Constructs a signed CREATE transaction with a given nonce and + /// returns the signed bytes, contract address, and transaction hash. + pub fn create_deployment_tx( + &self, + bytecode: Bytes, + nonce: u64, + ) -> Result<(Bytes, Address, TxHash)> { + let tx_request = OpTransactionRequest::default() + .from(self.address()) + .transaction_type(EIP1559_TX_TYPE) + .with_gas_limit(3_000_000) + .with_max_fee_per_gas(1_000_000_000) + .with_max_priority_fee_per_gas(0) + .with_chain_id(DEVNET_CHAIN_ID) + .with_deploy_code(bytecode) + .with_nonce(nonce); + + let tx = tx_request.build_typed_tx().map_err(|_| eyre!("invalid transaction request"))?; + let signature = self.signer().sign_hash_sync(&tx.signature_hash())?; + let signed_tx = tx.into_signed(signature); + let signed_tx_bytes = signed_tx.encoded_2718().into(); + + let contract_address = self.address().create(signed_tx.nonce()); + Ok((signed_tx_bytes, contract_address, *signed_tx.hash())) + } + + /// Sign a TransactionRequest and return the signed bytes. + pub fn sign_txn_request(&self, tx_request: OpTransactionRequest) -> Result<(Bytes, TxHash)> { + let tx_request = tx_request + .from(self.address()) + .transaction_type(EIP1559_TX_TYPE) + .with_gas_limit(500_000) + .with_chain_id(DEVNET_CHAIN_ID) + .with_max_fee_per_gas(1_000_000_000) + .with_max_priority_fee_per_gas(0); + + let tx = tx_request.build_typed_tx().map_err(|_| eyre!("invalid transaction request"))?; + let signature = self.signer().sign_hash_sync(&tx.signature_hash())?; + let signed_tx = tx.into_signed(signature); + let signed_tx_bytes = signed_tx.encoded_2718().into(); + let tx_hash = signed_tx.hash(); + Ok((signed_tx_bytes, *tx_hash)) + } +} diff --git a/crates/client/test-utils/src/contracts.rs b/crates/shared/primitives/src/test_utils/contracts.rs similarity index 53% rename from crates/client/test-utils/src/contracts.rs rename to crates/shared/primitives/src/test_utils/contracts.rs index c0bc4332..fe4214e6 100644 --- a/crates/client/test-utils/src/contracts.rs +++ b/crates/shared/primitives/src/test_utils/contracts.rs @@ -2,11 +2,6 @@ //! //! This module provides pre-compiled contract bindings that can be used //! across different test crates without needing relative path references. -//! -//! Contract sources: -//! - `DoubleCounter`: Custom test contract (src/DoubleCounter.sol) -//! - `MockERC20`: Solmate's MockERC20 (lib/solmate) -//! - `TransparentUpgradeableProxy`: OpenZeppelin's TransparentUpgradeableProxy (lib/openzeppelin-contracts) use alloy_sol_macro::sol; @@ -39,3 +34,39 @@ sol!( "/contracts/out/Minimal7702Account.sol/Minimal7702Account.json" ) ); + +sol!( + #[sol(rpc)] + AccessListContract, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/AccessList.sol/AccessList.json") +); + +sol!( + #[sol(rpc)] + ContractFactory, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/ContractFactory.sol/ContractFactory.json") +); + +sol!( + #[sol(rpc)] + SimpleStorage, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/ContractFactory.sol/SimpleStorage.json") +); + +sol!( + #[sol(rpc)] + Proxy, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/Proxy.sol/Proxy.json") +); + +sol!( + #[sol(rpc)] + Logic, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/Proxy.sol/Logic.json") +); + +sol!( + #[sol(rpc)] + Logic2, + concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/out/Proxy.sol/Logic2.json") +); diff --git a/crates/shared/primitives/src/test_utils/genesis.rs b/crates/shared/primitives/src/test_utils/genesis.rs new file mode 100644 index 00000000..df558287 --- /dev/null +++ b/crates/shared/primitives/src/test_utils/genesis.rs @@ -0,0 +1,99 @@ +//! Genesis configuration utilities for testing. + +use std::collections::BTreeMap; + +use alloy_genesis::{ChainConfig, Genesis, GenesisAccount}; +use alloy_primitives::{Address, B256, Bytes, U256, utils::parse_ether}; + +use super::Account; + +/// Chain ID for devnet test network. +pub const DEVNET_CHAIN_ID: u64 = 84538453; + +/// Gas limit for genesis block configuration. +pub const GENESIS_GAS_LIMIT: u64 = 100_000_000; + +/// Builds a test genesis configuration programmatically. +/// +/// Creates a Base Sepolia-like genesis with: +/// - All EVM and OP hardforks enabled from genesis +/// - Optimism EIP-1559 settings (elasticity=6, denominator=50) +/// - Pre-funded test accounts from the `Account` enum +pub fn build_test_genesis() -> Genesis { + // OP EIP-1559 base fee parameters + const EIP1559_ELASTICITY: u64 = 6; + const EIP1559_DENOMINATOR: u64 = 50; + + // Test account balance: 1 million ETH + let test_account_balance: U256 = parse_ether("1000000").expect("valid ether amount"); + + // Build chain config with all hardforks enabled at genesis + let config = ChainConfig { + chain_id: DEVNET_CHAIN_ID, + // Block-based EVM hardforks (all at block 0) + homestead_block: Some(0), + eip150_block: Some(0), + eip155_block: Some(0), + eip158_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + muir_glacier_block: Some(0), + berlin_block: Some(0), + london_block: Some(0), + arrow_glacier_block: Some(0), + gray_glacier_block: Some(0), + merge_netsplit_block: Some(0), + // Time-based hardforks + shanghai_time: Some(0), + cancun_time: Some(0), + prague_time: Some(0), + // Post-merge settings + terminal_total_difficulty: Some(U256::ZERO), + terminal_total_difficulty_passed: true, + // OP-specific hardforks and settings via extra_fields + extra_fields: [ + ("bedrockBlock", serde_json::json!(0)), + ("regolithTime", serde_json::json!(0)), + ("canyonTime", serde_json::json!(0)), + ("ecotoneTime", serde_json::json!(0)), + ("fjordTime", serde_json::json!(0)), + ("graniteTime", serde_json::json!(0)), + ("isthmusTime", serde_json::json!(0)), + ("jovianTime", serde_json::json!(0)), + ( + "optimism", + serde_json::json!({ + "eip1559Elasticity": EIP1559_ELASTICITY, + "eip1559Denominator": EIP1559_DENOMINATOR + }), + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + ..Default::default() + }; + + // Pre-fund all test accounts + let alloc: BTreeMap = Account::all() + .into_iter() + .map(|account| { + (account.address(), GenesisAccount::default().with_balance(test_account_balance)) + }) + .collect(); + + Genesis { + config, + alloc, + gas_limit: GENESIS_GAS_LIMIT, + difficulty: U256::ZERO, + nonce: 0, + timestamp: 0, + extra_data: Bytes::from_static(&[0x00]), + mix_hash: B256::ZERO, + coinbase: Address::ZERO, + ..Default::default() + } +} diff --git a/crates/shared/primitives/src/test_utils/mod.rs b/crates/shared/primitives/src/test_utils/mod.rs new file mode 100644 index 00000000..8626c87e --- /dev/null +++ b/crates/shared/primitives/src/test_utils/mod.rs @@ -0,0 +1,13 @@ +//! Test utilities including accounts, genesis configuration, and contract bindings. + +mod accounts; +pub use accounts::Account; + +mod genesis; +pub use genesis::{DEVNET_CHAIN_ID, GENESIS_GAS_LIMIT, build_test_genesis}; + +mod contracts; +pub use contracts::{ + AccessListContract, ContractFactory, DoubleCounter, Logic, Logic2, Minimal7702Account, + MockERC20, Proxy, SimpleStorage, TransparentUpgradeableProxy, +}; diff --git a/crates/client/reth-rpc-types/Cargo.toml b/crates/shared/reth-rpc-types/Cargo.toml similarity index 100% rename from crates/client/reth-rpc-types/Cargo.toml rename to crates/shared/reth-rpc-types/Cargo.toml diff --git a/crates/client/reth-rpc-types/README.md b/crates/shared/reth-rpc-types/README.md similarity index 100% rename from crates/client/reth-rpc-types/README.md rename to crates/shared/reth-rpc-types/README.md diff --git a/crates/client/reth-rpc-types/src/lib.rs b/crates/shared/reth-rpc-types/src/lib.rs similarity index 100% rename from crates/client/reth-rpc-types/src/lib.rs rename to crates/shared/reth-rpc-types/src/lib.rs diff --git a/lychee.toml b/lychee.toml index 885df032..2109d843 100644 --- a/lychee.toml +++ b/lychee.toml @@ -3,7 +3,7 @@ no_progress = false exclude_all_private = false accept = [200, 403] # 403 status code is often returned by private repos instead of 404 exclude_path = [ - "client/test-utils/contracts/lib" + "shared/primitives/contracts/lib" ] exclude = [ 'foo.bar', diff --git a/scripts/check-crate-deps.sh b/scripts/check-crate-deps.sh new file mode 100755 index 00000000..f8b478aa --- /dev/null +++ b/scripts/check-crate-deps.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Use cargo metadata to get structured dependency information +# --no-deps: only workspace members, not transitive deps +# --format-version 1: stable JSON format + +echo "Checking that shared crates don't depend on client crates..." + +# Find all violations using jq: +# 1. Select packages in crates/shared/ +# 2. For each, find dependencies with paths pointing to crates/client/ +# 3. Output violations as "shared_crate -> client_crate" +VIOLATIONS=$(cargo metadata --format-version 1 --no-deps | jq -r ' + [.packages[] + | select(.manifest_path | contains("/crates/shared/")) + | . as $pkg + | .dependencies[] + | select(.path) + | select(.path | contains("/crates/client/")) + | "\($pkg.name) -> \(.name)" + ] + | .[] +') + +if [ -n "$VIOLATIONS" ]; then + echo "ERROR: Found dependency boundary violations:" + echo "$VIOLATIONS" | while read -r violation; do + echo " - $violation" + done + echo "" + echo "Shared crates (crates/shared/) must not depend on client crates (crates/client/)" + exit 1 +fi + +echo "All shared crates have valid dependencies (no client crate dependencies)"