From 90dc96b731cee8d26f5f16f276a97d550d5a9296 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:35:35 -0500 Subject: [PATCH 01/38] wip: create test harness independently --- Cargo.lock | 256 +++++++++++------- Cargo.toml | 4 + crates/flashblocks-rpc/Cargo.toml | 2 + .../src/tests/assets/genesis.json | 6 + .../src/tests/framework_test.rs | 135 +++++++++ crates/flashblocks-rpc/src/tests/mod.rs | 1 + crates/flashblocks-rpc/src/tests/rpc.rs | 63 +++-- crates/test-utils/Cargo.toml | 82 ++++++ crates/test-utils/README.md | 225 +++++++++++++++ crates/test-utils/assets/genesis.json | 106 ++++++++ crates/test-utils/src/accounts.rs | 88 ++++++ crates/test-utils/src/engine.rs | 183 +++++++++++++ crates/test-utils/src/flashblocks.rs | 156 +++++++++++ crates/test-utils/src/harness.rs | 171 ++++++++++++ crates/test-utils/src/lib.rs | 44 +++ crates/test-utils/src/node.rs | 160 +++++++++++ 16 files changed, 1567 insertions(+), 115 deletions(-) create mode 100644 crates/flashblocks-rpc/src/tests/framework_test.rs create mode 100644 crates/test-utils/Cargo.toml create mode 100644 crates/test-utils/README.md create mode 100644 crates/test-utils/assets/genesis.json create mode 100644 crates/test-utils/src/accounts.rs create mode 100644 crates/test-utils/src/engine.rs create mode 100644 crates/test-utils/src/flashblocks.rs create mode 100644 crates/test-utils/src/harness.rs create mode 100644 crates/test-utils/src/lib.rs create mode 100644 crates/test-utils/src/node.rs diff --git a/Cargo.lock b/Cargo.lock index 4983dc9c..d9b26ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaa9ea039a6f9304b4a593d780b1f23e1ae183acdee938b11b38795acacc9f1" +checksum = "4bc32535569185cbcb6ad5fa64d989a47bccb9a08e27284b1f2a3ccf16e6d010" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90d103d3e440ad6f703dd71a5b58a6abd24834563bde8a5fabe706e00242f810" +checksum = "ad704069c12f68d0c742d0cad7e0a03882b42767350584627fbf8a47b1bf1846" dependencies = [ "alloy-eips", "alloy-primitives", @@ -122,6 +122,7 @@ dependencies = [ "alloy-tx-macros", "arbitrary", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -137,9 +138,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ead76c8c84ab3a50c31c56bc2c748c2d64357ad2131c32f9b10ab790a25e1a" +checksum = "bc374f640a5062224d7708402728e3d6879a514ba10f377da62e7dfb14c673e6" dependencies = [ "alloy-consensus", "alloy-eips", @@ -215,9 +216,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdbec74583d0067798d77afa43d58f00d93035335d7ceaa5d3f93857d461bb9" +checksum = "7e867b5fd52ed0372a95016f3a37cbff95a9d5409230fbaef2d8ea00e8618098" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -227,6 +228,7 @@ dependencies = [ "alloy-serde", "arbitrary", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -263,14 +265,15 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25d5acb35706e683df1ea333c862bdb6b7c5548836607cd5bb56e501cca0b4f" +checksum = "b90be17e9760a6ba6d13cebdb049cea405ebc8bf57d90664ed708cc5bc348342" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-serde", "alloy-trie", + "borsh", "serde", "serde_with", ] @@ -303,9 +306,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b67c5a702121e618217f7a86f314918acb2622276d0273490e2d4534490bc0" +checksum = "dcab4c51fb1273e3b0f59078e0cdf8aa99f697925b09f0d2055c18be46b4d48c" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -318,9 +321,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612296e6b723470bb1101420a73c63dfd535aa9bf738ce09951aedbd4ab7292e" +checksum = "196d7fd3f5d414f7bbd5886a628b7c42bd98d1b126f9a7cff69dbfd72007b39c" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -344,9 +347,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e7918396eecd69d9c907046ec8a93fb09b89e2f325d5e7ea9c4e3929aa0dd2" +checksum = "0d3ae2777e900a7a47ad9e3b8ab58eff3d93628265e73bbdee09acf90bf68f75" dependencies = [ "alloy-consensus", "alloy-eips", @@ -417,9 +420,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c1313a527a2e464d067c031f3c2ec073754ef615cc0eabca702fd0fe35729c" +checksum = "9f9bf40c9b2a90c7677f9c39bccd9f06af457f35362439c0497a706f16557703" dependencies = [ "alloy-chains", "alloy-consensus", @@ -459,9 +462,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810766eeed6b10ffa11815682b3f37afc5019809e3b470b23555297d5770ce63" +checksum = "acfdbe41e2ef1a7e79b5ea115baa750f9381ac9088fb600f4cedc731cf04a151" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -503,9 +506,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45f802228273056528dfd6cc8845cc91a7c7e0c6fc1a66d19e8673743dacdc7e" +checksum = "e7c2630fde9ff6033a780635e1af6ef40e92d74a9cacb8af3defc1b15cfebca5" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -529,9 +532,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff3df608dcabd6bdd197827ff2b8faaa6cefe0c462f7dc5e74108666a01f56" +checksum = "ad098153a12382c22a597e865530033f5e644473742d6c733562d448125e02a2" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", @@ -542,9 +545,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-admin" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e11a40c917c704888aa5aa6ffa563395123b732868d2e072ec7dd46c3d4672" +checksum = "b7604c415f725bd776d46dae44912c276cc3d8af37f37811e5675389791aa0c6" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -554,9 +557,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2bc988d7455e02dfb53460e1caa61f932b3f8452e12424e68ba8dcf60bba90" +checksum = "214d9d1033c173ab8fa32edd8a4655cd784447c820b0b66cd0d5167e049567d6" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -566,9 +569,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbf6d1766ca41e90ac21c4bc5cbc5e9e965978a25873c3f90b3992d905db4cb" +checksum = "50b8429b5b62d21bf3691eb1ae12aaae9bb496894d5a114e3cc73e27e6800ec8" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -577,9 +580,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab94e446a003dcef86843eea60d05a6cec360eb8e1829e4cf388ef94d799b5cf" +checksum = "f67f8269e8b5193a5328dd3ef4d60f93524071e53a993776e290581a59aa15fa" dependencies = [ "alloy-eips", "alloy-primitives", @@ -597,9 +600,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977698b458738369ba5ca645d2cdb4d51ba07a81db37306ff85322853161ea3a" +checksum = "01731601ea631bd825c652a225701ab466c09457f446b8d8129368a095389c5d" dependencies = [ "alloy-primitives", "derive_more", @@ -609,9 +612,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da696cc7fbfead4b1dda8afe408685cae80975cbb024f843ba74d9639cd0d3" +checksum = "9981491bb98e76099983f516ec7de550db0597031f5828c994961eb4bb993cce" dependencies = [ "alloy-consensus", "alloy-eips", @@ -629,9 +632,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15e4831b71eea9d20126a411c1c09facf1d01d5cac84fd51d532d3c429cfc26" +checksum = "29031a6bf46177d65efce661f7ab37829ca09dd341bc40afb5194e97600655cc" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -651,9 +654,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-mev" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c5d8f6f2c3b68af83a32d5c7fa1353d9b2e30441a3f0b8c3c5657c603b7238c" +checksum = "c5c5c78bdd2c72c47e66ab977af420fb4a10279707d4edbd2575693c47aa54a2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -666,9 +669,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0c800e2ce80829fca1491b3f9063c29092850dc6cf19249d5f678f0ce71bb0" +checksum = "01b842f5aac6676ff4b2e328262d03bdf49807eaec3fe3a4735c45c97388518b" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -680,9 +683,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f82e3068673a3cf93fbbc2f60a59059395cd54bbe39af895827faa5e641cc8f" +checksum = "7fa12c608873beeb7afa392944dce8829fa8a50c487f266863bb2dd6b743c4a2" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -692,9 +695,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751d1887f7d202514a82c5b3caf28ee8bd4a2ad9549e4f498b6f0bff99b52add" +checksum = "01e856112bfa0d9adc85bd7c13db03fad0e71d1d6fb4c2010e475b6718108236" dependencies = [ "alloy-primitives", "arbitrary", @@ -704,9 +707,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0b42ffbf558badfecf1dde0c3c5ed91f29bb7e97876d0bed008c3d5d67171" +checksum = "66a4f629da632d5279bbc5731634f0f5c9484ad9c4cad0cd974d9669dc1f46d6" dependencies = [ "alloy-primitives", "async-trait", @@ -719,9 +722,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e7d555ee5f27be29af4ae312be014b57c6cff9acb23fe2cf008500be6ca7e33" +checksum = "76c8950810dc43660c0f22883659c4218e090a5c75dce33fa4ca787715997b7b" dependencies = [ "alloy-consensus", "alloy-network", @@ -808,9 +811,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b3deee699d6f271eab587624a9fa84d02d0755db7a95a043d52a6488d16ebe" +checksum = "fe215a2f9b51d5f1aa5c8cf22c8be8cdb354934de09c9a4e37aefb79b77552fd" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -831,9 +834,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1720bd2ba8fe7e65138aca43bb0f680e4e0bcbd3ca39bf9d3035c9d7d2757f24" +checksum = "dc1b37b1a30d23deb3a8746e882c70b384c574d355bc2bbea9ea918b0c31366e" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -846,9 +849,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea89c214c7ddd2bcad100da929d6b642bbfed85788caf3b1be473abacd3111f9" +checksum = "52c81a4deeaa0d4b022095db17b286188d731e29ea141d4ec765e166732972e4" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -866,9 +869,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571aadf0afce0d515a28b2c6352662a39cb9f48b4eeff9a5c34557d6ea126730" +checksum = "4e9d6f5f304e8943afede2680e5fc7008780d4fc49387eafd53192ad95e20091" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -904,9 +907,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7ce8ed34106acd6e21942022b6a15be6454c2c3ead4d76811d3bdcd63cf771" +checksum = "7ccf423f6de62e8ce1d6c7a11fb7508ae3536d02e0d68aaeb05c8669337d0937" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -955,22 +958,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1531,6 +1534,7 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "arc-swap", + "base-reth-test-utils", "brotli", "eyre", "futures-util", @@ -1538,6 +1542,7 @@ dependencies = [ "jsonrpsee-types 0.26.0", "metrics", "metrics-derive", + "once_cell", "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types", @@ -1667,6 +1672,59 @@ dependencies = [ "uuid", ] +[[package]] +name = "base-reth-test-utils" +version = "0.2.1" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "base-reth-flashblocks-rpc", + "chrono", + "eyre", + "futures", + "futures-util", + "jsonrpsee 0.26.0", + "once_cell", + "op-alloy-consensus", + "op-alloy-network", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "reth", + "reth-db", + "reth-db-common", + "reth-e2e-test-utils", + "reth-exex", + "reth-ipc", + "reth-optimism-chainspec", + "reth-optimism-cli", + "reth-optimism-node", + "reth-optimism-primitives", + "reth-optimism-rpc", + "reth-primitives", + "reth-primitives-traits", + "reth-provider", + "reth-rpc-layer", + "reth-testing-utils", + "reth-tracing", + "rollup-boost", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tracing", + "url", +] + [[package]] name = "base-reth-transaction-tracing" version = "0.2.1" @@ -2147,9 +2205,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -2305,9 +2363,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -2315,9 +2373,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -4190,9 +4248,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -4261,9 +4319,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -5813,9 +5871,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "op-alloy-consensus" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d7ec388eb83a3e6c71774131dbbb2ba9c199b6acac7dce172ed8de2f819e91" +checksum = "e82f4f768ba39e52a4efe1b8f3425c04ab0d0e6f90c003fe97e5444cd963405e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5839,9 +5897,9 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "979fe768bbb571d1d0bd7f84bc35124243b4db17f944b94698872a4701e743a0" +checksum = "f2607d0d985f848f98fa79068d11c612f8476dba7deb7498881794bf51b3cfb5" dependencies = [ "alloy-consensus", "alloy-network", @@ -5855,9 +5913,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-jsonrpsee" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdbb3c0453fe2605fb008851ea0b45f3f2ba607722c9f2e4ffd7198958ce501" +checksum = "6911db73a4bf59bf8a963dec153ada1057fa426fdc35e0b35fe82657af3501a3" dependencies = [ "alloy-primitives", "jsonrpsee 0.26.0", @@ -5865,9 +5923,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc252b5fa74dbd33aa2f9a40e5ff9cfe34ed2af9b9b235781bc7cc8ec7d6aca8" +checksum = "890b51c3a619c263d52ee5a945dce173a4052d017f93bf5698613b21cbe0d237" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5884,9 +5942,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1abe694cd6718b8932da3f824f46778be0f43289e4103c88abc505c63533a04" +checksum = "c92f9dd709b3a769b7604d4d2257846b6de3d3f60e5163982cc4e90c0d0b6f95" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7047,9 +7105,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth" @@ -10198,9 +10256,9 @@ dependencies = [ [[package]] name = "revm-database" -version = "9.0.5" +version = "9.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b6c15bb255481fcf29f5ef7c97f00ed4c28a6ab6c490d77b990d73603031569" +checksum = "980d8d6bba78c5dd35b83abbb6585b0b902eb25ea4448ed7bfba6283b0337191" dependencies = [ "alloy-eips", "revm-bytecode", @@ -11008,9 +11066,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", @@ -11027,9 +11085,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -12878,13 +12936,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d7feac73..bd8197c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/flashblocks-rpc", "crates/metering", "crates/node", + "crates/test-utils", "crates/transaction-tracing", ] @@ -41,6 +42,7 @@ codegen-units = 1 base-reth-flashblocks-rpc = { path = "crates/flashblocks-rpc" } base-reth-metering = { path = "crates/metering" } base-reth-node = { path = "crates/node" } +base-reth-test-utils = { path = "crates/test-utils" } base-reth-transaction-tracing = { path = "crates/transaction-tracing" } # base/tips @@ -69,6 +71,8 @@ reth-exex = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } # revm revm = { version = "31.0.2", default-features = false } diff --git a/crates/flashblocks-rpc/Cargo.toml b/crates/flashblocks-rpc/Cargo.toml index 61467431..cb543821 100644 --- a/crates/flashblocks-rpc/Cargo.toml +++ b/crates/flashblocks-rpc/Cargo.toml @@ -74,8 +74,10 @@ brotli.workspace = true arc-swap.workspace = true [dev-dependencies] +base-reth-test-utils.workspace = true rand.workspace = true reth-db.workspace = true reth-testing-utils.workspace = true reth-db-common.workspace = true reth-e2e-test-utils.workspace = true +once_cell.workspace = true diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/src/tests/assets/genesis.json index 4d703497..79ab75e9 100644 --- a/crates/flashblocks-rpc/src/tests/assets/genesis.json +++ b/crates/flashblocks-rpc/src/tests/assets/genesis.json @@ -17,6 +17,12 @@ "mergeNetsplitBlock": 0, "bedrockBlock": 0, "regolithTime": 0, + "canyonTime": 0, + "ecotoneTime": 0, + "fjordTime": 0, + "graniteTime": 0, + "isthmusTime": 0, + "pragueTime": 0, "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true, "optimism": { diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs new file mode 100644 index 00000000..5628145f --- /dev/null +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -0,0 +1,135 @@ +//! Integration tests using the new test-utils framework +//! +//! These tests demonstrate using the test-utils framework with: +//! - TestNode for node setup +//! - EngineContext for canonical block production +//! - FlashblocksContext for pending state testing +//! - Pre-funded test accounts + +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::U256; +use alloy_provider::Provider; +use base_reth_test_utils::{EngineContext, TestNode}; +use eyre::Result; + +#[tokio::test] +async fn test_framework_node_setup() -> Result<()> { + reth_tracing::init_test_tracing(); + + // Create test node with Base Sepolia and pre-funded accounts + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Verify chain ID + let chain_id = provider.get_chain_id().await?; + assert_eq!(chain_id, 84532); // Base Sepolia + + // Verify test accounts are funded + let alice_balance = node.get_balance(node.alice().address).await?; + assert!(alice_balance > U256::ZERO, "Alice should have initial balance"); + + let bob_balance = node.get_balance(node.bob().address).await?; + assert!(bob_balance > U256::ZERO, "Bob should have initial balance"); + + Ok(()) +} + +#[tokio::test] +async fn test_framework_engine_api_block_production() -> Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Get genesis block + let genesis = provider + .get_block_by_number(BlockNumberOrTag::Number(0)) + .await? + .expect("Genesis block should exist"); + + let genesis_hash = genesis.header.hash; + + // Create engine context for canonical block production + let mut engine = EngineContext::new( + node.http_url(), + genesis_hash, + genesis.header.timestamp, + ) + .await?; + + // Build and finalize a single canonical block + let block_1_hash = engine.build_and_finalize_block().await?; + assert_ne!(block_1_hash, genesis_hash); + assert_eq!(engine.block_number(), 1); + + // Verify the block exists + let block_1 = provider + .get_block_by_hash(block_1_hash) + .await? + .expect("Block 1 should exist"); + assert_eq!(block_1.header.number, 1); + + // Advance chain by multiple blocks + let block_hashes = engine.advance_chain(3).await?; + assert_eq!(block_hashes.len(), 3); + assert_eq!(engine.block_number(), 4); + + // Verify latest block + let latest = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("Latest block should exist"); + assert_eq!(latest.header.number, 4); + assert_eq!(latest.header.hash, engine.head_hash()); + + Ok(()) +} + +#[tokio::test] +async fn test_framework_account_balances() -> Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Check all test accounts have their initial balances + let accounts = &node.accounts; + + for account in accounts.all() { + let balance = provider.get_balance(account.address).await?; + assert_eq!( + balance, + account.initial_balance_wei(), + "{} should have initial balance", + account.name + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_framework_parallel_nodes() -> Result<()> { + reth_tracing::init_test_tracing(); + + // Launch multiple nodes in parallel to verify isolation + let (node1_result, node2_result) = tokio::join!(TestNode::new(), TestNode::new()); + + let node1 = node1_result?; + let node2 = node2_result?; + + // Verify they have different ports + assert_ne!(node1.http_api_addr, node2.http_api_addr); + + // Verify both are functional + let provider1 = node1.provider().await?; + let provider2 = node2.provider().await?; + + let chain_id_1 = provider1.get_chain_id().await?; + let chain_id_2 = provider2.get_chain_id().await?; + + assert_eq!(chain_id_1, 84532); + assert_eq!(chain_id_2, 84532); + + Ok(()) +} diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/src/tests/mod.rs index 59995edb..1bc36868 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/src/tests/mod.rs @@ -1,5 +1,6 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; +mod framework_test; mod rpc; mod state; mod utils; diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs index c5a7b04f..8f5ec397 100644 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ b/crates/flashblocks-rpc/src/tests/rpc.rs @@ -12,17 +12,18 @@ mod tests { 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 alloy_rpc_types_eth::error::EthRpcErrorCode; + use alloy_rpc_types_eth::TransactionInput; + use once_cell::sync::OnceCell; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; use op_alloy_rpc_types::OpTransactionRequest; - use reth::{ - args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, - builder::{Node, NodeBuilder, NodeConfig, NodeHandle}, - chainspec::Chain, - core::exit::NodeExitFuture, - tasks::TaskManager, - }; + use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; + use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; + use reth::chainspec::Chain; + use reth::core::exit::NodeExitFuture; + use reth::tasks::TaskManager; + use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpecBuilder; use reth_optimism_node::{OpNode, args::RollupArgs}; use reth_optimism_primitives::OpReceipt; @@ -31,6 +32,7 @@ mod tests { use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; use serde_json; use tokio::sync::{mpsc, oneshot}; + use tokio_stream::StreamExt; use crate::{ rpc::{EthApiExt, EthApiOverrideServer}, @@ -115,31 +117,60 @@ mod tests { let node = OpNode::new(RollupArgs::default()); - // Start websocket server to simulate the builder and send payloads back to the node + // Start dummy websocket server to simulate the builder and send payloads back to the node let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let NodeHandle { node, node_exit_future } = NodeBuilder::new(node_config.clone()) + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + + 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()) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) .extend_rpc_modules(move |ctx| { - // We are not going to use the websocket connection to send payloads so we use - // a dummy url. - let flashblocks_state = Arc::new(FlashblocksState::new(ctx.provider().clone(), 5)); - flashblocks_state.start(); + // We are not going to use the websocket connection to send payloads so we don't + // initialize a flashblocks subscriber + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.start(); let api_ext = EthApiExt::new( ctx.registry.eth_api().clone(), ctx.registry.eth_handlers().filter.clone(), - flashblocks_state.clone(), + fb.clone(), ); ctx.modules.replace_configured(api_ext.into_rpc())?; tokio::spawn(async move { while let Some((payload, tx)) = receiver.recv().await { - flashblocks_state.on_flashblock_received(payload); + fb.on_flashblock_received(payload); tx.send(()).unwrap(); } }); diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 00000000..bd01f9ab --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,82 @@ +[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] +# internal +base-reth-flashblocks-rpc.workspace = true + +# reth +reth.workspace = true +reth-optimism-node.workspace = true +reth-optimism-chainspec.workspace = true +reth-optimism-cli.workspace = true +reth-optimism-primitives.workspace = true +reth-optimism-rpc.workspace = true +reth-provider.workspace = true +reth-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-db.workspace = true +reth-db-common.workspace = true +reth-testing-utils.workspace = true +reth-e2e-test-utils.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-eips.workspace = true +alloy-rpc-types.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-consensus.workspace = true +alloy-provider.workspace = true +alloy-rpc-client.workspace = true +alloy-serde.workspace = true + +# op-alloy +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true +op-alloy-network.workspace = true +op-alloy-consensus.workspace = true + +# rollup-boost +rollup-boost.workspace = true + +# tokio +tokio.workspace = true +tokio-stream.workspace = true +tokio-util = { version = "0.7", features = ["compat"] } + +# async +futures.workspace = true +futures-util.workspace = true + +# rpc +jsonrpsee.workspace = true + +# misc +tracing.workspace = true +serde.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/test-utils/README.md b/crates/test-utils/README.md new file mode 100644 index 00000000..08426b3b --- /dev/null +++ b/crates/test-utils/README.md @@ -0,0 +1,225 @@ +# 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: + +- **Node Setup**: Easy creation of test nodes with Base Sepolia chainspec +- **Engine API Integration**: Control canonical block production and chain advancement +- **Flashblocks Support**: Dummy flashblocks delivery mechanism for testing pending state +- **Test Accounts**: Pre-funded hardcoded accounts (Alice, Bob, Charlie, Deployer) + +## Features + +### 1. Test Node (`TestNode`) + +Create isolated test nodes with Base Sepolia configuration: + +```rust +use base_reth_test_utils::TestNode; + +#[tokio::test] +async fn test_example() -> eyre::Result<()> { + // Create a test node with Base Sepolia chainspec and pre-funded accounts + let node = TestNode::new().await?; + + // Get an alloy provider + let provider = node.provider().await?; + + // Access test accounts + let alice = node.alice(); + let balance = node.get_balance(alice.address).await?; + + Ok(()) +} +``` + +**Key Features:** +- Automatic port allocation (enables parallel test execution) +- Disabled P2P discovery (isolated testing) +- Pre-funded test accounts +- HTTP RPC server enabled + +### 2. Test Accounts (`TestAccounts`) + +Hardcoded test accounts with deterministic addresses and private keys: + +```rust +use base_reth_test_utils::TestAccounts; + +let accounts = TestAccounts::new(); + +// Access individual accounts +let alice = &accounts.alice; +let bob = &accounts.bob; +let charlie = &accounts.charlie; +let deployer = &accounts.deployer; + +// Each account has: +// - name: Account identifier +// - address: Ethereum address +// - private_key: Private key (hex string) +// - initial_balance_eth: Starting balance in ETH +``` + +**Account Details:** +- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH +- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH +- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH +- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH + +These are derived from Anvil's test mnemonic for compatibility. + +### 3. Engine API Integration (`EngineContext`) + +Control canonical block production via Engine API: + +```rust +use base_reth_test_utils::EngineContext; + +#[tokio::test] +async fn test_engine_api() -> eyre::Result<()> { + let node = TestNode::new().await?; + + // Create engine context + let mut engine = EngineContext::new( + node.http_url(), + B256::ZERO, // genesis hash + 1710338135, // initial timestamp + ).await?; + + // Build and finalize a single canonical block + let block_hash = engine.build_and_finalize_block().await?; + + // Advance the chain by multiple blocks + let block_hashes = engine.advance_chain(5).await?; + + // Check current state + let head = engine.head_hash(); + let block_number = engine.block_number(); + + Ok(()) +} +``` + +**Engine Operations:** +- `build_and_finalize_block()` - Create and finalize a single block +- `advance_chain(n)` - Build N blocks sequentially +- `update_forkchoice(...)` - Manual forkchoice updates +- Track current head, block number, and timestamp + +### 4. Flashblocks Integration (`FlashblocksContext`) + +Dummy flashblocks delivery for testing pending state: + +```rust +use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; + +#[tokio::test] +async fn test_flashblocks() -> eyre::Result<()> { + let (fb_ctx, receiver) = FlashblocksContext::new(); + + // Create a base flashblock (first flashblock with base payload) + let flashblock = FlashblockBuilder::new(1, 0) + .as_base(B256::ZERO, 1000) + .with_transaction(tx_bytes, tx_hash, 21000) + .with_balance(address, U256::from(1000)) + .build(); + + // Send flashblock and wait for processing + fb_ctx.send_flashblock(flashblock).await?; + + // Create a delta flashblock (subsequent flashblock) + let delta = FlashblockBuilder::new(1, 1) + .with_transaction(tx_bytes, tx_hash, 21000) + .build(); + + fb_ctx.send_flashblock(delta).await?; + + Ok(()) +} +``` + +**Flashblock Features:** +- Base flashblocks with `ExecutionPayloadBaseV1` +- Delta flashblocks with incremental changes +- Builder pattern for easy construction +- Channel-based delivery (non-WebSocket) + +## Architecture + +The framework is organized into modules: + +``` +test-utils/ +├── src/ +│ ├── lib.rs # Public API and re-exports +│ ├── accounts.rs # Test account definitions +│ ├── node.rs # TestNode implementation +│ ├── engine.rs # Engine API integration +│ └── flashblocks.rs # Flashblocks support +├── assets/ +│ └── genesis.json # Base Sepolia genesis configuration +└── Cargo.toml +``` + +## Usage in Other Crates + +Add `base-reth-test-utils` to your `dev-dependencies`: + +```toml +[dev-dependencies] +base-reth-test-utils.workspace = true +``` + +Then use in your integration tests: + +```rust +use base_reth_test_utils::{TestNode, TestAccounts}; + +#[tokio::test] +async fn my_integration_test() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Your test logic here + + Ok(()) +} +``` + +## Design Decisions + +1. **Anvil-Compatible Keys**: Uses the same deterministic mnemonic as Anvil for easy compatibility with other tools +2. **Port Allocation**: Random unused ports enable parallel test execution without conflicts +3. **Isolated Nodes**: Disabled P2P discovery ensures tests don't interfere with each other +4. **Channel-Based Flashblocks**: Non-WebSocket delivery mechanism simplifies testing +5. **Builder Patterns**: Fluent APIs for constructing complex test scenarios + +## Future Enhancements + +This framework is designed to be extended. Planned additions: + +- Transaction builders for common operations +- Smart contract deployment helpers +- Snapshot/restore functionality for test state +- Multi-node network simulation +- Performance benchmarking utilities + +## Testing + +Run the test suite: + +```bash +cargo test -p base-reth-test-utils +``` + +## References + +This framework was 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/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json new file mode 100644 index 00000000..79ab75e9 --- /dev/null +++ b/crates/test-utils/assets/genesis.json @@ -0,0 +1,106 @@ +{ + "config": { + "chainId": 8453, + "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, + "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" +} \ No newline at end of file diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs new file mode 100644 index 00000000..63bf4d8c --- /dev/null +++ b/crates/test-utils/src/accounts.rs @@ -0,0 +1,88 @@ +//! Test accounts with pre-funded balances for integration testing + +use alloy_primitives::{address, Address}; + +/// Hardcoded test account with a fixed private key +#[derive(Debug, Clone)] +pub struct TestAccount { + /// 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, +} + +/// Collection of all test accounts +#[derive(Debug, Clone)] +pub struct TestAccounts { + pub alice: TestAccount, + pub bob: TestAccount, + pub charlie: TestAccount, + 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/test-utils/src/engine.rs b/crates/test-utils/src/engine.rs new file mode 100644 index 00000000..4c1c19c1 --- /dev/null +++ b/crates/test-utils/src/engine.rs @@ -0,0 +1,183 @@ +//! Engine API integration for canonical block production +//! +//! This module provides a typed, type-safe Engine API client based on +//! reth's OpEngineApiClient trait instead of raw string-based RPC calls. + +use alloy_eips::eip7685::Requests; +use alloy_primitives::B256; +use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadId, PayloadStatus}; +use eyre::Result; +use jsonrpsee::core::client::SubscriptionClientT; +use op_alloy_rpc_types_engine::OpExecutionPayloadV4; +use reth::api::{EngineTypes, PayloadTypes}; +use reth::rpc::types::engine::ForkchoiceState; +use reth_optimism_node::OpEngineTypes; +use reth_optimism_rpc::engine::OpEngineApiClient; +use reth_rpc_layer::{AuthClientLayer, JwtSecret}; +use reth_tracing::tracing::debug; +use std::marker::PhantomData; +use std::time::Duration; +use url::Url; + +/// Default JWT secret for testing +const DEFAULT_JWT_SECRET: &str = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +#[derive(Clone, Debug)] +pub enum EngineAddress { + Http(Url), + Ipc(String), +} + +pub trait EngineProtocol: Send + Sync { + fn client( + jwt: JwtSecret, + address: EngineAddress, + ) -> impl std::future::Future< + Output = impl jsonrpsee::core::client::SubscriptionClientT + Send + Sync + Unpin + 'static, + > + Send; +} + +pub struct HttpEngine; + +impl EngineProtocol for HttpEngine { + async fn client( + jwt: JwtSecret, + address: EngineAddress, + ) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static { + let EngineAddress::Http(url) = address else { + unreachable!(); + }; + + let secret_layer = AuthClientLayer::new(jwt); + let middleware = tower::ServiceBuilder::default().layer(secret_layer); + + jsonrpsee::http_client::HttpClientBuilder::default() + .request_timeout(Duration::from_secs(10)) + .set_http_middleware(middleware) + .build(url) + .expect("Failed to create http client") + } +} + +pub struct IpcEngine; + +impl EngineProtocol for IpcEngine { + async fn client( + _: JwtSecret, // ipc does not use JWT + address: EngineAddress, + ) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static { + let EngineAddress::Ipc(path) = address else { + unreachable!(); + }; + reth_ipc::client::IpcClientBuilder::default() + .build(&path) + .await + .expect("Failed to create ipc client") + } +} + +pub struct EngineApi { + address: EngineAddress, + jwt_secret: JwtSecret, + _phantom: PhantomData

, +} + +impl EngineApi { + pub fn new(engine_url: String) -> Result { + let url: Url = engine_url.parse()?; + let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + + Ok(Self { + address: EngineAddress::Http(url), + jwt_secret, + _phantom: PhantomData, + }) + } +} + +impl EngineApi { + pub fn new(path: String) -> Result { + let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + + Ok(Self { + address: EngineAddress::Ipc(path), + jwt_secret, + _phantom: PhantomData, + }) + } +} + +impl EngineApi

{ + /// Get a client instance + async fn client(&self) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static + use

{ + P::client(self.jwt_secret, self.address.clone()).await + } + + /// Get a payload by ID from the Engine API + pub async fn get_payload( + &self, + payload_id: PayloadId, + ) -> eyre::Result<::ExecutionPayloadEnvelopeV4> { + debug!( + "Fetching payload with id: {} at {}", + payload_id, + chrono::Utc::now() + ); + Ok( + OpEngineApiClient::::get_payload_v4(&self.client().await, payload_id) + .await?, + ) + } + + /// Submit a new payload to the Engine API + pub async fn new_payload( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + execution_requests: Requests, + ) -> eyre::Result { + debug!("Submitting new payload at {}...", chrono::Utc::now()); + Ok(OpEngineApiClient::::new_payload_v4( + &self.client().await, + payload, + versioned_hashes, + parent_beacon_block_root, + execution_requests, + ) + .await?) + } + + /// Update forkchoice on the Engine API + pub async fn update_forkchoice( + &self, + current_head: B256, + new_head: B256, + payload_attributes: Option<::PayloadAttributes>, + ) -> eyre::Result { + debug!( + "Updating forkchoice at {} (current: {}, new: {})", + chrono::Utc::now(), + current_head, + new_head + ); + let result = OpEngineApiClient::::fork_choice_updated_v3( + &self.client().await, + ForkchoiceState { + head_block_hash: new_head, + safe_block_hash: current_head, + finalized_block_hash: current_head, + }, + payload_attributes, + ) + .await; + + match &result { + Ok(fcu) => debug!("Forkchoice updated successfully: {:?}", fcu), + Err(e) => debug!("Forkchoice update failed: {:?}", e), + } + + Ok(result?) + } +} diff --git a/crates/test-utils/src/flashblocks.rs b/crates/test-utils/src/flashblocks.rs new file mode 100644 index 00000000..484c4619 --- /dev/null +++ b/crates/test-utils/src/flashblocks.rs @@ -0,0 +1,156 @@ +//! Dummy flashblocks integration for testing pending state + +use alloy_primitives::{Bytes, TxHash}; +use base_reth_flashblocks_rpc::subscription::Flashblock; +use eyre::Result; +use tokio::sync::{mpsc, oneshot}; + +// Re-export types from flashblocks-rpc +pub use base_reth_flashblocks_rpc::subscription::{Flashblock as FlashblockPayload, Metadata as FlashblockMetadata}; + +/// Context for managing dummy flashblock delivery in tests +/// +/// This provides a non-WebSocket, queue-based mechanism for delivering +/// flashblocks to a test node, similar to how the rpc.rs tests work currently. +pub struct FlashblocksContext { + /// Channel for sending flashblocks to the node + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, +} + +impl FlashblocksContext { + /// Create a new flashblocks context with a channel + /// + /// Returns the context and a receiver that the node should consume + pub fn new() -> (Self, mpsc::Receiver<(Flashblock, oneshot::Sender<()>)>) { + let (sender, receiver) = mpsc::channel(100); + (Self { sender }, receiver) + } + + /// Send a flashblock to the node and wait for processing + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + + /// Send multiple flashblocks sequentially + pub async fn send_flashblocks(&self, flashblocks: Vec) -> Result<()> { + for flashblock in flashblocks { + self.send_flashblock(flashblock).await?; + } + Ok(()) + } +} + +impl Default for FlashblocksContext { + fn default() -> Self { + Self::new().0 + } +} + +/// Helper to extract transactions from a vec of flashblocks +pub fn extract_transactions_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { + let mut all_txs = Vec::new(); + + for flashblock in flashblocks { + all_txs.extend(flashblock.diff.transactions.clone()); + } + + all_txs +} + +/// Helper to get all transaction hashes from flashblock metadata +pub fn extract_tx_hashes_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { + let mut all_hashes = Vec::new(); + + for flashblock in flashblocks { + all_hashes.extend(flashblock.metadata.receipts.keys().copied()); + } + + all_hashes +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Address, B256, U256}; + use alloy_primitives::map::HashMap; + use alloy_consensus::Receipt; + use alloy_rpc_types_engine::PayloadId; + use base_reth_flashblocks_rpc::subscription::Metadata; + use op_alloy_consensus::OpDepositReceipt; + use reth_optimism_primitives::OpReceipt; + use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + + fn create_test_flashblock() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 0, + base: Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: B256::default(), + parent_hash: B256::default(), + fee_recipient: Address::ZERO, + prev_randao: B256::default(), + block_number: 1, + gas_limit: 30_000_000, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + }), + diff: ExecutionPayloadFlashblockDeltaV1 { + transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])], + ..Default::default() + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + B256::random(), + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 21000, + logs: vec![], + }, + deposit_nonce: Some(1), + deposit_receipt_version: None, + }), + ); + receipts + }, + new_account_balances: HashMap::default(), + }, + } + } + + #[tokio::test] + async fn test_flashblocks_context() { + let (ctx, mut receiver) = FlashblocksContext::new(); + let flashblock = create_test_flashblock(); + + // Spawn a task to receive and acknowledge + let handle = tokio::spawn(async move { + if let Some((fb, tx)) = receiver.recv().await { + assert_eq!(fb.metadata.block_number, 1); + tx.send(()).unwrap(); + } + }); + + // Send flashblock + ctx.send_flashblock(flashblock).await.unwrap(); + + // Wait for receiver task + handle.await.unwrap(); + } + + #[test] + fn test_extract_transactions() { + let flashblock = create_test_flashblock(); + let txs = extract_transactions_from_flashblocks(&[flashblock]); + + assert_eq!(txs.len(), 1); + assert_eq!(txs[0], Bytes::from(vec![0x01, 0x02, 0x03])); + } +} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs new file mode 100644 index 00000000..99da7863 --- /dev/null +++ b/crates/test-utils/src/harness.rs @@ -0,0 +1,171 @@ +//! Unified test harness combining node, engine API, and flashblocks functionality + +use crate::accounts::TestAccounts; +use crate::engine::{EngineApi, IpcEngine}; +use crate::node::LocalNode; +use crate::Flashblock; +use alloy_eips::eip7685::Requests; +use alloy_primitives::{Bytes, B256}; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_types::BlockNumberOrTag; +use alloy_rpc_types_engine::PayloadAttributes; +use eyre::{eyre, Result}; +use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use std::time::Duration; +use tokio::time::sleep; + +const BLOCK_TIME_SECONDS: u64 = 2; +const GAS_LIMIT: u64 = 200_000_000; +const NODE_STARTUP_DELAY_MS: u64 = 500; +const BLOCK_BUILD_DELAY_MS: u64 = 100; + +pub struct TestHarness { + node: LocalNode, + engine: EngineApi, + accounts: TestAccounts, +} + +impl TestHarness { + pub async fn new() -> Result { + let node = LocalNode::new().await?; + let engine = node.engine_api()?; + let accounts = TestAccounts::new(); + + sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; + + Ok(Self { + node, + engine, + accounts, + }) + } + + pub fn provider(&self) -> RootProvider { + self.node + .provider() + .expect("provider should always be available after node initialization") + } + + pub fn accounts(&self) -> &TestAccounts { + &self.accounts + } + + async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { + let latest_block = self + .provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| eyre!("No genesis block found"))?; + + let parent_hash = latest_block.header.hash; + let next_timestamp = latest_block.header.timestamp + BLOCK_TIME_SECONDS; + + let payload_attributes = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_timestamp, + parent_beacon_block_root: Some(B256::ZERO), + withdrawals: Some(vec![]), + ..Default::default() + }, + transactions: Some(transactions), + gas_limit: Some(GAS_LIMIT), + no_tx_pool: Some(true), + ..Default::default() + }; + + let forkchoice_result = self + .engine + .update_forkchoice(parent_hash, parent_hash, Some(payload_attributes)) + .await?; + + let payload_id = forkchoice_result + .payload_id + .ok_or_else(|| eyre!("Forkchoice update did not return payload ID"))?; + + sleep(Duration::from_millis(BLOCK_BUILD_DELAY_MS)).await; + + let payload_envelope = self.engine.get_payload(payload_id).await?; + + let execution_requests = if payload_envelope.execution_requests.is_empty() { + Requests::default() + } else { + Requests::new(payload_envelope.execution_requests) + }; + + let payload_status = self + .engine + .new_payload( + payload_envelope.execution_payload, + vec![], + payload_envelope.parent_beacon_block_root, + execution_requests, + ) + .await?; + + if payload_status.status.is_invalid() { + return Err(eyre!("Engine rejected payload: {:?}", payload_status)); + } + + let new_block_hash = payload_status + .latest_valid_hash + .ok_or_else(|| eyre!("Payload status missing latest_valid_hash"))?; + + self.engine + .update_forkchoice(parent_hash, new_block_hash, None) + .await?; + + Ok(()) + } + + pub async fn advance_chain(&self, n: u64) -> Result<()> { + for _ in 0..n { + self.build_block_from_transactions(vec![]).await?; + } + Ok(()) + } + + pub async fn build_block_from_flashblocks(&self, flashblocks: &[Flashblock]) -> Result<()> { + let transactions: Vec = flashblocks + .iter() + .flat_map(|fb| fb.diff.transactions.iter().cloned()) + .collect(); + self.build_block_from_transactions(transactions).await + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.node.send_flashblock(flashblock).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::U256; + use alloy_provider::Provider; + + #[tokio::test] + async fn test_harness_setup() -> Result<()> { + reth_tracing::init_test_tracing(); + 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::node::BASE_CHAIN_ID); + + let alice_balance = provider + .get_balance(harness.accounts().alice.address) + .await?; + assert!(alice_balance > U256::ZERO); + + let block_number = provider.get_block_number().await?; + harness.advance_chain(5).await?; + let new_block_number = provider.get_block_number().await?; + assert_eq!(new_block_number, block_number + 5); + + Ok(()) + } +} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 00000000..9237599c --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,44 @@ +//! Common integration test utilities for node-reth crates +//! +//! This crate provides a comprehensive test framework for integration testing. +//! +//! # Quick Start +//! +//! ```no_run +//! use base_reth_test_utils::TestHarness; +//! +//! #[tokio::test] +//! async fn test_example() -> eyre::Result<()> { +//! let harness = TestHarness::new().await?; +//! +//! // Send flashblocks for pending state testing +//! harness.send_flashblock(flashblock).await?; +//! +//! // Access test accounts +//! let alice = harness.alice(); +//! let balance = harness.get_balance(alice.address).await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Components +//! +//! - **TestHarness** - Unified interface combining node, engine API, and flashblocks +//! - **TestNode** - Node setup with Base Sepolia chainspec and flashblocks integration +//! - **EngineContext** - Engine API integration for canonical block production +//! - **TestAccounts** - Pre-funded test accounts (Alice, Bob, Charlie, Deployer - 10,000 ETH each) + +pub mod accounts; +pub mod engine; +pub mod flashblocks; +pub mod harness; +pub mod node; + +// Re-export commonly used types +pub use accounts::{TestAccount, TestAccounts}; +pub use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata as FlashblockMetadata}; +pub use engine::EngineApi; +pub use flashblocks::FlashblocksContext; +pub use harness::TestHarness; +pub use node::LocalNode; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs new file mode 100644 index 00000000..3809eee4 --- /dev/null +++ b/crates/test-utils/src/node.rs @@ -0,0 +1,160 @@ +//! Local node setup with Base Sepolia chainspec + +use crate::engine::EngineApi; +use crate::Flashblock; +use alloy_genesis::Genesis; +use alloy_provider::RootProvider; +use alloy_rpc_client::RpcClient; +use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; +use base_reth_flashblocks_rpc::state::FlashblocksState; +use base_reth_flashblocks_rpc::subscription::FlashblocksReceiver; +use eyre::Result; +use once_cell::sync::OnceCell; +use op_alloy_network::Optimism; +use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; +use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; +use reth::core::exit::NodeExitFuture; +use reth::tasks::TaskManager; +use reth_exex::ExExEvent; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_node::args::RollupArgs; +use reth_optimism_node::OpNode; +use reth_provider::providers::BlockchainProvider; +use std::any::Any; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; + +pub const BASE_CHAIN_ID: u64 = 8453; + +pub struct LocalNode { + http_api_addr: SocketAddr, + engine_ipc_path: String, + flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + _node_exit_future: NodeExitFuture, + _node: Box, + _task_manager: TaskManager, +} + +impl LocalNode { + pub async fn new() -> Result { + 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 node_config = NodeConfig::new(chain_spec.clone()) + .with_network(network_config) + .with_rpc( + RpcServerArgs::default() + .with_unused_ports() + .with_http() + .with_auth_ipc(), + ) + .with_unused_ports(); + + let node = OpNode::new(RollupArgs::default()); + + let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let NodeHandle { + node: node_handle, + 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()) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.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())?; + + // Spawn task to receive flashblocks from the test context + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb.on_flashblock_received(payload); + tx.send(()).unwrap(); + } + }); + + Ok(()) + }) + .launch() + .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 engine_ipc_path = node_config.rpc.auth_ipc_path; + + Ok(Self { + http_api_addr, + engine_ipc_path, + flashblock_sender: sender, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + }) + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.flashblock_sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + + pub fn provider(&self) -> Result> { + let url = format!("http://{}", self.http_api_addr); + let client = RpcClient::builder().http(url.parse()?); + Ok(RootProvider::::new(client)) + } + + pub fn engine_api(&self) -> Result> { + EngineApi::::new(self.engine_ipc_path.clone()) + } +} From 198e1d5b6f5ed936ab4178b0fcd3e7999778c864 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:46:56 -0500 Subject: [PATCH 02/38] wip: initialize foundry project --- .gitmodules | 3 ++ contracts/.github/workflows/test.yml | 37 ++++++++++++++++ contracts/.gitignore | 14 ++++++ contracts/README.md | 66 ++++++++++++++++++++++++++++ contracts/foundry.lock | 8 ++++ contracts/foundry.toml | 6 +++ contracts/lib/forge-std | 1 + contracts/script/Counter.s.sol | 19 ++++++++ contracts/src/Counter.sol | 14 ++++++ contracts/test/Counter.t.sol | 24 ++++++++++ 10 files changed, 192 insertions(+) create mode 100644 .gitmodules create mode 100644 contracts/.github/workflows/test.yml create mode 100644 contracts/.gitignore create mode 100644 contracts/README.md create mode 100644 contracts/foundry.lock create mode 100644 contracts/foundry.toml create mode 160000 contracts/lib/forge-std create mode 100644 contracts/script/Counter.s.sol create mode 100644 contracts/src/Counter.sol create mode 100644 contracts/test/Counter.t.sol diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c65a5965 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml new file mode 100644 index 00000000..c24b9832 --- /dev/null +++ b/contracts/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: CI + +permissions: + contents: read + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: forge --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build --sizes + + - name: Run Forge tests + run: forge test -vvv diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 00000000..fee8a957 --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.11.0", + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + } + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 00000000..8e40513d --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol new file mode 100644 index 00000000..f01d69c3 --- /dev/null +++ b/contracts/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol new file mode 100644 index 00000000..aded7997 --- /dev/null +++ b/contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol new file mode 100644 index 00000000..48319108 --- /dev/null +++ b/contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From 825a27701c86bf2f706e13672bf225c2b9e76b97 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:47:34 -0500 Subject: [PATCH 03/38] wip: install solmate --- .gitmodules | 3 +++ contracts/lib/solmate | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/solmate diff --git a/.gitmodules b/.gitmodules index c65a5965..29187eb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/solmate"] + path = contracts/lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/contracts/lib/solmate b/contracts/lib/solmate new file mode 160000 index 00000000..89365b88 --- /dev/null +++ b/contracts/lib/solmate @@ -0,0 +1 @@ +Subproject commit 89365b880c4f3c786bdd453d4b8e8fe410344a69 From 43755179df3893f926bc316b8f67210f57d18969 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:50:23 -0500 Subject: [PATCH 04/38] update readme --- crates/test-utils/README.md | 323 +++++++++++++++++++++++------------- 1 file changed, 212 insertions(+), 111 deletions(-) diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 08426b3b..630eb2b1 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -6,113 +6,210 @@ A comprehensive integration test framework for node-reth crates. This crate provides reusable testing utilities for integration tests across the node-reth workspace. It includes: -- **Node Setup**: Easy creation of test nodes with Base Sepolia chainspec -- **Engine API Integration**: Control canonical block production and chain advancement -- **Flashblocks Support**: Dummy flashblocks delivery mechanism for testing pending state +- **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 -## Features - -### 1. Test Node (`TestNode`) - -Create isolated test nodes with Base Sepolia configuration: +## Quick Start ```rust -use base_reth_test_utils::TestNode; +use base_reth_test_utils::TestHarness; #[tokio::test] async fn test_example() -> eyre::Result<()> { - // Create a test node with Base Sepolia chainspec and pre-funded accounts - let node = TestNode::new().await?; + let harness = TestHarness::new().await?; + + // Advance the chain + harness.advance_chain(5).await?; - // Get an alloy provider - let provider = node.provider().await?; + // Access accounts + let alice = &harness.accounts().alice; - // Access test accounts - let alice = node.alice(); - let balance = node.get_balance(alice.address).await?; + // Get balance via provider + let balance = harness.provider().get_balance(alice.address).await?; Ok(()) } ``` -**Key Features:** -- Automatic port allocation (enables parallel test execution) -- Disabled P2P discovery (isolated testing) -- Pre-funded test accounts -- HTTP RPC server enabled +## Architecture + +The framework follows a three-layer architecture: + +``` +┌─────────────────────────────────────┐ +│ TestHarness │ ← Orchestration layer (tests use this) +│ - Coordinates node + engine │ +│ - Builds blocks from transactions │ +│ - Manages test accounts │ +└─────────────────────────────────────┘ + │ │ + ┌──────┘ └──────┐ + ▼ ▼ +┌─────────┐ ┌──────────┐ +│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 -### 2. Test Accounts (`TestAccounts`) +### 1. TestHarness -Hardcoded test accounts with deterministic addresses and private keys: +The main entry point for integration tests. Combines node, engine, and accounts into a single interface. ```rust -use base_reth_test_utils::TestAccounts; +use base_reth_test_utils::TestHarness; +use alloy_primitives::Bytes; -let accounts = TestAccounts::new(); +#[tokio::test] +async fn test_harness() -> eyre::Result<()> { + let harness = TestHarness::new().await?; -// Access individual accounts -let alice = &accounts.alice; -let bob = &accounts.bob; -let charlie = &accounts.charlie; -let deployer = &accounts.deployer; + // 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?; + + // Build block from flashblocks + harness.build_block_from_flashblocks(&flashblocks).await?; + + // Send flashblocks for pending state testing + harness.send_flashblock(flashblock).await?; -// Each account has: -// - name: Account identifier -// - address: Ethereum address -// - private_key: Private key (hex string) -// - initial_balance_eth: Starting balance in ETH + Ok(()) +} ``` -**Account Details:** -- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH -- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH -- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH -- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH +**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 +- `build_block_from_flashblocks(&flashblocks)` - Extract txs from flashblocks and build block +- `send_flashblock(fb)` - Send flashblock to node for pending state -These are derived from Anvil's test mnemonic for compatibility. +**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 -### 3. Engine API Integration (`EngineContext`) +### 2. LocalNode -Control canonical block production via Engine API: +In-process Optimism node with Base Sepolia configuration. ```rust -use base_reth_test_utils::EngineContext; +use base_reth_test_utils::LocalNode; #[tokio::test] -async fn test_engine_api() -> eyre::Result<()> { - let node = TestNode::new().await?; - - // Create engine context - let mut engine = EngineContext::new( - node.http_url(), - B256::ZERO, // genesis hash - 1710338135, // initial timestamp - ).await?; +async fn test_node() -> eyre::Result<()> { + let node = LocalNode::new().await?; - // Build and finalize a single canonical block - let block_hash = engine.build_and_finalize_block().await?; + // Get provider + let provider = node.provider()?; - // Advance the chain by multiple blocks - let block_hashes = engine.advance_chain(5).await?; + // Get Engine API + let engine = node.engine_api()?; - // Check current state - let head = engine.head_hash(); - let block_number = engine.block_number(); + // Send flashblocks + node.send_flashblock(flashblock).await?; Ok(()) } ``` -**Engine Operations:** -- `build_and_finalize_block()` - Create and finalize a single block -- `advance_chain(n)` - Build N blocks sequentially -- `update_forkchoice(...)` - Manual forkchoice updates -- Track current head, block number, and timestamp +**Features:** +- 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` +- Flashblocks-canon ExEx integration + +**Note:** Most tests should use `TestHarness` instead of `LocalNode` directly. -### 4. Flashblocks Integration (`FlashblocksContext`) +### 3. EngineApi -Dummy flashblocks delivery for testing pending state: +Type-safe Engine API client wrapping raw CL operations. + +```rust +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 +use base_reth_test_utils::TestAccounts; + +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 + +Test flashblocks delivery without WebSocket connections. ```rust use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; @@ -121,94 +218,84 @@ use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; async fn test_flashblocks() -> eyre::Result<()> { let (fb_ctx, receiver) = FlashblocksContext::new(); - // Create a base flashblock (first flashblock with base payload) + // Create base flashblock let flashblock = FlashblockBuilder::new(1, 0) .as_base(B256::ZERO, 1000) .with_transaction(tx_bytes, tx_hash, 21000) .with_balance(address, U256::from(1000)) .build(); - // Send flashblock and wait for processing fb_ctx.send_flashblock(flashblock).await?; - // Create a delta flashblock (subsequent flashblock) - let delta = FlashblockBuilder::new(1, 1) - .with_transaction(tx_bytes, tx_hash, 21000) - .build(); - - fb_ctx.send_flashblock(delta).await?; - Ok(()) } ``` -**Flashblock Features:** -- Base flashblocks with `ExecutionPayloadBaseV1` -- Delta flashblocks with incremental changes -- Builder pattern for easy construction -- Channel-based delivery (non-WebSocket) +**Via TestHarness:** +```rust +let harness = TestHarness::new().await?; +harness.send_flashblock(flashblock).await?; +``` + +## Configuration Constants -## Architecture +Key constants defined in `harness.rs`: + +```rust +const BLOCK_TIME_SECONDS: u64 = 2; // Base L2 block time +const GAS_LIMIT: u64 = 200_000_000; // Default gas limit +const NODE_STARTUP_DELAY_MS: u64 = 500; // IPC endpoint initialization +const BLOCK_BUILD_DELAY_MS: u64 = 100; // Payload construction wait +``` -The framework is organized into modules: +## File Structure ``` test-utils/ ├── src/ │ ├── lib.rs # Public API and re-exports │ ├── accounts.rs # Test account definitions -│ ├── node.rs # TestNode implementation -│ ├── engine.rs # Engine API integration +│ ├── node.rs # LocalNode (EL wrapper) +│ ├── engine.rs # EngineApi (CL wrapper) +│ ├── harness.rs # TestHarness (orchestration) │ └── flashblocks.rs # Flashblocks support ├── assets/ -│ └── genesis.json # Base Sepolia genesis configuration +│ └── genesis.json # Base Sepolia genesis └── Cargo.toml ``` ## Usage in Other Crates -Add `base-reth-test-utils` to your `dev-dependencies`: +Add to `dev-dependencies`: ```toml [dev-dependencies] base-reth-test-utils.workspace = true ``` -Then use in your integration tests: +Import in tests: ```rust -use base_reth_test_utils::{TestNode, TestAccounts}; +use base_reth_test_utils::TestHarness; #[tokio::test] -async fn my_integration_test() -> eyre::Result<()> { +async fn my_test() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Your test logic here + let harness = TestHarness::new().await?; + // Your test logic Ok(()) } ``` -## Design Decisions +## Design Principles -1. **Anvil-Compatible Keys**: Uses the same deterministic mnemonic as Anvil for easy compatibility with other tools -2. **Port Allocation**: Random unused ports enable parallel test execution without conflicts -3. **Isolated Nodes**: Disabled P2P discovery ensures tests don't interfere with each other -4. **Channel-Based Flashblocks**: Non-WebSocket delivery mechanism simplifies testing -5. **Builder Patterns**: Fluent APIs for constructing complex test scenarios - -## Future Enhancements - -This framework is designed to be extended. Planned additions: - -- Transaction builders for common operations -- Smart contract deployment helpers -- Snapshot/restore functionality for test state -- Multi-node network simulation -- Performance benchmarking utilities +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 @@ -218,8 +305,22 @@ Run the test suite: 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 + ## References -This framework was inspired by: +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) From f50e02109cb91215dd2c187614ff767c4b5fe9ff Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 23:28:19 -0500 Subject: [PATCH 05/38] wip: generalize test harness, allow for ExEx's and RPC modules to be passed down --- Cargo.lock | 1 - .../src/tests/framework_test.rs | 138 ++-------------- crates/test-utils/Cargo.toml | 3 - crates/test-utils/src/flashblocks.rs | 156 ------------------ crates/test-utils/src/harness.rs | 31 ++-- crates/test-utils/src/lib.rs | 40 ----- crates/test-utils/src/node.rs | 152 +++++++++-------- 7 files changed, 110 insertions(+), 411 deletions(-) delete mode 100644 crates/test-utils/src/flashblocks.rs diff --git a/Cargo.lock b/Cargo.lock index d9b26ab5..cb006136 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1686,7 +1686,6 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", - "base-reth-flashblocks-rpc", "chrono", "eyre", "futures", diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 5628145f..8bb27456 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -1,135 +1,19 @@ -//! Integration tests using the new test-utils framework -//! -//! These tests demonstrate using the test-utils framework with: -//! - TestNode for node setup -//! - EngineContext for canonical block production -//! - FlashblocksContext for pending state testing -//! - Pre-funded test accounts - -use alloy_eips::BlockNumberOrTag; -use alloy_primitives::U256; -use alloy_provider::Provider; -use base_reth_test_utils::{EngineContext, TestNode}; +use base_reth_test_utils::harness::TestHarness; use eyre::Result; +use reth::api::FullNodeComponents; +use reth_exex::ExExContext; -#[tokio::test] -async fn test_framework_node_setup() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Create test node with Base Sepolia and pre-funded accounts - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Verify chain ID - let chain_id = provider.get_chain_id().await?; - assert_eq!(chain_id, 84532); // Base Sepolia - - // Verify test accounts are funded - let alice_balance = node.get_balance(node.alice().address).await?; - assert!(alice_balance > U256::ZERO, "Alice should have initial balance"); - - let bob_balance = node.get_balance(node.bob().address).await?; - assert!(bob_balance > U256::ZERO, "Bob should have initial balance"); - - Ok(()) -} +use futures_util::{Future, TryStreamExt}; +use reth_exex::{ExExEvent, ExExNotification}; +use reth_tracing::tracing::info; +#[cfg(test)] #[tokio::test] -async fn test_framework_engine_api_block_production() -> Result<()> { - reth_tracing::init_test_tracing(); - - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Get genesis block - let genesis = provider - .get_block_by_number(BlockNumberOrTag::Number(0)) - .await? - .expect("Genesis block should exist"); - - let genesis_hash = genesis.header.hash; - - // Create engine context for canonical block production - let mut engine = EngineContext::new( - node.http_url(), - genesis_hash, - genesis.header.timestamp, - ) - .await?; - - // Build and finalize a single canonical block - let block_1_hash = engine.build_and_finalize_block().await?; - assert_ne!(block_1_hash, genesis_hash); - assert_eq!(engine.block_number(), 1); - - // Verify the block exists - let block_1 = provider - .get_block_by_hash(block_1_hash) - .await? - .expect("Block 1 should exist"); - assert_eq!(block_1.header.number, 1); - - // Advance chain by multiple blocks - let block_hashes = engine.advance_chain(3).await?; - assert_eq!(block_hashes.len(), 3); - assert_eq!(engine.block_number(), 4); - - // Verify latest block - let latest = provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await? - .expect("Latest block should exist"); - assert_eq!(latest.header.number, 4); - assert_eq!(latest.header.hash, engine.head_hash()); - - Ok(()) -} - -#[tokio::test] -async fn test_framework_account_balances() -> Result<()> { - reth_tracing::init_test_tracing(); - - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Check all test accounts have their initial balances - let accounts = &node.accounts; - - for account in accounts.all() { - let balance = provider.get_balance(account.address).await?; - assert_eq!( - balance, - account.initial_balance_wei(), - "{} should have initial balance", - account.name - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_framework_parallel_nodes() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Launch multiple nodes in parallel to verify isolation - let (node1_result, node2_result) = tokio::join!(TestNode::new(), TestNode::new()); - - let node1 = node1_result?; - let node2 = node2_result?; - - // Verify they have different ports - assert_ne!(node1.http_api_addr, node2.http_api_addr); - - // Verify both are functional - let provider1 = node1.provider().await?; - let provider2 = node2.provider().await?; - - let chain_id_1 = provider1.get_chain_id().await?; - let chain_id_2 = provider2.get_chain_id().await?; +async fn test_framework_test() -> Result<()> { + use base_reth_test_utils::node::default_launcher; - assert_eq!(chain_id_1, 84532); - assert_eq!(chain_id_2, 84532); + let harness = TestHarness::new(default_launcher).await?; + let provider = harness.provider(); Ok(()) } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index bd01f9ab..be0cc9a6 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -12,9 +12,6 @@ description = "Common integration test utilities for node-reth crates" workspace = true [dependencies] -# internal -base-reth-flashblocks-rpc.workspace = true - # reth reth.workspace = true reth-optimism-node.workspace = true diff --git a/crates/test-utils/src/flashblocks.rs b/crates/test-utils/src/flashblocks.rs deleted file mode 100644 index 484c4619..00000000 --- a/crates/test-utils/src/flashblocks.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Dummy flashblocks integration for testing pending state - -use alloy_primitives::{Bytes, TxHash}; -use base_reth_flashblocks_rpc::subscription::Flashblock; -use eyre::Result; -use tokio::sync::{mpsc, oneshot}; - -// Re-export types from flashblocks-rpc -pub use base_reth_flashblocks_rpc::subscription::{Flashblock as FlashblockPayload, Metadata as FlashblockMetadata}; - -/// Context for managing dummy flashblock delivery in tests -/// -/// This provides a non-WebSocket, queue-based mechanism for delivering -/// flashblocks to a test node, similar to how the rpc.rs tests work currently. -pub struct FlashblocksContext { - /// Channel for sending flashblocks to the node - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, -} - -impl FlashblocksContext { - /// Create a new flashblocks context with a channel - /// - /// Returns the context and a receiver that the node should consume - pub fn new() -> (Self, mpsc::Receiver<(Flashblock, oneshot::Sender<()>)>) { - let (sender, receiver) = mpsc::channel(100); - (Self { sender }, receiver) - } - - /// Send a flashblock to the node and wait for processing - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.sender.send((flashblock, tx)).await?; - rx.await?; - Ok(()) - } - - /// Send multiple flashblocks sequentially - pub async fn send_flashblocks(&self, flashblocks: Vec) -> Result<()> { - for flashblock in flashblocks { - self.send_flashblock(flashblock).await?; - } - Ok(()) - } -} - -impl Default for FlashblocksContext { - fn default() -> Self { - Self::new().0 - } -} - -/// Helper to extract transactions from a vec of flashblocks -pub fn extract_transactions_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { - let mut all_txs = Vec::new(); - - for flashblock in flashblocks { - all_txs.extend(flashblock.diff.transactions.clone()); - } - - all_txs -} - -/// Helper to get all transaction hashes from flashblock metadata -pub fn extract_tx_hashes_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { - let mut all_hashes = Vec::new(); - - for flashblock in flashblocks { - all_hashes.extend(flashblock.metadata.receipts.keys().copied()); - } - - all_hashes -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Address, B256, U256}; - use alloy_primitives::map::HashMap; - use alloy_consensus::Receipt; - use alloy_rpc_types_engine::PayloadId; - use base_reth_flashblocks_rpc::subscription::Metadata; - use op_alloy_consensus::OpDepositReceipt; - use reth_optimism_primitives::OpReceipt; - use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - - fn create_test_flashblock() -> Flashblock { - Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: B256::default(), - parent_hash: B256::default(), - fee_recipient: Address::ZERO, - prev_randao: B256::default(), - block_number: 1, - gas_limit: 30_000_000, - timestamp: 0, - extra_data: Bytes::new(), - base_fee_per_gas: U256::ZERO, - }), - diff: ExecutionPayloadFlashblockDeltaV1 { - transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])], - ..Default::default() - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - B256::random(), - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 21000, - logs: vec![], - }, - deposit_nonce: Some(1), - deposit_receipt_version: None, - }), - ); - receipts - }, - new_account_balances: HashMap::default(), - }, - } - } - - #[tokio::test] - async fn test_flashblocks_context() { - let (ctx, mut receiver) = FlashblocksContext::new(); - let flashblock = create_test_flashblock(); - - // Spawn a task to receive and acknowledge - let handle = tokio::spawn(async move { - if let Some((fb, tx)) = receiver.recv().await { - assert_eq!(fb.metadata.block_number, 1); - tx.send(()).unwrap(); - } - }); - - // Send flashblock - ctx.send_flashblock(flashblock).await.unwrap(); - - // Wait for receiver task - handle.await.unwrap(); - } - - #[test] - fn test_extract_transactions() { - let flashblock = create_test_flashblock(); - let txs = extract_transactions_from_flashblocks(&[flashblock]); - - assert_eq!(txs.len(), 1); - assert_eq!(txs[0], Bytes::from(vec![0x01, 0x02, 0x03])); - } -} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 99da7863..9bb9ba11 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -2,16 +2,19 @@ use crate::accounts::TestAccounts; use crate::engine::{EngineApi, IpcEngine}; -use crate::node::LocalNode; -use crate::Flashblock; +use crate::node::{LocalNode, OpAddOns, OpBuilder}; use alloy_eips::eip7685::Requests; use alloy_primitives::{Bytes, B256}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; use eyre::{eyre, Result}; +use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; +use reth::builder::NodeHandle; +use reth_e2e_test_utils::Adapter; +use reth_optimism_node::OpNode; use std::time::Duration; use tokio::time::sleep; @@ -27,8 +30,12 @@ pub struct TestHarness { } impl TestHarness { - pub async fn new() -> Result { - let node = LocalNode::new().await?; + pub async fn new(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + let node = LocalNode::new(launcher).await?; let engine = node.engine_api()?; let accounts = TestAccounts::new(); @@ -124,22 +131,12 @@ impl TestHarness { } Ok(()) } - - pub async fn build_block_from_flashblocks(&self, flashblocks: &[Flashblock]) -> Result<()> { - let transactions: Vec = flashblocks - .iter() - .flat_map(|fb| fb.diff.transactions.iter().cloned()) - .collect(); - self.build_block_from_transactions(transactions).await - } - - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.node.send_flashblock(flashblock).await - } } #[cfg(test)] mod tests { + use crate::node::default_launcher; + use super::*; use alloy_primitives::U256; use alloy_provider::Provider; @@ -147,7 +144,7 @@ mod tests { #[tokio::test] async fn test_harness_setup() -> Result<()> { reth_tracing::init_test_tracing(); - let harness = TestHarness::new().await?; + let harness = TestHarness::new(default_launcher).await?; assert_eq!(harness.accounts().alice.name, "Alice"); assert_eq!(harness.accounts().bob.name, "Bob"); diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 9237599c..62913e26 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,44 +1,4 @@ -//! Common integration test utilities for node-reth crates -//! -//! This crate provides a comprehensive test framework for integration testing. -//! -//! # Quick Start -//! -//! ```no_run -//! use base_reth_test_utils::TestHarness; -//! -//! #[tokio::test] -//! async fn test_example() -> eyre::Result<()> { -//! let harness = TestHarness::new().await?; -//! -//! // Send flashblocks for pending state testing -//! harness.send_flashblock(flashblock).await?; -//! -//! // Access test accounts -//! let alice = harness.alice(); -//! let balance = harness.get_balance(alice.address).await?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! # Components -//! -//! - **TestHarness** - Unified interface combining node, engine API, and flashblocks -//! - **TestNode** - Node setup with Base Sepolia chainspec and flashblocks integration -//! - **EngineContext** - Engine API integration for canonical block production -//! - **TestAccounts** - Pre-funded test accounts (Alice, Bob, Charlie, Deployer - 10,000 ETH each) - pub mod accounts; pub mod engine; -pub mod flashblocks; pub mod harness; pub mod node; - -// Re-export commonly used types -pub use accounts::{TestAccount, TestAccounts}; -pub use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata as FlashblockMetadata}; -pub use engine::EngineApi; -pub use flashblocks::FlashblocksContext; -pub use harness::TestHarness; -pub use node::LocalNode; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 3809eee4..8a3a609b 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -1,21 +1,20 @@ //! Local node setup with Base Sepolia chainspec use crate::engine::EngineApi; -use crate::Flashblock; use alloy_genesis::Genesis; use alloy_provider::RootProvider; use alloy_rpc_client::RpcClient; -use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; -use base_reth_flashblocks_rpc::state::FlashblocksState; -use base_reth_flashblocks_rpc::subscription::FlashblocksReceiver; use eyre::Result; -use once_cell::sync::OnceCell; +use futures_util::Future; use op_alloy_network::Optimism; +use reth::api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}; use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; -use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; +use reth::builder::{ + Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, +}; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; -use reth_exex::ExExEvent; +use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; @@ -23,22 +22,45 @@ use reth_provider::providers::BlockchainProvider; use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; -use tokio::sync::{mpsc, oneshot}; -use tokio_stream::StreamExt; pub const BASE_CHAIN_ID: u64 = 8453; pub struct LocalNode { http_api_addr: SocketAddr, engine_ipc_path: String, - flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + // flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, _node_exit_future: NodeExitFuture, _node: Box, _task_manager: TaskManager, } +// Full node types for OpNode over the TmpDB used in tests +pub type OpTypes = + FullNodeTypesAdapter>>; + +// Components builder for OpNode +pub type OpComponentsBuilder = >::ComponentsBuilder; + +// AddOns (this includes the EthApi type etc.) +pub type OpAddOns = >::AddOns; + +// The builder type we’re going to pass into launch_with_fn +pub type OpBuilder = + WithLaunchContext>; + +pub async fn default_launcher( + builder: OpBuilder, +) -> eyre::Result, OpAddOns>> { + let launcher = builder.engine_api_launcher(); + builder.launch_with(launcher).await +} + impl LocalNode { - pub async fn new() -> Result { + pub async fn new(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { let tasks = TaskManager::current(); let exec = tasks.executor(); @@ -65,8 +87,9 @@ impl LocalNode { let node = OpNode::new(RollupArgs::default()); - let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + // let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + // let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let NodeHandle { node: node_handle, node_exit_future, @@ -75,53 +98,48 @@ impl LocalNode { .with_types_and_provider::>() .with_components(node.components_builder()) .with_add_ons(node.add_ons()) - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - move |mut ctx| async move { - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - Ok(async move { - while let Some(note) = ctx.notifications.try_next().await? { - if let Some(committed) = note.committed_chain() { - for b in committed.blocks_iter() { - fb.on_canonical_block_received(b); - } - let _ = ctx - .events - .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules(move |ctx| { - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - - fb.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())?; - - // Spawn task to receive flashblocks from the test context - tokio::spawn(async move { - while let Some((payload, tx)) = receiver.recv().await { - fb.on_flashblock_received(payload); - tx.send(()).unwrap(); - } - }); - - Ok(()) - }) - .launch() + // .install_exex("flashblocks-canon", { + // let fb_cell = fb_cell.clone(); + // move |mut ctx| async move { + // let fb = fb_cell + // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + // .clone(); + // Ok(async move { + // while let Some(note) = ctx.notifications.try_next().await? { + // if let Some(committed) = note.committed_chain() { + // for b in committed.blocks_iter() { + // fb.on_canonical_block_received(b); + // } + // let _ = ctx + // .events + // .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + // } + // } + // Ok(()) + // }) + // } + // }) + // .extend_rpc_modules(move |ctx| { + // let fb = fb_cell + // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + // .clone(); + // fb.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())?; + // // Spawn task to receive flashblocks from the test context + // tokio::spawn(async move { + // while let Some((payload, tx)) = receiver.recv().await { + // fb.on_flashblock_received(payload); + // tx.send(()).unwrap(); + // } + // }); + // Ok(()) + // }) + .launch_with_fn(launcher) .await?; let http_api_addr = node_handle @@ -134,19 +152,19 @@ impl LocalNode { Ok(Self { http_api_addr, engine_ipc_path, - flashblock_sender: sender, + // flashblock_sender: sender, _node_exit_future: node_exit_future, _node: Box::new(node_handle), _task_manager: tasks, }) } - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.flashblock_sender.send((flashblock, tx)).await?; - rx.await?; - Ok(()) - } + // pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + // let (tx, rx) = oneshot::channel(); + // self.flashblock_sender.send((flashblock, tx)).await?; + // rx.await?; + // Ok(()) + // } pub fn provider(&self) -> Result> { let url = format!("http://{}", self.http_api_addr); From ce1b5a73f6e6908298d9d39d76e3082c1bc293d4 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 23:29:18 -0500 Subject: [PATCH 06/38] remove foundry github workflow --- contracts/.github/workflows/test.yml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 contracts/.github/workflows/test.yml diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml deleted file mode 100644 index c24b9832..00000000 --- a/contracts/.github/workflows/test.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: CI - -permissions: - contents: read - -on: - push: - pull_request: - workflow_dispatch: - -env: - FOUNDRY_PROFILE: ci - -jobs: - check: - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - - name: Show Forge version - run: forge --version - - - name: Run Forge fmt - run: forge fmt --check - - - name: Run Forge build - run: forge build --sizes - - - name: Run Forge tests - run: forge test -vvv From 2c70a4d87f125ce6e56c66e6f3d0cd83369e0911 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Tue, 4 Nov 2025 12:19:39 -0500 Subject: [PATCH 07/38] wip: first test migration --- .../src/tests/framework_test.rs | 181 ++++++++++++++++-- crates/test-utils/src/node.rs | 7 - 2 files changed, 167 insertions(+), 21 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 8bb27456..3eeac3a3 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -1,19 +1,172 @@ -use base_reth_test_utils::harness::TestHarness; -use eyre::Result; -use reth::api::FullNodeComponents; -use reth_exex::ExExContext; +#[cfg(test)] +mod tests { + use crate::rpc::{EthApiExt, EthApiOverrideServer}; + use crate::state::FlashblocksState; + use crate::subscription::{Flashblock, FlashblocksReceiver, Metadata}; + use crate::tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + use alloy_consensus::Receipt; + use alloy_eips::BlockNumberOrTag; + use alloy_primitives::map::HashMap; + use alloy_primitives::{Address, Bytes, B256, U256}; + use alloy_provider::Provider; + use alloy_rpc_types_engine::PayloadId; + use base_reth_test_utils::harness::TestHarness; + use eyre::Result; + use once_cell::sync::OnceCell; + use op_alloy_consensus::OpDepositReceipt; + use reth_exex::ExExEvent; + use reth_optimism_primitives::OpReceipt; + use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + use std::sync::Arc; + use tokio::sync::{mpsc, oneshot}; + use tokio_stream::StreamExt; -use futures_util::{Future, TryStreamExt}; -use reth_exex::{ExExEvent, ExExNotification}; -use reth_tracing::tracing::info; + pub struct TestSetup { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + harness: TestHarness, + } -#[cfg(test)] -#[tokio::test] -async fn test_framework_test() -> Result<()> { - use base_reth_test_utils::node::default_launcher; + impl TestSetup { + pub async fn new() -> Result { + let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let harness = TestHarness::new(|builder| { + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + + builder + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| { + Arc::new(FlashblocksState::new(ctx.provider().clone())) + }) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx.events.send(ExExEvent::FinishedHeight( + committed.tip().num_hash(), + )); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.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())?; + + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb.on_flashblock_received(payload); + tx.send(()).unwrap(); + } + }); + + Ok(()) + }) + .launch() + }) + .await?; + + Ok(Self { sender, harness }) + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + } + + fn create_first_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 0, + base: Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: B256::default(), + parent_hash: B256::default(), + fee_recipient: Address::ZERO, + prev_randao: B256::default(), + block_number: 1, + gas_limit: 30_000_000, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + }), + diff: ExecutionPayloadFlashblockDeltaV1 { + transactions: vec![BLOCK_INFO_TXN], + ..Default::default() + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + BLOCK_INFO_TXN_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 10000, + logs: vec![], + }, + deposit_nonce: Some(4012991u64), + deposit_receipt_version: None, + }), + ); + receipts + }, + new_account_balances: HashMap::default(), + }, + } + } + + #[tokio::test] + async fn test_get_pending_block() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let latest_block = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("latest block expected"); + assert_eq!(latest_block.number(), 0); + + // Querying pending block when it does not exist yet + let pending_block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await?; + assert_eq!(pending_block.is_none(), true); + + let base_payload = create_first_payload(); + setup.send_flashblock(base_payload).await?; + + // Query pending block after sending the base payload with an empty delta + let pending_block = provider + .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) + .await? + .expect("pending block expected"); - let harness = TestHarness::new(default_launcher).await?; - let provider = harness.provider(); + assert_eq!(pending_block.number(), 1); + assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction - Ok(()) + Ok(()) + } } diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 8a3a609b..5cf1ec27 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -34,17 +34,10 @@ pub struct LocalNode { _task_manager: TaskManager, } -// Full node types for OpNode over the TmpDB used in tests pub type OpTypes = FullNodeTypesAdapter>>; - -// Components builder for OpNode pub type OpComponentsBuilder = >::ComponentsBuilder; - -// AddOns (this includes the EthApi type etc.) pub type OpAddOns = >::AddOns; - -// The builder type we’re going to pass into launch_with_fn pub type OpBuilder = WithLaunchContext>; From f9efa86cd136b23764ba0b74af856fe73c1b71ac Mon Sep 17 00:00:00 2001 From: Haardik H Date: Wed, 5 Nov 2025 11:25:55 -0500 Subject: [PATCH 08/38] migrate most rpc.rs tests --- Cargo.lock | 2 + .../src/tests/assets/genesis.json | 2 +- .../src/tests/framework_test.rs | 675 +++++++++++++++++- crates/metering/src/tests/assets/genesis.json | 2 +- crates/test-utils/Cargo.toml | 2 + crates/test-utils/assets/genesis.json | 2 +- crates/test-utils/src/accounts.rs | 41 +- crates/test-utils/src/harness.rs | 6 +- crates/test-utils/src/node.rs | 29 +- 9 files changed, 746 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb006136..98b84d41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1686,6 +1686,8 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", + "alloy-signer", + "alloy-signer-local", "chrono", "eyre", "futures", diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/src/tests/assets/genesis.json index 79ab75e9..b3099c33 100644 --- a/crates/flashblocks-rpc/src/tests/assets/genesis.json +++ b/crates/flashblocks-rpc/src/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 3eeac3a3..14a4cc59 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -7,16 +7,24 @@ mod tests { use alloy_consensus::Receipt; use alloy_eips::BlockNumberOrTag; use alloy_primitives::map::HashMap; - use alloy_primitives::{Address, Bytes, B256, U256}; + use alloy_primitives::{address, b256, bytes, Address, Bytes, LogData, TxHash, B256, U256}; use alloy_provider::Provider; + use alloy_rpc_client::RpcClient; + use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; + use alloy_rpc_types_eth::error::EthRpcErrorCode; + use alloy_rpc_types_eth::TransactionInput; use base_reth_test_utils::harness::TestHarness; use eyre::Result; use once_cell::sync::OnceCell; use op_alloy_consensus::OpDepositReceipt; + use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; + use op_alloy_rpc_types::OpTransactionRequest; use reth_exex::ExExEvent; use reth_optimism_primitives::OpReceipt; + use reth_rpc_eth_api::RpcReceipt; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + use std::str::FromStr; use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use tokio_stream::StreamExt; @@ -93,6 +101,91 @@ mod tests { rx.await?; Ok(()) } + + pub async fn send_test_payloads(&self) -> Result<()> { + let base_payload = create_first_payload(); + self.send_flashblock(base_payload).await?; + + let second_payload = create_second_payload(); + self.send_flashblock(second_payload).await?; + + Ok(()) + } + + pub async fn send_raw_transaction_sync( + &self, + tx: Bytes, + timeout_ms: Option, + ) -> Result> { + let url = self.harness.rpc_url(); + let client = RpcClient::new_http(url.parse()?); + + let receipt = client + .request::<_, RpcReceipt>("eth_sendRawTransactionSync", (tx, timeout_ms)) + .await?; + + Ok(receipt) + } + } + + // Test constants + const TEST_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); + const PENDING_BALANCE: u64 = 4660; + + const DEPOSIT_SENDER: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001"); + const TX_SENDER: Address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); + + const DEPOSIT_TX_HASH: TxHash = + b256!("0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548"); + const TRANSFER_ETH_HASH: TxHash = + b256!("0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a"); + + const DEPLOYMENT_HASH: TxHash = + b256!("0x2b14d58c13406f25a78cfb802fb711c0d2c27bf9eccaec2d1847dc4392918f63"); + + const INCREMENT_HASH: TxHash = + b256!("0x993ad6a332752f6748636ce899b3791e4a33f7eece82c0db4556c7339c1b2929"); + const INCREMENT2_HASH: TxHash = + b256!("0x617a3673399647d12bb82ec8eba2ca3fc468e99894bcf1c67eb50ef38ee615cb"); + + const COUNTER_ADDRESS: Address = address!("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"); + + // Test log topics - these represent common events + const TEST_LOG_TOPIC_0: B256 = + b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); // Transfer event + const TEST_LOG_TOPIC_1: B256 = + b256!("0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"); // From address + const TEST_LOG_TOPIC_2: B256 = + b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address + + // Transaction bytes + const DEPOSIT_TX: Bytes = bytes!("0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3"); + const TRANSFER_ETH_TX: Bytes = bytes!("0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31"); + const DEPLOYMENT_TX: Bytes = bytes!("0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c"); + const INCREMENT_TX: Bytes = bytes!("0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89"); + const INCREMENT2_TX: Bytes = bytes!("0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2"); + + fn create_test_logs() -> Vec { + vec![ + alloy_primitives::Log { + address: COUNTER_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], + bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") + .into(), // 1 ETH in wei + ) + .unwrap(), + }, + alloy_primitives::Log { + address: TEST_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0], + bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") + .into(), // Value: 1 + ) + .unwrap(), + }, + ] } fn create_first_payload() -> Flashblock { @@ -137,6 +230,87 @@ mod tests { } } + fn create_second_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 1, + base: None, + diff: ExecutionPayloadFlashblockDeltaV1 { + state_root: B256::default(), + receipts_root: B256::default(), + gas_used: 0, + block_hash: B256::default(), + transactions: vec![ + DEPOSIT_TX, + TRANSFER_ETH_TX, + DEPLOYMENT_TX, + INCREMENT_TX, + INCREMENT2_TX, + ], + withdrawals: Vec::new(), + logs_bloom: Default::default(), + withdrawals_root: Default::default(), + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + DEPOSIT_TX_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 31000, + logs: vec![], + }, + deposit_nonce: Some(4012992u64), + deposit_receipt_version: None, + }), + ); + receipts.insert( + TRANSFER_ETH_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 55000, + logs: vec![], + }), + ); + receipts.insert( + DEPLOYMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279, + logs: vec![], + }), + ); + receipts.insert( + INCREMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000, + logs: create_test_logs(), + }), + ); + receipts.insert( + INCREMENT2_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000 + 44000, + logs: vec![], + }), + ); + receipts + }, + new_account_balances: { + let mut map = HashMap::default(); + map.insert(TEST_ADDRESS, U256::from(PENDING_BALANCE)); + map.insert(COUNTER_ADDRESS, U256::from(0)); + map + }, + }, + } + } + #[tokio::test] async fn test_get_pending_block() -> Result<()> { reth_tracing::init_test_tracing(); @@ -169,4 +343,503 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_get_balance_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + let balance = provider.get_balance(TEST_ADDRESS).await?; + assert_eq!(balance, U256::ZERO); + + let pending_balance = provider.get_balance(TEST_ADDRESS).pending().await?; + assert_eq!(pending_balance, U256::from(PENDING_BALANCE)); + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_by_hash_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert!(provider + .get_transaction_by_hash(DEPOSIT_TX_HASH) + .await? + .is_none()); + assert!(provider + .get_transaction_by_hash(TRANSFER_ETH_HASH) + .await? + .is_none()); + + setup.send_test_payloads().await?; + + let tx1 = provider + .get_transaction_by_hash(DEPOSIT_TX_HASH) + .await? + .expect("tx1 expected"); + assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); + assert_eq!(tx1.from(), DEPOSIT_SENDER); + + let tx2 = provider + .get_transaction_by_hash(TRANSFER_ETH_HASH) + .await? + .expect("tx2 expected"); + assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); + assert_eq!(tx2.from(), TX_SENDER); + + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_receipt_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; + assert_eq!(receipt.is_none(), true); + + setup.send_test_payloads().await?; + + let receipt = provider + .get_transaction_receipt(DEPOSIT_TX_HASH) + .await? + .expect("receipt expected"); + assert_eq!(receipt.gas_used(), 21000); + + let receipt = provider + .get_transaction_receipt(TRANSFER_ETH_HASH) + .await? + .expect("receipt expected"); + assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 + + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_count() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!( + provider.get_transaction_count(TX_SENDER).pending().await?, + 0 + ); + + setup.send_test_payloads().await?; + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!( + provider.get_transaction_count(TX_SENDER).pending().await?, + 4 + ); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_call() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_call will succeed because we are on plain state + let send_eth_call = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = provider + .call(send_eth_call.clone()) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_call with fail + let res = provider + .call(send_eth_call.nonce(4)) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_err()); + assert!(res + .unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas")); + + // read count1 from counter contract + let eth_call_count1 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(5) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))); + let res_count1 = provider.call(eth_call_count1).await; + assert!(res_count1.is_ok()); + assert_eq!( + U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + // read count2 from counter contract + let eth_call_count2 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(6) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xd631c639"))); + let res_count2 = provider.call(eth_call_count2).await; + assert!(res_count2.is_ok()); + assert_eq!( + U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_estimate_gas() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_estimate_gas will succeed because we are on plain state + let send_estimate_gas = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = provider + .estimate_gas(send_estimate_gas.clone()) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_estimate_gas with fail + let res = provider + .estimate_gas(send_estimate_gas.nonce(4)) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_err()); + assert!(res + .unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas")); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_simulate_v1() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + setup.send_test_payloads().await?; + + let simulate_call = SimulatePayload { + block_state_calls: vec![SimBlock { + calls: vec![ + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + // increment() value in contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .input(TransactionInput::new(bytes!("0xd09de08a"))) + .into(), + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + ], + block_overrides: None, + state_overrides: None, + }], + trace_transfers: false, + validation: true, + return_full_transactions: true, + }; + let simulate_res = provider + .simulate(&simulate_call) + .block_id(BlockNumberOrTag::Pending.into()) + .await; + assert!(simulate_res.is_ok()); + let block = simulate_res.unwrap(); + assert_eq!(block.len(), 1); + assert_eq!(block[0].calls.len(), 3); + assert_eq!( + block[0].calls[0].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000002") + ); + assert_eq!(block[0].calls[1].return_data, bytes!("0x")); + assert_eq!( + block[0].calls[2].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000003") + ); + + Ok(()) + } + + #[tokio::test] + async fn test_send_raw_transaction_sync() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + + setup.send_flashblock(create_first_payload()).await?; + + // run the Tx sync and, in parallel, deliver the payload that contains the Tx + let (receipt_result, payload_result) = tokio::join!( + setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), + async { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + setup.send_flashblock(create_second_payload()).await + } + ); + + payload_result?; + let receipt = receipt_result?; + + assert_eq!(receipt.transaction_hash(), TRANSFER_ETH_HASH); + Ok(()) + } + + #[tokio::test] + async fn test_send_raw_transaction_sync_timeout() { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await.unwrap(); + + // fail request immediately by passing a timeout of 0 ms + let receipt_result = setup + .send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)) + .await; + + let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); + assert!(receipt_result + .err() + .unwrap() + .to_string() + .contains(format!("{}", error_code).as_str())); + } + + #[tokio::test] + async fn test_get_logs_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // Test no logs when no flashblocks sent + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .select(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + assert_eq!(logs.len(), 0); + + // Send payloads with transactions + setup.send_test_payloads().await?; + + // Test getting pending logs - must use both fromBlock and toBlock as "pending" + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // We should now have 2 logs from the INCREMENT_TX transaction + assert_eq!(logs.len(), 2); + + // Verify the first log is from COUNTER_ADDRESS + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Verify the second log is from TEST_ADDRESS + assert_eq!(logs[1].address(), TEST_ADDRESS); + assert_eq!(logs[1].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[1].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_filter_by_address() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by a specific address (COUNTER_ADDRESS) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(COUNTER_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from COUNTER_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Test filtering by TEST_ADDRESS + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(TEST_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from TEST_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), TEST_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_topic_filtering() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by topic - should match both logs + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .event_signature(TEST_LOG_TOPIC_0) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.topics()[0] == TEST_LOG_TOPIC_0)); + + // Test filtering by specific topic combination - should match only the first log + let filter = alloy_rpc_types_eth::Filter::default() + .topic1(TEST_LOG_TOPIC_1) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending); + + let logs = provider.get_logs(&filter).await?; + + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[1], TEST_LOG_TOPIC_1); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_mixed_block_ranges() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test fromBlock: 0, toBlock: pending (should include both historical and pending) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(0) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should now include pending logs (2 logs from our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: latest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Latest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: earliest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Earliest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + Ok(()) + } } diff --git a/crates/metering/src/tests/assets/genesis.json b/crates/metering/src/tests/assets/genesis.json index 4d703497..dbdbfe69 100644 --- a/crates/metering/src/tests/assets/genesis.json +++ b/crates/metering/src/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index be0cc9a6..56469561 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -42,6 +42,8 @@ alloy-consensus.workspace = true alloy-provider.workspace = true alloy-rpc-client.workspace = true alloy-serde.workspace = true +alloy-signer = "1.0" +alloy-signer-local = "1.1.0" # op-alloy op-alloy-rpc-types.workspace = true diff --git a/crates/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json index 79ab75e9..b3099c33 100644 --- a/crates/test-utils/assets/genesis.json +++ b/crates/test-utils/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs index 63bf4d8c..65ac2ccc 100644 --- a/crates/test-utils/src/accounts.rs +++ b/crates/test-utils/src/accounts.rs @@ -1,10 +1,15 @@ //! Test accounts with pre-funded balances for integration testing -use alloy_primitives::{address, Address}; +use alloy_consensus::{SignableTransaction, TxLegacy}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{address, hex, Address, Bytes, FixedBytes, U256}; +use alloy_signer::SignerSync; +use alloy_signer_local::PrivateKeySigner; +use eyre::Result; /// Hardcoded test account with a fixed private key #[derive(Debug, Clone)] -pub struct TestAccount { +pub struct Account { /// Account name for easy identification pub name: &'static str, /// Ethereum address @@ -13,6 +18,38 @@ pub struct TestAccount { pub private_key: &'static str, } +impl Account { + /// Sign a simple ETH transfer transaction and return the signed bytes + pub fn sign_transaction_bytes( + &self, + to: Address, + value: U256, + nonce: u64, + chain_id: u64, + ) -> Result { + let key_bytes = hex::decode(self.private_key)?; + let key_fixed: FixedBytes<32> = FixedBytes::from_slice(&key_bytes); + let signer = PrivateKeySigner::from_bytes(&key_fixed)?; + + let tx = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: 200, + gas_limit: 21_000, + to: alloy_primitives::TxKind::Call(to), + value, + input: Bytes::new(), + }; + + let signature = signer.sign_hash_sync(&tx.signature_hash())?; + let signed_tx = tx.into_signed(signature); + + Ok(signed_tx.encoded_2718().into()) + } +} + +pub type TestAccount = Account; + /// Collection of all test accounts #[derive(Debug, Clone)] pub struct TestAccounts { diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 9bb9ba11..854e679a 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -58,7 +58,11 @@ impl TestHarness { &self.accounts } - async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { + pub fn rpc_url(&self) -> String { + format!("http://{}", self.node.http_api_addr) + } + + pub async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { let latest_block = self .provider() .get_block_by_number(BlockNumberOrTag::Latest) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 5cf1ec27..7c4f9d91 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -23,10 +23,10 @@ use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; -pub const BASE_CHAIN_ID: u64 = 8453; +pub const BASE_CHAIN_ID: u64 = 84532; pub struct LocalNode { - http_api_addr: SocketAddr, + pub(crate) http_api_addr: SocketAddr, engine_ipc_path: String, // flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, _node_exit_future: NodeExitFuture, @@ -68,14 +68,27 @@ impl LocalNode { ..NetworkArgs::default() }; + // Generate unique IPC path for this test instance to avoid conflicts + // Use timestamp + thread ID + process ID for uniqueness + 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(); + rpc_args.auth_ipc_path = unique_ipc_path; + let node_config = NodeConfig::new(chain_spec.clone()) .with_network(network_config) - .with_rpc( - RpcServerArgs::default() - .with_unused_ports() - .with_http() - .with_auth_ipc(), - ) + .with_rpc(rpc_args) .with_unused_ports(); let node = OpNode::new(RollupArgs::default()); From 1b4ba2a9ca55c545e15fd0b74f210e8e7366ea0e Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Fri, 14 Nov 2025 12:08:04 -0600 Subject: [PATCH 09/38] test-utils: wire flashblocks state into harness --- Cargo.lock | 1 + crates/test-utils/Cargo.toml | 2 + crates/test-utils/README.md | 6 +- crates/test-utils/src/harness.rs | 46 +++++++++- crates/test-utils/src/node.rs | 148 ++++++++++++++++++++----------- 5 files changed, 145 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98b84d41..22db87a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1688,6 +1688,7 @@ dependencies = [ "alloy-serde", "alloy-signer", "alloy-signer-local", + "base-reth-flashblocks-rpc", "chrono", "eyre", "futures", diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 56469561..19df0084 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -78,4 +78,6 @@ chrono.workspace = true # tower for middleware tower = "0.5" +base-reth-flashblocks-rpc.workspace = true + [dev-dependencies] diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 630eb2b1..6d020e9b 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -104,9 +104,9 @@ async fn test_harness() -> eyre::Result<()> { - `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 -- `build_block_from_flashblocks(&flashblocks)` - Extract txs from flashblocks and build block -- `send_flashblock(fb)` - Send flashblock to node for pending state +- `build_block_from_transactions(txs)` - Build block with specific transactions (auto-prepends the L1 block info deposit) +- `send_flashblock(fb)` - Send a single flashblock to the node for pending state processing +- `send_flashblocks(iter)` - Convenience helper that sends multiple flashblocks sequentially **Block Building Process:** 1. Fetches latest block header from provider (no local state tracking) diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 854e679a..84b46fc5 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -2,12 +2,13 @@ use crate::accounts::TestAccounts; use crate::engine::{EngineApi, IpcEngine}; -use crate::node::{LocalNode, OpAddOns, OpBuilder}; +use crate::node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}; use alloy_eips::eip7685::Requests; -use alloy_primitives::{Bytes, B256}; +use alloy_primitives::{bytes, Bytes, B256}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; +use base_reth_flashblocks_rpc::subscription::Flashblock; use eyre::{eyre, Result}; use futures_util::Future; use op_alloy_network::Optimism; @@ -15,6 +16,7 @@ use op_alloy_rpc_types_engine::OpPayloadAttributes; use reth::builder::NodeHandle; use reth_e2e_test_utils::Adapter; use reth_optimism_node::OpNode; +use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; @@ -22,6 +24,8 @@ const BLOCK_TIME_SECONDS: u64 = 2; const GAS_LIMIT: u64 = 200_000_000; const NODE_STARTUP_DELAY_MS: u64 = 500; const BLOCK_BUILD_DELAY_MS: u64 = 100; +// Pre-captured L1 block info deposit transaction required by OP Stack. +const L1_BLOCK_INFO_DEPOSIT_TX: Bytes = bytes!("0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000"); pub struct TestHarness { node: LocalNode, @@ -58,11 +62,27 @@ impl TestHarness { &self.accounts } + pub fn blockchain_provider(&self) -> LocalNodeProvider { + self.node.blockchain_provider() + } + + pub fn flashblocks_state(&self) -> Arc { + self.node.flashblocks_state() + } + pub fn rpc_url(&self) -> String { format!("http://{}", self.node.http_api_addr) } - pub async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { + pub async fn build_block_from_transactions(&self, mut transactions: Vec) -> Result<()> { + // Ensure the block always starts with the required L1 block info deposit. + if !transactions + .first() + .is_some_and(|tx| tx == &L1_BLOCK_INFO_DEPOSIT_TX) + { + transactions.insert(0, L1_BLOCK_INFO_DEPOSIT_TX.clone()); + } + let latest_block = self .provider() .get_block_by_number(BlockNumberOrTag::Latest) @@ -70,12 +90,16 @@ impl TestHarness { .ok_or_else(|| eyre!("No genesis block found"))?; let parent_hash = latest_block.header.hash; + let parent_beacon_block_root = latest_block + .header + .parent_beacon_block_root + .unwrap_or(B256::ZERO); let next_timestamp = latest_block.header.timestamp + BLOCK_TIME_SECONDS; let payload_attributes = OpPayloadAttributes { payload_attributes: PayloadAttributes { timestamp: next_timestamp, - parent_beacon_block_root: Some(B256::ZERO), + parent_beacon_block_root: Some(parent_beacon_block_root), withdrawals: Some(vec![]), ..Default::default() }, @@ -129,6 +153,20 @@ impl TestHarness { Ok(()) } + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.node.send_flashblock(flashblock).await + } + + pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> + where + I: IntoIterator, + { + for flashblock in flashblocks { + self.send_flashblock(flashblock).await?; + } + Ok(()) + } + pub async fn advance_chain(&self, n: u64) -> Result<()> { for _ in 0..n { self.build_block_from_transactions(vec![]).await?; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 7c4f9d91..cd713b8c 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -4,8 +4,12 @@ use crate::engine::EngineApi; use alloy_genesis::Genesis; use alloy_provider::RootProvider; use alloy_rpc_client::RpcClient; +use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; +use base_reth_flashblocks_rpc::state::FlashblocksState; +use base_reth_flashblocks_rpc::subscription::{Flashblock, FlashblocksReceiver}; use eyre::Result; use futures_util::Future; +use once_cell::sync::OnceCell; use op_alloy_network::Optimism; use reth::api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}; use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; @@ -15,6 +19,7 @@ use reth::builder::{ use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; use reth_e2e_test_utils::{Adapter, TmpDB}; +use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; @@ -22,13 +27,20 @@ use reth_provider::providers::BlockchainProvider; use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; pub const BASE_CHAIN_ID: u64 = 84532; +pub type LocalNodeProvider = BlockchainProvider>; +pub type LocalFlashblocksState = FlashblocksState; + pub struct LocalNode { pub(crate) http_api_addr: SocketAddr, engine_ipc_path: String, - // flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + flashblocks_state: Arc, + provider: LocalNodeProvider, _node_exit_future: NodeExitFuture, _node: Box, _task_manager: TaskManager, @@ -93,8 +105,9 @@ impl LocalNode { let node = OpNode::new(RollupArgs::default()); - // let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - // let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let provider_cell: Arc> = Arc::new(OnceCell::new()); let NodeHandle { node: node_handle, @@ -104,47 +117,59 @@ impl LocalNode { .with_types_and_provider::>() .with_components(node.components_builder()) .with_add_ons(node.add_ons()) - // .install_exex("flashblocks-canon", { - // let fb_cell = fb_cell.clone(); - // move |mut ctx| async move { - // let fb = fb_cell - // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - // .clone(); - // Ok(async move { - // while let Some(note) = ctx.notifications.try_next().await? { - // if let Some(committed) = note.committed_chain() { - // for b in committed.blocks_iter() { - // fb.on_canonical_block_received(b); - // } - // let _ = ctx - // .events - // .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); - // } - // } - // Ok(()) - // }) - // } - // }) - // .extend_rpc_modules(move |ctx| { - // let fb = fb_cell - // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - // .clone(); - // fb.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())?; - // // Spawn task to receive flashblocks from the test context - // tokio::spawn(async move { - // while let Some((payload, tx)) = receiver.recv().await { - // fb.on_flashblock_received(payload); - // tx.send(()).unwrap(); - // } - // }); - // Ok(()) - // }) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + move |mut ctx| async move { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(provider.clone()))) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for block in committed.blocks_iter() { + fb.on_canonical_block_received(block); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules({ + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + let mut receiver = Some(receiver); + move |ctx| { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(provider.clone()))) + .clone(); + fb.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())?; + // Spawn task to receive flashblocks from the test context + let fb_for_task = fb.clone(); + let mut receiver = receiver + .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(()) + } + }) .launch_with_fn(launcher) .await?; @@ -154,23 +179,36 @@ impl LocalNode { .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; let engine_ipc_path = node_config.rpc.auth_ipc_path; + let flashblocks_state = fb_cell + .get() + .expect("FlashblocksState should be initialized during node launch") + .clone(); + let provider = provider_cell + .get() + .expect("Provider should be initialized during node launch") + .clone(); Ok(Self { http_api_addr, engine_ipc_path, - // flashblock_sender: sender, + flashblock_sender: sender, + flashblocks_state, + provider, _node_exit_future: node_exit_future, _node: Box::new(node_handle), _task_manager: tasks, }) } - // pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - // let (tx, rx) = oneshot::channel(); - // self.flashblock_sender.send((flashblock, tx)).await?; - // rx.await?; - // Ok(()) - // } + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.flashblock_sender + .send((flashblock, tx)) + .await + .map_err(|err| eyre::eyre!(err))?; + rx.await.map_err(|err| eyre::eyre!(err))?; + Ok(()) + } pub fn provider(&self) -> Result> { let url = format!("http://{}", self.http_api_addr); @@ -181,4 +219,12 @@ impl LocalNode { pub fn engine_api(&self) -> Result> { EngineApi::::new(self.engine_ipc_path.clone()) } + + pub fn flashblocks_state(&self) -> Arc { + self.flashblocks_state.clone() + } + + pub fn blockchain_provider(&self) -> LocalNodeProvider { + self.provider.clone() + } } From f0c746e6470a1ccd0abd0b055433d5891a90725a Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Mon, 17 Nov 2025 15:34:02 -0600 Subject: [PATCH 10/38] Remove legacy flashblocks RPC tests --- .../src/tests/framework_test.rs | 12 + crates/flashblocks-rpc/src/tests/mod.rs | 1 - crates/flashblocks-rpc/src/tests/rpc.rs | 921 ------------------ 3 files changed, 12 insertions(+), 922 deletions(-) delete mode 100644 crates/flashblocks-rpc/src/tests/rpc.rs diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 14a4cc59..a1e1bc5b 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -341,6 +341,18 @@ mod tests { assert_eq!(pending_block.number(), 1); assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction + let second_payload = create_second_payload(); + setup.send_flashblock(second_payload).await?; + + // Query pending block after sending the second payload with two transactions + let block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await? + .expect("pending block expected"); + + assert_eq!(block.number(), 1); + assert_eq!(block.transactions.hashes().len(), 6); + Ok(()) } diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/src/tests/mod.rs index 1bc36868..f0684fef 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/src/tests/mod.rs @@ -1,7 +1,6 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; mod framework_test; -mod rpc; mod state; mod utils; diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs deleted file mode 100644 index 8f5ec397..00000000 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ /dev/null @@ -1,921 +0,0 @@ -#[cfg(test)] -mod tests { - use std::{any::Any, net::SocketAddr, str::FromStr, sync::Arc}; - - use alloy_consensus::Receipt; - use alloy_eips::BlockNumberOrTag; - use alloy_genesis::Genesis; - use alloy_primitives::{ - Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes, map::HashMap, - }; - use alloy_provider::{Provider, RootProvider}; - use alloy_rpc_client::RpcClient; - use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; - use alloy_rpc_types_engine::PayloadId; - use alloy_rpc_types_eth::error::EthRpcErrorCode; - use alloy_rpc_types_eth::TransactionInput; - use once_cell::sync::OnceCell; - use op_alloy_consensus::OpDepositReceipt; - use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; - use op_alloy_rpc_types::OpTransactionRequest; - use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; - use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; - use reth::chainspec::Chain; - use reth::core::exit::NodeExitFuture; - use reth::tasks::TaskManager; - use reth_exex::ExExEvent; - use reth_optimism_chainspec::OpChainSpecBuilder; - use reth_optimism_node::{OpNode, args::RollupArgs}; - use reth_optimism_primitives::OpReceipt; - use reth_provider::providers::BlockchainProvider; - use reth_rpc_eth_api::RpcReceipt; - use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - use serde_json; - use tokio::sync::{mpsc, oneshot}; - use tokio_stream::StreamExt; - - use crate::{ - rpc::{EthApiExt, EthApiOverrideServer}, - state::FlashblocksState, - subscription::{Flashblock, FlashblocksReceiver, Metadata}, - tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}, - }; - - pub struct NodeContext { - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - http_api_addr: SocketAddr, - _node_exit_future: NodeExitFuture, - _node: Box, - _task_manager: TaskManager, - } - - impl NodeContext { - pub async fn send_payload(&self, payload: Flashblock) -> eyre::Result<()> { - let (tx, rx) = oneshot::channel(); - self.sender.send((payload, tx)).await?; - rx.await?; - Ok(()) - } - - pub async fn provider(&self) -> eyre::Result> { - let url = format!("http://{}", self.http_api_addr); - let client = RpcClient::builder().http(url.parse()?); - - Ok(RootProvider::::new(client)) - } - - pub async fn send_test_payloads(&self) -> eyre::Result<()> { - let base_payload = create_first_payload(); - self.send_payload(base_payload).await?; - - let second_payload = create_second_payload(); - self.send_payload(second_payload).await?; - - Ok(()) - } - - pub async fn send_raw_transaction_sync( - &self, - tx: Bytes, - timeout_ms: Option, - ) -> eyre::Result> { - let url = format!("http://{}", self.http_api_addr); - let client = RpcClient::new_http(url.parse()?); - - let receipt = client - .request::<_, RpcReceipt>("eth_sendRawTransactionSync", (tx, timeout_ms)) - .await?; - - Ok(receipt) - } - } - - async fn setup_node() -> eyre::Result { - let tasks = TaskManager::current(); - let exec = tasks.executor(); - const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; - - let genesis: Genesis = serde_json::from_str(include_str!("assets/genesis.json")).unwrap(); - let chain_spec = Arc::new( - OpChainSpecBuilder::base_mainnet() - .genesis(genesis) - .ecotone_activated() - .chain(Chain::from(BASE_SEPOLIA_CHAIN_ID)) - .build(), - ); - - let network_config = NetworkArgs { - discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, - ..NetworkArgs::default() - }; - - // Use with_unused_ports() to let Reth allocate random ports and avoid port collisions - 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()); - - // Start dummy websocket server to simulate the builder and send payloads back to the node - let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - - 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()) - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - move |mut ctx| async move { - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - Ok(async move { - while let Some(note) = ctx.notifications.try_next().await? { - if let Some(committed) = note.committed_chain() { - for b in committed.blocks_iter() { - fb.on_canonical_block_received(b); - } - let _ = ctx - .events - .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules(move |ctx| { - // We are not going to use the websocket connection to send payloads so we don't - // initialize a flashblocks subscriber - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - - fb.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())?; - - tokio::spawn(async move { - while let Some((payload, tx)) = receiver.recv().await { - fb.on_flashblock_received(payload); - tx.send(()).unwrap(); - } - }); - - 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 { - sender, - http_api_addr, - _node_exit_future: node_exit_future, - _node: Box::new(node), - _task_manager: tasks, - }) - } - - fn create_first_payload() -> Flashblock { - Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: B256::default(), - parent_hash: B256::default(), - fee_recipient: Address::ZERO, - prev_randao: B256::default(), - block_number: 1, - gas_limit: 30_000_000, - timestamp: 0, - extra_data: Bytes::new(), - base_fee_per_gas: U256::ZERO, - }), - diff: ExecutionPayloadFlashblockDeltaV1 { - transactions: vec![BLOCK_INFO_TXN], - ..Default::default() - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - BLOCK_INFO_TXN_HASH, - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 10000, - logs: vec![], - }, - deposit_nonce: Some(4012991u64), - deposit_receipt_version: None, - }), - ); - receipts - }, - new_account_balances: HashMap::default(), - }, - } - } - - const TEST_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); - const PENDING_BALANCE: u64 = 4660; - - const DEPOSIT_SENDER: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001"); - const TX_SENDER: Address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); - - const DEPOSIT_TX_HASH: TxHash = - b256!("0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548"); - const TRANSFER_ETH_HASH: TxHash = - b256!("0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a"); - - const DEPLOYMENT_HASH: TxHash = - b256!("0x2b14d58c13406f25a78cfb802fb711c0d2c27bf9eccaec2d1847dc4392918f63"); - - const INCREMENT_HASH: TxHash = - b256!("0x993ad6a332752f6748636ce899b3791e4a33f7eece82c0db4556c7339c1b2929"); - const INCREMENT2_HASH: TxHash = - b256!("0x617a3673399647d12bb82ec8eba2ca3fc468e99894bcf1c67eb50ef38ee615cb"); - - const COUNTER_ADDRESS: Address = address!("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"); - - // Test log topics - these represent common events - const TEST_LOG_TOPIC_0: B256 = - b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); // Transfer event - const TEST_LOG_TOPIC_1: B256 = - b256!("0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"); // From address - const TEST_LOG_TOPIC_2: B256 = - b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address - - fn create_test_logs() -> Vec { - vec![ - alloy_primitives::Log { - address: COUNTER_ADDRESS, - data: LogData::new( - vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], - bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") - .into(), // 1 ETH in wei - ) - .unwrap(), - }, - alloy_primitives::Log { - address: TEST_ADDRESS, - data: LogData::new( - vec![TEST_LOG_TOPIC_0], - bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") - .into(), // Value: 1 - ) - .unwrap(), - }, - ] - } - - // NOTE: - // To create tx use cast mktx/ - // Example: `cast mktx --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --nonce 1 --gas-limit 100000 --gas-price 1499576 --chain 84532 --value 0 --priority-gas-price 0 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 0x` - // Create second payload (index 1) with transactions - // tx1 hash: 0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548 (deposit transaction) - // tx2 hash: 0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a - const DEPOSIT_TX: Bytes = bytes!( - "0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3" - ); - const TRANSFER_ETH_TX: Bytes = bytes!( - "0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31" - ); - - // NOTE: - // Following txns deploy a double Counter contract (Compiled with solc 0.8.13) - // contains a `uint256 public count = 1` and a function increment() { count++ }; - // and a `uint256 public count2 = 1` and a function increment2() { count2++ }; - // Following txn calls increment once, so count should be 2 - // Raw Bytecode: 0x608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033 - const DEPLOYMENT_TX: Bytes = bytes!( - "0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c" - ); - // Increment tx: call increment() - const INCREMENT_TX: Bytes = bytes!( - "0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89" - ); - // Increment2 tx: call increment2() - const INCREMENT2_TX: Bytes = bytes!( - "0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2" - ); - - fn create_second_payload() -> Flashblock { - let payload = Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 1, - base: None, - diff: ExecutionPayloadFlashblockDeltaV1 { - state_root: B256::default(), - receipts_root: B256::default(), - gas_used: 0, - block_hash: B256::default(), - transactions: vec![ - DEPOSIT_TX, - TRANSFER_ETH_TX, - DEPLOYMENT_TX, - INCREMENT_TX, - INCREMENT2_TX, - ], - withdrawals: Vec::new(), - logs_bloom: Default::default(), - withdrawals_root: Default::default(), - blob_gas_used: Default::default(), - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - DEPOSIT_TX_HASH, - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 31000, - logs: vec![], - }, - deposit_nonce: Some(4012992u64), - deposit_receipt_version: None, - }), - ); - receipts.insert( - TRANSFER_ETH_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 55000, - logs: vec![], - }), - ); - receipts.insert( - DEPLOYMENT_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279, - logs: vec![], - }), - ); - receipts.insert( - INCREMENT_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279 + 44000, - logs: create_test_logs(), - }), - ); - receipts.insert( - INCREMENT2_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279 + 44000 + 44000, - logs: vec![], - }), - ); - receipts - }, - new_account_balances: { - let mut map = HashMap::default(); - map.insert(TEST_ADDRESS, U256::from(PENDING_BALANCE)); - map.insert(COUNTER_ADDRESS, U256::from(0)); - map - }, - }, - }; - - payload - } - - #[tokio::test] - async fn test_get_pending_block() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - let latest_block = provider - .get_block_by_number(alloy_eips::BlockNumberOrTag::Latest) - .await? - .expect("latest block expected"); - assert_eq!(latest_block.number(), 0); - - // Querying pending block when it does not exist yet - let pending_block = provider - .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) - .await? - .expect("latest block expected"); - - assert_eq!(pending_block.number(), latest_block.number()); - assert_eq!(pending_block.hash(), latest_block.hash()); - - let base_payload = create_first_payload(); - node.send_payload(base_payload).await?; - - // Query pending block after sending the base payload with an empty delta - let pending_block = provider - .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) - .await? - .expect("pending block expected"); - - assert_eq!(pending_block.number(), 1); - assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction - - let second_payload = create_second_payload(); - node.send_payload(second_payload).await?; - - // Query pending block after sending the second payload with two transactions - let block = provider - .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) - .await? - .expect("pending block expected"); - - assert_eq!(block.number(), 1); - assert_eq!(block.transactions.hashes().len(), 6); - - Ok(()) - } - - #[tokio::test] - async fn test_get_balance_pending() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - node.send_test_payloads().await?; - - let balance = provider.get_balance(TEST_ADDRESS).await?; - assert_eq!(balance, U256::ZERO); - - let pending_balance = provider.get_balance(TEST_ADDRESS).pending().await?; - assert_eq!(pending_balance, U256::from(PENDING_BALANCE)); - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_by_hash_pending() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); - assert!(provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.is_none()); - - node.send_test_payloads().await?; - - let tx1 = provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.expect("tx1 expected"); - assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); - assert_eq!(tx1.from(), DEPOSIT_SENDER); - - let tx2 = provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.expect("tx2 expected"); - assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); - assert_eq!(tx2.from(), TX_SENDER); - - // TODO: Verify more properties of the txns here. - - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_receipt_pending() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; - assert_eq!(receipt.is_none(), true); - - node.send_test_payloads().await?; - - let receipt = - provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?.expect("receipt expected"); - assert_eq!(receipt.gas_used(), 21000); - - let receipt = - provider.get_transaction_receipt(TRANSFER_ETH_HASH).await?.expect("receipt expected"); - assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 - - // TODO: Add a new payload and validate that the receipts from the previous payload - // are not returned. - - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_count() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 0); - - node.send_test_payloads().await?; - - assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 4); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_call() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - - let provider = node.provider().await?; - - // We ensure that eth_call will succeed because we are on plain state - let send_eth_call = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(200000) - .nonce(1) - .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) - .value(U256::from(9999999999849942300000u128)) - .input(TransactionInput::new(bytes!("0x"))); - - let res = - provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; - - assert!(res.is_ok()); - - node.send_test_payloads().await?; - - // We included a heavy spending transaction and now don't have enough funds for this request, so - // this eth_call with fail - let res = - provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; - - assert!(res.is_err()); - assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") - ); - - // read count1 from counter contract - let eth_call_count1 = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(20000000) - .nonce(5) - .to(COUNTER_ADDRESS) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))); - let res_count1 = provider.call(eth_call_count1).await; - assert!(res_count1.is_ok()); - assert_eq!( - U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); - - // read count2 from counter contract - let eth_call_count2 = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(20000000) - .nonce(6) - .to(COUNTER_ADDRESS) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xd631c639"))); - let res_count2 = provider.call(eth_call_count2).await; - assert!(res_count2.is_ok()); - assert_eq!( - U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_estimate_gas() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - - let provider = node.provider().await?; - - // We ensure that eth_estimate_gas will succeed because we are on plain state - let send_estimate_gas = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(200000) - .nonce(1) - .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) - .value(U256::from(9999999999849942300000u128)) - .input(TransactionInput::new(bytes!("0x"))); - - let res = provider - .estimate_gas(send_estimate_gas.clone()) - .block(BlockNumberOrTag::Pending.into()) - .await; - - assert!(res.is_ok()); - - node.send_test_payloads().await?; - - // We included a heavy spending transaction and now don't have enough funds for this request, so - // this eth_estimate_gas with fail - let res = provider - .estimate_gas(send_estimate_gas.nonce(4)) - .block(BlockNumberOrTag::Pending.into()) - .await; - - assert!(res.is_err()); - assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") - ); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_simulate_v1() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - node.send_test_payloads().await?; - - let simulate_call = SimulatePayload { - block_state_calls: vec![SimBlock { - calls: vec![ - // read number from counter contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))) - .into(), - // increment() value in contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .input(TransactionInput::new(bytes!("0xd09de08a"))) - .into(), - // read number from counter contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))) - .into(), - ], - block_overrides: None, - state_overrides: None, - }], - trace_transfers: false, - validation: true, - return_full_transactions: true, - }; - let simulate_res = - provider.simulate(&simulate_call).block_id(BlockNumberOrTag::Pending.into()).await; - assert!(simulate_res.is_ok()); - let block = simulate_res.unwrap(); - assert_eq!(block.len(), 1); - assert_eq!(block[0].calls.len(), 3); - assert_eq!( - block[0].calls[0].return_data, - bytes!("0x0000000000000000000000000000000000000000000000000000000000000002") - ); - assert_eq!(block[0].calls[1].return_data, bytes!("0x")); - assert_eq!( - block[0].calls[2].return_data, - bytes!("0x0000000000000000000000000000000000000000000000000000000000000003") - ); - - Ok(()) - } - - #[tokio::test] - async fn test_send_raw_transaction_sync() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - - node.send_payload(create_first_payload()).await?; - - // run the Tx sync and, in parallel, deliver the payload that contains the Tx - let (receipt_result, payload_result) = - tokio::join!(node.send_raw_transaction_sync(TRANSFER_ETH_TX, None), async { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - node.send_payload(create_second_payload()).await - }); - - payload_result?; - let receipt = receipt_result?; - - assert_eq!(receipt.transaction_hash(), TRANSFER_ETH_HASH); - Ok(()) - } - - #[tokio::test] - async fn test_send_raw_transaction_sync_timeout() { - reth_tracing::init_test_tracing(); - let node = setup_node().await.unwrap(); - - // fail request immediately by passing a timeout of 0 ms - let receipt_result = node.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; - - let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); - assert!( - receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str()) - ); - } - - #[tokio::test] - async fn test_get_logs_pending() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - // Test no logs when no flashblocks sent - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .select(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - assert_eq!(logs.len(), 0); - - // Send payloads with transactions - node.send_test_payloads().await?; - - // Test getting pending logs - must use both fromBlock and toBlock as "pending" - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // We should now have 2 logs from the INCREMENT_TX transaction - assert_eq!(logs.len(), 2); - - // Verify the first log is from COUNTER_ADDRESS - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].topics()[0], TEST_LOG_TOPIC_0); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - // Verify the second log is from TEST_ADDRESS - assert_eq!(logs[1].address(), TEST_ADDRESS); - assert_eq!(logs[1].topics()[0], TEST_LOG_TOPIC_0); - assert_eq!(logs[1].transaction_hash, Some(INCREMENT_HASH)); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_filter_by_address() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - node.send_test_payloads().await?; - - // Test filtering by a specific address (COUNTER_ADDRESS) - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .address(COUNTER_ADDRESS) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should get only 1 log from COUNTER_ADDRESS - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - // Test filtering by TEST_ADDRESS - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .address(TEST_ADDRESS) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should get only 1 log from TEST_ADDRESS - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), TEST_ADDRESS); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_topic_filtering() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - node.send_test_payloads().await?; - - // Test filtering by topic - should match both logs - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .event_signature(TEST_LOG_TOPIC_0) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.topics()[0] == TEST_LOG_TOPIC_0)); - - // Test filtering by specific topic combination - should match only the first log - let filter = alloy_rpc_types_eth::Filter::default() - .topic1(TEST_LOG_TOPIC_1) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending); - - let logs = provider.get_logs(&filter).await?; - - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].topics()[1], TEST_LOG_TOPIC_1); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_mixed_block_ranges() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - let node = setup_node().await?; - let provider = node.provider().await?; - - node.send_test_payloads().await?; - - // Test fromBlock: 0, toBlock: pending (should include both historical and pending) - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(0) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should now include pending logs (2 logs from our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - // Test fromBlock: latest, toBlock: pending - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Latest) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should include pending logs (historical part is empty in our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - // Test fromBlock: earliest, toBlock: pending - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Earliest) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should include pending logs (historical part is empty in our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - Ok(()) - } -} From 5aa6d9b4212249e58ba7851c4bbedbb25b43c8ab Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Tue, 18 Nov 2025 11:06:19 -0600 Subject: [PATCH 11/38] Port flashblocks state tests to shared harness --- crates/flashblocks-rpc/src/tests/state.rs | 335 ++++++++++------------ 1 file changed, 144 insertions(+), 191 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 8893e097..84d605a7 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -1,43 +1,28 @@ #[cfg(test)] mod tests { - use std::{sync::Arc, time::Duration}; - - use alloy_consensus::{ - BlockHeader, Header, Receipt, Transaction, crypto::secp256k1::public_key_to_address, - }; - use alloy_eips::{BlockHashOrNumber, Decodable2718, Encodable2718}; - use alloy_genesis::GenesisAccount; - use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, map::foldhash::HashMap}; - use alloy_provider::network::BlockResponse; + use crate::rpc::{FlashblocksAPI, PendingBlocksAPI}; + use crate::state::FlashblocksState; + use crate::subscription::{Flashblock, FlashblocksReceiver, Metadata}; + use crate::tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + use alloy_consensus::{Receipt, Transaction}; + use alloy_eips::{BlockHashOrNumber, Encodable2718}; + use alloy_primitives::map::foldhash::HashMap; + use alloy_primitives::{hex, Address, BlockNumber, Bytes, B256, U256}; use alloy_rpc_types_engine::PayloadId; + use base_reth_test_utils::accounts::TestAccounts; + use base_reth_test_utils::harness::TestHarness as BaseHarness; + use base_reth_test_utils::node::{default_launcher, LocalNodeProvider}; use op_alloy_consensus::OpDepositReceipt; - use reth::{ - builder::NodeTypesWithDBAdapter, - chainspec::EthChainSpec, - providers::{AccountReader, BlockNumReader, BlockReader}, - revm::database::StateProviderDatabase, - transaction_pool::test_utils::TransactionBuilder, - }; - use reth_db::{DatabaseEnv, test_utils::TempDatabase}; - use reth_evm::{ConfigureEvm, execute::Executor}; - use reth_optimism_chainspec::{BASE_MAINNET, OpChainSpecBuilder}; - use reth_optimism_evm::OpEvmConfig; - use reth_optimism_node::OpNode; - use reth_optimism_primitives::{OpBlock, OpBlockBody, OpReceipt, OpTransactionSigned}; - use reth_primitives_traits::{Account, Block, RecoveredBlock, SealedHeader}; - use reth_provider::{ - BlockWriter, ChainSpecProvider, ExecutionOutcome, LatestStateProviderRef, ProviderFactory, - StateProviderFactory, providers::BlockchainProvider, - }; + use op_alloy_network::BlockResponse; + use reth::chainspec::EthChainSpec; + use reth::providers::{AccountReader, BlockReader}; + 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_provider::{ChainSpecProvider, StateProviderFactory}; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + use std::time::Duration; use tokio::time::sleep; - - use crate::{ - rpc::{FlashblocksAPI, PendingBlocksAPI}, - state::FlashblocksState, - subscription::{Flashblock, FlashblocksReceiver, Metadata}, - tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH, utils::create_test_provider_factory}, - }; // 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; @@ -49,18 +34,68 @@ mod tests { Charlie, } - type NodeTypes = NodeTypesWithDBAdapter>>; - - #[derive(Debug, Clone)] struct TestHarness { - flashblocks: FlashblocksState>, - provider: BlockchainProvider, - factory: ProviderFactory, + node: BaseHarness, + flashblocks: FlashblocksState, + provider: LocalNodeProvider, + canonical_block: RecoveredBlock, user_to_address: HashMap, user_to_private_key: HashMap, } impl TestHarness { + async fn new() -> Self { + let node = BaseHarness::new(default_launcher) + .await + .expect("able to launch base harness"); + let provider = node.blockchain_provider(); + let flashblocks = FlashblocksState::new(provider.clone()); + flashblocks.start(); + + let genesis_block = provider + .block(BlockHashOrNumber::Number(0)) + .expect("able to load block") + .expect("block exists") + .try_into_recovered() + .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, + canonical_block: genesis_block, + user_to_address, + user_to_private_key, + } + } + + fn decode_private_key(key: &str) -> B256 { + let bytes = hex::decode(key).expect("valid hex-encoded key"); + B256::from_slice(&bytes) + } + fn address(&self, u: User) -> Address { assert!(self.user_to_address.contains_key(&u)); self.user_to_address[&u] @@ -72,23 +107,26 @@ mod tests { } fn current_canonical_block(&self) -> RecoveredBlock { - let latest_block_num = - self.provider.last_block_number().expect("should be a latest block"); + self.canonical_block.clone() + } + fn canonical_account(&self, u: User) -> Account { self.provider - .block(BlockHashOrNumber::Number(latest_block_num)) - .expect("able to load block") - .expect("block exists") - .try_into_recovered() - .expect("able to recover block") + .basic_account(&self.address(u)) + .expect("can lookup account state") + .expect("should be existing account state") + } + + fn canonical_balance(&self, u: User) -> U256 { + self.canonical_account(u).balance + } + + fn expected_pending_balance(&self, u: User, delta: u128) -> U256 { + self.canonical_balance(u) + U256::from(delta) } fn account_state(&self, u: User) -> Account { - let basic_account = self - .provider - .basic_account(&self.address(u)) - .expect("can lookup account state") - .expect("should be existing account state"); + let basic_account = self.canonical_account(u); let nonce = self .flashblocks @@ -121,7 +159,8 @@ mod tests { .nonce(self.account_state(from).nonce) .value(amount) .gas_limit(21_000) - .max_fee_per_gas(200) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) .into_eip1559() .as_eip1559() .unwrap() @@ -144,7 +183,8 @@ mod tests { .nonce(nonce) .value(amount) .gas_limit(21_000) - .max_fee_per_gas(200) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) .into_eip1559() .as_eip1559() .unwrap() @@ -160,59 +200,33 @@ mod tests { async fn new_canonical_block_without_processing( &mut self, - mut user_transactions: Vec, + user_transactions: Vec, ) -> RecoveredBlock { - let current_tip = self.current_canonical_block(); - - let deposit_transaction = - OpTransactionSigned::decode_2718_exact(&BLOCK_INFO_TXN.iter().as_slice()).unwrap(); - - let mut transactions: Vec = vec![deposit_transaction]; - transactions.append(&mut user_transactions); - - let block = OpBlock::new_sealed( - SealedHeader::new_unhashed(Header { - parent_beacon_block_root: Some(current_tip.hash()), - parent_hash: current_tip.hash(), - number: current_tip.number() + 1, - timestamp: current_tip.header().timestamp() + 2, - gas_limit: current_tip.header().gas_limit(), - ..Header::default() - }), - OpBlockBody { transactions, ommers: vec![], withdrawals: None }, - ) - .try_recover() - .expect("able to recover block"); - - let provider = self.factory.provider().unwrap(); - - // Execute the block to produce a block execution output - let mut block_execution_output = OpEvmConfig::optimism(self.provider.chain_spec()) - .batch_executor(StateProviderDatabase::new(LatestStateProviderRef::new(&provider))) - .execute(&block) - .unwrap(); - - block_execution_output.state.reverts.sort(); - - let execution_outcome = ExecutionOutcome { - bundle: block_execution_output.state.clone(), - receipts: vec![block_execution_output.receipts.clone()], - first_block: block.number, - requests: vec![block_execution_output.requests.clone()], - }; - - // Commit the block's execution outcome to the database - let provider_rw = self.factory.provider_rw().unwrap(); - provider_rw - .append_blocks_with_state( - vec![block.clone()], - &execution_outcome, - Default::default(), - ) - .unwrap(); - provider_rw.commit().unwrap(); - - block + let previous_tip = self.current_canonical_block().number; + let txs: Vec = user_transactions + .into_iter() + .map(|tx| tx.encoded_2718().into()) + .collect(); + self.node + .build_block_from_transactions(txs) + .await + .expect("able to build block"); + let target_block_number = previous_tip + 1; + for _ in 0..10 { + if let Some(block) = self + .provider + .block(BlockHashOrNumber::Number(target_block_number)) + .expect("able to load block") + { + let recovered = block + .try_into_recovered() + .expect("able to recover newly built block"); + self.canonical_block = recovered.clone(); + return recovered; + } + sleep(Duration::from_millis(SLEEP_TIME)).await; + } + panic!("new canonical block not found after building payload"); } async fn new_canonical_block(&mut self, user_transactions: Vec) { @@ -220,79 +234,18 @@ mod tests { self.flashblocks.on_canonical_block_received(&block); sleep(Duration::from_millis(SLEEP_TIME)).await; } - - fn new() -> Self { - let keys = reth_testing_utils::generators::generate_keys(&mut rand::rng(), 3); - let alice_signer = keys[0]; - let bob_signer = keys[1]; - let charli_signer = keys[2]; - - let alice = public_key_to_address(alice_signer.public_key()); - let bob = public_key_to_address(bob_signer.public_key()); - let charlie = public_key_to_address(charli_signer.public_key()); - - let items = vec![ - (alice, GenesisAccount::default().with_balance(U256::from(100_000_000))), - (bob, GenesisAccount::default().with_balance(U256::from(100_000_000))), - (charlie, GenesisAccount::default().with_balance(U256::from(100_000_000))), - ]; - - let genesis = - BASE_MAINNET.genesis.clone().extend_accounts(items).with_gas_limit(100_000_000); - - let chain_spec = - OpChainSpecBuilder::base_mainnet().genesis(genesis).isthmus_activated().build(); - - let factory = create_test_provider_factory::(Arc::new(chain_spec)); - assert!(reth_db_common::init::init_genesis(&factory).is_ok()); - - let provider = - BlockchainProvider::new(factory.clone()).expect("able to setup provider"); - - let block = provider - .block(BlockHashOrNumber::Number(0)) - .expect("able to load block") - .expect("block exists") - .try_into_recovered() - .expect("able to recover block"); - - let flashblocks = FlashblocksState::new(provider.clone(), 4); - flashblocks.start(); - - flashblocks.on_canonical_block_received(&block); - - Self { - factory, - flashblocks, - provider, - user_to_address: { - let mut res = HashMap::default(); - res.insert(User::Alice, alice); - res.insert(User::Bob, bob); - res.insert(User::Charlie, charlie); - res - }, - user_to_private_key: { - let mut res = HashMap::default(); - res.insert(User::Alice, alice_signer.secret_bytes().into()); - res.insert(User::Bob, bob_signer.secret_bytes().into()); - res.insert(User::Charlie, charli_signer.secret_bytes().into()); - res - }, - } - } } - struct FlashblockBuilder { + struct FlashblockBuilder<'a> { transactions: Vec, receipts: HashMap, - harness: TestHarness, + harness: &'a TestHarness, canonical_block_number: Option, index: u64, } - impl FlashblockBuilder { - pub fn new_base(harness: &TestHarness) -> Self { + impl<'a> FlashblockBuilder<'a> { + pub fn new_base(harness: &'a TestHarness) -> Self { Self { canonical_block_number: None, transactions: vec![BLOCK_INFO_TXN.clone()], @@ -313,15 +266,15 @@ mod tests { receipts }, index: 0, - harness: harness.clone(), + harness, } } - pub fn new(harness: &TestHarness, index: u64) -> Self { + pub fn new(harness: &'a TestHarness, index: u64) -> Self { Self { canonical_block_number: None, transactions: Vec::new(), receipts: HashMap::default(), - harness: harness.clone(), + harness, index, } } @@ -404,7 +357,7 @@ mod tests { #[tokio::test] async fn test_state_overrides_persisted_across_flashblocks() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -456,7 +409,7 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_100_000) + test.expected_pending_balance(User::Bob, 100_000) ); test.send_flashblock(FlashblockBuilder::new(&test, 2).build()).await; @@ -474,14 +427,14 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_100_000) + test.expected_pending_balance(User::Bob, 100_000) ); } #[tokio::test] async fn test_state_overrides_persisted_across_blocks() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; let initial_base = FlashblockBuilder::new_base(&test).build(); let initial_block_number = initial_base.metadata.block_number; @@ -535,7 +488,7 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_100_000) + test.expected_pending_balance(User::Bob, 100_000) ); test.send_flashblock( @@ -598,14 +551,14 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_200_000) + test.expected_pending_balance(User::Bob, 200_000) ); } #[tokio::test] async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { reth_tracing::init_test_tracing(); - let mut test = TestHarness::new(); + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -655,7 +608,7 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_100_000) + test.expected_pending_balance(User::Bob, 100_000) ); test.send_flashblock( @@ -691,7 +644,7 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_200_000) + test.expected_pending_balance(User::Bob, 200_000) ); test.new_canonical_block(vec![test.build_transaction_to_send_eth_with_nonce( @@ -720,7 +673,7 @@ mod tests { .expect("should be set as txn receiver") .balance .expect("should be changed due to receiving funds"), - U256::from(100_100_100) + test.expected_pending_balance(User::Bob, 100_000) ); } @@ -732,7 +685,7 @@ mod tests { // because underlying reth node `latest` block is already updated, but // relevant pending state has not been cleared yet reth_tracing::init_test_tracing(); - let mut test = TestHarness::new(); + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; test.send_flashblock( @@ -793,7 +746,7 @@ mod tests { #[tokio::test] async fn test_missing_receipts_will_not_process() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -820,7 +773,7 @@ mod tests { #[tokio::test] async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -842,7 +795,7 @@ mod tests { #[tokio::test] async fn test_flashblock_for_new_canonical_block_works_if_sequential() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -867,7 +820,7 @@ mod tests { #[tokio::test] async fn test_non_sequential_payload_clears_pending_state() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -901,7 +854,7 @@ mod tests { #[tokio::test] async fn test_duplicate_flashblock_ignored() { reth_tracing::init_test_tracing(); - let test = TestHarness::new(); + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -925,7 +878,7 @@ mod tests { #[tokio::test] async fn test_progress_canonical_blocks_without_flashblocks() { reth_tracing::init_test_tracing(); - let mut test = TestHarness::new(); + let mut test = TestHarness::new().await; let genesis_block = test.current_canonical_block(); assert_eq!(genesis_block.number, 0); From b13e469753688d57e3d0372c5878683ca554d4cf Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Tue, 18 Nov 2025 11:17:33 -0600 Subject: [PATCH 12/38] Sync harness provider with canonical chain --- crates/test-utils/src/node.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index cd713b8c..f3f5364f 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -18,6 +18,7 @@ use reth::builder::{ }; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; +use reth_provider::CanonStateSubscriptions; use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpec; @@ -108,6 +109,7 @@ impl LocalNode { let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); let fb_cell: Arc>>> = Arc::new(OnceCell::new()); let provider_cell: Arc> = Arc::new(OnceCell::new()); + let canon_sync_cell: Arc> = Arc::new(OnceCell::new()); let NodeHandle { node: node_handle, @@ -143,6 +145,7 @@ impl LocalNode { .extend_rpc_modules({ let fb_cell = fb_cell.clone(); let provider_cell = provider_cell.clone(); + let canon_sync_cell = canon_sync_cell.clone(); let mut receiver = Some(receiver); move |ctx| { let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); @@ -150,6 +153,23 @@ impl LocalNode { .get_or_init(|| Arc::new(FlashblocksState::new(provider.clone()))) .clone(); fb.start(); + + if canon_sync_cell.get().is_none() { + let provider_for_task = provider.clone(); + let mut canon_stream = + tokio_stream::wrappers::BroadcastStream::new( + ctx.provider().subscribe_to_canonical_state(), + ); + let _ = canon_sync_cell.set(()); + tokio::spawn(async move { + use tokio_stream::StreamExt; + while let Some(Ok(notification)) = canon_stream.next().await { + provider_for_task + .canonical_in_memory_state() + .notify_canon_state(notification); + } + }); + } let api_ext = EthApiExt::new( ctx.registry.eth_api().clone(), ctx.registry.eth_handlers().filter.clone(), From 28cff76dac34c8c8645aeb9ce41ea0210121f928 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Tue, 18 Nov 2025 12:16:55 -0600 Subject: [PATCH 13/38] Cleanup flashblocks OnceCell usage --- crates/test-utils/src/node.rs | 55 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index f3f5364f..77ce6a24 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -18,13 +18,13 @@ use reth::builder::{ }; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; -use reth_provider::CanonStateSubscriptions; use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; use reth_provider::providers::BlockchainProvider; +use reth_provider::CanonStateSubscriptions; use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; @@ -109,7 +109,6 @@ impl LocalNode { let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); let fb_cell: Arc>>> = Arc::new(OnceCell::new()); let provider_cell: Arc> = Arc::new(OnceCell::new()); - let canon_sync_cell: Arc> = Arc::new(OnceCell::new()); let NodeHandle { node: node_handle, @@ -124,9 +123,7 @@ impl LocalNode { let provider_cell = provider_cell.clone(); move |mut ctx| async move { let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(provider.clone()))) - .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() { @@ -145,31 +142,23 @@ impl LocalNode { .extend_rpc_modules({ let fb_cell = fb_cell.clone(); let provider_cell = provider_cell.clone(); - let canon_sync_cell = canon_sync_cell.clone(); let mut receiver = Some(receiver); move |ctx| { let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(provider.clone()))) - .clone(); - fb.start(); - - if canon_sync_cell.get().is_none() { - let provider_for_task = provider.clone(); - let mut canon_stream = - tokio_stream::wrappers::BroadcastStream::new( - ctx.provider().subscribe_to_canonical_state(), - ); - let _ = canon_sync_cell.set(()); - tokio::spawn(async move { - use tokio_stream::StreamExt; - while let Some(Ok(notification)) = canon_stream.next().await { - provider_for_task - .canonical_in_memory_state() - .notify_canon_state(notification); - } - }); - } + let fb = init_flashblocks_state(&fb_cell, &provider); + + let provider_for_task = provider.clone(); + 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_for_task + .canonical_in_memory_state() + .notify_canon_state(notification); + } + }); let api_ext = EthApiExt::new( ctx.registry.eth_api().clone(), ctx.registry.eth_handlers().filter.clone(), @@ -248,3 +237,15 @@ impl LocalNode { self.provider.clone() } } + +fn init_flashblocks_state( + cell: &Arc>>, + provider: &LocalNodeProvider, +) -> Arc { + cell.get_or_init(|| { + let fb = Arc::new(FlashblocksState::new(provider.clone())); + fb.start(); + fb + }) + .clone() +} From a885476a2030739f8e736b19560b4b5823ac2265 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 15:39:49 -0600 Subject: [PATCH 14/38] Add latest_block() utility to base TestHarness - Add latest_block() method to TestHarness in test-utils that encapsulates the two-step process of getting best_block_number then fetching that block - Refactor test_progress_canonical_blocks_without_flashblocks to use the new method - Refactor FlashblockBuilder::build() to use the new method - Reduces code duplication and improves maintainability across all tests --- crates/flashblocks-rpc/src/tests/state.rs | 39 +++++++++-------------- crates/test-utils/src/harness.rs | 18 ++++++++++- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 84d605a7..11a3578a 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -15,7 +15,7 @@ mod tests { use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; use reth::chainspec::EthChainSpec; - use reth::providers::{AccountReader, BlockReader}; + use reth::providers::{AccountReader, BlockNumReader, BlockReader}; use reth::transaction_pool::test_utils::TransactionBuilder; use reth_optimism_primitives::{OpBlock, OpReceipt, OpTransactionSigned}; use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; @@ -38,7 +38,6 @@ mod tests { node: BaseHarness, flashblocks: FlashblocksState, provider: LocalNodeProvider, - canonical_block: RecoveredBlock, user_to_address: HashMap, user_to_private_key: HashMap, } @@ -81,14 +80,7 @@ mod tests { Self::decode_private_key(accounts.charlie.private_key), ); - Self { - node, - flashblocks, - provider, - canonical_block: genesis_block, - user_to_address, - user_to_private_key, - } + Self { node, flashblocks, provider, user_to_address, user_to_private_key } } fn decode_private_key(key: &str) -> B256 { @@ -106,10 +98,6 @@ mod tests { self.user_to_private_key[&u] } - fn current_canonical_block(&self) -> RecoveredBlock { - self.canonical_block.clone() - } - fn canonical_account(&self, u: User) -> Account { self.provider .basic_account(&self.address(u)) @@ -202,7 +190,10 @@ mod tests { &mut self, user_transactions: Vec, ) -> RecoveredBlock { - let previous_tip = self.current_canonical_block().number; + let previous_tip = self + .provider + .best_block_number() + .expect("able to read best block number"); let txs: Vec = user_transactions .into_iter() .map(|tx| tx.encoded_2718().into()) @@ -218,11 +209,9 @@ mod tests { .block(BlockHashOrNumber::Number(target_block_number)) .expect("able to load block") { - let recovered = block + return block .try_into_recovered() .expect("able to recover newly built block"); - self.canonical_block = recovered.clone(); - return recovered; } sleep(Duration::from_millis(SLEEP_TIME)).await; } @@ -310,9 +299,11 @@ mod tests { } pub fn build(&self) -> Flashblock { - let current_block = self.harness.current_canonical_block(); - let canonical_block_num = - self.canonical_block_number.unwrap_or_else(|| current_block.number) + 1; + let current_block = self.harness.node.latest_block(); + let canonical_block_num = self + .canonical_block_number + .unwrap_or_else(|| current_block.number) + + 1; let base = if self.index == 0 { Some(ExecutionPayloadBaseV1 { @@ -880,7 +871,7 @@ mod tests { reth_tracing::init_test_tracing(); let mut test = TestHarness::new().await; - let genesis_block = test.current_canonical_block(); + let genesis_block = test.node.latest_block(); assert_eq!(genesis_block.number, 0); assert_eq!(genesis_block.transaction_count(), 0); assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -892,7 +883,7 @@ mod tests { )]) .await; - let block_one = test.current_canonical_block(); + let block_one = test.node.latest_block(); assert_eq!(block_one.number, 1); assert_eq!(block_one.transaction_count(), 2); assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -903,7 +894,7 @@ mod tests { ]) .await; - let block_two = test.current_canonical_block(); + let block_two = test.node.latest_block(); assert_eq!(block_two.number, 2); assert_eq!(block_two.transaction_count(), 3); assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 84b46fc5..ab2112a6 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -3,7 +3,7 @@ use crate::accounts::TestAccounts; use crate::engine::{EngineApi, IpcEngine}; use crate::node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}; -use alloy_eips::eip7685::Requests; +use alloy_eips::{eip7685::Requests, BlockHashOrNumber}; use alloy_primitives::{bytes, Bytes, B256}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; @@ -14,8 +14,11 @@ use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; use reth::builder::NodeHandle; +use reth::providers::{BlockNumReader, BlockReader}; use reth_e2e_test_utils::Adapter; use reth_optimism_node::OpNode; +use reth_optimism_primitives::OpBlock; +use reth_primitives_traits::{Block as BlockT, RecoveredBlock}; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; @@ -173,6 +176,19 @@ impl TestHarness { } Ok(()) } + + pub fn latest_block(&self) -> RecoveredBlock { + let provider = self.blockchain_provider(); + let best_number = provider + .best_block_number() + .expect("able to read best block number"); + let block = provider + .block(BlockHashOrNumber::Number(best_number)) + .expect("able to load canonical block") + .expect("canonical block exists"); + BlockT::try_into_recovered(block) + .expect("able to recover canonical block") + } } #[cfg(test)] From 85b5f88a6f8c0cd7e65f08574b67a69e279d8847 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 17:00:24 -0600 Subject: [PATCH 15/38] Remove unused utils --- crates/flashblocks-rpc/src/tests/mod.rs | 1 - crates/flashblocks-rpc/src/tests/utils.rs | 37 ----------------------- 2 files changed, 38 deletions(-) delete mode 100644 crates/flashblocks-rpc/src/tests/utils.rs diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/src/tests/mod.rs index f0684fef..2159021e 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/src/tests/mod.rs @@ -2,7 +2,6 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; mod framework_test; mod state; -mod utils; const BLOCK_INFO_TXN: Bytes = bytes!( "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" diff --git a/crates/flashblocks-rpc/src/tests/utils.rs b/crates/flashblocks-rpc/src/tests/utils.rs deleted file mode 100644 index a84815b0..00000000 --- a/crates/flashblocks-rpc/src/tests/utils.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::sync::Arc; - -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_provider::{ProviderFactory, providers::StaticFileProvider}; - -pub fn create_test_provider_factory( - chain_spec: Arc, -) -> ProviderFactory>>> { - let (static_dir, _) = create_test_static_files_dir(); - let db = create_test_db(); - ProviderFactory::new( - db, - chain_spec, - StaticFileProvider::read_write(static_dir.keep()).expect("static file provider"), - ) -} - -fn create_test_db() -> Arc> { - let path = tempdir_path(); - let emsg = format!("{ERROR_DB_CREATION}: {path:?}"); - - let db = init_db( - &path, - DatabaseArguments::new(ClientVersion::default()) - .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Unbounded)) - .with_geometry_max_size(Some(4 * MEGABYTE)) - .with_growth_step(Some(4 * KILOBYTE)), - ) - .expect(&emsg); - - Arc::new(TempDatabase::new(db, path)) -} From 3a3c4f152658932338e74a64a1cbcb36276fc075 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 21:49:05 -0600 Subject: [PATCH 16/38] refactor(test-utils): replace testing_node() with manual database setup Replace NodeBuilder::testing_node() with LocalNode::create_test_database() to reduce memory usage in parallel tests. The testing_node() helper hardcodes an 8 TB map size which causes ENOMEM errors when running multiple test processes concurrently with cargo test. This prevents memory exhaustion when running parallel tests while maintaining the same functionality. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/test-utils/Cargo.toml | 1 + crates/test-utils/src/node.rs | 52 +++++++++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22db87a9..02358295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1705,6 +1705,7 @@ dependencies = [ "reth-e2e-test-utils", "reth-exex", "reth-ipc", + "reth-node-core", "reth-optimism-chainspec", "reth-optimism-cli", "reth-optimism-node", diff --git a/Cargo.toml b/Cargo.toml index bd8197c5..57444056 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9. reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-node-core = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } # revm revm = { version = "31.0.2", default-features = false } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 19df0084..0d559e9f 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -26,6 +26,7 @@ reth-db.workspace = true reth-db-common.workspace = true reth-testing-utils.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 diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 77ce6a24..23d466c8 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -16,8 +16,18 @@ use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; use reth::builder::{ Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, }; +use reth_node_core::{ + args::DatadirArgs, + dirs::{DataDirPath, MaybePlatformPath}, +}; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; +use reth_db::{ + init_db, + mdbx::DatabaseArguments, + test_utils::{tempdir_path, TempDatabase, ERROR_DB_CREATION}, + ClientVersion, DatabaseEnv, +}; use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpec; @@ -99,12 +109,21 @@ impl LocalNode { .with_auth_ipc(); rpc_args.auth_ipc_path = unique_ipc_path; - let node_config = NodeConfig::new(chain_spec.clone()) + let node = OpNode::new(RollupArgs::default()); + + let temp_db = Self::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 node = OpNode::new(RollupArgs::default()); + let datadir_path = MaybePlatformPath::::from(db_path.clone()); + node_config = node_config.with_datadir_args(DatadirArgs { + datadir: datadir_path, + ..Default::default() + }); let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); let fb_cell: Arc>>> = Arc::new(OnceCell::new()); @@ -114,7 +133,8 @@ impl LocalNode { node: node_handle, node_exit_future, } = NodeBuilder::new(node_config.clone()) - .testing_node(exec.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()) @@ -209,6 +229,32 @@ impl LocalNode { }) } + /// 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))) + } + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { let (tx, rx) = oneshot::channel(); self.flashblock_sender From 4199afc5666176052834fb43ebfc2bdb16119ab7 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:00:17 -0600 Subject: [PATCH 17/38] refactor: remove retry loop from new_canonical_block_without_processing --- crates/flashblocks-rpc/src/tests/state.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 11a3578a..0bdef9a4 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -203,19 +203,16 @@ mod tests { .await .expect("able to build block"); let target_block_number = previous_tip + 1; - for _ in 0..10 { - if let Some(block) = self - .provider - .block(BlockHashOrNumber::Number(target_block_number)) - .expect("able to load block") - { - return block - .try_into_recovered() - .expect("able to recover newly built block"); - } - sleep(Duration::from_millis(SLEEP_TIME)).await; - } - panic!("new canonical block not found after building payload"); + + let block = self + .provider + .block(BlockHashOrNumber::Number(target_block_number)) + .expect("able to load block") + .expect("new canonical block should be available after building payload"); + + block + .try_into_recovered() + .expect("able to recover newly built block") } async fn new_canonical_block(&mut self, user_transactions: Vec) { From 4dc2787f9466f5aaea8f158a9cf08931da68c006 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:12:41 -0600 Subject: [PATCH 18/38] Rename framework_test back to rpc --- crates/flashblocks-rpc/src/tests/mod.rs | 2 +- crates/flashblocks-rpc/src/tests/{framework_test.rs => rpc.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename crates/flashblocks-rpc/src/tests/{framework_test.rs => rpc.rs} (100%) diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/src/tests/mod.rs index 2159021e..dca64a96 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/src/tests/mod.rs @@ -1,6 +1,6 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; -mod framework_test; +mod rpc; mod state; const BLOCK_INFO_TXN: Bytes = bytes!( diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/rpc.rs similarity index 100% rename from crates/flashblocks-rpc/src/tests/framework_test.rs rename to crates/flashblocks-rpc/src/tests/rpc.rs From 364ed3ddb3cc45f70a331e529b5353b82b17794b Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:14:42 -0600 Subject: [PATCH 19/38] cargo clippy --- crates/flashblocks-rpc/src/tests/state.rs | 12 +++++++++--- crates/test-utils/src/harness.rs | 3 +-- crates/test-utils/src/node.rs | 12 ++++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 0bdef9a4..4457b756 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -80,7 +80,13 @@ mod tests { Self::decode_private_key(accounts.charlie.private_key), ); - Self { node, flashblocks, provider, user_to_address, user_to_private_key } + Self { + node, + flashblocks, + provider, + user_to_address, + user_to_private_key, + } } fn decode_private_key(key: &str) -> B256 { @@ -203,13 +209,13 @@ mod tests { .await .expect("able to build block"); let target_block_number = previous_tip + 1; - + let block = self .provider .block(BlockHashOrNumber::Number(target_block_number)) .expect("able to load block") .expect("new canonical block should be available after building payload"); - + block .try_into_recovered() .expect("able to recover newly built block") diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index ab2112a6..3a1a652d 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -186,8 +186,7 @@ impl TestHarness { .block(BlockHashOrNumber::Number(best_number)) .expect("able to load canonical block") .expect("canonical block exists"); - BlockT::try_into_recovered(block) - .expect("able to recover canonical block") + BlockT::try_into_recovered(block).expect("able to recover canonical block") } } diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 23d466c8..34b5f61a 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -16,10 +16,6 @@ use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; use reth::builder::{ Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, }; -use reth_node_core::{ - args::DatadirArgs, - dirs::{DataDirPath, MaybePlatformPath}, -}; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; use reth_db::{ @@ -30,6 +26,10 @@ use reth_db::{ }; 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::args::RollupArgs; use reth_optimism_node::OpNode; @@ -249,8 +249,8 @@ impl LocalNode { 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 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))) } From 2d2aa20909bdcbd2b3d11ee834111b0111074d91 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:37:36 -0600 Subject: [PATCH 20/38] fix: restore missing max_pending_blocks_depth parameter and fix builder pattern - Add missing max_pending_blocks_depth parameter (5) to FlashblocksState::new() calls in rpc.rs tests - Add missing max_pending_blocks_depth parameter to init_flashblocks_state in test-utils - Fix builder pattern to use engine_api_launcher() + launch_with() instead of .launch() to match TestHarness::new() signature - Enable 'client' feature for reth-optimism-rpc dependency to fix OpEngineApiClient import error These changes ensure no logic was lost during the rebase and fix compilation errors. --- crates/flashblocks-rpc/src/tests/rpc.rs | 21 +++++++++++++-------- crates/flashblocks-rpc/src/tests/state.rs | 2 +- crates/test-utils/Cargo.toml | 2 +- crates/test-utils/src/engine.rs | 2 +- crates/test-utils/src/node.rs | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs index a1e1bc5b..9a4bd43d 100644 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ b/crates/flashblocks-rpc/src/tests/rpc.rs @@ -40,13 +40,13 @@ mod tests { let harness = TestHarness::new(|builder| { let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - builder + let builder = builder .install_exex("flashblocks-canon", { let fb_cell = fb_cell.clone(); move |mut ctx| async move { let fb = fb_cell .get_or_init(|| { - Arc::new(FlashblocksState::new(ctx.provider().clone())) + Arc::new(FlashblocksState::new(ctx.provider().clone(), 5)) }) .clone(); Ok(async move { @@ -66,7 +66,7 @@ mod tests { }) .extend_rpc_modules(move |ctx| { let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone(), 5))) .clone(); fb.start(); @@ -87,8 +87,10 @@ mod tests { }); Ok(()) - }) - .launch() + }); + + let launcher = builder.engine_api_launcher(); + builder.launch_with(launcher) }) .await?; @@ -326,15 +328,18 @@ mod tests { // Querying pending block when it does not exist yet let pending_block = provider .get_block_by_number(BlockNumberOrTag::Pending) - .await?; - assert_eq!(pending_block.is_none(), true); + .await? + .expect("latest block expected"); + + assert_eq!(pending_block.number(), latest_block.number()); + assert_eq!(pending_block.hash(), latest_block.hash()); let base_payload = create_first_payload(); setup.send_flashblock(base_payload).await?; // Query pending block after sending the base payload with an empty delta let pending_block = provider - .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) + .get_block_by_number(BlockNumberOrTag::Pending) .await? .expect("pending block expected"); diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 4457b756..9317d3a5 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -48,7 +48,7 @@ mod tests { .await .expect("able to launch base harness"); let provider = node.blockchain_provider(); - let flashblocks = FlashblocksState::new(provider.clone()); + let flashblocks = FlashblocksState::new(provider.clone(), 5); flashblocks.start(); let genesis_block = provider diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 0d559e9f..1d163a98 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -18,7 +18,7 @@ reth-optimism-node.workspace = true reth-optimism-chainspec.workspace = true reth-optimism-cli.workspace = true reth-optimism-primitives.workspace = true -reth-optimism-rpc.workspace = true +reth-optimism-rpc = { workspace = true, features = ["client"] } reth-provider.workspace = true reth-primitives.workspace = true reth-primitives-traits.workspace = true diff --git a/crates/test-utils/src/engine.rs b/crates/test-utils/src/engine.rs index 4c1c19c1..d1738bc7 100644 --- a/crates/test-utils/src/engine.rs +++ b/crates/test-utils/src/engine.rs @@ -12,7 +12,7 @@ use op_alloy_rpc_types_engine::OpExecutionPayloadV4; use reth::api::{EngineTypes, PayloadTypes}; use reth::rpc::types::engine::ForkchoiceState; use reth_optimism_node::OpEngineTypes; -use reth_optimism_rpc::engine::OpEngineApiClient; +use reth_optimism_rpc::OpEngineApiClient; use reth_rpc_layer::{AuthClientLayer, JwtSecret}; use reth_tracing::tracing::debug; use std::marker::PhantomData; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 34b5f61a..49cf9cf0 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -289,7 +289,7 @@ fn init_flashblocks_state( provider: &LocalNodeProvider, ) -> Arc { cell.get_or_init(|| { - let fb = Arc::new(FlashblocksState::new(provider.clone())); + let fb = Arc::new(FlashblocksState::new(provider.clone(), 5)); fb.start(); fb }) From ae607dc25186e0d854a666f5e3fdc3b82fc00865 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:43:24 -0600 Subject: [PATCH 21/38] update test-utils docs --- crates/test-utils/README.md | 53 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 6d020e9b..92899153 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -15,11 +15,12 @@ This crate provides reusable testing utilities for integration tests across the ## Quick Start ```rust -use base_reth_test_utils::TestHarness; +use base_reth_test_utils::harness::TestHarness; +use base_reth_test_utils::node::default_launcher; #[tokio::test] async fn test_example() -> eyre::Result<()> { - let harness = TestHarness::new().await?; + let harness = TestHarness::new(default_launcher).await?; // Advance the chain harness.advance_chain(5).await?; @@ -44,6 +45,7 @@ The framework follows a three-layer architecture: │ - Coordinates node + engine │ │ - Builds blocks from transactions │ │ - Manages test accounts │ +│ - Manages flashblocks │ └─────────────────────────────────────┘ │ │ ┌──────┘ └──────┐ @@ -67,12 +69,13 @@ The framework follows a three-layer architecture: The main entry point for integration tests. Combines node, engine, and accounts into a single interface. ```rust -use base_reth_test_utils::TestHarness; +use base_reth_test_utils::harness::TestHarness; +use base_reth_test_utils::node::default_launcher; use alloy_primitives::Bytes; #[tokio::test] async fn test_harness() -> eyre::Result<()> { - let harness = TestHarness::new().await?; + let harness = TestHarness::new(default_launcher).await?; // Access provider let provider = harness.provider(); @@ -89,9 +92,6 @@ async fn test_harness() -> eyre::Result<()> { let txs: Vec = vec![/* signed transaction bytes */]; harness.build_block_from_transactions(txs).await?; - // Build block from flashblocks - harness.build_block_from_flashblocks(&flashblocks).await?; - // Send flashblocks for pending state testing harness.send_flashblock(flashblock).await?; @@ -122,11 +122,11 @@ async fn test_harness() -> eyre::Result<()> { In-process Optimism node with Base Sepolia configuration. ```rust -use base_reth_test_utils::LocalNode; +use base_reth_test_utils::node::{LocalNode, default_launcher}; #[tokio::test] async fn test_node() -> eyre::Result<()> { - let node = LocalNode::new().await?; + let node = LocalNode::new(default_launcher).await?; // Get provider let provider = node.provider()?; @@ -156,7 +156,7 @@ async fn test_node() -> eyre::Result<()> { Type-safe Engine API client wrapping raw CL operations. ```rust -use base_reth_test_utils::EngineApi; +use base_reth_test_utils::engine::EngineApi; use alloy_primitives::B256; use op_alloy_rpc_types_engine::OpPayloadAttributes; @@ -181,7 +181,8 @@ let status = engine.new_payload(payload, vec![], parent_root, requests).await?; Hardcoded test accounts with deterministic addresses (Anvil-compatible). ```rust -use base_reth_test_utils::TestAccounts; +use base_reth_test_utils::accounts::TestAccounts; +use base_reth_test_utils::{harness::TestHarness, node::default_launcher}; let accounts = TestAccounts::new(); @@ -191,7 +192,7 @@ let charlie = &accounts.charlie; let deployer = &accounts.deployer; // Access via harness -let harness = TestHarness::new().await?; +let harness = TestHarness::new(default_launcher).await?; let alice = &harness.accounts().alice; ``` @@ -209,23 +210,20 @@ Each account includes: ### 5. Flashblocks Support -Test flashblocks delivery without WebSocket connections. +Test flashblocks delivery without WebSocket connections. Flashblocks can be manually constructed and sent via the harness. ```rust -use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; +use base_reth_flashblocks_rpc::subscription::Flashblock; #[tokio::test] async fn test_flashblocks() -> eyre::Result<()> { - let (fb_ctx, receiver) = FlashblocksContext::new(); + let harness = TestHarness::new(default_launcher).await?; - // Create base flashblock - let flashblock = FlashblockBuilder::new(1, 0) - .as_base(B256::ZERO, 1000) - .with_transaction(tx_bytes, tx_hash, 21000) - .with_balance(address, U256::from(1000)) - .build(); + // Construct a Flashblock manually + // Use base_reth_flashblocks_rpc imports to build the struct + let flashblock = Flashblock { ... }; - fb_ctx.send_flashblock(flashblock).await?; + harness.send_flashblock(flashblock).await?; Ok(()) } @@ -233,7 +231,7 @@ async fn test_flashblocks() -> eyre::Result<()> { **Via TestHarness:** ```rust -let harness = TestHarness::new().await?; +let harness = TestHarness::new(default_launcher).await?; harness.send_flashblock(flashblock).await?; ``` @@ -257,8 +255,7 @@ test-utils/ │ ├── accounts.rs # Test account definitions │ ├── node.rs # LocalNode (EL wrapper) │ ├── engine.rs # EngineApi (CL wrapper) -│ ├── harness.rs # TestHarness (orchestration) -│ └── flashblocks.rs # Flashblocks support +│ └── harness.rs # TestHarness (orchestration) ├── assets/ │ └── genesis.json # Base Sepolia genesis └── Cargo.toml @@ -276,13 +273,14 @@ base-reth-test-utils.workspace = true Import in tests: ```rust -use base_reth_test_utils::TestHarness; +use base_reth_test_utils::harness::TestHarness; +use base_reth_test_utils::node::default_launcher; #[tokio::test] async fn my_test() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let harness = TestHarness::new().await?; + let harness = TestHarness::new(default_launcher).await?; // Your test logic Ok(()) @@ -318,6 +316,7 @@ cargo test -p base-reth-test-utils test_harness_setup - Snapshot/restore functionality - Multi-node network simulation - Performance benchmarking utilities +- Helper builder for Flashblocks ## References From f758448f8be374bb9101be99ed4864248435bc6a Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 22:50:02 -0600 Subject: [PATCH 22/38] cargo fmt --- crates/flashblocks-rpc/src/tests/rpc.rs | 146 ++++++++---------- crates/flashblocks-rpc/src/tests/state.rs | 64 +++----- crates/test-utils/src/accounts.rs | 9 +- crates/test-utils/src/engine.rs | 24 +-- crates/test-utils/src/harness.rs | 43 ++---- crates/test-utils/src/node.rs | 177 ++++++++++------------ 6 files changed, 180 insertions(+), 283 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs index 9a4bd43d..efe0afa1 100644 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ b/crates/flashblocks-rpc/src/tests/rpc.rs @@ -7,13 +7,13 @@ mod tests { use alloy_consensus::Receipt; use alloy_eips::BlockNumberOrTag; use alloy_primitives::map::HashMap; - use alloy_primitives::{address, b256, bytes, Address, Bytes, LogData, TxHash, B256, U256}; + use alloy_primitives::{Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes}; use alloy_provider::Provider; use alloy_rpc_client::RpcClient; use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; - use alloy_rpc_types_eth::error::EthRpcErrorCode; use alloy_rpc_types_eth::TransactionInput; + use alloy_rpc_types_eth::error::EthRpcErrorCode; use base_reth_test_utils::harness::TestHarness; use eyre::Result; use once_cell::sync::OnceCell; @@ -66,7 +66,9 @@ mod tests { }) .extend_rpc_modules(move |ctx| { let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone(), 5))) + .get_or_init(|| { + Arc::new(FlashblocksState::new(ctx.provider().clone(), 5)) + }) .clone(); fb.start(); @@ -161,11 +163,21 @@ mod tests { b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address // Transaction bytes - const DEPOSIT_TX: Bytes = bytes!("0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3"); - const TRANSFER_ETH_TX: Bytes = bytes!("0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31"); - const DEPLOYMENT_TX: Bytes = bytes!("0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c"); - const INCREMENT_TX: Bytes = bytes!("0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89"); - const INCREMENT2_TX: Bytes = bytes!("0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2"); + const DEPOSIT_TX: Bytes = bytes!( + "0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3" + ); + const TRANSFER_ETH_TX: Bytes = bytes!( + "0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31" + ); + const DEPLOYMENT_TX: Bytes = bytes!( + "0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c" + ); + const INCREMENT_TX: Bytes = bytes!( + "0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89" + ); + const INCREMENT2_TX: Bytes = bytes!( + "0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2" + ); fn create_test_logs() -> Vec { vec![ @@ -383,28 +395,16 @@ mod tests { let setup = TestSetup::new().await?; let provider = setup.harness.provider(); - assert!(provider - .get_transaction_by_hash(DEPOSIT_TX_HASH) - .await? - .is_none()); - assert!(provider - .get_transaction_by_hash(TRANSFER_ETH_HASH) - .await? - .is_none()); + assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); + assert!(provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.is_none()); setup.send_test_payloads().await?; - let tx1 = provider - .get_transaction_by_hash(DEPOSIT_TX_HASH) - .await? - .expect("tx1 expected"); + let tx1 = provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.expect("tx1 expected"); assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); assert_eq!(tx1.from(), DEPOSIT_SENDER); - let tx2 = provider - .get_transaction_by_hash(TRANSFER_ETH_HASH) - .await? - .expect("tx2 expected"); + let tx2 = provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.expect("tx2 expected"); assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); assert_eq!(tx2.from(), TX_SENDER); @@ -422,16 +422,12 @@ mod tests { setup.send_test_payloads().await?; - let receipt = provider - .get_transaction_receipt(DEPOSIT_TX_HASH) - .await? - .expect("receipt expected"); + let receipt = + provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?.expect("receipt expected"); assert_eq!(receipt.gas_used(), 21000); - let receipt = provider - .get_transaction_receipt(TRANSFER_ETH_HASH) - .await? - .expect("receipt expected"); + let receipt = + provider.get_transaction_receipt(TRANSFER_ETH_HASH).await?.expect("receipt expected"); assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 Ok(()) @@ -444,18 +440,12 @@ mod tests { let provider = setup.harness.provider(); assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!( - provider.get_transaction_count(TX_SENDER).pending().await?, - 0 - ); + assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 0); setup.send_test_payloads().await?; assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!( - provider.get_transaction_count(TX_SENDER).pending().await?, - 4 - ); + assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 4); Ok(()) } @@ -476,10 +466,8 @@ mod tests { .value(U256::from(9999999999849942300000u128)) .input(TransactionInput::new(bytes!("0x"))); - let res = provider - .call(send_eth_call.clone()) - .block(BlockNumberOrTag::Pending.into()) - .await; + let res = + provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; assert!(res.is_ok()); @@ -487,18 +475,17 @@ mod tests { // We included a heavy spending transaction and now don't have enough funds for this request, so // this eth_call with fail - let res = provider - .call(send_eth_call.nonce(4)) - .block(BlockNumberOrTag::Pending.into()) - .await; + let res = + provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; assert!(res.is_err()); - assert!(res - .unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas")); + assert!( + res.unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas") + ); // read count1 from counter contract let eth_call_count1 = OpTransactionRequest::default() @@ -568,12 +555,13 @@ mod tests { .await; assert!(res.is_err()); - assert!(res - .unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas")); + assert!( + res.unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas") + ); Ok(()) } @@ -622,10 +610,8 @@ mod tests { validation: true, return_full_transactions: true, }; - let simulate_res = provider - .simulate(&simulate_call) - .block_id(BlockNumberOrTag::Pending.into()) - .await; + let simulate_res = + provider.simulate(&simulate_call).block_id(BlockNumberOrTag::Pending.into()).await; assert!(simulate_res.is_ok()); let block = simulate_res.unwrap(); assert_eq!(block.len(), 1); @@ -651,13 +637,11 @@ mod tests { setup.send_flashblock(create_first_payload()).await?; // run the Tx sync and, in parallel, deliver the payload that contains the Tx - let (receipt_result, payload_result) = tokio::join!( - setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), - async { + let (receipt_result, payload_result) = + tokio::join!(setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), async { tokio::time::sleep(std::time::Duration::from_millis(100)).await; setup.send_flashblock(create_second_payload()).await - } - ); + }); payload_result?; let receipt = receipt_result?; @@ -672,16 +656,12 @@ mod tests { let setup = TestSetup::new().await.unwrap(); // fail request immediately by passing a timeout of 0 ms - let receipt_result = setup - .send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)) - .await; + let receipt_result = setup.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); - assert!(receipt_result - .err() - .unwrap() - .to_string() - .contains(format!("{}", error_code).as_str())); + assert!( + receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str()) + ); } #[tokio::test] @@ -823,9 +803,7 @@ mod tests { // Should now include pending logs (2 logs from our test setup) assert_eq!(logs.len(), 2); - assert!(logs - .iter() - .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); // Test fromBlock: latest, toBlock: pending let logs = provider @@ -838,9 +816,7 @@ mod tests { // Should include pending logs (historical part is empty in our test setup) assert_eq!(logs.len(), 2); - assert!(logs - .iter() - .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); // Test fromBlock: earliest, toBlock: pending let logs = provider @@ -853,9 +829,7 @@ mod tests { // Should include pending logs (historical part is empty in our test setup) assert_eq!(logs.len(), 2); - assert!(logs - .iter() - .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); Ok(()) } diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index 9317d3a5..d66d8f03 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -7,11 +7,11 @@ mod tests { use alloy_consensus::{Receipt, Transaction}; use alloy_eips::{BlockHashOrNumber, Encodable2718}; use alloy_primitives::map::foldhash::HashMap; - use alloy_primitives::{hex, Address, BlockNumber, Bytes, B256, U256}; + use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex}; use alloy_rpc_types_engine::PayloadId; use base_reth_test_utils::accounts::TestAccounts; use base_reth_test_utils::harness::TestHarness as BaseHarness; - use base_reth_test_utils::node::{default_launcher, LocalNodeProvider}; + use base_reth_test_utils::node::{LocalNodeProvider, default_launcher}; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; use reth::chainspec::EthChainSpec; @@ -44,9 +44,8 @@ mod tests { impl TestHarness { async fn new() -> Self { - let node = BaseHarness::new(default_launcher) - .await - .expect("able to launch base harness"); + let node = + BaseHarness::new(default_launcher).await.expect("able to launch base harness"); let provider = node.blockchain_provider(); let flashblocks = FlashblocksState::new(provider.clone(), 5); flashblocks.start(); @@ -67,26 +66,14 @@ mod tests { 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, - } + 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 } } fn decode_private_key(key: &str) -> B256 { @@ -196,18 +183,11 @@ mod tests { &mut self, user_transactions: Vec, ) -> RecoveredBlock { - let previous_tip = self - .provider - .best_block_number() - .expect("able to read best block number"); - let txs: Vec = user_transactions - .into_iter() - .map(|tx| tx.encoded_2718().into()) - .collect(); - self.node - .build_block_from_transactions(txs) - .await - .expect("able to build block"); + let previous_tip = + self.provider.best_block_number().expect("able to read best block number"); + let txs: Vec = + user_transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(); + self.node.build_block_from_transactions(txs).await.expect("able to build block"); let target_block_number = previous_tip + 1; let block = self @@ -216,9 +196,7 @@ mod tests { .expect("able to load block") .expect("new canonical block should be available after building payload"); - block - .try_into_recovered() - .expect("able to recover newly built block") + block.try_into_recovered().expect("able to recover newly built block") } async fn new_canonical_block(&mut self, user_transactions: Vec) { @@ -303,10 +281,8 @@ mod tests { pub fn build(&self) -> Flashblock { let current_block = self.harness.node.latest_block(); - let canonical_block_num = self - .canonical_block_number - .unwrap_or_else(|| current_block.number) - + 1; + let canonical_block_num = + self.canonical_block_number.unwrap_or_else(|| current_block.number) + 1; let base = if self.index == 0 { Some(ExecutionPayloadBaseV1 { diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs index 65ac2ccc..d26bf6ad 100644 --- a/crates/test-utils/src/accounts.rs +++ b/crates/test-utils/src/accounts.rs @@ -2,7 +2,7 @@ use alloy_consensus::{SignableTransaction, TxLegacy}; use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{address, hex, Address, Bytes, FixedBytes, U256}; +use alloy_primitives::{Address, Bytes, FixedBytes, U256, address, hex}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; use eyre::Result; @@ -62,12 +62,7 @@ pub struct TestAccounts { impl TestAccounts { /// Create a new instance with all test accounts pub fn new() -> Self { - Self { - alice: ALICE, - bob: BOB, - charlie: CHARLIE, - deployer: DEPLOYER, - } + Self { alice: ALICE, bob: BOB, charlie: CHARLIE, deployer: DEPLOYER } } /// Get all accounts as a vector diff --git a/crates/test-utils/src/engine.rs b/crates/test-utils/src/engine.rs index d1738bc7..47b69f70 100644 --- a/crates/test-utils/src/engine.rs +++ b/crates/test-utils/src/engine.rs @@ -88,11 +88,7 @@ impl EngineApi { let url: Url = engine_url.parse()?; let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; - Ok(Self { - address: EngineAddress::Http(url), - jwt_secret, - _phantom: PhantomData, - }) + Ok(Self { address: EngineAddress::Http(url), jwt_secret, _phantom: PhantomData }) } } @@ -100,11 +96,7 @@ impl EngineApi { pub fn new(path: String) -> Result { let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; - Ok(Self { - address: EngineAddress::Ipc(path), - jwt_secret, - _phantom: PhantomData, - }) + Ok(Self { address: EngineAddress::Ipc(path), jwt_secret, _phantom: PhantomData }) } } @@ -119,15 +111,9 @@ impl EngineApi

{ &self, payload_id: PayloadId, ) -> eyre::Result<::ExecutionPayloadEnvelopeV4> { - debug!( - "Fetching payload with id: {} at {}", - payload_id, - chrono::Utc::now() - ); - Ok( - OpEngineApiClient::::get_payload_v4(&self.client().await, payload_id) - .await?, - ) + debug!("Fetching payload with id: {} at {}", payload_id, chrono::Utc::now()); + Ok(OpEngineApiClient::::get_payload_v4(&self.client().await, payload_id) + .await?) } /// Submit a new payload to the Engine API diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 3a1a652d..a9d95545 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -3,13 +3,13 @@ use crate::accounts::TestAccounts; use crate::engine::{EngineApi, IpcEngine}; use crate::node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}; -use alloy_eips::{eip7685::Requests, BlockHashOrNumber}; -use alloy_primitives::{bytes, Bytes, B256}; +use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; +use alloy_primitives::{B256, Bytes, bytes}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; use base_reth_flashblocks_rpc::subscription::Flashblock; -use eyre::{eyre, Result}; +use eyre::{Result, eyre}; use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; @@ -28,7 +28,9 @@ const GAS_LIMIT: u64 = 200_000_000; const NODE_STARTUP_DELAY_MS: u64 = 500; const BLOCK_BUILD_DELAY_MS: u64 = 100; // Pre-captured L1 block info deposit transaction required by OP Stack. -const L1_BLOCK_INFO_DEPOSIT_TX: Bytes = bytes!("0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000"); +const L1_BLOCK_INFO_DEPOSIT_TX: Bytes = bytes!( + "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" +); pub struct TestHarness { node: LocalNode, @@ -48,17 +50,11 @@ impl TestHarness { sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; - Ok(Self { - node, - engine, - accounts, - }) + Ok(Self { node, engine, accounts }) } pub fn provider(&self) -> RootProvider { - self.node - .provider() - .expect("provider should always be available after node initialization") + self.node.provider().expect("provider should always be available after node initialization") } pub fn accounts(&self) -> &TestAccounts { @@ -79,10 +75,7 @@ impl TestHarness { pub async fn build_block_from_transactions(&self, mut transactions: Vec) -> Result<()> { // Ensure the block always starts with the required L1 block info deposit. - if !transactions - .first() - .is_some_and(|tx| tx == &L1_BLOCK_INFO_DEPOSIT_TX) - { + if !transactions.first().is_some_and(|tx| tx == &L1_BLOCK_INFO_DEPOSIT_TX) { transactions.insert(0, L1_BLOCK_INFO_DEPOSIT_TX.clone()); } @@ -93,10 +86,8 @@ impl TestHarness { .ok_or_else(|| eyre!("No genesis block found"))?; let parent_hash = latest_block.header.hash; - let parent_beacon_block_root = latest_block - .header - .parent_beacon_block_root - .unwrap_or(B256::ZERO); + let parent_beacon_block_root = + latest_block.header.parent_beacon_block_root.unwrap_or(B256::ZERO); let next_timestamp = latest_block.header.timestamp + BLOCK_TIME_SECONDS; let payload_attributes = OpPayloadAttributes { @@ -149,9 +140,7 @@ impl TestHarness { .latest_valid_hash .ok_or_else(|| eyre!("Payload status missing latest_valid_hash"))?; - self.engine - .update_forkchoice(parent_hash, new_block_hash, None) - .await?; + self.engine.update_forkchoice(parent_hash, new_block_hash, None).await?; Ok(()) } @@ -179,9 +168,7 @@ impl TestHarness { pub fn latest_block(&self) -> RecoveredBlock { let provider = self.blockchain_provider(); - let best_number = provider - .best_block_number() - .expect("able to read best block number"); + let best_number = provider.best_block_number().expect("able to read best block number"); let block = provider .block(BlockHashOrNumber::Number(best_number)) .expect("able to load canonical block") @@ -210,9 +197,7 @@ mod tests { let chain_id = provider.get_chain_id().await?; assert_eq!(chain_id, crate::node::BASE_CHAIN_ID); - let alice_balance = provider - .get_balance(harness.accounts().alice.address) - .await?; + let alice_balance = provider.get_balance(harness.accounts().alice.address).await?; assert!(alice_balance > U256::ZERO); let block_number = provider.get_block_number().await?; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 49cf9cf0..207d132b 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -19,10 +19,9 @@ use reth::builder::{ use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; use reth_db::{ - init_db, + ClientVersion, DatabaseEnv, init_db, mdbx::DatabaseArguments, - test_utils::{tempdir_path, TempDatabase, ERROR_DB_CREATION}, - ClientVersion, DatabaseEnv, + test_utils::{ERROR_DB_CREATION, TempDatabase, tempdir_path}, }; use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_exex::ExExEvent; @@ -31,10 +30,10 @@ use reth_node_core::{ dirs::{DataDirPath, MaybePlatformPath}, }; use reth_optimism_chainspec::OpChainSpec; -use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; -use reth_provider::providers::BlockchainProvider; +use reth_optimism_node::args::RollupArgs; use reth_provider::CanonStateSubscriptions; +use reth_provider::providers::BlockchainProvider; use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; @@ -84,10 +83,7 @@ impl LocalNode { let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); let network_config = NetworkArgs { - discovery: DiscoveryArgs { - disable_discovery: true, - ..DiscoveryArgs::default() - }, + discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, ..NetworkArgs::default() }; @@ -95,18 +91,12 @@ impl LocalNode { // Use timestamp + thread ID + process ID for uniqueness let unique_ipc_path = format!( "/tmp/reth_engine_api_{}_{}_{:?}.ipc", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(), + 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(); + let mut rpc_args = RpcServerArgs::default().with_unused_ports().with_http().with_auth_ipc(); rpc_args.auth_ipc_path = unique_ipc_path; let node = OpNode::new(RollupArgs::default()); @@ -120,87 +110,83 @@ impl LocalNode { .with_unused_ports(); let datadir_path = MaybePlatformPath::::from(db_path.clone()); - node_config = node_config.with_datadir_args(DatadirArgs { - datadir: datadir_path, - ..Default::default() - }); + node_config = node_config + .with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); let fb_cell: Arc>>> = Arc::new(OnceCell::new()); let provider_cell: Arc> = Arc::new(OnceCell::new()); - let NodeHandle { - node: node_handle, - node_exit_future, - } = 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()) - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - move |mut ctx| async move { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { - for block in committed.blocks_iter() { - fb.on_canonical_block_received(block); + let NodeHandle { node: node_handle, node_exit_future } = + 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()) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + move |mut ctx| async move { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { + for block in committed.blocks_iter() { + fb.on_canonical_block_received(block); + } + let _ = ctx.events.send(ExExEvent::FinishedHeight( + committed.tip().num_hash(), + )); } - let _ = ctx - .events - .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); } - } - Ok(()) - }) - } - }) - .extend_rpc_modules({ - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - let mut receiver = Some(receiver); - move |ctx| { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); - let fb = init_flashblocks_state(&fb_cell, &provider); + Ok(()) + }) + } + }) + .extend_rpc_modules({ + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + let mut receiver = Some(receiver); + move |ctx| { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); - let provider_for_task = provider.clone(); - 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_for_task - .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())?; - // Spawn task to receive flashblocks from the test context - let fb_for_task = fb.clone(); - let mut receiver = receiver - .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(()) - } - }) - .launch_with_fn(launcher) - .await?; + let provider_for_task = provider.clone(); + 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_for_task + .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())?; + // Spawn task to receive flashblocks from the test context + let fb_for_task = fb.clone(); + let mut receiver = receiver + .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(()) + } + }) + .launch_with_fn(launcher) + .await?; let http_api_addr = node_handle .rpc_server_handle() @@ -212,10 +198,8 @@ impl LocalNode { .get() .expect("FlashblocksState should be initialized during node launch") .clone(); - let provider = provider_cell - .get() - .expect("Provider should be initialized during node launch") - .clone(); + let provider = + provider_cell.get().expect("Provider should be initialized during node launch").clone(); Ok(Self { http_api_addr, @@ -257,10 +241,7 @@ impl LocalNode { pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { let (tx, rx) = oneshot::channel(); - self.flashblock_sender - .send((flashblock, tx)) - .await - .map_err(|err| eyre::eyre!(err))?; + self.flashblock_sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; rx.await.map_err(|err| eyre::eyre!(err))?; Ok(()) } From 42bcb8705eba3f29f83d5836b4b5d6aeda60873d Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Wed, 19 Nov 2025 23:03:37 -0600 Subject: [PATCH 23/38] just fix --- crates/flashblocks-rpc/src/tests/rpc.rs | 23 ++++++++------- crates/flashblocks-rpc/src/tests/state.rs | 33 +++++++++++++-------- crates/test-utils/src/engine.rs | 10 ++++--- crates/test-utils/src/harness.rs | 25 +++++++++------- crates/test-utils/src/node.rs | 36 ++++++++++++----------- 5 files changed, 73 insertions(+), 54 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs index efe0afa1..b05e3647 100644 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ b/crates/flashblocks-rpc/src/tests/rpc.rs @@ -1,19 +1,17 @@ #[cfg(test)] mod tests { - use crate::rpc::{EthApiExt, EthApiOverrideServer}; - use crate::state::FlashblocksState; - use crate::subscription::{Flashblock, FlashblocksReceiver, Metadata}; - use crate::tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + use std::{str::FromStr, sync::Arc}; + use alloy_consensus::Receipt; use alloy_eips::BlockNumberOrTag; - use alloy_primitives::map::HashMap; - use alloy_primitives::{Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes}; + use alloy_primitives::{ + Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes, map::HashMap, + }; use alloy_provider::Provider; use alloy_rpc_client::RpcClient; use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; - use alloy_rpc_types_eth::TransactionInput; - use alloy_rpc_types_eth::error::EthRpcErrorCode; + use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_test_utils::harness::TestHarness; use eyre::Result; use once_cell::sync::OnceCell; @@ -24,11 +22,16 @@ mod tests { use reth_optimism_primitives::OpReceipt; use reth_rpc_eth_api::RpcReceipt; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - use std::str::FromStr; - use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use tokio_stream::StreamExt; + use crate::{ + rpc::{EthApiExt, EthApiOverrideServer}, + state::FlashblocksState, + subscription::{Flashblock, FlashblocksReceiver, Metadata}, + tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}, + }; + pub struct TestSetup { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, harness: TestHarness, diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs index d66d8f03..ae2e94db 100644 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ b/crates/flashblocks-rpc/src/tests/state.rs @@ -1,28 +1,35 @@ #[cfg(test)] mod tests { - use crate::rpc::{FlashblocksAPI, PendingBlocksAPI}; - use crate::state::FlashblocksState; - use crate::subscription::{Flashblock, FlashblocksReceiver, Metadata}; - use crate::tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + use std::time::Duration; + use alloy_consensus::{Receipt, Transaction}; use alloy_eips::{BlockHashOrNumber, Encodable2718}; - use alloy_primitives::map::foldhash::HashMap; - use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex}; + use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex, map::foldhash::HashMap}; use alloy_rpc_types_engine::PayloadId; - use base_reth_test_utils::accounts::TestAccounts; - use base_reth_test_utils::harness::TestHarness as BaseHarness; - use base_reth_test_utils::node::{LocalNodeProvider, default_launcher}; + use base_reth_test_utils::{ + accounts::TestAccounts, + harness::TestHarness as BaseHarness, + node::{LocalNodeProvider, default_launcher}, + }; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; - use reth::chainspec::EthChainSpec; - use reth::providers::{AccountReader, BlockNumReader, BlockReader}; - use reth::transaction_pool::test_utils::TransactionBuilder; + use reth::{ + chainspec::EthChainSpec, + providers::{AccountReader, BlockNumReader, BlockReader}, + transaction_pool::test_utils::TransactionBuilder, + }; use reth_optimism_primitives::{OpBlock, OpReceipt, OpTransactionSigned}; use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; use reth_provider::{ChainSpecProvider, StateProviderFactory}; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - use std::time::Duration; use tokio::time::sleep; + + use crate::{ + rpc::{FlashblocksAPI, PendingBlocksAPI}, + state::FlashblocksState, + subscription::{Flashblock, FlashblocksReceiver, Metadata}, + tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}, + }; // 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; diff --git a/crates/test-utils/src/engine.rs b/crates/test-utils/src/engine.rs index 47b69f70..20b2853a 100644 --- a/crates/test-utils/src/engine.rs +++ b/crates/test-utils/src/engine.rs @@ -3,20 +3,22 @@ //! This module provides a typed, type-safe Engine API client based on //! reth's OpEngineApiClient trait instead of raw string-based RPC calls. +use std::{marker::PhantomData, time::Duration}; + use alloy_eips::eip7685::Requests; use alloy_primitives::B256; use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadId, PayloadStatus}; use eyre::Result; use jsonrpsee::core::client::SubscriptionClientT; use op_alloy_rpc_types_engine::OpExecutionPayloadV4; -use reth::api::{EngineTypes, PayloadTypes}; -use reth::rpc::types::engine::ForkchoiceState; +use reth::{ + api::{EngineTypes, PayloadTypes}, + rpc::types::engine::ForkchoiceState, +}; use reth_optimism_node::OpEngineTypes; use reth_optimism_rpc::OpEngineApiClient; use reth_rpc_layer::{AuthClientLayer, JwtSecret}; use reth_tracing::tracing::debug; -use std::marker::PhantomData; -use std::time::Duration; use url::Url; /// Default JWT secret for testing diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index a9d95545..f7d93af7 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -1,8 +1,7 @@ //! Unified test harness combining node, engine API, and flashblocks functionality -use crate::accounts::TestAccounts; -use crate::engine::{EngineApi, IpcEngine}; -use crate::node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}; +use std::{sync::Arc, time::Duration}; + use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; use alloy_primitives::{B256, Bytes, bytes}; use alloy_provider::{Provider, RootProvider}; @@ -13,16 +12,22 @@ use eyre::{Result, eyre}; use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; -use reth::builder::NodeHandle; -use reth::providers::{BlockNumReader, BlockReader}; +use reth::{ + builder::NodeHandle, + providers::{BlockNumReader, BlockReader}, +}; use reth_e2e_test_utils::Adapter; use reth_optimism_node::OpNode; use reth_optimism_primitives::OpBlock; use reth_primitives_traits::{Block as BlockT, RecoveredBlock}; -use std::sync::Arc; -use std::time::Duration; use tokio::time::sleep; +use crate::{ + accounts::TestAccounts, + engine::{EngineApi, IpcEngine}, + node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, +}; + const BLOCK_TIME_SECONDS: u64 = 2; const GAS_LIMIT: u64 = 200_000_000; const NODE_STARTUP_DELAY_MS: u64 = 500; @@ -179,12 +184,12 @@ impl TestHarness { #[cfg(test)] mod tests { - use crate::node::default_launcher; - - use super::*; use alloy_primitives::U256; use alloy_provider::Provider; + use super::*; + use crate::node::default_launcher; + #[tokio::test] async fn test_harness_setup() -> Result<()> { reth_tracing::init_test_tracing(); diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 207d132b..48464a6c 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -1,23 +1,28 @@ //! Local node setup with Base Sepolia chainspec -use crate::engine::EngineApi; +use std::{any::Any, net::SocketAddr, sync::Arc}; + use alloy_genesis::Genesis; use alloy_provider::RootProvider; use alloy_rpc_client::RpcClient; -use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; -use base_reth_flashblocks_rpc::state::FlashblocksState; -use base_reth_flashblocks_rpc::subscription::{Flashblock, FlashblocksReceiver}; +use base_reth_flashblocks_rpc::{ + rpc::{EthApiExt, EthApiOverrideServer}, + state::FlashblocksState, + subscription::{Flashblock, FlashblocksReceiver}, +}; use eyre::Result; use futures_util::Future; use once_cell::sync::OnceCell; use op_alloy_network::Optimism; -use reth::api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}; -use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; -use reth::builder::{ - Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, +use reth::{ + api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}, + args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, + builder::{ + Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, + }, + core::exit::NodeExitFuture, + tasks::TaskManager, }; -use reth::core::exit::NodeExitFuture; -use reth::tasks::TaskManager; use reth_db::{ ClientVersion, DatabaseEnv, init_db, mdbx::DatabaseArguments, @@ -30,16 +35,13 @@ use reth_node_core::{ dirs::{DataDirPath, MaybePlatformPath}, }; use reth_optimism_chainspec::OpChainSpec; -use reth_optimism_node::OpNode; -use reth_optimism_node::args::RollupArgs; -use reth_provider::CanonStateSubscriptions; -use reth_provider::providers::BlockchainProvider; -use std::any::Any; -use std::net::SocketAddr; -use std::sync::Arc; +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; + pub const BASE_CHAIN_ID: u64 = 84532; pub type LocalNodeProvider = BlockchainProvider>; From 3ed04a24ee0e0e9719dc7becea82b99cd4b6c897 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Fri, 21 Nov 2025 09:45:08 -0600 Subject: [PATCH 24/38] add jovianTime --- crates/test-utils/assets/genesis.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json index b3099c33..dde20c5e 100644 --- a/crates/test-utils/assets/genesis.json +++ b/crates/test-utils/assets/genesis.json @@ -22,6 +22,7 @@ "fjordTime": 0, "graniteTime": 0, "isthmusTime": 0, + "jovianTime": 0, "pragueTime": 0, "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true, @@ -103,4 +104,4 @@ } }, "number": "0x0" -} \ No newline at end of file +} From ca2eecf6d013f5f2f4f91499630e2fd60911e26c Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 20:05:12 -0600 Subject: [PATCH 25/38] Move flashblocks tests to integration suite --- crates/flashblocks-rpc/src/lib.rs | 2 - crates/flashblocks-rpc/src/tests/rpc.rs | 839 ----------------- crates/flashblocks-rpc/src/tests/state.rs | 888 ------------------ .../{src => }/tests/assets/genesis.json | 0 .../{src/tests => tests/common}/mod.rs | 7 +- crates/flashblocks-rpc/tests/rpc.rs | 769 +++++++++++++++ crates/flashblocks-rpc/tests/state.rs | 887 +++++++++++++++++ crates/test-utils/src/harness.rs | 12 +- 8 files changed, 1668 insertions(+), 1736 deletions(-) delete mode 100644 crates/flashblocks-rpc/src/tests/rpc.rs delete mode 100644 crates/flashblocks-rpc/src/tests/state.rs rename crates/flashblocks-rpc/{src => }/tests/assets/genesis.json (100%) rename crates/flashblocks-rpc/{src/tests => tests/common}/mod.rs (87%) create mode 100644 crates/flashblocks-rpc/tests/rpc.rs create mode 100644 crates/flashblocks-rpc/tests/state.rs diff --git a/crates/flashblocks-rpc/src/lib.rs b/crates/flashblocks-rpc/src/lib.rs index 3df51459..30b268b3 100644 --- a/crates/flashblocks-rpc/src/lib.rs +++ b/crates/flashblocks-rpc/src/lib.rs @@ -3,5 +3,3 @@ mod pending_blocks; pub mod rpc; pub mod state; pub mod subscription; -#[cfg(test)] -mod tests; diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs deleted file mode 100644 index b05e3647..00000000 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ /dev/null @@ -1,839 +0,0 @@ -#[cfg(test)] -mod tests { - use std::{str::FromStr, sync::Arc}; - - use alloy_consensus::Receipt; - use alloy_eips::BlockNumberOrTag; - use alloy_primitives::{ - Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes, map::HashMap, - }; - use alloy_provider::Provider; - 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_reth_test_utils::harness::TestHarness; - use eyre::Result; - use once_cell::sync::OnceCell; - use op_alloy_consensus::OpDepositReceipt; - use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; - use op_alloy_rpc_types::OpTransactionRequest; - use reth_exex::ExExEvent; - use reth_optimism_primitives::OpReceipt; - use reth_rpc_eth_api::RpcReceipt; - use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - use tokio::sync::{mpsc, oneshot}; - use tokio_stream::StreamExt; - - use crate::{ - rpc::{EthApiExt, EthApiOverrideServer}, - state::FlashblocksState, - subscription::{Flashblock, FlashblocksReceiver, Metadata}, - tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}, - }; - - pub struct TestSetup { - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - harness: TestHarness, - } - - impl TestSetup { - pub async fn new() -> Result { - let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let harness = TestHarness::new(|builder| { - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - - let builder = builder - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - move |mut ctx| async move { - let fb = fb_cell - .get_or_init(|| { - Arc::new(FlashblocksState::new(ctx.provider().clone(), 5)) - }) - .clone(); - Ok(async move { - while let Some(note) = ctx.notifications.try_next().await? { - if let Some(committed) = note.committed_chain() { - for b in committed.blocks_iter() { - fb.on_canonical_block_received(b); - } - let _ = ctx.events.send(ExExEvent::FinishedHeight( - committed.tip().num_hash(), - )); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules(move |ctx| { - let fb = fb_cell - .get_or_init(|| { - Arc::new(FlashblocksState::new(ctx.provider().clone(), 5)) - }) - .clone(); - - fb.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())?; - - tokio::spawn(async move { - while let Some((payload, tx)) = receiver.recv().await { - fb.on_flashblock_received(payload); - tx.send(()).unwrap(); - } - }); - - Ok(()) - }); - - let launcher = builder.engine_api_launcher(); - builder.launch_with(launcher) - }) - .await?; - - Ok(Self { sender, harness }) - } - - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.sender.send((flashblock, tx)).await?; - rx.await?; - Ok(()) - } - - pub async fn send_test_payloads(&self) -> Result<()> { - let base_payload = create_first_payload(); - self.send_flashblock(base_payload).await?; - - let second_payload = create_second_payload(); - self.send_flashblock(second_payload).await?; - - Ok(()) - } - - pub async fn send_raw_transaction_sync( - &self, - tx: Bytes, - timeout_ms: Option, - ) -> Result> { - let url = self.harness.rpc_url(); - let client = RpcClient::new_http(url.parse()?); - - let receipt = client - .request::<_, RpcReceipt>("eth_sendRawTransactionSync", (tx, timeout_ms)) - .await?; - - Ok(receipt) - } - } - - // Test constants - const TEST_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); - const PENDING_BALANCE: u64 = 4660; - - const DEPOSIT_SENDER: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001"); - const TX_SENDER: Address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); - - const DEPOSIT_TX_HASH: TxHash = - b256!("0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548"); - const TRANSFER_ETH_HASH: TxHash = - b256!("0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a"); - - const DEPLOYMENT_HASH: TxHash = - b256!("0x2b14d58c13406f25a78cfb802fb711c0d2c27bf9eccaec2d1847dc4392918f63"); - - const INCREMENT_HASH: TxHash = - b256!("0x993ad6a332752f6748636ce899b3791e4a33f7eece82c0db4556c7339c1b2929"); - const INCREMENT2_HASH: TxHash = - b256!("0x617a3673399647d12bb82ec8eba2ca3fc468e99894bcf1c67eb50ef38ee615cb"); - - const COUNTER_ADDRESS: Address = address!("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"); - - // Test log topics - these represent common events - const TEST_LOG_TOPIC_0: B256 = - b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); // Transfer event - const TEST_LOG_TOPIC_1: B256 = - b256!("0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"); // From address - const TEST_LOG_TOPIC_2: B256 = - b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address - - // Transaction bytes - const DEPOSIT_TX: Bytes = bytes!( - "0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3" - ); - const TRANSFER_ETH_TX: Bytes = bytes!( - "0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31" - ); - const DEPLOYMENT_TX: Bytes = bytes!( - "0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c" - ); - const INCREMENT_TX: Bytes = bytes!( - "0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89" - ); - const INCREMENT2_TX: Bytes = bytes!( - "0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2" - ); - - fn create_test_logs() -> Vec { - vec![ - alloy_primitives::Log { - address: COUNTER_ADDRESS, - data: LogData::new( - vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], - bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") - .into(), // 1 ETH in wei - ) - .unwrap(), - }, - alloy_primitives::Log { - address: TEST_ADDRESS, - data: LogData::new( - vec![TEST_LOG_TOPIC_0], - bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") - .into(), // Value: 1 - ) - .unwrap(), - }, - ] - } - - fn create_first_payload() -> Flashblock { - Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: B256::default(), - parent_hash: B256::default(), - fee_recipient: Address::ZERO, - prev_randao: B256::default(), - block_number: 1, - gas_limit: 30_000_000, - timestamp: 0, - extra_data: Bytes::new(), - base_fee_per_gas: U256::ZERO, - }), - diff: ExecutionPayloadFlashblockDeltaV1 { - transactions: vec![BLOCK_INFO_TXN], - ..Default::default() - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - BLOCK_INFO_TXN_HASH, - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 10000, - logs: vec![], - }, - deposit_nonce: Some(4012991u64), - deposit_receipt_version: None, - }), - ); - receipts - }, - new_account_balances: HashMap::default(), - }, - } - } - - fn create_second_payload() -> Flashblock { - Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 1, - base: None, - diff: ExecutionPayloadFlashblockDeltaV1 { - state_root: B256::default(), - receipts_root: B256::default(), - gas_used: 0, - block_hash: B256::default(), - transactions: vec![ - DEPOSIT_TX, - TRANSFER_ETH_TX, - DEPLOYMENT_TX, - INCREMENT_TX, - INCREMENT2_TX, - ], - withdrawals: Vec::new(), - logs_bloom: Default::default(), - withdrawals_root: Default::default(), - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - DEPOSIT_TX_HASH, - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 31000, - logs: vec![], - }, - deposit_nonce: Some(4012992u64), - deposit_receipt_version: None, - }), - ); - receipts.insert( - TRANSFER_ETH_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 55000, - logs: vec![], - }), - ); - receipts.insert( - DEPLOYMENT_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279, - logs: vec![], - }), - ); - receipts.insert( - INCREMENT_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279 + 44000, - logs: create_test_logs(), - }), - ); - receipts.insert( - INCREMENT2_HASH, - OpReceipt::Legacy(Receipt { - status: true.into(), - cumulative_gas_used: 272279 + 44000 + 44000, - logs: vec![], - }), - ); - receipts - }, - new_account_balances: { - let mut map = HashMap::default(); - map.insert(TEST_ADDRESS, U256::from(PENDING_BALANCE)); - map.insert(COUNTER_ADDRESS, U256::from(0)); - map - }, - }, - } - } - - #[tokio::test] - async fn test_get_pending_block() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - let latest_block = provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await? - .expect("latest block expected"); - assert_eq!(latest_block.number(), 0); - - // Querying pending block when it does not exist yet - let pending_block = provider - .get_block_by_number(BlockNumberOrTag::Pending) - .await? - .expect("latest block expected"); - - assert_eq!(pending_block.number(), latest_block.number()); - assert_eq!(pending_block.hash(), latest_block.hash()); - - let base_payload = create_first_payload(); - setup.send_flashblock(base_payload).await?; - - // Query pending block after sending the base payload with an empty delta - let pending_block = provider - .get_block_by_number(BlockNumberOrTag::Pending) - .await? - .expect("pending block expected"); - - assert_eq!(pending_block.number(), 1); - assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction - - let second_payload = create_second_payload(); - setup.send_flashblock(second_payload).await?; - - // Query pending block after sending the second payload with two transactions - let block = provider - .get_block_by_number(BlockNumberOrTag::Pending) - .await? - .expect("pending block expected"); - - assert_eq!(block.number(), 1); - assert_eq!(block.transactions.hashes().len(), 6); - - Ok(()) - } - - #[tokio::test] - async fn test_get_balance_pending() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - setup.send_test_payloads().await?; - - let balance = provider.get_balance(TEST_ADDRESS).await?; - assert_eq!(balance, U256::ZERO); - - let pending_balance = provider.get_balance(TEST_ADDRESS).pending().await?; - assert_eq!(pending_balance, U256::from(PENDING_BALANCE)); - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_by_hash_pending() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); - assert!(provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.is_none()); - - setup.send_test_payloads().await?; - - let tx1 = provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.expect("tx1 expected"); - assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); - assert_eq!(tx1.from(), DEPOSIT_SENDER); - - let tx2 = provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.expect("tx2 expected"); - assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); - assert_eq!(tx2.from(), TX_SENDER); - - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_receipt_pending() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; - assert_eq!(receipt.is_none(), true); - - setup.send_test_payloads().await?; - - let receipt = - provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?.expect("receipt expected"); - assert_eq!(receipt.gas_used(), 21000); - - let receipt = - provider.get_transaction_receipt(TRANSFER_ETH_HASH).await?.expect("receipt expected"); - assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 - - Ok(()) - } - - #[tokio::test] - async fn test_get_transaction_count() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 0); - - setup.send_test_payloads().await?; - - assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); - assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 4); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_call() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - // We ensure that eth_call will succeed because we are on plain state - let send_eth_call = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(200000) - .nonce(1) - .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) - .value(U256::from(9999999999849942300000u128)) - .input(TransactionInput::new(bytes!("0x"))); - - let res = - provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; - - assert!(res.is_ok()); - - setup.send_test_payloads().await?; - - // We included a heavy spending transaction and now don't have enough funds for this request, so - // this eth_call with fail - let res = - provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; - - assert!(res.is_err()); - assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") - ); - - // read count1 from counter contract - let eth_call_count1 = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(20000000) - .nonce(5) - .to(COUNTER_ADDRESS) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))); - let res_count1 = provider.call(eth_call_count1).await; - assert!(res_count1.is_ok()); - assert_eq!( - U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); - - // read count2 from counter contract - let eth_call_count2 = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(20000000) - .nonce(6) - .to(COUNTER_ADDRESS) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xd631c639"))); - let res_count2 = provider.call(eth_call_count2).await; - assert!(res_count2.is_ok()); - assert_eq!( - U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_estimate_gas() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - // We ensure that eth_estimate_gas will succeed because we are on plain state - let send_estimate_gas = OpTransactionRequest::default() - .from(TX_SENDER) - .transaction_type(0) - .gas_limit(200000) - .nonce(1) - .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) - .value(U256::from(9999999999849942300000u128)) - .input(TransactionInput::new(bytes!("0x"))); - - let res = provider - .estimate_gas(send_estimate_gas.clone()) - .block(BlockNumberOrTag::Pending.into()) - .await; - - assert!(res.is_ok()); - - setup.send_test_payloads().await?; - - // We included a heavy spending transaction and now don't have enough funds for this request, so - // this eth_estimate_gas with fail - let res = provider - .estimate_gas(send_estimate_gas.nonce(4)) - .block(BlockNumberOrTag::Pending.into()) - .await; - - assert!(res.is_err()); - assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") - ); - - Ok(()) - } - - #[tokio::test] - async fn test_eth_simulate_v1() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - setup.send_test_payloads().await?; - - let simulate_call = SimulatePayload { - block_state_calls: vec![SimBlock { - calls: vec![ - // read number from counter contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))) - .into(), - // increment() value in contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .input(TransactionInput::new(bytes!("0xd09de08a"))) - .into(), - // read number from counter contract - OpTransactionRequest::default() - .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) - .transaction_type(0) - .gas_limit(200000) - .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) - .value(U256::ZERO) - .input(TransactionInput::new(bytes!("0xa87d942c"))) - .into(), - ], - block_overrides: None, - state_overrides: None, - }], - trace_transfers: false, - validation: true, - return_full_transactions: true, - }; - let simulate_res = - provider.simulate(&simulate_call).block_id(BlockNumberOrTag::Pending.into()).await; - assert!(simulate_res.is_ok()); - let block = simulate_res.unwrap(); - assert_eq!(block.len(), 1); - assert_eq!(block[0].calls.len(), 3); - assert_eq!( - block[0].calls[0].return_data, - bytes!("0x0000000000000000000000000000000000000000000000000000000000000002") - ); - assert_eq!(block[0].calls[1].return_data, bytes!("0x")); - assert_eq!( - block[0].calls[2].return_data, - bytes!("0x0000000000000000000000000000000000000000000000000000000000000003") - ); - - Ok(()) - } - - #[tokio::test] - async fn test_send_raw_transaction_sync() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - - setup.send_flashblock(create_first_payload()).await?; - - // run the Tx sync and, in parallel, deliver the payload that contains the Tx - let (receipt_result, payload_result) = - tokio::join!(setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), async { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - setup.send_flashblock(create_second_payload()).await - }); - - payload_result?; - let receipt = receipt_result?; - - assert_eq!(receipt.transaction_hash(), TRANSFER_ETH_HASH); - Ok(()) - } - - #[tokio::test] - async fn test_send_raw_transaction_sync_timeout() { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await.unwrap(); - - // fail request immediately by passing a timeout of 0 ms - let receipt_result = setup.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; - - let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); - assert!( - receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str()) - ); - } - - #[tokio::test] - async fn test_get_logs_pending() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - // Test no logs when no flashblocks sent - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .select(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - assert_eq!(logs.len(), 0); - - // Send payloads with transactions - setup.send_test_payloads().await?; - - // Test getting pending logs - must use both fromBlock and toBlock as "pending" - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // We should now have 2 logs from the INCREMENT_TX transaction - assert_eq!(logs.len(), 2); - - // Verify the first log is from COUNTER_ADDRESS - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].topics()[0], TEST_LOG_TOPIC_0); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - // Verify the second log is from TEST_ADDRESS - assert_eq!(logs[1].address(), TEST_ADDRESS); - assert_eq!(logs[1].topics()[0], TEST_LOG_TOPIC_0); - assert_eq!(logs[1].transaction_hash, Some(INCREMENT_HASH)); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_filter_by_address() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - setup.send_test_payloads().await?; - - // Test filtering by a specific address (COUNTER_ADDRESS) - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .address(COUNTER_ADDRESS) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should get only 1 log from COUNTER_ADDRESS - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - // Test filtering by TEST_ADDRESS - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .address(TEST_ADDRESS) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should get only 1 log from TEST_ADDRESS - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), TEST_ADDRESS); - assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_topic_filtering() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - setup.send_test_payloads().await?; - - // Test filtering by topic - should match both logs - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .event_signature(TEST_LOG_TOPIC_0) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.topics()[0] == TEST_LOG_TOPIC_0)); - - // Test filtering by specific topic combination - should match only the first log - let filter = alloy_rpc_types_eth::Filter::default() - .topic1(TEST_LOG_TOPIC_1) - .from_block(alloy_eips::BlockNumberOrTag::Pending) - .to_block(alloy_eips::BlockNumberOrTag::Pending); - - let logs = provider.get_logs(&filter).await?; - - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].address(), COUNTER_ADDRESS); - assert_eq!(logs[0].topics()[1], TEST_LOG_TOPIC_1); - - Ok(()) - } - - #[tokio::test] - async fn test_get_logs_mixed_block_ranges() -> Result<()> { - reth_tracing::init_test_tracing(); - let setup = TestSetup::new().await?; - let provider = setup.harness.provider(); - - setup.send_test_payloads().await?; - - // Test fromBlock: 0, toBlock: pending (should include both historical and pending) - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(0) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should now include pending logs (2 logs from our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - // Test fromBlock: latest, toBlock: pending - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Latest) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should include pending logs (historical part is empty in our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - // Test fromBlock: earliest, toBlock: pending - let logs = provider - .get_logs( - &alloy_rpc_types_eth::Filter::default() - .from_block(alloy_eips::BlockNumberOrTag::Earliest) - .to_block(alloy_eips::BlockNumberOrTag::Pending), - ) - .await?; - - // Should include pending logs (historical part is empty in our test setup) - assert_eq!(logs.len(), 2); - assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); - - Ok(()) - } -} diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs deleted file mode 100644 index ae2e94db..00000000 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ /dev/null @@ -1,888 +0,0 @@ -#[cfg(test)] -mod tests { - use std::time::Duration; - - use alloy_consensus::{Receipt, Transaction}; - use alloy_eips::{BlockHashOrNumber, Encodable2718}; - use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex, map::foldhash::HashMap}; - use alloy_rpc_types_engine::PayloadId; - use base_reth_test_utils::{ - accounts::TestAccounts, - harness::TestHarness as BaseHarness, - node::{LocalNodeProvider, default_launcher}, - }; - use op_alloy_consensus::OpDepositReceipt; - use op_alloy_network::BlockResponse; - use reth::{ - chainspec::EthChainSpec, - providers::{AccountReader, BlockNumReader, BlockReader}, - transaction_pool::test_utils::TransactionBuilder, - }; - use reth_optimism_primitives::{OpBlock, OpReceipt, OpTransactionSigned}; - use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; - use reth_provider::{ChainSpecProvider, StateProviderFactory}; - use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - use tokio::time::sleep; - - use crate::{ - rpc::{FlashblocksAPI, PendingBlocksAPI}, - state::FlashblocksState, - subscription::{Flashblock, FlashblocksReceiver, Metadata}, - tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}, - }; - // 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: BaseHarness, - flashblocks: FlashblocksState, - provider: LocalNodeProvider, - user_to_address: HashMap, - user_to_private_key: HashMap, - } - - impl TestHarness { - async fn new() -> Self { - let node = - BaseHarness::new(default_launcher).await.expect("able to launch base harness"); - let provider = node.blockchain_provider(); - let flashblocks = FlashblocksState::new(provider.clone(), 5); - flashblocks.start(); - - let genesis_block = provider - .block(BlockHashOrNumber::Number(0)) - .expect("able to load block") - .expect("block exists") - .try_into_recovered() - .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 } - } - - fn decode_private_key(key: &str) -> B256 { - let bytes = hex::decode(key).expect("valid hex-encoded key"); - B256::from_slice(&bytes) - } - - 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 { - self.provider - .basic_account(&self.address(u)) - .expect("can lookup account state") - .expect("should be existing account state") - } - - fn canonical_balance(&self, u: User) -> U256 { - self.canonical_account(u).balance - } - - fn expected_pending_balance(&self, u: User, delta: u128) -> U256 { - self.canonical_balance(u) + U256::from(delta) - } - - fn account_state(&self, u: User) -> Account { - let basic_account = self.canonical_account(u); - - let nonce = self - .flashblocks - .get_pending_blocks() - .get_transaction_count(self.address(u)) - .to::(); - let balance = self - .flashblocks - .get_pending_blocks() - .get_balance(self.address(u)) - .unwrap_or(basic_account.balance); - - Account { - nonce: nonce + basic_account.nonce, - balance, - bytecode_hash: basic_account.bytecode_hash, - } - } - - fn build_transaction_to_send_eth( - &self, - from: User, - to: User, - amount: u128, - ) -> OpTransactionSigned { - let txn = TransactionBuilder::default() - .signer(self.signer(from)) - .chain_id(self.provider.chain_spec().chain_id()) - .to(self.address(to)) - .nonce(self.account_state(from).nonce) - .value(amount) - .gas_limit(21_000) - .max_fee_per_gas(1_000_000_000) - .max_priority_fee_per_gas(1_000_000_000) - .into_eip1559() - .as_eip1559() - .unwrap() - .clone(); - - OpTransactionSigned::Eip1559(txn) - } - - fn build_transaction_to_send_eth_with_nonce( - &self, - from: User, - to: User, - amount: u128, - nonce: u64, - ) -> OpTransactionSigned { - let txn = TransactionBuilder::default() - .signer(self.signer(from)) - .chain_id(self.provider.chain_spec().chain_id()) - .to(self.address(to)) - .nonce(nonce) - .value(amount) - .gas_limit(21_000) - .max_fee_per_gas(1_000_000_000) - .max_priority_fee_per_gas(1_000_000_000) - .into_eip1559() - .as_eip1559() - .unwrap() - .clone(); - - OpTransactionSigned::Eip1559(txn) - } - - async fn send_flashblock(&self, flashblock: Flashblock) { - self.flashblocks.on_flashblock_received(flashblock); - sleep(Duration::from_millis(SLEEP_TIME)).await; - } - - async fn new_canonical_block_without_processing( - &mut self, - user_transactions: Vec, - ) -> RecoveredBlock { - let previous_tip = - self.provider.best_block_number().expect("able to read best block number"); - let txs: Vec = - user_transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(); - self.node.build_block_from_transactions(txs).await.expect("able to build block"); - let target_block_number = previous_tip + 1; - - let block = self - .provider - .block(BlockHashOrNumber::Number(target_block_number)) - .expect("able to load block") - .expect("new canonical block should be available after building payload"); - - block.try_into_recovered().expect("able to recover newly built block") - } - - async fn new_canonical_block(&mut self, user_transactions: Vec) { - let block = self.new_canonical_block_without_processing(user_transactions).await; - self.flashblocks.on_canonical_block_received(&block); - sleep(Duration::from_millis(SLEEP_TIME)).await; - } - } - - struct FlashblockBuilder<'a> { - transactions: Vec, - receipts: HashMap, - harness: &'a TestHarness, - canonical_block_number: Option, - index: u64, - } - - impl<'a> FlashblockBuilder<'a> { - pub fn new_base(harness: &'a TestHarness) -> Self { - Self { - canonical_block_number: None, - transactions: vec![BLOCK_INFO_TXN.clone()], - receipts: { - let mut receipts = alloy_primitives::map::HashMap::default(); - receipts.insert( - BLOCK_INFO_TXN_HASH, - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 10000, - logs: vec![], - }, - deposit_nonce: Some(4012991u64), - deposit_receipt_version: None, - }), - ); - receipts - }, - index: 0, - harness, - } - } - pub fn new(harness: &'a TestHarness, index: u64) -> Self { - Self { - canonical_block_number: None, - transactions: Vec::new(), - receipts: HashMap::default(), - harness, - index, - } - } - - pub fn with_receipts(&mut self, receipts: HashMap) -> &mut Self { - self.receipts = receipts; - self - } - - pub fn with_transactions(&mut self, transactions: Vec) -> &mut Self { - assert_ne!(self.index, 0, "Cannot set txns for initial flashblock"); - self.transactions.clear(); - - let mut cumulative_gas_used = 0; - for txn in transactions.iter() { - cumulative_gas_used = cumulative_gas_used + txn.gas_limit(); - self.transactions.push(txn.encoded_2718().into()); - self.receipts.insert( - txn.hash().clone(), - OpReceipt::Eip1559(Receipt { - status: true.into(), - cumulative_gas_used, - logs: vec![], - }), - ); - } - self - } - - pub fn with_canonical_block_number(&mut self, num: BlockNumber) -> &mut Self { - self.canonical_block_number = Some(num); - self - } - - pub fn build(&self) -> Flashblock { - let current_block = self.harness.node.latest_block(); - let canonical_block_num = - self.canonical_block_number.unwrap_or_else(|| current_block.number) + 1; - - let base = if self.index == 0 { - Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: current_block.hash(), - parent_hash: current_block.hash(), - fee_recipient: Address::random(), - prev_randao: B256::random(), - block_number: canonical_block_num, - gas_limit: current_block.gas_limit, - timestamp: current_block.timestamp + 2, - extra_data: Bytes::new(), - base_fee_per_gas: U256::from(100), - }) - } else { - None - }; - - Flashblock { - payload_id: PayloadId::default(), - index: self.index, - base, - diff: ExecutionPayloadFlashblockDeltaV1 { - state_root: B256::default(), - receipts_root: B256::default(), - block_hash: B256::default(), - gas_used: 0, - withdrawals: Vec::new(), - logs_bloom: Default::default(), - withdrawals_root: Default::default(), - transactions: self.transactions.clone(), - blob_gas_used: Default::default(), - }, - metadata: Metadata { - block_number: canonical_block_num, - receipts: self.receipts.clone(), - new_account_balances: HashMap::default(), - }, - } - } - } - - #[tokio::test] - async fn test_state_overrides_persisted_across_flashblocks() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("block is built") - .transactions - .len(), - 1 - ); - - assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); - assert!( - !test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .unwrap() - .contains_key(&test.address(User::Alice)) - ); - - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100_000, - )]) - .build(), - ) - .await; - - let pending = test.flashblocks.get_pending_blocks().get_block(true); - assert!(pending.is_some()); - let pending = pending.unwrap(); - assert_eq!(pending.transactions.len(), 2); - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .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.send_flashblock(FlashblockBuilder::new(&test, 2).build()).await; - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution in flashblock index 1"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .expect("should be set as txn receiver") - .balance - .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) - ); - } - - #[tokio::test] - async fn test_state_overrides_persisted_across_blocks() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - let initial_base = FlashblockBuilder::new_base(&test).build(); - let initial_block_number = initial_base.metadata.block_number; - test.send_flashblock(initial_base).await; - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("block is built") - .transactions - .len(), - 1 - ); - - assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); - assert!( - !test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .unwrap() - .contains_key(&test.address(User::Alice)) - ); - - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100_000, - )]) - .build(), - ) - .await; - - let pending = test.flashblocks.get_pending_blocks().get_block(true); - assert!(pending.is_some()); - let pending = pending.unwrap(); - assert_eq!(pending.transactions.len(), 2); - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .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.send_flashblock( - FlashblockBuilder::new_base(&test) - .with_canonical_block_number(initial_block_number) - .build(), - ) - .await; - - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("block is built") - .transactions - .len(), - 1 - ); - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("block is built") - .header - .number, - initial_block_number + 1 - ); - - assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); - assert!( - test.flashblocks - .get_pending_blocks() - .get_state_overrides() - .unwrap() - .contains_key(&test.address(User::Alice)) - ); - - 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, - 100_000, - )]) - .build(), - ) - .await; - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .expect("should be set as txn receiver") - .balance - .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 200_000) - ); - } - - #[tokio::test] - async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { - reth_tracing::init_test_tracing(); - let mut test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("block is built") - .transactions - .len(), - 1 - ); - assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); - assert!( - !test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .unwrap() - .contains_key(&test.address(User::Alice)) - ); - - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100_000, - )]) - .build(), - ) - .await; - let pending = test.flashblocks.get_pending_blocks().get_block(true); - assert!(pending.is_some()); - let pending = pending.unwrap(); - assert_eq!(pending.transactions.len(), 2); - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .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.send_flashblock( - FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), - ) - .await; - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_canonical_block_number(1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100_000, - )]) - .build(), - ) - .await; - let pending = test.flashblocks.get_pending_blocks().get_block(true); - assert!(pending.is_some()); - let pending = pending.unwrap(); - assert_eq!(pending.transactions.len(), 2); - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .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.new_canonical_block(vec![test.build_transaction_to_send_eth_with_nonce( - User::Alice, - User::Bob, - 100, - 0, - )]) - .await; - - let pending = test.flashblocks.get_pending_blocks().get_block(true); - assert!(pending.is_some()); - let pending = pending.unwrap(); - assert_eq!(pending.transactions.len(), 2); - - let overrides = test - .flashblocks - .get_pending_blocks() - .get_state_overrides() - .expect("should be set from txn execution"); - - assert!(overrides.get(&test.address(User::Alice)).is_some()); - assert_eq!( - overrides - .get(&test.address(User::Bob)) - .expect("should be set as txn receiver") - .balance - .expect("should be changed due to receiving funds"), - test.expected_pending_balance(User::Bob, 100_000) - ); - } - - #[tokio::test] - async fn test_nonce_uses_pending_canon_block_instead_of_latest() { - // Test for race condition when a canon block comes in but user - // requests their nonce prior to the StateProcessor processing the canon block - // causing it to return an n+1 nonce instead of n - // because underlying reth node `latest` block is already updated, but - // relevant pending state has not been cleared yet - reth_tracing::init_test_tracing(); - let mut test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100, - )]) - .build(), - ) - .await; - - let pending_nonce = - test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce - + test - .flashblocks - .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) - .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), - ]) - .await; - - let pending_nonce = - test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce - + test - .flashblocks - .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) - .to::(); - - // This is 2, because canon block has reached the underlying chain - // but the StateProcessor hasn't processed it - // so pending nonce is effectively double-counting the same transaction, leading to a nonce of 2 - assert_eq!(pending_nonce, 2); - - // On the RPC level, we correctly return 1 because we - // use the pending canon block instead of the latest block when fetching - // onchain nonce count to compute - // pending_nonce = onchain_nonce + pending_txn_count - 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(); - let pending_nonce = canon_nonce - + test - .flashblocks - .get_pending_blocks() - .get_transaction_count(test.address(User::Alice)) - .to::(); - assert_eq!(pending_nonce, 1); - } - - #[tokio::test] - async fn test_missing_receipts_will_not_process() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - - let current_block = test.flashblocks.get_pending_blocks().get_block(true); - - test.send_flashblock( - FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100, - )]) - .with_receipts(HashMap::default()) // Clear the receipts - .build(), - ) - .await; - - let pending_block = test.flashblocks.get_pending_blocks().get_block(true); - - // When the flashblock is invalid, the chain doesn't progress - assert_eq!(pending_block.unwrap().hash(), current_block.unwrap().hash()); - } - - #[tokio::test] - async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - - let current_block = - test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); - - assert_eq!(current_block.header().number, 1); - assert_eq!(current_block.transactions.len(), 1); - - test.send_flashblock( - FlashblockBuilder::new(&test, 1).with_canonical_block_number(100).build(), - ) - .await; - - let current_block = test.flashblocks.get_pending_blocks().get_block(true); - assert!(current_block.is_none()); - } - - #[tokio::test] - async fn test_flashblock_for_new_canonical_block_works_if_sequential() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - - let current_block = - test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); - - assert_eq!(current_block.header().number, 1); - assert_eq!(current_block.transactions.len(), 1); - - test.send_flashblock( - FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), - ) - .await; - - let current_block = - test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); - - assert_eq!(current_block.header().number, 2); - assert_eq!(current_block.transactions.len(), 1); - } - - #[tokio::test] - async fn test_non_sequential_payload_clears_pending_state() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - - // Just the block info transaction - assert_eq!( - test.flashblocks - .get_pending_blocks() - .get_block(true) - .expect("should be set") - .transactions - .len(), - 1 - ); - - test.send_flashblock( - FlashblockBuilder::new(&test, 3) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100, - )]) - .build(), - ) - .await; - - assert_eq!(test.flashblocks.get_pending_blocks().is_none(), true); - } - - #[tokio::test] - async fn test_duplicate_flashblock_ignored() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new().await; - - test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; - - let fb = FlashblockBuilder::new(&test, 1) - .with_transactions(vec![test.build_transaction_to_send_eth( - User::Alice, - User::Bob, - 100_000, - )]) - .build(); - - test.send_flashblock(fb.clone()).await; - let block = test.flashblocks.get_pending_blocks().get_block(true); - - test.send_flashblock(fb.clone()).await; - let block_two = test.flashblocks.get_pending_blocks().get_block(true); - - assert_eq!(block, block_two); - } - - #[tokio::test] - async fn test_progress_canonical_blocks_without_flashblocks() { - reth_tracing::init_test_tracing(); - let mut test = TestHarness::new().await; - - let genesis_block = test.node.latest_block(); - assert_eq!(genesis_block.number, 0); - 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; - - let block_one = test.node.latest_block(); - assert_eq!(block_one.number, 1); - assert_eq!(block_one.transaction_count(), 2); - 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), - ]) - .await; - - let block_two = test.node.latest_block(); - assert_eq!(block_two.number, 2); - assert_eq!(block_two.transaction_count(), 3); - assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); - } -} diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/tests/assets/genesis.json similarity index 100% rename from crates/flashblocks-rpc/src/tests/assets/genesis.json rename to crates/flashblocks-rpc/tests/assets/genesis.json diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/tests/common/mod.rs similarity index 87% rename from crates/flashblocks-rpc/src/tests/mod.rs rename to crates/flashblocks-rpc/tests/common/mod.rs index dca64a96..5ffd4a5b 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/tests/common/mod.rs @@ -1,10 +1,7 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; -mod rpc; -mod state; - -const BLOCK_INFO_TXN: Bytes = bytes!( +pub const BLOCK_INFO_TXN: Bytes = bytes!( "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" ); -const BLOCK_INFO_TXN_HASH: B256 = +pub const BLOCK_INFO_TXN_HASH: B256 = b256!("0xba56c8b0deb460ff070f8fca8e2ee01e51a3db27841cc862fdd94cc1a47662b6"); diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs new file mode 100644 index 00000000..7f53a034 --- /dev/null +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -0,0 +1,769 @@ +mod common; + +use std::str::FromStr; + +use alloy_consensus::Receipt; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::{ + Address, B256, Bytes, LogData, TxHash, U256, address, b256, bytes, map::HashMap, +}; +use alloy_provider::Provider; +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_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; +use base_reth_test_utils::{harness::TestHarness, node::default_launcher}; +use eyre::Result; +use op_alloy_consensus::OpDepositReceipt; +use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; +use op_alloy_rpc_types::OpTransactionRequest; +use reth_optimism_primitives::OpReceipt; +use reth_rpc_eth_api::RpcReceipt; +use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + +use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + +struct TestSetup { + harness: TestHarness, +} + +impl TestSetup { + async fn new() -> Result { + let harness = TestHarness::new(default_launcher).await?; + Ok(Self { harness }) + } + + async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.harness.send_flashblock(flashblock).await + } + + async fn send_test_payloads(&self) -> Result<()> { + let base_payload = create_first_payload(); + self.send_flashblock(base_payload).await?; + + let second_payload = create_second_payload(); + self.send_flashblock(second_payload).await?; + + Ok(()) + } + + async fn send_raw_transaction_sync( + &self, + tx: Bytes, + timeout_ms: Option, + ) -> Result> { + let url = self.harness.rpc_url(); + let client = RpcClient::new_http(url.parse()?); + + let receipt = client + .request::<_, RpcReceipt>("eth_sendRawTransactionSync", (tx, timeout_ms)) + .await?; + + Ok(receipt) + } +} + +// Test constants +const TEST_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); +const PENDING_BALANCE: u64 = 4660; + +const DEPOSIT_SENDER: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001"); +const TX_SENDER: Address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); + +const DEPOSIT_TX_HASH: TxHash = + b256!("0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548"); +const TRANSFER_ETH_HASH: TxHash = + b256!("0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a"); + +const DEPLOYMENT_HASH: TxHash = + b256!("0x2b14d58c13406f25a78cfb802fb711c0d2c27bf9eccaec2d1847dc4392918f63"); + +const INCREMENT_HASH: TxHash = + b256!("0x993ad6a332752f6748636ce899b3791e4a33f7eece82c0db4556c7339c1b2929"); +const INCREMENT2_HASH: TxHash = + b256!("0x617a3673399647d12bb82ec8eba2ca3fc468e99894bcf1c67eb50ef38ee615cb"); + +const COUNTER_ADDRESS: Address = address!("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"); + +// Test log topics - these represent common events +const TEST_LOG_TOPIC_0: B256 = + b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); // Transfer event +const TEST_LOG_TOPIC_1: B256 = + b256!("0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"); // From address +const TEST_LOG_TOPIC_2: B256 = + b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address + +// Transaction bytes +const DEPOSIT_TX: Bytes = bytes!( + "0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3" +); +const TRANSFER_ETH_TX: Bytes = bytes!( + "0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31" +); +const DEPLOYMENT_TX: Bytes = bytes!( + "0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c" +); +const INCREMENT_TX: Bytes = bytes!( + "0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89" +); +const INCREMENT2_TX: Bytes = bytes!( + "0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2" +); + +fn create_test_logs() -> Vec { + vec![ + alloy_primitives::Log { + address: COUNTER_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], + bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") + .into(), // 1 ETH in wei + ) + .unwrap(), + }, + alloy_primitives::Log { + address: TEST_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0], + bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") + .into(), // Value: 1 + ) + .unwrap(), + }, + ] +} + +fn create_first_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 0, + base: Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: B256::default(), + parent_hash: B256::default(), + fee_recipient: Address::ZERO, + prev_randao: B256::default(), + block_number: 1, + gas_limit: 30_000_000, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + }), + diff: ExecutionPayloadFlashblockDeltaV1 { + blob_gas_used: Some(0), + transactions: vec![BLOCK_INFO_TXN], + ..Default::default() + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + BLOCK_INFO_TXN_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 10000, + logs: vec![], + }, + deposit_nonce: Some(4012991u64), + deposit_receipt_version: None, + }), + ); + receipts + }, + new_account_balances: HashMap::default(), + }, + } +} + +fn create_second_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 1, + base: None, + diff: ExecutionPayloadFlashblockDeltaV1 { + state_root: B256::default(), + receipts_root: B256::default(), + gas_used: 0, + block_hash: B256::default(), + blob_gas_used: Some(0), + transactions: vec![ + DEPOSIT_TX, + TRANSFER_ETH_TX, + DEPLOYMENT_TX, + INCREMENT_TX, + INCREMENT2_TX, + ], + withdrawals: Vec::new(), + logs_bloom: Default::default(), + withdrawals_root: Default::default(), + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + DEPOSIT_TX_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 31000, + logs: vec![], + }, + deposit_nonce: Some(4012992u64), + deposit_receipt_version: None, + }), + ); + receipts.insert( + TRANSFER_ETH_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 55000, + logs: vec![], + }), + ); + receipts.insert( + DEPLOYMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279, + logs: vec![], + }), + ); + receipts.insert( + INCREMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000, + logs: create_test_logs(), + }), + ); + receipts.insert( + INCREMENT2_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000 + 44000, + logs: vec![], + }), + ); + receipts + }, + new_account_balances: { + let mut map = HashMap::default(); + map.insert(TEST_ADDRESS, U256::from(PENDING_BALANCE)); + map.insert(COUNTER_ADDRESS, U256::from(0)); + map + }, + }, + } +} + +#[tokio::test] +async fn test_get_pending_block() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let latest_block = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("latest block expected"); + assert_eq!(latest_block.number(), 0); + + // Querying pending block when it does not exist yet + let pending_block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await? + .expect("latest block expected"); + + assert_eq!(pending_block.number(), latest_block.number()); + assert_eq!(pending_block.hash(), latest_block.hash()); + + let base_payload = create_first_payload(); + setup.send_flashblock(base_payload).await?; + + // Query pending block after sending the base payload with an empty delta + let pending_block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await? + .expect("pending block expected"); + + assert_eq!(pending_block.number(), 1); + assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction + + let second_payload = create_second_payload(); + setup.send_flashblock(second_payload).await?; + + // Query pending block after sending the second payload with two transactions + let block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await? + .expect("pending block expected"); + + assert_eq!(block.number(), 1); + assert_eq!(block.transactions.hashes().len(), 6); + + Ok(()) +} + +#[tokio::test] +async fn test_get_balance_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + let balance = provider.get_balance(TEST_ADDRESS).await?; + assert_eq!(balance, U256::ZERO); + + let pending_balance = provider.get_balance(TEST_ADDRESS).pending().await?; + assert_eq!(pending_balance, U256::from(PENDING_BALANCE)); + Ok(()) +} + +#[tokio::test] +async fn test_get_transaction_by_hash_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); + assert!(provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.is_none()); + + setup.send_test_payloads().await?; + + let tx1 = provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.expect("tx1 expected"); + assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); + assert_eq!(tx1.from(), DEPOSIT_SENDER); + + let tx2 = provider.get_transaction_by_hash(TRANSFER_ETH_HASH).await?.expect("tx2 expected"); + assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); + assert_eq!(tx2.from(), TX_SENDER); + + Ok(()) +} + +#[tokio::test] +async fn test_get_transaction_receipt_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; + assert_eq!(receipt.is_none(), true); + + setup.send_test_payloads().await?; + + let receipt = + provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?.expect("receipt expected"); + assert_eq!(receipt.gas_used(), 21000); + + let receipt = + provider.get_transaction_receipt(TRANSFER_ETH_HASH).await?.expect("receipt expected"); + assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 + + Ok(()) +} + +#[tokio::test] +async fn test_get_transaction_count() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 0); + + setup.send_test_payloads().await?; + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!(provider.get_transaction_count(TX_SENDER).pending().await?, 4); + + Ok(()) +} + +#[tokio::test] +async fn test_eth_call() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_call will succeed because we are on plain state + let send_eth_call = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = + provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_call with fail + let res = + provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; + + assert!(res.is_err()); + assert!( + res.unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas") + ); + + // read count1 from counter contract + let eth_call_count1 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(5) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))); + let res_count1 = provider.call(eth_call_count1).await; + assert!(res_count1.is_ok()); + assert_eq!( + U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + // read count2 from counter contract + let eth_call_count2 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(6) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xd631c639"))); + let res_count2 = provider.call(eth_call_count2).await; + assert!(res_count2.is_ok()); + assert_eq!( + U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + Ok(()) +} + +#[tokio::test] +async fn test_eth_estimate_gas() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_estimate_gas will succeed because we are on plain state + let send_estimate_gas = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = provider + .estimate_gas(send_estimate_gas.clone()) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_estimate_gas with fail + let res = provider + .estimate_gas(send_estimate_gas.nonce(4)) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_err()); + assert!( + res.unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas") + ); + + Ok(()) +} + +#[tokio::test] +async fn test_eth_simulate_v1() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + setup.send_test_payloads().await?; + + let simulate_call = SimulatePayload { + block_state_calls: vec![SimBlock { + calls: vec![ + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + // increment() value in contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .input(TransactionInput::new(bytes!("0xd09de08a"))) + .into(), + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + ], + block_overrides: None, + state_overrides: None, + }], + trace_transfers: false, + validation: true, + return_full_transactions: true, + }; + let simulate_res = + provider.simulate(&simulate_call).block_id(BlockNumberOrTag::Pending.into()).await; + assert!(simulate_res.is_ok()); + let block = simulate_res.unwrap(); + assert_eq!(block.len(), 1); + assert_eq!(block[0].calls.len(), 3); + assert_eq!( + block[0].calls[0].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000002") + ); + assert_eq!(block[0].calls[1].return_data, bytes!("0x")); + assert_eq!( + block[0].calls[2].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000003") + ); + + Ok(()) +} + +#[tokio::test] +async fn test_send_raw_transaction_sync() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + + setup.send_flashblock(create_first_payload()).await?; + + // run the Tx sync and, in parallel, deliver the payload that contains the Tx + let (receipt_result, payload_result) = + tokio::join!(setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), async { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + setup.send_flashblock(create_second_payload()).await + }); + + payload_result?; + let receipt = receipt_result?; + + assert_eq!(receipt.transaction_hash(), TRANSFER_ETH_HASH); + Ok(()) +} + +#[tokio::test] +async fn test_send_raw_transaction_sync_timeout() { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await.unwrap(); + + // fail request immediately by passing a timeout of 0 ms + let receipt_result = setup.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; + + let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); + assert!( + receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str()) + ); +} + +#[tokio::test] +async fn test_get_logs_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // Test no logs when no flashblocks sent + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .select(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + assert_eq!(logs.len(), 0); + + // Send payloads with transactions + setup.send_test_payloads().await?; + + // Test getting pending logs - must use both fromBlock and toBlock as "pending" + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // We should now have 2 logs from the INCREMENT_TX transaction + assert_eq!(logs.len(), 2); + + // Verify the first log is from COUNTER_ADDRESS + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Verify the second log is from TEST_ADDRESS + assert_eq!(logs[1].address(), TEST_ADDRESS); + assert_eq!(logs[1].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[1].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) +} + +#[tokio::test] +async fn test_get_logs_filter_by_address() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by a specific address (COUNTER_ADDRESS) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(COUNTER_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from COUNTER_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Test filtering by TEST_ADDRESS + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(TEST_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from TEST_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), TEST_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) +} + +#[tokio::test] +async fn test_get_logs_topic_filtering() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by topic - should match both logs + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .event_signature(TEST_LOG_TOPIC_0) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.topics()[0] == TEST_LOG_TOPIC_0)); + + // Test filtering by specific topic combination - should match only the first log + let filter = alloy_rpc_types_eth::Filter::default() + .topic1(TEST_LOG_TOPIC_1) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending); + + let logs = provider.get_logs(&filter).await?; + + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[1], TEST_LOG_TOPIC_1); + + Ok(()) +} + +#[tokio::test] +async fn test_get_logs_mixed_block_ranges() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test fromBlock: 0, toBlock: pending (should include both historical and pending) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(0) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should now include pending logs (2 logs from our test setup) + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: latest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Latest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: earliest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Earliest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + Ok(()) +} diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs new file mode 100644 index 00000000..b2815914 --- /dev/null +++ b/crates/flashblocks-rpc/tests/state.rs @@ -0,0 +1,887 @@ +mod common; + +use std::time::Duration; + +use alloy_consensus::{Receipt, Transaction}; +use alloy_eips::{BlockHashOrNumber, Encodable2718}; +use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex, map::foldhash::HashMap}; +use alloy_rpc_types_engine::PayloadId; +use base_reth_flashblocks_rpc::{ + rpc::{FlashblocksAPI, PendingBlocksAPI}, + state::FlashblocksState, + subscription::{Flashblock, FlashblocksReceiver, Metadata}, +}; +use base_reth_test_utils::{ + accounts::TestAccounts, + harness::TestHarness as BaseHarness, + node::{LocalNodeProvider, default_launcher}, +}; +use op_alloy_consensus::OpDepositReceipt; +use op_alloy_network::BlockResponse; +use reth::{ + chainspec::EthChainSpec, + providers::{AccountReader, BlockNumReader, BlockReader}, + transaction_pool::test_utils::TransactionBuilder, +}; +use reth_optimism_primitives::{OpBlock, OpReceipt, OpTransactionSigned}; +use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; +use reth_provider::{ChainSpecProvider, StateProviderFactory}; +use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; +use tokio::time::sleep; + +use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; +// 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: BaseHarness, + flashblocks: FlashblocksState, + provider: LocalNodeProvider, + user_to_address: HashMap, + user_to_private_key: HashMap, +} + +impl TestHarness { + async fn new() -> Self { + let node = + BaseHarness::new(default_launcher).await.expect("able to launch base harness"); + let provider = node.blockchain_provider(); + let flashblocks = FlashblocksState::new(provider.clone(), 5); + flashblocks.start(); + + let genesis_block = provider + .block(BlockHashOrNumber::Number(0)) + .expect("able to load block") + .expect("block exists") + .try_into_recovered() + .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 } + } + + fn decode_private_key(key: &str) -> B256 { + let bytes = hex::decode(key).expect("valid hex-encoded key"); + B256::from_slice(&bytes) + } + + 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 { + self.provider + .basic_account(&self.address(u)) + .expect("can lookup account state") + .expect("should be existing account state") + } + + fn canonical_balance(&self, u: User) -> U256 { + self.canonical_account(u).balance + } + + fn expected_pending_balance(&self, u: User, delta: u128) -> U256 { + self.canonical_balance(u) + U256::from(delta) + } + + fn account_state(&self, u: User) -> Account { + let basic_account = self.canonical_account(u); + + let nonce = self + .flashblocks + .get_pending_blocks() + .get_transaction_count(self.address(u)) + .to::(); + let balance = self + .flashblocks + .get_pending_blocks() + .get_balance(self.address(u)) + .unwrap_or(basic_account.balance); + + Account { + nonce: nonce + basic_account.nonce, + balance, + bytecode_hash: basic_account.bytecode_hash, + } + } + + fn build_transaction_to_send_eth( + &self, + from: User, + to: User, + amount: u128, + ) -> OpTransactionSigned { + let txn = TransactionBuilder::default() + .signer(self.signer(from)) + .chain_id(self.provider.chain_spec().chain_id()) + .to(self.address(to)) + .nonce(self.account_state(from).nonce) + .value(amount) + .gas_limit(21_000) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) + .into_eip1559() + .as_eip1559() + .unwrap() + .clone(); + + OpTransactionSigned::Eip1559(txn) + } + + fn build_transaction_to_send_eth_with_nonce( + &self, + from: User, + to: User, + amount: u128, + nonce: u64, + ) -> OpTransactionSigned { + let txn = TransactionBuilder::default() + .signer(self.signer(from)) + .chain_id(self.provider.chain_spec().chain_id()) + .to(self.address(to)) + .nonce(nonce) + .value(amount) + .gas_limit(21_000) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) + .into_eip1559() + .as_eip1559() + .unwrap() + .clone(); + + OpTransactionSigned::Eip1559(txn) + } + + async fn send_flashblock(&self, flashblock: Flashblock) { + self.flashblocks.on_flashblock_received(flashblock); + sleep(Duration::from_millis(SLEEP_TIME)).await; + } + + async fn new_canonical_block_without_processing( + &mut self, + user_transactions: Vec, + ) -> RecoveredBlock { + let previous_tip = + self.provider.best_block_number().expect("able to read best block number"); + let txs: Vec = + user_transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(); + self.node.build_block_from_transactions(txs).await.expect("able to build block"); + let target_block_number = previous_tip + 1; + + let block = self + .provider + .block(BlockHashOrNumber::Number(target_block_number)) + .expect("able to load block") + .expect("new canonical block should be available after building payload"); + + block.try_into_recovered().expect("able to recover newly built block") + } + + async fn new_canonical_block(&mut self, user_transactions: Vec) { + let block = self.new_canonical_block_without_processing(user_transactions).await; + self.flashblocks.on_canonical_block_received(&block); + sleep(Duration::from_millis(SLEEP_TIME)).await; + } +} + +struct FlashblockBuilder<'a> { + transactions: Vec, + receipts: HashMap, + harness: &'a TestHarness, + canonical_block_number: Option, + index: u64, +} + +impl<'a> FlashblockBuilder<'a> { + pub fn new_base(harness: &'a TestHarness) -> Self { + Self { + canonical_block_number: None, + transactions: vec![BLOCK_INFO_TXN.clone()], + receipts: { + let mut receipts = alloy_primitives::map::HashMap::default(); + receipts.insert( + BLOCK_INFO_TXN_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 10000, + logs: vec![], + }, + deposit_nonce: Some(4012991u64), + deposit_receipt_version: None, + }), + ); + receipts + }, + index: 0, + harness, + } + } + pub fn new(harness: &'a TestHarness, index: u64) -> Self { + Self { + canonical_block_number: None, + transactions: Vec::new(), + receipts: HashMap::default(), + harness, + index, + } + } + + pub fn with_receipts(&mut self, receipts: HashMap) -> &mut Self { + self.receipts = receipts; + self + } + + pub fn with_transactions(&mut self, transactions: Vec) -> &mut Self { + assert_ne!(self.index, 0, "Cannot set txns for initial flashblock"); + self.transactions.clear(); + + let mut cumulative_gas_used = 0; + for txn in transactions.iter() { + cumulative_gas_used = cumulative_gas_used + txn.gas_limit(); + self.transactions.push(txn.encoded_2718().into()); + self.receipts.insert( + txn.hash().clone(), + OpReceipt::Eip1559(Receipt { + status: true.into(), + cumulative_gas_used, + logs: vec![], + }), + ); + } + self + } + + pub fn with_canonical_block_number(&mut self, num: BlockNumber) -> &mut Self { + self.canonical_block_number = Some(num); + self + } + + pub fn build(&self) -> Flashblock { + let current_block = self.harness.node.latest_block(); + let canonical_block_num = + self.canonical_block_number.unwrap_or_else(|| current_block.number) + 1; + + let base = if self.index == 0 { + Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: current_block.hash(), + parent_hash: current_block.hash(), + fee_recipient: Address::random(), + prev_randao: B256::random(), + block_number: canonical_block_num, + gas_limit: current_block.gas_limit, + timestamp: current_block.timestamp + 2, + extra_data: Bytes::new(), + base_fee_per_gas: U256::from(100), + }) + } else { + None + }; + + Flashblock { + payload_id: PayloadId::default(), + index: self.index, + base, + diff: ExecutionPayloadFlashblockDeltaV1 { + state_root: B256::default(), + receipts_root: B256::default(), + block_hash: B256::default(), + gas_used: 0, + withdrawals: Vec::new(), + logs_bloom: Default::default(), + withdrawals_root: Default::default(), + transactions: self.transactions.clone(), + blob_gas_used: Default::default(), + }, + metadata: Metadata { + block_number: canonical_block_num, + receipts: self.receipts.clone(), + new_account_balances: HashMap::default(), + }, + } + } +} + +#[tokio::test] +async fn test_state_overrides_persisted_across_flashblocks() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("block is built") + .transactions + .len(), + 1 + ); + + assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); + assert!( + !test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .unwrap() + .contains_key(&test.address(User::Alice)) + ); + + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100_000, + )]) + .build(), + ) + .await; + + let pending = test.flashblocks.get_pending_blocks().get_block(true); + assert!(pending.is_some()); + let pending = pending.unwrap(); + assert_eq!(pending.transactions.len(), 2); + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .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.send_flashblock(FlashblockBuilder::new(&test, 2).build()).await; + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution in flashblock index 1"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .expect("should be set as txn receiver") + .balance + .expect("should be changed due to receiving funds"), + test.expected_pending_balance(User::Bob, 100_000) + ); +} + +#[tokio::test] +async fn test_state_overrides_persisted_across_blocks() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + let initial_base = FlashblockBuilder::new_base(&test).build(); + let initial_block_number = initial_base.metadata.block_number; + test.send_flashblock(initial_base).await; + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("block is built") + .transactions + .len(), + 1 + ); + + assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); + assert!( + !test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .unwrap() + .contains_key(&test.address(User::Alice)) + ); + + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100_000, + )]) + .build(), + ) + .await; + + let pending = test.flashblocks.get_pending_blocks().get_block(true); + assert!(pending.is_some()); + let pending = pending.unwrap(); + assert_eq!(pending.transactions.len(), 2); + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .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.send_flashblock( + FlashblockBuilder::new_base(&test) + .with_canonical_block_number(initial_block_number) + .build(), + ) + .await; + + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("block is built") + .transactions + .len(), + 1 + ); + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("block is built") + .header + .number, + initial_block_number + 1 + ); + + assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); + assert!( + test.flashblocks + .get_pending_blocks() + .get_state_overrides() + .unwrap() + .contains_key(&test.address(User::Alice)) + ); + + 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, + 100_000, + )]) + .build(), + ) + .await; + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .expect("should be set as txn receiver") + .balance + .expect("should be changed due to receiving funds"), + test.expected_pending_balance(User::Bob, 200_000) + ); +} + +#[tokio::test] +async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { + reth_tracing::init_test_tracing(); + let mut test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("block is built") + .transactions + .len(), + 1 + ); + assert!(test.flashblocks.get_pending_blocks().get_state_overrides().is_some()); + assert!( + !test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .unwrap() + .contains_key(&test.address(User::Alice)) + ); + + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100_000, + )]) + .build(), + ) + .await; + let pending = test.flashblocks.get_pending_blocks().get_block(true); + assert!(pending.is_some()); + let pending = pending.unwrap(); + assert_eq!(pending.transactions.len(), 2); + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .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.send_flashblock( + FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), + ) + .await; + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_canonical_block_number(1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100_000, + )]) + .build(), + ) + .await; + let pending = test.flashblocks.get_pending_blocks().get_block(true); + assert!(pending.is_some()); + let pending = pending.unwrap(); + assert_eq!(pending.transactions.len(), 2); + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .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.new_canonical_block(vec![test.build_transaction_to_send_eth_with_nonce( + User::Alice, + User::Bob, + 100, + 0, + )]) + .await; + + let pending = test.flashblocks.get_pending_blocks().get_block(true); + assert!(pending.is_some()); + let pending = pending.unwrap(); + assert_eq!(pending.transactions.len(), 2); + + let overrides = test + .flashblocks + .get_pending_blocks() + .get_state_overrides() + .expect("should be set from txn execution"); + + assert!(overrides.get(&test.address(User::Alice)).is_some()); + assert_eq!( + overrides + .get(&test.address(User::Bob)) + .expect("should be set as txn receiver") + .balance + .expect("should be changed due to receiving funds"), + test.expected_pending_balance(User::Bob, 100_000) + ); +} + +#[tokio::test] +async fn test_nonce_uses_pending_canon_block_instead_of_latest() { + // Test for race condition when a canon block comes in but user + // requests their nonce prior to the StateProcessor processing the canon block + // causing it to return an n+1 nonce instead of n + // because underlying reth node `latest` block is already updated, but + // relevant pending state has not been cleared yet + reth_tracing::init_test_tracing(); + let mut test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100, + )]) + .build(), + ) + .await; + + let pending_nonce = + test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce + + test + .flashblocks + .get_pending_blocks() + .get_transaction_count(test.address(User::Alice)) + .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), + ]) + .await; + + let pending_nonce = + test.provider.basic_account(&test.address(User::Alice)).unwrap().unwrap().nonce + + test + .flashblocks + .get_pending_blocks() + .get_transaction_count(test.address(User::Alice)) + .to::(); + + // This is 2, because canon block has reached the underlying chain + // but the StateProcessor hasn't processed it + // so pending nonce is effectively double-counting the same transaction, leading to a nonce of 2 + assert_eq!(pending_nonce, 2); + + // On the RPC level, we correctly return 1 because we + // use the pending canon block instead of the latest block when fetching + // onchain nonce count to compute + // pending_nonce = onchain_nonce + pending_txn_count + 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(); + let pending_nonce = canon_nonce + + test + .flashblocks + .get_pending_blocks() + .get_transaction_count(test.address(User::Alice)) + .to::(); + assert_eq!(pending_nonce, 1); +} + +#[tokio::test] +async fn test_missing_receipts_will_not_process() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + + let current_block = test.flashblocks.get_pending_blocks().get_block(true); + + test.send_flashblock( + FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100, + )]) + .with_receipts(HashMap::default()) // Clear the receipts + .build(), + ) + .await; + + let pending_block = test.flashblocks.get_pending_blocks().get_block(true); + + // When the flashblock is invalid, the chain doesn't progress + assert_eq!(pending_block.unwrap().hash(), current_block.unwrap().hash()); +} + +#[tokio::test] +async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + + let current_block = + test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); + + assert_eq!(current_block.header().number, 1); + assert_eq!(current_block.transactions.len(), 1); + + test.send_flashblock( + FlashblockBuilder::new(&test, 1).with_canonical_block_number(100).build(), + ) + .await; + + let current_block = test.flashblocks.get_pending_blocks().get_block(true); + assert!(current_block.is_none()); +} + +#[tokio::test] +async fn test_flashblock_for_new_canonical_block_works_if_sequential() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + + let current_block = + test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); + + assert_eq!(current_block.header().number, 1); + assert_eq!(current_block.transactions.len(), 1); + + test.send_flashblock( + FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), + ) + .await; + + let current_block = + test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); + + assert_eq!(current_block.header().number, 2); + assert_eq!(current_block.transactions.len(), 1); +} + +#[tokio::test] +async fn test_non_sequential_payload_clears_pending_state() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + + // Just the block info transaction + assert_eq!( + test.flashblocks + .get_pending_blocks() + .get_block(true) + .expect("should be set") + .transactions + .len(), + 1 + ); + + test.send_flashblock( + FlashblockBuilder::new(&test, 3) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100, + )]) + .build(), + ) + .await; + + assert_eq!(test.flashblocks.get_pending_blocks().is_none(), true); +} + +#[tokio::test] +async fn test_duplicate_flashblock_ignored() { + reth_tracing::init_test_tracing(); + let test = TestHarness::new().await; + + test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; + + let fb = FlashblockBuilder::new(&test, 1) + .with_transactions(vec![test.build_transaction_to_send_eth( + User::Alice, + User::Bob, + 100_000, + )]) + .build(); + + test.send_flashblock(fb.clone()).await; + let block = test.flashblocks.get_pending_blocks().get_block(true); + + test.send_flashblock(fb.clone()).await; + let block_two = test.flashblocks.get_pending_blocks().get_block(true); + + assert_eq!(block, block_two); +} + +#[tokio::test] +async fn test_progress_canonical_blocks_without_flashblocks() { + reth_tracing::init_test_tracing(); + let mut test = TestHarness::new().await; + + let genesis_block = test.node.latest_block(); + assert_eq!(genesis_block.number, 0); + 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; + + let block_one = test.node.latest_block(); + assert_eq!(block_one.number, 1); + assert_eq!(block_one.transaction_count(), 2); + 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), + ]) + .await; + + let block_two = test.node.latest_block(); + assert_eq!(block_two.number, 2); + assert_eq!(block_two.transaction_count(), 3); + assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); +} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index f7d93af7..521e8963 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration}; use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; -use alloy_primitives::{B256, Bytes, bytes}; +use alloy_primitives::{B256, B64, Bytes, bytes}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; @@ -14,7 +14,7 @@ use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; use reth::{ builder::NodeHandle, - providers::{BlockNumReader, BlockReader}, + providers::{BlockNumReader, BlockReader, ChainSpecProvider}, }; use reth_e2e_test_utils::Adapter; use reth_optimism_node::OpNode; @@ -95,6 +95,12 @@ impl TestHarness { latest_block.header.parent_beacon_block_root.unwrap_or(B256::ZERO); let next_timestamp = latest_block.header.timestamp + BLOCK_TIME_SECONDS; + let min_base_fee = latest_block.header.base_fee_per_gas.unwrap_or_default(); + let chain_spec = self.node.blockchain_provider().chain_spec(); + let base_fee_params = chain_spec.base_fee_params_at_timestamp(next_timestamp); + let eip_1559_params = ((base_fee_params.max_change_denominator as u64) << 32) + | (base_fee_params.elasticity_multiplier as u64); + let payload_attributes = OpPayloadAttributes { payload_attributes: PayloadAttributes { timestamp: next_timestamp, @@ -105,6 +111,8 @@ impl TestHarness { transactions: Some(transactions), gas_limit: Some(GAS_LIMIT), no_tx_pool: Some(true), + min_base_fee: Some(min_base_fee), + eip_1559_params: Some(B64::from(eip_1559_params)), ..Default::default() }; From 573bfe0566c87089afe1bddb7dc101ce0bfb47bd Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 20:14:05 -0600 Subject: [PATCH 26/38] Silence executor warnings in flashblocks tests --- Cargo.lock | 1 + Cargo.toml | 1 + crates/flashblocks-rpc/Cargo.toml | 1 + crates/flashblocks-rpc/tests/common/mod.rs | 24 +++++++++++++++++++ crates/flashblocks-rpc/tests/rpc.rs | 28 +++++++++++----------- crates/flashblocks-rpc/tests/state.rs | 20 ++++++++-------- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02358295..43a3f2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1573,6 +1573,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tracing", + "tracing-subscriber 0.3.20", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 57444056..6964da5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ op-alloy-consensus = { version = "0.22.0", default-features = false } # rollup-boost rollup-boost = { git = "http://github.com/flashbots/rollup-boost", rev = "v0.7.11" } rustls = "0.23.23" +tracing-subscriber = "0.3" # tokio tokio = { version = "1.44.2", features = ["full"] } diff --git a/crates/flashblocks-rpc/Cargo.toml b/crates/flashblocks-rpc/Cargo.toml index cb543821..d12a57eb 100644 --- a/crates/flashblocks-rpc/Cargo.toml +++ b/crates/flashblocks-rpc/Cargo.toml @@ -81,3 +81,4 @@ reth-testing-utils.workspace = true reth-db-common.workspace = true reth-e2e-test-utils.workspace = true once_cell.workspace = true +tracing-subscriber.workspace = true diff --git a/crates/flashblocks-rpc/tests/common/mod.rs b/crates/flashblocks-rpc/tests/common/mod.rs index 5ffd4a5b..7d2e982e 100644 --- a/crates/flashblocks-rpc/tests/common/mod.rs +++ b/crates/flashblocks-rpc/tests/common/mod.rs @@ -1,7 +1,31 @@ +use std::sync::Once; + use alloy_primitives::{B256, Bytes, b256, bytes}; +use tracing_subscriber::{filter::LevelFilter, EnvFilter}; pub const BLOCK_INFO_TXN: Bytes = bytes!( "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" ); pub const BLOCK_INFO_TXN_HASH: B256 = b256!("0xba56c8b0deb460ff070f8fca8e2ee01e51a3db27841cc862fdd94cc1a47662b6"); + +static TRACING: Once = Once::new(); + +pub fn init_tracing() { + TRACING.call_once(|| { + let mut filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + for directive in ["reth_tasks=off", "reth_node_builder::launch::common=off"] { + if let Ok(directive) = directive.parse() { + filter = filter.add_directive(directive); + } + } + + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); +} diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 7f53a034..7688b7ab 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -261,7 +261,7 @@ fn create_second_payload() -> Flashblock { #[tokio::test] async fn test_get_pending_block() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -309,7 +309,7 @@ async fn test_get_pending_block() -> Result<()> { #[tokio::test] async fn test_get_balance_pending() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -325,7 +325,7 @@ async fn test_get_balance_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_by_hash_pending() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -347,7 +347,7 @@ async fn test_get_transaction_by_hash_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_receipt_pending() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -369,7 +369,7 @@ async fn test_get_transaction_receipt_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_count() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -386,7 +386,7 @@ async fn test_get_transaction_count() -> Result<()> { #[tokio::test] async fn test_eth_call() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -458,7 +458,7 @@ async fn test_eth_call() -> Result<()> { #[tokio::test] async fn test_eth_estimate_gas() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -502,7 +502,7 @@ async fn test_eth_estimate_gas() -> Result<()> { #[tokio::test] async fn test_eth_simulate_v1() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -565,7 +565,7 @@ async fn test_eth_simulate_v1() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; setup.send_flashblock(create_first_payload()).await?; @@ -586,7 +586,7 @@ async fn test_send_raw_transaction_sync() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync_timeout() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await.unwrap(); // fail request immediately by passing a timeout of 0 ms @@ -600,7 +600,7 @@ async fn test_send_raw_transaction_sync_timeout() { #[tokio::test] async fn test_get_logs_pending() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -643,7 +643,7 @@ async fn test_get_logs_pending() -> Result<()> { #[tokio::test] async fn test_get_logs_filter_by_address() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -684,7 +684,7 @@ async fn test_get_logs_filter_by_address() -> Result<()> { #[tokio::test] async fn test_get_logs_topic_filtering() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); @@ -720,7 +720,7 @@ async fn test_get_logs_topic_filtering() -> Result<()> { #[tokio::test] async fn test_get_logs_mixed_block_ranges() -> Result<()> { - reth_tracing::init_test_tracing(); + common::init_tracing(); let setup = TestSetup::new().await?; let provider = setup.harness.provider(); diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index b2815914..cc1f3834 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -333,7 +333,7 @@ impl<'a> FlashblockBuilder<'a> { #[tokio::test] async fn test_state_overrides_persisted_across_flashblocks() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -410,7 +410,7 @@ async fn test_state_overrides_persisted_across_flashblocks() { #[tokio::test] async fn test_state_overrides_persisted_across_blocks() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; let initial_base = FlashblockBuilder::new_base(&test).build(); @@ -534,7 +534,7 @@ async fn test_state_overrides_persisted_across_blocks() { #[tokio::test] async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -661,7 +661,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { // causing it to return an n+1 nonce instead of n // because underlying reth node `latest` block is already updated, but // relevant pending state has not been cleared yet - reth_tracing::init_test_tracing(); + common::init_tracing(); let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -722,7 +722,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { #[tokio::test] async fn test_missing_receipts_will_not_process() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -749,7 +749,7 @@ async fn test_missing_receipts_will_not_process() { #[tokio::test] async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -771,7 +771,7 @@ async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non #[tokio::test] async fn test_flashblock_for_new_canonical_block_works_if_sequential() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -796,7 +796,7 @@ async fn test_flashblock_for_new_canonical_block_works_if_sequential() { #[tokio::test] async fn test_non_sequential_payload_clears_pending_state() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -830,7 +830,7 @@ async fn test_non_sequential_payload_clears_pending_state() { #[tokio::test] async fn test_duplicate_flashblock_ignored() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -854,7 +854,7 @@ async fn test_duplicate_flashblock_ignored() { #[tokio::test] async fn test_progress_canonical_blocks_without_flashblocks() { - reth_tracing::init_test_tracing(); + common::init_tracing(); let mut test = TestHarness::new().await; let genesis_block = test.node.latest_block(); From aa647d0728c14e3fcc3aca35c76383313c02ddf6 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 20:21:37 -0600 Subject: [PATCH 27/38] Add shared test tracing helper --- Cargo.lock | 2 + crates/flashblocks-rpc/tests/common/mod.rs | 24 ------------ crates/flashblocks-rpc/tests/rpc.rs | 45 ++++++++-------------- crates/flashblocks-rpc/tests/state.rs | 32 ++++++--------- crates/metering/Cargo.toml | 2 +- crates/metering/src/tests/rpc.rs | 11 +----- crates/test-utils/Cargo.toml | 1 + crates/test-utils/README.md | 9 +++-- crates/test-utils/src/harness.rs | 3 +- crates/test-utils/src/lib.rs | 1 + crates/test-utils/src/tracing.rs | 27 +++++++++++++ 11 files changed, 70 insertions(+), 87 deletions(-) create mode 100644 crates/test-utils/src/tracing.rs diff --git a/Cargo.lock b/Cargo.lock index 43a3f2f4..fc818c79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,7 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-rpc-client", + "base-reth-test-utils", "eyre", "jsonrpsee 0.26.0", "op-alloy-consensus", @@ -1726,6 +1727,7 @@ dependencies = [ "tokio-util", "tower 0.5.2", "tracing", + "tracing-subscriber 0.3.20", "url", ] diff --git a/crates/flashblocks-rpc/tests/common/mod.rs b/crates/flashblocks-rpc/tests/common/mod.rs index 7d2e982e..5ffd4a5b 100644 --- a/crates/flashblocks-rpc/tests/common/mod.rs +++ b/crates/flashblocks-rpc/tests/common/mod.rs @@ -1,31 +1,7 @@ -use std::sync::Once; - use alloy_primitives::{B256, Bytes, b256, bytes}; -use tracing_subscriber::{filter::LevelFilter, EnvFilter}; pub const BLOCK_INFO_TXN: Bytes = bytes!( "0x7ef90104a06c0c775b6b492bab9d7e81abdf27f77cafb698551226455a82f559e0f93fea3794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000008dd00101c1200000000000000020000000068869d6300000000015f277f000000000000000000000000000000000000000000000000000000000d42ac290000000000000000000000000000000000000000000000000000000000000001abf52777e63959936b1bf633a2a643f0da38d63deffe49452fed1bf8a44975d50000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9000000000000000000000000" ); pub const BLOCK_INFO_TXN_HASH: B256 = b256!("0xba56c8b0deb460ff070f8fca8e2ee01e51a3db27841cc862fdd94cc1a47662b6"); - -static TRACING: Once = Once::new(); - -pub fn init_tracing() { - TRACING.call_once(|| { - let mut filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); - - for directive in ["reth_tasks=off", "reth_node_builder::launch::common=off"] { - if let Ok(directive) = directive.parse() { - filter = filter.add_directive(directive); - } - } - - let _ = tracing_subscriber::fmt() - .with_env_filter(filter) - .with_test_writer() - .try_init(); - }); -} diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 7688b7ab..005e463a 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -13,7 +13,7 @@ use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; -use base_reth_test_utils::{harness::TestHarness, node::default_launcher}; +use base_reth_test_utils::{harness::TestHarness, node::default_launcher, tracing::init_silenced_tracing}; use eyre::Result; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; @@ -30,6 +30,7 @@ struct TestSetup { impl TestSetup { async fn new() -> Result { + init_silenced_tracing(); let harness = TestHarness::new(default_launcher).await?; Ok(Self { harness }) } @@ -261,8 +262,7 @@ fn create_second_payload() -> Flashblock { #[tokio::test] async fn test_get_pending_block() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); let latest_block = provider @@ -309,8 +309,7 @@ async fn test_get_pending_block() -> Result<()> { #[tokio::test] async fn test_get_balance_pending() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -325,8 +324,7 @@ async fn test_get_balance_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_by_hash_pending() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); @@ -347,8 +345,7 @@ async fn test_get_transaction_by_hash_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_receipt_pending() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; @@ -369,8 +366,7 @@ async fn test_get_transaction_receipt_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_count() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); @@ -386,8 +382,7 @@ async fn test_get_transaction_count() -> Result<()> { #[tokio::test] async fn test_eth_call() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // We ensure that eth_call will succeed because we are on plain state @@ -458,8 +453,7 @@ async fn test_eth_call() -> Result<()> { #[tokio::test] async fn test_eth_estimate_gas() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // We ensure that eth_estimate_gas will succeed because we are on plain state @@ -502,8 +496,7 @@ async fn test_eth_estimate_gas() -> Result<()> { #[tokio::test] async fn test_eth_simulate_v1() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -565,8 +558,7 @@ async fn test_eth_simulate_v1() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; setup.send_flashblock(create_first_payload()).await?; @@ -586,8 +578,7 @@ async fn test_send_raw_transaction_sync() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync_timeout() { - common::init_tracing(); - let setup = TestSetup::new().await.unwrap(); + let setup = TestSetup::new().await.unwrap(); // fail request immediately by passing a timeout of 0 ms let receipt_result = setup.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; @@ -600,8 +591,7 @@ async fn test_send_raw_transaction_sync_timeout() { #[tokio::test] async fn test_get_logs_pending() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // Test no logs when no flashblocks sent @@ -643,8 +633,7 @@ async fn test_get_logs_pending() -> Result<()> { #[tokio::test] async fn test_get_logs_filter_by_address() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -684,8 +673,7 @@ async fn test_get_logs_filter_by_address() -> Result<()> { #[tokio::test] async fn test_get_logs_topic_filtering() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -720,8 +708,7 @@ async fn test_get_logs_topic_filtering() -> Result<()> { #[tokio::test] async fn test_get_logs_mixed_block_ranges() -> Result<()> { - common::init_tracing(); - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index cc1f3834..44adda27 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -15,6 +15,7 @@ use base_reth_test_utils::{ accounts::TestAccounts, harness::TestHarness as BaseHarness, node::{LocalNodeProvider, default_launcher}, + tracing::init_silenced_tracing, }; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; @@ -51,6 +52,7 @@ struct TestHarness { impl TestHarness { async fn new() -> Self { + init_silenced_tracing(); let node = BaseHarness::new(default_launcher).await.expect("able to launch base harness"); let provider = node.blockchain_provider(); @@ -333,8 +335,7 @@ impl<'a> FlashblockBuilder<'a> { #[tokio::test] async fn test_state_overrides_persisted_across_flashblocks() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -410,8 +411,7 @@ async fn test_state_overrides_persisted_across_flashblocks() { #[tokio::test] async fn test_state_overrides_persisted_across_blocks() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; let initial_base = FlashblockBuilder::new_base(&test).build(); let initial_block_number = initial_base.metadata.block_number; @@ -534,8 +534,7 @@ async fn test_state_overrides_persisted_across_blocks() { #[tokio::test] async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { - common::init_tracing(); - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -661,8 +660,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { // causing it to return an n+1 nonce instead of n // because underlying reth node `latest` block is already updated, but // relevant pending state has not been cleared yet - common::init_tracing(); - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; test.send_flashblock( @@ -722,8 +720,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { #[tokio::test] async fn test_missing_receipts_will_not_process() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -749,8 +746,7 @@ async fn test_missing_receipts_will_not_process() { #[tokio::test] async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -771,8 +767,7 @@ async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non #[tokio::test] async fn test_flashblock_for_new_canonical_block_works_if_sequential() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -796,8 +791,7 @@ async fn test_flashblock_for_new_canonical_block_works_if_sequential() { #[tokio::test] async fn test_non_sequential_payload_clears_pending_state() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -830,8 +824,7 @@ async fn test_non_sequential_payload_clears_pending_state() { #[tokio::test] async fn test_duplicate_flashblock_ignored() { - common::init_tracing(); - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -854,8 +847,7 @@ async fn test_duplicate_flashblock_ignored() { #[tokio::test] async fn test_progress_canonical_blocks_without_flashblocks() { - common::init_tracing(); - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; let genesis_block = test.node.latest_block(); assert_eq!(genesis_block.number, 0); diff --git a/crates/metering/Cargo.toml b/crates/metering/Cargo.toml index 42341f6c..27e9590e 100644 --- a/crates/metering/Cargo.toml +++ b/crates/metering/Cargo.toml @@ -59,5 +59,5 @@ reth-tracing.workspace = true reth-transaction-pool = { workspace = true, features = ["test-utils"] } serde_json.workspace = true tokio.workspace = true - +base-reth-test-utils.workspace = true diff --git a/crates/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index b11002fc..c4a04fc7 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -5,6 +5,7 @@ mod tests { use alloy_eips::Encodable2718; use alloy_genesis::Genesis; use alloy_primitives::{Bytes, U256, address, b256, bytes}; + use base_reth_test_utils::tracing::init_silenced_tracing; use alloy_rpc_client::RpcClient; use op_alloy_consensus::OpTxEnvelope; use reth::{ @@ -54,6 +55,7 @@ mod tests { } async fn setup_node() -> eyre::Result { + init_silenced_tracing(); let tasks = TaskManager::current(); let exec = tasks.executor(); const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; @@ -106,7 +108,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_empty() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -125,7 +126,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_single_transaction() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -176,7 +176,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_multiple_transactions() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -249,7 +248,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_invalid_transaction() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -269,7 +267,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_uses_latest_block() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -287,7 +284,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_ignores_bundle_block_number() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -313,7 +309,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_custom_timestamp() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -335,7 +330,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_arbitrary_block_number() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; @@ -354,7 +348,6 @@ mod tests { #[tokio::test] async fn test_meter_bundle_gas_calculations() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); let node = setup_node().await?; let client = node.rpc_client().await?; diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 1d163a98..59e35f4c 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -69,6 +69,7 @@ jsonrpsee.workspace = true # misc tracing.workspace = true +tracing-subscriber.workspace = true serde.workspace = true serde_json.workspace = true eyre.workspace = true diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 92899153..97f0449a 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -15,8 +15,11 @@ This crate provides reusable testing utilities for integration tests across the ## Quick Start ```rust -use base_reth_test_utils::harness::TestHarness; -use base_reth_test_utils::node::default_launcher; +use base_reth_test_utils::{ + harness::TestHarness, + node::default_launcher, + tracing::init_silenced_tracing, +}; #[tokio::test] async fn test_example() -> eyre::Result<()> { @@ -278,7 +281,7 @@ use base_reth_test_utils::node::default_launcher; #[tokio::test] async fn my_test() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); + init_silenced_tracing(); let harness = TestHarness::new(default_launcher).await?; // Your test logic diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 521e8963..feacf68e 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -26,6 +26,7 @@ use crate::{ accounts::TestAccounts, engine::{EngineApi, IpcEngine}, node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, + tracing::init_silenced_tracing, }; const BLOCK_TIME_SECONDS: u64 = 2; @@ -49,6 +50,7 @@ impl TestHarness { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { + init_silenced_tracing(); let node = LocalNode::new(launcher).await?; let engine = node.engine_api()?; let accounts = TestAccounts::new(); @@ -200,7 +202,6 @@ mod tests { #[tokio::test] async fn test_harness_setup() -> Result<()> { - reth_tracing::init_test_tracing(); let harness = TestHarness::new(default_launcher).await?; assert_eq!(harness.accounts().alice.name, "Alice"); diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 62913e26..6a9f56a3 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -2,3 +2,4 @@ pub mod accounts; pub mod engine; pub mod harness; pub mod node; +pub mod tracing; diff --git a/crates/test-utils/src/tracing.rs b/crates/test-utils/src/tracing.rs new file mode 100644 index 00000000..30baad0d --- /dev/null +++ b/crates/test-utils/src/tracing.rs @@ -0,0 +1,27 @@ +use std::sync::Once; + +use tracing_subscriber::{filter::LevelFilter, EnvFilter}; + +static INIT: Once = Once::new(); + +/// Initializes tracing for integration tests while silencing the noisy executor warnings +/// (`reth_tasks` and `reth_node_builder::launch::common`) that appear whenever multiple nodes +/// reuse the global rayon/Tokio pools in a single process. +/// +/// Tests call this helper before booting a harness; repeated calls are cheap and only the first one +/// installs the subscriber. +pub fn init_silenced_tracing() { + INIT.call_once(|| { + let mut filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + for directive in ["reth_tasks=off", "reth_node_builder::launch::common=off"].into_iter() { + if let Ok(directive) = directive.parse() { + filter = filter.add_directive(directive); + } + } + + let _ = tracing_subscriber::fmt().with_env_filter(filter).with_test_writer().try_init(); + }); +} From 61cf4f8677fbadbf96813629bd41871abeda2128 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 20:23:14 -0600 Subject: [PATCH 28/38] Document flashblocks integration tests --- crates/flashblocks-rpc/tests/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 crates/flashblocks-rpc/tests/README.md diff --git a/crates/flashblocks-rpc/tests/README.md b/crates/flashblocks-rpc/tests/README.md new file mode 100644 index 00000000..d7152a24 --- /dev/null +++ b/crates/flashblocks-rpc/tests/README.md @@ -0,0 +1,14 @@ +## Flashblocks RPC Integration Tests + +The suites under this directory exercise `base-reth-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: + +1. **Single compilation unit:** both the tests and `base_reth_test_utils` depend on the standard + library build, so types like `Flashblock` stay aligned (no duplicate definitions across lib/test + targets and no `E0308` mismatches on channels). +2. **Public API coverage:** integration tests only touch exported items, which makes it easy to + detect when a public surface change would break downstream users. + +If you are adding new flashblocks tests that need access to private internals, keep those as unit +tests under `src/`. All cases that can be expressed via the public API should live here. From 34b5f0cfb7a94fb33c3b7e14ed63ae20d3b5f9d7 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 20:39:32 -0600 Subject: [PATCH 29/38] Default TestHarness launcher --- crates/flashblocks-rpc/tests/rpc.rs | 5 ++--- crates/flashblocks-rpc/tests/state.rs | 11 ++--------- crates/test-utils/README.md | 24 ++++++++---------------- crates/test-utils/src/harness.rs | 12 +++++++----- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 005e463a..140f6240 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -13,7 +13,7 @@ use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; -use base_reth_test_utils::{harness::TestHarness, node::default_launcher, tracing::init_silenced_tracing}; +use base_reth_test_utils::harness::TestHarness; use eyre::Result; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; @@ -30,8 +30,7 @@ struct TestSetup { impl TestSetup { async fn new() -> Result { - init_silenced_tracing(); - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; Ok(Self { harness }) } diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index 44adda27..c8320765 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -11,12 +11,7 @@ use base_reth_flashblocks_rpc::{ state::FlashblocksState, subscription::{Flashblock, FlashblocksReceiver, Metadata}, }; -use base_reth_test_utils::{ - accounts::TestAccounts, - harness::TestHarness as BaseHarness, - node::{LocalNodeProvider, default_launcher}, - tracing::init_silenced_tracing, -}; +use base_reth_test_utils::{accounts::TestAccounts, harness::TestHarness as BaseHarness, node::LocalNodeProvider}; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; use reth::{ @@ -52,9 +47,7 @@ struct TestHarness { impl TestHarness { async fn new() -> Self { - init_silenced_tracing(); - let node = - BaseHarness::new(default_launcher).await.expect("able to launch base harness"); + let node = BaseHarness::new().await.expect("able to launch base harness"); let provider = node.blockchain_provider(); let flashblocks = FlashblocksState::new(provider.clone(), 5); flashblocks.start(); diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 97f0449a..921267b4 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -15,15 +15,11 @@ This crate provides reusable testing utilities for integration tests across the ## Quick Start ```rust -use base_reth_test_utils::{ - harness::TestHarness, - node::default_launcher, - tracing::init_silenced_tracing, -}; +use base_reth_test_utils::harness::TestHarness; #[tokio::test] async fn test_example() -> eyre::Result<()> { - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; // Advance the chain harness.advance_chain(5).await?; @@ -73,12 +69,11 @@ The main entry point for integration tests. Combines node, engine, and accounts ```rust use base_reth_test_utils::harness::TestHarness; -use base_reth_test_utils::node::default_launcher; use alloy_primitives::Bytes; #[tokio::test] async fn test_harness() -> eyre::Result<()> { - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; // Access provider let provider = harness.provider(); @@ -185,7 +180,7 @@ Hardcoded test accounts with deterministic addresses (Anvil-compatible). ```rust use base_reth_test_utils::accounts::TestAccounts; -use base_reth_test_utils::{harness::TestHarness, node::default_launcher}; +use base_reth_test_utils::harness::TestHarness; let accounts = TestAccounts::new(); @@ -195,7 +190,7 @@ let charlie = &accounts.charlie; let deployer = &accounts.deployer; // Access via harness -let harness = TestHarness::new(default_launcher).await?; +let harness = TestHarness::new().await?; let alice = &harness.accounts().alice; ``` @@ -220,7 +215,7 @@ use base_reth_flashblocks_rpc::subscription::Flashblock; #[tokio::test] async fn test_flashblocks() -> eyre::Result<()> { - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; // Construct a Flashblock manually // Use base_reth_flashblocks_rpc imports to build the struct @@ -234,7 +229,7 @@ async fn test_flashblocks() -> eyre::Result<()> { **Via TestHarness:** ```rust -let harness = TestHarness::new(default_launcher).await?; +let harness = TestHarness::new().await?; harness.send_flashblock(flashblock).await?; ``` @@ -277,13 +272,10 @@ Import in tests: ```rust use base_reth_test_utils::harness::TestHarness; -use base_reth_test_utils::node::default_launcher; #[tokio::test] async fn my_test() -> eyre::Result<()> { - init_silenced_tracing(); - - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; // Your test logic Ok(()) diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index feacf68e..bd97f55e 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -25,7 +25,7 @@ use tokio::time::sleep; use crate::{ accounts::TestAccounts, engine::{EngineApi, IpcEngine}, - node::{LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, + node::{default_launcher, LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, tracing::init_silenced_tracing, }; @@ -45,7 +45,11 @@ pub struct TestHarness { } impl TestHarness { - pub async fn new(launcher: L) -> Result + pub async fn new() -> Result { + Self::with_launcher(default_launcher).await + } + + pub async fn with_launcher(launcher: L) -> Result where L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, @@ -198,11 +202,9 @@ mod tests { use alloy_provider::Provider; use super::*; - use crate::node::default_launcher; - #[tokio::test] async fn test_harness_setup() -> Result<()> { - let harness = TestHarness::new(default_launcher).await?; + let harness = TestHarness::new().await?; assert_eq!(harness.accounts().alice.name, "Alice"); assert_eq!(harness.accounts().bob.name, "Bob"); From 4794c97418dad96f6e6120681d12fa72ecbeb55b Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 21:39:56 -0600 Subject: [PATCH 30/38] Split flashblocks harness --- crates/flashblocks-rpc/tests/rpc.rs | 6 +- crates/test-utils/README.md | 22 +++- crates/test-utils/src/harness.rs | 80 ++++++++++---- crates/test-utils/src/node.rs | 157 +++++++++++++++++++--------- 4 files changed, 194 insertions(+), 71 deletions(-) diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 140f6240..35bec77a 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -13,7 +13,7 @@ use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; -use base_reth_test_utils::harness::TestHarness; +use base_reth_test_utils::harness::FlashblocksHarness; use eyre::Result; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; @@ -25,12 +25,12 @@ use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; struct TestSetup { - harness: TestHarness, + harness: FlashblocksHarness, } impl TestSetup { async fn new() -> Result { - let harness = TestHarness::new().await?; + let harness = FlashblocksHarness::new().await?; Ok(Self { harness }) } diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 921267b4..157637e6 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -65,7 +65,7 @@ The framework follows a three-layer architecture: ### 1. TestHarness -The main entry point for integration tests. Combines node, engine, and accounts into a single interface. +The main entry point for integration tests that only need canonical chain control. Combines node, engine, and accounts into a single interface. ```rust use base_reth_test_utils::harness::TestHarness; @@ -208,6 +208,26 @@ Each account includes: ### 5. Flashblocks Support +Use `FlashblocksHarness` when you need `send_flashblock` and access to the in-memory pending state. + +```rust +use base_reth_test_utils::harness::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. Flashblocks can be manually constructed and sent via the harness. ```rust diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index bd97f55e..c5094aa1 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -1,6 +1,6 @@ -//! Unified test harness combining node, engine API, and flashblocks functionality +//! Unified test harness combining node and engine helpers, plus an optional flashblocks adapter. -use std::{sync::Arc, time::Duration}; +use std::{ops::Deref, sync::Arc, time::Duration}; use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; use alloy_primitives::{B256, B64, Bytes, bytes}; @@ -56,6 +56,10 @@ impl TestHarness { { init_silenced_tracing(); let node = LocalNode::new(launcher).await?; + Self::from_node(node).await + } + + async fn from_node(node: LocalNode) -> Result { let engine = node.engine_api()?; let accounts = TestAccounts::new(); @@ -76,10 +80,6 @@ impl TestHarness { self.node.blockchain_provider() } - pub fn flashblocks_state(&self) -> Arc { - self.node.flashblocks_state() - } - pub fn rpc_url(&self) -> String { format!("http://{}", self.node.http_api_addr) } @@ -164,20 +164,6 @@ impl TestHarness { Ok(()) } - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.node.send_flashblock(flashblock).await - } - - pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> - where - I: IntoIterator, - { - for flashblock in flashblocks { - self.send_flashblock(flashblock).await?; - } - Ok(()) - } - pub async fn advance_chain(&self, n: u64) -> Result<()> { for _ in 0..n { self.build_block_from_transactions(vec![]).await?; @@ -196,6 +182,60 @@ impl TestHarness { } } +pub struct FlashblocksHarness { + inner: TestHarness, +} + +impl FlashblocksHarness { + pub async fn new() -> Result { + Self::with_launcher(default_launcher).await + } + + pub async fn with_launcher(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + init_silenced_tracing(); + let node = LocalNode::new_flashblocks(launcher).await?; + let inner = TestHarness::from_node(node).await?; + Ok(Self { inner }) + } + + pub fn flashblocks_state(&self) -> Arc { + self.inner + .node + .flashblocks_state() + .expect("flashblocks harness must have flashblocks state") + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.inner.node.send_flashblock(flashblock).await + } + + pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> + where + I: IntoIterator, + { + for flashblock in flashblocks { + self.send_flashblock(flashblock).await?; + } + Ok(()) + } + + pub fn into_inner(self) -> TestHarness { + self.inner + } +} + +impl Deref for FlashblocksHarness { + type Target = TestHarness; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + #[cfg(test)] mod tests { use alloy_primitives::U256; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 48464a6c..51587b33 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -42,6 +42,12 @@ use tokio_stream::StreamExt; use crate::engine::EngineApi; +#[derive(Clone, Copy)] +enum LocalNodeKind { + Base, + Flashblocks, +} + pub const BASE_CHAIN_ID: u64 = 84532; pub type LocalNodeProvider = BlockchainProvider>; @@ -50,12 +56,22 @@ pub type LocalFlashblocksState = FlashblocksState; pub struct LocalNode { pub(crate) http_api_addr: SocketAddr, engine_ipc_path: String, - flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - flashblocks_state: Arc, provider: LocalNodeProvider, _node_exit_future: NodeExitFuture, _node: Box, _task_manager: TaskManager, + flashblocks: Option, +} + +#[derive(Clone)] +pub struct FlashblocksParts { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + state: Arc, +} + +struct FlashblocksSetup { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + fb_cell: Arc>>>, } pub type OpTypes = @@ -74,6 +90,30 @@ pub async fn default_launcher( impl LocalNode { pub async fn new(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + let (node, _) = Self::build_with_kind(launcher, LocalNodeKind::Base).await?; + Ok(node) + } + + pub async fn new_flashblocks( + launcher: L, + ) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + let (node, flashblocks) = Self::build_with_kind(launcher, LocalNodeKind::Flashblocks).await?; + flashblocks.expect("flashblocks parts initialized"); + Ok(node) + } + + async fn build_with_kind( + launcher: L, + kind: LocalNodeKind, + ) -> Result<(Self, Option)> where L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, @@ -115,22 +155,28 @@ impl LocalNode { node_config = node_config .with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); - let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - let provider_cell: Arc> = Arc::new(OnceCell::new()); + let mut 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 } = - 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 mut flashblocks_setup: Option = None; + + if matches!(kind, LocalNodeKind::Flashblocks) { + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let provider_cell: Arc> = Arc::new(OnceCell::new()); + let mut receiver = Some(receiver); + + builder = builder .install_exex("flashblocks-canon", { let fb_cell = fb_cell.clone(); let provider_cell = provider_cell.clone(); move |mut ctx| async move { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let provider = + provider_cell.get_or_init(|| ctx.provider().clone()).clone(); let fb = init_flashblocks_state(&fb_cell, &provider); Ok(async move { while let Some(note) = ctx.notifications.try_next().await? { @@ -150,9 +196,9 @@ impl LocalNode { .extend_rpc_modules({ let fb_cell = fb_cell.clone(); let provider_cell = provider_cell.clone(); - let mut receiver = Some(receiver); move |ctx| { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let provider = + provider_cell.get_or_init(|| ctx.provider().clone()).clone(); let fb = init_flashblocks_state(&fb_cell, &provider); let provider_for_task = provider.clone(); @@ -173,7 +219,7 @@ impl LocalNode { fb.clone(), ); ctx.modules.replace_configured(api_ext.into_rpc())?; - // Spawn task to receive flashblocks from the test context + let fb_for_task = fb.clone(); let mut receiver = receiver .take() @@ -184,11 +230,16 @@ impl LocalNode { let _ = tx.send(()); } }); + Ok(()) } - }) - .launch_with_fn(launcher) - .await?; + }); + + flashblocks_setup = Some(FlashblocksSetup { sender, fb_cell }); + } + + let NodeHandle { node: node_handle, node_exit_future } = + builder.launch_with_fn(launcher).await?; let http_api_addr = node_handle .rpc_server_handle() @@ -196,23 +247,30 @@ impl LocalNode { .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; let engine_ipc_path = node_config.rpc.auth_ipc_path; - let flashblocks_state = fb_cell - .get() - .expect("FlashblocksState should be initialized during node launch") - .clone(); - let provider = - provider_cell.get().expect("Provider should be initialized during node launch").clone(); - - Ok(Self { - http_api_addr, - engine_ipc_path, - flashblock_sender: sender, - flashblocks_state, - provider, - _node_exit_future: node_exit_future, - _node: Box::new(node_handle), - _task_manager: tasks, - }) + let provider = node_handle.provider().clone(); + + let flashblocks_parts = flashblocks_setup.map(|setup| FlashblocksParts { + sender: setup.sender, + state: setup + .fb_cell + .get() + .expect("FlashblocksState should be initialized during node launch") + .clone(), + }); + let flashblocks_clone = flashblocks_parts.clone(); + + Ok(( + Self { + http_api_addr, + engine_ipc_path, + provider, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + flashblocks: flashblocks_clone, + }, + flashblocks_parts, + )) } /// Creates a test database with a smaller map size to reduce memory usage. @@ -241,13 +299,6 @@ impl LocalNode { Ok(Arc::new(TempDatabase::new(db, path))) } - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.flashblock_sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; - rx.await.map_err(|err| eyre::eyre!(err))?; - Ok(()) - } - pub fn provider(&self) -> Result> { let url = format!("http://{}", self.http_api_addr); let client = RpcClient::builder().http(url.parse()?); @@ -258,13 +309,25 @@ impl LocalNode { EngineApi::::new(self.engine_ipc_path.clone()) } - pub fn flashblocks_state(&self) -> Arc { - self.flashblocks_state.clone() - } - pub fn blockchain_provider(&self) -> LocalNodeProvider { self.provider.clone() } + + pub(crate) fn flashblocks_state(&self) -> Option> { + self.flashblocks.as_ref().map(|parts| parts.state.clone()) + } + + pub(crate) async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let parts = self + .flashblocks + .as_ref() + .ok_or_else(|| eyre::eyre!("flashblocks not configured for this node"))?; + + let (tx, rx) = oneshot::channel(); + parts.sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; + rx.await.map_err(|err| eyre::eyre!(err))?; + Ok(()) + } } fn init_flashblocks_state( From c8992a3036f69a378122e71305846057cec8b8f7 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 21:56:14 -0600 Subject: [PATCH 31/38] Introduce FlashblocksLocalNode --- crates/test-utils/src/harness.rs | 15 +++--- crates/test-utils/src/node.rs | 80 ++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index c5094aa1..34cb80a5 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -25,7 +25,7 @@ use tokio::time::sleep; use crate::{ accounts::TestAccounts, engine::{EngineApi, IpcEngine}, - node::{default_launcher, LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, + node::{default_launcher, FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, tracing::init_silenced_tracing, }; @@ -184,6 +184,7 @@ impl TestHarness { pub struct FlashblocksHarness { inner: TestHarness, + parts: FlashblocksParts, } impl FlashblocksHarness { @@ -197,20 +198,18 @@ impl FlashblocksHarness { LRet: Future, OpAddOns>>>, { init_silenced_tracing(); - let node = LocalNode::new_flashblocks(launcher).await?; + let flash_node = FlashblocksLocalNode::with_launcher(launcher).await?; + let (node, parts) = flash_node.into_parts(); let inner = TestHarness::from_node(node).await?; - Ok(Self { inner }) + Ok(Self { inner, parts }) } pub fn flashblocks_state(&self) -> Arc { - self.inner - .node - .flashblocks_state() - .expect("flashblocks harness must have flashblocks state") + self.parts.state() } pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.inner.node.send_flashblock(flashblock).await + self.parts.send(flashblock).await } pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 51587b33..4294f679 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -60,7 +60,6 @@ pub struct LocalNode { _node_exit_future: NodeExitFuture, _node: Box, _task_manager: TaskManager, - flashblocks: Option, } #[derive(Clone)] @@ -69,6 +68,19 @@ pub struct FlashblocksParts { state: Arc, } +impl FlashblocksParts { + pub fn state(&self) -> Arc { + self.state.clone() + } + + 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(()) + } +} + struct FlashblocksSetup { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, fb_cell: Arc>>>, @@ -98,18 +110,6 @@ impl LocalNode { Ok(node) } - pub async fn new_flashblocks( - launcher: L, - ) -> Result - where - L: FnOnce(OpBuilder) -> LRet, - LRet: Future, OpAddOns>>>, - { - let (node, flashblocks) = Self::build_with_kind(launcher, LocalNodeKind::Flashblocks).await?; - flashblocks.expect("flashblocks parts initialized"); - Ok(node) - } - async fn build_with_kind( launcher: L, kind: LocalNodeKind, @@ -257,7 +257,6 @@ impl LocalNode { .expect("FlashblocksState should be initialized during node launch") .clone(), }); - let flashblocks_clone = flashblocks_parts.clone(); Ok(( Self { @@ -267,7 +266,6 @@ impl LocalNode { _node_exit_future: node_exit_future, _node: Box::new(node_handle), _task_manager: tasks, - flashblocks: flashblocks_clone, }, flashblocks_parts, )) @@ -313,21 +311,6 @@ impl LocalNode { self.provider.clone() } - pub(crate) fn flashblocks_state(&self) -> Option> { - self.flashblocks.as_ref().map(|parts| parts.state.clone()) - } - - pub(crate) async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let parts = self - .flashblocks - .as_ref() - .ok_or_else(|| eyre::eyre!("flashblocks not configured for this node"))?; - - let (tx, rx) = oneshot::channel(); - parts.sender.send((flashblock, tx)).await.map_err(|err| eyre::eyre!(err))?; - rx.await.map_err(|err| eyre::eyre!(err))?; - Ok(()) - } } fn init_flashblocks_state( @@ -341,3 +324,40 @@ fn init_flashblocks_state( }) .clone() } + +pub struct FlashblocksLocalNode { + node: LocalNode, + parts: FlashblocksParts, +} + +impl FlashblocksLocalNode { + pub async fn new() -> Result { + Self::with_launcher(default_launcher).await + } + + pub async fn with_launcher(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + let (node, parts) = LocalNode::build_with_kind(launcher, LocalNodeKind::Flashblocks).await?; + let parts = parts.expect("flashblocks parts initialized"); + Ok(Self { node, parts }) + } + + pub fn flashblocks_state(&self) -> Arc { + self.parts.state() + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.parts.send(flashblock).await + } + + pub fn into_parts(self) -> (LocalNode, FlashblocksParts) { + (self.node, self.parts) + } + + pub fn as_node(&self) -> &LocalNode { + &self.node + } +} From c40e761b74d22513266a45ba97d0a5bfb570dddd Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 22 Nov 2025 23:41:26 -0600 Subject: [PATCH 32/38] Decouple Flashblocks node --- crates/test-utils/README.md | 53 ++--- crates/test-utils/src/harness.rs | 7 +- crates/test-utils/src/node.rs | 327 +++++++++++++++---------------- 3 files changed, 177 insertions(+), 210 deletions(-) diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 157637e6..3152028f 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -90,21 +90,18 @@ async fn test_harness() -> eyre::Result<()> { let txs: Vec = vec![/* signed transaction bytes */]; harness.build_block_from_transactions(txs).await?; - // Send flashblocks for pending state testing - harness.send_flashblock(flashblock).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) -- `send_flashblock(fb)` - Send a single flashblock to the node for pending state processing -- `send_flashblocks(iter)` - Convenience helper that sends multiple flashblocks sequentially **Block Building Process:** 1. Fetches latest block header from provider (no local state tracking) @@ -120,32 +117,35 @@ async fn test_harness() -> eyre::Result<()> { In-process Optimism node with Base Sepolia configuration. ```rust -use base_reth_test_utils::node::{LocalNode, default_launcher}; +use base_reth_test_utils::node::LocalNode; #[tokio::test] async fn test_node() -> eyre::Result<()> { let node = LocalNode::new(default_launcher).await?; - // Get provider let provider = node.provider()?; - - // Get Engine API let engine = node.engine_api()?; - // Send flashblocks - node.send_flashblock(flashblock).await?; - Ok(()) } ``` -**Features:** +**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` -- Flashblocks-canon ExEx integration + +For flashblocks-enabled nodes, use `FlashblocksLocalNode`: + +```rust +use base_reth_test_utils::node::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. @@ -228,30 +228,7 @@ async fn test_flashblocks() -> eyre::Result<()> { `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. Flashblocks can be manually constructed and sent via the harness. - -```rust -use base_reth_flashblocks_rpc::subscription::Flashblock; - -#[tokio::test] -async fn test_flashblocks() -> eyre::Result<()> { - let harness = TestHarness::new().await?; - - // Construct a Flashblock manually - // Use base_reth_flashblocks_rpc imports to build the struct - let flashblock = Flashblock { ... }; - - harness.send_flashblock(flashblock).await?; - - Ok(()) -} -``` - -**Via TestHarness:** -```rust -let harness = TestHarness::new().await?; -harness.send_flashblock(flashblock).await?; -``` +Test flashblocks delivery without WebSocket connections by constructing payloads and sending them through `FlashblocksHarness` (or the lower-level `FlashblocksLocalNode`). ## Configuration Constants diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 34cb80a5..ee141a40 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -3,7 +3,7 @@ use std::{ops::Deref, sync::Arc, time::Duration}; use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; -use alloy_primitives::{B256, B64, Bytes, bytes}; +use alloy_primitives::{B64, B256, Bytes, bytes}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; @@ -25,7 +25,10 @@ use tokio::time::sleep; use crate::{ accounts::TestAccounts, engine::{EngineApi, IpcEngine}, - node::{default_launcher, FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, LocalNode, LocalNodeProvider, OpAddOns, OpBuilder}, + node::{ + FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, LocalNode, + LocalNodeProvider, OpAddOns, OpBuilder, default_launcher, + }, tracing::init_silenced_tracing, }; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 4294f679..f663a5fe 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -42,12 +42,6 @@ use tokio_stream::StreamExt; use crate::engine::EngineApi; -#[derive(Clone, Copy)] -enum LocalNodeKind { - Base, - Flashblocks, -} - pub const BASE_CHAIN_ID: u64 = 84532; pub type LocalNodeProvider = BlockchainProvider>; @@ -106,169 +100,7 @@ impl LocalNode { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - let (node, _) = Self::build_with_kind(launcher, LocalNodeKind::Base).await?; - Ok(node) - } - - async fn build_with_kind( - launcher: L, - kind: LocalNodeKind, - ) -> Result<(Self, Option)> - 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() - }; - - // Generate unique IPC path for this test instance to avoid conflicts - // Use timestamp + thread ID + process ID for uniqueness - 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(); - rpc_args.auth_ipc_path = unique_ipc_path; - - let node = OpNode::new(RollupArgs::default()); - - let temp_db = Self::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 mut 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 mut flashblocks_setup: Option = None; - - if matches!(kind, LocalNodeKind::Flashblocks) { - let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - let provider_cell: Arc> = Arc::new(OnceCell::new()); - let mut receiver = Some(receiver); - - builder = builder - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - move |mut ctx| async move { - let provider = - provider_cell.get_or_init(|| ctx.provider().clone()).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() { - for block in committed.blocks_iter() { - fb.on_canonical_block_received(block); - } - let _ = ctx.events.send(ExExEvent::FinishedHeight( - committed.tip().num_hash(), - )); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules({ - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - move |ctx| { - let provider = - provider_cell.get_or_init(|| ctx.provider().clone()).clone(); - let fb = init_flashblocks_state(&fb_cell, &provider); - - let provider_for_task = provider.clone(); - 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_for_task - .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())?; - - let fb_for_task = fb.clone(); - let mut receiver = receiver - .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(()) - } - }); - - flashblocks_setup = Some(FlashblocksSetup { sender, fb_cell }); - } - - 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 engine_ipc_path = node_config.rpc.auth_ipc_path; - let provider = node_handle.provider().clone(); - - let flashblocks_parts = flashblocks_setup.map(|setup| FlashblocksParts { - sender: setup.sender, - state: setup - .fb_cell - .get() - .expect("FlashblocksState should be initialized during node launch") - .clone(), - }); - - Ok(( - Self { - http_api_addr, - engine_ipc_path, - provider, - _node_exit_future: node_exit_future, - _node: Box::new(node_handle), - _task_manager: tasks, - }, - flashblocks_parts, - )) + build_node(launcher, false).await.map(|(node, _)| node) } /// Creates a test database with a smaller map size to reduce memory usage. @@ -310,7 +142,162 @@ impl LocalNode { pub fn blockchain_provider(&self) -> LocalNodeProvider { self.provider.clone() } +} + +async fn build_node( + launcher: L, + enable_flashblocks: bool, +) -> Result<(LocalNode, Option)> +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(); + 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 mut 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 mut flashblocks_setup: Option = None; + + if enable_flashblocks { + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let provider_cell: Arc> = Arc::new(OnceCell::new()); + let mut receiver = Some(receiver); + + builder = builder + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + move |mut ctx| async move { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { + for block in committed.blocks_iter() { + fb.on_canonical_block_received(block); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules({ + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + move |ctx| { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + + let provider_for_task = provider.clone(); + 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_for_task + .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())?; + + let fb_for_task = fb.clone(); + let mut receiver = receiver + .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(()) + } + }); + + flashblocks_setup = Some(FlashblocksSetup { sender, fb_cell }); + } + 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 engine_ipc_path = node_config.rpc.auth_ipc_path; + let provider = node_handle.provider().clone(); + + let flashblocks_parts = flashblocks_setup.map(|setup| FlashblocksParts { + sender: setup.sender, + state: setup + .fb_cell + .get() + .expect("FlashblocksState should be initialized during node launch") + .clone(), + }); + + let node = LocalNode { + http_api_addr, + engine_ipc_path, + provider, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + }; + + Ok((node, flashblocks_parts)) } fn init_flashblocks_state( @@ -340,7 +327,7 @@ impl FlashblocksLocalNode { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - let (node, parts) = LocalNode::build_with_kind(launcher, LocalNodeKind::Flashblocks).await?; + let (node, parts) = build_node(launcher, true).await?; let parts = parts.expect("flashblocks parts initialized"); Ok(Self { node, parts }) } From a70a280d83b411bfacffcb6d22a1da08e499e562 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 00:39:59 -0600 Subject: [PATCH 33/38] Refactor flashblocks harness and tests --- crates/flashblocks-rpc/tests/rpc.rs | 72 +++------ crates/flashblocks-rpc/tests/state.rs | 56 +++---- crates/metering/src/tests/rpc.rs | 2 +- crates/test-utils/src/node.rs | 221 ++++++++++++++------------ crates/test-utils/src/tracing.rs | 7 +- 5 files changed, 175 insertions(+), 183 deletions(-) diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 35bec77a..23057652 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -14,6 +14,7 @@ use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; use base_reth_test_utils::harness::FlashblocksHarness; +use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; use eyre::Result; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; @@ -22,8 +23,6 @@ use reth_optimism_primitives::OpReceipt; use reth_rpc_eth_api::RpcReceipt; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; -use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; - struct TestSetup { harness: FlashblocksHarness, } @@ -117,8 +116,7 @@ fn create_test_logs() -> Vec { address: COUNTER_ADDRESS, data: LogData::new( vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], - bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") - .into(), // 1 ETH in wei + bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000").into(), // 1 ETH in wei ) .unwrap(), }, @@ -126,8 +124,7 @@ fn create_test_logs() -> Vec { address: TEST_ADDRESS, data: LogData::new( vec![TEST_LOG_TOPIC_0], - bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") - .into(), // Value: 1 + bytes!("0x0000000000000000000000000000000000000000000000000000000000000001").into(), // Value: 1 ) .unwrap(), }, @@ -261,7 +258,7 @@ fn create_second_payload() -> Flashblock { #[tokio::test] async fn test_get_pending_block() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); let latest_block = provider @@ -308,7 +305,7 @@ async fn test_get_pending_block() -> Result<()> { #[tokio::test] async fn test_get_balance_pending() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -323,7 +320,7 @@ async fn test_get_balance_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_by_hash_pending() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); assert!(provider.get_transaction_by_hash(DEPOSIT_TX_HASH).await?.is_none()); @@ -344,7 +341,7 @@ async fn test_get_transaction_by_hash_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_receipt_pending() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; @@ -365,7 +362,7 @@ async fn test_get_transaction_receipt_pending() -> Result<()> { #[tokio::test] async fn test_get_transaction_count() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); @@ -381,7 +378,7 @@ async fn test_get_transaction_count() -> Result<()> { #[tokio::test] async fn test_eth_call() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // We ensure that eth_call will succeed because we are on plain state @@ -394,8 +391,7 @@ async fn test_eth_call() -> Result<()> { .value(U256::from(9999999999849942300000u128)) .input(TransactionInput::new(bytes!("0x"))); - let res = - provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; + let res = provider.call(send_eth_call.clone()).block(BlockNumberOrTag::Pending.into()).await; assert!(res.is_ok()); @@ -403,16 +399,11 @@ async fn test_eth_call() -> Result<()> { // We included a heavy spending transaction and now don't have enough funds for this request, so // this eth_call with fail - let res = - provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; + let res = provider.call(send_eth_call.nonce(4)).block(BlockNumberOrTag::Pending.into()).await; assert!(res.is_err()); assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") + res.unwrap_err().as_error_resp().unwrap().message.contains("insufficient funds for gas") ); // read count1 from counter contract @@ -426,10 +417,7 @@ async fn test_eth_call() -> Result<()> { .input(TransactionInput::new(bytes!("0xa87d942c"))); let res_count1 = provider.call(eth_call_count1).await; assert!(res_count1.is_ok()); - assert_eq!( - U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); + assert_eq!(U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), U256::from(2)); // read count2 from counter contract let eth_call_count2 = OpTransactionRequest::default() @@ -442,17 +430,14 @@ async fn test_eth_call() -> Result<()> { .input(TransactionInput::new(bytes!("0xd631c639"))); let res_count2 = provider.call(eth_call_count2).await; assert!(res_count2.is_ok()); - assert_eq!( - U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), - U256::from(2) - ); + assert_eq!(U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), U256::from(2)); Ok(()) } #[tokio::test] async fn test_eth_estimate_gas() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // We ensure that eth_estimate_gas will succeed because we are on plain state @@ -483,11 +468,7 @@ async fn test_eth_estimate_gas() -> Result<()> { assert!(res.is_err()); assert!( - res.unwrap_err() - .as_error_resp() - .unwrap() - .message - .contains("insufficient funds for gas") + res.unwrap_err().as_error_resp().unwrap().message.contains("insufficient funds for gas") ); Ok(()) @@ -495,7 +476,7 @@ async fn test_eth_estimate_gas() -> Result<()> { #[tokio::test] async fn test_eth_simulate_v1() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -557,7 +538,7 @@ async fn test_eth_simulate_v1() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; setup.send_flashblock(create_first_payload()).await?; @@ -577,27 +558,24 @@ async fn test_send_raw_transaction_sync() -> Result<()> { #[tokio::test] async fn test_send_raw_transaction_sync_timeout() { - let setup = TestSetup::new().await.unwrap(); + let setup = TestSetup::new().await.unwrap(); // fail request immediately by passing a timeout of 0 ms let receipt_result = setup.send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)).await; let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); - assert!( - receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str()) - ); + assert!(receipt_result.err().unwrap().to_string().contains(format!("{}", error_code).as_str())); } #[tokio::test] async fn test_get_logs_pending() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); // Test no logs when no flashblocks sent let logs = provider .get_logs( - &alloy_rpc_types_eth::Filter::default() - .select(alloy_eips::BlockNumberOrTag::Pending), + &alloy_rpc_types_eth::Filter::default().select(alloy_eips::BlockNumberOrTag::Pending), ) .await?; assert_eq!(logs.len(), 0); @@ -632,7 +610,7 @@ async fn test_get_logs_pending() -> Result<()> { #[tokio::test] async fn test_get_logs_filter_by_address() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -672,7 +650,7 @@ async fn test_get_logs_filter_by_address() -> Result<()> { #[tokio::test] async fn test_get_logs_topic_filtering() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; @@ -707,7 +685,7 @@ async fn test_get_logs_topic_filtering() -> Result<()> { #[tokio::test] async fn test_get_logs_mixed_block_ranges() -> Result<()> { - let setup = TestSetup::new().await?; + let setup = TestSetup::new().await?; let provider = setup.harness.provider(); setup.send_test_payloads().await?; diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index c8320765..aae612c2 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -11,7 +11,10 @@ use base_reth_flashblocks_rpc::{ state::FlashblocksState, subscription::{Flashblock, FlashblocksReceiver, Metadata}, }; -use base_reth_test_utils::{accounts::TestAccounts, harness::TestHarness as BaseHarness, node::LocalNodeProvider}; +use base_reth_test_utils::{ + accounts::TestAccounts, harness::TestHarness as BaseHarness, node::LocalNodeProvider, +}; +use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::BlockResponse; use reth::{ @@ -24,8 +27,6 @@ use reth_primitives_traits::{Account, Block as BlockT, RecoveredBlock}; use reth_provider::{ChainSpecProvider, StateProviderFactory}; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; use tokio::time::sleep; - -use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; // 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; @@ -70,8 +71,7 @@ impl TestHarness { 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::Bob, Self::decode_private_key(accounts.bob.private_key)); user_to_private_key .insert(User::Charlie, Self::decode_private_key(accounts.charlie.private_key)); @@ -328,7 +328,7 @@ impl<'a> FlashblockBuilder<'a> { #[tokio::test] async fn test_state_overrides_persisted_across_flashblocks() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -404,7 +404,7 @@ async fn test_state_overrides_persisted_across_flashblocks() { #[tokio::test] async fn test_state_overrides_persisted_across_blocks() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; let initial_base = FlashblockBuilder::new_base(&test).build(); let initial_block_number = initial_base.metadata.block_number; @@ -527,7 +527,7 @@ async fn test_state_overrides_persisted_across_blocks() { #[tokio::test] async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; assert_eq!( @@ -580,10 +580,8 @@ async fn test_only_current_pending_state_cleared_upon_canonical_block_reorg() { test.expected_pending_balance(User::Bob, 100_000) ); - test.send_flashblock( - FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), - ) - .await; + test.send_flashblock(FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build()) + .await; test.send_flashblock( FlashblockBuilder::new(&test, 1) .with_canonical_block_number(1) @@ -653,7 +651,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { // causing it to return an n+1 nonce instead of n // because underlying reth node `latest` block is already updated, but // relevant pending state has not been cleared yet - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; test.send_flashblock( @@ -713,7 +711,7 @@ async fn test_nonce_uses_pending_canon_block_instead_of_latest() { #[tokio::test] async fn test_missing_receipts_will_not_process() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -739,7 +737,7 @@ async fn test_missing_receipts_will_not_process() { #[tokio::test] async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non_zero_index() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -749,10 +747,8 @@ async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non assert_eq!(current_block.header().number, 1); assert_eq!(current_block.transactions.len(), 1); - test.send_flashblock( - FlashblockBuilder::new(&test, 1).with_canonical_block_number(100).build(), - ) - .await; + test.send_flashblock(FlashblockBuilder::new(&test, 1).with_canonical_block_number(100).build()) + .await; let current_block = test.flashblocks.get_pending_blocks().get_block(true); assert!(current_block.is_none()); @@ -760,7 +756,7 @@ async fn test_flashblock_for_new_canonical_block_clears_older_flashblocks_if_non #[tokio::test] async fn test_flashblock_for_new_canonical_block_works_if_sequential() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -770,10 +766,8 @@ async fn test_flashblock_for_new_canonical_block_works_if_sequential() { assert_eq!(current_block.header().number, 1); assert_eq!(current_block.transactions.len(), 1); - test.send_flashblock( - FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build(), - ) - .await; + test.send_flashblock(FlashblockBuilder::new_base(&test).with_canonical_block_number(1).build()) + .await; let current_block = test.flashblocks.get_pending_blocks().get_block(true).expect("should be a block"); @@ -784,7 +778,7 @@ async fn test_flashblock_for_new_canonical_block_works_if_sequential() { #[tokio::test] async fn test_non_sequential_payload_clears_pending_state() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; assert!(test.flashblocks.get_pending_blocks().get_block(true).is_none()); @@ -817,7 +811,7 @@ async fn test_non_sequential_payload_clears_pending_state() { #[tokio::test] async fn test_duplicate_flashblock_ignored() { - let test = TestHarness::new().await; + let test = TestHarness::new().await; test.send_flashblock(FlashblockBuilder::new_base(&test).build()).await; @@ -840,19 +834,15 @@ async fn test_duplicate_flashblock_ignored() { #[tokio::test] async fn test_progress_canonical_blocks_without_flashblocks() { - let mut test = TestHarness::new().await; + let mut test = TestHarness::new().await; let genesis_block = test.node.latest_block(); assert_eq!(genesis_block.number, 0); 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(User::Alice, User::Bob, 100)]) + .await; let block_one = test.node.latest_block(); assert_eq!(block_one.number, 1); diff --git a/crates/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index c4a04fc7..b77f9534 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -5,8 +5,8 @@ mod tests { use alloy_eips::Encodable2718; use alloy_genesis::Genesis; use alloy_primitives::{Bytes, U256, address, b256, bytes}; - use base_reth_test_utils::tracing::init_silenced_tracing; use alloy_rpc_client::RpcClient; + use base_reth_test_utils::tracing::init_silenced_tracing; use op_alloy_consensus::OpTxEnvelope; use reth::{ args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index f663a5fe..16d01e1b 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -1,6 +1,10 @@ //! Local node setup with Base Sepolia chainspec -use std::{any::Any, net::SocketAddr, sync::Arc}; +use std::{ + any::Any, + net::SocketAddr, + sync::{Arc, Mutex}, +}; use alloy_genesis::Genesis; use alloy_provider::RootProvider; @@ -75,9 +79,108 @@ impl FlashblocksParts { } } -struct FlashblocksSetup { +#[derive(Clone)] +struct FlashblocksLauncherContext { + inner: Arc, +} + +struct FlashblocksLauncherInner { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, - fb_cell: Arc>>>, + receiver: Arc)>>>>, + fb_cell: Arc>>, + provider_cell: Arc>, +} + +impl FlashblocksLauncherContext { + fn new() -> Self { + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let inner = FlashblocksLauncherInner { + sender, + receiver: Arc::new(Mutex::new(Some(receiver))), + fb_cell: Arc::new(OnceCell::new()), + provider_cell: Arc::new(OnceCell::new()), + }; + Self { inner: Arc::new(inner) } + } + + fn configure_builder(&self, builder: OpBuilder) -> OpBuilder { + let fb_cell = self.inner.fb_cell.clone(); + let provider_cell = self.inner.provider_cell.clone(); + let receiver = self.inner.receiver.clone(); + + let fb_cell_for_exex = fb_cell.clone(); + let provider_cell_for_exex = provider_cell.clone(); + + builder + .install_exex("flashblocks-canon", move |mut ctx| { + let fb_cell = fb_cell_for_exex.clone(); + let provider_cell = provider_cell_for_exex.clone(); + async move { + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { + for block in committed.blocks_iter() { + fb.on_canonical_block_received(block); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb_cell = fb_cell.clone(); + let provider_cell = provider_cell.clone(); + let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + + let provider_for_task = provider.clone(); + 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_for_task + .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())?; + + 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 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() }) + } } pub type OpTypes = @@ -100,7 +203,7 @@ impl LocalNode { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - build_node(launcher, false).await.map(|(node, _)| node) + build_node(launcher).await } /// Creates a test database with a smaller map size to reduce memory usage. @@ -144,10 +247,7 @@ impl LocalNode { } } -async fn build_node( - launcher: L, - enable_flashblocks: bool, -) -> Result<(LocalNode, Option)> +async fn build_node(launcher: L) -> Result where L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, @@ -187,87 +287,13 @@ where node_config = node_config.with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); - let mut builder = NodeBuilder::new(node_config.clone()) + 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 mut flashblocks_setup: Option = None; - - if enable_flashblocks { - let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); - let provider_cell: Arc> = Arc::new(OnceCell::new()); - let mut receiver = Some(receiver); - - builder = builder - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - move |mut ctx| async move { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { - for block in committed.blocks_iter() { - fb.on_canonical_block_received(block); - } - let _ = ctx - .events - .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules({ - let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - move |ctx| { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); - let fb = init_flashblocks_state(&fb_cell, &provider); - - let provider_for_task = provider.clone(); - 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_for_task - .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())?; - - let fb_for_task = fb.clone(); - let mut receiver = receiver - .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(()) - } - }); - - flashblocks_setup = Some(FlashblocksSetup { sender, fb_cell }); - } - let NodeHandle { node: node_handle, node_exit_future } = builder.launch_with_fn(launcher).await?; @@ -279,25 +305,14 @@ where let engine_ipc_path = node_config.rpc.auth_ipc_path; let provider = node_handle.provider().clone(); - let flashblocks_parts = flashblocks_setup.map(|setup| FlashblocksParts { - sender: setup.sender, - state: setup - .fb_cell - .get() - .expect("FlashblocksState should be initialized during node launch") - .clone(), - }); - - let node = LocalNode { + Ok(LocalNode { http_api_addr, engine_ipc_path, provider, _node_exit_future: node_exit_future, _node: Box::new(node_handle), _task_manager: tasks, - }; - - Ok((node, flashblocks_parts)) + }) } fn init_flashblocks_state( @@ -327,8 +342,18 @@ impl FlashblocksLocalNode { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - let (node, parts) = build_node(launcher, true).await?; - let parts = parts.expect("flashblocks parts initialized"); + let context = FlashblocksLauncherContext::new(); + let builder_context = context.clone(); + let mut launcher = Some(launcher); + + let node = LocalNode::new(move |builder| { + let builder = builder_context.configure_builder(builder); + let launcher = launcher.take().expect("launcher already taken"); + launcher(builder) + }) + .await?; + + let parts = context.parts()?; Ok(Self { node, parts }) } diff --git a/crates/test-utils/src/tracing.rs b/crates/test-utils/src/tracing.rs index 30baad0d..f75b275a 100644 --- a/crates/test-utils/src/tracing.rs +++ b/crates/test-utils/src/tracing.rs @@ -1,6 +1,6 @@ use std::sync::Once; -use tracing_subscriber::{filter::LevelFilter, EnvFilter}; +use tracing_subscriber::{EnvFilter, filter::LevelFilter}; static INIT: Once = Once::new(); @@ -12,9 +12,8 @@ static INIT: Once = Once::new(); /// installs the subscriber. pub fn init_silenced_tracing() { INIT.call_once(|| { - let mut filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); + let mut filter = + EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy(); for directive in ["reth_tasks=off", "reth_node_builder::launch::common=off"].into_iter() { if let Ok(directive) = directive.parse() { From 0ecfe42c451a902baec445602630ca7f55fc62bc Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 01:06:42 -0600 Subject: [PATCH 34/38] Add manual flashblocks harness --- crates/flashblocks-rpc/tests/state.rs | 24 ++++++++++------- crates/test-utils/src/harness.rs | 26 +++++++++++++++--- crates/test-utils/src/node.rs | 38 ++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index aae612c2..c6f388ab 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -1,6 +1,6 @@ mod common; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use alloy_consensus::{Receipt, Transaction}; use alloy_eips::{BlockHashOrNumber, Encodable2718}; @@ -9,10 +9,10 @@ use alloy_rpc_types_engine::PayloadId; use base_reth_flashblocks_rpc::{ rpc::{FlashblocksAPI, PendingBlocksAPI}, state::FlashblocksState, - subscription::{Flashblock, FlashblocksReceiver, Metadata}, + subscription::{Flashblock, Metadata}, }; use base_reth_test_utils::{ - accounts::TestAccounts, harness::TestHarness as BaseHarness, node::LocalNodeProvider, + accounts::TestAccounts, harness::FlashblocksHarness, node::LocalNodeProvider, }; use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; use op_alloy_consensus::OpDepositReceipt; @@ -39,8 +39,8 @@ enum User { } struct TestHarness { - node: BaseHarness, - flashblocks: FlashblocksState, + node: FlashblocksHarness, + flashblocks: Arc>, provider: LocalNodeProvider, user_to_address: HashMap, user_to_private_key: HashMap, @@ -48,10 +48,13 @@ struct TestHarness { impl TestHarness { async fn new() -> Self { - let node = BaseHarness::new().await.expect("able to launch base harness"); + // These tests simulate pathological timing (missing receipts, reorgs, etc.), so we disable + // the automatic canonical listener and only apply blocks when the test explicitly requests it. + let node = FlashblocksHarness::manual_canonical() + .await + .expect("able to launch flashblocks harness"); let provider = node.blockchain_provider(); - let flashblocks = FlashblocksState::new(provider.clone(), 5); - flashblocks.start(); + let flashblocks = node.flashblocks_state(); let genesis_block = provider .block(BlockHashOrNumber::Number(0)) @@ -177,7 +180,10 @@ impl TestHarness { } async fn send_flashblock(&self, flashblock: Flashblock) { - self.flashblocks.on_flashblock_received(flashblock); + self.node + .send_flashblock(flashblock) + .await + .expect("flashblocks channel should accept payload"); sleep(Duration::from_millis(SLEEP_TIME)).await; } diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index ee141a40..2938cf22 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -195,6 +195,12 @@ impl FlashblocksHarness { Self::with_launcher(default_launcher).await } + /// Same as `new` but canonical block processing is left to the test, which is useful when + /// reproducing races or reorg scenarios that require deterministic sequencing. + pub async fn manual_canonical() -> Result { + Self::manual_canonical_with_launcher(default_launcher).await + } + pub async fn with_launcher(launcher: L) -> Result where L: FnOnce(OpBuilder) -> LRet, @@ -202,9 +208,17 @@ impl FlashblocksHarness { { init_silenced_tracing(); let flash_node = FlashblocksLocalNode::with_launcher(launcher).await?; - let (node, parts) = flash_node.into_parts(); - let inner = TestHarness::from_node(node).await?; - Ok(Self { inner, parts }) + Self::from_flashblocks_node(flash_node).await + } + + 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 } pub fn flashblocks_state(&self) -> Arc { @@ -228,6 +242,12 @@ impl FlashblocksHarness { 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 }) + } } impl Deref for FlashblocksHarness { diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 16d01e1b..f5c6c000 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -89,16 +89,18 @@ struct FlashblocksLauncherInner { receiver: Arc)>>>>, fb_cell: Arc>>, provider_cell: Arc>, + process_canonical: bool, } impl FlashblocksLauncherContext { - fn new() -> Self { + fn new(process_canonical: bool) -> Self { let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); let inner = FlashblocksLauncherInner { sender, receiver: Arc::new(Mutex::new(Some(receiver))), fb_cell: Arc::new(OnceCell::new()), provider_cell: Arc::new(OnceCell::new()), + process_canonical, }; Self { inner: Arc::new(inner) } } @@ -107,6 +109,7 @@ impl FlashblocksLauncherContext { let fb_cell = self.inner.fb_cell.clone(); let provider_cell = self.inner.provider_cell.clone(); let receiver = self.inner.receiver.clone(); + let process_canonical = self.inner.process_canonical; let fb_cell_for_exex = fb_cell.clone(); let provider_cell_for_exex = provider_cell.clone(); @@ -115,14 +118,19 @@ impl FlashblocksLauncherContext { .install_exex("flashblocks-canon", move |mut ctx| { let fb_cell = fb_cell_for_exex.clone(); let provider_cell = provider_cell_for_exex.clone(); + let process_canonical = process_canonical; async move { let provider = provider_cell.get_or_init(|| ctx.provider().clone()).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() { - for block in committed.blocks_iter() { - fb.on_canonical_block_received(block); + if process_canonical { + // Many suites drive canonical updates manually to reproduce race conditions, so + // allowing this to be disabled keeps canonical replay deterministic. + for block in committed.blocks_iter() { + fb.on_canonical_block_received(block); + } } let _ = ctx .events @@ -337,12 +345,34 @@ impl FlashblocksLocalNode { 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 + } + pub async fn with_launcher(launcher: L) -> Result where L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - let context = FlashblocksLauncherContext::new(); + Self::with_launcher_inner(launcher, true).await + } + + 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 context = FlashblocksLauncherContext::new(process_canonical); let builder_context = context.clone(); let mut launcher = Some(launcher); From 788330e7f72b677d1f054498fbf92ebe0ce97831 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 01:17:34 -0600 Subject: [PATCH 35/38] Clarify flashblocks launcher wrapping --- crates/test-utils/src/node.rs | 38 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index f5c6c000..1843cb6a 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -80,11 +80,11 @@ impl FlashblocksParts { } #[derive(Clone)] -struct FlashblocksLauncherContext { - inner: Arc, +struct FlashblocksNodeExtensions { + inner: Arc, } -struct FlashblocksLauncherInner { +struct FlashblocksNodeExtensionsInner { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, receiver: Arc)>>>>, fb_cell: Arc>>, @@ -92,10 +92,10 @@ struct FlashblocksLauncherInner { process_canonical: bool, } -impl FlashblocksLauncherContext { +impl FlashblocksNodeExtensions { fn new(process_canonical: bool) -> Self { let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let inner = FlashblocksLauncherInner { + let inner = FlashblocksNodeExtensionsInner { sender, receiver: Arc::new(Mutex::new(Some(receiver))), fb_cell: Arc::new(OnceCell::new()), @@ -105,7 +105,7 @@ impl FlashblocksLauncherContext { Self { inner: Arc::new(inner) } } - fn configure_builder(&self, builder: OpBuilder) -> OpBuilder { + fn apply(&self, builder: OpBuilder) -> OpBuilder { let fb_cell = self.inner.fb_cell.clone(); let provider_cell = self.inner.provider_cell.clone(); let receiver = self.inner.receiver.clone(); @@ -183,6 +183,17 @@ impl FlashblocksLauncherContext { }) } + 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") @@ -372,18 +383,11 @@ impl FlashblocksLocalNode { L: FnOnce(OpBuilder) -> LRet, LRet: Future, OpAddOns>>>, { - let context = FlashblocksLauncherContext::new(process_canonical); - let builder_context = context.clone(); - let mut launcher = Some(launcher); - - let node = LocalNode::new(move |builder| { - let builder = builder_context.configure_builder(builder); - let launcher = launcher.take().expect("launcher already taken"); - launcher(builder) - }) - .await?; + let extensions = FlashblocksNodeExtensions::new(process_canonical); + let wrapped_launcher = extensions.wrap_launcher(launcher); + let node = LocalNode::new(wrapped_launcher).await?; - let parts = context.parts()?; + let parts = extensions.parts()?; Ok(Self { node, parts }) } From 83390a48aa83237158b6ac1ca53a73887dbc243e Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 01:22:10 -0600 Subject: [PATCH 36/38] Simplify flashblocks provider wiring --- crates/test-utils/src/node.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 1843cb6a..5a731bfd 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -88,7 +88,6 @@ struct FlashblocksNodeExtensionsInner { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, receiver: Arc)>>>>, fb_cell: Arc>>, - provider_cell: Arc>, process_canonical: bool, } @@ -99,7 +98,6 @@ impl FlashblocksNodeExtensions { sender, receiver: Arc::new(Mutex::new(Some(receiver))), fb_cell: Arc::new(OnceCell::new()), - provider_cell: Arc::new(OnceCell::new()), process_canonical, }; Self { inner: Arc::new(inner) } @@ -107,20 +105,17 @@ impl FlashblocksNodeExtensions { fn apply(&self, builder: OpBuilder) -> OpBuilder { let fb_cell = self.inner.fb_cell.clone(); - let provider_cell = self.inner.provider_cell.clone(); let receiver = self.inner.receiver.clone(); let process_canonical = self.inner.process_canonical; let fb_cell_for_exex = fb_cell.clone(); - let provider_cell_for_exex = provider_cell.clone(); builder .install_exex("flashblocks-canon", move |mut ctx| { let fb_cell = fb_cell_for_exex.clone(); - let provider_cell = provider_cell_for_exex.clone(); let process_canonical = process_canonical; async move { - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + 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? { @@ -143,20 +138,16 @@ impl FlashblocksNodeExtensions { }) .extend_rpc_modules(move |ctx| { let fb_cell = fb_cell.clone(); - let provider_cell = provider_cell.clone(); - let provider = provider_cell.get_or_init(|| ctx.provider().clone()).clone(); + let provider = ctx.provider().clone(); let fb = init_flashblocks_state(&fb_cell, &provider); - let provider_for_task = provider.clone(); 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_for_task - .canonical_in_memory_state() - .notify_canon_state(notification); + provider.canonical_in_memory_state().notify_canon_state(notification); } }); let api_ext = EthApiExt::new( From 69023455bf36f8970d84853c99d4ce989b426eb4 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 01:31:37 -0600 Subject: [PATCH 37/38] Extract FlashblocksHarness module --- crates/flashblocks-rpc/tests/rpc.rs | 2 +- crates/flashblocks-rpc/tests/state.rs | 2 +- crates/test-utils/README.md | 2 +- crates/test-utils/src/flashblocks_harness.rs | 88 ++++++++++++++++++++ crates/test-utils/src/harness.rs | 85 +------------------ crates/test-utils/src/lib.rs | 1 + 6 files changed, 96 insertions(+), 84 deletions(-) create mode 100644 crates/test-utils/src/flashblocks_harness.rs diff --git a/crates/flashblocks-rpc/tests/rpc.rs b/crates/flashblocks-rpc/tests/rpc.rs index 23057652..e9e10eed 100644 --- a/crates/flashblocks-rpc/tests/rpc.rs +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -13,7 +13,7 @@ use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::{TransactionInput, error::EthRpcErrorCode}; use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata}; -use base_reth_test_utils::harness::FlashblocksHarness; +use base_reth_test_utils::flashblocks_harness::FlashblocksHarness; use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; use eyre::Result; use op_alloy_consensus::OpDepositReceipt; diff --git a/crates/flashblocks-rpc/tests/state.rs b/crates/flashblocks-rpc/tests/state.rs index c6f388ab..5c93c1e8 100644 --- a/crates/flashblocks-rpc/tests/state.rs +++ b/crates/flashblocks-rpc/tests/state.rs @@ -12,7 +12,7 @@ use base_reth_flashblocks_rpc::{ subscription::{Flashblock, Metadata}, }; use base_reth_test_utils::{ - accounts::TestAccounts, harness::FlashblocksHarness, node::LocalNodeProvider, + accounts::TestAccounts, flashblocks_harness::FlashblocksHarness, node::LocalNodeProvider, }; use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; use op_alloy_consensus::OpDepositReceipt; diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 3152028f..e2222b28 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -211,7 +211,7 @@ Each account includes: Use `FlashblocksHarness` when you need `send_flashblock` and access to the in-memory pending state. ```rust -use base_reth_test_utils::harness::FlashblocksHarness; +use base_reth_test_utils::flashblocks_harness::FlashblocksHarness; #[tokio::test] async fn test_flashblocks() -> eyre::Result<()> { diff --git a/crates/test-utils/src/flashblocks_harness.rs b/crates/test-utils/src/flashblocks_harness.rs new file mode 100644 index 00000000..6ed61caf --- /dev/null +++ b/crates/test-utils/src/flashblocks_harness.rs @@ -0,0 +1,88 @@ +use std::{ops::Deref, sync::Arc}; + +use base_reth_flashblocks_rpc::subscription::Flashblock; +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, + node::{ + FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, OpAddOns, OpBuilder, + default_launcher, + }, + tracing::init_silenced_tracing, +}; + +pub struct FlashblocksHarness { + inner: TestHarness, + parts: FlashblocksParts, +} + +impl FlashblocksHarness { + pub async fn new() -> Result { + Self::with_launcher(default_launcher).await + } + + pub async fn manual_canonical() -> Result { + Self::manual_canonical_with_launcher(default_launcher).await + } + + 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 + } + + 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 + } + + pub fn flashblocks_state(&self) -> Arc { + self.parts.state() + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.parts.send(flashblock).await + } + + pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> + where + I: IntoIterator, + { + for flashblock in flashblocks { + self.send_flashblock(flashblock).await?; + } + Ok(()) + } + + 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 }) + } +} + +impl Deref for FlashblocksHarness { + type Target = TestHarness; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 2938cf22..be656c82 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -1,13 +1,12 @@ -//! Unified test harness combining node and engine helpers, plus an optional flashblocks adapter. +//! Unified test harness combining node and engine helpers, plus optional flashblocks adapter. -use std::{ops::Deref, sync::Arc, time::Duration}; +use std::time::Duration; use alloy_eips::{BlockHashOrNumber, eip7685::Requests}; use alloy_primitives::{B64, B256, Bytes, bytes}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; -use base_reth_flashblocks_rpc::subscription::Flashblock; use eyre::{Result, eyre}; use futures_util::Future; use op_alloy_network::Optimism; @@ -25,10 +24,7 @@ use tokio::time::sleep; use crate::{ accounts::TestAccounts, engine::{EngineApi, IpcEngine}, - node::{ - FlashblocksLocalNode, FlashblocksParts, LocalFlashblocksState, LocalNode, - LocalNodeProvider, OpAddOns, OpBuilder, default_launcher, - }, + node::{LocalNode, LocalNodeProvider, OpAddOns, OpBuilder, default_launcher}, tracing::init_silenced_tracing, }; @@ -62,7 +58,7 @@ impl TestHarness { Self::from_node(node).await } - async fn from_node(node: LocalNode) -> Result { + pub(crate) async fn from_node(node: LocalNode) -> Result { let engine = node.engine_api()?; let accounts = TestAccounts::new(); @@ -185,79 +181,6 @@ impl TestHarness { } } -pub struct FlashblocksHarness { - inner: TestHarness, - parts: FlashblocksParts, -} - -impl FlashblocksHarness { - pub async fn new() -> Result { - Self::with_launcher(default_launcher).await - } - - /// Same as `new` but canonical block processing is left to the test, which is useful when - /// reproducing races or reorg scenarios that require deterministic sequencing. - pub async fn manual_canonical() -> Result { - Self::manual_canonical_with_launcher(default_launcher).await - } - - 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 - } - - 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 - } - - pub fn flashblocks_state(&self) -> Arc { - self.parts.state() - } - - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.parts.send(flashblock).await - } - - pub async fn send_flashblocks(&self, flashblocks: I) -> Result<()> - where - I: IntoIterator, - { - for flashblock in flashblocks { - self.send_flashblock(flashblock).await?; - } - Ok(()) - } - - 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 }) - } -} - -impl Deref for FlashblocksHarness { - type Target = TestHarness; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - #[cfg(test)] mod tests { use alloy_primitives::U256; diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 6a9f56a3..2c6b290d 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,5 +1,6 @@ pub mod accounts; pub mod engine; +pub mod flashblocks_harness; pub mod harness; pub mod node; pub mod tracing; From 56ba57b1df8cc589ced77a691f9bdaef7d8ff298 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sun, 23 Nov 2025 01:35:40 -0600 Subject: [PATCH 38/38] Document flashblocks harness module --- crates/test-utils/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index e2222b28..70f7f37e 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -250,7 +250,8 @@ test-utils/ │ ├── accounts.rs # Test account definitions │ ├── node.rs # LocalNode (EL wrapper) │ ├── engine.rs # EngineApi (CL wrapper) -│ └── harness.rs # TestHarness (orchestration) +│ ├── harness.rs # TestHarness (orchestration) +│ └── flashblocks_harness.rs # FlashblocksHarness + helpers ├── assets/ │ └── genesis.json # Base Sepolia genesis └── Cargo.toml