diff --git a/Cargo.lock b/Cargo.lock index 765ba980125..b2253f7b79e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.17" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6068f356948cd84b5ad9ac30c50478e433847f14a50714d2b68f15d052724049" +checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3abecb92ba478a285fbf5689100dbafe4003ded4a09bf4b5ef62cca87cd4f79e" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", @@ -124,6 +124,7 @@ dependencies = [ "alloy-tx-macros", "arbitrary", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -139,9 +140,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e864d4f11d1fb8d3ac2fd8f3a15f1ee46d55ec6d116b342ed1b2cb737f25894" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -154,9 +155,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c98d21aeef3e0783046c207abd3eb6cb41f6e77e0c0fc8077ebecd6df4f9d171" +checksum = "08d39c80ffc806f27a76ed42f3351a455f3dc4f81d6ff92c8aad2cf36b7d3a34" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -239,9 +240,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd2c7ae05abcab4483ce821f12f285e01c0b33804e6883dd9ca1569a87ee2be" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -265,9 +266,9 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d4291974e3564db30f1d2bcb3ba4a53dbc927e9a6fce2edaf389a712204fbd" +checksum = "527b47dc39850c6168002ddc1f7a2063e15d26137c1bb5330f6065a7524c1aa9" dependencies = [ "alloy-consensus", "alloy-eips", @@ -288,9 +289,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc47eaae86488b07ea8e20236184944072a78784a1f4993f8ec17b3aa5d08c21" +checksum = "1ba4b1be0988c11f0095a2380aa596e35533276b8fa6c9e06961bbfe0aebcac5" dependencies = [ "alloy-eips", "alloy-primitives", @@ -303,9 +304,9 @@ dependencies = [ [[package]] name = "alloy-hardforks" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e29d7eacf42f89c21d7f089916d0bdb4f36139a31698790e8837d2dbbd4b2c3" +checksum = "2d9a33550fc21fd77a3f8b63e99969d17660eec8dcc50a95a80f7c9964f7680b" dependencies = [ "alloy-chains", "alloy-eip2124", @@ -329,9 +330,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87b774478fcc616993e97659697f3e3c7988fdad598e46ee0ed11209cd0d8ee" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -344,9 +345,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d6ed73d440bae8f27771b7cd507fa8f10f19ddf0b8f67e7622a52e0dbf798e" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -370,9 +371,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219dccd2cf753a43bd9b0fbb7771a16927ffdb56e43e3a15755bef1a74d614aa" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -383,9 +384,9 @@ dependencies = [ [[package]] name = "alloy-op-evm" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab958a76714744eff19308dd42a4b72c27e7624557dbdc4dfe69ac3d5af2583c" +checksum = "6eea81517a852d9e3b03979c10febe00aacc3d50fbd34c5c30281051773285f7" dependencies = [ "alloy-consensus", "alloy-eips", @@ -401,9 +402,9 @@ dependencies = [ [[package]] name = "alloy-op-hardforks" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ac97adaba4c26e17192d81f49186ac20c1e844e35a00e169c8d3d58bc84e6b" +checksum = "f96fb2fce4024ada5b2c11d4076acf778a0d3e4f011c6dfd2ffce6d0fcf84ee9" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -426,8 +427,8 @@ dependencies = [ "derive_more", "foldhash 0.2.0", "getrandom 0.3.4", - "hashbrown 0.16.0", - "indexmap 2.12.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -444,9 +445,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ef8cbc2b68e2512acf04b2d296c05c98a661bc460462add6414528f4ff3d9b" +checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -489,9 +490,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be028fb1c6c173f5765d0baa3580a11d69826ea89fe00ee5c9d7eddb2c3509cd" +checksum = "cdd4c64eb250a18101d22ae622357c6b505e158e9165d4c7974d59082a600c5e" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -528,14 +529,14 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "alloy-rpc-client" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0f67d1e655ed93efca217213340d21cce982333cc44a1d918af9150952ef66" +checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -559,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe106e50522980bc9e7cc9016f445531edf1a53e0fdba904c833b98c6fdff3f0" +checksum = "39cf1398cb33aacb139a960fa3d8cf8b1202079f320e77e952a0b95967bf7a9f" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", @@ -572,9 +573,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-admin" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b67bf1ed8cac6fde7dd017ca0a1c33be846e613a265956089f983af1354f13" +checksum = "65a583d2029b171301f5dcf122aa2ef443a65a373778ec76540d999691ae867d" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -584,9 +585,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cf94d581b3aa13ebacb90ea52e0179985b7c20d8a522319e7d40768d56667a" +checksum = "c3ce4c24e416bd0f17fceeb2f26cd8668df08fe19e1dc02f9d41c3b8ed1e93e0" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -596,9 +597,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "425e14ee32eb8b7edd6a2247fe0ed640785e6eba75af27db27f1e6220c15ef0d" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -607,9 +608,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440655ffd9ff8724fa76a07c7dbe18cb4353617215c23e3921163516b6c07ff8" +checksum = "16633087e23d8d75161c3a59aa183203637b817a5a8d2f662f612ccb6d129af0" dependencies = [ "alloy-eips", "alloy-primitives", @@ -627,9 +628,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69c12784cdf1059936249a6e705ec03bf8cea1a12181ed5cea9ca2be9cca684" +checksum = "4936f579d9d10eae01772b2ab3497f9d568684f05f26f8175e12f9a1a2babc33" dependencies = [ "alloy-primitives", "derive_more", @@ -639,9 +640,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabc17f0eac3f747eeddebc768c8e30763d6f6c53188f5335a935dedc57ddfbd" +checksum = "4c60bdce3be295924122732b7ecd0b2495ce4790bedc5370ca7019c08ad3f26e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -660,9 +661,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0185f68a0f8391ab996d335a887087d7ccdbc97952efab3516f6307d456ba2cd" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -682,9 +683,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-mev" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c89422163337ff64d9aaa13f3e4df53d60d789004044cd64ebc7dc4d5765a64" +checksum = "81c0dd81c24944cfbf45b5df7cd149d9cd3e354db81ccf08aa47e0e05be8ab97" dependencies = [ "alloy-consensus", "alloy-eips", @@ -697,9 +698,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d31a6766c8f91d18d07a36b57f55efd981752df619d30b395a92332a8b28ea05" +checksum = "ef206a4b8d436fbb7cf2e6a61c692d11df78f9382becc3c9a283bd58e64f0583" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -711,9 +712,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c208cbe2ea28368c3f61bd1e27b14238b7b03796e90370de3c0d8722e0f9830" +checksum = "ecb5a795264a02222f9534435b8f40dcbd88de8e9d586647884aae24f389ebf2" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -723,9 +724,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f180c399ca7c1e2fe17ea58343910cad0090878a696ff5a50241aee12fc529" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "arbitrary", @@ -735,9 +736,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f06333680d04370c8ed3a6b0eccff384e422c3d8e6b19e61fedc3a9f0ab7743" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives", "async-trait", @@ -750,9 +751,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590dcaeb290cdce23155e68af4791d093afc3754b1a331198a25d2d44c5456e8" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -778,7 +779,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -790,11 +791,11 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.12.0", + "indexmap 2.12.1", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "syn-solidity", "tiny-keccak", ] @@ -811,7 +812,7 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "syn-solidity", ] @@ -839,9 +840,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55bbdcee53e4e3857b5ddbc2986ebe9c2ab5f352ec285cb0da04c1e8f2ca9c18" +checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -862,9 +863,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793967215109b4a334047c810ed6db5e873ad3ea07f65cc02202bd4b810d9615" +checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -877,9 +878,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e182e5ae0c4858bb87df23ebfe31018d7e51fe1a264b8a8a2b26932cb04861" +checksum = "d8db249779ebc20dc265920c7e706ed0d31dbde8627818d1cbde60919b875bb0" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -897,9 +898,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e9dc891c80d6216003d4b04f0a7463015d0873d36e4ac2ec0bcc9196aa4ea7" +checksum = "5ad2344a12398d7105e3722c9b7a7044ea837128e11d453604dec6e3731a86e2" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -935,14 +936,14 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.42" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab54221eccefa254ce9f65b079c097b1796e48c21c7ce358230f8988d75392fb" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -992,22 +993,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]] @@ -1027,7 +1028,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1169,7 +1170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1207,7 +1208,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1296,7 +1297,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1369,9 +1370,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.32" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" dependencies = [ "compression-codecs", "compression-core", @@ -1413,7 +1414,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1424,7 +1425,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1462,7 +1463,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1529,9 +1530,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bech32" @@ -1589,7 +1590,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1607,7 +1608,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1627,15 +1628,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", @@ -1719,7 +1720,7 @@ dependencies = [ "boa_interner", "boa_macros", "boa_string", - "indexmap 2.12.0", + "indexmap 2.12.1", "num-bigint", "rustc-hash", ] @@ -1749,9 +1750,9 @@ dependencies = [ "futures-channel", "futures-concurrency", "futures-lite 2.6.1", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "icu_normalizer", - "indexmap 2.12.0", + "indexmap 2.12.1", "intrusive-collections", "itertools 0.14.0", "num-bigint", @@ -1784,7 +1785,7 @@ checksum = "f1179f690cbfcbe5364cceee5f1cb577265bb6f07b0be6f210aabe270adcf9da" dependencies = [ "boa_macros", "boa_string", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "thin-vec", ] @@ -1796,8 +1797,8 @@ checksum = "9626505d33dc63d349662437297df1d3afd9d5fc4a2b3ad34e5e1ce879a78848" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.16.0", - "indexmap 2.12.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "once_cell", "phf", "rustc-hash", @@ -1814,7 +1815,7 @@ dependencies = [ "cow-utils", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] @@ -1852,9 +1853,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1862,22 +1863,22 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "boyer-moore-magiclen" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e6233f2d926b5b123caf9d58e3885885255567fbe7776a7fdcae2a4d7241c4" +checksum = "7441b4796eb8a7107d4cd99d829810be75f5573e1081c37faa0e8094169ea0d6" dependencies = [ "debug-helper", ] @@ -1959,7 +1960,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1970,9 +1971,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", ] @@ -2161,9 +2162,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", @@ -2171,9 +2172,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", @@ -2190,7 +2191,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2367,9 +2368,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.31" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" dependencies = [ "brotli", "compression-core", @@ -2381,9 +2382,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "concat-kdf" @@ -2461,9 +2462,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -2520,9 +2521,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -2651,9 +2652,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -2725,7 +2726,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2772,7 +2773,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2787,7 +2788,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2798,7 +2799,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2809,7 +2810,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2862,7 +2863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2921,7 +2922,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2932,7 +2933,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2953,7 +2954,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2963,28 +2964,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.108", + "rustc_version 0.4.1", + "syn 2.0.111", "unicode-xid", ] @@ -3116,7 +3118,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3163,7 +3165,7 @@ checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3215,7 +3217,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3323,7 +3325,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3343,7 +3345,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3363,7 +3365,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -3439,7 +3441,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4144,7 +4146,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4268,9 +4270,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ "bitflags 2.10.0", "libc", @@ -4364,7 +4366,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -4422,14 +4424,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -4471,9 +4474,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -4546,12 +4549,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -4640,9 +4642,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -4695,9 +4697,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -4884,7 +4886,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4925,13 +4927,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -4983,15 +4985,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ "darling 0.20.11", "indoc", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5047,9 +5049,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -5139,9 +5141,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -5251,7 +5253,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5396,15 +5398,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -5430,9 +5432,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libp2p-identity" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "asn1_der", "bs58", @@ -5471,9 +5473,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -5489,12 +5491,12 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linked_hash_set" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae85b5be22d9843c80e5fc80e9b64c8a3b1f98f867c709956eca3efff4e92e2" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" dependencies = [ "linked-hash-map", - "serde", + "serde_core", ] [[package]] @@ -5533,9 +5535,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -5547,7 +5549,7 @@ dependencies = [ "generator", "scoped-tls", "tracing", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", ] [[package]] @@ -5616,7 +5618,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5665,9 +5667,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" dependencies = [ "ahash", "portable-atomic", @@ -5682,7 +5684,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5692,7 +5694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "metrics", "metrics-util", "quanta", @@ -5724,7 +5726,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.15.5", - "indexmap 2.12.0", + "indexmap 2.12.1", "metrics", "ordered-float", "quanta", @@ -5803,9 +5805,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", @@ -6085,7 +6087,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6151,9 +6153,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "op-alloy-consensus" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42e9de945efe3c2fbd207e69720c9c1af2b8caa6872aee0e216450c25a3ca70" +checksum = "726da827358a547be9f1e37c2a756b9e3729cb0350f43408164794b370cad8ae" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6177,9 +6179,9 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9da49a2812a0189dd05e81e4418c3ae13fd607a92654107f02ebad8e91ed9e" +checksum = "f63f27e65be273ec8fcb0b6af0fd850b550979465ab93423705ceb3dfddbd2ab" dependencies = [ "alloy-consensus", "alloy-network", @@ -6193,9 +6195,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-jsonrpsee" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ceb771ab9323647093ea2e58dc7f25289a1b95cbef2faa2620f6ca2dee4d9" +checksum = "8ef9114426b16172254555aad34a8ea96c01895e40da92f5d12ea680a1baeaa7" dependencies = [ "alloy-primitives", "jsonrpsee", @@ -6203,9 +6205,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd1eb7bddd2232856ba9d259320a094f9edf2b9061acfe5966e7960208393e6" +checksum = "562dd4462562c41f9fdc4d860858c40e14a25df7f983ae82047f15f08fce4d19" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6223,9 +6225,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5429622150d18d8e6847a701135082622413e2451b64d03f979415d764566bef" +checksum = "d8f24b8cb66e4b33e6c9e508bf46b8ecafc92eadd0b93fedd306c0accb477657" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6428,7 +6430,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6494,9 +6496,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", @@ -6543,7 +6545,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6572,7 +6574,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6699,7 +6701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6728,7 +6730,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.9", ] [[package]] @@ -6750,7 +6752,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6845,7 +6847,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6856,7 +6858,7 @@ checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6879,7 +6881,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6980,9 +6982,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -7224,7 +7226,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -7262,7 +7264,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", "memchr", ] @@ -7274,9 +7276,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -7317,9 +7319,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" @@ -7663,7 +7665,7 @@ dependencies = [ "proc-macro2", "quote", "similar-asserts", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -9451,19 +9453,19 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rpc-types-engine", - "alloy-serde", "brotli", "derive_more", "eyre", "futures-util", "metrics", + "op-alloy-consensus", + "op-alloy-rpc-types-engine", "reth-chain-state", "reth-engine-primitives", "reth-errors", "reth-evm", "reth-execution-types", "reth-metrics", - "reth-optimism-evm", "reth-optimism-payload-builder", "reth-optimism-primitives", "reth-payload-primitives", @@ -9473,7 +9475,6 @@ dependencies = [ "reth-storage-api", "reth-tasks", "ringbuffer", - "serde", "serde_json", "test-case", "tokio", @@ -10695,7 +10696,7 @@ dependencies = [ "tracing-appender", "tracing-journald", "tracing-logfmt", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", "url", ] @@ -10711,7 +10712,7 @@ dependencies = [ "opentelemetry_sdk", "tracing", "tracing-opentelemetry", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", "url", ] @@ -11019,9 +11020,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", @@ -11294,7 +11295,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.108", + "syn 2.0.111", "unicode-ident", ] @@ -11421,9 +11422,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -11448,9 +11449,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -11556,9 +11557,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -11759,7 +11760,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -11768,7 +11769,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "memchr", "ryu", @@ -11810,17 +11811,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -11829,14 +11830,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -11938,9 +11939,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -11957,9 +11958,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" @@ -12169,7 +12170,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12181,7 +12182,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12203,9 +12204,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -12221,7 +12222,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12241,7 +12242,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12328,7 +12329,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12339,15 +12340,15 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "test-case-core", ] [[package]] name = "test-fuzz" -version = "7.2.4" +version = "7.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6696b1bcee3edb0553566f632c31b3b18fda42cf4d529327ca47f230c4acd3ab" +checksum = "11e5c77910b1d5b469a342be541cf44933f0ad2c4b8d5acb32ee46697fd60546" dependencies = [ "serde", "serde_combinators", @@ -12358,9 +12359,9 @@ dependencies = [ [[package]] name = "test-fuzz-internal" -version = "7.2.4" +version = "7.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5988511fdb342582013a17a4263e994bce92828a1bae039f92a2f05a5f95ce78" +checksum = "4d25f2f0ee315b130411a98570dd128dfe344bfaa0a28bf33d38f4a1fe85f39b" dependencies = [ "bincode 2.0.1", "cargo_metadata 0.19.2", @@ -12369,9 +12370,9 @@ dependencies = [ [[package]] name = "test-fuzz-macro" -version = "7.2.4" +version = "7.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8893e583c5af79a67761a9285535d26612cb1617fcbf388c3abc0c1d35a0b89" +checksum = "b8c03ba0a9e3e4032f94d71c85e149af147843c6f212e4ca4383542d606b04a6" dependencies = [ "darling 0.21.3", "heck", @@ -12379,14 +12380,14 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "test-fuzz-runtime" -version = "7.2.4" +version = "7.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be06afdb9cb50c76ef938e2e4bda2e28e1cbb4d3d305603d57a5e374a6d6e7" +checksum = "f9a4ac481aa983d386e857a7be0006c2f0ef26e0c5326bbc7262f73c2891b91d" dependencies = [ "hex", "num-traits", @@ -12427,7 +12428,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12438,7 +12439,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12594,7 +12595,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -12638,9 +12639,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -12687,7 +12688,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -12697,11 +12698,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", "winnow", @@ -12768,7 +12769,7 @@ dependencies = [ "futures-core", "futures-util", "hdrhistogram", - "indexmap 2.12.0", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper", @@ -12781,9 +12782,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", "base64 0.22.1", @@ -12824,9 +12825,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -12836,32 +12837,32 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -12879,13 +12880,13 @@ dependencies = [ [[package]] name = "tracing-journald" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657" +checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0" dependencies = [ "libc", "tracing-core", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", ] [[package]] @@ -12908,7 +12909,7 @@ dependencies = [ "time", "tracing", "tracing-core", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", ] [[package]] @@ -12926,7 +12927,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", "web-time", ] @@ -12951,9 +12952,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -12972,9 +12973,9 @@ dependencies = [ [[package]] name = "tracy-client" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef54005d3d760186fd662dad4b7bb27ecd5531cdef54d1573ebd3f20a9205ed7" +checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" dependencies = [ "loom", "once_cell", @@ -12984,9 +12985,9 @@ dependencies = [ [[package]] name = "tracy-client-sys" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319c70195101a93f56db4c74733e272d720768e13471f400c78406a326b172b0" +checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" dependencies = [ "cc", "windows-targets 0.52.6", @@ -13014,7 +13015,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -13213,9 +13214,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -13295,7 +13296,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -13355,9 +13356,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -13368,9 +13369,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -13381,9 +13382,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -13391,22 +13392,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -13440,9 +13441,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -13652,7 +13653,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -13663,7 +13664,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -13674,7 +13675,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -13685,7 +13686,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -14082,9 +14083,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -14186,28 +14187,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -14227,7 +14228,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] @@ -14248,7 +14249,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -14282,7 +14283,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9ea5090314b..77be8e4a178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -523,11 +523,11 @@ alloy-transport-ws = { version = "1.0.41", default-features = false } # op alloy-op-evm = { version = "0.23.0", default-features = false } alloy-op-hardforks = "0.4.4" -op-alloy-rpc-types = { version = "0.22.0", default-features = false } -op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false } -op-alloy-network = { version = "0.22.0", default-features = false } -op-alloy-consensus = { version = "0.22.0", default-features = false } -op-alloy-rpc-jsonrpsee = { version = "0.22.0", default-features = false } +op-alloy-rpc-types = { version = "0.22.1", default-features = false } +op-alloy-rpc-types-engine = { version = "0.22.1", default-features = false } +op-alloy-network = { version = "0.22.1", default-features = false } +op-alloy-consensus = { version = "0.22.1", default-features = false } +op-alloy-rpc-jsonrpsee = { version = "0.22.1", default-features = false } op-alloy-flz = { version = "0.13.1", default-features = false } # misc diff --git a/crates/node/builder/src/rpc.rs b/crates/node/builder/src/rpc.rs index a66d7b222e4..a9c9640e3ca 100644 --- a/crates/node/builder/src/rpc.rs +++ b/crates/node/builder/src/rpc.rs @@ -973,7 +973,12 @@ where ); let eth_config = config.rpc.eth_config().max_batch_size(config.txpool.max_batch_size()); - let ctx = EthApiCtx { components: &node, config: eth_config, cache }; + let ctx = EthApiCtx { + components: &node, + config: eth_config, + cache, + engine_handle: beacon_engine_handle.clone(), + }; let eth_api = eth_api_builder.build_eth_api(ctx).await?; let auth_config = config.rpc.auth_server_config(jwt_secret)?; @@ -1137,6 +1142,8 @@ pub struct EthApiCtx<'a, N: FullNodeTypes> { pub config: EthConfig, /// Cache for eth state pub cache: EthStateCache>, + /// Handle to the beacon consensus engine + pub engine_handle: ConsensusEngineHandle<::Payload>, } impl<'a, N: FullNodeComponents>> diff --git a/crates/optimism/evm/src/config.rs b/crates/optimism/evm/src/config.rs index 6ae2a91a6cb..1f1068c40d9 100644 --- a/crates/optimism/evm/src/config.rs +++ b/crates/optimism/evm/src/config.rs @@ -1,6 +1,7 @@ pub use alloy_op_evm::{ spec as revm_spec, spec_by_timestamp_after_bedrock as revm_spec_by_timestamp_after_bedrock, }; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use revm::primitives::{Address, Bytes, B256}; /// Context relevant for execution of a next block w.r.t OP. @@ -35,3 +36,16 @@ impl reth_rpc_eth_api::helpers::pending_block:: } } } + +impl From for OpNextBlockEnvAttributes { + fn from(base: OpFlashblockPayloadBase) -> Self { + Self { + timestamp: base.timestamp, + suggested_fee_recipient: base.fee_recipient, + prev_randao: base.prev_randao, + gas_limit: base.gas_limit, + parent_beacon_block_root: Some(base.parent_beacon_block_root), + extra_data: base.extra_data, + } + } +} diff --git a/crates/optimism/flashblocks/Cargo.toml b/crates/optimism/flashblocks/Cargo.toml index 977e28d37e1..e0754aab95e 100644 --- a/crates/optimism/flashblocks/Cargo.toml +++ b/crates/optimism/flashblocks/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] # reth reth-optimism-primitives = { workspace = true, features = ["serde"] } -reth-optimism-evm.workspace = true reth-chain-state = { workspace = true, features = ["serde"] } reth-primitives-traits = { workspace = true, features = ["serde"] } reth-engine-primitives = { workspace = true, features = ["std"] } @@ -30,15 +29,16 @@ reth-metrics.workspace = true # alloy alloy-eips = { workspace = true, features = ["serde"] } -alloy-serde.workspace = true alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types-engine = { workspace = true, features = ["serde"] } alloy-consensus.workspace = true +# op-alloy +op-alloy-rpc-types-engine = { workspace = true, features = ["k256"] } + # io tokio.workspace = true tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] } -serde.workspace = true serde_json.workspace = true url.workspace = true futures-util.workspace = true @@ -57,3 +57,4 @@ derive_more.workspace = true [dev-dependencies] test-case.workspace = true alloy-consensus.workspace = true +op-alloy-consensus.workspace = true diff --git a/crates/optimism/flashblocks/src/cache.rs b/crates/optimism/flashblocks/src/cache.rs new file mode 100644 index 00000000000..9aeed3435e3 --- /dev/null +++ b/crates/optimism/flashblocks/src/cache.rs @@ -0,0 +1,482 @@ +//! Sequence cache management for flashblocks. +//! +//! The `SequenceManager` maintains a ring buffer of recently completed flashblock sequences +//! and intelligently selects which sequence to build based on the local chain tip. + +use crate::{ + sequence::{FlashBlockPendingSequence, SequenceExecutionOutcome}, + worker::BuildArgs, + FlashBlock, FlashBlockCompleteSequence, PendingFlashBlock, +}; +use alloy_eips::eip2718::WithEncoded; +use alloy_primitives::B256; +use reth_primitives_traits::{NodePrimitives, Recovered, SignedTransaction}; +use reth_revm::cached::CachedReads; +use ringbuffer::{AllocRingBuffer, RingBuffer}; +use tokio::sync::broadcast; +use tracing::*; + +/// Maximum number of cached sequences in the ring buffer. +const CACHE_SIZE: usize = 3; +/// 200 ms flashblock time. +pub(crate) const FLASHBLOCK_BLOCK_TIME: u64 = 200; + +/// Manages flashblock sequences with caching support. +/// +/// This struct handles: +/// - Tracking the current pending sequence +/// - Caching completed sequences in a fixed-size ring buffer +/// - Finding the best sequence to build based on local chain tip +/// - Broadcasting completed sequences to subscribers +#[derive(Debug)] +pub(crate) struct SequenceManager { + /// Current pending sequence being built up from incoming flashblocks + pending: FlashBlockPendingSequence, + /// Cached recovered transactions for the pending sequence + pending_transactions: Vec>>, + /// Ring buffer of recently completed sequences bundled with their decoded transactions (FIFO, + /// size 3) + completed_cache: AllocRingBuffer<(FlashBlockCompleteSequence, Vec>>)>, + /// Broadcast channel for completed sequences + block_broadcaster: broadcast::Sender, + /// Whether to compute state roots when building blocks + compute_state_root: bool, +} + +impl SequenceManager { + /// Creates a new sequence manager. + pub(crate) fn new(compute_state_root: bool) -> Self { + let (block_broadcaster, _) = broadcast::channel(128); + Self { + pending: FlashBlockPendingSequence::new(), + pending_transactions: Vec::new(), + completed_cache: AllocRingBuffer::new(CACHE_SIZE), + block_broadcaster, + compute_state_root, + } + } + + /// Returns the sender half of the flashblock sequence broadcast channel. + pub(crate) const fn block_sequence_broadcaster( + &self, + ) -> &broadcast::Sender { + &self.block_broadcaster + } + + /// Gets a subscriber to the flashblock sequences produced. + pub(crate) fn subscribe_block_sequence(&self) -> crate::FlashBlockCompleteSequenceRx { + self.block_broadcaster.subscribe() + } + + /// Inserts a new flashblock into the pending sequence. + /// + /// When a flashblock with index 0 arrives (indicating a new block), the current + /// pending sequence is finalized, cached, and broadcast immediately. If the sequence + /// is later built on top of local tip, `on_build_complete()` will broadcast again + /// with computed `state_root`. + /// + /// Transactions are recovered once and cached for reuse during block building. + pub(crate) fn insert_flashblock(&mut self, flashblock: FlashBlock) -> eyre::Result<()> { + // If this starts a new block, finalize and cache the previous sequence BEFORE inserting + if flashblock.index == 0 && self.pending.count() > 0 { + let completed = self.pending.finalize()?; + let block_number = completed.block_number(); + let parent_hash = completed.payload_base().parent_hash; + + trace!( + target: "flashblocks", + block_number, + %parent_hash, + cache_size = self.completed_cache.len(), + "Caching completed flashblock sequence" + ); + + // Broadcast immediately to consensus client (even without state_root) + // This ensures sequences are forwarded during catch-up even if not buildable on tip. + // ConsensusClient checks execution_outcome and skips newPayload if state_root is zero. + if self.block_broadcaster.receiver_count() > 0 { + let _ = self.block_broadcaster.send(completed.clone()); + } + + // Bundle completed sequence with its decoded transactions and push to cache + // Ring buffer automatically evicts oldest entry when full + let txs = std::mem::take(&mut self.pending_transactions); + self.completed_cache.push((completed, txs)); + + // ensure cache is wiped on new flashblock + let _ = self.pending.take_cached_reads(); + } + + self.pending_transactions + .extend(flashblock.recover_transactions().collect::, _>>()?); + self.pending.insert(flashblock); + Ok(()) + } + + /// Returns the current pending sequence for inspection. + pub(crate) const fn pending(&self) -> &FlashBlockPendingSequence { + &self.pending + } + + /// Finds the next sequence to build and returns ready-to-use `BuildArgs`. + /// + /// Priority order: + /// 1. Current pending sequence (if parent matches local tip) + /// 2. Cached sequence with exact parent match + /// + /// Returns None if nothing is buildable right now. + pub(crate) fn next_buildable_args( + &mut self, + local_tip_hash: B256, + local_tip_timestamp: u64, + ) -> Option>>>> { + // Try to find a buildable sequence: (base, last_fb, transactions, cached_state, + // source_name) + let (base, last_flashblock, transactions, cached_state, source_name) = + // Priority 1: Try current pending sequence + if let Some(base) = self.pending.payload_base().filter(|b| b.parent_hash == local_tip_hash) { + let cached_state = self.pending.take_cached_reads().map(|r| (base.parent_hash, r)); + let last_fb = self.pending.last_flashblock()?; + let transactions = self.pending_transactions.clone(); + (base, last_fb, transactions, cached_state, "pending") + } + // Priority 2: Try cached sequence with exact parent match + else if let Some((cached, txs)) = self.completed_cache.iter().find(|(c, _)| c.payload_base().parent_hash == local_tip_hash) { + let base = cached.payload_base().clone(); + let last_fb = cached.last(); + let transactions = txs.clone(); + let cached_state = None; + (base, last_fb, transactions, cached_state, "cached") + } else { + return None; + }; + + // Auto-detect when to compute state root: only if the builder didn't provide it (sent + // B256::ZERO) and we're near the expected final flashblock index. + // + // Background: Each block period receives multiple flashblocks at regular intervals. + // The sequencer sends an initial "base" flashblock at index 0 when a new block starts, + // then subsequent flashblocks are produced every FLASHBLOCK_BLOCK_TIME intervals (200ms). + // + // Examples with different block times: + // - Base (2s blocks): expect 2000ms / 200ms = 10 intervals → Flashblocks: index 0 (base) + // + indices 1-10 = potentially 11 total + // + // - Unichain (1s blocks): expect 1000ms / 200ms = 5 intervals → Flashblocks: index 0 (base) + // + indices 1-5 = potentially 6 total + // + // Why compute at N-1 instead of N: + // 1. Timing variance in flashblock producing time may mean only N flashblocks were produced + // instead of N+1 (missing the final one). Computing at N-1 ensures we get the state root + // for most common cases. + // + // 2. The +1 case (index 0 base + N intervals): If all N+1 flashblocks do arrive, we'll + // still calculate state root for flashblock N, which sacrifices a little performance but + // still ensures correctness for common cases. + // + // Note: Pathological cases may result in fewer flashblocks than expected (e.g., builder + // downtime, flashblock execution exceeding timing budget). When this occurs, we won't + // compute the state root, causing FlashblockConsensusClient to lack precomputed state for + // engine_newPayload. This is safe: we still have op-node as backstop to maintain + // chain progression. + let block_time_ms = (base.timestamp - local_tip_timestamp) * 1000; + let expected_final_flashblock = block_time_ms / FLASHBLOCK_BLOCK_TIME; + let compute_state_root = self.compute_state_root && + last_flashblock.diff.state_root.is_zero() && + last_flashblock.index >= expected_final_flashblock.saturating_sub(1); + + trace!( + target: "flashblocks", + block_number = base.block_number, + source = source_name, + flashblock_index = last_flashblock.index, + expected_final_flashblock, + compute_state_root_enabled = self.compute_state_root, + state_root_is_zero = last_flashblock.diff.state_root.is_zero(), + will_compute_state_root = compute_state_root, + "Building from flashblock sequence" + ); + + Some(BuildArgs { + base, + transactions, + cached_state, + last_flashblock_index: last_flashblock.index, + last_flashblock_hash: last_flashblock.diff.block_hash, + compute_state_root, + }) + } + + /// Records the result of building a sequence and re-broadcasts with execution outcome. + /// + /// Updates execution outcome and cached reads. For cached sequences (already broadcast + /// once during finalize), this broadcasts again with the computed `state_root`, allowing + /// the consensus client to submit via `engine_newPayload`. + pub(crate) fn on_build_complete( + &mut self, + parent_hash: B256, + result: Option<(PendingFlashBlock, CachedReads)>, + ) { + let Some((computed_block, cached_reads)) = result else { + return; + }; + + // Extract execution outcome + let execution_outcome = computed_block.computed_state_root().map(|state_root| { + SequenceExecutionOutcome { block_hash: computed_block.block().hash(), state_root } + }); + + // Update pending sequence with execution results + if self.pending.payload_base().is_some_and(|base| base.parent_hash == parent_hash) { + self.pending.set_execution_outcome(execution_outcome); + self.pending.set_cached_reads(cached_reads); + trace!( + target: "flashblocks", + block_number = self.pending.block_number(), + has_computed_state_root = execution_outcome.is_some(), + "Updated pending sequence with build results" + ); + } + // Check if this completed sequence in cache and broadcast with execution outcome + else if let Some((cached, _)) = self + .completed_cache + .iter_mut() + .find(|(c, _)| c.payload_base().parent_hash == parent_hash) + { + // Only re-broadcast if we computed new information (state_root was missing). + // If sequencer already provided state_root, we already broadcast in insert_flashblock, + // so skip re-broadcast to avoid duplicate FCU calls. + let needs_rebroadcast = + execution_outcome.is_some() && cached.execution_outcome().is_none(); + + cached.set_execution_outcome(execution_outcome); + + if needs_rebroadcast && self.block_broadcaster.receiver_count() > 0 { + trace!( + target: "flashblocks", + block_number = cached.block_number(), + "Re-broadcasting sequence with computed state_root" + ); + let _ = self.block_broadcaster.send(cached.clone()); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestFlashBlockFactory; + use alloy_primitives::B256; + use op_alloy_consensus::OpTxEnvelope; + + #[test] + fn test_sequence_manager_new() { + let manager: SequenceManager = SequenceManager::new(true); + assert_eq!(manager.pending().count(), 0); + } + + #[test] + fn test_insert_flashblock_creates_pending_sequence() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + manager.insert_flashblock(fb0).unwrap(); + + assert_eq!(manager.pending().count(), 1); + assert_eq!(manager.pending().block_number(), Some(100)); + } + + #[test] + fn test_insert_flashblock_caches_completed_sequence() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + // Build first sequence + let fb0 = factory.flashblock_at(0).build(); + manager.insert_flashblock(fb0.clone()).unwrap(); + + let fb1 = factory.flashblock_after(&fb0).build(); + manager.insert_flashblock(fb1).unwrap(); + + // Insert new base (index 0) which should finalize and cache previous sequence + let fb2 = factory.flashblock_for_next_block(&fb0).build(); + manager.insert_flashblock(fb2).unwrap(); + + // New sequence should be pending + assert_eq!(manager.pending().count(), 1); + assert_eq!(manager.pending().block_number(), Some(101)); + assert_eq!(manager.completed_cache.len(), 1); + let (cached_sequence, _txs) = manager.completed_cache.get(0).unwrap(); + assert_eq!(cached_sequence.block_number(), 100); + } + + #[test] + fn test_next_buildable_args_returns_none_when_empty() { + let mut manager: SequenceManager = SequenceManager::new(true); + let local_tip_hash = B256::random(); + let local_tip_timestamp = 1000; + + let args = manager.next_buildable_args(local_tip_hash, local_tip_timestamp); + assert!(args.is_none()); + } + + #[test] + fn test_next_buildable_args_matches_pending_parent() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + manager.insert_flashblock(fb0).unwrap(); + + let args = manager.next_buildable_args(parent_hash, 1000000); + assert!(args.is_some()); + + let build_args = args.unwrap(); + assert_eq!(build_args.last_flashblock_index, 0); + } + + #[test] + fn test_next_buildable_args_returns_none_when_parent_mismatch() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + manager.insert_flashblock(fb0).unwrap(); + + // Use different parent hash + let wrong_parent = B256::random(); + let args = manager.next_buildable_args(wrong_parent, 1000000); + assert!(args.is_none()); + } + + #[test] + fn test_next_buildable_args_prefers_pending_over_cached() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + // Create and finalize first sequence + let fb0 = factory.flashblock_at(0).build(); + manager.insert_flashblock(fb0.clone()).unwrap(); + + // Create new sequence (finalizes previous) + let fb1 = factory.flashblock_for_next_block(&fb0).build(); + let parent_hash = fb1.base.as_ref().unwrap().parent_hash; + manager.insert_flashblock(fb1).unwrap(); + + // Request with first sequence's parent (should find cached) + let args = manager.next_buildable_args(parent_hash, 1000000); + assert!(args.is_some()); + } + + #[test] + fn test_next_buildable_args_finds_cached_sequence() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + // Build and cache first sequence + let fb0 = factory.flashblock_at(0).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + manager.insert_flashblock(fb0.clone()).unwrap(); + + // Start new sequence to finalize first + let fb1 = factory.flashblock_for_next_block(&fb0).build(); + manager.insert_flashblock(fb1.clone()).unwrap(); + + // Clear pending by starting another sequence + let fb2 = factory.flashblock_for_next_block(&fb1).build(); + manager.insert_flashblock(fb2).unwrap(); + + // Request first sequence's parent - should find in cache + let args = manager.next_buildable_args(parent_hash, 1000000); + assert!(args.is_some()); + } + + #[test] + fn test_compute_state_root_logic_near_expected_final() { + let mut manager: SequenceManager = SequenceManager::new(true); + let block_time = 2u64; + let factory = TestFlashBlockFactory::new().with_block_time(block_time); + + // Create sequence with zero state root (needs computation) + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + let base_timestamp = fb0.base.as_ref().unwrap().timestamp; + manager.insert_flashblock(fb0.clone()).unwrap(); + + // Add flashblocks up to expected final index (2000ms / 200ms = 10) + for i in 1..=9 { + let fb = factory.flashblock_after(&fb0).index(i).state_root(B256::ZERO).build(); + manager.insert_flashblock(fb).unwrap(); + } + + // Request with proper timing - should compute state root for index 9 + let args = manager.next_buildable_args(parent_hash, base_timestamp - block_time); + assert!(args.is_some()); + assert!(args.unwrap().compute_state_root); + } + + #[test] + fn test_no_compute_state_root_when_provided_by_sequencer() { + let mut manager: SequenceManager = SequenceManager::new(true); + let block_time = 2u64; + let factory = TestFlashBlockFactory::new().with_block_time(block_time); + + // Create sequence with non-zero state root (provided by sequencer) + let fb0 = factory.flashblock_at(0).state_root(B256::random()).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + let base_timestamp = fb0.base.as_ref().unwrap().timestamp; + manager.insert_flashblock(fb0).unwrap(); + + let args = manager.next_buildable_args(parent_hash, base_timestamp - block_time); + assert!(args.is_some()); + assert!(!args.unwrap().compute_state_root); + } + + #[test] + fn test_no_compute_state_root_when_disabled() { + let mut manager: SequenceManager = SequenceManager::new(false); + let block_time = 2u64; + let factory = TestFlashBlockFactory::new().with_block_time(block_time); + + // Create sequence with zero state root (needs computation) + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + let base_timestamp = fb0.base.as_ref().unwrap().timestamp; + manager.insert_flashblock(fb0.clone()).unwrap(); + + // Add flashblocks up to expected final index (2000ms / 200ms = 10) + for i in 1..=9 { + let fb = factory.flashblock_after(&fb0).index(i).state_root(B256::ZERO).build(); + manager.insert_flashblock(fb).unwrap(); + } + + // Request with proper timing - should compute state root for index 9 + let args = manager.next_buildable_args(parent_hash, base_timestamp - block_time); + assert!(args.is_some()); + assert!(!args.unwrap().compute_state_root); + } + + #[test] + fn test_cache_ring_buffer_evicts_oldest() { + let mut manager: SequenceManager = SequenceManager::new(true); + let factory = TestFlashBlockFactory::new(); + + // Fill cache with 4 sequences (cache size is 3, so oldest should be evicted) + let mut last_fb = factory.flashblock_at(0).build(); + manager.insert_flashblock(last_fb.clone()).unwrap(); + + for _ in 0..3 { + last_fb = factory.flashblock_for_next_block(&last_fb).build(); + manager.insert_flashblock(last_fb.clone()).unwrap(); + } + + // The first sequence should have been evicted, so we can't build it + let first_parent = factory.flashblock_at(0).build().base.unwrap().parent_hash; + let args = manager.next_buildable_args(first_parent, 1000000); + // Should not find it (evicted from ring buffer) + assert!(args.is_none()); + } +} diff --git a/crates/optimism/flashblocks/src/consensus.rs b/crates/optimism/flashblocks/src/consensus.rs index 60314d2f6c8..0b502c07387 100644 --- a/crates/optimism/flashblocks/src/consensus.rs +++ b/crates/optimism/flashblocks/src/consensus.rs @@ -1,86 +1,458 @@ -use crate::FlashBlockCompleteSequenceRx; +use crate::{FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx}; use alloy_primitives::B256; +use alloy_rpc_types_engine::PayloadStatusEnum; +use op_alloy_rpc_types_engine::OpExecutionData; use reth_engine_primitives::ConsensusEngineHandle; use reth_optimism_payload_builder::OpPayloadTypes; -use reth_payload_primitives::EngineApiMessageVersion; -use ringbuffer::{AllocRingBuffer, RingBuffer}; -use tracing::warn; +use reth_payload_primitives::{EngineApiMessageVersion, ExecutionPayload, PayloadTypes}; +use tracing::*; -/// Consensus client that sends FCUs and new payloads using blocks from a [`FlashBlockService`] +/// Consensus client that sends FCUs and new payloads using blocks from a [`FlashBlockService`]. +/// +/// This client receives completed flashblock sequences and: +/// - Attempts to submit `engine_newPayload` if `state_root` is available (non-zero) +/// - Always sends `engine_forkChoiceUpdated` to drive chain forward /// /// [`FlashBlockService`]: crate::FlashBlockService #[derive(Debug)] -pub struct FlashBlockConsensusClient { +pub struct FlashBlockConsensusClient

+where + P: PayloadTypes, +{ /// Handle to execution client. - engine_handle: ConsensusEngineHandle, + engine_handle: ConsensusEngineHandle

, + /// Receiver for completed flashblock sequences from `FlashBlockService`. sequence_receiver: FlashBlockCompleteSequenceRx, } -impl FlashBlockConsensusClient { +impl

FlashBlockConsensusClient

+where + P: PayloadTypes, + P::ExecutionData: for<'a> TryFrom<&'a FlashBlockCompleteSequence, Error: std::fmt::Display>, +{ /// Create a new `FlashBlockConsensusClient` with the given Op engine and sequence receiver. pub const fn new( - engine_handle: ConsensusEngineHandle, + engine_handle: ConsensusEngineHandle

, sequence_receiver: FlashBlockCompleteSequenceRx, ) -> eyre::Result { Ok(Self { engine_handle, sequence_receiver }) } - /// Get previous block hash using previous block hash buffer. If it isn't available (buffer - /// started more recently than `offset`), return default zero hash - fn get_previous_block_hash( + /// Attempts to submit a new payload to the engine. + /// + /// The `TryFrom` conversion will fail if `execution_outcome.state_root` is `B256::ZERO`, + /// in which case this returns the `parent_hash` instead to drive the chain forward. + /// + /// Returns the block hash to use for FCU (either the new block or parent). + async fn submit_new_payload(&self, sequence: &FlashBlockCompleteSequence) -> B256 { + let payload = match P::ExecutionData::try_from(sequence) { + Ok(payload) => payload, + Err(err) => { + trace!(target: "flashblocks", %err, "Failed payload conversion, using parent hash"); + return sequence.payload_base().parent_hash; + } + }; + + let block_number = payload.block_number(); + let block_hash = payload.block_hash(); + match self.engine_handle.new_payload(payload).await { + Ok(result) => { + debug!( + target: "flashblocks", + flashblock_count = sequence.count(), + block_number, + %block_hash, + ?result, + "Submitted engine_newPayload", + ); + + if let PayloadStatusEnum::Invalid { validation_error } = result.status { + debug!( + target: "flashblocks", + block_number, + %block_hash, + %validation_error, + "Payload validation error", + ); + }; + } + Err(err) => { + error!( + target: "flashblocks", + %err, + block_number, + "Failed to submit new payload", + ); + } + } + + block_hash + } + + /// Submit a forkchoice update to the engine. + async fn submit_forkchoice_update( &self, - previous_block_hashes: &AllocRingBuffer, - offset: usize, - ) -> B256 { - *previous_block_hashes - .len() - .checked_sub(offset) - .and_then(|index| previous_block_hashes.get(index)) - .unwrap_or_default() + head_block_hash: B256, + sequence: &FlashBlockCompleteSequence, + ) { + let block_number = sequence.block_number(); + let safe_hash = sequence.payload_base().parent_hash; + let finalized_hash = sequence.payload_base().parent_hash; + let fcu_state = alloy_rpc_types_engine::ForkchoiceState { + head_block_hash, + safe_block_hash: safe_hash, + finalized_block_hash: finalized_hash, + }; + + match self + .engine_handle + .fork_choice_updated(fcu_state, None, EngineApiMessageVersion::V5) + .await + { + Ok(result) => { + debug!( + target: "flashblocks", + flashblock_count = sequence.count(), + block_number, + %head_block_hash, + %safe_hash, + %finalized_hash, + ?result, + "Submitted engine_forkChoiceUpdated", + ) + } + Err(err) => { + error!( + target: "flashblocks", + %err, + block_number, + %head_block_hash, + %safe_hash, + %finalized_hash, + "Failed to submit fork choice update", + ); + } + } } - /// Spawn the client to start sending FCUs and new payloads by periodically fetching recent - /// blocks. + /// Runs the consensus client loop. + /// + /// Continuously receives completed flashblock sequences and submits them to the execution + /// engine: + /// 1. Attempts `engine_newPayload` (only if `state_root` is available) + /// 2. Always sends `engine_forkChoiceUpdated` to drive chain forward pub async fn run(mut self) { - let mut previous_block_hashes = AllocRingBuffer::new(64); - loop { - match self.sequence_receiver.recv().await { - Ok(sequence) => { - let block_hash = sequence.payload_base().parent_hash; - previous_block_hashes.push(block_hash); - - if sequence.state_root().is_none() { - warn!("Missing state root for the complete sequence") - } - - // Load previous block hashes. We're using (head - 32) and (head - 64) as the - // safe and finalized block hashes. - let safe_block_hash = self.get_previous_block_hash(&previous_block_hashes, 32); - let finalized_block_hash = - self.get_previous_block_hash(&previous_block_hashes, 64); - - let state = alloy_rpc_types_engine::ForkchoiceState { - head_block_hash: block_hash, - safe_block_hash, - finalized_block_hash, - }; - - // Send FCU - let _ = self - .engine_handle - .fork_choice_updated(state, None, EngineApiMessageVersion::V3) - .await; - } - Err(err) => { - warn!( - target: "consensus::flashblock-client", - %err, - "error while fetching flashblock completed sequence" - ); - break; - } - } + let Ok(sequence) = self.sequence_receiver.recv().await else { + continue; + }; + + // Returns block_hash for FCU: + // - If state_root is available: submits newPayload and returns the new block's hash + // - If state_root is zero: skips newPayload and returns parent_hash (no progress yet) + let block_hash = self.submit_new_payload(&sequence).await; + + self.submit_forkchoice_update(block_hash, &sequence).await; + } + } +} + +impl TryFrom<&FlashBlockCompleteSequence> for OpExecutionData { + type Error = &'static str; + + fn try_from(sequence: &FlashBlockCompleteSequence) -> Result { + let mut data = Self::from_flashblocks_unchecked(sequence); + + // If execution outcome is available, use the computed state_root and block_hash. + // FlashBlockService computes these when building sequences on top of the local tip. + if let Some(execution_outcome) = sequence.execution_outcome() { + let payload = data.payload.as_v1_mut(); + payload.state_root = execution_outcome.state_root; + payload.block_hash = execution_outcome.block_hash; + } + + // Only proceed if we have a valid state_root (non-zero). + if data.payload.as_v1_mut().state_root == B256::ZERO { + return Err("No state_root available for payload"); + } + + Ok(data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{sequence::SequenceExecutionOutcome, test_utils::TestFlashBlockFactory}; + + mod op_execution_data_conversion { + use super::*; + + #[test] + fn test_try_from_fails_with_zero_state_root() { + // When execution_outcome is None, state_root remains zero and conversion fails + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + let result = OpExecutionData::try_from(&sequence); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "No state_root available for payload"); + } + + #[test] + fn test_try_from_succeeds_with_execution_outcome() { + // When execution_outcome has state_root, conversion succeeds + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let execution_outcome = SequenceExecutionOutcome { + block_hash: B256::random(), + state_root: B256::random(), // Non-zero + }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0], Some(execution_outcome)).unwrap(); + + let result = OpExecutionData::try_from(&sequence); + assert!(result.is_ok()); + + let mut data = result.unwrap(); + assert_eq!(data.payload.as_v1_mut().state_root, execution_outcome.state_root); + assert_eq!(data.payload.as_v1_mut().block_hash, execution_outcome.block_hash); + } + + #[test] + fn test_try_from_succeeds_with_provided_state_root() { + // When sequencer provides non-zero state_root, conversion succeeds + let factory = TestFlashBlockFactory::new(); + let provided_state_root = B256::random(); + let fb0 = factory.flashblock_at(0).state_root(provided_state_root).build(); + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + let result = OpExecutionData::try_from(&sequence); + assert!(result.is_ok()); + + let mut data = result.unwrap(); + assert_eq!(data.payload.as_v1_mut().state_root, provided_state_root); + } + + #[test] + fn test_try_from_execution_outcome_overrides_provided_state_root() { + // execution_outcome takes precedence over sequencer-provided state_root + let factory = TestFlashBlockFactory::new(); + let provided_state_root = B256::random(); + let fb0 = factory.flashblock_at(0).state_root(provided_state_root).build(); + + let execution_outcome = SequenceExecutionOutcome { + block_hash: B256::random(), + state_root: B256::random(), // Different from provided + }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0], Some(execution_outcome)).unwrap(); + + let result = OpExecutionData::try_from(&sequence); + assert!(result.is_ok()); + + let mut data = result.unwrap(); + // Should use execution_outcome, not the provided state_root + assert_eq!(data.payload.as_v1_mut().state_root, execution_outcome.state_root); + assert_ne!(data.payload.as_v1_mut().state_root, provided_state_root); + } + + #[test] + fn test_try_from_with_multiple_flashblocks() { + // Test conversion with sequence of multiple flashblocks + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let fb1 = factory.flashblock_after(&fb0).state_root(B256::ZERO).build(); + let fb2 = factory.flashblock_after(&fb1).state_root(B256::ZERO).build(); + + let execution_outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0, fb1, fb2], Some(execution_outcome)) + .unwrap(); + + let result = OpExecutionData::try_from(&sequence); + assert!(result.is_ok()); + + let mut data = result.unwrap(); + assert_eq!(data.payload.as_v1_mut().state_root, execution_outcome.state_root); + assert_eq!(data.payload.as_v1_mut().block_hash, execution_outcome.block_hash); + } + } + + mod consensus_client_creation { + use super::*; + use tokio::sync::broadcast; + + #[test] + fn test_new_creates_client() { + let (engine_tx, _) = tokio::sync::mpsc::unbounded_channel(); + let engine_handle = ConsensusEngineHandle::::new(engine_tx); + + let (_, sequence_rx) = broadcast::channel(1); + + let result = FlashBlockConsensusClient::new(engine_handle, sequence_rx); + assert!(result.is_ok()); + } + } + + mod submit_new_payload_behavior { + use super::*; + + #[test] + fn test_submit_new_payload_returns_parent_hash_when_no_state_root() { + // When conversion fails (no state_root), should return parent_hash + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + // Verify conversion would fail + let conversion_result = OpExecutionData::try_from(&sequence); + assert!(conversion_result.is_err()); + + // In the actual run loop, submit_new_payload would return parent_hash + assert_eq!(sequence.payload_base().parent_hash, parent_hash); + } + + #[test] + fn test_submit_new_payload_returns_block_hash_when_state_root_available() { + // When conversion succeeds, should return the new block's hash + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let execution_outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0], Some(execution_outcome)).unwrap(); + + // Verify conversion succeeds + let conversion_result = OpExecutionData::try_from(&sequence); + assert!(conversion_result.is_ok()); + + let mut data = conversion_result.unwrap(); + assert_eq!(data.payload.as_v1_mut().block_hash, execution_outcome.block_hash); + } + } + + mod forkchoice_update_behavior { + use super::*; + + #[test] + fn test_forkchoice_state_uses_parent_hash_for_safe_and_finalized() { + // Both safe_hash and finalized_hash should be set to parent_hash + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + // Verify the expected forkchoice state + assert_eq!(sequence.payload_base().parent_hash, parent_hash); + } + + #[test] + fn test_forkchoice_update_with_new_block_hash() { + // When newPayload succeeds, FCU should use the new block's hash as head + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let execution_outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0], Some(execution_outcome)).unwrap(); + + // The head_block_hash for FCU would be execution_outcome.block_hash + assert_eq!( + sequence.execution_outcome().unwrap().block_hash, + execution_outcome.block_hash + ); + } + + #[test] + fn test_forkchoice_update_with_parent_hash_when_no_state_root() { + // When newPayload is skipped (no state_root), FCU should use parent_hash as head + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let parent_hash = fb0.base.as_ref().unwrap().parent_hash; + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + // The head_block_hash for FCU would be parent_hash (fallback) + assert_eq!(sequence.payload_base().parent_hash, parent_hash); + } + } + + mod run_loop_logic { + use super::*; + + #[test] + fn test_run_loop_processes_sequence_with_state_root() { + // Scenario: Sequence with state_root should trigger both newPayload and FCU + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let execution_outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + + let sequence = + FlashBlockCompleteSequence::new(vec![fb0], Some(execution_outcome)).unwrap(); + + // Verify sequence is ready for newPayload + let conversion = OpExecutionData::try_from(&sequence); + assert!(conversion.is_ok()); + } + + #[test] + fn test_run_loop_processes_sequence_without_state_root() { + // Scenario: Sequence without state_root should skip newPayload but still do FCU + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + + let sequence = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + + // Verify sequence cannot be converted (newPayload will be skipped) + let conversion = OpExecutionData::try_from(&sequence); + assert!(conversion.is_err()); + + // But FCU should still happen with parent_hash + assert!(sequence.payload_base().parent_hash != B256::ZERO); + } + + #[test] + fn test_run_loop_handles_multiple_sequences() { + // Multiple sequences should be processed independently + let factory = TestFlashBlockFactory::new(); + + // Sequence 1: With state_root + let fb0_seq1 = factory.flashblock_at(0).state_root(B256::ZERO).build(); + let outcome1 = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + let seq1 = + FlashBlockCompleteSequence::new(vec![fb0_seq1.clone()], Some(outcome1)).unwrap(); + + // Sequence 2: Without state_root (for next block) + let fb0_seq2 = factory.flashblock_for_next_block(&fb0_seq1).build(); + let seq2 = FlashBlockCompleteSequence::new(vec![fb0_seq2], None).unwrap(); + + // Both should be valid sequences + assert_eq!(seq1.block_number(), 100); + assert_eq!(seq2.block_number(), 101); + + // seq1 can be converted + assert!(OpExecutionData::try_from(&seq1).is_ok()); + // seq2 cannot be converted + assert!(OpExecutionData::try_from(&seq2).is_err()); } } } diff --git a/crates/optimism/flashblocks/src/lib.rs b/crates/optimism/flashblocks/src/lib.rs index 7220f443cc1..6c5d9c1e86e 100644 --- a/crates/optimism/flashblocks/src/lib.rs +++ b/crates/optimism/flashblocks/src/lib.rs @@ -11,23 +11,30 @@ use reth_primitives_traits::NodePrimitives; use std::sync::Arc; -pub use payload::{ - ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashBlock, FlashBlockDecoder, - Metadata, -}; -pub use service::{FlashBlockBuildInfo, FlashBlockService}; -pub use ws::{WsConnect, WsFlashBlockStream}; +// Included to enable serde feature for OpReceipt type used transitively +use reth_optimism_primitives as _; mod consensus; pub use consensus::FlashBlockConsensusClient; + mod payload; -pub use payload::PendingFlashBlock; +pub use payload::{FlashBlock, PendingFlashBlock}; + mod sequence; pub use sequence::{FlashBlockCompleteSequence, FlashBlockPendingSequence}; mod service; +pub use service::{FlashBlockBuildInfo, FlashBlockService}; + mod worker; + +mod cache; + +#[cfg(test)] +mod test_utils; + mod ws; +pub use ws::{WsConnect, WsFlashBlockStream}; /// Receiver of the most recent [`PendingFlashBlock`] built out of [`FlashBlock`]s. /// diff --git a/crates/optimism/flashblocks/src/payload.rs b/crates/optimism/flashblocks/src/payload.rs index 7469538ee3b..c7031c18567 100644 --- a/crates/optimism/flashblocks/src/payload.rs +++ b/crates/optimism/flashblocks/src/payload.rs @@ -1,154 +1,11 @@ use alloy_consensus::BlockHeader; -use alloy_eips::eip4895::Withdrawal; -use alloy_primitives::{bytes, Address, Bloom, Bytes, B256, U256}; -use alloy_rpc_types_engine::PayloadId; +use alloy_primitives::B256; use derive_more::Deref; -use reth_optimism_evm::OpNextBlockEnvAttributes; -use reth_optimism_primitives::OpReceipt; use reth_primitives_traits::NodePrimitives; use reth_rpc_eth_types::PendingBlock; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -/// Represents a Flashblock, a real-time block-like structure emitted by the Base L2 chain. -/// -/// A Flashblock provides a snapshot of a block’s effects before finalization, -/// allowing faster insight into state transitions, balance changes, and logs. -/// It includes a diff of the block’s execution and associated metadata. -/// -/// See: [Base Flashblocks Documentation](https://docs.base.org/chain/flashblocks) -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct FlashBlock { - /// The unique payload ID as assigned by the execution engine for this block. - pub payload_id: PayloadId, - /// A sequential index that identifies the order of this Flashblock. - pub index: u64, - /// A subset of block header fields. - pub base: Option, - /// The execution diff representing state transitions and transactions. - pub diff: ExecutionPayloadFlashblockDeltaV1, - /// Additional metadata about the block such as receipts and balances. - pub metadata: Metadata, -} - -impl FlashBlock { - /// Returns the block number of this flashblock. - pub const fn block_number(&self) -> u64 { - self.metadata.block_number - } - - /// Returns the first parent hash of this flashblock. - pub fn parent_hash(&self) -> Option { - Some(self.base.as_ref()?.parent_hash) - } - - /// Returns the receipt for the given transaction hash. - pub fn receipt_by_hash(&self, hash: &B256) -> Option<&OpReceipt> { - self.metadata.receipt_by_hash(hash) - } -} - -/// A trait for decoding flashblocks from bytes. -pub trait FlashBlockDecoder: Send + 'static { - /// Decodes `bytes` into a [`FlashBlock`]. - fn decode(&self, bytes: bytes::Bytes) -> eyre::Result; -} - -/// Default implementation of the decoder. -impl FlashBlockDecoder for () { - fn decode(&self, bytes: bytes::Bytes) -> eyre::Result { - FlashBlock::decode(bytes) - } -} - -/// Provides metadata about the block that may be useful for indexing or analysis. -// Note: this uses mixed camel, snake case: -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct Metadata { - /// The number of the block in the L2 chain. - pub block_number: u64, - /// A map of addresses to their updated balances after the block execution. - /// This represents balance changes due to transactions, rewards, or system transfers. - pub new_account_balances: BTreeMap, - /// Execution receipts for all transactions in the block. - /// Contains logs, gas usage, and other EVM-level metadata. - pub receipts: BTreeMap, -} - -impl Metadata { - /// Returns the receipt for the given transaction hash. - pub fn receipt_by_hash(&self, hash: &B256) -> Option<&OpReceipt> { - self.receipts.get(hash) - } -} - -/// Represents the base configuration of an execution payload that remains constant -/// throughout block construction. This includes fundamental block properties like -/// parent hash, block number, and other header fields that are determined at -/// block creation and cannot be modified. -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)] -pub struct ExecutionPayloadBaseV1 { - /// Ecotone parent beacon block root - pub parent_beacon_block_root: B256, - /// The parent hash of the block. - pub parent_hash: B256, - /// The fee recipient of the block. - pub fee_recipient: Address, - /// The previous randao of the block. - pub prev_randao: B256, - /// The block number. - #[serde(with = "alloy_serde::quantity")] - pub block_number: u64, - /// The gas limit of the block. - #[serde(with = "alloy_serde::quantity")] - pub gas_limit: u64, - /// The timestamp of the block. - #[serde(with = "alloy_serde::quantity")] - pub timestamp: u64, - /// The extra data of the block. - pub extra_data: Bytes, - /// The base fee per gas of the block. - pub base_fee_per_gas: U256, -} - -/// Represents the modified portions of an execution payload within a flashblock. -/// This structure contains only the fields that can be updated during block construction, -/// such as state root, receipts, logs, and new transactions. Other immutable block fields -/// like parent hash and block number are excluded since they remain constant throughout -/// the block's construction. -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)] -pub struct ExecutionPayloadFlashblockDeltaV1 { - /// The state root of the block. - pub state_root: B256, - /// The receipts root of the block. - pub receipts_root: B256, - /// The logs bloom of the block. - pub logs_bloom: Bloom, - /// The gas used of the block. - #[serde(with = "alloy_serde::quantity")] - pub gas_used: u64, - /// The block hash of the block. - pub block_hash: B256, - /// The transactions of the block. - pub transactions: Vec, - /// Array of [`Withdrawal`] enabled with V2 - pub withdrawals: Vec, - /// The withdrawals root of the block. - pub withdrawals_root: B256, -} - -impl From for OpNextBlockEnvAttributes { - fn from(value: ExecutionPayloadBaseV1) -> Self { - Self { - timestamp: value.timestamp, - suggested_fee_recipient: value.fee_recipient, - prev_randao: value.prev_randao, - gas_limit: value.gas_limit, - parent_beacon_block_root: Some(value.parent_beacon_block_root), - extra_data: value.extra_data, - } - } -} +/// Type alias for the Optimism flashblock payload. +pub type FlashBlock = op_alloy_rpc_types_engine::OpFlashblockPayload; /// The pending block built with all received Flashblocks alongside the metadata for the last added /// Flashblock. diff --git a/crates/optimism/flashblocks/src/sequence.rs b/crates/optimism/flashblocks/src/sequence.rs index f2363207e38..abf9e6d514c 100644 --- a/crates/optimism/flashblocks/src/sequence.rs +++ b/crates/optimism/flashblocks/src/sequence.rs @@ -1,41 +1,54 @@ -use crate::{ExecutionPayloadBaseV1, FlashBlock, FlashBlockCompleteSequenceRx}; -use alloy_eips::eip2718::WithEncoded; -use alloy_primitives::B256; +use crate::{FlashBlock, FlashBlockCompleteSequenceRx}; +use alloy_primitives::{Bytes, B256}; use alloy_rpc_types_engine::PayloadId; use core::mem; use eyre::{bail, OptionExt}; -use reth_primitives_traits::{Recovered, SignedTransaction}; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; +use reth_revm::cached::CachedReads; use std::{collections::BTreeMap, ops::Deref}; use tokio::sync::broadcast; -use tracing::{debug, trace, warn}; +use tracing::*; /// The size of the broadcast channel for completed flashblock sequences. const FLASHBLOCK_SEQUENCE_CHANNEL_SIZE: usize = 128; +/// Outcome from executing a flashblock sequence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SequenceExecutionOutcome { + /// The block hash of the executed pending block + pub block_hash: B256, + /// Properly computed state root + pub state_root: B256, +} + /// An ordered B-tree keeping the track of a sequence of [`FlashBlock`]s by their indices. #[derive(Debug)] -pub struct FlashBlockPendingSequence { +pub struct FlashBlockPendingSequence { /// tracks the individual flashblocks in order - /// - /// With a blocktime of 2s and flashblock tick-rate of 200ms plus one extra flashblock per new - /// pending block, we expect 11 flashblocks per slot. - inner: BTreeMap>, + inner: BTreeMap, /// Broadcasts flashblocks to subscribers. block_broadcaster: broadcast::Sender, - /// Optional properly computed state root for the current sequence. - state_root: Option, + /// Optional execution outcome from building the current sequence. + execution_outcome: Option, + /// Cached state reads for the current block. + /// Current `PendingFlashBlock` is built out of a sequence of `FlashBlocks`, and executed again + /// when fb received on top of the same block. Avoid redundant I/O across multiple + /// executions within the same block. + cached_reads: Option, } -impl FlashBlockPendingSequence -where - T: SignedTransaction, -{ +impl FlashBlockPendingSequence { /// Create a new pending sequence. pub fn new() -> Self { // Note: if the channel is full, send will not block but rather overwrite the oldest // messages. Order is preserved. let (tx, _) = broadcast::channel(FLASHBLOCK_SEQUENCE_CHANNEL_SIZE); - Self { inner: BTreeMap::new(), block_broadcaster: tx, state_root: None } + Self { + inner: BTreeMap::new(), + block_broadcaster: tx, + execution_outcome: None, + cached_reads: None, + } } /// Returns the sender half of the [`FlashBlockCompleteSequence`] channel. @@ -50,91 +63,55 @@ where self.block_broadcaster.subscribe() } - // Clears the state and broadcasts the blocks produced to subscribers. - fn clear_and_broadcast_blocks(&mut self) { - let flashblocks = mem::take(&mut self.inner); - - // If there are any subscribers, send the flashblocks to them. - if self.block_broadcaster.receiver_count() > 0 { - let flashblocks = match FlashBlockCompleteSequence::new( - flashblocks.into_iter().map(|block| block.1.into()).collect(), - self.state_root, - ) { - Ok(flashblocks) => flashblocks, - Err(err) => { - debug!(target: "flashblocks", error = ?err, "Failed to create full flashblock complete sequence"); - return; - } - }; - - // Note: this should only ever fail if there are no receivers. This can happen if - // there is a race condition between the clause right above and this - // one. We can simply warn the user and continue. - if let Err(err) = self.block_broadcaster.send(flashblocks) { - warn!(target: "flashblocks", error = ?err, "Failed to send flashblocks to subscribers"); - } - } - } - /// Inserts a new block into the sequence. /// /// A [`FlashBlock`] with index 0 resets the set. - pub fn insert(&mut self, flashblock: FlashBlock) -> eyre::Result<()> { + pub fn insert(&mut self, flashblock: FlashBlock) { if flashblock.index == 0 { - trace!(number=%flashblock.block_number(), "Tracking new flashblock sequence"); - - // Flash block at index zero resets the whole state. - self.clear_and_broadcast_blocks(); - - self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?); - return Ok(()) + trace!(target: "flashblocks", number=%flashblock.block_number(), "Tracking new flashblock sequence"); + self.inner.insert(flashblock.index, flashblock); + return; } // only insert if we previously received the same block and payload, assume we received // index 0 - let same_block = self.block_number() == Some(flashblock.metadata.block_number); + let same_block = self.block_number() == Some(flashblock.block_number()); let same_payload = self.payload_id() == Some(flashblock.payload_id); if same_block && same_payload { - trace!(number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock"); - self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?); + trace!(target: "flashblocks", number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock"); + self.inner.insert(flashblock.index, flashblock); } else { - trace!(number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following"); + trace!(target: "flashblocks", number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following"); } + } - Ok(()) + /// Set execution outcome from building the flashblock sequence + pub const fn set_execution_outcome( + &mut self, + execution_outcome: Option, + ) { + self.execution_outcome = execution_outcome; } - /// Set state root - pub const fn set_state_root(&mut self, state_root: Option) { - self.state_root = state_root; + /// Set cached reads for this sequence + pub fn set_cached_reads(&mut self, cached_reads: CachedReads) { + self.cached_reads = Some(cached_reads); } - /// Iterator over sequence of executable transactions. - /// - /// A flashblocks is not ready if there's missing previous flashblocks, i.e. there's a gap in - /// the sequence - /// - /// Note: flashblocks start at `index 0`. - pub fn ready_transactions(&self) -> impl Iterator>> + '_ { - self.inner - .values() - .enumerate() - .take_while(|(idx, block)| { - // flashblock index 0 is the first flashblock - block.block().index == *idx as u64 - }) - .flat_map(|(_, block)| block.txs.clone()) + /// Removes the cached reads for this sequence + pub const fn take_cached_reads(&mut self) -> Option { + self.cached_reads.take() } /// Returns the first block number pub fn block_number(&self) -> Option { - Some(self.inner.values().next()?.block().metadata.block_number) + Some(self.inner.values().next()?.block_number()) } /// Returns the payload base of the first tracked flashblock. - pub fn payload_base(&self) -> Option { - self.inner.values().next()?.block().base.clone() + pub fn payload_base(&self) -> Option { + self.inner.values().next()?.base.clone() } /// Returns the number of tracked flashblocks. @@ -144,23 +121,40 @@ where /// Returns the reference to the last flashblock. pub fn last_flashblock(&self) -> Option<&FlashBlock> { - self.inner.last_key_value().map(|(_, b)| &b.block) + self.inner.last_key_value().map(|(_, b)| b) } /// Returns the current/latest flashblock index in the sequence pub fn index(&self) -> Option { - Some(self.inner.values().last()?.block().index) + Some(self.inner.values().last()?.index) } /// Returns the payload id of the first tracked flashblock in the current sequence. pub fn payload_id(&self) -> Option { - Some(self.inner.values().next()?.block().payload_id) + Some(self.inner.values().next()?.payload_id) + } + + /// Finalizes the current pending sequence and returns it as a complete sequence. + /// + /// Clears the internal state and returns an error if the sequence is empty or validation fails. + pub fn finalize(&mut self) -> eyre::Result { + if self.inner.is_empty() { + bail!("Cannot finalize empty flashblock sequence"); + } + + let flashblocks = mem::take(&mut self.inner); + let execution_outcome = mem::take(&mut self.execution_outcome); + self.cached_reads = None; + + FlashBlockCompleteSequence::new(flashblocks.into_values().collect(), execution_outcome) + } + + /// Returns an iterator over all flashblocks in the sequence. + pub fn flashblocks(&self) -> impl Iterator { + self.inner.values() } } -impl Default for FlashBlockPendingSequence -where - T: SignedTransaction, -{ +impl Default for FlashBlockPendingSequence { fn default() -> Self { Self::new() } @@ -170,12 +164,12 @@ where /// /// Ensures invariants of a complete flashblocks sequence. /// If this entire sequence of flashblocks was executed on top of latest block, this also includes -/// the computed state root. +/// the execution outcome with block hash and state root. #[derive(Debug, Clone)] pub struct FlashBlockCompleteSequence { inner: Vec, - /// Optional state root for the current sequence - state_root: Option, + /// Optional execution outcome from building the flashblock sequence + execution_outcome: Option, } impl FlashBlockCompleteSequence { @@ -184,7 +178,10 @@ impl FlashBlockCompleteSequence { /// * vector is not empty /// * first flashblock have the base payload /// * sequence of flashblocks is sound (successive index from 0, same payload id, ...) - pub fn new(blocks: Vec, state_root: Option) -> eyre::Result { + pub fn new( + blocks: Vec, + execution_outcome: Option, + ) -> eyre::Result { let first_block = blocks.first().ok_or_eyre("No flashblocks in sequence")?; // Ensure that first flashblock have base @@ -194,21 +191,21 @@ impl FlashBlockCompleteSequence { if !blocks.iter().enumerate().all(|(idx, block)| { idx == block.index as usize && block.payload_id == first_block.payload_id && - block.metadata.block_number == first_block.metadata.block_number + block.block_number() == first_block.block_number() }) { bail!("Flashblock inconsistencies detected in sequence"); } - Ok(Self { inner: blocks, state_root }) + Ok(Self { inner: blocks, execution_outcome }) } /// Returns the block number pub fn block_number(&self) -> u64 { - self.inner.first().unwrap().metadata.block_number + self.inner.first().unwrap().block_number() } /// Returns the payload base of the first flashblock. - pub fn payload_base(&self) -> &ExecutionPayloadBaseV1 { + pub fn payload_base(&self) -> &OpFlashblockPayloadBase { self.inner.first().unwrap().base.as_ref().unwrap() } @@ -222,9 +219,22 @@ impl FlashBlockCompleteSequence { self.inner.last().unwrap() } - /// Returns the state root for the current sequence - pub const fn state_root(&self) -> Option { - self.state_root + /// Returns the execution outcome of the sequence. + pub const fn execution_outcome(&self) -> Option { + self.execution_outcome + } + + /// Updates execution outcome of the sequence. + pub const fn set_execution_outcome( + &mut self, + execution_outcome: Option, + ) { + self.execution_outcome = execution_outcome; + } + + /// Returns all transactions from all flashblocks in the sequence + pub fn all_transactions(&self) -> Vec { + self.inner.iter().flat_map(|fb| fb.diff.transactions.iter().cloned()).collect() } } @@ -236,169 +246,437 @@ impl Deref for FlashBlockCompleteSequence { } } -impl TryFrom> for FlashBlockCompleteSequence { +impl TryFrom for FlashBlockCompleteSequence { type Error = eyre::Error; - fn try_from(sequence: FlashBlockPendingSequence) -> Result { - Self::new( - sequence.inner.into_values().map(|block| block.block().clone()).collect::>(), - sequence.state_root, - ) + fn try_from(sequence: FlashBlockPendingSequence) -> Result { + Self::new(sequence.inner.into_values().collect(), sequence.execution_outcome) } } -#[derive(Debug)] -struct PreparedFlashBlock { - /// The prepared transactions, ready for execution - txs: Vec>>, - /// The tracked flashblock - block: FlashBlock, -} +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestFlashBlockFactory; -impl PreparedFlashBlock { - const fn block(&self) -> &FlashBlock { - &self.block - } -} + mod pending_sequence_insert { + use super::*; -impl From> for FlashBlock { - fn from(val: PreparedFlashBlock) -> Self { - val.block - } -} + #[test] + fn test_insert_index_zero_creates_new_sequence() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).build(); + let payload_id = fb0.payload_id; -impl PreparedFlashBlock -where - T: SignedTransaction, -{ - /// Creates a flashblock that is ready for execution by preparing all transactions - /// - /// Returns an error if decoding or signer recovery fails. - fn new(block: FlashBlock) -> eyre::Result { - let mut txs = Vec::with_capacity(block.diff.transactions.len()); - for encoded in block.diff.transactions.iter().cloned() { - let tx = T::decode_2718_exact(encoded.as_ref())?; - let signer = tx.try_recover()?; - let tx = WithEncoded::new(encoded, tx.with_signer(signer)); - txs.push(tx); + sequence.insert(fb0); + + assert_eq!(sequence.count(), 1); + assert_eq!(sequence.block_number(), Some(100)); + assert_eq!(sequence.payload_id(), Some(payload_id)); } - Ok(Self { txs, block }) + #[test] + fn test_insert_followup_same_block_and_payload() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0.clone()); + + let fb1 = factory.flashblock_after(&fb0).build(); + sequence.insert(fb1.clone()); + + let fb2 = factory.flashblock_after(&fb1).build(); + sequence.insert(fb2); + + assert_eq!(sequence.count(), 3); + assert_eq!(sequence.index(), Some(2)); + } + + #[test] + fn test_insert_ignores_different_block_number() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0.clone()); + + // Try to insert followup with different block number + let fb1 = factory.flashblock_after(&fb0).block_number(101).build(); + sequence.insert(fb1); + + assert_eq!(sequence.count(), 1); + assert_eq!(sequence.block_number(), Some(100)); + } + + #[test] + fn test_insert_ignores_different_payload_id() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let payload_id1 = fb0.payload_id; + sequence.insert(fb0.clone()); + + // Try to insert followup with different payload_id + let payload_id2 = alloy_rpc_types_engine::PayloadId::new([2u8; 8]); + let fb1 = factory.flashblock_after(&fb0).payload_id(payload_id2).build(); + sequence.insert(fb1); + + assert_eq!(sequence.count(), 1); + assert_eq!(sequence.payload_id(), Some(payload_id1)); + } + + #[test] + fn test_insert_maintains_btree_order() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0.clone()); + + let fb2 = factory.flashblock_after(&fb0).index(2).build(); + sequence.insert(fb2); + + let fb1 = factory.flashblock_after(&fb0).build(); + sequence.insert(fb1); + + let indices: Vec = sequence.flashblocks().map(|fb| fb.index).collect(); + assert_eq!(indices, vec![0, 1, 2]); + } } -} -impl Deref for PreparedFlashBlock { - type Target = FlashBlock; + mod pending_sequence_finalize { + use super::*; - fn deref(&self) -> &Self::Target { - &self.block + #[test] + fn test_finalize_empty_sequence_fails() { + let mut sequence = FlashBlockPendingSequence::new(); + let result = sequence.finalize(); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Cannot finalize empty flashblock sequence" + ); + } + + #[test] + fn test_finalize_clears_pending_state() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0); + + assert_eq!(sequence.count(), 1); + + let _complete = sequence.finalize().unwrap(); + + // After finalize, sequence should be empty + assert_eq!(sequence.count(), 0); + assert_eq!(sequence.block_number(), None); + } + + #[test] + fn test_finalize_preserves_execution_outcome() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0); + + let outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + sequence.set_execution_outcome(Some(outcome)); + + let complete = sequence.finalize().unwrap(); + + assert_eq!(complete.execution_outcome(), Some(outcome)); + } + + #[test] + fn test_finalize_clears_cached_reads() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0); + + let cached_reads = CachedReads::default(); + sequence.set_cached_reads(cached_reads); + assert!(sequence.take_cached_reads().is_some()); + + let _complete = sequence.finalize().unwrap(); + + // Cached reads should be cleared + assert!(sequence.take_cached_reads().is_none()); + } + + #[test] + fn test_finalize_multiple_times_after_refill() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + // First sequence + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0); + + let complete1 = sequence.finalize().unwrap(); + assert_eq!(complete1.count(), 1); + + // Add new sequence for next block + let fb1 = factory.flashblock_for_next_block(&complete1.last().clone()).build(); + sequence.insert(fb1); + + let complete2 = sequence.finalize().unwrap(); + assert_eq!(complete2.count(), 1); + assert_eq!(complete2.block_number(), 101); + } } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::ExecutionPayloadFlashblockDeltaV1; - use alloy_consensus::{ - transaction::SignerRecoverable, EthereumTxEnvelope, EthereumTypedTransaction, TxEip1559, - }; - use alloy_eips::Encodable2718; - use alloy_primitives::{hex, Signature, TxKind, U256}; - - #[test] - fn test_sequence_stops_before_gap() { - let mut sequence = FlashBlockPendingSequence::new(); - let tx = EthereumTxEnvelope::new_unhashed( - EthereumTypedTransaction::::Eip1559(TxEip1559 { - chain_id: 4, - nonce: 26u64, - max_priority_fee_per_gas: 1500000000, - max_fee_per_gas: 1500000013, - gas_limit: 21_000u64, - to: TxKind::Call(hex!("61815774383099e24810ab832a5b2a5425c154d5").into()), - value: U256::from(3000000000000000000u64), - input: Default::default(), - access_list: Default::default(), - }), - Signature::new( - U256::from_be_bytes(hex!( - "59e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafd" - )), - U256::from_be_bytes(hex!( - "016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469" - )), - true, - ), - ); - let tx = Recovered::new_unchecked(tx.clone(), tx.recover_signer_unchecked().unwrap()); - - sequence - .insert(FlashBlock { - payload_id: Default::default(), - index: 0, - base: None, - diff: ExecutionPayloadFlashblockDeltaV1 { - transactions: vec![tx.encoded_2718().into()], - ..Default::default() - }, - metadata: Default::default(), - }) - .unwrap(); - - sequence - .insert(FlashBlock { - payload_id: Default::default(), - index: 2, - base: None, - diff: Default::default(), - metadata: Default::default(), - }) - .unwrap(); - - let actual_txs: Vec<_> = sequence.ready_transactions().collect(); - let expected_txs = vec![WithEncoded::new(tx.encoded_2718().into(), tx)]; - - assert_eq!(actual_txs, expected_txs); + mod complete_sequence_invariants { + use super::*; + + #[test] + fn test_new_empty_sequence_fails() { + let result = FlashBlockCompleteSequence::new(vec![], None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "No flashblocks in sequence"); + } + + #[test] + fn test_new_requires_base_at_index_zero() { + let factory = TestFlashBlockFactory::new(); + // Use builder() with index 1 first to create a flashblock, then change its index to 0 + // to bypass the auto-base creation logic + let mut fb0_no_base = factory.flashblock_at(1).build(); + fb0_no_base.index = 0; + fb0_no_base.base = None; + + let result = FlashBlockCompleteSequence::new(vec![fb0_no_base], None); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Flashblock at index 0 has no base"); + } + + #[test] + fn test_new_validates_successive_indices() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + // Skip index 1, go straight to 2 + let fb2 = factory.flashblock_after(&fb0).index(2).build(); + + let result = FlashBlockCompleteSequence::new(vec![fb0, fb2], None); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Flashblock inconsistencies detected in sequence" + ); + } + + #[test] + fn test_new_validates_same_block_number() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let fb1 = factory.flashblock_after(&fb0).block_number(101).build(); + + let result = FlashBlockCompleteSequence::new(vec![fb0, fb1], None); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Flashblock inconsistencies detected in sequence" + ); + } + + #[test] + fn test_new_validates_same_payload_id() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let payload_id2 = alloy_rpc_types_engine::PayloadId::new([2u8; 8]); + let fb1 = factory.flashblock_after(&fb0).payload_id(payload_id2).build(); + + let result = FlashBlockCompleteSequence::new(vec![fb0, fb1], None); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Flashblock inconsistencies detected in sequence" + ); + } + + #[test] + fn test_new_valid_single_flashblock() { + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).build(); + + let result = FlashBlockCompleteSequence::new(vec![fb0], None); + assert!(result.is_ok()); + + let complete = result.unwrap(); + assert_eq!(complete.count(), 1); + assert_eq!(complete.block_number(), 100); + } + + #[test] + fn test_new_valid_multiple_flashblocks() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let fb1 = factory.flashblock_after(&fb0).build(); + let fb2 = factory.flashblock_after(&fb1).build(); + + let result = FlashBlockCompleteSequence::new(vec![fb0, fb1, fb2], None); + assert!(result.is_ok()); + + let complete = result.unwrap(); + assert_eq!(complete.count(), 3); + assert_eq!(complete.last().index, 2); + } + + #[test] + fn test_all_transactions_aggregates_correctly() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory + .flashblock_at(0) + .transactions(vec![Bytes::from_static(&[1, 2, 3]), Bytes::from_static(&[4, 5, 6])]) + .build(); + + let fb1 = factory + .flashblock_after(&fb0) + .transactions(vec![Bytes::from_static(&[7, 8, 9])]) + .build(); + + let complete = FlashBlockCompleteSequence::new(vec![fb0, fb1], None).unwrap(); + let all_txs = complete.all_transactions(); + + assert_eq!(all_txs.len(), 3); + assert_eq!(all_txs[0], Bytes::from_static(&[1, 2, 3])); + assert_eq!(all_txs[1], Bytes::from_static(&[4, 5, 6])); + assert_eq!(all_txs[2], Bytes::from_static(&[7, 8, 9])); + } + + #[test] + fn test_payload_base_returns_first_block_base() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let fb1 = factory.flashblock_after(&fb0).build(); + + let complete = FlashBlockCompleteSequence::new(vec![fb0.clone(), fb1], None).unwrap(); + + assert_eq!(complete.payload_base().block_number, fb0.base.unwrap().block_number); + } + + #[test] + fn test_execution_outcome_mutation() { + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).build(); + + let mut complete = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + assert!(complete.execution_outcome().is_none()); + + let outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + complete.set_execution_outcome(Some(outcome)); + + assert_eq!(complete.execution_outcome(), Some(outcome)); + } + + #[test] + fn test_deref_provides_vec_access() { + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + let fb1 = factory.flashblock_after(&fb0).build(); + + let complete = FlashBlockCompleteSequence::new(vec![fb0, fb1], None).unwrap(); + + // Use deref to access Vec methods + assert_eq!(complete.len(), 2); + assert!(!complete.is_empty()); + } } - #[test] - fn test_sequence_sends_flashblocks_to_subscribers() { - let mut sequence = FlashBlockPendingSequence::>::new(); - let mut subscriber = sequence.subscribe_block_sequence(); - - for idx in 0..10 { - sequence - .insert(FlashBlock { - payload_id: Default::default(), - index: idx, - base: Some(ExecutionPayloadBaseV1::default()), - diff: Default::default(), - metadata: Default::default(), - }) - .unwrap(); + mod sequence_conversion { + use super::*; + + #[test] + fn test_try_from_pending_to_complete_valid() { + let mut pending = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + pending.insert(fb0); + + let complete: Result = pending.try_into(); + assert!(complete.is_ok()); + assert_eq!(complete.unwrap().count(), 1); + } + + #[test] + fn test_try_from_pending_to_complete_empty_fails() { + let pending = FlashBlockPendingSequence::new(); + + let complete: Result = pending.try_into(); + assert!(complete.is_err()); } - assert_eq!(sequence.count(), 10); + #[test] + fn test_try_from_preserves_execution_outcome() { + let mut pending = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + pending.insert(fb0); + + let outcome = + SequenceExecutionOutcome { block_hash: B256::random(), state_root: B256::random() }; + pending.set_execution_outcome(Some(outcome)); - // Then we don't receive anything until we insert a new flashblock - let no_flashblock = subscriber.try_recv(); - assert!(no_flashblock.is_err()); + let complete: FlashBlockCompleteSequence = pending.try_into().unwrap(); + assert_eq!(complete.execution_outcome(), Some(outcome)); + } + } - // Let's insert a new flashblock with index 0 - sequence - .insert(FlashBlock { - payload_id: Default::default(), - index: 0, - base: Some(ExecutionPayloadBaseV1::default()), - diff: Default::default(), - metadata: Default::default(), - }) - .unwrap(); + mod pending_sequence_helpers { + use super::*; - let flashblocks = subscriber.try_recv().unwrap(); - assert_eq!(flashblocks.count(), 10); + #[test] + fn test_last_flashblock_returns_highest_index() { + let mut sequence = FlashBlockPendingSequence::new(); + let factory = TestFlashBlockFactory::new(); + + let fb0 = factory.flashblock_at(0).build(); + sequence.insert(fb0.clone()); + + let fb1 = factory.flashblock_after(&fb0).build(); + sequence.insert(fb1); + + let last = sequence.last_flashblock().unwrap(); + assert_eq!(last.index, 1); + } - for (idx, block) in flashblocks.iter().enumerate() { - assert_eq!(block.index, idx as u64); + #[test] + fn test_subscribe_block_sequence_channel() { + let sequence = FlashBlockPendingSequence::new(); + let mut rx = sequence.subscribe_block_sequence(); + + // Spawn a task that sends a complete sequence + let tx = sequence.block_sequence_broadcaster().clone(); + std::thread::spawn(move || { + let factory = TestFlashBlockFactory::new(); + let fb0 = factory.flashblock_at(0).build(); + let complete = FlashBlockCompleteSequence::new(vec![fb0], None).unwrap(); + let _ = tx.send(complete); + }); + + // Should receive the broadcast + let received = rx.blocking_recv(); + assert!(received.is_ok()); + assert_eq!(received.unwrap().count(), 1); } } } diff --git a/crates/optimism/flashblocks/src/service.rs b/crates/optimism/flashblocks/src/service.rs index f5d4a4a810d..4eed74683f7 100644 --- a/crates/optimism/flashblocks/src/service.rs +++ b/crates/optimism/flashblocks/src/service.rs @@ -1,35 +1,20 @@ use crate::{ - sequence::FlashBlockPendingSequence, - worker::{BuildArgs, FlashBlockBuilder}, - ExecutionPayloadBaseV1, FlashBlock, FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx, - InProgressFlashBlockRx, PendingFlashBlock, + cache::SequenceManager, worker::FlashBlockBuilder, FlashBlock, FlashBlockCompleteSequence, + FlashBlockCompleteSequenceRx, InProgressFlashBlockRx, PendingFlashBlock, }; -use alloy_eips::eip2718::WithEncoded; use alloy_primitives::B256; use futures_util::{FutureExt, Stream, StreamExt}; -use metrics::Histogram; -use reth_chain_state::{CanonStateNotification, CanonStateNotifications, CanonStateSubscriptions}; +use metrics::{Gauge, Histogram}; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use reth_evm::ConfigureEvm; use reth_metrics::Metrics; -use reth_primitives_traits::{ - AlloyBlockHeader, BlockTy, HeaderTy, NodePrimitives, ReceiptTy, Recovered, -}; +use reth_primitives_traits::{AlloyBlockHeader, BlockTy, HeaderTy, NodePrimitives, ReceiptTy}; use reth_revm::cached::CachedReads; use reth_storage_api::{BlockReaderIdExt, StateProviderFactory}; use reth_tasks::TaskExecutor; -use std::{ - pin::Pin, - sync::Arc, - task::{ready, Context, Poll}, - time::Instant, -}; -use tokio::{ - pin, - sync::{oneshot, watch}, -}; -use tracing::{debug, trace, warn}; - -pub(crate) const FB_STATE_ROOT_FROM_INDEX: usize = 9; +use std::{sync::Arc, time::Instant}; +use tokio::sync::{oneshot, watch}; +use tracing::*; /// The `FlashBlockService` maintains an in-memory [`PendingFlashBlock`] built out of a sequence of /// [`FlashBlock`]s. @@ -37,41 +22,37 @@ pub(crate) const FB_STATE_ROOT_FROM_INDEX: usize = 9; pub struct FlashBlockService< N: NodePrimitives, S, - EvmConfig: ConfigureEvm, + EvmConfig: ConfigureEvm + Unpin>, Provider, > { - rx: S, - current: Option>, - blocks: FlashBlockPendingSequence, + /// Incoming flashblock stream. + incoming_flashblock_rx: S, + /// Signals when a block build is in progress. + in_progress_tx: watch::Sender>, /// Broadcast channel to forward received flashblocks from the subscription. received_flashblocks_tx: tokio::sync::broadcast::Sender>, - rebuild: bool, + + /// Executes flashblock sequences to build pending blocks. builder: FlashBlockBuilder, - canon_receiver: CanonStateNotifications, + /// Task executor for spawning block build jobs. spawner: TaskExecutor, + /// Currently running block build job with start time and result receiver. job: Option>, - /// Cached state reads for the current block. - /// Current `PendingFlashBlock` is built out of a sequence of `FlashBlocks`, and executed again - /// when fb received on top of the same block. Avoid redundant I/O across multiple - /// executions within the same block. - cached_state: Option<(B256, CachedReads)>, - /// Signals when a block build is in progress - in_progress_tx: watch::Sender>, + /// Manages flashblock sequences with caching and intelligent build selection. + sequences: SequenceManager, + /// `FlashBlock` service's metrics metrics: FlashBlockServiceMetrics, - /// Enable state root calculation from flashblock with index [`FB_STATE_ROOT_FROM_INDEX`] - compute_state_root: bool, } impl FlashBlockService where N: NodePrimitives, S: Stream> + Unpin + 'static, - EvmConfig: ConfigureEvm + Unpin> + EvmConfig: ConfigureEvm + Unpin> + Clone + 'static, Provider: StateProviderFactory - + CanonStateSubscriptions + BlockReaderIdExt< Header = HeaderTy, Block = BlockTy, @@ -82,32 +63,27 @@ where + 'static, { /// Constructs a new `FlashBlockService` that receives [`FlashBlock`]s from `rx` stream. - pub fn new(rx: S, evm_config: EvmConfig, provider: Provider, spawner: TaskExecutor) -> Self { + pub fn new( + incoming_flashblock_rx: S, + evm_config: EvmConfig, + provider: Provider, + spawner: TaskExecutor, + compute_state_root: bool, + ) -> Self { let (in_progress_tx, _) = watch::channel(None); let (received_flashblocks_tx, _) = tokio::sync::broadcast::channel(128); Self { - rx, - current: None, - blocks: FlashBlockPendingSequence::new(), + incoming_flashblock_rx, + in_progress_tx, received_flashblocks_tx, - canon_receiver: provider.subscribe_to_canonical_state(), builder: FlashBlockBuilder::new(evm_config, provider), - rebuild: false, spawner, job: None, - cached_state: None, - in_progress_tx, + sequences: SequenceManager::new(compute_state_root), metrics: FlashBlockServiceMetrics::default(), - compute_state_root: false, } } - /// Enable state root calculation from flashblock - pub const fn compute_state_root(mut self, enable_state_root: bool) -> Self { - self.compute_state_root = enable_state_root; - self - } - /// Returns the sender half to the received flashblocks. pub const fn flashblocks_broadcaster( &self, @@ -119,12 +95,12 @@ where pub const fn block_sequence_broadcaster( &self, ) -> &tokio::sync::broadcast::Sender { - self.blocks.block_sequence_broadcaster() + self.sequences.block_sequence_broadcaster() } /// Returns a subscriber to the flashblock sequence. pub fn subscribe_block_sequence(&self) -> FlashBlockCompleteSequenceRx { - self.blocks.subscribe_block_sequence() + self.sequences.subscribe_block_sequence() } /// Returns a receiver that signals when a flashblock is being built. @@ -132,231 +108,129 @@ where self.in_progress_tx.subscribe() } - /// Drives the services and sends new blocks to the receiver + /// Drives the service and sends new blocks to the receiver. /// - /// Note: this should be spawned - pub async fn run(mut self, tx: tokio::sync::watch::Sender>>) { - while let Some(block) = self.next().await { - if let Ok(block) = block.inspect_err(|e| tracing::error!("{e}")) { - let _ = tx.send(block).inspect_err(|e| tracing::error!("{e}")); - } - } - - warn!("Flashblock service has stopped"); - } - - /// Notifies all subscribers about the received flashblock - fn notify_received_flashblock(&self, flashblock: &FlashBlock) { - if self.received_flashblocks_tx.receiver_count() > 0 { - let _ = self.received_flashblocks_tx.send(Arc::new(flashblock.clone())); - } - } - - /// Returns the [`BuildArgs`] made purely out of [`FlashBlock`]s that were received earlier. + /// This loop: + /// 1. Checks if any build job has completed and processes results + /// 2. Receives and batches all immediately available flashblocks + /// 3. Attempts to build a block from the complete sequence /// - /// Returns `None` if the flashblock have no `base` or the base is not a child block of latest. - fn build_args( - &mut self, - ) -> Option< - BuildArgs< - impl IntoIterator>> - + use, - >, - > { - let Some(base) = self.blocks.payload_base() else { - trace!( - flashblock_number = ?self.blocks.block_number(), - count = %self.blocks.count(), - "Missing flashblock payload base" - ); - - return None - }; - - // attempt an initial consecutive check - if let Some(latest) = self.builder.provider().latest_header().ok().flatten() && - latest.hash() != base.parent_hash - { - trace!(flashblock_parent=?base.parent_hash, flashblock_number=base.block_number, local_latest=?latest.num_hash(), "Skipping non consecutive build attempt"); - return None - } - - let Some(last_flashblock) = self.blocks.last_flashblock() else { - trace!(flashblock_number = ?self.blocks.block_number(), count = %self.blocks.count(), "Missing last flashblock"); - return None - }; - - // Check if state root must be computed - let compute_state_root = - self.compute_state_root && self.blocks.index() >= Some(FB_STATE_ROOT_FROM_INDEX as u64); - - Some(BuildArgs { - base, - transactions: self.blocks.ready_transactions().collect::>(), - cached_state: self.cached_state.take(), - last_flashblock_index: last_flashblock.index, - last_flashblock_hash: last_flashblock.diff.block_hash, - compute_state_root, - }) - } - - /// Takes out `current` [`PendingFlashBlock`] if `state` is not preceding it. - fn on_new_tip(&mut self, state: CanonStateNotification) -> Option> { - let tip = state.tip_checked()?; - let tip_hash = tip.hash(); - let current = self.current.take_if(|current| current.parent_hash() != tip_hash); - - // Prefill the cache with state from the new canonical tip, similar to payload/basic - let mut cached = CachedReads::default(); - let committed = state.committed(); - let new_execution_outcome = committed.execution_outcome(); - for (addr, acc) in new_execution_outcome.bundle_accounts_iter() { - if let Some(info) = acc.info.clone() { - // Pre-cache existing accounts and their storage (only changed accounts/storage) - let storage = - acc.storage.iter().map(|(key, slot)| (*key, slot.present_value)).collect(); - cached.insert_account(addr, info, storage); - } - } - self.cached_state = Some((tip_hash, cached)); - - current - } -} - -impl Stream for FlashBlockService -where - N: NodePrimitives, - S: Stream> + Unpin + 'static, - EvmConfig: ConfigureEvm + Unpin> - + Clone - + 'static, - Provider: StateProviderFactory - + CanonStateSubscriptions - + BlockReaderIdExt< - Header = HeaderTy, - Block = BlockTy, - Transaction = N::SignedTx, - Receipt = ReceiptTy, - > + Unpin - + Clone - + 'static, -{ - type Item = eyre::Result>>; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - + /// Note: this should be spawned + pub async fn run(mut self, tx: watch::Sender>>) { loop { - // drive pending build job to completion - let result = match this.job.as_mut() { - Some((now, rx)) => { - let result = ready!(rx.poll_unpin(cx)); - result.ok().map(|res| (*now, res)) - } - None => None, - }; - // reset job - this.job.take(); - // No build in progress - let _ = this.in_progress_tx.send(None); - - if let Some((now, result)) = result { - match result { - Ok(Some((new_pending, cached_reads))) => { - // update state root of the current sequence - this.blocks.set_state_root(new_pending.computed_state_root()); + tokio::select! { + // Event 1: job exists, listen to job results + Some(result) = async { + match self.job.as_mut() { + Some((_, rx)) => rx.await.ok(), + None => std::future::pending().await, + } + } => { + let (start_time, _) = self.job.take().unwrap(); + let _ = self.in_progress_tx.send(None); - // built a new pending block - this.current = Some(new_pending.clone()); - // cache reads - this.cached_state = Some((new_pending.parent_hash(), cached_reads)); - this.rebuild = false; + match result { + Ok(Some((pending, cached_reads))) => { + let parent_hash = pending.parent_hash(); + self.sequences + .on_build_complete(parent_hash, Some((pending.clone(), cached_reads))); - let elapsed = now.elapsed(); - this.metrics.execution_duration.record(elapsed.as_secs_f64()); - trace!( - parent_hash = %new_pending.block().parent_hash(), - block_number = new_pending.block().number(), - flash_blocks = this.blocks.count(), - ?elapsed, - "Built new block with flashblocks" - ); + let elapsed = start_time.elapsed(); + self.metrics.execution_duration.record(elapsed.as_secs_f64()); - return Poll::Ready(Some(Ok(Some(new_pending)))); - } - Ok(None) => { - // nothing to do because tracked flashblock doesn't attach to latest - } - Err(err) => { - // we can ignore this error - debug!(%err, "failed to execute flashblock"); + let _ = tx.send(Some(pending)); + } + Ok(None) => { + trace!(target: "flashblocks", "Build job returned None"); + } + Err(err) => { + warn!(target: "flashblocks", %err, "Build job failed"); + } } } - } - // consume new flashblocks while they're ready - while let Poll::Ready(Some(result)) = this.rx.poll_next_unpin(cx) { - match result { - Ok(flashblock) => { - this.notify_received_flashblock(&flashblock); - if flashblock.index == 0 { - this.metrics.last_flashblock_length.record(this.blocks.count() as f64); + // Event 2: New flashblock arrives (batch process all ready flashblocks) + result = self.incoming_flashblock_rx.next() => { + match result { + Some(Ok(flashblock)) => { + // Process first flashblock + self.process_flashblock(flashblock); + + // Batch process all other immediately available flashblocks + while let Some(result) = self.incoming_flashblock_rx.next().now_or_never().flatten() { + match result { + Ok(fb) => self.process_flashblock(fb), + Err(err) => warn!(target: "flashblocks", %err, "Error receiving flashblock"), + } + } + + self.try_start_build_job(); + } + Some(Err(err)) => { + warn!(target: "flashblocks", %err, "Error receiving flashblock"); } - match this.blocks.insert(flashblock) { - Ok(_) => this.rebuild = true, - Err(err) => debug!(%err, "Failed to prepare flashblock"), + None => { + warn!(target: "flashblocks", "Flashblock stream ended"); + break; } } - Err(err) => return Poll::Ready(Some(Err(err))), } } + } + } - // update on new head block - if let Poll::Ready(Ok(state)) = { - let fut = this.canon_receiver.recv(); - pin!(fut); - fut.poll_unpin(cx) - } && let Some(current) = this.on_new_tip(state) - { - trace!( - parent_hash = %current.block().parent_hash(), - block_number = current.block().number(), - "Clearing current flashblock on new canonical block" - ); + /// Processes a single flashblock: notifies subscribers, records metrics, and inserts into + /// sequence. + fn process_flashblock(&mut self, flashblock: FlashBlock) { + self.notify_received_flashblock(&flashblock); - return Poll::Ready(Some(Ok(None))) - } + if flashblock.index == 0 { + self.metrics.last_flashblock_length.record(self.sequences.pending().count() as f64); + } - if !this.rebuild && this.current.is_some() { - return Poll::Pending - } + if let Err(err) = self.sequences.insert_flashblock(flashblock) { + trace!(target: "flashblocks", %err, "Failed to insert flashblock"); + } + } - // try to build a block on top of latest - if let Some(args) = this.build_args() { - let now = Instant::now(); + /// Notifies all subscribers about the received flashblock. + fn notify_received_flashblock(&self, flashblock: &FlashBlock) { + if self.received_flashblocks_tx.receiver_count() > 0 { + let _ = self.received_flashblocks_tx.send(Arc::new(flashblock.clone())); + } + } - let fb_info = FlashBlockBuildInfo { - parent_hash: args.base.parent_hash, - index: args.last_flashblock_index, - block_number: args.base.block_number, - }; - // Signal that a flashblock build has started with build metadata - let _ = this.in_progress_tx.send(Some(fb_info)); - let (tx, rx) = oneshot::channel(); - let builder = this.builder.clone(); + /// Attempts to build a block if no job is currently running and a buildable sequence exists. + fn try_start_build_job(&mut self) { + if self.job.is_some() { + return; // Already building + } - this.spawner.spawn_blocking(async move { - let _ = tx.send(builder.execute(args)); - }); - this.job.replace((now, rx)); + let Some(latest) = self.builder.provider().latest_header().ok().flatten() else { + return; + }; - // continue and poll the spawned job - continue - } + let Some(args) = self.sequences.next_buildable_args(latest.hash(), latest.timestamp()) + else { + return; // Nothing buildable + }; - return Poll::Pending - } + // Spawn build job + let fb_info = FlashBlockBuildInfo { + parent_hash: args.base.parent_hash, + index: args.last_flashblock_index, + block_number: args.base.block_number, + }; + self.metrics.current_block_height.set(fb_info.block_number as f64); + self.metrics.current_index.set(fb_info.index as f64); + let _ = self.in_progress_tx.send(Some(fb_info)); + + let (tx, rx) = oneshot::channel(); + let builder = self.builder.clone(); + self.spawner.spawn_blocking(Box::pin(async move { + let _ = tx.send(builder.execute(args)); + })); + self.job = Some((Instant::now(), rx)); } } @@ -381,4 +255,8 @@ struct FlashBlockServiceMetrics { last_flashblock_length: Histogram, /// The duration applying flashblock state changes in seconds. execution_duration: Histogram, + /// Current block height. + current_block_height: Gauge, + /// Current flashblock index. + current_index: Gauge, } diff --git a/crates/optimism/flashblocks/src/test_utils.rs b/crates/optimism/flashblocks/src/test_utils.rs new file mode 100644 index 00000000000..1c2da1f7c80 --- /dev/null +++ b/crates/optimism/flashblocks/src/test_utils.rs @@ -0,0 +1,340 @@ +//! Test utilities for flashblocks. +//! +//! Provides a factory for creating test flashblocks with automatic timestamp management. +//! +//! # Examples +//! +//! ## Simple: Create a flashblock sequence for the same block +//! +//! ```ignore +//! let factory = FlashBlockTestFactory::new(2); // 2 second block time +//! let fb0 = factory.flashblock_at(0).build(); +//! let fb1 = factory.flashblock_after(&fb0).build(); +//! let fb2 = factory.flashblock_after(&fb1).build(); +//! ``` +//! +//! ## Create flashblocks with transactions +//! +//! ```ignore +//! let factory = FlashBlockTestFactory::new(2); +//! let fb0 = factory.flashblock_at(0).build(); +//! let txs = vec![Bytes::from_static(&[1, 2, 3])]; +//! let fb1 = factory.flashblock_after(&fb0).transactions(txs).build(); +//! ``` +//! +//! ## Test across multiple blocks (timestamps auto-increment) +//! +//! ```ignore +//! let factory = FlashBlockTestFactory::new(2); // 2 second blocks +//! +//! // Block 100 at timestamp 1000000 +//! let fb0 = factory.flashblock_at(0).build(); +//! let fb1 = factory.flashblock_after(&fb0).build(); +//! +//! // Block 101 at timestamp 1000002 (auto-incremented by block_time) +//! let fb2 = factory.flashblock_for_next_block(&fb1).build(); +//! let fb3 = factory.flashblock_after(&fb2).build(); +//! ``` +//! +//! ## Full control with builder +//! +//! ```ignore +//! let factory = FlashBlockTestFactory::new(1); +//! let fb = factory.custom() +//! .block_number(100) +//! .parent_hash(specific_hash) +//! .state_root(computed_root) +//! .transactions(txs) +//! .build(); +//! ``` + +use crate::FlashBlock; +use alloy_primitives::{Address, Bloom, Bytes, B256, U256}; +use alloy_rpc_types_engine::PayloadId; +use op_alloy_rpc_types_engine::{ + OpFlashblockPayloadBase, OpFlashblockPayloadDelta, OpFlashblockPayloadMetadata, +}; + +/// Factory for creating test flashblocks with automatic timestamp management. +/// +/// Tracks `block_time` to automatically increment timestamps when creating new blocks. +/// Returns builders that can be further customized before calling `build()`. +/// +/// # Examples +/// +/// ```ignore +/// let factory = TestFlashBlockFactory::new(2); // 2 second block time +/// let fb0 = factory.flashblock_at(0).build(); +/// let fb1 = factory.flashblock_after(&fb0).build(); +/// let fb2 = factory.flashblock_for_next_block(&fb1).build(); // timestamp auto-increments +/// ``` +#[derive(Debug)] +pub(crate) struct TestFlashBlockFactory { + /// Block time in seconds (used to auto-increment timestamps) + block_time: u64, + /// Starting timestamp for the first block + base_timestamp: u64, + /// Current block number being tracked + current_block_number: u64, +} + +impl TestFlashBlockFactory { + /// Creates a new builder with the specified block time in seconds. + /// + /// # Arguments + /// + /// * `block_time` - Time between blocks in seconds (e.g., 2 for 2-second blocks) + /// + /// # Examples + /// + /// ```ignore + /// let factory = TestFlashBlockFactory::new(2); // 2 second blocks + /// let factory_fast = TestFlashBlockFactory::new(1); // 1 second blocks + /// ``` + pub(crate) fn new() -> Self { + Self { block_time: 2, base_timestamp: 1_000_000, current_block_number: 100 } + } + + pub(crate) fn with_block_time(mut self, block_time: u64) -> Self { + self.block_time = block_time; + self + } + + /// Creates a builder for a flashblock at the specified index (within the current block). + /// + /// Returns a builder with index set, allowing further customization before building. + /// + /// # Examples + /// + /// ```ignore + /// let factory = TestFlashBlockFactory::new(2); + /// let fb0 = factory.flashblock_at(0).build(); // Simple usage + /// let fb1 = factory.flashblock_at(1).state_root(specific_root).build(); // Customize + /// ``` + pub(crate) fn flashblock_at(&self, index: u64) -> TestFlashBlockBuilder { + self.builder().index(index).block_number(self.current_block_number) + } + + /// Creates a builder for a flashblock following the previous one in the same sequence. + /// + /// Automatically increments the index and maintains `block_number` and `payload_id`. + /// Returns a builder allowing further customization. + /// + /// # Examples + /// + /// ```ignore + /// let factory = TestFlashBlockFactory::new(2); + /// let fb0 = factory.flashblock_at(0).build(); + /// let fb1 = factory.flashblock_after(&fb0).build(); // Simple + /// let fb2 = factory.flashblock_after(&fb1).transactions(txs).build(); // With txs + /// ``` + pub(crate) fn flashblock_after(&self, previous: &FlashBlock) -> TestFlashBlockBuilder { + let parent_hash = + previous.base.as_ref().map(|b| b.parent_hash).unwrap_or(previous.diff.block_hash); + + self.builder() + .index(previous.index + 1) + .block_number(previous.metadata.block_number) + .payload_id(previous.payload_id) + .parent_hash(parent_hash) + .timestamp(previous.base.as_ref().map(|b| b.timestamp).unwrap_or(self.base_timestamp)) + } + + /// Creates a builder for a flashblock for the next block, starting a new sequence at index 0. + /// + /// Increments block number, uses previous `block_hash` as `parent_hash`, generates new + /// `payload_id`, and automatically increments the timestamp by `block_time`. + /// Returns a builder allowing further customization. + /// + /// # Examples + /// + /// ```ignore + /// let factory = TestFlashBlockFactory::new(2); // 2 second blocks + /// let fb0 = factory.flashblock_at(0).build(); // Block 100, timestamp 1000000 + /// let fb1 = factory.flashblock_for_next_block(&fb0).build(); // Block 101, timestamp 1000002 + /// let fb2 = factory.flashblock_for_next_block(&fb1).transactions(txs).build(); // Customize + /// ``` + pub(crate) fn flashblock_for_next_block(&self, previous: &FlashBlock) -> TestFlashBlockBuilder { + let prev_timestamp = + previous.base.as_ref().map(|b| b.timestamp).unwrap_or(self.base_timestamp); + + self.builder() + .index(0) + .block_number(previous.metadata.block_number + 1) + .payload_id(PayloadId::new(B256::random().0[0..8].try_into().unwrap())) + .parent_hash(previous.diff.block_hash) + .timestamp(prev_timestamp + self.block_time) + } + + /// Returns a custom builder for full control over flashblock creation. + /// + /// Use this when the convenience methods don't provide enough control. + /// + /// # Examples + /// + /// ```ignore + /// let factory = TestFlashBlockFactory::new(2); + /// let fb = factory.builder() + /// .index(5) + /// .block_number(200) + /// .parent_hash(specific_hash) + /// .state_root(computed_root) + /// .build(); + /// ``` + pub(crate) fn builder(&self) -> TestFlashBlockBuilder { + TestFlashBlockBuilder { + index: 0, + block_number: self.current_block_number, + payload_id: PayloadId::new([1u8; 8]), + parent_hash: B256::random(), + timestamp: self.base_timestamp, + base: None, + block_hash: B256::random(), + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::default(), + gas_used: 0, + transactions: vec![], + withdrawals: vec![], + withdrawals_root: B256::ZERO, + blob_gas_used: None, + } + } +} + +/// Custom builder for creating test flashblocks with full control. +/// +/// Created via [`TestFlashBlockFactory::builder()`]. +#[derive(Debug)] +pub(crate) struct TestFlashBlockBuilder { + index: u64, + block_number: u64, + payload_id: PayloadId, + parent_hash: B256, + timestamp: u64, + base: Option, + block_hash: B256, + state_root: B256, + receipts_root: B256, + logs_bloom: Bloom, + gas_used: u64, + transactions: Vec, + withdrawals: Vec, + withdrawals_root: B256, + blob_gas_used: Option, +} + +impl TestFlashBlockBuilder { + /// Sets the flashblock index. + pub(crate) fn index(mut self, index: u64) -> Self { + self.index = index; + self + } + + /// Sets the block number. + pub(crate) fn block_number(mut self, block_number: u64) -> Self { + self.block_number = block_number; + self + } + + /// Sets the payload ID. + pub(crate) fn payload_id(mut self, payload_id: PayloadId) -> Self { + self.payload_id = payload_id; + self + } + + /// Sets the parent hash. + pub(crate) fn parent_hash(mut self, parent_hash: B256) -> Self { + self.parent_hash = parent_hash; + self + } + + /// Sets the timestamp. + pub(crate) fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Sets the base payload. Automatically created for index 0 if not set. + #[allow(dead_code)] + pub(crate) fn base(mut self, base: OpFlashblockPayloadBase) -> Self { + self.base = Some(base); + self + } + + /// Sets the block hash in the diff. + #[allow(dead_code)] + pub(crate) fn block_hash(mut self, block_hash: B256) -> Self { + self.block_hash = block_hash; + self + } + + /// Sets the state root in the diff. + #[allow(dead_code)] + pub(crate) fn state_root(mut self, state_root: B256) -> Self { + self.state_root = state_root; + self + } + + /// Sets the receipts root in the diff. + #[allow(dead_code)] + pub(crate) fn receipts_root(mut self, receipts_root: B256) -> Self { + self.receipts_root = receipts_root; + self + } + + /// Sets the transactions in the diff. + pub(crate) fn transactions(mut self, transactions: Vec) -> Self { + self.transactions = transactions; + self + } + + /// Sets the gas used in the diff. + #[allow(dead_code)] + pub(crate) fn gas_used(mut self, gas_used: u64) -> Self { + self.gas_used = gas_used; + self + } + + /// Builds the flashblock. + /// + /// If index is 0 and no base was explicitly set, creates a default base. + pub(crate) fn build(mut self) -> FlashBlock { + // Auto-create base for index 0 if not set + if self.index == 0 && self.base.is_none() { + self.base = Some(OpFlashblockPayloadBase { + parent_hash: self.parent_hash, + parent_beacon_block_root: B256::random(), + fee_recipient: Address::default(), + prev_randao: B256::random(), + block_number: self.block_number, + gas_limit: 30_000_000, + timestamp: self.timestamp, + extra_data: Default::default(), + base_fee_per_gas: U256::from(1_000_000_000u64), + }); + } + + FlashBlock { + index: self.index, + payload_id: self.payload_id, + base: self.base, + diff: OpFlashblockPayloadDelta { + block_hash: self.block_hash, + state_root: self.state_root, + receipts_root: self.receipts_root, + logs_bloom: self.logs_bloom, + gas_used: self.gas_used, + transactions: self.transactions, + withdrawals: self.withdrawals, + withdrawals_root: self.withdrawals_root, + blob_gas_used: self.blob_gas_used, + }, + metadata: OpFlashblockPayloadMetadata { + block_number: self.block_number, + receipts: Default::default(), + new_account_balances: Default::default(), + }, + } + } +} diff --git a/crates/optimism/flashblocks/src/worker.rs b/crates/optimism/flashblocks/src/worker.rs index 8cf7777f6a6..f929bdb47fd 100644 --- a/crates/optimism/flashblocks/src/worker.rs +++ b/crates/optimism/flashblocks/src/worker.rs @@ -1,7 +1,8 @@ -use crate::{ExecutionPayloadBaseV1, PendingFlashBlock}; +use crate::PendingFlashBlock; use alloy_eips::{eip2718::WithEncoded, BlockNumberOrTag}; use alloy_primitives::B256; -use reth_chain_state::{CanonStateSubscriptions, ExecutedBlock}; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; +use reth_chain_state::ExecutedBlock; use reth_errors::RethError; use reth_evm::{ execute::{BlockBuilder, BlockBuilderOutcome}, @@ -38,7 +39,7 @@ impl FlashBlockBuilder { } pub(crate) struct BuildArgs { - pub(crate) base: ExecutionPayloadBaseV1, + pub(crate) base: OpFlashblockPayloadBase, pub(crate) transactions: I, pub(crate) cached_state: Option<(B256, CachedReads)>, pub(crate) last_flashblock_index: u64, @@ -49,9 +50,8 @@ pub(crate) struct BuildArgs { impl FlashBlockBuilder where N: NodePrimitives, - EvmConfig: ConfigureEvm + Unpin>, + EvmConfig: ConfigureEvm + Unpin>, Provider: StateProviderFactory - + CanonStateSubscriptions + BlockReaderIdExt< Header = HeaderTy, Block = BlockTy, @@ -60,14 +60,14 @@ where > + Unpin, { /// Returns the [`PendingFlashBlock`] made purely out of transactions and - /// [`ExecutionPayloadBaseV1`] in `args`. + /// [`OpFlashblockPayloadBase`] in `args`. /// /// Returns `None` if the flashblock doesn't attach to the latest header. pub(crate) fn execute>>>( &self, mut args: BuildArgs, ) -> eyre::Result, CachedReads)>> { - trace!("Attempting new pending block from flashblocks"); + trace!(target: "flashblocks", "Attempting new pending block from flashblocks"); let latest = self .provider @@ -76,7 +76,7 @@ where let latest_hash = latest.hash(); if args.base.parent_hash != latest_hash { - trace!(flashblock_parent = ?args.base.parent_hash, local_latest=?latest.num_hash(),"Skipping non consecutive flashblock"); + trace!(target: "flashblocks", flashblock_parent = ?args.base.parent_hash, local_latest=?latest.num_hash(),"Skipping non consecutive flashblock"); // doesn't attach to the latest block return Ok(None) } @@ -106,6 +106,7 @@ where // if the real state root should be computed let BlockBuilderOutcome { execution_result, block, hashed_state, .. } = if args.compute_state_root { + trace!(target: "flashblocks", "Computing block state root"); builder.finish(&state_provider)? } else { builder.finish(NoopProvider::default())? diff --git a/crates/optimism/flashblocks/src/ws/decoding.rs b/crates/optimism/flashblocks/src/ws/decoding.rs index 267f79cf19a..64d96dc5e3e 100644 --- a/crates/optimism/flashblocks/src/ws/decoding.rs +++ b/crates/optimism/flashblocks/src/ws/decoding.rs @@ -1,50 +1,27 @@ -use crate::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashBlock, Metadata}; +use crate::FlashBlock; use alloy_primitives::bytes::Bytes; -use alloy_rpc_types_engine::PayloadId; -use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, io}; +use std::io; -/// Internal helper for decoding -#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize)] -struct FlashblocksPayloadV1 { - /// The payload id of the flashblock - pub payload_id: PayloadId, - /// The index of the flashblock in the block - pub index: u64, - /// The base execution payload configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub base: Option, - /// The delta/diff containing modified portions of the execution payload - pub diff: ExecutionPayloadFlashblockDeltaV1, - /// Additional metadata associated with the flashblock - pub metadata: serde_json::Value, +/// A trait for decoding flashblocks from bytes. +pub trait FlashBlockDecoder: Send + 'static { + /// Decodes `bytes` into a [`FlashBlock`]. + fn decode(&self, bytes: Bytes) -> eyre::Result; } -impl FlashBlock { - /// Decodes `bytes` into [`FlashBlock`]. - /// - /// This function is specific to the Base Optimism websocket encoding. - /// - /// It is assumed that the `bytes` are encoded in JSON and optionally compressed using brotli. - /// Whether the `bytes` is compressed or not is determined by looking at the first - /// non ascii-whitespace character. - pub(crate) fn decode(bytes: Bytes) -> eyre::Result { - let bytes = try_parse_message(bytes)?; +/// Default implementation of the decoder. +impl FlashBlockDecoder for () { + fn decode(&self, bytes: Bytes) -> eyre::Result { + decode_flashblock(bytes) + } +} - let payload: FlashblocksPayloadV1 = serde_json::from_slice(&bytes) - .map_err(|e| eyre::eyre!("failed to parse message: {e}"))?; +pub(crate) fn decode_flashblock(bytes: Bytes) -> eyre::Result { + let bytes = crate::ws::decoding::try_parse_message(bytes)?; - let metadata: Metadata = serde_json::from_value(payload.metadata) - .map_err(|e| eyre::eyre!("failed to parse message metadata: {e}"))?; + let payload: FlashBlock = + serde_json::from_slice(&bytes).map_err(|e| eyre::eyre!("failed to parse message: {e}"))?; - Ok(Self { - payload_id: payload.payload_id, - index: payload.index, - base: payload.base, - diff: payload.diff, - metadata, - }) - } + Ok(payload) } /// Maps `bytes` into a potentially different [`Bytes`]. diff --git a/crates/optimism/flashblocks/src/ws/mod.rs b/crates/optimism/flashblocks/src/ws/mod.rs index 2b820899312..8c8a5910892 100644 --- a/crates/optimism/flashblocks/src/ws/mod.rs +++ b/crates/optimism/flashblocks/src/ws/mod.rs @@ -1,4 +1,6 @@ pub use stream::{WsConnect, WsFlashBlockStream}; mod decoding; +pub(crate) use decoding::FlashBlockDecoder; + mod stream; diff --git a/crates/optimism/flashblocks/src/ws/stream.rs b/crates/optimism/flashblocks/src/ws/stream.rs index 64cf6f718e2..e46fd6d747f 100644 --- a/crates/optimism/flashblocks/src/ws/stream.rs +++ b/crates/optimism/flashblocks/src/ws/stream.rs @@ -1,4 +1,4 @@ -use crate::{FlashBlock, FlashBlockDecoder}; +use crate::{ws::FlashBlockDecoder, FlashBlock}; use futures_util::{ stream::{SplitSink, SplitStream}, FutureExt, Sink, Stream, StreamExt, @@ -126,7 +126,9 @@ where } Ok(Message::Ping(bytes)) => this.ping(bytes), Ok(Message::Close(frame)) => this.close(frame), - Ok(msg) => debug!("Received unexpected message: {:?}", msg), + Ok(msg) => { + debug!(target: "flashblocks", "Received unexpected message: {:?}", msg) + } Err(err) => return Poll::Ready(Some(Err(err.into()))), } } @@ -238,7 +240,6 @@ impl WsConnect for WsConnector { #[cfg(test)] mod tests { use super::*; - use crate::ExecutionPayloadBaseV1; use alloy_primitives::bytes::Bytes; use brotli::enc::BrotliEncoderParams; use std::{future, iter}; @@ -449,23 +450,7 @@ mod tests { } fn flashblock() -> FlashBlock { - FlashBlock { - payload_id: Default::default(), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: Default::default(), - parent_hash: Default::default(), - fee_recipient: Default::default(), - prev_randao: Default::default(), - block_number: 0, - gas_limit: 0, - timestamp: 0, - extra_data: Default::default(), - base_fee_per_gas: Default::default(), - }), - diff: Default::default(), - metadata: Default::default(), - } + Default::default() } #[test_case::test_case(to_json_message(Message::Binary); "json binary")] diff --git a/crates/optimism/node/src/args.rs b/crates/optimism/node/src/args.rs index 4e9bb2ce7c3..9d27c99320d 100644 --- a/crates/optimism/node/src/args.rs +++ b/crates/optimism/node/src/args.rs @@ -74,6 +74,14 @@ pub struct RollupArgs { /// block tag will use the pending state based on flashblocks. #[arg(long)] pub flashblocks_url: Option, + + /// Enable flashblock consensus client to drive the chain forward + /// + /// When enabled, the flashblock consensus client will process flashblock sequences and submit + /// them to the engine API to advance the chain. + /// Requires `flashblocks_url` to be set. + #[arg(long, default_value_t = false, requires = "flashblocks_url")] + pub flashblock_consensus: bool, } impl Default for RollupArgs { @@ -90,6 +98,7 @@ impl Default for RollupArgs { historical_rpc: None, min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, } } } diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 65055eb6717..c177051677d 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -194,6 +194,7 @@ impl OpNode { .with_min_suggested_priority_fee(self.args.min_suggested_priority_fee) .with_historical_rpc(self.args.historical_rpc.clone()) .with_flashblocks(self.args.flashblocks_url.clone()) + .with_flashblock_consensus(self.args.flashblock_consensus) } /// Instantiates the [`ProviderFactoryBuilder`] for an opstack node. @@ -695,6 +696,8 @@ pub struct OpAddOnsBuilder { tokio_runtime: Option, /// A URL pointing to a secure websocket service that streams out flashblocks. flashblocks_url: Option, + /// Enable flashblock consensus client to drive chain forward. + flashblock_consensus: bool, } impl Default for OpAddOnsBuilder { @@ -711,6 +714,7 @@ impl Default for OpAddOnsBuilder { rpc_middleware: Identity::new(), tokio_runtime: None, flashblocks_url: None, + flashblock_consensus: false, } } } @@ -779,6 +783,7 @@ impl OpAddOnsBuilder { tokio_runtime, _nt, flashblocks_url, + flashblock_consensus, .. } = self; OpAddOnsBuilder { @@ -793,6 +798,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + flashblock_consensus, } } @@ -801,6 +807,12 @@ impl OpAddOnsBuilder { self.flashblocks_url = flashblocks_url; self } + + /// With a flashblock consensus client to drive chain forward. + pub const fn with_flashblock_consensus(mut self, flashblock_consensus: bool) -> Self { + self.flashblock_consensus = flashblock_consensus; + self + } } impl OpAddOnsBuilder { @@ -826,6 +838,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + flashblock_consensus, .. } = self; @@ -835,7 +848,8 @@ impl OpAddOnsBuilder { .with_sequencer(sequencer_url.clone()) .with_sequencer_headers(sequencer_headers.clone()) .with_min_suggested_priority_fee(min_suggested_priority_fee) - .with_flashblocks(flashblocks_url), + .with_flashblocks(flashblocks_url) + .with_flashblock_consensus(flashblock_consensus), PVB::default(), EB::default(), EVB::default(), diff --git a/crates/optimism/node/src/rpc.rs b/crates/optimism/node/src/rpc.rs index db811a7f921..b87800a54ee 100644 --- a/crates/optimism/node/src/rpc.rs +++ b/crates/optimism/node/src/rpc.rs @@ -13,7 +13,7 @@ //! components::ComponentsBuilder, //! hooks::OnComponentInitializedHook, //! rpc::{EthApiBuilder, EthApiCtx}, -//! LaunchContext, NodeConfig, RethFullAdapter, +//! ConsensusEngineHandle, LaunchContext, NodeConfig, RethFullAdapter, //! }; //! use reth_optimism_chainspec::OP_SEPOLIA; //! use reth_optimism_evm::OpEvmConfig; @@ -67,7 +67,14 @@ //! config.cache, //! node.task_executor().clone(), //! ); -//! let ctx = EthApiCtx { components: node.node_adapter(), config, cache }; +//! // Create a dummy beacon engine handle for offline mode +//! let (tx, _) = tokio::sync::mpsc::unbounded_channel(); +//! let ctx = EthApiCtx { +//! components: node.node_adapter(), +//! config, +//! cache, +//! engine_handle: ConsensusEngineHandle::new(tx), +//! }; //! let eth_api = OpEthApiBuilder::::default().build_eth_api(ctx).await.unwrap(); //! //! // build `trace` namespace API diff --git a/crates/optimism/rpc/src/eth/mod.rs b/crates/optimism/rpc/src/eth/mod.rs index 8adbee93adc..f605d58c839 100644 --- a/crates/optimism/rpc/src/eth/mod.rs +++ b/crates/optimism/rpc/src/eth/mod.rs @@ -16,6 +16,7 @@ use alloy_consensus::BlockHeader; use alloy_primitives::{B256, U256}; use eyre::WrapErr; use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; pub use receipt::{OpReceiptBuilder, OpReceiptFieldsBuilder}; use reqwest::Url; use reth_chainspec::{EthereumHardforks, Hardforks}; @@ -23,8 +24,9 @@ use reth_evm::ConfigureEvm; use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, NodeTypes}; use reth_node_builder::rpc::{EthApiBuilder, EthApiCtx}; use reth_optimism_flashblocks::{ - ExecutionPayloadBaseV1, FlashBlockBuildInfo, FlashBlockCompleteSequenceRx, FlashBlockRx, - FlashBlockService, FlashblocksListeners, PendingBlockRx, PendingFlashBlock, WsFlashBlockStream, + FlashBlockBuildInfo, FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx, + FlashBlockConsensusClient, FlashBlockRx, FlashBlockService, FlashblocksListeners, + PendingBlockRx, PendingFlashBlock, WsFlashBlockStream, }; use reth_rpc::eth::core::EthApiInner; use reth_rpc_eth_api::{ @@ -398,6 +400,12 @@ pub struct OpEthApiBuilder { /// /// [flashblocks]: reth_optimism_flashblocks flashblocks_url: Option, + /// Enable flashblock consensus client to drive the chain forward. + /// + /// When enabled, flashblock sequences are submitted to the engine API via + /// `newPayload` and `forkchoiceUpdated` calls, advancing the canonical chain state. + /// Requires `flashblocks_url` to be set. + flashblock_consensus: bool, /// Marker for network types. _nt: PhantomData, } @@ -409,6 +417,7 @@ impl Default for OpEthApiBuilder { sequencer_headers: Vec::new(), min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, _nt: PhantomData, } } @@ -422,6 +431,7 @@ impl OpEthApiBuilder { sequencer_headers: Vec::new(), min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, _nt: PhantomData, } } @@ -449,6 +459,12 @@ impl OpEthApiBuilder { self.flashblocks_url = flashblocks_url; self } + + /// With flashblock consensus client enabled to drive chain forward + pub const fn with_flashblock_consensus(mut self, flashblock_consensus: bool) -> Self { + self.flashblock_consensus = flashblock_consensus; + self + } } impl EthApiBuilder for OpEthApiBuilder @@ -456,10 +472,18 @@ where N: FullNodeComponents< Evm: ConfigureEvm< NextBlockEnvCtx: BuildPendingEnv> - + From + + From + Unpin, >, - Types: NodeTypes, + Types: NodeTypes< + ChainSpec: Hardforks + EthereumHardforks, + Payload: reth_node_api::PayloadTypes< + ExecutionData: for<'a> TryFrom< + &'a FlashBlockCompleteSequence, + Error: std::fmt::Display, + >, + >, + >, >, NetworkT: RpcTypes, OpRpcConvert: RpcConvert, @@ -474,6 +498,7 @@ where sequencer_headers, min_suggested_priority_fee, flashblocks_url, + flashblock_consensus, .. } = self; let rpc_converter = @@ -500,14 +525,24 @@ where ctx.components.evm_config().clone(), ctx.components.provider().clone(), ctx.components.task_executor().clone(), + // enable state root calculation if flashblock_consensus is enabled. + flashblock_consensus, ); let flashblocks_sequence = service.block_sequence_broadcaster().clone(); let received_flashblocks = service.flashblocks_broadcaster().clone(); let in_progress_rx = service.subscribe_in_progress(); - ctx.components.task_executor().spawn(Box::pin(service.run(tx))); + if flashblock_consensus { + info!(target: "reth::cli", "Launching FlashBlockConsensusClient"); + let flashblock_client = FlashBlockConsensusClient::new( + ctx.engine_handle.clone(), + flashblocks_sequence.subscribe(), + )?; + ctx.components.task_executor().spawn(Box::pin(flashblock_client.run())); + } + Some(FlashblocksListeners::new( pending_rx, flashblocks_sequence, diff --git a/crates/optimism/rpc/src/eth/transaction.rs b/crates/optimism/rpc/src/eth/transaction.rs index 14ed9dbe247..661fd11da0c 100644 --- a/crates/optimism/rpc/src/eth/transaction.rs +++ b/crates/optimism/rpc/src/eth/transaction.rs @@ -7,14 +7,14 @@ use futures::StreamExt; use op_alloy_consensus::{transaction::OpTransactionInfo, OpTransaction}; use reth_chain_state::CanonStateSubscriptions; use reth_optimism_primitives::DepositReceipt; -use reth_primitives_traits::{BlockBody, SignedTransaction}; +use reth_primitives_traits::{BlockBody, SignedTransaction, SignerRecoverable}; use reth_rpc_eth_api::{ - helpers::{spec::SignersForRpc, EthTransactions, LoadReceipt, LoadTransaction}, + helpers::{spec::SignersForRpc, EthTransactions, LoadReceipt, LoadTransaction, SpawnBlocking}, try_into_op_tx_info, EthApiTypes as _, FromEthApiError, FromEvmError, RpcConvert, RpcNodeCore, RpcReceipt, TxInfoMapper, }; -use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError}; -use reth_storage_api::{errors::ProviderError, ReceiptProvider}; +use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError, TransactionSource}; +use reth_storage_api::{errors::ProviderError, ProviderTx, ReceiptProvider, TransactionsProvider}; use reth_transaction_pool::{ AddedTransactionOutcome, PoolTransaction, TransactionOrigin, TransactionPool, }; @@ -179,6 +179,53 @@ where OpEthApiError: FromEvmError, Rpc: RpcConvert, { + async fn transaction_by_hash( + &self, + hash: B256, + ) -> Result>>, Self::Error> { + // 1. Try to find the transaction on disk (historical blocks) + if let Some((tx, meta)) = self + .spawn_blocking_io(move |this| { + this.provider() + .transaction_by_hash_with_meta(hash) + .map_err(Self::Error::from_eth_err) + }) + .await? + { + let transaction = tx + .try_into_recovered_unchecked() + .map_err(|_| EthApiError::InvalidTransactionSignature)?; + + return Ok(Some(TransactionSource::Block { + transaction, + index: meta.index, + block_hash: meta.block_hash, + block_number: meta.block_number, + base_fee: meta.base_fee, + })); + } + + // 2. check flashblocks (sequencer preconfirmations) + if let Ok(Some(pending_block)) = self.pending_flashblock().await && + let Some(indexed_tx) = pending_block.block().find_indexed(hash) + { + let meta = indexed_tx.meta(); + return Ok(Some(TransactionSource::Block { + transaction: indexed_tx.recovered_tx().cloned(), + index: meta.index, + block_hash: meta.block_hash, + block_number: meta.block_number, + base_fee: meta.base_fee, + })); + } + + // 3. check local pool + if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus()) { + return Ok(Some(TransactionSource::Pool(tx))); + } + + Ok(None) + } } impl OpEthApi diff --git a/crates/rpc/rpc-eth-api/src/helpers/transaction.rs b/crates/rpc/rpc-eth-api/src/helpers/transaction.rs index 8a49208cd8c..fa09027d601 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/transaction.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/transaction.rs @@ -597,45 +597,37 @@ pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt { > + Send { async move { // Try to find the transaction on disk - let mut resp = self + if let Some((tx, meta)) = self .spawn_blocking_io(move |this| { - match this - .provider() + this.provider() .transaction_by_hash_with_meta(hash) - .map_err(Self::Error::from_eth_err)? - { - None => Ok(None), - Some((tx, meta)) => { - // Note: we assume this transaction is valid, because it's mined (or - // part of pending block) and already. We don't need to - // check for pre EIP-2 because this transaction could be pre-EIP-2. - let transaction = tx - .try_into_recovered_unchecked() - .map_err(|_| EthApiError::InvalidTransactionSignature)?; - - let tx = TransactionSource::Block { - transaction, - index: meta.index, - block_hash: meta.block_hash, - block_number: meta.block_number, - base_fee: meta.base_fee, - }; - Ok(Some(tx)) - } - } + .map_err(Self::Error::from_eth_err) }) - .await?; - - if resp.is_none() { - // tx not found on disk, check pool - if let Some(tx) = - self.pool().get(&hash).map(|tx| tx.transaction.clone().into_consensus()) - { - resp = Some(TransactionSource::Pool(tx.into())); - } + .await? + { + // Note: we assume this transaction is valid, because it's mined (or + // part of pending block) and already. We don't need to + // check for pre EIP-2 because this transaction could be pre-EIP-2. + let transaction = tx + .try_into_recovered_unchecked() + .map_err(|_| EthApiError::InvalidTransactionSignature)?; + + return Ok(Some(TransactionSource::Block { + transaction, + index: meta.index, + block_hash: meta.block_hash, + block_number: meta.block_number, + base_fee: meta.base_fee, + })); } - Ok(resp) + // tx not found on disk, check pool + if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus()) + { + return Ok(Some(TransactionSource::Pool(tx.into()))); + } + + Ok(None) } } diff --git a/examples/custom-node/src/engine.rs b/examples/custom-node/src/engine.rs index 0c80e52a661..53a3dca84c6 100644 --- a/examples/custom-node/src/engine.rs +++ b/examples/custom-node/src/engine.rs @@ -68,6 +68,17 @@ impl ExecutionPayload for CustomExecutionData { } } +impl TryFrom<&reth_optimism_flashblocks::FlashBlockCompleteSequence> for CustomExecutionData { + type Error = &'static str; + + fn try_from( + sequence: &reth_optimism_flashblocks::FlashBlockCompleteSequence, + ) -> Result { + let inner = OpExecutionData::try_from(sequence)?; + Ok(Self { inner, extension: sequence.last().diff.gas_used }) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomPayloadAttributes { #[serde(flatten)] diff --git a/examples/custom-node/src/evm/config.rs b/examples/custom-node/src/evm/config.rs index a7dee31a835..f2bd3326893 100644 --- a/examples/custom-node/src/evm/config.rs +++ b/examples/custom-node/src/evm/config.rs @@ -9,6 +9,7 @@ use alloy_eips::{eip2718::WithEncoded, Decodable2718}; use alloy_evm::EvmEnv; use alloy_op_evm::OpBlockExecutionCtx; use alloy_rpc_types_engine::PayloadError; +use op_alloy_rpc_types_engine::flashblock::OpFlashblockPayloadBase; use op_revm::OpSpecId; use reth_engine_primitives::ExecutableTxIterator; use reth_ethereum::{ @@ -23,7 +24,6 @@ use reth_op::{ node::{OpEvmConfig, OpNextBlockEnvAttributes, OpRethReceiptBuilder}, primitives::SignedTransaction, }; -use reth_optimism_flashblocks::ExecutionPayloadBaseV1; use reth_rpc_api::eth::helpers::pending_block::BuildPendingEnv; use std::sync::Arc; @@ -143,8 +143,8 @@ pub struct CustomNextBlockEnvAttributes { extension: u64, } -impl From for CustomNextBlockEnvAttributes { - fn from(value: ExecutionPayloadBaseV1) -> Self { +impl From for CustomNextBlockEnvAttributes { + fn from(value: OpFlashblockPayloadBase) -> Self { Self { inner: value.into(), extension: 0 } } }