diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..29187eb2 --- /dev/null +++ b/.gitmodules @@ -0,0 +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/Cargo.lock b/Cargo.lock index 4983dc9c..fc818c79 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", @@ -1568,6 +1573,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tracing", + "tracing-subscriber 0.3.20", "url", ] @@ -1580,6 +1586,7 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-rpc-client", + "base-reth-test-utils", "eyre", "jsonrpsee 0.26.0", "op-alloy-consensus", @@ -1667,6 +1674,63 @@ 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", + "alloy-signer", + "alloy-signer-local", + "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-node-core", + "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", + "tracing-subscriber 0.3.20", + "url", +] + [[package]] name = "base-reth-transaction-tracing" version = "0.2.1" @@ -2147,9 +2211,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 +2369,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 +2379,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 +4254,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 +4325,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 +5877,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 +5903,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 +5919,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 +5929,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 +5948,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 +7111,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 +10262,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 +11072,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 +11091,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 +12942,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..6964da5e 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,9 @@ 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" } +reth-node-core = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } # revm revm = { version = "31.0.2", default-features = false } @@ -100,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/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/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 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); + } +} diff --git a/crates/flashblocks-rpc/Cargo.toml b/crates/flashblocks-rpc/Cargo.toml index 61467431..d12a57eb 100644 --- a/crates/flashblocks-rpc/Cargo.toml +++ b/crates/flashblocks-rpc/Cargo.toml @@ -74,8 +74,11 @@ 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 +tracing-subscriber.workspace = true 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 c5a7b04f..00000000 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ /dev/null @@ -1,890 +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::{TransactionInput, error::EthRpcErrorCode}; - 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_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 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 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()) - .testing_node(exec.clone()) - .with_types_and_provider::>() - .with_components(node.components_builder()) - .with_add_ons(node.add_ons()) - .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(); - - let api_ext = EthApiExt::new( - ctx.registry.eth_api().clone(), - ctx.registry.eth_handlers().filter.clone(), - flashblocks_state.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); - 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(()) - } -} diff --git a/crates/flashblocks-rpc/src/tests/state.rs b/crates/flashblocks-rpc/src/tests/state.rs deleted file mode 100644 index 8893e097..00000000 --- a/crates/flashblocks-rpc/src/tests/state.rs +++ /dev/null @@ -1,958 +0,0 @@ -#[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 alloy_rpc_types_engine::PayloadId; - 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 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, 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; - - #[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] - enum User { - Alice, - Bob, - Charlie, - } - - type NodeTypes = NodeTypesWithDBAdapter>>; - - #[derive(Debug, Clone)] - struct TestHarness { - flashblocks: FlashblocksState>, - provider: BlockchainProvider, - factory: ProviderFactory, - user_to_address: HashMap, - user_to_private_key: HashMap, - } - - impl TestHarness { - 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 current_canonical_block(&self) -> RecoveredBlock { - let latest_block_num = - self.provider.last_block_number().expect("should be a latest block"); - - self.provider - .block(BlockHashOrNumber::Number(latest_block_num)) - .expect("able to load block") - .expect("block exists") - .try_into_recovered() - .expect("able to recover block") - } - - 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 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(200) - .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(200) - .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, - mut 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 - } - - 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; - } - - 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 { - transactions: Vec, - receipts: HashMap, - harness: TestHarness, - canonical_block_number: Option, - index: u64, - } - - impl FlashblockBuilder { - pub fn new_base(harness: &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: harness.clone(), - } - } - pub fn new(harness: &TestHarness, index: u64) -> Self { - Self { - canonical_block_number: None, - transactions: Vec::new(), - receipts: HashMap::default(), - harness: harness.clone(), - 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.current_canonical_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(); - - 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"), - U256::from(100_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"), - U256::from(100_100_000) - ); - } - - #[tokio::test] - async fn test_state_overrides_persisted_across_blocks() { - reth_tracing::init_test_tracing(); - let test = TestHarness::new(); - - 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"), - U256::from(100_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"), - U256::from(100_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(); - - 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"), - U256::from(100_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"), - U256::from(100_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"), - U256::from(100_100_100) - ); - } - - #[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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - let genesis_block = test.current_canonical_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.current_canonical_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.current_canonical_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/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)) -} 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. diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/tests/assets/genesis.json similarity index 95% rename from crates/flashblocks-rpc/src/tests/assets/genesis.json rename to crates/flashblocks-rpc/tests/assets/genesis.json index 4d703497..b3099c33 100644 --- a/crates/flashblocks-rpc/src/tests/assets/genesis.json +++ b/crates/flashblocks-rpc/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, @@ -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/mod.rs b/crates/flashblocks-rpc/tests/common/mod.rs similarity index 86% rename from crates/flashblocks-rpc/src/tests/mod.rs rename to crates/flashblocks-rpc/tests/common/mod.rs index 59995edb..5ffd4a5b 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/tests/common/mod.rs @@ -1,11 +1,7 @@ use alloy_primitives::{B256, Bytes, b256, bytes}; -mod rpc; -mod state; -mod utils; - -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..e9e10eed --- /dev/null +++ b/crates/flashblocks-rpc/tests/rpc.rs @@ -0,0 +1,733 @@ +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::flashblocks_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}; +use op_alloy_rpc_types::OpTransactionRequest; +use reth_optimism_primitives::OpReceipt; +use reth_rpc_eth_api::RpcReceipt; +use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + +struct TestSetup { + harness: FlashblocksHarness, +} + +impl TestSetup { + async fn new() -> Result { + let harness = FlashblocksHarness::new().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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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() { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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..5c93c1e8 --- /dev/null +++ b/crates/flashblocks-rpc/tests/state.rs @@ -0,0 +1,868 @@ +mod common; + +use std::{sync::Arc, time::Duration}; + +use alloy_consensus::{Receipt, Transaction}; +use alloy_eips::{BlockHashOrNumber, Encodable2718}; +use alloy_primitives::{Address, B256, BlockNumber, Bytes, U256, hex, map::foldhash::HashMap}; +use alloy_rpc_types_engine::PayloadId; +use base_reth_flashblocks_rpc::{ + rpc::{FlashblocksAPI, PendingBlocksAPI}, + state::FlashblocksState, + subscription::{Flashblock, Metadata}, +}; +use base_reth_test_utils::{ + accounts::TestAccounts, flashblocks_harness::FlashblocksHarness, node::LocalNodeProvider, +}; +use common::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; +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; +// The amount of time to wait (in milliseconds) after sending a new flashblock or canonical block +// so it can be processed by the state processor +const SLEEP_TIME: u64 = 10; + +#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] +enum User { + Alice, + Bob, + Charlie, +} + +struct TestHarness { + node: FlashblocksHarness, + flashblocks: Arc>, + provider: LocalNodeProvider, + user_to_address: HashMap, + user_to_private_key: HashMap, +} + +impl TestHarness { + async fn new() -> Self { + // 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 = node.flashblocks_state(); + + 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.node + .send_flashblock(flashblock) + .await + .expect("flashblocks channel should accept payload"); + 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() { + 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() { + 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() { + 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 + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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/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/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/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index b11002fc..b77f9534 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -6,6 +6,7 @@ mod tests { use alloy_genesis::Genesis; use alloy_primitives::{Bytes, U256, address, b256, bytes}; 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}, @@ -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 new file mode 100644 index 00000000..59e35f4c --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,85 @@ +[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] +# 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, features = ["client"] } +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-node-core.workspace = true +reth-exex.workspace = true +reth-tracing.workspace = true +reth-rpc-layer.workspace = true +reth-ipc.workspace = true + +# alloy +alloy-primitives.workspace = true +alloy-genesis.workspace = true +alloy-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 +alloy-signer = "1.0" +alloy-signer-local = "1.1.0" + +# op-alloy +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true +op-alloy-network.workspace = true +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 +tracing-subscriber.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" + +base-reth-flashblocks-rpc.workspace = true + +[dev-dependencies] diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md new file mode 100644 index 00000000..70f7f37e --- /dev/null +++ b/crates/test-utils/README.md @@ -0,0 +1,318 @@ +# Test Utils + +A comprehensive integration test framework for node-reth crates. + +## Overview + +This crate provides reusable testing utilities for integration tests across the node-reth workspace. It includes: + +- **LocalNode**: Isolated in-process node with Base Sepolia chainspec +- **TestHarness**: Unified orchestration layer combining node, Engine API, and flashblocks +- **EngineApi**: Type-safe Engine API client for CL operations +- **Test Accounts**: Pre-funded hardcoded accounts (Alice, Bob, Charlie, Deployer) +- **Flashblocks Support**: Testing pending state with flashblocks delivery + +## Quick Start + +```rust +use base_reth_test_utils::harness::TestHarness; + +#[tokio::test] +async fn test_example() -> eyre::Result<()> { + let harness = TestHarness::new().await?; + + // Advance the chain + harness.advance_chain(5).await?; + + // Access accounts + let alice = &harness.accounts().alice; + + // Get balance via provider + let balance = harness.provider().get_balance(alice.address).await?; + + Ok(()) +} +``` + +## Architecture + +The framework follows a three-layer architecture: + +``` +┌─────────────────────────────────────┐ +│ TestHarness │ ← Orchestration layer (tests use this) +│ - Coordinates node + engine │ +│ - Builds blocks from transactions │ +│ - Manages test accounts │ +│ - Manages flashblocks │ +└─────────────────────────────────────┘ + │ │ + ┌──────┘ └──────┐ + ▼ ▼ +┌─────────┐ ┌──────────┐ +│LocalNode│ │EngineApi │ ← Raw API wrappers +│ (EL) │ │ (CL) │ +└─────────┘ └──────────┘ +``` + +### Component Responsibilities + +- **LocalNode** (EL wrapper): In-process Optimism node with HTTP RPC + Engine API IPC +- **EngineApi** (CL wrapper): Raw Engine API calls (forkchoice, payloads) +- **TestHarness**: Orchestrates block building by fetching latest block headers and calling Engine API + +## Components + +### 1. TestHarness + +The main entry point for integration tests that only need canonical chain control. Combines node, engine, and accounts into a single interface. + +```rust +use base_reth_test_utils::harness::TestHarness; +use alloy_primitives::Bytes; + +#[tokio::test] +async fn test_harness() -> eyre::Result<()> { + let harness = TestHarness::new().await?; + + // Access provider + let provider = harness.provider(); + let chain_id = provider.get_chain_id().await?; + + // Access accounts + let alice = &harness.accounts().alice; + let bob = &harness.accounts().bob; + + // Build empty blocks + harness.advance_chain(10).await?; + + // Build block with transactions + let txs: Vec = vec![/* signed transaction bytes */]; + harness.build_block_from_transactions(txs).await?; + + Ok(()) +} +``` + +> Need pending-state testing? Use `FlashblocksHarness` (see Flashblocks section below) to gain `send_flashblock` helpers. + +**Key Methods:** +- `new()` - Create new harness with node, engine, and accounts +- `provider()` - Get Alloy RootProvider for RPC calls +- `accounts()` - Access test accounts +- `advance_chain(n)` - Build N empty blocks +- `build_block_from_transactions(txs)` - Build block with specific transactions (auto-prepends the L1 block info deposit) + +**Block Building Process:** +1. Fetches latest block header from provider (no local state tracking) +2. Calculates next timestamp (parent + 2 seconds for Base) +3. Calls `engine.update_forkchoice()` with payload attributes +4. Waits for block construction +5. Calls `engine.get_payload()` to retrieve built payload +6. Calls `engine.new_payload()` to validate and submit +7. Calls `engine.update_forkchoice()` again to finalize + +### 2. LocalNode + +In-process Optimism node with Base Sepolia configuration. + +```rust +use base_reth_test_utils::node::LocalNode; + +#[tokio::test] +async fn test_node() -> eyre::Result<()> { + let node = LocalNode::new(default_launcher).await?; + + let provider = node.provider()?; + let engine = node.engine_api()?; + + Ok(()) +} +``` + +**Features (base):** +- Base Sepolia chain configuration +- Disabled P2P discovery (isolated testing) +- Random unused ports (parallel test safety) +- HTTP RPC server at `node.http_api_addr` +- Engine API IPC at `node.engine_ipc_path` + +For flashblocks-enabled nodes, use `FlashblocksLocalNode`: + +```rust +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. + +### 3. EngineApi + +Type-safe Engine API client wrapping raw CL operations. + +```rust +use base_reth_test_utils::engine::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::accounts::TestAccounts; +use base_reth_test_utils::harness::TestHarness; + +let accounts = TestAccounts::new(); + +let alice = &accounts.alice; +let bob = &accounts.bob; +let charlie = &accounts.charlie; +let deployer = &accounts.deployer; + +// Access via harness +let harness = TestHarness::new().await?; +let alice = &harness.accounts().alice; +``` + +**Account Details:** +- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH +- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH +- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH +- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH + +Each account includes: +- `name` - Account identifier +- `address` - Ethereum address +- `private_key` - Private key (hex string) +- `initial_balance_eth` - Starting balance in ETH + +### 5. Flashblocks Support + +Use `FlashblocksHarness` when you need `send_flashblock` and access to the in-memory pending state. + +```rust +use base_reth_test_utils::flashblocks_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 by constructing payloads and sending them through `FlashblocksHarness` (or the lower-level `FlashblocksLocalNode`). + +## Configuration Constants + +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 +``` + +## File Structure + +``` +test-utils/ +├── src/ +│ ├── lib.rs # Public API and re-exports +│ ├── accounts.rs # Test account definitions +│ ├── node.rs # LocalNode (EL wrapper) +│ ├── engine.rs # EngineApi (CL wrapper) +│ ├── harness.rs # TestHarness (orchestration) +│ └── flashblocks_harness.rs # FlashblocksHarness + helpers +├── assets/ +│ └── genesis.json # Base Sepolia genesis +└── Cargo.toml +``` + +## Usage in Other Crates + +Add to `dev-dependencies`: + +```toml +[dev-dependencies] +base-reth-test-utils.workspace = true +``` + +Import in tests: + +```rust +use base_reth_test_utils::harness::TestHarness; + +#[tokio::test] +async fn my_test() -> eyre::Result<()> { + let harness = TestHarness::new().await?; + // Your test logic + + Ok(()) +} +``` + +## Design Principles + +1. **Separation of Concerns**: LocalNode (EL), EngineApi (CL), TestHarness (orchestration) +2. **Stateless Components**: No local state tracking; always fetch from provider +3. **Type Safety**: Use reth's `OpEngineApiClient` trait instead of raw RPC strings +4. **Parallel Testing**: Random ports + isolated nodes enable concurrent tests +5. **Anvil Compatibility**: Same mnemonic as Anvil for tooling compatibility + +## Testing + +Run the test suite: + +```bash +cargo test -p base-reth-test-utils +``` + +Run specific test: + +```bash +cargo test -p base-reth-test-utils test_harness_setup +``` + +## Future Enhancements + +- Transaction builders for common operations +- Smart contract deployment helpers (Foundry integration planned) +- Snapshot/restore functionality +- Multi-node network simulation +- Performance benchmarking utilities +- Helper builder for Flashblocks + +## References + +Inspired by: +- [op-rbuilder test framework](https://github.com/flashbots/op-rbuilder/tree/main/crates/op-rbuilder/src/tests/framework) +- [reth e2e-test-utils](https://github.com/paradigmxyz/reth/tree/main/crates/e2e-test-utils) diff --git a/crates/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json new file mode 100644 index 00000000..dde20c5e --- /dev/null +++ b/crates/test-utils/assets/genesis.json @@ -0,0 +1,107 @@ +{ + "config": { + "chainId": 84532, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "mergeNetsplitBlock": 0, + "bedrockBlock": 0, + "regolithTime": 0, + "canyonTime": 0, + "ecotoneTime": 0, + "fjordTime": 0, + "graniteTime": 0, + "isthmusTime": 0, + "jovianTime": 0, + "pragueTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50 + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "0x1c9c380", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0x14dc79964da2c08b23698b3d3cc7ca32193d9955": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x1cbd3b2770909d4e10f157cabc84c7264073c9ec": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x2546bcd3c84621e976d8185a91a922ae77ecec30": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x71be63f3384f5fb98995898a86b02fb2426c5788": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x90f79bf6eb2c4f870365e785982e1f101e93b906": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x976ea74026e726554db657fa54763abd0c3a0aa9": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x9c41de96b2088cdc640c6182dfcf5491dc574a57": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xa0ee7a142d267c1f36714e4a8f75612f20a79720": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xbcd4042de499d14e55001ccbb24a551f3b954096": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xbda5747bfd65f08deb54cb465eb87d40e51b197e": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xcd3b766ccdd6ae721141f452c550ca635964ce71": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xdd2fd4581271e230360230f9337d5c0430bf44c0": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xfabb0ac9d68b0b445fb7357272ff202c5651694a": { + "balance": "0xd3c21bcecceda1000000" + } + }, + "number": "0x0" +} diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs new file mode 100644 index 00000000..d26bf6ad --- /dev/null +++ b/crates/test-utils/src/accounts.rs @@ -0,0 +1,120 @@ +//! Test accounts with pre-funded balances for integration testing + +use alloy_consensus::{SignableTransaction, TxLegacy}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, Bytes, FixedBytes, U256, address, hex}; +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 Account { + /// Account name for easy identification + pub name: &'static str, + /// Ethereum address + pub address: Address, + /// Private key (hex string without 0x prefix) + pub private_key: &'static str, +} + +impl Account { + /// 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 { + 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..20b2853a --- /dev/null +++ b/crates/test-utils/src/engine.rs @@ -0,0 +1,171 @@ +//! 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 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}, + 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 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_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 new file mode 100644 index 00000000..be656c82 --- /dev/null +++ b/crates/test-utils/src/harness.rs @@ -0,0 +1,211 @@ +//! Unified test harness combining node and engine helpers, plus optional flashblocks adapter. + +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 eyre::{Result, eyre}; +use futures_util::Future; +use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use reth::{ + builder::NodeHandle, + providers::{BlockNumReader, BlockReader, ChainSpecProvider}, +}; +use reth_e2e_test_utils::Adapter; +use reth_optimism_node::OpNode; +use reth_optimism_primitives::OpBlock; +use reth_primitives_traits::{Block as BlockT, RecoveredBlock}; +use tokio::time::sleep; + +use crate::{ + accounts::TestAccounts, + engine::{EngineApi, IpcEngine}, + node::{LocalNode, LocalNodeProvider, OpAddOns, OpBuilder, default_launcher}, + tracing::init_silenced_tracing, +}; + +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, + engine: EngineApi, + accounts: TestAccounts, +} + +impl TestHarness { + 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(launcher).await?; + Self::from_node(node).await + } + + pub(crate) async fn from_node(node: LocalNode) -> Result { + let engine = node.engine_api()?; + let accounts = TestAccounts::new(); + + sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; + + Ok(Self { node, engine, accounts }) + } + + pub fn provider(&self) -> RootProvider { + self.node.provider().expect("provider should always be available after node initialization") + } + + pub fn accounts(&self) -> &TestAccounts { + &self.accounts + } + + pub fn blockchain_provider(&self) -> LocalNodeProvider { + self.node.blockchain_provider() + } + + pub fn rpc_url(&self) -> String { + format!("http://{}", self.node.http_api_addr) + } + + 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) + .await? + .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 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, + parent_beacon_block_root: Some(parent_beacon_block_root), + withdrawals: Some(vec![]), + ..Default::default() + }, + 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() + }; + + 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 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)] +mod tests { + use alloy_primitives::U256; + use alloy_provider::Provider; + + use super::*; + #[tokio::test] + async fn test_harness_setup() -> Result<()> { + let harness = TestHarness::new().await?; + + assert_eq!(harness.accounts().alice.name, "Alice"); + assert_eq!(harness.accounts().bob.name, "Bob"); + + let provider = harness.provider(); + let chain_id = provider.get_chain_id().await?; + assert_eq!(chain_id, crate::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..2c6b290d --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,6 @@ +pub mod accounts; +pub mod engine; +pub mod flashblocks_harness; +pub mod harness; +pub mod node; +pub mod tracing; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs new file mode 100644 index 00000000..5a731bfd --- /dev/null +++ b/crates/test-utils/src/node.rs @@ -0,0 +1,400 @@ +//! Local node setup with Base Sepolia chainspec + +use std::{ + any::Any, + net::SocketAddr, + sync::{Arc, Mutex}, +}; + +use alloy_genesis::Genesis; +use alloy_provider::RootProvider; +use alloy_rpc_client::RpcClient; +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}, + args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, + builder::{ + Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, + }, + core::exit::NodeExitFuture, + tasks::TaskManager, +}; +use reth_db::{ + ClientVersion, DatabaseEnv, init_db, + mdbx::DatabaseArguments, + test_utils::{ERROR_DB_CREATION, TempDatabase, tempdir_path}, +}; +use reth_e2e_test_utils::{Adapter, TmpDB}; +use reth_exex::ExExEvent; +use reth_node_core::{ + args::DatadirArgs, + dirs::{DataDirPath, MaybePlatformPath}, +}; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_node::{OpNode, args::RollupArgs}; +use reth_provider::{CanonStateSubscriptions, providers::BlockchainProvider}; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; + +use crate::engine::EngineApi; + +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, + provider: LocalNodeProvider, + _node_exit_future: NodeExitFuture, + _node: Box, + _task_manager: TaskManager, +} + +#[derive(Clone)] +pub struct FlashblocksParts { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + 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(()) + } +} + +#[derive(Clone)] +struct FlashblocksNodeExtensions { + inner: Arc, +} + +struct FlashblocksNodeExtensionsInner { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + receiver: Arc)>>>>, + fb_cell: Arc>>, + process_canonical: bool, +} + +impl FlashblocksNodeExtensions { + fn new(process_canonical: bool) -> Self { + let (sender, receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let inner = FlashblocksNodeExtensionsInner { + sender, + receiver: Arc::new(Mutex::new(Some(receiver))), + fb_cell: Arc::new(OnceCell::new()), + process_canonical, + }; + Self { inner: Arc::new(inner) } + } + + fn apply(&self, builder: OpBuilder) -> OpBuilder { + let fb_cell = self.inner.fb_cell.clone(); + let receiver = self.inner.receiver.clone(); + let process_canonical = self.inner.process_canonical; + + let fb_cell_for_exex = fb_cell.clone(); + + builder + .install_exex("flashblocks-canon", move |mut ctx| { + let fb_cell = fb_cell_for_exex.clone(); + let process_canonical = process_canonical; + async move { + let provider = ctx.provider().clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + 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 + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb_cell = fb_cell.clone(); + let provider = ctx.provider().clone(); + let fb = init_flashblocks_state(&fb_cell, &provider); + + let mut canon_stream = tokio_stream::wrappers::BroadcastStream::new( + ctx.provider().subscribe_to_canonical_state(), + ); + tokio::spawn(async move { + use tokio_stream::StreamExt; + while let Some(Ok(notification)) = canon_stream.next().await { + provider.canonical_in_memory_state().notify_canon_state(notification); + } + }); + let api_ext = EthApiExt::new( + ctx.registry.eth_api().clone(), + ctx.registry.eth_handlers().filter.clone(), + fb.clone(), + ); + ctx.modules.replace_configured(api_ext.into_rpc())?; + + let fb_for_task = fb.clone(); + let mut receiver = receiver + .lock() + .expect("flashblock receiver mutex poisoned") + .take() + .expect("flashblock receiver should only be initialized once"); + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb_for_task.on_flashblock_received(payload); + let _ = tx.send(()); + } + }); + + Ok(()) + }) + } + + fn wrap_launcher(&self, launcher: L) -> impl FnOnce(OpBuilder) -> LRet + where + L: FnOnce(OpBuilder) -> LRet, + { + let extensions = self.clone(); + move |builder| { + let builder = extensions.apply(builder); + launcher(builder) + } + } + + fn parts(&self) -> Result { + let state = self.inner.fb_cell.get().ok_or_else(|| { + eyre::eyre!("FlashblocksState should be initialized during node launch") + })?; + Ok(FlashblocksParts { sender: self.inner.sender.clone(), state: state.clone() }) + } +} + +pub type OpTypes = + FullNodeTypesAdapter>>; +pub type OpComponentsBuilder = >::ComponentsBuilder; +pub type OpAddOns = >::AddOns; +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(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + build_node(launcher).await + } + + /// Creates a test database with a smaller map size to reduce memory usage. + /// + /// Unlike `NodeBuilder::testing_node()` which hardcodes an 8 TB map size, + /// this method configures the database with a 100 MB map size. This prevents + /// `ENOMEM` errors when running parallel tests with `cargo test`, as the + /// default 8 TB size can cause memory exhaustion when multiple test processes + /// run concurrently. + fn create_test_database() -> Result>> { + let default_size = 100 * 1024 * 1024; // 100 MB + Self::create_test_database_with_size(default_size) + } + + /// Creates a test database with a configurable map size to reduce memory usage. + /// + /// # Arguments + /// + /// * `max_size` - Maximum map size in bytes. + fn create_test_database_with_size(max_size: usize) -> Result>> { + let path = tempdir_path(); + let emsg = format!("{ERROR_DB_CREATION}: {path:?}"); + let args = + DatabaseArguments::new(ClientVersion::default()).with_geometry_max_size(Some(max_size)); + let db = init_db(&path, args).expect(&emsg); + Ok(Arc::new(TempDatabase::new(db, path))) + } + + 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()) + } + + pub fn blockchain_provider(&self) -> LocalNodeProvider { + self.provider.clone() + } +} + +async fn build_node(launcher: L) -> Result +where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, +{ + let tasks = TaskManager::current(); + let exec = tasks.executor(); + + let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json"))?; + let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); + + let network_config = NetworkArgs { + discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, + ..NetworkArgs::default() + }; + + let unique_ipc_path = format!( + "/tmp/reth_engine_api_{}_{}_{:?}.ipc", + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(), + std::process::id(), + std::thread::current().id() + ); + + let mut rpc_args = RpcServerArgs::default().with_unused_ports().with_http().with_auth_ipc(); + rpc_args.auth_ipc_path = unique_ipc_path; + + let node = OpNode::new(RollupArgs::default()); + + let temp_db = LocalNode::create_test_database()?; + let db_path = temp_db.path().to_path_buf(); + + let mut node_config = NodeConfig::new(chain_spec.clone()) + .with_network(network_config) + .with_rpc(rpc_args) + .with_unused_ports(); + + let datadir_path = MaybePlatformPath::::from(db_path.clone()); + node_config = + node_config.with_datadir_args(DatadirArgs { datadir: datadir_path, ..Default::default() }); + + let builder = NodeBuilder::new(node_config.clone()) + .with_database(temp_db) + .with_launch_context(exec.clone()) + .with_types_and_provider::>() + .with_components(node.components_builder()) + .with_add_ons(node.add_ons()); + + let NodeHandle { node: node_handle, node_exit_future } = + builder.launch_with_fn(launcher).await?; + + let http_api_addr = node_handle + .rpc_server_handle() + .http_local_addr() + .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; + + let engine_ipc_path = node_config.rpc.auth_ipc_path; + let provider = node_handle.provider().clone(); + + Ok(LocalNode { + http_api_addr, + engine_ipc_path, + provider, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + }) +} + +fn init_flashblocks_state( + cell: &Arc>>, + provider: &LocalNodeProvider, +) -> Arc { + cell.get_or_init(|| { + let fb = Arc::new(FlashblocksState::new(provider.clone(), 5)); + fb.start(); + fb + }) + .clone() +} + +pub struct FlashblocksLocalNode { + node: LocalNode, + parts: FlashblocksParts, +} + +impl FlashblocksLocalNode { + pub async fn new() -> Result { + Self::with_launcher(default_launcher).await + } + + /// Builds a flashblocks-enabled node with canonical block streaming disabled so tests can call + /// `FlashblocksState::on_canonical_block_received` at precise points. + pub async fn manual_canonical() -> Result { + Self::with_manual_canonical_launcher(default_launcher).await + } + + pub async fn with_launcher(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + 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 extensions = FlashblocksNodeExtensions::new(process_canonical); + let wrapped_launcher = extensions.wrap_launcher(launcher); + let node = LocalNode::new(wrapped_launcher).await?; + + let parts = extensions.parts()?; + Ok(Self { node, parts }) + } + + 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 + } +} diff --git a/crates/test-utils/src/tracing.rs b/crates/test-utils/src/tracing.rs new file mode 100644 index 00000000..f75b275a --- /dev/null +++ b/crates/test-utils/src/tracing.rs @@ -0,0 +1,26 @@ +use std::sync::Once; + +use tracing_subscriber::{EnvFilter, filter::LevelFilter}; + +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(); + }); +}