From ee5f5bd43e490b8cd6f157ce0611f179e855143d Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 15:31:01 +0800 Subject: [PATCH 01/13] chore: bump deps to reth v2.3.0 stack (revm 40, alloy 2.0.5, mega-evm cz/chore/upgrade-revm-40) --- Cargo.lock | 2233 ++++++++++++++++++++++++++++++---------------------- Cargo.toml | 56 +- 2 files changed, 1320 insertions(+), 969 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef655ed0..d89050fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addchain" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e69442aa5628ea6951fa33e24efe8313f4321a91bd729fc2f75bdfc858570" -dependencies = [ - "num-bigint 0.3.3", - "num-integer", - "num-traits", -] - [[package]] name = "adler2" version = "2.0.1" @@ -40,6 +29,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -63,22 +61,22 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.20" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc32535569185cbcb6ad5fa64d989a47bccb9a08e27284b1f2a3ccf16e6d010" +checksum = "e5fdcfed8f106be3df944054aaa42bc13ae103a3ac8a9f4b08d4f053e3a743f8" dependencies = [ "alloy-primitives", "alloy-rlp", "num_enum", + "phf 0.14.0", "serde", - "strum", ] [[package]] name = "alloy-consensus" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6440213a22df93a87ed512d2f668e7dc1d62a05642d107f82d61edc9e12370" +checksum = "83447eeb17816e172f1dfc0db1f9dc0b7c5d069bd1f7cecbecceb382bf931015" dependencies = [ "alloy-eips", "alloy-primitives", @@ -98,14 +96,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-consensus-any" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d0bea09287942405c4f9d2a4f22d1e07611c2dbd9d5bf94b75366340f9e6e0" +checksum = "5406343e306856dc2be762700e98a16904de45dee14a07f233e742ce68daff2f" dependencies = [ "alloy-consensus", "alloy-eips", @@ -125,7 +123,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] @@ -151,18 +149,47 @@ dependencies = [ "borsh", "k256", "serde", - "thiserror 2.0.17", + "thiserror 2.0.20", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b827a6d7784fe3eb3489d40699407a4cdcce74271421a01bdffe60cf573bb16" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", + "thiserror 2.0.20", +] + +[[package]] +name = "alloy-eip7928" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b12337f74cbfa451cb04dac173974814a6ff463079e1793aa09600ba8813ab" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", + "thiserror 2.0.20", ] [[package]] name = "alloy-eips" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd2c7ae05abcab4483ce821f12f285e01c0b33804e6883dd9ca1569a87ee2be" +checksum = "0dca4c89ace90684b4b77366d00631ed498c9af962079af2a5dbc593a0618a77" dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", + "alloy-eip7928 0.3.7", "alloy-primitives", "alloy-rlp", "alloy-serde", @@ -171,37 +198,38 @@ dependencies = [ "c-kzg", "derive_more", "either", + "ethereum_ssz", + "ethereum_ssz_derive", "serde", "serde_with", "sha2 0.10.9", - "thiserror 2.0.17", ] [[package]] name = "alloy-evm" -version = "0.15.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28de0dd1bbb0634ef7c3715e8e60176b77b82f8b6b15b2e35fe64cf6640f6550" +checksum = "a6e494529570a4d25eb4d1a7456d8f969c3202aceaf703e4006cec5f730aee96" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-hardforks", "alloy-primitives", + "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-sol-types", "auto_impl", "derive_more", - "op-alloy-consensus", - "op-revm", "revm", - "thiserror 2.0.17", + "thiserror 2.0.20", + "tracing", ] [[package]] name = "alloy-genesis" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc47eaae86488b07ea8e20236184944072a78784a1f4993f8ec17b3aa5d08c21" +checksum = "ab0e0fe9e6d1120ad7bb9254c3fc2b9bc80a8df42a033fb626be6559c13d5153" dependencies = [ "alloy-eips", "alloy-primitives", @@ -214,9 +242,9 @@ dependencies = [ [[package]] name = "alloy-hardforks" -version = "0.2.13" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" +checksum = "83ba208044232d14d4adbfa77e57d6329f51bc1acc21f5667bb7db72d88a0831" dependencies = [ "alloy-chains", "alloy-eip2124", @@ -227,9 +255,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" +checksum = "7c36c9d7f9021601b04bfef14a4b64849f6d73116a4e91e071d7fbfe10247901" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -239,24 +267,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003f46c54f22854a32b9cc7972660a476968008ad505427eabab49225309ec40" +checksum = "989d701ece9bccec3294991ce4b4a39dd1cecd0979ea58e86b9878679591b4f5" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", "tracing", ] [[package]] name = "alloy-network" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4029954d9406a40979f3a3b46950928a0fdcfe3ea8a9b0c17490d57e8aa0e3" +checksum = "a7db7b095b0b1db1d18ce7e91dcd2e82007f2d52bfb8125e6b64633a74a06bc3" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -275,14 +303,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-network-primitives" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7805124ad69e57bbae7731c9c344571700b2a18d351bda9e0eba521c991d1bcb" +checksum = "cd28d9bfd11729037d194f2b1d43db8642eb3f342032691f4ca96bb745479c3c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -293,9 +321,8 @@ dependencies = [ [[package]] name = "alloy-op-evm" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afe768962308a08b42fddef8a4296324f140b5a8dd0d4360038229885ce9434" +version = "0.32.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-consensus", "alloy-eips", @@ -303,35 +330,36 @@ dependencies = [ "alloy-op-hardforks", "alloy-primitives", "auto_impl", - "op-alloy-consensus", + "op-alloy", "op-revm", "revm", + "thiserror 2.0.20", ] [[package]] name = "alloy-op-hardforks" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3417f4187eaf7f7fb0d7556f0197bca26f0b23c4bb3aca0c9d566dc1c5d727a2" +version = "0.5.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-chains", "alloy-hardforks", + "alloy-primitives", "auto_impl", ] [[package]] name = "alloy-primitives" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", "derive_more", - "foldhash 0.2.0", - "hashbrown 0.16.1", + "foldhash", + "hashbrown 0.17.1", "indexmap 2.12.1", "itoa", "k256", @@ -342,16 +370,16 @@ dependencies = [ "rapidhash", "ruint", "rustc-hash", + "secp256k1 0.31.1", "serde", "sha3", - "tiny-keccak", ] [[package]] name = "alloy-provider" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369e12c92870d069e0c9dc5350377067af8a056e29e3badf8446099d7e00889" +checksum = "8955ab30418343de57b356de2ea60200f9fb8016a7ea3bc7f5c6176f01a8b1cf" dependencies = [ "alloy-chains", "alloy-consensus", @@ -361,7 +389,9 @@ dependencies = [ "alloy-network-primitives", "alloy-primitives", "alloy-rpc-client", + "alloy-rpc-types-debug", "alloy-rpc-types-eth", + "alloy-rpc-types-trace", "alloy-signer", "alloy-sol-types", "alloy-transport", @@ -376,10 +406,10 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tracing", "url", @@ -388,9 +418,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -399,9 +429,9 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "9d4311c03125e8a18296504560b9de3d75ecbd0dcda7f71e6cf2a196d57e6fba" dependencies = [ "proc-macro2", "quote", @@ -410,9 +440,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c89883fe6b7381744cbe80fef638ac488ead4f1956a4278956a1362c71cd2e" +checksum = "f5274f9ada7f558d2feaba16fadcb96022512566cf424147f9cfa2000bc2e458" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -420,7 +450,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "tokio", @@ -433,20 +463,56 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b43c1622aac2508d528743fd4cfdac1dea92d5a8fa894038488ff7edd0af0b32" +checksum = "0a6561ed4759c974d9c144500a59e3fb8c1d87327a12900d5ce455c0cae6dcb6" dependencies = [ "alloy-consensus-any", + "alloy-network-primitives", + "alloy-primitives", "alloy-rpc-types-eth", "alloy-serde", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddf06191e2b12eaaeb3a50ef29a8f3416fb74c61421b80f9d7554c4c1d59a56" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eba59e1c069f168a01982f42a57797736923b76aa854194df4930be17867e1c" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "ethereum_ssz", + "ethereum_ssz_derive", + "rand 0.8.5", + "serde", + "strum", ] [[package]] name = "alloy-rpc-types-eth" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5fafb741c19b3cca4cdd04fa215c89413491f9695a3e928dee2ae5657f607e" +checksum = "175a2a5b6017d7f61b5e4b800d21215fe8e94fe729d00828e13bb6d93dcf3492" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -460,28 +526,28 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-rpc-types-trace" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55324323aa634b01bdecb2d47462a8dce05f5505b14a6e5db361eef16eda476" +checksum = "514b4b1ce3354f65067b4fc7eb75358e0f2ec8be3340c96dea65d6894f9ca435" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", "alloy-serde", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-serde" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f180c399ca7c1e2fe17ea58343910cad0090878a696ff5a50241aee12fc529" +checksum = "3ad7efdd5e8fe92f5bab87956d6d3f717d1614c4ab6ad0d1f869b16e83b0dff9" dependencies = [ "alloy-primitives", "serde", @@ -490,9 +556,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc39ad2c0a3d2da8891f4081565780703a593f090f768f884049aa3aa929cbc" +checksum = "afa150cd076471ef21aa7b181d8186cb86c3239518859a2e5d80fbec74f0ec38" dependencies = [ "alloy-primitives", "async-trait", @@ -500,14 +566,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-signer-local" -version = "1.1.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930e17cb1e46446a193a593a3bfff8d0ecee4e510b802575ebe300ae2e43ef75" +checksum = "e48366d2c42b8d95ef951101bafa28486590f21b7a1e68b6b2d069746557bbe3" dependencies = [ "alloy-consensus", "alloy-network", @@ -516,18 +582,18 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "alloy-sol-macro" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" +checksum = "b5655c38d5f84955bf727b2eeb62fddd91ebb98fd1d7ae6eb77f73ea88f9b9cf" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", - "proc-macro-error2", + "proc-macro-error3", "proc-macro2", "quote", "syn 2.0.117", @@ -535,27 +601,27 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" +checksum = "6277c780e07b76951e09a59788dde230d1582612324177d11a43a61e21a6bb83" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", "indexmap 2.12.1", - "proc-macro-error2", + "proc-macro-error3", "proc-macro2", "quote", + "sha3", "syn 2.0.117", "syn-solidity", - "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" +checksum = "9762b2ad3e5a0c09886de54fe549ab0056681df843cb082e2df7e1c0eb270d30" dependencies = [ "const-hex", "dunce", @@ -569,19 +635,19 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" +checksum = "da4c7130f0f01f4719678bda3db3bc7267fc2f7f9d0565e3bd964cd2bb45050d" dependencies = [ "serde", - "winnow", + "winnow 1.0.4", ] [[package]] name = "alloy-sol-types" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" +checksum = "384cf252de0db2dec52821eac037a7f57e2aa33fe5b900ce6fe39973402341f1" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -591,9 +657,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae82426d98f8bc18f53c5223862907cac30ab8fc5e4cd2bb50808e6d3ab43d8" +checksum = "3cd5dbd32cae5d1bc3afaf455064ebd9bcce70b57de220468908717c225225dd" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -604,7 +670,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tower", "tracing", @@ -614,13 +680,14 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90aa6825760905898c106aba9c804b131816a15041523e80b6d4fe7af6380ada" +checksum = "f0b7b1f0e528ae788d05937f5b351afae3b4c1e08f17a061d868bd28e56aa9e3" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest", + "itertools 0.14.0", + "reqwest 0.13.4", "serde_json", "tower", "tracing", @@ -629,27 +696,27 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.1" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" dependencies = [ "alloy-primitives", "alloy-rlp", - "arrayvec", "derive_more", "nybbles", "serde", "smallvec", + "thiserror 2.0.20", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae109e33814b49fc0a62f2528993aa8a2dd346c26959b151f05441dc0b9da292" +checksum = "b3e2386593cef5b12c7d439dbcf62d7d51c3a50dc943909b42ae34b48f1b69dc" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -720,7 +787,7 @@ dependencies = [ "fnv", "hashbrown 0.15.5", "itertools 0.13.0", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "zeroize", @@ -740,7 +807,7 @@ dependencies = [ "fnv", "hashbrown 0.15.5", "itertools 0.14.0", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "rayon", @@ -769,7 +836,7 @@ dependencies = [ "ark-serialize 0.3.0", "ark-std 0.3.0", "derivative", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "rustc_version 0.3.3", @@ -789,7 +856,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "rustc_version 0.4.1", @@ -810,7 +877,7 @@ dependencies = [ "digest 0.10.7", "educe", "itertools 0.13.0", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "zeroize", @@ -829,7 +896,7 @@ dependencies = [ "digest 0.10.7", "educe", "itertools 0.14.0", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "rayon", @@ -881,7 +948,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "quote", "syn 1.0.109", @@ -893,7 +960,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -906,7 +973,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -918,7 +985,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/megaeth-labs/algebra.git?rev=80ca69c37f79d5d00750edc1602af81b5f456695#80ca69c37f79d5d00750edc1602af81b5f456695" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -965,7 +1032,7 @@ dependencies = [ "ark-relations", "ark-std 0.5.0", "educe", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "tracing", @@ -1001,7 +1068,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-std 0.4.0", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", ] [[package]] @@ -1014,7 +1081,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", ] [[package]] @@ -1026,7 +1093,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", "rayon", ] @@ -1093,9 +1160,6 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "async-compression" @@ -1176,10 +1240,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "az" -version = "1.2.1" +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] [[package]] name = "banderwagon" @@ -1212,15 +1293,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bincode" version = "2.0.1" @@ -1264,9 +1336,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -1315,6 +1387,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "blst" version = "0.3.16" @@ -1329,25 +1410,26 @@ dependencies = [ [[package]] name = "boa_ast" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c340fe0f0b267787095cbe35240c6786ff19da63ec7b69367ba338eace8169b" +checksum = "6339a700715bda376f5ea65c76e8fe8fc880930d8b0638cea68e7f3da6538e0a" dependencies = [ "bitflags", "boa_interner", "boa_macros", "boa_string", "indexmap 2.12.1", - "num-bigint 0.4.6", + "num-bigint", "rustc-hash", ] [[package]] name = "boa_engine" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f620c3f06f51e65c0504ddf04978be1b814ac6586f0b45f6019801ab5efd37f9" +checksum = "1521be326f8a5c8887e95d4ce7f002917a002a23f7b93b9a6a2bf50ed4157824" dependencies = [ + "aligned-vec", "arrayvec", "bitflags", "boa_ast", @@ -1355,73 +1437,80 @@ dependencies = [ "boa_interner", "boa_macros", "boa_parser", - "boa_profiler", "boa_string", "bytemuck", "cfg-if", + "cow-utils", "dashmap", + "dynify", "fast-float2", - "hashbrown 0.15.5", - "icu_normalizer 1.5.0", + "float16", + "futures-channel", + "futures-concurrency", + "futures-lite", + "hashbrown 0.16.1", + "icu_normalizer", "indexmap 2.12.1", "intrusive-collections", - "itertools 0.13.0", - "num-bigint 0.4.6", + "itertools 0.14.0", + "num-bigint", "num-integer", "num-traits", "num_enum", - "once_cell", - "pollster", + "paste", "portable-atomic", - "rand 0.8.5", + "rand 0.9.2", "regress", "rustc-hash", "ryu-js", "serde", "serde_json", - "sptr", + "small_btree", "static_assertions", + "tag_ptr", "tap", "thin-vec", - "thiserror 2.0.17", + "thiserror 2.0.20", "time", + "xsum", ] [[package]] name = "boa_gc" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2425c0b7720d42d73eaa6a883fbb77a5c920da8694964a3d79a67597ac55cce2" +checksum = "17323a98cf2e631afacf1a6d659c1212c48a68bacfa85afab0a66ade80582e51" dependencies = [ "boa_macros", - "boa_profiler", "boa_string", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "thin-vec", ] [[package]] name = "boa_interner" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42407a3b724cfaecde8f7d4af566df4b56af32a2f11f0956f5570bb974e7f749" +checksum = "20510b8b02bcde9b0a01cf34c0c308c56156503d1d91cdab4c8cfbd292b747ea" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap 2.12.1", "once_cell", - "phf", + "phf 0.13.1", "rustc-hash", "static_assertions", ] [[package]] name = "boa_macros" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd3f870829131332587f607a7ff909f1af5fc523fd1b192db55fbbdf52e8d3c" +checksum = "5822cb4f146d243060e588bc5a5f2e709683fdad3d7111f42c48e6b5c921d23d" dependencies = [ + "cfg-if", + "cow-utils", "proc-macro2", "quote", "syn 2.0.117", @@ -1430,39 +1519,33 @@ dependencies = [ [[package]] name = "boa_parser" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc142dac798cdc6e2dbccfddeb50f36d2523bb977a976e19bdb3ae19b740804" +checksum = "35bd957fa9fa93e3a001a8aba5a5cd40c2bbfde486378be4c4b472fd304aaddb" dependencies = [ "bitflags", "boa_ast", "boa_interner", "boa_macros", - "boa_profiler", "fast-float2", - "icu_properties 1.5.1", - "num-bigint 0.4.6", + "icu_properties", + "num-bigint", "num-traits", "regress", "rustc-hash", ] -[[package]] -name = "boa_profiler" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4064908e7cdf9b6317179e9b04dcb27f1510c1c144aeab4d0394014f37a0f922" - [[package]] name = "boa_string" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7debc13fbf7997bf38bf8e9b20f1ad5e2a7d27a900e1f6039fe244ce30f589b5" +checksum = "ca2da1d7f4a76fd9040788a122f0d807910800a7b86f5952e9244848c36511de" dependencies = [ "fast-float2", + "itoa", "paste", "rustc-hash", - "sptr", + "ryu-js", "static_assertions", ] @@ -1559,9 +1642,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.5" +version = "2.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +checksum = "38d04308254695569fdb9bfe3bacc1c91837a670d0806605eb82d63748fbd3a6" dependencies = [ "blst", "cc", @@ -1648,12 +1731,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -1681,7 +1783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1720,19 +1822,35 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[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", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1742,6 +1860,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1834,14 +1961,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1851,6 +1997,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ "ident_case", "proc-macro2", "quote", @@ -1865,7 +2024,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -1882,6 +2052,7 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core", + "serde", ] [[package]] @@ -1919,7 +2090,7 @@ dependencies = [ "quick_cache", "rayon", "redb", - "reqwest", + "reqwest 0.12.24", "revm", "revm-inspectors", "serde", @@ -1930,7 +2101,7 @@ dependencies = [ "stateless-r2", "stateless-test-utils", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tokio-util", "tower", @@ -1994,22 +2165,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version 0.4.1", "syn 2.0.117", "unicode-xid", ] @@ -2031,10 +2203,20 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2064,6 +2246,26 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dynify" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81acb15628a3e22358bf73de5e7e62360b8a777dbcb5fc9ac7dfa9ae73723747" +dependencies = [ + "dynify-macros", +] + +[[package]] +name = "dynify-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -2093,9 +2295,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -2140,6 +2342,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2156,6 +2378,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ethereum_serde_utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38df44a7a271ab43835678f9215b53cc2523e4714a215da6643d83dc110245da" +dependencies = [ + "alloy-primitives", + "hex", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "ethereum_ssz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e462875ad8693755ea8913d6e905715c76ea4836e2254e18c9cf0f7a8f8c2a13" +dependencies = [ + "alloy-primitives", + "ethereum_serde_utils", + "itertools 0.14.0", + "serde", + "serde_derive", + "smallvec", + "typenum", +] + +[[package]] +name = "ethereum_ssz_derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf022360bdbe9456eda5f35718a50476d5b2a0d51a97ed4eae27420737a6fba" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "eyre" version = "0.6.12" @@ -2206,25 +2468,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "byteorder", - "ff_derive", "rand_core 0.6.4", "subtle", ] [[package]] -name = "ff_derive" -version = "0.13.1" +name = "filetime" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "addchain", - "num-bigint 0.3.3", - "num-integer", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", + "cfg-if", + "libc", ] [[package]] @@ -2245,6 +2500,33 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixed-map" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ed19add84e8cb9e8cc5f7074de0324247149ffef0b851e215fb0edc50c229b" +dependencies = [ + "fixed-map-derive", + "serde", +] + +[[package]] +name = "fixed-map-derive" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dc7a9cb3326bafb80642c5ce99b39a2c0702d4bfa8ee8a3e773791a6cbe2407" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -2252,20 +2534,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "float16" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "7bffafbd079d520191c7c2779ae9cf757601266cf4167d3f659ff09617ff8483" +dependencies = [ + "cfg-if", + "rustc_version 0.2.3", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -2282,6 +2568,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2313,6 +2605,19 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -2336,6 +2641,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2383,12 +2701,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" -[[package]] -name = "gcd" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" - [[package]] name = "generic-array" version = "0.14.9" @@ -2433,16 +2745,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "gmp-mpfr-sys" -version = "1.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f8970a75c006bb2f8ae79c6768a116dd215fa8346a87aed99bf9d82ca43394" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - [[package]] name = "group" version = "0.13.0" @@ -2492,8 +2794,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", - "equivalent", - "foldhash 0.1.5", ] [[package]] @@ -2504,8 +2804,17 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", "rayon", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", "serde", "serde_core", ] @@ -2591,6 +2900,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2681,27 +2999,15 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.1", + "yoke", "zerofrom", - "zerovec 0.11.5", + "zerovec", ] [[package]] @@ -2711,146 +3017,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", - "litemap 0.8.1", - "tinystr 0.8.2", - "writeable 0.6.2", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "tinystr 0.7.6", - "writeable 0.5.5", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider 1.5.0", - "tinystr 0.7.6", - "zerovec 0.10.4", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", ] -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" dependencies = [ "displaydoc", - "icu_collections 1.5.0", - "icu_normalizer_data 1.5.1", - "icu_properties 1.5.1", - "icu_provider 1.5.0", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", "smallvec", "utf16_iter", - "utf8_iter", "write16", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections 2.1.1", - "icu_normalizer_data 2.1.1", - "icu_properties 2.1.1", - "icu_provider 2.1.1", - "smallvec", - "zerovec 0.11.5", + "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" dependencies = [ "displaydoc", - "icu_collections 1.5.0", - "icu_locid_transform", - "icu_properties_data 1.5.1", - "icu_provider 1.5.0", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections 2.1.1", + "icu_collections", "icu_locale_core", - "icu_properties_data 2.1.1", - "icu_provider 2.1.1", + "icu_properties_data", + "icu_provider", + "potential_utf", "zerotrie", - "zerovec 0.11.5", + "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr 0.7.6", - "writeable 0.5.5", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -2860,22 +3077,13 @@ checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "writeable 0.6.2", - "yoke 0.8.1", + "serde", + "stable_deref_trait", + "writeable", + "yoke", "zerofrom", "zerotrie", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "zerovec", ] [[package]] @@ -2901,8 +3109,8 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "icu_normalizer 2.1.1", - "icu_properties 2.1.1", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -2976,7 +3184,7 @@ dependencies = [ "rustc-hash", "salt-macros", "sha2 0.9.9", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] @@ -3004,15 +3212,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3037,6 +3236,55 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -3090,7 +3338,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tower", "tracing", @@ -3128,7 +3376,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-util", @@ -3145,7 +3393,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] @@ -3174,37 +3422,24 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" dependencies = [ - "cpufeatures", + "cfg-if", + "cpufeatures 0.3.0", ] [[package]] name = "keccak-asm" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" dependencies = [ "digest 0.10.7", "sha3-asm", ] -[[package]] -name = "kzg-rs" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9201effeea3fcc93b587904ae2df9ce97e433184b9d6d299e9ebc9830a546636" -dependencies = [ - "ff", - "hex", - "serde_arrays", - "sha2 0.10.9", - "sp1_bls12_381", - "spin 0.9.8", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3223,64 +3458,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libsecp256k1" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" -dependencies = [ - "arrayref", - "base64", - "digest 0.9.0", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.8.5", - "serde", - "sha2 0.9.9", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - [[package]] name = "litemap" version = "0.8.1" @@ -3304,11 +3487,11 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" -version = "0.13.0" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -3325,9 +3508,9 @@ checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" [[package]] name = "macro-string" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" dependencies = [ "proc-macro2", "quote", @@ -3346,7 +3529,7 @@ dependencies = [ [[package]] name = "mega-evm" version = "1.7.0" -source = "git+https://github.com/megaeth-labs/mega-evm.git?tag=v1.7.0#30ce038cfb0ec108e886ef48105a7ab367a99b33" +source = "git+https://github.com/megaeth-labs/mega-evm.git?branch=cz%2Fchore%2Fupgrade-revm-40#8d1fc07cf38e434e6a4fafc5bd2504f8482d911a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -3367,14 +3550,14 @@ dependencies = [ "op-revm", "revm", "serde", - "thiserror 2.0.17", + "thiserror 2.0.20", "tracing", ] [[package]] name = "mega-system-contracts" version = "1.7.0" -source = "git+https://github.com/megaeth-labs/mega-evm.git?tag=v1.7.0#30ce038cfb0ec108e886ef48105a7ab367a99b33" +source = "git+https://github.com/megaeth-labs/mega-evm.git?branch=cz%2Fchore%2Fupgrade-revm-40#8d1fc07cf38e434e6a4fafc5bd2504f8482d911a" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -3435,7 +3618,7 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tracing", ] @@ -3466,6 +3649,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "serde", +] + [[package]] name = "mio" version = "1.2.0" @@ -3479,9 +3672,9 @@ dependencies = [ [[package]] name = "modular-bitfield" -version = "0.11.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +checksum = "2956e537fc68236d2aa048f55704f231cc93f1c4de42fe1ecb5bd7938061fc4a" dependencies = [ "modular-bitfield-impl", "static_assertions", @@ -3489,13 +3682,22 @@ dependencies = [ [[package]] name = "modular-bitfield-impl" -version = "0.11.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +checksum = "59b43b4fd69e3437618106f7754f34021b831a514f9e1a98ae863cabcd8d8dad" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" +dependencies = [ + "serde", ] [[package]] @@ -3513,7 +3715,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-complex", "num-integer", "num-iter", @@ -3521,17 +3723,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -3584,7 +3775,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", ] @@ -3642,9 +3833,9 @@ dependencies = [ [[package]] name = "nybbles" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" dependencies = [ "alloy-rlp", "cfg-if", @@ -3665,213 +3856,143 @@ dependencies = [ ] [[package]] -name = "op-alloy-consensus" -version = "0.18.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c88d2940558fd69f8f07b3cbd7bb3c02fc7d31159c1a7ba9deede50e7881024" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "serde", - "thiserror 2.0.17", -] - -[[package]] -name = "op-alloy-flz" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" - -[[package]] -name = "op-alloy-network" -version = "0.18.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7071d7c3457d02aa0d35799cb8fbd93eabd51a21d100dcf411f4fcab6fdd2ea5" +name = "op-alloy" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-eth", - "alloy-signer", "op-alloy-consensus", + "op-alloy-network", + "op-alloy-provider", "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", ] [[package]] -name = "op-alloy-rpc-types" -version = "0.18.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22201e53e8cbb67a053e88b534b4e7f02265c5406994bf35978482a9ad0ae26" +name = "op-alloy-consensus" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-network-primitives", + "alloy-network", "alloy-primitives", + "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", + "bytes", "derive_more", - "op-alloy-consensus", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "op-revm" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce1dc7533f4e5716c55cd3d62488c6200cb4dfda96e0c75a7e484652464343b" -dependencies = [ - "auto_impl", - "once_cell", - "revm", + "reth-codecs", + "reth-zstd-compressors", "serde", + "serde_with", + "thiserror 2.0.20", ] [[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] - -[[package]] -name = "p3-baby-bear" -version = "0.2.3-succinct" +name = "op-alloy-flz" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7521838ecab2ddf4f7bc4ceebad06ec02414729598485c1ada516c39900820e8" -dependencies = [ - "num-bigint 0.4.6", - "p3-field", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.5", - "serde", -] +checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] -name = "p3-dft" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46414daedd796f1eefcdc1811c0484e4bced5729486b6eaba9521c572c76761a" +name = "op-alloy-network" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", + "alloy-consensus", + "alloy-network", + "alloy-provider", + "alloy-rpc-types-eth", + "op-alloy-consensus", + "op-alloy-rpc-types", ] [[package]] -name = "p3-field" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48948a0516b349e9d1cdb95e7236a6ee010c44e68c5cc78b4b92bf1c4022a0d9" +name = "op-alloy-provider" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "itertools 0.12.1", - "num-bigint 0.4.6", - "num-traits", - "p3-util", - "rand 0.8.5", - "serde", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-engine", + "alloy-transport", + "async-trait", + "op-alloy-rpc-types-engine", ] [[package]] -name = "p3-matrix" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4de3f373589477cb735ea58e125898ed20935e03664b4614c7fac258b3c42f" +name = "op-alloy-rpc-types" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "itertools 0.12.1", - "p3-field", - "p3-maybe-rayon", - "p3-util", - "rand 0.8.5", + "alloy-consensus", + "alloy-eips", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "derive_more", + "op-alloy-consensus", + "reth-rpc-traits", "serde", - "tracing", + "serde_json", + "thiserror 2.0.20", ] [[package]] -name = "p3-maybe-rayon" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3968ad1160310296eb04f91a5f4edfa38fe1d6b2b8cd6b5c64e6f9b7370979e" - -[[package]] -name = "p3-mds" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2356b1ed0add6d5dfbf7a338ce534a6fde827374394a52cec16a0840af6e97c9" +name = "op-alloy-rpc-types-engine" +version = "2.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "itertools 0.12.1", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-symmetric", - "p3-util", - "rand 0.8.5", + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "alloy-serde", + "ethereum_ssz", + "ethereum_ssz_derive", + "op-alloy-consensus", + "serde", + "sha2 0.10.9", + "snap", + "thiserror 2.0.20", ] [[package]] -name = "p3-poseidon2" -version = "0.2.3-succinct" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da1eec7e1b6900581bedd95e76e1ef4975608dd55be9872c9d257a8a9651c3a" +name = "op-revm" +version = "20.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ - "gcd", - "p3-field", - "p3-mds", - "p3-symmetric", - "rand 0.8.5", + "auto_impl", + "revm", "serde", ] [[package]] -name = "p3-symmetric" -version = "0.2.3-succinct" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb439bea1d822623b41ff4b51e3309e80d13cadf8b86d16ffd5e6efb9fdc360" -dependencies = [ - "itertools 0.12.1", - "p3-field", - "serde", -] +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "p3-util" -version = "0.2.3-succinct" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c2c2010678b9332b563eaa38364915b585c1a94b5ca61e2c7541c087ddda5c" -dependencies = [ - "serde", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "pairing" -version = "0.23.0" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "group", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", ] [[package]] @@ -3883,6 +4004,7 @@ dependencies = [ "arrayvec", "bitvec", "byte-slice-cast", + "bytes", "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -3902,6 +4024,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3949,33 +4077,42 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.13.1", "serde", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_shared 0.14.0", +] + [[package]] name = "phf_generator" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "phf_shared", - "rand 0.8.5", + "fastrand", + "phf_shared 0.13.1", ] [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.117", @@ -3983,9 +4120,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" dependencies = [ "siphasher", ] @@ -4038,12 +4184,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - [[package]] name = "portable-atomic" version = "1.11.1" @@ -4056,7 +4196,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "zerovec 0.11.5", + "zerovec", ] [[package]] @@ -4104,22 +4244,22 @@ dependencies = [ ] [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" +name = "proc-macro-error-attr3" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +checksum = "82366fd7d8b7a440d66d13418820c69df9b3908bcb1a0476d7f5ce5d12f5a04d" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "proc-macro-error2" -version = "2.0.1" +name = "proc-macro-error3" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +checksum = "b511283ea8a74b4b39447b128c5d00f03a356b7424554b13e298a5550100d9ac" dependencies = [ - "proc-macro-error-attr2", + "proc-macro-error-attr3", "proc-macro2", "quote", "syn 2.0.117", @@ -4198,7 +4338,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -4210,6 +4350,7 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -4219,7 +4360,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -4497,10 +4638,47 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reth-chainspec" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4519,8 +4697,9 @@ dependencies = [ [[package]] name = "reth-codecs" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c37bb4d1bac98661bf9128e1141b969034640d68697b22f70377e492fd332c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -4529,7 +4708,7 @@ dependencies = [ "alloy-trie", "bytes", "modular-bitfield", - "op-alloy-consensus", + "parity-scale-codec", "reth-codecs-derive", "reth-zstd-compressors", "serde", @@ -4537,10 +4716,10 @@ dependencies = [ [[package]] name = "reth-codecs-derive" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceeb8dab90194305d3f7e3f043a22d2db1fb54b6dcf5d8e75c5a4fa8540e1f57" dependencies = [ - "convert_case", "proc-macro2", "quote", "syn 2.0.117", @@ -4548,8 +4727,8 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -4561,34 +4740,34 @@ dependencies = [ [[package]] name = "reth-execution-errors" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-evm", "alloy-primitives", "alloy-rlp", "nybbles", "reth-storage-errors", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "reth-network-peers" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-primitives", "alloy-rlp", "secp256k1 0.30.0", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.20", "url", ] [[package]] name = "reth-optimism-chainspec" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "1.11.3" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4597,7 +4776,7 @@ dependencies = [ "alloy-hardforks", "alloy-primitives", "derive_more", - "miniz_oxide", + "miniz_oxide 0.9.1", "op-alloy-consensus", "op-alloy-rpc-types", "reth-chainspec", @@ -4608,13 +4787,17 @@ dependencies = [ "reth-primitives-traits", "serde", "serde_json", - "thiserror 2.0.17", + "sha2 0.10.9", + "tar", + "thiserror 2.0.20", + "toml", + "zstd", ] [[package]] name = "reth-optimism-forks" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "1.11.3" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-op-hardforks", "alloy-primitives", @@ -4624,26 +4807,24 @@ dependencies = [ [[package]] name = "reth-optimism-primitives" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "1.11.3" +source = "git+https://github.com/ethereum-optimism/optimism?rev=f67d87cd5c9992b62d215c043917b94e3f019629#f67d87cd5c9992b62d215c043917b94e3f019629" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", "alloy-rlp", - "bytes", "op-alloy-consensus", - "reth-codecs", "reth-primitives-traits", - "reth-zstd-compressors", "serde", "serde_with", ] [[package]] name = "reth-primitives-traits" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9102518f0bbf99bc8f0e656a56fb2a7513248630275b608d3706076a82fde42b" dependencies = [ "alloy-consensus", "alloy-eips", @@ -4652,63 +4833,97 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-trie", - "auto_impl", "bytes", + "dashmap", "derive_more", "once_cell", - "op-alloy-consensus", + "rayon", "reth-codecs", "revm-bytecode", "revm-primitives", "revm-state", "secp256k1 0.30.0", "serde", - "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "reth-prune-types" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-primitives", "derive_more", + "reth-codecs", + "serde", + "strum", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "reth-rpc-traits" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa03a2c9681c385ae1c72b169ac9c3525163b71db0b3362f16e8929e19e45fd" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-signer", + "reth-primitives-traits", + "thiserror 2.0.20", +] + +[[package]] +name = "reth-stages-types" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" +dependencies = [ + "alloy-primitives", + "bytes", + "reth-codecs", + "reth-trie-common", "serde", - "thiserror 2.0.17", ] [[package]] name = "reth-static-file-types" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-primitives", "derive_more", + "fixed-map", + "reth-stages-types", "serde", "strum", + "tracing", ] [[package]] name = "reth-storage-errors" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", "derive_more", + "reth-codecs", "reth-primitives-traits", "reth-prune-types", "reth-static-file-types", "revm-database-interface", - "thiserror 2.0.17", + "revm-state", + "thiserror 2.0.20", ] [[package]] name = "reth-trie-common" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -4716,10 +4931,12 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "alloy-trie", + "arrayvec", "bytes", "derive_more", "itertools 0.14.0", "nybbles", + "reth-codecs", "reth-primitives-traits", "revm-database", "serde", @@ -4728,33 +4945,38 @@ dependencies = [ [[package]] name = "reth-trie-sparse" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "2.3.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-trie", - "auto_impl", + "either", + "rayon", "reth-execution-errors", "reth-primitives-traits", "reth-trie-common", + "serde", + "serde_json", + "slotmap", "smallvec", "tracing", ] [[package]] name = "reth-zstd-compressors" -version = "1.6.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e86cf594718932d42cebce1fa48292fb7b92721f7c914631add1ca970e814" dependencies = [ "zstd", ] [[package]] name = "revm" -version = "27.1.0" +version = "40.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6bf82101a1ad8a2b637363a37aef27f88b4efc8a6e24c72bf5f64923dc5532" +checksum = "823da6e5509bb8e5dcd91295870e494917a030ad506fc83301f3f08ad8b15b17" dependencies = [ "revm-bytecode", "revm-context", @@ -4771,22 +4993,23 @@ dependencies = [ [[package]] name = "revm-bytecode" -version = "6.2.2" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c52031b73cae95d84cd1b07725808b5fd1500da3e5e24574a3b2dc13d9f16d" +checksum = "d8b378c2653331fe60969d9745e802cd773d82a20d8aaced914dfcf26ab8f0d9" dependencies = [ "bitvec", - "phf", + "phf 0.13.1", "revm-primitives", "serde", ] [[package]] name = "revm-context" -version = "8.0.4" +version = "18.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd508416a35a4d8a9feaf5ccd06ac6d6661cd31ee2dc0252f9f7316455d71f9" +checksum = "bafa298114f3cab706945de14c04e73e6e6d7896302e4183dae273f968e52f80" dependencies = [ + "bitvec", "cfg-if", "derive-where", "revm-bytecode", @@ -4799,9 +5022,9 @@ dependencies = [ [[package]] name = "revm-context-interface" -version = "9.0.0" +version = "19.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc90302642d21c8f93e0876e201f3c5f7913c4fcb66fb465b0fd7b707dfe1c79" +checksum = "db9c13f1dfc79425931fd184b6bd373dfac7baba50859b01107d5c0e20549cbb" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -4815,13 +5038,14 @@ dependencies = [ [[package]] name = "revm-database" -version = "7.0.5" +version = "15.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a276ed142b4718dcf64bc9624f474373ed82ef20611025045c3fb23edbef9c" +checksum = "a69c3ce73454a09ef89a66177239d7c4f5f697227ae27254c99451866603b19d" dependencies = [ "alloy-eips", "alloy-provider", "alloy-transport", + "derive_more", "revm-bytecode", "revm-database-interface", "revm-primitives", @@ -4832,23 +5056,24 @@ dependencies = [ [[package]] name = "revm-database-interface" -version = "7.0.5" +version = "12.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c523c77e74eeedbac5d6f7c092e3851dbe9c7fec6f418b85992bd79229db361" +checksum = "4a2656187f9f9c22ef9dd9300ed71aeaeca3506a6a0a229a07f264649b960d68" dependencies = [ "auto_impl", "either", "revm-primitives", "revm-state", "serde", + "thiserror 2.0.20", "tokio", ] [[package]] name = "revm-handler" -version = "8.1.0" +version = "20.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1529c8050e663be64010e80ec92bf480315d21b1f2dbf65540028653a621b27d" +checksum = "3ce1d66037ca1394128313bb995fa9f50d834927a389386bb34f8f0ef914648f" dependencies = [ "auto_impl", "derive-where", @@ -4865,9 +5090,9 @@ dependencies = [ [[package]] name = "revm-inspector" -version = "8.1.0" +version = "21.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78db140e332489094ef314eaeb0bd1849d6d01172c113ab0eb6ea8ab9372926" +checksum = "9fe3635d3411e8318849546570ca0220e783443319e28f5397c9f80b05bf4344" dependencies = [ "auto_impl", "either", @@ -4883,9 +5108,9 @@ dependencies = [ [[package]] name = "revm-inspectors" -version = "0.27.3" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2483827fec2c31db4ada14e0b5523c377d1084337d1dbe135faaf14c218ab1af" +checksum = "15e0cdc845c8dbf41255c9add80923b45df7bf1dd0473e41483ac398005dba12" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -4898,26 +5123,27 @@ dependencies = [ "revm", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "revm-interpreter" -version = "24.0.0" +version = "37.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9d7d9d71e8a33740b277b602165b6e3d25fff091ba3d7b5a8d373bf55f28a7" +checksum = "bae56c57ddca1f5c4abd443f826f1b3e49a86a528e7e1ea0fc207cdc4671a37e" dependencies = [ "revm-bytecode", "revm-context-interface", "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-precompile" -version = "25.0.0" +version = "36.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cee3f336b83621294b4cfe84d817e3eef6f3d0fce00951973364cc7f860424d" +checksum = "191db865091e07ecb80b12ce3048192c76071ca3d2b0a315b111b271cd4ced37" dependencies = [ "ark-bls12-381 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "ark-bn254", @@ -4930,36 +5156,34 @@ dependencies = [ "c-kzg", "cfg-if", "k256", - "kzg-rs", - "libsecp256k1", - "once_cell", "p256", + "revm-context-interface", "revm-primitives", "ripemd", - "rug", "secp256k1 0.31.1", "sha2 0.10.9", ] [[package]] name = "revm-primitives" -version = "20.2.1" +version = "24.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa29d9da06fe03b249b6419b33968ecdf92ad6428e2f012dc57bcd619b5d94e" +checksum = "fe5102d804892908d4ebf68da29b8562895922dffa26c230ff2c4dadcf93916f" dependencies = [ "alloy-primitives", - "num_enum", "once_cell", "serde", ] [[package]] name = "revm-state" -version = "7.0.5" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f64fbacb86008394aaebd3454f9643b7d5a782bd251135e17c5b33da592d84d" +checksum = "40eff6067185cf80932e06f6a9c8045b012ecb6f99a8d6edc618ec2792373e14" dependencies = [ + "alloy-eip7928 0.4.5", "bitflags", + "nonmax", "revm-bytecode", "revm-primitives", "serde", @@ -5023,18 +5247,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" -[[package]] -name = "rug" -version = "1.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad2e973fe3c3214251a840a621812a4f40468da814b1a3d6947d433c2af11f" -dependencies = [ - "az", - "gmp-mpfr-sys", - "libc", - "libm", -] - [[package]] name = "ruint" version = "1.17.2" @@ -5048,7 +5260,7 @@ dependencies = [ "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "parity-scale-codec", @@ -5081,6 +5293,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.3.3" @@ -5118,12 +5339,25 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "once_cell", - "ring", + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "schannel", + "security-framework", ] [[package]] @@ -5136,12 +5370,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5194,8 +5456,8 @@ dependencies = [ "salt-macros", "serde", "serde_arrays", - "spin 0.10.0", - "thiserror 2.0.17", + "spin", + "thiserror 2.0.20", "zerocopy 0.7.35", ] @@ -5207,6 +5469,24 @@ dependencies = [ "rayon", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -5293,13 +5573,45 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + [[package]] name = "semver" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "semver-parser", + "semver-parser 0.10.3", ] [[package]] @@ -5312,6 +5624,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "semver-parser" version = "0.10.3" @@ -5362,16 +5680,25 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.12.1", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", ] [[package]] @@ -5411,7 +5738,7 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -5434,7 +5761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5446,7 +5773,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5458,25 +5785,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ - "digest 0.10.7", + "digest 0.11.3", "keccak", ] [[package]] name = "sha3-asm" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" dependencies = [ "cc", "cfg-if", @@ -5522,6 +5849,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -5540,6 +5883,24 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "small_btree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba60d2df92ba73864714808ca68c059734853e6ab722b40e1cf543ebb3a057a" +dependencies = [ + "arrayvec", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -5549,6 +5910,12 @@ dependencies = [ "serde", ] +[[package]] +name = "snap" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886" + [[package]] name = "socket2" version = "0.6.3" @@ -5575,58 +5942,6 @@ dependencies = [ "sha1", ] -[[package]] -name = "sp1-lib" -version = "5.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1a9935d58cb1dcd757a1b10d727090f5b718f1f03b512d48f0c1952e6ead00" -dependencies = [ - "bincode 1.3.3", - "serde", - "sp1-primitives", -] - -[[package]] -name = "sp1-primitives" -version = "5.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d2a6187e394c30097ea7a975a4832f172918690dc89a979f0fad67422d3a8b" -dependencies = [ - "bincode 1.3.3", - "blake3", - "cfg-if", - "hex", - "lazy_static", - "num-bigint 0.4.6", - "p3-baby-bear", - "p3-field", - "p3-poseidon2", - "p3-symmetric", - "serde", - "sha2 0.10.9", -] - -[[package]] -name = "sp1_bls12_381" -version = "0.8.0-sp1-5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac255e1704ebcdeec5e02f6a0ebc4d2e9e6b802161938330b6810c13a610c583" -dependencies = [ - "cfg-if", - "ff", - "group", - "pairing", - "rand_core 0.6.4", - "sp1-lib", - "subtle", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spin" version = "0.10.0" @@ -5643,12 +5958,6 @@ dependencies = [ "der", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5665,7 +5974,7 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-trie", "base64", - "bincode 2.0.1", + "bincode", "clap", "eyre", "fastrand", @@ -5674,14 +5983,14 @@ dependencies = [ "kanal", "op-alloy-network", "op-alloy-rpc-types", - "reqwest", + "reqwest 0.12.24", "revm", "rolling-file", "salt", "serde", "stateless-core", "stateless-test-utils", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -5705,7 +6014,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", - "bincode 2.0.1", + "bincode", "dyn-clone", "eyre", "hashbrown 0.16.1", @@ -5724,7 +6033,7 @@ dependencies = [ "serde", "serde_json", "stateless-test-utils", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -5736,7 +6045,7 @@ version = "2.0.16" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "bincode 2.0.1", + "bincode", "dashmap", "lz4_flex", "op-alloy-rpc-types", @@ -5759,7 +6068,7 @@ dependencies = [ "hex", "hmac", "percent-encoding", - "reqwest", + "reqwest 0.12.24", "sha2 0.10.9", "stateless-test-utils", "tokio", @@ -5773,7 +6082,7 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-rpc-types-eth", - "bincode 2.0.1", + "bincode", "eyre", "op-alloy-rpc-types", "revm", @@ -5809,7 +6118,7 @@ dependencies = [ "stateless-r2", "stateless-test-utils", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -5876,11 +6185,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-solidity" -version = "1.4.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" +checksum = "083be3061e64d362cbe6ef12cfe1307ba3884326d8856448fe8a120fa2c44ebf" dependencies = [ "paste", "proc-macro2", @@ -5908,12 +6228,29 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tag_ptr" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05" + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -5944,11 +6281,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.20", ] [[package]] @@ -5964,13 +6301,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -5999,7 +6336,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "libc", "num-conv", "num_threads", @@ -6025,25 +6361,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec 0.10.4", -] - [[package]] name = "tinystr" version = "0.8.2" @@ -6051,7 +6368,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", - "zerovec 0.11.5", + "serde_core", + "zerovec", ] [[package]] @@ -6132,11 +6450,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -6150,18 +6483,24 @@ dependencies = [ "indexmap 2.12.1", "toml_datetime", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow", + "winnow 1.0.4", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tower" version = "0.5.2" @@ -6301,9 +6640,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -6404,6 +6743,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -6520,6 +6869,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -6545,6 +6903,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -6775,6 +7142,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -6787,12 +7163,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "writeable" version = "0.6.2" @@ -6809,17 +7179,21 @@ dependencies = [ ] [[package]] -name = "yoke" -version = "0.7.5" +name = "xattr" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", + "libc", + "rustix", ] +[[package]] +name = "xsum" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" + [[package]] name = "yoke" version = "0.8.1" @@ -6827,22 +7201,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - [[package]] name = "yoke-derive" version = "0.8.1" @@ -6943,19 +7305,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", - "yoke 0.8.1", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke 0.7.5", + "yoke", "zerofrom", - "zerovec-derive 0.10.3", ] [[package]] @@ -6964,16 +7315,17 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "yoke 0.8.1", + "serde", + "yoke", "zerofrom", - "zerovec-derive 0.11.2", + "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -6981,15 +7333,10 @@ dependencies = [ ] [[package]] -name = "zerovec-derive" -version = "0.11.2" +name = "zmij" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index c021fc3f..3627b924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,44 +20,48 @@ exclude = [".github/"] [workspace.dependencies] # alloy -alloy-consensus = { version = "1.0.23", default-features = false } -alloy-eips = { version = "1.0.23", default-features = false } -alloy-evm = { version = "0.15.0", default-features = false } -alloy-genesis = { version = "1.0.23", default-features = false } -alloy-hardforks = { version = "0.2.7", default-features = false } -alloy-network = "1.0.23" -alloy-op-evm = { version = "0.15.0", default-features = false } -alloy-op-hardforks = { version = "0.2.7", default-features = false } -alloy-primitives = { version = "1.3.0", default-features = false } -alloy-provider = { version = "1.0.23", features = [ +alloy-consensus = { version = "2.0.5", default-features = false } +alloy-eips = { version = "2.0.5", default-features = false } +alloy-evm = { version = "0.36.0", default-features = false } +alloy-genesis = { version = "2.0.5", default-features = false } +alloy-hardforks = { version = "0.4.7", default-features = false } +alloy-network = "2.0.5" +alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +alloy-primitives = { version = "1.6.0", default-features = false } +alloy-provider = { version = "2.0.5", features = [ "reqwest", "reqwest-rustls-tls", ], default-features = false } -alloy-rlp = { version = "0.3.10", default-features = false } -alloy-rpc-types-eth = { version = "1.0.23", default-features = false } -alloy-rpc-types-trace = "1.0.23" -alloy-serde = { version = "1.0.23", default-features = false } -alloy-signer-local = "1.0.23" -alloy-trie = { version = "0.9.0", default-features = false } +alloy-rlp = { version = "0.3.13", default-features = false } +alloy-rpc-types-eth = { version = "2.0.5", default-features = false } +alloy-rpc-types-trace = "2.0.5" +alloy-serde = { version = "2.0.5", default-features = false } +alloy-signer-local = "2.0.5" +alloy-trie = { version = "0.9.4", default-features = false } # mega -mega-evm = { git = "https://github.com/megaeth-labs/mega-evm.git", tag = "v1.7.0", default-features = false } +mega-evm = { git = "https://github.com/megaeth-labs/mega-evm.git", branch = "cz/chore/upgrade-revm-40", default-features = false } salt = { git = "https://github.com/megaeth-labs/salt.git", tag = "v1.0.5", default-features = false } # op -op-alloy-consensus = { version = "0.18.12", default-features = false } -op-alloy-network = { version = "0.18.12", default-features = false } -op-alloy-rpc-types = { version = "0.18.12", default-features = false } +# The OP crates are pinned to the same Optimism monorepo revision as mega-evm's +# cz/chore/upgrade-revm-40 branch: crates.io has no op-revm / alloy-op-evm release built +# against revm 40, and every OP crate must share one source identity for OpTxEnvelope, +# receipt, and hardfork types. reth-optimism-chainspec also lives in that monorepo now. +op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } # reth -reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0", default-features = false } -reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0", default-features = false } -reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0", default-features = false } -reth-trie-sparse = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0", default-features = false } +reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } +reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } +reth-trie-sparse = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } # revm -revm = { version = "27.1.0", default-features = false } -revm-inspectors = { version = "0.27.3", features = ["std", "js-tracer"], default-features = false } +revm = { version = "40.0.3", default-features = false } +revm-inspectors = { version = "0.40.1", features = ["std", "js-tracer"], default-features = false } # misc base64 = { version = "0.22", default-features = false } From 88f5bb9f05beec731263da564c417f3dcffaa8e4 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 15:41:09 +0800 Subject: [PATCH 02/13] refactor: migrate stateless-core to reth v2.3.0 / revm 40 APIs --- crates/stateless-core/src/evm_database.rs | 3 +++ crates/stateless-core/src/executor.rs | 15 +++++++----- crates/stateless-core/src/withdrawals.rs | 29 ++++++++++------------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/stateless-core/src/evm_database.rs b/crates/stateless-core/src/evm_database.rs index abdf2f3e..afe042a3 100644 --- a/crates/stateless-core/src/evm_database.rs +++ b/crates/stateless-core/src/evm_database.rs @@ -110,6 +110,9 @@ where nonce: acc.nonce, code_hash: acc.codehash.unwrap_or(KECCAK_EMPTY), code, + // Witness state is keyed by address only; the id-based storage + // lookup fast path is never used. + account_id: None, })) } None => Ok(None), diff --git a/crates/stateless-core/src/executor.rs b/crates/stateless-core/src/executor.rs index daee20fd..f46b5f14 100644 --- a/crates/stateless-core/src/executor.rs +++ b/crates/stateless-core/src/executor.rs @@ -40,8 +40,8 @@ use alloy_evm::{ }; use alloy_op_evm::block::OpAlloyReceiptBuilder; use alloy_primitives::{ - Address, Bloom, keccak256, - map::{B256Map, HashMap}, + Bloom, keccak256, + map::{AddressMap, B256Map, HashMap}, }; use alloy_rpc_types_eth::{Block, BlockTransactions}; use mega_evm::{ @@ -278,6 +278,9 @@ pub fn create_evm_env( difficulty: header.difficulty, prevrandao: Some(header.mix_hash), blob_excess_gas_and_price: None, + // EIP-7843 slot number. MegaETH has not activated Amsterdam, so headers carry + // no slot number and this stays 0 (the upstream `unwrap_or_default()` path). + slot_num: 0, }; if let Some(excess_blob_gas) = header.excess_blob_gas { @@ -374,7 +377,7 @@ pub fn replay_block( db: &DB, env_oracle: ENV, #[cfg(feature = "std")] trace_writer: Option>, -) -> Result<(HashMap, BlockExecutionOutput), ValidationError> +) -> Result<(AddressMap, BlockExecutionOutput), ValidationError> where B: BlockInput, DB: DatabaseRef + Debug, @@ -506,7 +509,7 @@ where /// Extracts the withdrawal-contract storage updates (only changed slots) from the replayed /// accounts, keyed by the hashed slot as [`MptWitness::verify`] expects. -fn withdrawal_storage(accounts: &HashMap) -> B256Map { +fn withdrawal_storage(accounts: &AddressMap) -> B256Map { accounts .get(&ADDRESS_L2_TO_L1_MESSAGE_PASSER) .map(|a| { @@ -527,7 +530,7 @@ fn withdrawal_storage(accounts: &HashMap) -> B256Map, + accounts: AddressMap, ) -> Result { // Flatten Revm's BundleAccount format into plain key-value pairs let mut kv_updates: BTreeMap, Option>> = BTreeMap::new(); @@ -651,7 +654,7 @@ struct VerifiedReplay { /// The proof-verified witness the block was replayed over. witness: Witness, /// Net per-account state changes from the replay. - accounts: HashMap, + accounts: AddressMap, /// Execution outputs claimed by the header, plus state access counts. output: BlockExecutionOutput, /// Stats for the completed stages; [`ValidationStats::salt_update_time`] is left `0.0` diff --git a/crates/stateless-core/src/withdrawals.rs b/crates/stateless-core/src/withdrawals.rs index 9a0ab048..56dbd0bc 100644 --- a/crates/stateless-core/src/withdrawals.rs +++ b/crates/stateless-core/src/withdrawals.rs @@ -13,10 +13,10 @@ use std::{ use alloy_consensus::{Header, constants::KECCAK_EMPTY}; use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256, map::B256Map}; use alloy_rlp::Encodable; -use reth_trie_common::{EMPTY_ROOT_HASH, LeafNode, Nibbles, TrieAccount, TrieNode}; -use reth_trie_sparse::{ - SerialSparseTrie, SparseStateTrie, SparseTrie, provider::DefaultTrieNodeProviderFactory, +use reth_trie_common::{ + DecodedMultiProofV2, EMPTY_ROOT_HASH, LeafNode, Nibbles, TrieAccount, TrieNode, }; +use reth_trie_sparse::{RevealableSparseTrie, SparseStateTrie}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -87,26 +87,28 @@ impl MptWitness { // Synthesize account-level witness and reveal via BFS let (synthesized_state_root, witness_map, hashed_address) = synthesize_state_witness(self); - let mut state_trie = SparseStateTrie::::default(); + let mut state_trie = SparseStateTrie::default(); + let multiproof = DecodedMultiProofV2::from_witness(synthesized_state_root, &witness_map) + .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; state_trie - .reveal_witness(synthesized_state_root, &witness_map) + .reveal_decoded_multiproof_v2(multiproof) .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - // `reveal_witness` skips storage subtrees whose root is `EMPTY_ROOT_HASH`, + // `from_witness` skips storage subtrees whose root is `EMPTY_ROOT_HASH`, // leaving the per-address storage trie blind. That's fine for pre-state // verification, but if the block writes any non-zero slot to an // empty-pre-state trie (e.g. the first-ever withdrawal), the subsequent // `update_storage_leaf` call would fail with "sparse trie is blind". // Insert a revealed-empty storage trie so writes can proceed. if self.storage_root == EMPTY_ROOT_HASH { - state_trie.insert_storage_trie(hashed_address, SparseTrie::revealed_empty()); + state_trie.insert_storage_trie(hashed_address, RevealableSparseTrie::revealed_empty()); } // Verify pre-state root. `storage_root` returns `None` when the storage // trie wasn't revealed; that only happens when `self.storage_root` was // non-empty and reveal_witness failed to populate it, which the check // below will catch as a mismatch. - let pre_root = state_trie.storage_root(hashed_address).unwrap_or(EMPTY_ROOT_HASH); + let pre_root = state_trie.storage_root(&hashed_address).unwrap_or(EMPTY_ROOT_HASH); if pre_root != self.storage_root { return Err(WithdrawalValidationError::PreStateRootMismatch { expected: self.storage_root, @@ -120,22 +122,17 @@ impl MptWitness { if !value.is_zero() { let encoded = alloy_rlp::encode_fixed_size(&value).to_vec(); state_trie - .update_storage_leaf( - hashed_address, - nibbles, - encoded, - DefaultTrieNodeProviderFactory, - ) + .update_storage_leaf(hashed_address, nibbles, encoded) .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; } else { state_trie - .remove_storage_leaf(hashed_address, &nibbles, DefaultTrieNodeProviderFactory) + .remove_storage_leaf(hashed_address, &nibbles) .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; } } // Verify post-state root (same empty-trie caveat as pre-state). - let post_root = state_trie.storage_root(hashed_address).unwrap_or(EMPTY_ROOT_HASH); + let post_root = state_trie.storage_root(&hashed_address).unwrap_or(EMPTY_ROOT_HASH); if post_root != expected_post_root { return Err(WithdrawalValidationError::PostStateRootMismatch { expected: expected_post_root, From 9c1bd3010939cf130a6e589236b1b2c8880f1fec Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 16:13:06 +0800 Subject: [PATCH 03/13] fix: vendor reth-1.6 serial sparse trie for withdrawal MPT verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reth v2.3.0's V2 sparse tries merge extension nodes into their child branches and silently drop extensions whose child branch is absent from the witness — a legal minimal exclusion proof that the frozen MegaETH witness format produces, and whose split-on-insert never needs the child. Vendors the proven v1.6.0 serial sparse trie into stateless-core, reveals the storage trie directly (dropping the synthesized account-leaf wrapper reveal_witness required), and adds a regression test for the extension-split shape. --- Cargo.lock | 125 +- Cargo.toml | 3 +- .../src/block_data_cache.rs | 1 + bin/debug-trace-server/src/metrics.rs | 1 + bin/debug-trace-server/src/response_cache.rs | 3 + .../src/tracing_executor.rs | 62 +- crates/stateless-core/Cargo.toml | 4 +- crates/stateless-core/src/lib.rs | 3 + .../{withdrawals.rs => withdrawals/mod.rs} | 246 ++- .../src/withdrawals/sparse_trie/errors.rs | 81 + .../src/withdrawals/sparse_trie/mod.rs | 15 + .../src/withdrawals/sparse_trie/provider.rs | 87 + .../src/withdrawals/sparse_trie/traits.rs | 326 +++ .../src/withdrawals/sparse_trie/trie.rs | 1933 +++++++++++++++++ 14 files changed, 2649 insertions(+), 241 deletions(-) rename crates/stateless-core/src/{withdrawals.rs => withdrawals/mod.rs} (50%) create mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/errors.rs create mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/mod.rs create mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/provider.rs create mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/traits.rs create mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/trie.rs diff --git a/Cargo.lock b/Cargo.lock index d89050fe..25f58714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2500,27 +2500,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "fixed-map" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ed19add84e8cb9e8cc5f7074de0324247149ffef0b851e215fb0edc50c229b" -dependencies = [ - "fixed-map-derive", - "serde", -] - -[[package]] -name = "fixed-map-derive" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dc7a9cb3326bafb80642c5ce99b39a2c0702d4bfa8ee8a3e773791a6cbe2407" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "fixedbitset" version = "0.5.7" @@ -4738,19 +4717,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "reth-execution-errors" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-evm", - "alloy-primitives", - "alloy-rlp", - "nybbles", - "reth-storage-errors", - "thiserror 2.0.20", -] - [[package]] name = "reth-network-peers" version = "2.3.0" @@ -4837,7 +4803,6 @@ dependencies = [ "dashmap", "derive_more", "once_cell", - "rayon", "reth-codecs", "revm-bytecode", "revm-primitives", @@ -4847,20 +4812,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "reth-prune-types" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-primitives", - "derive_more", - "reth-codecs", - "serde", - "strum", - "thiserror 2.0.20", - "tracing", -] - [[package]] name = "reth-rpc-traits" version = "0.4.2" @@ -4876,50 +4827,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "reth-stages-types" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-primitives", - "bytes", - "reth-codecs", - "reth-trie-common", - "serde", -] - -[[package]] -name = "reth-static-file-types" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-primitives", - "derive_more", - "fixed-map", - "reth-stages-types", - "serde", - "strum", - "tracing", -] - -[[package]] -name = "reth-storage-errors" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more", - "reth-codecs", - "reth-primitives-traits", - "reth-prune-types", - "reth-static-file-types", - "revm-database-interface", - "revm-state", - "thiserror 2.0.20", -] - [[package]] name = "reth-trie-common" version = "2.3.0" @@ -4943,26 +4850,6 @@ dependencies = [ "serde_with", ] -[[package]] -name = "reth-trie-sparse" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "either", - "rayon", - "reth-execution-errors", - "reth-primitives-traits", - "reth-trie-common", - "serde", - "serde_json", - "slotmap", - "smallvec", - "tracing", -] - [[package]] name = "reth-zstd-compressors" version = "0.4.2" @@ -5883,15 +5770,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - [[package]] name = "small_btree" version = "0.1.0" @@ -6014,6 +5892,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", + "auto_impl", "bincode", "dyn-clone", "eyre", @@ -6026,12 +5905,12 @@ dependencies = [ "reth-ethereum-forks", "reth-optimism-chainspec", "reth-trie-common", - "reth-trie-sparse", "revm", "rustc-hash", "salt", "serde", "serde_json", + "smallvec", "stateless-test-utils", "thiserror 2.0.20", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3627b924..4846e362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,13 +57,13 @@ op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", re reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } -reth-trie-sparse = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } # revm revm = { version = "40.0.3", default-features = false } revm-inspectors = { version = "0.40.1", features = ["std", "js-tracer"], default-features = false } # misc +auto_impl = { version = "1.3", default-features = false } base64 = { version = "0.22", default-features = false } bincode = { version = "2.0", features = ["serde", "alloc"], default-features = false } bytes = "1.11" @@ -102,6 +102,7 @@ rustc-hash = { version = "2.1", default-features = false } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } sha2 = { version = "0.10", default-features = false } +smallvec = { version = "1.15", features = ["const_new"], default-features = false } tempfile = { version = "3.27", default-features = false } thiserror = { version = "2.0", default-features = false } tokio = { version = "1.51", features = ["rt-multi-thread", "signal"], default-features = false } diff --git a/bin/debug-trace-server/src/block_data_cache.rs b/bin/debug-trace-server/src/block_data_cache.rs index 61959654..0a55dada 100644 --- a/bin/debug-trace-server/src/block_data_cache.rs +++ b/bin/debug-trace-server/src/block_data_cache.rs @@ -294,6 +294,7 @@ mod tests { inner: Recovered::new_unchecked(OpTxEnvelope::Eip7702(signed), Address::ZERO), block_hash: None, block_number: None, + block_timestamp: None, transaction_index: None, effective_gas_price: None, }, diff --git a/bin/debug-trace-server/src/metrics.rs b/bin/debug-trace-server/src/metrics.rs index 459492fe..eab0dd2c 100644 --- a/bin/debug-trace-server/src/metrics.rs +++ b/bin/debug-trace-server/src/metrics.rs @@ -397,6 +397,7 @@ pub const REQUEST_SHAPES: &[&str] = &[ "struct_logger_config", "js_tracer", "mux_tracer", + "erc7562_tracer", ]; /// Records one request of the given parameter shape for `method`. diff --git a/bin/debug-trace-server/src/response_cache.rs b/bin/debug-trace-server/src/response_cache.rs index 89e0fa2a..bef1b81f 100644 --- a/bin/debug-trace-server/src/response_cache.rs +++ b/bin/debug-trace-server/src/response_cache.rs @@ -235,6 +235,9 @@ impl RequestShape { // Exhaustive on purpose: a future alloy builtin variant must make an // explicit cache-whitelist decision here instead of silently bypassing. GethDebugBuiltInTracerType::MuxTracer => Self::Bypass("mux_tracer"), + // Not implemented by the tracing executor (rejected there with a + // request error), so it must not claim a cache slot. + GethDebugBuiltInTracerType::Erc7562Tracer => Self::Bypass("erc7562_tracer"), }, Some(GethDebugTracerType::JsTracer(_)) => Self::Bypass("js_tracer"), } diff --git a/bin/debug-trace-server/src/tracing_executor.rs b/bin/debug-trace-server/src/tracing_executor.rs index 50cd47f6..36ca5528 100644 --- a/bin/debug-trace-server/src/tracing_executor.rs +++ b/bin/debug-trace-server/src/tracing_executor.rs @@ -175,6 +175,7 @@ fn tx_info_at(block: &Block, tx: &OpTransaction, index: usize) -> block_hash: Some(block.header.hash), block_number: Some(block.header.number), base_fee: block.header.base_fee_per_gas, + block_timestamp: Some(block.header.timestamp), } } @@ -313,7 +314,7 @@ fn trace_block_with_tracing_inspector( let trace_result = match tracer { TracerKind::Call(call_config) => { - let gas_used = outcome.inner.result.gas_used(); + let gas_used = outcome.inner.result.tx_gas_used(); let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx.inner.gas_limit()); let frame = @@ -321,10 +322,7 @@ fn trace_block_with_tracing_inspector( GethTrace::from(frame) } TracerKind::PreState(prestate_config) => { - let result_and_state = revm::context::result::ResultAndState { - result: outcome.inner.result, - state: outcome.inner.state, - }; + let result_and_state = outcome.inner.result_and_state; executor.inspector_mut().set_transaction_gas_limit(tx.inner.gas_limit()); let frame_result = { let db = executor.evm.db(); @@ -359,7 +357,7 @@ fn trace_block_with_tracing_inspector( GethTrace::from(frame) } TracerKind::Default(opts) => { - let gas_used = outcome.inner.result.gas_used(); + let gas_used = outcome.inner.result.tx_gas_used(); let return_value = outcome.inner.result.output().cloned().unwrap_or_default(); let inspector = executor.inspector_mut(); @@ -423,7 +421,7 @@ fn trace_tx_with_tracing_inspector( match tracer { TracerKind::Call(call_config) => { - let gas_used = outcome.inner.result.gas_used(); + let gas_used = outcome.inner.result.tx_gas_used(); let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx_gas_limit); let frame = inspector.geth_builder().geth_call_traces(*call_config, gas_used); @@ -431,10 +429,7 @@ fn trace_tx_with_tracing_inspector( } TracerKind::PreState(prestate_config) => { let state_changes = outcome.inner.state.clone(); - let result_and_state = revm::context::result::ResultAndState { - result: outcome.inner.result, - state: outcome.inner.state, - }; + let result_and_state = outcome.inner.result_and_state; executor.inspector_mut().set_transaction_gas_limit(tx_gas_limit); let frame_result = { @@ -471,7 +466,7 @@ fn trace_tx_with_tracing_inspector( Ok(frame.into()) } TracerKind::Default(opts) => { - let gas_used = outcome.inner.result.gas_used(); + let gas_used = outcome.inner.result.tx_gas_used(); let return_value = outcome.inner.result.output().cloned().unwrap_or_default(); let inspector = executor.inspector_mut(); @@ -573,7 +568,7 @@ pub fn trace_block( Ok(outcome) => { let frame = FourByteFrame::from(executor.inspector()); *executor.inspector_mut() = FourByteInspector::default(); - executor.evm.db_mut().commit(outcome.inner.state); + executor.evm.db_mut().commit(outcome.inner.result_and_state.state); results.push(TraceResult::Success { result: frame.into(), tx_hash: Some(tx_hash), @@ -599,12 +594,8 @@ pub fn trace_block( match executor.run_transaction(recovered_tx) { Ok(outcome) => { - let result = outcome.inner.result; - let state_changes = outcome.inner.state.clone(); - let result_and_state = revm::context::result::ResultAndState { - result, - state: outcome.inner.state, - }; + let result_and_state = outcome.inner.result_and_state; + let state_changes = result_and_state.state.clone(); let mux_result = { let db = executor.evm.db(); @@ -640,6 +631,14 @@ pub fn trace_block( } results } + + // Not implemented (request-attributable, like an unknown JS tracer + // before alloy knew the name): supporting it means wiring the + // erc7562 inspector config through `TracerKind` and the response + // cache — its own change, not part of the dependency upgrade. + GethDebugBuiltInTracerType::Erc7562Tracer => { + return Err(request_error("Unsupported tracer", "erc7562Tracer")); + } }, GethDebugTracerType::JsTracer(code) => { @@ -681,12 +680,8 @@ pub fn trace_block( match executor.run_transaction(recovered_tx) { Ok(outcome) => { - let result = outcome.inner.result; - let state_changes = outcome.inner.state.clone(); - let result_and_state = revm::context::result::ResultAndState { - result, - state: outcome.inner.state, - }; + let result_and_state = outcome.inner.result_and_state; + let state_changes = result_and_state.state.clone(); let evm_env_ref = env.evm_env.clone(); let tx_env = TxEnv::default(); @@ -837,11 +832,7 @@ pub fn trace_transaction( let outcome = executor .run_transaction(recovered_target) .map_err(ValidationError::BlockReplayFailed)?; - let result = outcome.inner.result; - let result_and_state = revm::context::result::ResultAndState { - result, - state: outcome.inner.state, - }; + let result_and_state = outcome.inner.result_and_state; let db = executor.evm.db(); let inspector = executor.inspector(); @@ -852,6 +843,11 @@ pub fn trace_transaction( .map(|frame| frame.into()) .map_err(|e| frame_build_error("MuxFrame creation failed", e).into()) } + + // Not implemented; see the block-level match for rationale. + GethDebugBuiltInTracerType::Erc7562Tracer => { + Err(request_error("Unsupported tracer", "erc7562Tracer")) + } }, GethDebugTracerType::JsTracer(code) => { @@ -868,9 +864,7 @@ pub fn trace_transaction( let outcome = executor .run_transaction(recovered_target) .map_err(ValidationError::BlockReplayFailed)?; - let result = outcome.inner.result; - let result_and_state = - revm::context::result::ResultAndState { result, state: outcome.inner.state }; + let result_and_state = outcome.inner.result_and_state; let evm_env_ref = env.evm_env.clone(); let tx_env = TxEnv::default(); @@ -916,7 +910,7 @@ pub fn parity_trace_block( match executor.run_transaction(recovered_tx) { Ok(outcome) => { - let state_changes = outcome.inner.state; + let state_changes = outcome.inner.result_and_state.state; let traces = executor .inspector() .clone() diff --git a/crates/stateless-core/Cargo.toml b/crates/stateless-core/Cargo.toml index b35f706a..6060eb0a 100644 --- a/crates/stateless-core/Cargo.toml +++ b/crates/stateless-core/Cargo.toml @@ -33,12 +33,12 @@ op-alloy-rpc-types.workspace = true reth-ethereum-forks.workspace = true reth-optimism-chainspec.workspace = true reth-trie-common.workspace = true -reth-trie-sparse.workspace = true # revm revm.workspace = true # misc +auto_impl.workspace = true dyn-clone.workspace = true eyre = { workspace = true, optional = true } hashbrown.workspace = true @@ -47,6 +47,7 @@ num_cpus = { workspace = true, optional = true } rustc-hash = { workspace = true, default-features = false } serde.workspace = true serde_json.workspace = true +smallvec.workspace = true thiserror = { workspace = true, default-features = false } tokio = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } @@ -82,7 +83,6 @@ std = [ "reth-ethereum-forks/std", "reth-optimism-chainspec/std", "reth-trie-common/std", - "reth-trie-sparse/std", "revm/default", "rustc-hash/std", "salt/default", diff --git a/crates/stateless-core/src/lib.rs b/crates/stateless-core/src/lib.rs index 48b6609e..2d3068bd 100644 --- a/crates/stateless-core/src/lib.rs +++ b/crates/stateless-core/src/lib.rs @@ -21,6 +21,9 @@ #[cfg(not(feature = "std"))] extern crate alloc as std; +// The vendored sparse-trie code (`withdrawals::sparse_trie`) uses `alloc::` paths verbatim. +extern crate alloc; + pub mod chain_spec; pub mod light_witness; pub use light_witness::{LightWitness, LightWitnessExecutor, LightWitnessFromSalt}; diff --git a/crates/stateless-core/src/withdrawals.rs b/crates/stateless-core/src/withdrawals/mod.rs similarity index 50% rename from crates/stateless-core/src/withdrawals.rs rename to crates/stateless-core/src/withdrawals/mod.rs index 56dbd0bc..2e24b2f8 100644 --- a/crates/stateless-core/src/withdrawals.rs +++ b/crates/stateless-core/src/withdrawals/mod.rs @@ -4,22 +4,33 @@ //! which stores commitments to withdrawal transactions. Given a pre-state witness and //! storage updates from block execution, it cryptographically proves the storage root //! transition is valid. +//! +//! The trie machinery lives in [`sparse_trie`], vendored from reth v1.6.0: the witness +//! format is frozen upstream and can contain extension nodes whose child branch is +//! absent (a minimal exclusion proof), a shape reth v2.3.0's V2 sparse tries cannot +//! represent. + +#[allow(dead_code)] +mod sparse_trie; use std::{ + collections::VecDeque, string::{String, ToString}, vec::Vec, }; -use alloy_consensus::{Header, constants::KECCAK_EMPTY}; +use alloy_consensus::Header; use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256, map::B256Map}; -use alloy_rlp::Encodable; -use reth_trie_common::{ - DecodedMultiProofV2, EMPTY_ROOT_HASH, LeafNode, Nibbles, TrieAccount, TrieNode, -}; -use reth_trie_sparse::{RevealableSparseTrie, SparseStateTrie}; +use alloy_rlp::Decodable; +use reth_trie_common::{EMPTY_ROOT_HASH, Nibbles, RlpNode, TrieNode}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use self::sparse_trie::{ + SerialSparseTrie, SparseTrie, SparseTrieInterface, TrieMasks, + provider::DefaultTrieNodeProvider, +}; + /// L2 contract `L2ToL1MessagePasser`, storing commitments to withdrawal transactions. pub const ADDRESS_L2_TO_L1_MESSAGE_PASSER: Address = address!("0x4200000000000000000000000000000000000016"); @@ -60,10 +71,9 @@ impl MptWitness { /// /// # Process /// - /// 1. Synthesizes an account-level state witness wrapping the storage witness - /// 2. Reveals the trie via BFS, verifies pre-state root against `storage_root` - /// 3. Applies storage updates (inserts non-zero, removes zero values) - /// 4. Computes post-state root, verifies against `header.withdrawals_root` + /// 1. Reveals the storage trie via BFS from `storage_root`, verifies the pre-state root + /// 2. Applies storage updates (inserts non-zero, removes zero values) + /// 3. Computes the post-state root, verifies against `header.withdrawals_root` /// /// # Arguments /// @@ -84,31 +94,12 @@ impl MptWitness { let expected_post_root = header.withdrawals_root.ok_or(WithdrawalValidationError::MissingWithdrawalsRoot)?; - // Synthesize account-level witness and reveal via BFS - let (synthesized_state_root, witness_map, hashed_address) = synthesize_state_witness(self); - - let mut state_trie = SparseStateTrie::default(); - let multiproof = DecodedMultiProofV2::from_witness(synthesized_state_root, &witness_map) - .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - state_trie - .reveal_decoded_multiproof_v2(multiproof) - .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - - // `from_witness` skips storage subtrees whose root is `EMPTY_ROOT_HASH`, - // leaving the per-address storage trie blind. That's fine for pre-state - // verification, but if the block writes any non-zero slot to an - // empty-pre-state trie (e.g. the first-ever withdrawal), the subsequent - // `update_storage_leaf` call would fail with "sparse trie is blind". - // Insert a revealed-empty storage trie so writes can proceed. - if self.storage_root == EMPTY_ROOT_HASH { - state_trie.insert_storage_trie(hashed_address, RevealableSparseTrie::revealed_empty()); - } + let mut trie = self.reveal()?; - // Verify pre-state root. `storage_root` returns `None` when the storage - // trie wasn't revealed; that only happens when `self.storage_root` was - // non-empty and reveal_witness failed to populate it, which the check - // below will catch as a mismatch. - let pre_root = state_trie.storage_root(&hashed_address).unwrap_or(EMPTY_ROOT_HASH); + // Verify the pre-state root. `root()` returns `None` when the trie is still + // blind, which happens when `storage_root` is non-empty but the witness lacks + // the root node; the check below catches that as a mismatch. + let pre_root = trie.root().unwrap_or(EMPTY_ROOT_HASH); if pre_root != self.storage_root { return Err(WithdrawalValidationError::PreStateRootMismatch { expected: self.storage_root, @@ -121,18 +112,16 @@ impl MptWitness { let nibbles = Nibbles::unpack(slot); if !value.is_zero() { let encoded = alloy_rlp::encode_fixed_size(&value).to_vec(); - state_trie - .update_storage_leaf(hashed_address, nibbles, encoded) + trie.update_leaf(nibbles, encoded, DefaultTrieNodeProvider) .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; } else { - state_trie - .remove_storage_leaf(hashed_address, &nibbles) + trie.remove_leaf(&nibbles, DefaultTrieNodeProvider) .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; } } - // Verify post-state root (same empty-trie caveat as pre-state). - let post_root = state_trie.storage_root(&hashed_address).unwrap_or(EMPTY_ROOT_HASH); + // Verify the post-state root (same blind-trie caveat as the pre-state). + let post_root = trie.root().unwrap_or(EMPTY_ROOT_HASH); if post_root != expected_post_root { return Err(WithdrawalValidationError::PostStateRootMismatch { expected: expected_post_root, @@ -142,45 +131,68 @@ impl MptWitness { Ok(()) } -} -/// Wraps a storage-only `MptWitness` as a single-account state witness so that -/// `SparseStateTrie::reveal_witness` can BFS-reveal the storage nodes automatically. -/// -/// `reveal_witness` expects an account-level state trie, but `MptWitness` only contains -/// storage nodes for the L2ToL1MessagePasser contract. This helper synthesizes a one-leaf -/// account trie whose `TrieAccount.storage_root` points to the real storage witness, -/// allowing the existing BFS traversal to discover and reveal all storage nodes. -/// -/// Returns `(synthesized_state_root, witness_map, hashed_address)`. -fn synthesize_state_witness(witness: &MptWitness) -> (B256, B256Map, B256) { - let mut witness_map: B256Map = - witness.state.iter().map(|node: &Bytes| (keccak256(node.as_ref()), node.clone())).collect(); - - let hashed_address = keccak256(ADDRESS_L2_TO_L1_MESSAGE_PASSER); - // `nonce`, `balance`, and `code_hash` are intentionally dummy values — they - // do not reflect the real L2ToL1MessagePasser account state. `reveal_witness` - // only reads `storage_root` from this leaf to navigate into the storage trie; - // the other fields are never inspected or validated. Do not change them - // thinking it makes verification more correct. - let trie_account = TrieAccount { - nonce: 0, - balance: U256::ZERO, - storage_root: witness.storage_root, - code_hash: KECCAK_EMPTY, - }; - let mut account_rlp = Vec::new(); - trie_account.encode(&mut account_rlp); - let account_leaf = TrieNode::Leaf(LeafNode::new(Nibbles::unpack(hashed_address), account_rlp)); - let mut leaf_rlp = Vec::new(); - account_leaf.encode(&mut leaf_rlp); - let leaf_bytes = Bytes::from(leaf_rlp); - let synthesized_state_root = keccak256(&leaf_bytes); - // Synthesized leaf shares witness_map keyspace with real storage nodes; - // keccak-256 collision resistance makes a clash cryptographically negligible. - witness_map.insert(synthesized_state_root, leaf_bytes); - - (synthesized_state_root, witness_map, hashed_address) + /// Reveals the witness into a sparse storage trie via BFS from `storage_root`. + /// + /// Nodes referenced by hash but absent from the witness stay blind — legal for + /// subtrees the block does not touch (and, for extension children, even for + /// inserts that split the extension). An empty `storage_root` yields a + /// revealed-empty trie so that first-ever withdrawals can insert into it. + fn reveal(&self) -> Result { + if self.storage_root == EMPTY_ROOT_HASH { + return Ok(SparseTrie::revealed_empty()); + } + + let witness_map: B256Map<&Bytes> = + self.state.iter().map(|node| (keccak256(node.as_ref()), node)).collect(); + + let trie_err = |e: sparse_trie::SparseTrieError| { + WithdrawalValidationError::TrieOperationFailed(e.to_string()) + }; + + let mut trie = SparseTrie::::blind(); + let mut queue = VecDeque::from([(self.storage_root, Nibbles::default())]); + while let Some((hash, path)) = queue.pop_front() { + // Nodes referenced by hash but not present stay blind. + let Some(bytes) = witness_map.get(&hash) else { continue }; + let node = TrieNode::decode(&mut &bytes[..]) + .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; + + // Queue hash-referenced children; inline children are revealed as part of + // their parent node. + match &node { + TrieNode::Branch(branch) => { + for (idx, maybe_child) in branch.as_ref().children() { + if let Some(child_hash) = maybe_child.and_then(RlpNode::as_hash) { + let mut child_path = path; + child_path.push_unchecked(idx); + queue.push_back((child_hash, child_path)); + } + } + } + TrieNode::Extension(ext) => { + if let Some(child_hash) = ext.child.as_hash() { + let mut child_path = path; + child_path.extend(&ext.key); + queue.push_back((child_hash, child_path)); + } + } + TrieNode::Leaf(_) | TrieNode::EmptyRoot => {} + } + + // BFS visits parents before children, so the root is always first. + if path.is_empty() { + trie.reveal_root(node, TrieMasks::none(), false).map_err(trie_err)?; + } else { + trie.as_revealed_mut() + .expect("root node is revealed first") + .reveal_node(path, node, TrieMasks::none()) + .map_err(trie_err)?; + } + } + + Ok(trie) + } } #[cfg(test)] @@ -189,6 +201,8 @@ mod tests { use WithdrawalValidationError::*; use alloy_primitives::b256; + use alloy_rlp::Encodable; + use reth_trie_common::{BranchNode, ExtensionNode, LeafNode}; use super::*; @@ -288,12 +302,82 @@ mod tests { } /// First-ever withdrawal: empty pre-state storage trie, one new non-zero slot. - /// Exercises the `EMPTY_ROOT_HASH` pre-state path where `reveal_witness` skips - /// the storage subtree, so `update_storage_leaf` must still initialize and - /// write into the trie correctly. + /// Exercises the `EMPTY_ROOT_HASH` pre-state path where the trie starts + /// revealed-empty so the insert can proceed. #[test] fn empty_pre_state_with_new_leaf() { let (post, _) = leaf(42); run(EMPTY_ROOT_HASH, vec![], Some(post), &[(SLOT, U256::from(42u64))]).unwrap(); } + + /// Regression: an insert that splits an extension node whose child branch is + /// absent from the witness (a legal minimal exclusion proof — splitting never + /// reads the child). reth v2.3.0's V2 sparse tries drop such extensions on + /// reveal and then fail the insert with "attempted to update blind node"; + /// the vendored v1.6.0 trie must handle it. + /// + /// Trie shape: root extension (key = first 63 nibbles of `SLOT`) → branch with + /// leaf children at `SLOT`'s last nibble (1) and at nibble 2. The witness + /// contains only the extension node; the update inserts a slot diverging at the + /// extension's first key nibble. + #[test] + fn insert_splitting_extension_with_absent_child_branch() { + let leaf_node = |value: u64| { + let value_rlp = alloy_rlp::encode_fixed_size(&U256::from(value)).to_vec(); + TrieNode::Leaf(LeafNode::new(Nibbles::default(), value_rlp)) + }; + let rlp_of = |node: &TrieNode| { + let mut buf = Vec::new(); + node.encode(&mut buf); + RlpNode::from_rlp(&buf) + }; + + // Child branch of the extension: two empty-key leaves at nibbles 1 and 2. + let branch = TrieNode::Branch(BranchNode::new( + vec![rlp_of(&leaf_node(7)), rlp_of(&leaf_node(8))], + 0b0110.into(), + )); + let mut branch_rlp = Vec::new(); + branch.encode(&mut branch_rlp); + let branch_hash = keccak256(&branch_rlp); + + // Root extension covering the first 63 nibbles of SLOT. + let ext_key = Nibbles::unpack(SLOT).slice(0..63); + let ext = + TrieNode::Extension(ExtensionNode::new(ext_key, RlpNode::word_rlp(&branch_hash))); + let mut ext_rlp = Vec::new(); + ext.encode(&mut ext_rlp); + let pre_root = keccak256(&ext_rlp); + + // Insert a slot that diverges from the extension key at nibble 0. + let diverging_slot = + b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); + + // Expected post-state: branch at the root splitting nibble 1 (old subtree, + // reached via the shortened extension) from nibble 2 (the new leaf). + let shortened_ext = TrieNode::Extension(ExtensionNode::new( + ext_key.slice(1..), + RlpNode::word_rlp(&branch_hash), + )); + let new_leaf = TrieNode::Leaf(LeafNode::new( + Nibbles::unpack(diverging_slot).slice(1..), + alloy_rlp::encode_fixed_size(&U256::from(9u64)).to_vec(), + )); + let split_branch = TrieNode::Branch(BranchNode::new( + vec![rlp_of(&shortened_ext), rlp_of(&new_leaf)], + 0b0110.into(), + )); + let mut split_rlp = Vec::new(); + split_branch.encode(&mut split_rlp); + let expected_post_root = keccak256(&split_rlp); + + // Witness: extension only — the child branch is intentionally absent. + run( + pre_root, + vec![Bytes::from(ext_rlp)], + Some(expected_post_root), + &[(diverging_slot, U256::from(9u64))], + ) + .unwrap(); + } } diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs b/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs new file mode 100644 index 00000000..c498366d --- /dev/null +++ b/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs @@ -0,0 +1,81 @@ +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/evm/execution-errors/src/trie.rs (SparseTrieError portion)), trimmed to the +//! serial sparse trie needed by withdrawal MPT witness verification. +//! +//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension +//! nodes into their child branches; an extension whose child branch is absent +//! from the witness (a legal, minimal exclusion proof — the child is never +//! needed to split the extension on insert) cannot be represented and is +//! silently dropped on reveal, breaking withdrawal-slot inserts that split +//! such extensions. The MegaETH witness format is frozen upstream, so the +//! proven v1.6.0 semantics are pinned here instead. +//! +//! Local changes: import paths only (crate-relative + vendored errors); +//! test modules and metrics are not carried over. Do not edit otherwise. + +use alloc::boxed::Box; +use alloy_primitives::B256; +use reth_trie_common::Nibbles; +use thiserror::Error; + +/// Result type with [`SparseTrieError`] as error. +pub type SparseTrieResult = Result; + +/// Error encountered in `SparseTrie`. +#[derive(Error, Debug)] +#[error(transparent)] +pub struct SparseTrieError(#[from] Box); + +impl> From for SparseTrieError { + #[cold] + fn from(value: T) -> Self { + Self(Box::new(value.into())) + } +} + +impl SparseTrieError { + /// Returns the error kind. + pub const fn kind(&self) -> &SparseTrieErrorKind { + &self.0 + } + + /// Consumes the error and returns the error kind. + pub fn into_kind(self) -> SparseTrieErrorKind { + *self.0 + } +} + +/// [`SparseTrieError`] kind. +#[derive(Error, Debug)] +pub enum SparseTrieErrorKind { + /// Sparse trie is still blind. Thrown on attempt to update it. + #[error("sparse trie is blind")] + Blind, + /// Encountered blinded node on update. + #[error("attempted to update blind node at {path:?}: {hash}")] + BlindedNode { + /// Blind node path. + path: Nibbles, + /// Node hash + hash: B256, + }, + /// Encountered unexpected node at path when revealing. + #[error("encountered an invalid node at path {path:?} when revealing: {node:?}")] + Reveal { + /// Path to the node. + path: Nibbles, + /// Node that was at the path when revealing. + node: Box, + }, + /// RLP error. + #[error(transparent)] + Rlp(#[from] alloy_rlp::Error), + /// Node not found in provider during revealing. + #[error("node {path:?} not found in provider during removal")] + NodeNotFoundInProvider { + /// Path to the missing node. + path: Nibbles, + }, + /// Other. + #[error(transparent)] + Other(#[from] Box), +} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs b/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs new file mode 100644 index 00000000..92906fed --- /dev/null +++ b/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs @@ -0,0 +1,15 @@ +//! Serial sparse MPT vendored from reth v1.6.0 for withdrawal witness verification. +//! +//! See the module docs in [`trie`] for why this is vendored rather than using +//! `reth-trie-sparse` v2.3.0. + +mod errors; +pub use errors::*; + +pub mod provider; + +mod traits; +pub use traits::*; + +mod trie; +pub use trie::*; diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs b/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs new file mode 100644 index 00000000..fb3c1dbd --- /dev/null +++ b/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs @@ -0,0 +1,87 @@ +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/provider.rs), trimmed to the +//! serial sparse trie needed by withdrawal MPT witness verification. +//! +//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension +//! nodes into their child branches; an extension whose child branch is absent +//! from the witness (a legal, minimal exclusion proof — the child is never +//! needed to split the extension on insert) cannot be represented and is +//! silently dropped on reveal, breaking withdrawal-slot inserts that split +//! such extensions. The MegaETH witness format is frozen upstream, so the +//! proven v1.6.0 semantics are pinned here instead. +//! +//! Local changes: import paths only (crate-relative + vendored errors); +//! test modules and metrics are not carried over. Do not edit otherwise. + +//! Traits and default implementations related to retrieval of blinded trie nodes. + +use alloy_primitives::{Bytes, B256}; +use super::SparseTrieError; +use reth_trie_common::{Nibbles, TrieMask}; + +/// Factory for instantiating trie node providers. +#[auto_impl::auto_impl(&)] +pub trait TrieNodeProviderFactory { + /// Type capable of fetching blinded account nodes. + type AccountNodeProvider: TrieNodeProvider; + /// Type capable of fetching blinded storage nodes. + type StorageNodeProvider: TrieNodeProvider; + + /// Returns blinded account node provider. + fn account_node_provider(&self) -> Self::AccountNodeProvider; + + /// Returns blinded storage node provider. + fn storage_node_provider(&self, account: B256) -> Self::StorageNodeProvider; +} + +/// Revealed blinded trie node. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct RevealedNode { + /// Raw trie node. + pub node: Bytes, + /// Branch node tree mask, if any. + pub tree_mask: Option, + /// Branch node hash mask, if any. + pub hash_mask: Option, +} + +/// Trie node provider for retrieving trie nodes. +#[auto_impl::auto_impl(&)] +pub trait TrieNodeProvider { + /// Retrieve trie node by path. + fn trie_node(&self, path: &Nibbles) -> Result, SparseTrieError>; +} + +/// Default trie node provider factory that creates [`DefaultTrieNodeProviderFactory`]. +#[derive(PartialEq, Eq, Clone, Default, Debug)] +pub struct DefaultTrieNodeProviderFactory; + +impl TrieNodeProviderFactory for DefaultTrieNodeProviderFactory { + type AccountNodeProvider = DefaultTrieNodeProvider; + type StorageNodeProvider = DefaultTrieNodeProvider; + + fn account_node_provider(&self) -> Self::AccountNodeProvider { + DefaultTrieNodeProvider + } + + fn storage_node_provider(&self, _account: B256) -> Self::StorageNodeProvider { + DefaultTrieNodeProvider + } +} + +/// Default trie node provider that always returns `Ok(None)`. +#[derive(PartialEq, Eq, Clone, Default, Debug)] +pub struct DefaultTrieNodeProvider; + +impl TrieNodeProvider for DefaultTrieNodeProvider { + fn trie_node(&self, _path: &Nibbles) -> Result, SparseTrieError> { + Ok(None) + } +} + +/// Right pad the path with 0s and return as [`B256`]. +#[inline] +pub fn pad_path_to_key(path: &Nibbles) -> B256 { + let mut padded = path.pack(); + padded.resize(32, 0); + B256::from_slice(&padded) +} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs b/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs new file mode 100644 index 00000000..3221f94f --- /dev/null +++ b/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs @@ -0,0 +1,326 @@ +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/traits.rs), trimmed to the +//! serial sparse trie needed by withdrawal MPT witness verification. +//! +//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension +//! nodes into their child branches; an extension whose child branch is absent +//! from the witness (a legal, minimal exclusion proof — the child is never +//! needed to split the extension on insert) cannot be represented and is +//! silently dropped on reveal, breaking withdrawal-slot inserts that split +//! such extensions. The MegaETH witness format is frozen upstream, so the +//! proven v1.6.0 semantics are pinned here instead. +//! +//! Local changes: import paths only (crate-relative + vendored errors); +//! test modules and metrics are not carried over. Do not edit otherwise. + +//! Traits for sparse trie implementations. + +use core::fmt::Debug; + +use alloc::{borrow::Cow, vec, vec::Vec}; +use alloy_primitives::{ + map::{HashMap, HashSet}, + B256, +}; +use reth_trie_common::{BranchNodeCompact, TrieMask}; +use super::SparseTrieResult; +use reth_trie_common::{Nibbles, TrieNode}; + +use super::provider::TrieNodeProvider; + +/// Trait defining common operations for revealed sparse trie implementations. +/// +/// This trait abstracts over different sparse trie implementations (serial vs parallel) +/// while providing a unified interface for the core trie operations needed by the +/// [`crate::SparseTrie`] enum. +pub trait SparseTrieInterface: Sized + Debug + Send + Sync { + /// Configures the trie to have the given root node revealed. + /// + /// # Arguments + /// + /// * `root` - The root node to reveal + /// * `masks` - Trie masks for root branch node + /// * `retain_updates` - Whether to track updates + /// + /// # Returns + /// + /// Self if successful, or an error if revealing fails. + /// + /// # Panics + /// + /// May panic if the trie is not new/cleared, and has already revealed nodes. + fn with_root( + self, + root: TrieNode, + masks: TrieMasks, + retain_updates: bool, + ) -> SparseTrieResult; + + /// Configures the trie to retain information about updates. + /// + /// If `retain_updates` is true, the trie will record branch node updates + /// and deletions. This information can be used to efficiently update + /// an external database. + /// + /// # Arguments + /// + /// * `retain_updates` - Whether to track updates + /// + /// # Returns + /// + /// Self for method chaining. + fn with_updates(self, retain_updates: bool) -> Self; + + /// Reserves capacity for additional trie nodes. + /// + /// # Arguments + /// + /// * `additional` - The number of additional trie nodes to reserve capacity for. + fn reserve_nodes(&mut self, _additional: usize) {} + + /// The single-node version of `reveal_nodes`. + /// + /// # Returns + /// + /// `Ok(())` if successful, or an error if the node was not revealed. + fn reveal_node( + &mut self, + path: Nibbles, + node: TrieNode, + masks: TrieMasks, + ) -> SparseTrieResult<()> { + self.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]) + } + + /// Reveals one or more trie nodes if they have not been revealed before. + /// + /// This function decodes trie nodes and inserts them into the trie structure. It handles + /// different node types (leaf, extension, branch) by appropriately adding them to the trie and + /// recursively revealing their children. + /// + /// # Arguments + /// + /// * `nodes` - The nodes to be revealed, each having a path and optional set of branch node + /// masks. The nodes will be unsorted. + /// + /// # Returns + /// + /// `Ok(())` if successful, or an error if any of the nodes was not revealed. + fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()>; + + /// Updates the value of a leaf node at the specified path. + /// + /// If the leaf doesn't exist, it will be created. + /// If it does exist, its value will be updated. + /// + /// # Arguments + /// + /// * `full_path` - The full path to the leaf + /// * `value` - The new value for the leaf + /// * `provider` - The trie provider for resolving missing nodes + /// + /// # Returns + /// + /// `Ok(())` if successful, or an error if the update failed. + fn update_leaf( + &mut self, + full_path: Nibbles, + value: Vec, + provider: P, + ) -> SparseTrieResult<()>; + + /// Removes a leaf node at the specified path. + /// + /// This will also handle collapsing the trie structure as needed + /// (e.g., removing branch nodes that become unnecessary). + /// + /// # Arguments + /// + /// * `full_path` - The full path to the leaf to remove + /// * `provider` - The trie node provider for resolving missing nodes + /// + /// # Returns + /// + /// `Ok(())` if successful, or an error if the removal failed. + fn remove_leaf( + &mut self, + full_path: &Nibbles, + provider: P, + ) -> SparseTrieResult<()>; + + /// Calculates and returns the root hash of the trie. + /// + /// This processes any dirty nodes by updating their RLP encodings + /// and returns the root hash. + /// + /// # Returns + /// + /// The root hash of the trie. + fn root(&mut self) -> B256; + + /// Recalculates and updates the RLP hashes of subtries deeper than a certain level. The level + /// is defined in the implementation. + /// + /// The root node is considered to be at level 0. This method is useful for optimizing + /// hash recalculations after localized changes to the trie structure. + fn update_subtrie_hashes(&mut self); + + /// Retrieves a reference to the leaf value at the specified path. + /// + /// # Arguments + /// + /// * `full_path` - The full path to the leaf value + /// + /// # Returns + /// + /// A reference to the leaf value stored at the given full path, if it is revealed. + /// + /// Note: a value can exist in the full trie and this function still returns `None` + /// because the value has not been revealed. + /// + /// Hence a `None` indicates two possibilities: + /// - The value does not exists in the trie, so it cannot be revealed + /// - The value has not yet been revealed. In order to determine which is true, one would need + /// an exclusion proof. + fn get_leaf_value(&self, full_path: &Nibbles) -> Option<&Vec>; + + /// Attempts to find a leaf node at the specified path. + /// + /// This method traverses the trie from the root down to the given path, checking + /// if a leaf exists at that path. It can be used to verify the existence of a leaf + /// or to generate an exclusion proof (proof that a leaf does not exist). + /// + /// # Parameters + /// + /// - `full_path`: The path to search for. + /// - `expected_value`: Optional expected value. If provided, will verify the leaf value + /// matches. + /// + /// # Returns + /// + /// - `Ok(LeafLookup::Exists)` if the leaf exists with the expected value. + /// - `Ok(LeafLookup::NonExistent)` if the leaf definitely does not exist (exclusion proof). + /// - `Err(LeafLookupError)` if the search encountered a blinded node or found a different + /// value. + fn find_leaf( + &self, + full_path: &Nibbles, + expected_value: Option<&Vec>, + ) -> Result; + + /// Returns a reference to the current sparse trie updates. + /// + /// If no updates have been made/recorded, returns an empty update set. + fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates>; + + /// Consumes and returns the currently accumulated trie updates. + /// + /// This is useful when you want to apply the updates to an external database + /// and then start tracking a new set of updates. + /// + /// # Returns + /// + /// The accumulated updates, or an empty set if updates weren't being tracked. + fn take_updates(&mut self) -> SparseTrieUpdates; + + /// Removes all nodes and values from the trie, resetting it to a blank state + /// with only an empty root node. This is used when a storage root is deleted. + /// + /// This should not be used when intending to reuse the trie for a fresh account/storage root; + /// use `clear` for that. + /// + /// Note: All previously tracked changes to the trie are also removed. + fn wipe(&mut self); + + /// This clears all data structures in the sparse trie, keeping the backing data structures + /// allocated. A [`crate::SparseNode::Empty`] is inserted at the root. + /// + /// This is useful for reusing the trie without needing to reallocate memory. + fn clear(&mut self); +} + +/// Struct for passing around branch node mask information. +/// +/// Branch nodes can have up to 16 children (one for each nibble). +/// The masks represent which children are stored in different ways: +/// - `hash_mask`: Indicates which children are stored as hashes in the database +/// - `tree_mask`: Indicates which children are complete subtrees stored in the database +/// +/// These masks are essential for efficient trie traversal and serialization, as they +/// determine how nodes should be encoded and stored on disk. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct TrieMasks { + /// Branch node hash mask, if any. + /// + /// When a bit is set, the corresponding child node's hash is stored in the trie. + /// + /// This mask enables selective hashing of child nodes. + pub hash_mask: Option, + /// Branch node tree mask, if any. + /// + /// When a bit is set, the corresponding child subtree is stored in the database. + pub tree_mask: Option, +} + +impl TrieMasks { + /// Helper function, returns both fields `hash_mask` and `tree_mask` as [`None`] + pub const fn none() -> Self { + Self { hash_mask: None, tree_mask: None } + } +} + +/// Tracks modifications to the sparse trie structure. +/// +/// Maintains references to both modified and pruned/removed branches, enabling +/// one to make batch updates to a persistent database. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SparseTrieUpdates { + /// Collection of updated intermediate nodes indexed by full path. + pub updated_nodes: HashMap, + /// Collection of removed intermediate nodes indexed by full path. + pub removed_nodes: HashSet, + /// Flag indicating whether the trie was wiped. + pub wiped: bool, +} + +/// Error type for a leaf lookup operation +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LeafLookupError { + /// The path leads to a blinded node, cannot determine if leaf exists. + /// This means the witness is not complete. + BlindedNode { + /// Path to the blinded node. + path: Nibbles, + /// Hash of the blinded node. + hash: B256, + }, + /// The path leads to a leaf with a different value than expected. + /// This means the witness is malformed. + ValueMismatch { + /// Path to the leaf. + path: Nibbles, + /// Expected value. + expected: Option>, + /// Actual value found. + actual: Vec, + }, +} + +/// Success value for a leaf lookup operation +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LeafLookup { + /// Leaf exists with expected value. + Exists, + /// Leaf does not exist (exclusion proof found). + NonExistent, +} + +/// Carries all information needed by a sparse trie to reveal a particular node. +#[derive(Debug, PartialEq, Eq)] +pub struct RevealedSparseNode { + /// Path of the node. + pub path: Nibbles, + /// The node itself. + pub node: TrieNode, + /// Tree and hash masks for the node, if known. + pub masks: TrieMasks, +} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs b/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs new file mode 100644 index 00000000..a1aaf4b7 --- /dev/null +++ b/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs @@ -0,0 +1,1933 @@ +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/trie.rs (non-test portion)), trimmed to the +//! serial sparse trie needed by withdrawal MPT witness verification. +//! +//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension +//! nodes into their child branches; an extension whose child branch is absent +//! from the witness (a legal, minimal exclusion proof — the child is never +//! needed to split the extension on insert) cannot be represented and is +//! silently dropped on reveal, breaking withdrawal-slot inserts that split +//! such extensions. The MegaETH witness format is frozen upstream, so the +//! proven v1.6.0 semantics are pinned here instead. +//! +//! Local changes: import paths only (crate-relative + vendored errors); +//! test modules and metrics are not carried over. Do not edit otherwise. + +use super::{ + provider::{RevealedNode, TrieNodeProvider}, + LeafLookup, LeafLookupError, RevealedSparseNode, SparseTrieInterface, SparseTrieUpdates, + TrieMasks, +}; +use alloc::{ + borrow::Cow, + boxed::Box, + fmt, + string::{String, ToString}, + vec, + vec::Vec, +}; +use alloy_primitives::{ + hex, keccak256, + map::{Entry, HashMap, HashSet}, + B256, +}; +use alloy_rlp::Decodable; +use super::{SparseTrieErrorKind, SparseTrieResult}; +use reth_trie_common::{ + prefix_set::{PrefixSet, PrefixSetMut}, + BranchNodeCompact, BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, RlpNode, TrieMask, + TrieNode, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, +}; +use smallvec::SmallVec; +use tracing::trace; + +/// The level below which the sparse trie hashes are calculated in +/// [`SerialSparseTrie::update_subtrie_hashes`]. +const SPARSE_TRIE_SUBTRIE_HASHES_LEVEL: usize = 2; + +/// A sparse trie that is either in a "blind" state (no nodes are revealed, root node hash is +/// unknown) or in a "revealed" state (root node has been revealed and the trie can be updated). +/// +/// In blind mode the trie does not contain any decoded node data, which saves memory but +/// prevents direct access to node contents. The revealed mode stores decoded nodes along +/// with additional information such as values, allowing direct manipulation. +/// +/// The sparse trie design is optimised for: +/// 1. Memory efficiency - only revealed nodes are loaded into memory +/// 2. Update tracking - changes to the trie structure can be tracked and selectively persisted +/// 3. Incremental operations - nodes can be revealed as needed without loading the entire trie. +/// This is what gives rise to the notion of a "sparse" trie. +#[derive(PartialEq, Eq, Debug)] +pub enum SparseTrie { + /// The trie is blind -- no nodes have been revealed + /// + /// This is the default state. In this state, the trie cannot be directly queried or modified + /// until nodes are revealed. + /// + /// In this state the `SparseTrie` can optionally carry with it a cleared `SerialSparseTrie`. + /// This allows for reusing the trie's allocations between payload executions. + Blind(Option>), + /// Some nodes in the Trie have been revealed. + /// + /// In this state, the trie can be queried and modified for the parts + /// that have been revealed. Other parts remain blind and require revealing + /// before they can be accessed. + Revealed(Box), +} + +impl Default for SparseTrie { + fn default() -> Self { + Self::Blind(None) + } +} + +impl SparseTrie { + /// Creates a new revealed but empty sparse trie with `SparseNode::Empty` as root node. + /// + /// # Examples + /// + /// ```ignore + /// use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; + /// + /// let trie = SparseTrie::::revealed_empty(); + /// assert!(!trie.is_blind()); + /// ``` + pub fn revealed_empty() -> Self { + Self::Revealed(Box::default()) + } + + /// Reveals the root node, converting a blind trie into a revealed one. + /// + /// If the trie is blinded, its root node is replaced with `root`. + /// + /// The `masks` are used to determine how the node's children are stored. + /// The `retain_updates` flag controls whether changes to the trie structure + /// should be tracked. + /// + /// # Returns + /// + /// A mutable reference to the underlying [`SparseTrieInterface`]. + pub fn reveal_root( + &mut self, + root: TrieNode, + masks: TrieMasks, + retain_updates: bool, + ) -> SparseTrieResult<&mut T> { + // if `Blind`, we initialize the revealed trie with the given root node, using a + // pre-allocated trie if available. + if self.is_blind() { + let mut revealed_trie = if let Self::Blind(Some(cleared_trie)) = core::mem::take(self) { + cleared_trie + } else { + Box::default() + }; + + *revealed_trie = revealed_trie.with_root(root, masks, retain_updates)?; + *self = Self::Revealed(revealed_trie); + } + + Ok(self.as_revealed_mut().unwrap()) + } +} + +impl SparseTrie { + /// Creates a new blind sparse trie. + /// + /// # Examples + /// + /// ```ignore + /// use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; + /// + /// let trie = SparseTrie::::blind(); + /// assert!(trie.is_blind()); + /// let trie = SparseTrie::::default(); + /// assert!(trie.is_blind()); + /// ``` + pub const fn blind() -> Self { + Self::Blind(None) + } + + /// Returns `true` if the sparse trie has no revealed nodes. + pub const fn is_blind(&self) -> bool { + matches!(self, Self::Blind(_)) + } + + /// Returns `true` if the sparse trie is revealed. + pub const fn is_revealed(&self) -> bool { + matches!(self, Self::Revealed(_)) + } + + /// Returns an immutable reference to the underlying revealed sparse trie. + /// + /// Returns `None` if the trie is blinded. + pub const fn as_revealed_ref(&self) -> Option<&T> { + if let Self::Revealed(revealed) = self { + Some(revealed) + } else { + None + } + } + + /// Returns a mutable reference to the underlying revealed sparse trie. + /// + /// Returns `None` if the trie is blinded. + pub fn as_revealed_mut(&mut self) -> Option<&mut T> { + if let Self::Revealed(revealed) = self { + Some(revealed) + } else { + None + } + } + + /// Wipes the trie by removing all nodes and values, + /// and resetting the trie to only contain an empty root node. + /// + /// Note: This method will error if the trie is blinded. + pub fn wipe(&mut self) -> SparseTrieResult<()> { + let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; + revealed.wipe(); + Ok(()) + } + + /// Calculates the root hash of the trie. + /// + /// This will update any remaining dirty nodes before computing the root hash. + /// "dirty" nodes are nodes that need their hashes to be recomputed because one or more of their + /// children's hashes have changed. + /// + /// # Returns + /// + /// - `Some(B256)` with the calculated root hash if the trie is revealed. + /// - `None` if the trie is still blind. + pub fn root(&mut self) -> Option { + Some(self.as_revealed_mut()?.root()) + } + + /// Returns the root hash along with any accumulated update information. + /// + /// This is useful for when you need both the root hash and information about + /// what nodes were modified, which can be used to efficiently update + /// an external database. + /// + /// # Returns + /// + /// An `Option` tuple consisting of: + /// - The trie root hash (`B256`). + /// - A [`SparseTrieUpdates`] structure containing information about updated nodes. + /// - `None` if the trie is still blind. + pub fn root_with_updates(&mut self) -> Option<(B256, SparseTrieUpdates)> { + let revealed = self.as_revealed_mut()?; + Some((revealed.root(), revealed.take_updates())) + } + + /// Returns a [`SparseTrie::Blind`] based on this one. If this instance was revealed, or was + /// itself a `Blind` with a pre-allocated [`SparseTrieInterface`], this will return + /// a `Blind` carrying a cleared pre-allocated [`SparseTrieInterface`]. + pub fn clear(self) -> Self { + match self { + Self::Blind(_) => self, + Self::Revealed(mut trie) => { + trie.clear(); + Self::Blind(Some(trie)) + } + } + } + + /// Updates (or inserts) a leaf at the given key path with the specified RLP-encoded value. + /// + /// # Errors + /// + /// Returns an error if the trie is still blind, or if the update fails. + pub fn update_leaf( + &mut self, + path: Nibbles, + value: Vec, + provider: impl TrieNodeProvider, + ) -> SparseTrieResult<()> { + let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; + revealed.update_leaf(path, value, provider)?; + Ok(()) + } + + /// Removes a leaf node at the specified key path. + /// + /// # Errors + /// + /// Returns an error if the trie is still blind, or if the leaf cannot be removed + pub fn remove_leaf( + &mut self, + path: &Nibbles, + provider: impl TrieNodeProvider, + ) -> SparseTrieResult<()> { + let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; + revealed.remove_leaf(path, provider)?; + Ok(()) + } +} + +/// The representation of revealed sparse trie. +/// +/// The revealed sparse trie contains the actual trie structure with nodes, values, and +/// tracking for changes. It supports operations like inserting, updating, and removing +/// nodes. +/// +/// +/// ## Invariants +/// +/// - The root node is always present in `nodes` collection. +/// - Each leaf entry in `nodes` collection must have a corresponding entry in `values` collection. +/// The opposite is also true. +/// - All keys in `values` collection are full leaf paths. +#[derive(Clone, PartialEq, Eq)] +pub struct SerialSparseTrie { + /// Map from a path (nibbles) to its corresponding sparse trie node. + /// This contains all of the revealed nodes in trie. + nodes: HashMap, + /// When a branch is set, the corresponding child subtree is stored in the database. + branch_node_tree_masks: HashMap, + /// When a bit is set, the corresponding child is stored as a hash in the database. + branch_node_hash_masks: HashMap, + /// Map from leaf key paths to their values. + /// All values are stored here instead of directly in leaf nodes. + values: HashMap>, + /// Set of prefixes (key paths) that have been marked as updated. + /// This is used to track which parts of the trie need to be recalculated. + prefix_set: PrefixSetMut, + /// Optional tracking of trie updates for later use. + updates: Option, + /// Reusable buffer for RLP encoding of nodes. + rlp_buf: Vec, +} + +impl fmt::Debug for SerialSparseTrie { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SerialSparseTrie") + .field("nodes", &self.nodes) + .field("branch_tree_masks", &self.branch_node_tree_masks) + .field("branch_hash_masks", &self.branch_node_hash_masks) + .field("values", &self.values) + .field("prefix_set", &self.prefix_set) + .field("updates", &self.updates) + .field("rlp_buf", &hex::encode(&self.rlp_buf)) + .finish_non_exhaustive() + } +} + +/// Turns a [`Nibbles`] into a [`String`] by concatenating each nibbles' hex character. +fn encode_nibbles(nibbles: &Nibbles) -> String { + let encoded = hex::encode(nibbles.pack()); + encoded[..nibbles.len()].to_string() +} + +impl fmt::Display for SerialSparseTrie { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This prints the trie in preorder traversal, using a stack + let mut stack = Vec::new(); + let mut visited = HashSet::new(); + + // 4 spaces as indent per level + const INDENT: &str = " "; + + // Track both path and depth + stack.push((Nibbles::default(), self.nodes_ref().get(&Nibbles::default()).unwrap(), 0)); + + while let Some((path, node, depth)) = stack.pop() { + if !visited.insert(path) { + continue; + } + + // Add indentation if alternate flag (#) is set + if f.alternate() { + write!(f, "{}", INDENT.repeat(depth))?; + } + + let packed_path = if depth == 0 { String::from("Root") } else { encode_nibbles(&path) }; + + match node { + SparseNode::Empty | SparseNode::Hash(_) => { + writeln!(f, "{packed_path} -> {node:?}")?; + } + SparseNode::Leaf { key, .. } => { + // we want to append the key to the path + let mut full_path = path; + full_path.extend(key); + let packed_path = encode_nibbles(&full_path); + + writeln!(f, "{packed_path} -> {node:?}")?; + } + SparseNode::Extension { key, .. } => { + writeln!(f, "{packed_path} -> {node:?}")?; + + // push the child node onto the stack with increased depth + let mut child_path = path; + child_path.extend(key); + if let Some(child_node) = self.nodes_ref().get(&child_path) { + stack.push((child_path, child_node, depth + 1)); + } + } + SparseNode::Branch { state_mask, .. } => { + writeln!(f, "{packed_path} -> {node:?}")?; + + for i in CHILD_INDEX_RANGE.rev() { + if state_mask.is_bit_set(i) { + let mut child_path = path; + child_path.push_unchecked(i); + if let Some(child_node) = self.nodes_ref().get(&child_path) { + stack.push((child_path, child_node, depth + 1)); + } + } + } + } + } + } + + Ok(()) + } +} + +impl Default for SerialSparseTrie { + fn default() -> Self { + Self { + nodes: HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]), + branch_node_tree_masks: HashMap::default(), + branch_node_hash_masks: HashMap::default(), + values: HashMap::default(), + prefix_set: PrefixSetMut::default(), + updates: None, + rlp_buf: Vec::new(), + } + } +} + +impl SparseTrieInterface for SerialSparseTrie { + fn with_root( + mut self, + root: TrieNode, + masks: TrieMasks, + retain_updates: bool, + ) -> SparseTrieResult { + self = self.with_updates(retain_updates); + + // A fresh/cleared `SerialSparseTrie` has a `SparseNode::Empty` at its root. Delete that + // so we can reveal the new root node. + let path = Nibbles::default(); + let _removed_root = self.nodes.remove(&path).expect("root node should exist"); + debug_assert_eq!(_removed_root, SparseNode::Empty); + + self.reveal_node(path, root, masks)?; + Ok(self) + } + + fn with_updates(mut self, retain_updates: bool) -> Self { + if retain_updates { + self.updates = Some(SparseTrieUpdates::default()); + } + self + } + + fn reserve_nodes(&mut self, additional: usize) { + self.nodes.reserve(additional); + } + fn reveal_node( + &mut self, + path: Nibbles, + node: TrieNode, + masks: TrieMasks, + ) -> SparseTrieResult<()> { + // If the node is already revealed and it's not a hash node, do nothing. + if self.nodes.get(&path).is_some_and(|node| !node.is_hash()) { + return Ok(()) + } + + if let Some(tree_mask) = masks.tree_mask { + self.branch_node_tree_masks.insert(path, tree_mask); + } + if let Some(hash_mask) = masks.hash_mask { + self.branch_node_hash_masks.insert(path, hash_mask); + } + + match node { + TrieNode::EmptyRoot => { + // For an empty root, ensure that we are at the root path. + debug_assert!(path.is_empty()); + self.nodes.insert(path, SparseNode::Empty); + } + TrieNode::Branch(branch) => { + // For a branch node, iterate over all potential children + let mut stack_ptr = branch.as_ref().first_child_index(); + for idx in CHILD_INDEX_RANGE { + if branch.state_mask.is_bit_set(idx) { + let mut child_path = path; + child_path.push_unchecked(idx); + // Reveal each child node or hash it has + self.reveal_node_or_hash(child_path, &branch.stack[stack_ptr])?; + stack_ptr += 1; + } + } + // Update the branch node entry in the nodes map, handling cases where a blinded + // node is now replaced with a revealed node. + match self.nodes.entry(path) { + Entry::Occupied(mut entry) => match entry.get() { + // Replace a hash node with a fully revealed branch node. + SparseNode::Hash(hash) => { + entry.insert(SparseNode::Branch { + state_mask: branch.state_mask, + // Memoize the hash of a previously blinded node in a new branch + // node. + hash: Some(*hash), + store_in_db_trie: Some( + masks.hash_mask.is_some_and(|mask| !mask.is_empty()) || + masks.tree_mask.is_some_and(|mask| !mask.is_empty()), + ), + }); + } + // Branch node already exists, or an extension node was placed where a + // branch node was before. + SparseNode::Branch { .. } | SparseNode::Extension { .. } => {} + // All other node types can't be handled. + node @ (SparseNode::Empty | SparseNode::Leaf { .. }) => { + return Err(SparseTrieErrorKind::Reveal { + path: *entry.key(), + node: Box::new(node.clone()), + } + .into()) + } + }, + Entry::Vacant(entry) => { + entry.insert(SparseNode::new_branch(branch.state_mask)); + } + } + } + TrieNode::Extension(ext) => match self.nodes.entry(path) { + Entry::Occupied(mut entry) => match entry.get() { + // Replace a hash node with a revealed extension node. + SparseNode::Hash(hash) => { + let mut child_path = *entry.key(); + child_path.extend(&ext.key); + entry.insert(SparseNode::Extension { + key: ext.key, + // Memoize the hash of a previously blinded node in a new extension + // node. + hash: Some(*hash), + store_in_db_trie: None, + }); + self.reveal_node_or_hash(child_path, &ext.child)?; + } + // Extension node already exists, or an extension node was placed where a branch + // node was before. + SparseNode::Extension { .. } | SparseNode::Branch { .. } => {} + // All other node types can't be handled. + node @ (SparseNode::Empty | SparseNode::Leaf { .. }) => { + return Err(SparseTrieErrorKind::Reveal { + path: *entry.key(), + node: Box::new(node.clone()), + } + .into()) + } + }, + Entry::Vacant(entry) => { + let mut child_path = *entry.key(); + child_path.extend(&ext.key); + entry.insert(SparseNode::new_ext(ext.key)); + self.reveal_node_or_hash(child_path, &ext.child)?; + } + }, + TrieNode::Leaf(leaf) => match self.nodes.entry(path) { + Entry::Occupied(mut entry) => match entry.get() { + // Replace a hash node with a revealed leaf node and store leaf node value. + SparseNode::Hash(hash) => { + let mut full = *entry.key(); + full.extend(&leaf.key); + self.values.insert(full, leaf.value.clone()); + entry.insert(SparseNode::Leaf { + key: leaf.key, + // Memoize the hash of a previously blinded node in a new leaf + // node. + hash: Some(*hash), + }); + } + // Left node already exists. + SparseNode::Leaf { .. } => {} + // All other node types can't be handled. + node @ (SparseNode::Empty | + SparseNode::Extension { .. } | + SparseNode::Branch { .. }) => { + return Err(SparseTrieErrorKind::Reveal { + path: *entry.key(), + node: Box::new(node.clone()), + } + .into()) + } + }, + Entry::Vacant(entry) => { + let mut full = *entry.key(); + full.extend(&leaf.key); + entry.insert(SparseNode::new_leaf(leaf.key)); + self.values.insert(full, leaf.value.clone()); + } + }, + } + + Ok(()) + } + + fn reveal_nodes(&mut self, mut nodes: Vec) -> SparseTrieResult<()> { + nodes.sort_unstable_by_key(|node| node.path); + for node in nodes { + self.reveal_node(node.path, node.node, node.masks)?; + } + Ok(()) + } + + fn update_leaf( + &mut self, + full_path: Nibbles, + value: Vec, + provider: P, + ) -> SparseTrieResult<()> { + self.prefix_set.insert(full_path); + let existing = self.values.insert(full_path, value); + if existing.is_some() { + // trie structure unchanged, return immediately + return Ok(()) + } + + let mut current = Nibbles::default(); + while let Some(node) = self.nodes.get_mut(¤t) { + match node { + SparseNode::Empty => { + *node = SparseNode::new_leaf(full_path); + break + } + &mut SparseNode::Hash(hash) => { + return Err(SparseTrieErrorKind::BlindedNode { path: current, hash }.into()) + } + SparseNode::Leaf { key: current_key, .. } => { + current.extend(current_key); + + // this leaf is being updated + if current == full_path { + unreachable!("we already checked leaf presence in the beginning"); + } + + // find the common prefix + let common = current.common_prefix_length(&full_path); + + // update existing node + let new_ext_key = current.slice(current.len() - current_key.len()..common); + *node = SparseNode::new_ext(new_ext_key); + + // create a branch node and corresponding leaves + self.nodes.reserve(3); + self.nodes.insert( + current.slice(..common), + SparseNode::new_split_branch( + current.get_unchecked(common), + full_path.get_unchecked(common), + ), + ); + self.nodes.insert( + full_path.slice(..=common), + SparseNode::new_leaf(full_path.slice(common + 1..)), + ); + self.nodes.insert( + current.slice(..=common), + SparseNode::new_leaf(current.slice(common + 1..)), + ); + + break; + } + SparseNode::Extension { key, .. } => { + current.extend(key); + + if !full_path.starts_with(¤t) { + // find the common prefix + let common = current.common_prefix_length(&full_path); + *key = current.slice(current.len() - key.len()..common); + + // If branch node updates retention is enabled, we need to query the + // extension node child to later set the hash mask for a parent branch node + // correctly. + if self.updates.is_some() { + // Check if the extension node child is a hash that needs to be revealed + if self.nodes.get(¤t).unwrap().is_hash() { + if let Some(RevealedNode { node, tree_mask, hash_mask }) = + provider.trie_node(¤t)? + { + let decoded = TrieNode::decode(&mut &node[..])?; + trace!( + target: "trie::sparse", + ?current, + ?decoded, + ?tree_mask, + ?hash_mask, + "Revealing extension node child", + ); + self.reveal_node( + current, + decoded, + TrieMasks { hash_mask, tree_mask }, + )?; + } + } + } + + // create state mask for new branch node + // NOTE: this might overwrite the current extension node + self.nodes.reserve(3); + let branch = SparseNode::new_split_branch( + current.get_unchecked(common), + full_path.get_unchecked(common), + ); + self.nodes.insert(current.slice(..common), branch); + + // create new leaf + let new_leaf = SparseNode::new_leaf(full_path.slice(common + 1..)); + self.nodes.insert(full_path.slice(..=common), new_leaf); + + // recreate extension to previous child if needed + let key = current.slice(common + 1..); + if !key.is_empty() { + self.nodes.insert(current.slice(..=common), SparseNode::new_ext(key)); + } + + break; + } + } + SparseNode::Branch { state_mask, .. } => { + let nibble = full_path.get_unchecked(current.len()); + current.push_unchecked(nibble); + if !state_mask.is_bit_set(nibble) { + state_mask.set_bit(nibble); + let new_leaf = SparseNode::new_leaf(full_path.slice(current.len()..)); + self.nodes.insert(current, new_leaf); + break; + } + } + }; + } + + Ok(()) + } + + fn remove_leaf( + &mut self, + full_path: &Nibbles, + provider: P, + ) -> SparseTrieResult<()> { + if self.values.remove(full_path).is_none() { + if let Some(&SparseNode::Hash(hash)) = self.nodes.get(full_path) { + // Leaf is present in the trie, but it's blinded. + return Err(SparseTrieErrorKind::BlindedNode { path: *full_path, hash }.into()) + } + + trace!(target: "trie::sparse", ?full_path, "Leaf node is not present in the trie"); + // Leaf is not present in the trie. + return Ok(()) + } + self.prefix_set.insert(*full_path); + + // If the path wasn't present in `values`, we still need to walk the trie and ensure that + // there is no node at the path. When a leaf node is a blinded `Hash`, it will have an entry + // in `nodes`, but not in the `values`. + + let mut removed_nodes = self.take_nodes_for_path(full_path)?; + // Pop the first node from the stack which is the leaf node we want to remove. + let mut child = removed_nodes.pop().expect("leaf exists"); + #[cfg(debug_assertions)] + { + let mut child_path = child.path; + let SparseNode::Leaf { key, .. } = &child.node else { panic!("expected leaf node") }; + child_path.extend(key); + assert_eq!(&child_path, full_path); + } + + // If we don't have any other removed nodes, insert an empty node at the root. + if removed_nodes.is_empty() { + debug_assert!(self.nodes.is_empty()); + self.nodes.insert(Nibbles::default(), SparseNode::Empty); + + return Ok(()) + } + + // Walk the stack of removed nodes from the back and re-insert them back into the trie, + // adjusting the node type as needed. + while let Some(removed_node) = removed_nodes.pop() { + let removed_path = removed_node.path; + + let new_node = match &removed_node.node { + SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), + &SparseNode::Hash(hash) => { + return Err(SparseTrieErrorKind::BlindedNode { path: removed_path, hash }.into()) + } + SparseNode::Leaf { .. } => { + unreachable!("we already popped the leaf node") + } + SparseNode::Extension { key, .. } => { + // If the node is an extension node, we need to look at its child to see if we + // need to merge them. + match &child.node { + SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), + &SparseNode::Hash(hash) => { + return Err( + SparseTrieErrorKind::BlindedNode { path: child.path, hash }.into() + ) + } + // For a leaf node, we collapse the extension node into a leaf node, + // extending the key. While it's impossible to encounter an extension node + // followed by a leaf node in a complete trie, it's possible here because we + // could have downgraded the extension node's child into a leaf node from + // another node type. + SparseNode::Leaf { key: leaf_key, .. } => { + self.nodes.remove(&child.path); + + let mut new_key = *key; + new_key.extend(leaf_key); + SparseNode::new_leaf(new_key) + } + // For an extension node, we collapse them into one extension node, + // extending the key + SparseNode::Extension { key: extension_key, .. } => { + self.nodes.remove(&child.path); + + let mut new_key = *key; + new_key.extend(extension_key); + SparseNode::new_ext(new_key) + } + // For a branch node, we just leave the extension node as-is. + SparseNode::Branch { .. } => removed_node.node, + } + } + &SparseNode::Branch { mut state_mask, hash: _, store_in_db_trie: _ } => { + // If the node is a branch node, we need to check the number of children left + // after deleting the child at the given nibble. + + if let Some(removed_nibble) = removed_node.unset_branch_nibble { + state_mask.unset_bit(removed_nibble); + } + + // If only one child is left set in the branch node, we need to collapse it. + if state_mask.count_bits() == 1 { + let child_nibble = + state_mask.first_set_bit_index().expect("state mask is not empty"); + + // Get full path of the only child node left. + let mut child_path = removed_path; + child_path.push_unchecked(child_nibble); + + trace!(target: "trie::sparse", ?removed_path, ?child_path, "Branch node has only one child"); + + if self.nodes.get(&child_path).unwrap().is_hash() { + trace!(target: "trie::sparse", ?child_path, "Retrieving remaining blinded branch child"); + if let Some(RevealedNode { node, tree_mask, hash_mask }) = + provider.trie_node(&child_path)? + { + let decoded = TrieNode::decode(&mut &node[..])?; + trace!( + target: "trie::sparse", + ?child_path, + ?decoded, + ?tree_mask, + ?hash_mask, + "Revealing remaining blinded branch child" + ); + self.reveal_node( + child_path, + decoded, + TrieMasks { hash_mask, tree_mask }, + )?; + } + } + + // Get the only child node. + let child = self.nodes.get(&child_path).unwrap(); + + let mut delete_child = false; + let new_node = match child { + SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), + &SparseNode::Hash(hash) => { + return Err(SparseTrieErrorKind::BlindedNode { + path: child_path, + hash, + } + .into()) + } + // If the only child is a leaf node, we downgrade the branch node into a + // leaf node, prepending the nibble to the key, and delete the old + // child. + SparseNode::Leaf { key, .. } => { + delete_child = true; + + let mut new_key = Nibbles::from_nibbles_unchecked([child_nibble]); + new_key.extend(key); + SparseNode::new_leaf(new_key) + } + // If the only child node is an extension node, we downgrade the branch + // node into an even longer extension node, prepending the nibble to the + // key, and delete the old child. + SparseNode::Extension { key, .. } => { + delete_child = true; + + let mut new_key = Nibbles::from_nibbles_unchecked([child_nibble]); + new_key.extend(key); + SparseNode::new_ext(new_key) + } + // If the only child is a branch node, we downgrade the current branch + // node into a one-nibble extension node. + SparseNode::Branch { .. } => { + SparseNode::new_ext(Nibbles::from_nibbles_unchecked([child_nibble])) + } + }; + + if delete_child { + self.nodes.remove(&child_path); + } + + if let Some(updates) = self.updates.as_mut() { + updates.updated_nodes.remove(&removed_path); + updates.removed_nodes.insert(removed_path); + } + + new_node + } + // If more than one child is left set in the branch, we just re-insert it as-is. + else { + SparseNode::new_branch(state_mask) + } + } + }; + + child = RemovedSparseNode { + path: removed_path, + node: new_node.clone(), + unset_branch_nibble: None, + }; + trace!(target: "trie::sparse", ?removed_path, ?new_node, "Re-inserting the node"); + self.nodes.insert(removed_path, new_node); + } + + Ok(()) + } + + fn root(&mut self) -> B256 { + // Take the current prefix set + let mut prefix_set = core::mem::take(&mut self.prefix_set).freeze(); + let rlp_node = self.rlp_node_allocate(&mut prefix_set); + if let Some(root_hash) = rlp_node.as_hash() { + root_hash + } else { + keccak256(rlp_node) + } + } + + fn update_subtrie_hashes(&mut self) { + self.update_rlp_node_level(SPARSE_TRIE_SUBTRIE_HASHES_LEVEL); + } + + fn get_leaf_value(&self, full_path: &Nibbles) -> Option<&Vec> { + self.values.get(full_path) + } + + fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates> { + self.updates.as_ref().map_or(Cow::Owned(SparseTrieUpdates::default()), Cow::Borrowed) + } + + fn take_updates(&mut self) -> SparseTrieUpdates { + self.updates.take().unwrap_or_default() + } + + fn wipe(&mut self) { + self.nodes = HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]); + self.values = HashMap::default(); + self.prefix_set = PrefixSetMut::all(); + self.updates = self.updates.is_some().then(SparseTrieUpdates::wiped); + } + + fn clear(&mut self) { + self.nodes.clear(); + self.nodes.insert(Nibbles::default(), SparseNode::Empty); + + self.branch_node_tree_masks.clear(); + self.branch_node_hash_masks.clear(); + self.values.clear(); + self.prefix_set.clear(); + self.updates = None; + self.rlp_buf.clear(); + } + + fn find_leaf( + &self, + full_path: &Nibbles, + expected_value: Option<&Vec>, + ) -> Result { + // Helper function to check if a value matches the expected value + fn check_value_match( + actual_value: &Vec, + expected_value: Option<&Vec>, + path: &Nibbles, + ) -> Result<(), LeafLookupError> { + if let Some(expected) = expected_value { + if actual_value != expected { + return Err(LeafLookupError::ValueMismatch { + path: *path, + expected: Some(expected.clone()), + actual: actual_value.clone(), + }); + } + } + Ok(()) + } + + let mut current = Nibbles::default(); // Start at the root + + // Inclusion proof + // + // First, do a quick check if the value exists in our values map. + // We assume that if there exists a leaf node, then its value will + // be in the `values` map. + if let Some(actual_value) = self.values.get(full_path) { + // We found the leaf, check if the value matches (if expected value was provided) + check_value_match(actual_value, expected_value, full_path)?; + return Ok(LeafLookup::Exists); + } + + // If the value does not exist in the `values` map, then this means that the leaf either: + // - Does not exist in the trie + // - Is missing from the witness + // We traverse the trie to find the location where this leaf would have been, showing + // that it is not in the trie. Or we find a blinded node, showing that the witness is + // not complete. + while current.len() < full_path.len() { + match self.nodes.get(¤t) { + Some(SparseNode::Empty) | None => { + // None implies no node is at the current path (even in the full trie) + // Empty node means there is a node at this path and it is "Empty" + return Ok(LeafLookup::NonExistent); + } + Some(&SparseNode::Hash(hash)) => { + // We hit a blinded node - cannot determine if leaf exists + return Err(LeafLookupError::BlindedNode { path: current, hash }); + } + Some(SparseNode::Leaf { key, .. }) => { + // We found a leaf node before reaching our target depth + current.extend(key); + if ¤t == full_path { + // This should have been handled by our initial values map check + if let Some(value) = self.values.get(full_path) { + check_value_match(value, expected_value, full_path)?; + return Ok(LeafLookup::Exists); + } + } + + // The leaf node's path doesn't match our target path, + // providing an exclusion proof + return Ok(LeafLookup::NonExistent); + } + Some(SparseNode::Extension { key, .. }) => { + // Temporarily append the extension key to `current` + let saved_len = current.len(); + current.extend(key); + + if full_path.len() < current.len() || !full_path.starts_with(¤t) { + current.truncate(saved_len); // restore + return Ok(LeafLookup::NonExistent); + } + // Prefix matched, so we keep walking with the longer `current`. + } + Some(SparseNode::Branch { state_mask, .. }) => { + // Check if branch has a child at the next nibble in our path + let nibble = full_path.get_unchecked(current.len()); + if !state_mask.is_bit_set(nibble) { + // No child at this nibble - exclusion proof + return Ok(LeafLookup::NonExistent); + } + + // Continue down the branch + current.push_unchecked(nibble); + } + } + } + + // We've traversed to the end of the path and didn't find a leaf + // Check if there's a node exactly at our target path + match self.nodes.get(full_path) { + Some(SparseNode::Leaf { key, .. }) if key.is_empty() => { + // We found a leaf with an empty key (exact match) + // This should be handled by the values map check above + if let Some(value) = self.values.get(full_path) { + check_value_match(value, expected_value, full_path)?; + return Ok(LeafLookup::Exists); + } + } + Some(&SparseNode::Hash(hash)) => { + return Err(LeafLookupError::BlindedNode { path: *full_path, hash }); + } + _ => { + // No leaf at exactly the target path + return Ok(LeafLookup::NonExistent); + } + } + + // If we get here, there's no leaf at the target path + Ok(LeafLookup::NonExistent) + } +} + +impl SerialSparseTrie { + /// Creates a new revealed sparse trie from the given root node. + /// + /// This function initializes the internal structures and then reveals the root. + /// It is a convenient method to create a trie when you already have the root node available. + /// + /// # Arguments + /// + /// * `root` - The root node of the trie + /// * `masks` - Trie masks for root branch node + /// * `retain_updates` - Whether to track updates + /// + /// # Returns + /// + /// Self if successful, or an error if revealing fails. + pub fn from_root( + root: TrieNode, + masks: TrieMasks, + retain_updates: bool, + ) -> SparseTrieResult { + Self::default().with_root(root, masks, retain_updates) + } + + /// Returns a reference to the current sparse trie updates. + /// + /// If no updates have been made/recorded, returns an empty update set. + pub fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates> { + self.updates.as_ref().map_or(Cow::Owned(SparseTrieUpdates::default()), Cow::Borrowed) + } + + /// Returns an immutable reference to all nodes in the sparse trie. + pub const fn nodes_ref(&self) -> &HashMap { + &self.nodes + } + + /// Reveals either a node or its hash placeholder based on the provided child data. + /// + /// When traversing the trie, we often encounter references to child nodes that + /// are either directly embedded or represented by their hash. This method + /// handles both cases: + /// + /// 1. If the child data represents a hash (32+1=33 bytes), store it as a hash node + /// 2. Otherwise, decode the data as a [`TrieNode`] and recursively reveal it using + /// `reveal_node` + /// + /// # Returns + /// + /// Returns `Ok(())` if successful, or an error if the node cannot be revealed. + /// + /// # Error Handling + /// + /// Will error if there's a conflict between a new hash node and an existing one + /// at the same path + fn reveal_node_or_hash(&mut self, path: Nibbles, child: &[u8]) -> SparseTrieResult<()> { + if child.len() == B256::len_bytes() + 1 { + let hash = B256::from_slice(&child[1..]); + match self.nodes.entry(path) { + Entry::Occupied(entry) => match entry.get() { + // Hash node with a different hash can't be handled. + SparseNode::Hash(previous_hash) if previous_hash != &hash => { + return Err(SparseTrieErrorKind::Reveal { + path: *entry.key(), + node: Box::new(SparseNode::Hash(hash)), + } + .into()) + } + _ => {} + }, + Entry::Vacant(entry) => { + entry.insert(SparseNode::Hash(hash)); + } + } + return Ok(()) + } + + self.reveal_node(path, TrieNode::decode(&mut &child[..])?, TrieMasks::none()) + } + + /// Traverse the trie from the root down to the leaf at the given path, + /// removing and collecting all nodes along that path. + /// + /// This helper function is used during leaf removal to extract the nodes of the trie + /// that will be affected by the deletion. These nodes are then re-inserted and modified + /// as needed (collapsing extension nodes etc) given that the leaf has now been removed. + /// + /// # Returns + /// + /// Returns a vector of [`RemovedSparseNode`] representing the nodes removed during the + /// traversal. + /// + /// # Errors + /// + /// Returns an error if a blinded node or an empty node is encountered unexpectedly, + /// as these prevent proper removal of the leaf. + fn take_nodes_for_path(&mut self, path: &Nibbles) -> SparseTrieResult> { + let mut current = Nibbles::default(); // Start traversal from the root + let mut nodes = Vec::new(); // Collect traversed nodes + + while let Some(node) = self.nodes.remove(¤t) { + match &node { + SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), + &SparseNode::Hash(hash) => { + return Err(SparseTrieErrorKind::BlindedNode { path: current, hash }.into()) + } + SparseNode::Leaf { key: _key, .. } => { + // Leaf node is always the one that we're deleting, and no other leaf nodes can + // be found during traversal. + + #[cfg(debug_assertions)] + { + let mut current = current; + current.extend(_key); + assert_eq!(¤t, path); + } + + nodes.push(RemovedSparseNode { + path: current, + node, + unset_branch_nibble: None, + }); + break + } + SparseNode::Extension { key, .. } => { + #[cfg(debug_assertions)] + { + let mut current = current; + current.extend(key); + assert!( + path.starts_with(¤t), + "path: {path:?}, current: {current:?}, key: {key:?}", + ); + } + + let path = current; + current.extend(key); + nodes.push(RemovedSparseNode { path, node, unset_branch_nibble: None }); + } + SparseNode::Branch { state_mask, .. } => { + let nibble = path.get_unchecked(current.len()); + debug_assert!( + state_mask.is_bit_set(nibble), + "current: {current:?}, path: {path:?}, nibble: {nibble:?}, state_mask: {state_mask:?}", + ); + + // If the branch node has a child that is a leaf node that we're removing, + // we need to unset this nibble. + // Any other branch nodes will not require unsetting the nibble, because + // deleting one leaf node can not remove the whole path + // where the branch node is located. + let mut child_path = current; + child_path.push_unchecked(nibble); + let unset_branch_nibble = self + .nodes + .get(&child_path) + .is_some_and(move |node| match node { + SparseNode::Leaf { key, .. } => { + // Get full path of the leaf node + child_path.extend(key); + &child_path == path + } + _ => false, + }) + .then_some(nibble); + + nodes.push(RemovedSparseNode { path: current, node, unset_branch_nibble }); + + current.push_unchecked(nibble); + } + } + } + + Ok(nodes) + } + + /// Recalculates and updates the RLP hashes of nodes deeper than or equal to the specified + /// `depth`. + /// + /// The root node is considered to be at level 0. This method is useful for optimizing + /// hash recalculations after localized changes to the trie structure: + /// + /// This function identifies all nodes that have changed (based on the prefix set) at the given + /// depth and recalculates their RLP representation. + pub fn update_rlp_node_level(&mut self, depth: usize) { + // Take the current prefix set + let mut prefix_set = core::mem::take(&mut self.prefix_set).freeze(); + let mut buffers = RlpNodeBuffers::default(); + + // Get the nodes that have changed at the given depth. + let (targets, new_prefix_set) = self.get_changed_nodes_at_depth(&mut prefix_set, depth); + // Update the prefix set to the prefix set of the nodes that still need to be updated. + self.prefix_set = new_prefix_set; + + trace!(target: "trie::sparse", ?depth, ?targets, "Updating nodes at depth"); + + let mut temp_rlp_buf = core::mem::take(&mut self.rlp_buf); + for (level, path) in targets { + buffers.path_stack.push(RlpNodePathStackItem { + level, + path, + is_in_prefix_set: Some(true), + }); + self.rlp_node(&mut prefix_set, &mut buffers, &mut temp_rlp_buf); + } + self.rlp_buf = temp_rlp_buf; + } + + /// Returns a list of (level, path) tuples identifying the nodes that have changed at the + /// specified depth, along with a new prefix set for the paths above the provided depth that + /// remain unchanged. + /// + /// Leaf nodes with a depth less than `depth` are returned too. + /// + /// This method helps optimize hash recalculations by identifying which specific + /// nodes need to be updated at each level of the trie. + /// + /// # Parameters + /// + /// - `prefix_set`: The current prefix set tracking which paths need updates. + /// - `depth`: The minimum depth (relative to the root) to include nodes in the targets. + /// + /// # Returns + /// + /// A tuple containing: + /// - A vector of `(level, Nibbles)` pairs for nodes that require updates at or below the + /// specified depth. + /// - A `PrefixSetMut` containing paths shallower than the specified depth that still need to be + /// tracked for future updates. + fn get_changed_nodes_at_depth( + &self, + prefix_set: &mut PrefixSet, + depth: usize, + ) -> (Vec<(usize, Nibbles)>, PrefixSetMut) { + let mut unchanged_prefix_set = PrefixSetMut::default(); + let mut paths = Vec::from([(Nibbles::default(), 0)]); + let mut targets = Vec::new(); + + while let Some((mut path, level)) = paths.pop() { + match self.nodes.get(&path).unwrap() { + SparseNode::Empty | SparseNode::Hash(_) => {} + SparseNode::Leaf { key: _, hash } => { + if hash.is_some() && !prefix_set.contains(&path) { + continue + } + + targets.push((level, path)); + } + SparseNode::Extension { key, hash, store_in_db_trie: _ } => { + if hash.is_some() && !prefix_set.contains(&path) { + continue + } + + if level >= depth { + targets.push((level, path)); + } else { + unchanged_prefix_set.insert(path); + + path.extend(key); + paths.push((path, level + 1)); + } + } + SparseNode::Branch { state_mask, hash, store_in_db_trie: _ } => { + if hash.is_some() && !prefix_set.contains(&path) { + continue + } + + if level >= depth { + targets.push((level, path)); + } else { + unchanged_prefix_set.insert(path); + + for bit in CHILD_INDEX_RANGE.rev() { + if state_mask.is_bit_set(bit) { + let mut child_path = path; + child_path.push_unchecked(bit); + paths.push((child_path, level + 1)); + } + } + } + } + } + } + + (targets, unchanged_prefix_set) + } + + /// Look up or calculate the RLP of the node at the root path. + /// + /// # Panics + /// + /// If the node at provided path does not exist. + pub fn rlp_node_allocate(&mut self, prefix_set: &mut PrefixSet) -> RlpNode { + let mut buffers = RlpNodeBuffers::new_with_root_path(); + let mut temp_rlp_buf = core::mem::take(&mut self.rlp_buf); + let result = self.rlp_node(prefix_set, &mut buffers, &mut temp_rlp_buf); + self.rlp_buf = temp_rlp_buf; + + result + } + + /// Looks up or computes the RLP encoding of the node specified by the current + /// path in the provided buffers. + /// + /// The function uses a stack (`RlpNodeBuffers::path_stack`) to track the traversal and + /// accumulate RLP encodings. + /// + /// # Parameters + /// + /// - `prefix_set`: The set of trie paths that need their nodes updated. + /// - `buffers`: The reusable buffers for stack management and temporary RLP values. + /// + /// # Panics + /// + /// If the node at provided path does not exist. + pub fn rlp_node( + &mut self, + prefix_set: &mut PrefixSet, + buffers: &mut RlpNodeBuffers, + rlp_buf: &mut Vec, + ) -> RlpNode { + let _starting_path = buffers.path_stack.last().map(|item| item.path); + + 'main: while let Some(RlpNodePathStackItem { level, path, mut is_in_prefix_set }) = + buffers.path_stack.pop() + { + let node = self.nodes.get_mut(&path).unwrap(); + trace!( + target: "trie::sparse", + ?_starting_path, + ?level, + ?path, + ?is_in_prefix_set, + ?node, + "Popped node from path stack" + ); + + // Check if the path is in the prefix set. + // First, check the cached value. If it's `None`, then check the prefix set, and update + // the cached value. + let mut prefix_set_contains = + |path: &Nibbles| *is_in_prefix_set.get_or_insert_with(|| prefix_set.contains(path)); + + let (rlp_node, node_type) = match node { + SparseNode::Empty => (RlpNode::word_rlp(&EMPTY_ROOT_HASH), SparseNodeType::Empty), + SparseNode::Hash(hash) => (RlpNode::word_rlp(hash), SparseNodeType::Hash), + SparseNode::Leaf { key, hash } => { + let mut path = path; + path.extend(key); + if let Some(hash) = hash.filter(|_| !prefix_set_contains(&path)) { + (RlpNode::word_rlp(&hash), SparseNodeType::Leaf) + } else { + let value = self.values.get(&path).unwrap(); + rlp_buf.clear(); + let rlp_node = LeafNodeRef { key, value }.rlp(rlp_buf); + *hash = rlp_node.as_hash(); + (rlp_node, SparseNodeType::Leaf) + } + } + SparseNode::Extension { key, hash, store_in_db_trie } => { + let mut child_path = path; + child_path.extend(key); + if let Some((hash, store_in_db_trie)) = + hash.zip(*store_in_db_trie).filter(|_| !prefix_set_contains(&path)) + { + ( + RlpNode::word_rlp(&hash), + SparseNodeType::Extension { store_in_db_trie: Some(store_in_db_trie) }, + ) + } else if buffers.rlp_node_stack.last().is_some_and(|e| e.path == child_path) { + let RlpNodeStackItem { + path: _, + rlp_node: child, + node_type: child_node_type, + } = buffers.rlp_node_stack.pop().unwrap(); + rlp_buf.clear(); + let rlp_node = ExtensionNodeRef::new(key, &child).rlp(rlp_buf); + *hash = rlp_node.as_hash(); + + let store_in_db_trie_value = child_node_type.store_in_db_trie(); + + trace!( + target: "trie::sparse", + ?path, + ?child_path, + ?child_node_type, + "Extension node" + ); + + *store_in_db_trie = store_in_db_trie_value; + + ( + rlp_node, + SparseNodeType::Extension { + // Inherit the `store_in_db_trie` flag from the child node, which is + // always the branch node + store_in_db_trie: store_in_db_trie_value, + }, + ) + } else { + // need to get rlp node for child first + buffers.path_stack.extend([ + RlpNodePathStackItem { level, path, is_in_prefix_set }, + RlpNodePathStackItem { + level: level + 1, + path: child_path, + is_in_prefix_set: None, + }, + ]); + continue + } + } + SparseNode::Branch { state_mask, hash, store_in_db_trie } => { + if let Some((hash, store_in_db_trie)) = + hash.zip(*store_in_db_trie).filter(|_| !prefix_set_contains(&path)) + { + buffers.rlp_node_stack.push(RlpNodeStackItem { + path, + rlp_node: RlpNode::word_rlp(&hash), + node_type: SparseNodeType::Branch { + store_in_db_trie: Some(store_in_db_trie), + }, + }); + continue + } + let retain_updates = self.updates.is_some() && prefix_set_contains(&path); + + buffers.branch_child_buf.clear(); + // Walk children in a reverse order from `f` to `0`, so we pop the `0` first + // from the stack and keep walking in the sorted order. + for bit in CHILD_INDEX_RANGE.rev() { + if state_mask.is_bit_set(bit) { + let mut child = path; + child.push_unchecked(bit); + buffers.branch_child_buf.push(child); + } + } + + buffers + .branch_value_stack_buf + .resize(buffers.branch_child_buf.len(), Default::default()); + let mut added_children = false; + + let mut tree_mask = TrieMask::default(); + let mut hash_mask = TrieMask::default(); + let mut hashes = Vec::new(); + for (i, child_path) in buffers.branch_child_buf.iter().enumerate() { + if buffers.rlp_node_stack.last().is_some_and(|e| &e.path == child_path) { + let RlpNodeStackItem { + path: _, + rlp_node: child, + node_type: child_node_type, + } = buffers.rlp_node_stack.pop().unwrap(); + + // Update the masks only if we need to retain trie updates + if retain_updates { + // SAFETY: it's a child, so it's never empty + let last_child_nibble = child_path.last().unwrap(); + + // Determine whether we need to set trie mask bit. + let should_set_tree_mask_bit = if let Some(store_in_db_trie) = + child_node_type.store_in_db_trie() + { + // A branch or an extension node explicitly set the + // `store_in_db_trie` flag + store_in_db_trie + } else { + // A blinded node has the tree mask bit set + child_node_type.is_hash() && + self.branch_node_tree_masks.get(&path).is_some_and( + |mask| mask.is_bit_set(last_child_nibble), + ) + }; + if should_set_tree_mask_bit { + tree_mask.set_bit(last_child_nibble); + } + + // Set the hash mask. If a child node is a revealed branch node OR + // is a blinded node that has its hash mask bit set according to the + // database, set the hash mask bit and save the hash. + let hash = child.as_hash().filter(|_| { + child_node_type.is_branch() || + (child_node_type.is_hash() && + self.branch_node_hash_masks + .get(&path) + .is_some_and(|mask| { + mask.is_bit_set(last_child_nibble) + })) + }); + if let Some(hash) = hash { + hash_mask.set_bit(last_child_nibble); + hashes.push(hash); + } + } + + // Insert children in the resulting buffer in a normal order, + // because initially we iterated in reverse. + // SAFETY: i < len and len is never 0 + let original_idx = buffers.branch_child_buf.len() - i - 1; + buffers.branch_value_stack_buf[original_idx] = child; + added_children = true; + } else { + debug_assert!(!added_children); + buffers.path_stack.push(RlpNodePathStackItem { + level, + path, + is_in_prefix_set, + }); + buffers.path_stack.extend(buffers.branch_child_buf.drain(..).map( + |path| RlpNodePathStackItem { + level: level + 1, + path, + is_in_prefix_set: None, + }, + )); + continue 'main + } + } + + trace!( + target: "trie::sparse", + ?path, + ?tree_mask, + ?hash_mask, + "Branch node masks" + ); + + rlp_buf.clear(); + let branch_node_ref = + BranchNodeRef::new(&buffers.branch_value_stack_buf, *state_mask); + let rlp_node = branch_node_ref.rlp(rlp_buf); + *hash = rlp_node.as_hash(); + + // Save a branch node update only if it's not a root node, and we need to + // persist updates. + let store_in_db_trie_value = if let Some(updates) = + self.updates.as_mut().filter(|_| retain_updates && !path.is_empty()) + { + let store_in_db_trie = !tree_mask.is_empty() || !hash_mask.is_empty(); + if store_in_db_trie { + // Store in DB trie if there are either any children that are stored in + // the DB trie, or any children represent hashed values + hashes.reverse(); + let branch_node = BranchNodeCompact::new( + *state_mask, + tree_mask, + hash_mask, + hashes, + hash.filter(|_| path.is_empty()), + ); + updates.updated_nodes.insert(path, branch_node); + } else if self + .branch_node_tree_masks + .get(&path) + .is_some_and(|mask| !mask.is_empty()) || + self.branch_node_hash_masks + .get(&path) + .is_some_and(|mask| !mask.is_empty()) + { + // If new tree and hash masks are empty, but previously they weren't, we + // need to remove the node update and add the node itself to the list of + // removed nodes. + updates.updated_nodes.remove(&path); + updates.removed_nodes.insert(path); + } else if self + .branch_node_tree_masks + .get(&path) + .is_none_or(|mask| mask.is_empty()) && + self.branch_node_hash_masks + .get(&path) + .is_none_or(|mask| mask.is_empty()) + { + // If new tree and hash masks are empty, and they were previously empty + // as well, we need to remove the node update. + updates.updated_nodes.remove(&path); + } + + store_in_db_trie + } else { + false + }; + *store_in_db_trie = Some(store_in_db_trie_value); + + ( + rlp_node, + SparseNodeType::Branch { store_in_db_trie: Some(store_in_db_trie_value) }, + ) + } + }; + + trace!( + target: "trie::sparse", + ?_starting_path, + ?level, + ?path, + ?node, + ?node_type, + ?is_in_prefix_set, + "Added node to rlp node stack" + ); + + buffers.rlp_node_stack.push(RlpNodeStackItem { path, rlp_node, node_type }); + } + + debug_assert_eq!(buffers.rlp_node_stack.len(), 1); + buffers.rlp_node_stack.pop().unwrap().rlp_node + } +} + +/// Enum representing sparse trie node type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SparseNodeType { + /// Empty trie node. + Empty, + /// A placeholder that stores only the hash for a node that has not been fully revealed. + Hash, + /// Sparse leaf node. + Leaf, + /// Sparse extension node. + Extension { + /// A flag indicating whether the extension node should be stored in the database. + store_in_db_trie: Option, + }, + /// Sparse branch node. + Branch { + /// A flag indicating whether the branch node should be stored in the database. + store_in_db_trie: Option, + }, +} + +impl SparseNodeType { + /// Returns true if the node is a hash node. + pub const fn is_hash(&self) -> bool { + matches!(self, Self::Hash) + } + + /// Returns true if the node is a branch node. + pub const fn is_branch(&self) -> bool { + matches!(self, Self::Branch { .. }) + } + + /// Returns true if the node should be stored in the database. + pub const fn store_in_db_trie(&self) -> Option { + match *self { + Self::Extension { store_in_db_trie } | Self::Branch { store_in_db_trie } => { + store_in_db_trie + } + _ => None, + } + } +} + +/// Enum representing trie nodes in sparse trie. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SparseNode { + /// Empty trie node. + Empty, + /// The hash of the node that was not revealed. + Hash(B256), + /// Sparse leaf node with remaining key suffix. + Leaf { + /// Remaining key suffix for the leaf node. + key: Nibbles, + /// Pre-computed hash of the sparse node. + /// Can be reused unless this trie path has been updated. + hash: Option, + }, + /// Sparse extension node with key. + Extension { + /// The key slice stored by this extension node. + key: Nibbles, + /// Pre-computed hash of the sparse node. + /// Can be reused unless this trie path has been updated. + /// + /// If [`None`], then the value is not known and should be calculated from scratch. + hash: Option, + /// Pre-computed flag indicating whether the trie node should be stored in the database. + /// Can be reused unless this trie path has been updated. + /// + /// If [`None`], then the value is not known and should be calculated from scratch. + store_in_db_trie: Option, + }, + /// Sparse branch node with state mask. + Branch { + /// The bitmask representing children present in the branch node. + state_mask: TrieMask, + /// Pre-computed hash of the sparse node. + /// Can be reused unless this trie path has been updated. + /// + /// If [`None`], then the value is not known and should be calculated from scratch. + hash: Option, + /// Pre-computed flag indicating whether the trie node should be stored in the database. + /// Can be reused unless this trie path has been updated. + /// + /// If [`None`], then the value is not known and should be calculated from scratch. + store_in_db_trie: Option, + }, +} + +impl SparseNode { + /// Create new sparse node from [`TrieNode`]. + pub fn from_node(node: TrieNode) -> Self { + match node { + TrieNode::EmptyRoot => Self::Empty, + TrieNode::Leaf(leaf) => Self::new_leaf(leaf.key), + TrieNode::Extension(ext) => Self::new_ext(ext.key), + TrieNode::Branch(branch) => Self::new_branch(branch.state_mask), + } + } + + /// Create new [`SparseNode::Branch`] from state mask. + pub const fn new_branch(state_mask: TrieMask) -> Self { + Self::Branch { state_mask, hash: None, store_in_db_trie: None } + } + + /// Create new [`SparseNode::Branch`] with two bits set. + pub const fn new_split_branch(bit_a: u8, bit_b: u8) -> Self { + let state_mask = TrieMask::new( + // set bits for both children + (1u16 << bit_a) | (1u16 << bit_b), + ); + Self::Branch { state_mask, hash: None, store_in_db_trie: None } + } + + /// Create new [`SparseNode::Extension`] from the key slice. + pub const fn new_ext(key: Nibbles) -> Self { + Self::Extension { key, hash: None, store_in_db_trie: None } + } + + /// Create new [`SparseNode::Leaf`] from leaf key and value. + pub const fn new_leaf(key: Nibbles) -> Self { + Self::Leaf { key, hash: None } + } + + /// Returns `true` if the node is a hash node. + pub const fn is_hash(&self) -> bool { + matches!(self, Self::Hash(_)) + } + + /// Returns the hash of the node if it exists. + pub const fn hash(&self) -> Option { + match self { + Self::Empty => None, + Self::Hash(hash) => Some(*hash), + Self::Leaf { hash, .. } | Self::Extension { hash, .. } | Self::Branch { hash, .. } => { + *hash + } + } + } + + /// Sets the hash of the node for testing purposes. + /// + /// For [`SparseNode::Empty`] and [`SparseNode::Hash`] nodes, this method does nothing. + #[cfg(test)] + pub const fn set_hash(&mut self, new_hash: Option) { + match self { + Self::Empty | Self::Hash(_) => { + // Cannot set hash for Empty or Hash nodes + } + Self::Leaf { hash, .. } | Self::Extension { hash, .. } | Self::Branch { hash, .. } => { + *hash = new_hash; + } + } + } +} + +/// A helper struct used to store information about a node that has been removed +/// during a deletion operation. +#[derive(Debug)] +struct RemovedSparseNode { + /// The path at which the node was located. + path: Nibbles, + /// The removed node + node: SparseNode, + /// For branch nodes, an optional nibble that should be unset due to the node being removed. + /// + /// During leaf deletion, this identifies the specific branch nibble path that + /// connects to the leaf being deleted. Then when restructuring the trie after deletion, + /// this nibble position will be cleared from the branch node's to + /// indicate that the child no longer exists. + /// + /// This is only set for branch nodes that have a direct path to the leaf being deleted. + unset_branch_nibble: Option, +} + +/// Collection of reusable buffers for [`SerialSparseTrie::rlp_node`] calculations. +/// +/// These buffers reduce allocations when computing RLP representations during trie updates. +#[derive(Debug, Default)] +pub struct RlpNodeBuffers { + /// Stack of RLP node paths + path_stack: Vec, + /// Stack of RLP nodes + rlp_node_stack: Vec, + /// Reusable branch child path + branch_child_buf: SmallVec<[Nibbles; 16]>, + /// Reusable branch value stack + branch_value_stack_buf: SmallVec<[RlpNode; 16]>, +} + +impl RlpNodeBuffers { + /// Creates a new instance of buffers with the root path on the stack. + fn new_with_root_path() -> Self { + Self { + path_stack: vec![RlpNodePathStackItem { + level: 0, + path: Nibbles::default(), + is_in_prefix_set: None, + }], + rlp_node_stack: Vec::new(), + branch_child_buf: SmallVec::<[Nibbles; 16]>::new_const(), + branch_value_stack_buf: SmallVec::<[RlpNode; 16]>::new_const(), + } + } +} + +/// RLP node path stack item. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RlpNodePathStackItem { + /// Level at which the node is located. Higher numbers correspond to lower levels in the trie. + pub level: usize, + /// Path to the node. + pub path: Nibbles, + /// Whether the path is in the prefix set. If [`None`], then unknown yet. + pub is_in_prefix_set: Option, +} + +/// RLP node stack item. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RlpNodeStackItem { + /// Path to the node. + pub path: Nibbles, + /// RLP node. + pub rlp_node: RlpNode, + /// Type of the node. + pub node_type: SparseNodeType, +} + +impl SparseTrieUpdates { + /// Create new wiped sparse trie updates. + pub fn wiped() -> Self { + Self { wiped: true, ..Default::default() } + } + + /// Clears the updates, but keeps the backing data structures allocated. + /// + /// Sets `wiped` to `false`. + pub fn clear(&mut self) { + self.updated_nodes.clear(); + self.removed_nodes.clear(); + self.wiped = false; + } + + /// Extends the updates with another set of updates. + pub fn extend(&mut self, other: Self) { + self.updated_nodes.extend(other.updated_nodes); + self.removed_nodes.extend(other.removed_nodes); + self.wiped |= other.wiped; + } +} + From 3e3da5e615803e80cc8b75f6638e22dc247c027b Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 16:17:43 +0800 Subject: [PATCH 04/13] chore: align struct-logger output with alloy 2.x, bump MSRV to 1.94, update docs - Drop the returnValue 0x-strip shim: alloy-rpc-types-trace 2.x serializes the prefix on both mega-reth and this server, so emitting the frame as-is is what now matches. - rust-version 1.94 (mega-evm and the optimism monorepo crates require it). - Document the vendored withdrawal sparse trie and erc7562Tracer handling. --- AGENTS.md | 6 +-- Cargo.toml | 2 +- README.md | 4 +- .../src/tracing_executor.rs | 29 ++++--------- crates/stateless-core/src/withdrawals/mod.rs | 10 ++--- .../src/withdrawals/sparse_trie/errors.rs | 6 ++- .../src/withdrawals/sparse_trie/provider.rs | 5 ++- .../src/withdrawals/sparse_trie/traits.rs | 10 ++--- .../src/withdrawals/sparse_trie/trie.rs | 43 +++++++------------ 9 files changed, 45 insertions(+), 70 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eb3b0557..7916f3bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ cargo clippy --workspace --all-targets --all-features cargo sort --check --workspace --grouped --order package,workspace,lints,profile,bin,benches,dependencies,dev-dependencies,features ``` -The project uses nightly `2026-02-03` toolchain (edition 2024, rust-version 1.95). +The project uses nightly `2026-02-03` toolchain (edition 2024, rust-version 1.94). ## Workspace Structure @@ -112,7 +112,7 @@ Inbound JSON-RPC batch entries execute concurrently as independent runtime tasks HTTP responses negotiate gzip/zstd compression per request via `Accept-Encoding` (kill switch: `--response-compression-disabled`); bodies under 4 KiB stay identity, the stack order lives in `http_middleware()` (compression outside the size/timing layers keeps `x-response-size` at the uncompressed size and `x-execution-time-ns` free of compression CPU), and response-cache hits re-compress per hit. Body-streaming cost is metered by the outermost middleware layer (`debug_trace_body_cpu_time_seconds` / `debug_trace_wire_bytes_total`, labeled by encoding), since every request-scoped measurement is sealed before the body streams. The response cache is keyed by `(resource, block hash, typed tracer variant)`; by-number handlers resolve number → canonical hash before the lookup (local `CANONICAL_CHAIN` first, upstream fallback). -It only stores idempotent request shapes: the five built-in tracers (keyed by their parsed typed `tracerConfig`) and the bare default struct-logger request; JS tracers, `muxTracer`, and struct-logger requests with non-default flags bypass it entirely, and a type-malformed `tracerConfig` on call/prestate/flatCall is rejected with `-32602`. +It only stores idempotent request shapes: the five supported built-in tracers (keyed by their parsed typed `tracerConfig`) and the bare default struct-logger request; JS tracers, `muxTracer`, the unimplemented `erc7562Tracer` (rejected with a request error), and struct-logger requests with non-default flags bypass it entirely, and a type-malformed `tracerConfig` on call/prestate/flatCall is rejected with `-32602`. Canonical-hash resolution reads the bounded `CANONICAL_CHAIN` window first (local cache mode), then a bounded in-memory memo of upstream-resolved bindings (`--canonical-hash-memo-capacity`; only depth-final heights are memoized, so a memoized binding can no longer reorg), and only then upstream — so historical heights resolve locally after first touch within a process lifetime. The memo's depth gate learns the tip from the window and `latest`-tag lookups, falling back to a throttled upstream `eth_blockNumber` seed (stateless mode with numeric-only traffic); chain sync clears the memo on stale resets and on reorgs at least the safety depth deep. Below the response cache, a bounded in-memory `BlockData` cache keyed by block hash (`--block-data-cache-max-size`, default 1GB, 0 disables) fronts the DB and RPC tiers; block-number lookups resolve number → hash before touching it, so canonicality is never cached and it needs no reorg invalidation. @@ -129,7 +129,7 @@ The background chain-sync prefetch routes by freshness against the last observed | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay (generic over the `BlockInput` projection) | | `crates/stateless-core/src/evm_database.rs` | WitnessDatabase implementing `revm::DatabaseRef` | | `crates/stateless-core/src/db.rs` | Shared storage traits (`ContractStore`, `ChainStore`) + `StoreError` / `StoreResult` | -| `crates/stateless-core/src/withdrawals.rs` | Withdrawal validation and MPT witness handling | +| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (sparse trie vendored from reth v1.6.0) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | RPC client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | RpcMethod, RpcMetrics, RpcClientConfig | diff --git a/Cargo.toml b/Cargo.toml index 4846e362..f5892eb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ resolver = "2" [workspace.package] version = "2.0.16" edition = "2024" -rust-version = "1.91" +rust-version = "1.94" license = "MIT OR Apache-2.0" repository = "https://github.com/megaeth-labs/stateless-validator" exclude = [".github/"] diff --git a/README.md b/README.md index 029a74e5..f3f26fb0 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The HTTP response cache is keyed by `(resource, block hash, tracer variant)` — Entries hold the reply's raw JSON bytes, serialized exactly once from the trace output and spliced verbatim into every response — a hit shares the bytes by `Arc` instead of re-parsing and re-serializing the JSON tree. By-number requests resolve their canonical hash before the lookup (local index, then an in-memory memo, then upstream `eth_getHeaderByNumber`), so a reorged height resolves to the new hash and misses cleanly. Cacheable shapes are the five built-in tracers (`callTracer`, `prestateTracer`, `4byteTracer`, `noopTracer`, `flatCallTracer`, keyed by their parsed, typed `tracerConfig` — equivalent configs collapse onto one entry) and the bare default struct-logger request (no tracer, no `tracerConfig`, default flags). -JS tracers, `muxTracer`, and struct-logger requests with non-default flags bypass the cache and are recomputed on every request. +JS tracers, `muxTracer`, the unimplemented `erc7562Tracer` (rejected with a request error), and struct-logger requests with non-default flags bypass the cache and are recomputed on every request. A type-malformed `tracerConfig` on a config-reading builtin (`callTracer`/`prestateTracer`/`flatCallTracer`) is rejected with `-32602 invalid params` instead of being silently traced with default settings. Disable the cache with `--response-cache-disabled`; `--response-cache-estimated-items` must be at least 1 (the old `=0` disable convention is rejected at startup). @@ -273,7 +273,7 @@ The pipeline is configured via `PipelineConfig` and customized through trait imp | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay | | `crates/stateless-core/src/db.rs` | Shared storage traits: `ChainStore`, `ContractStore`, `StoreError` (scenario stores live in their binaries) | | `crates/stateless-core/src/evm_database.rs` | `WitnessDatabase` implementing `revm::DatabaseRef` | -| `crates/stateless-core/src/withdrawals.rs` | Withdrawal validation and MPT witness handling | +| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (sparse trie vendored from reth v1.6.0) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and bounded `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | `RpcClient`: multi-endpoint HTTP client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | `RpcMethod`, `RpcMetrics`, `RpcClientConfig` | diff --git a/bin/debug-trace-server/src/tracing_executor.rs b/bin/debug-trace-server/src/tracing_executor.rs index 36ca5528..e992fcfb 100644 --- a/bin/debug-trace-server/src/tracing_executor.rs +++ b/bin/debug-trace-server/src/tracing_executor.rs @@ -367,19 +367,10 @@ fn trace_block_with_tracing_inspector( return_value, opts.config, ); - - // Convert DefaultFrame to JSON and fix returnValue serialization. - // alloy-rpc-types-trace 1.1.2 serializes Bytes with "0x" prefix, - // but mega-reth uses v1.0.23 which serializes without prefix. - let mut frame_value = serde_json::to_value(frame).unwrap(); - if let Some(rv) = frame_value.get_mut("returnValue") && - let Some(s) = rv.as_str() - { - *rv = serde_json::Value::String( - s.strip_prefix("0x").unwrap_or(s).to_string(), - ); - } - GethTrace::JS(frame_value) + // alloy-rpc-types-trace 2.x serializes `returnValue` with a "0x" + // prefix on both sides, so the frame is emitted as-is (the 1.x + // era needed a prefix-stripping shim to match mega-reth). + GethTrace::from(frame) } }; @@ -472,14 +463,10 @@ fn trace_tx_with_tracing_inspector( let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx_gas_limit); let frame = inspector.geth_builder().geth_traces(gas_used, return_value, opts.config); - - let mut frame_value = serde_json::to_value(frame).unwrap(); - if let Some(rv) = frame_value.get_mut("returnValue") && - let Some(s) = rv.as_str() - { - *rv = serde_json::Value::String(s.strip_prefix("0x").unwrap_or(s).to_string()); - } - Ok(GethTrace::JS(frame_value)) + // alloy-rpc-types-trace 2.x serializes `returnValue` with a "0x" prefix on + // both sides, so the frame is emitted as-is (the 1.x era needed a + // prefix-stripping shim to match mega-reth). + Ok(frame.into()) } } } diff --git a/crates/stateless-core/src/withdrawals/mod.rs b/crates/stateless-core/src/withdrawals/mod.rs index 2e24b2f8..ce88a0d9 100644 --- a/crates/stateless-core/src/withdrawals/mod.rs +++ b/crates/stateless-core/src/withdrawals/mod.rs @@ -10,7 +10,9 @@ //! absent (a minimal exclusion proof), a shape reth v2.3.0's V2 sparse tries cannot //! represent. -#[allow(dead_code)] +// Vendored verbatim (see its module docs): unused API surface and pre-let-chains +// style are expected, so both lints are silenced instead of editing the copy. +#[allow(dead_code, clippy::collapsible_if)] mod sparse_trie; use std::{ @@ -27,8 +29,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use self::sparse_trie::{ - SerialSparseTrie, SparseTrie, SparseTrieInterface, TrieMasks, - provider::DefaultTrieNodeProvider, + SerialSparseTrie, SparseTrie, SparseTrieInterface, TrieMasks, provider::DefaultTrieNodeProvider, }; /// L2 contract `L2ToL1MessagePasser`, storing commitments to withdrawal transactions. @@ -343,8 +344,7 @@ mod tests { // Root extension covering the first 63 nibbles of SLOT. let ext_key = Nibbles::unpack(SLOT).slice(0..63); - let ext = - TrieNode::Extension(ExtensionNode::new(ext_key, RlpNode::word_rlp(&branch_hash))); + let ext = TrieNode::Extension(ExtensionNode::new(ext_key, RlpNode::word_rlp(&branch_hash))); let mut ext_rlp = Vec::new(); ext.encode(&mut ext_rlp); let pre_root = keccak256(&ext_rlp); diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs b/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs index c498366d..d600f0b6 100644 --- a/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs +++ b/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs @@ -1,5 +1,6 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/evm/execution-errors/src/trie.rs (SparseTrieError portion)), trimmed to the -//! serial sparse trie needed by withdrawal MPT witness verification. +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/evm/execution-errors/src/trie.rs +//! (SparseTrieError portion)), trimmed to the serial sparse trie needed by withdrawal MPT witness +//! verification. //! //! reth v2.3.0's sparse tries moved to a V2 node model that merges extension //! nodes into their child branches; an extension whose child branch is absent @@ -13,6 +14,7 @@ //! test modules and metrics are not carried over. Do not edit otherwise. use alloc::boxed::Box; + use alloy_primitives::B256; use reth_trie_common::Nibbles; use thiserror::Error; diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs b/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs index fb3c1dbd..d0895f56 100644 --- a/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs +++ b/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs @@ -14,10 +14,11 @@ //! Traits and default implementations related to retrieval of blinded trie nodes. -use alloy_primitives::{Bytes, B256}; -use super::SparseTrieError; +use alloy_primitives::{B256, Bytes}; use reth_trie_common::{Nibbles, TrieMask}; +use super::SparseTrieError; + /// Factory for instantiating trie node providers. #[auto_impl::auto_impl(&)] pub trait TrieNodeProviderFactory { diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs b/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs index 3221f94f..1892c7d3 100644 --- a/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs +++ b/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs @@ -14,18 +14,16 @@ //! Traits for sparse trie implementations. +use alloc::{borrow::Cow, vec, vec::Vec}; use core::fmt::Debug; -use alloc::{borrow::Cow, vec, vec::Vec}; use alloy_primitives::{ - map::{HashMap, HashSet}, B256, + map::{HashMap, HashSet}, }; -use reth_trie_common::{BranchNodeCompact, TrieMask}; -use super::SparseTrieResult; -use reth_trie_common::{Nibbles, TrieNode}; +use reth_trie_common::{BranchNodeCompact, Nibbles, TrieMask, TrieNode}; -use super::provider::TrieNodeProvider; +use super::{SparseTrieResult, provider::TrieNodeProvider}; /// Trait defining common operations for revealed sparse trie implementations. /// diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs b/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs index a1aaf4b7..3cfc5ae8 100644 --- a/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs +++ b/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs @@ -1,5 +1,5 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/trie.rs (non-test portion)), trimmed to the -//! serial sparse trie needed by withdrawal MPT witness verification. +//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/trie.rs (non-test portion)), +//! trimmed to the serial sparse trie needed by withdrawal MPT witness verification. //! //! reth v2.3.0's sparse tries moved to a V2 node model that merges extension //! nodes into their child branches; an extension whose child branch is absent @@ -12,11 +12,6 @@ //! Local changes: import paths only (crate-relative + vendored errors); //! test modules and metrics are not carried over. Do not edit otherwise. -use super::{ - provider::{RevealedNode, TrieNodeProvider}, - LeafLookup, LeafLookupError, RevealedSparseNode, SparseTrieInterface, SparseTrieUpdates, - TrieMasks, -}; use alloc::{ borrow::Cow, boxed::Box, @@ -25,21 +20,26 @@ use alloc::{ vec, vec::Vec, }; + use alloy_primitives::{ - hex, keccak256, + B256, hex, keccak256, map::{Entry, HashMap, HashSet}, - B256, }; use alloy_rlp::Decodable; -use super::{SparseTrieErrorKind, SparseTrieResult}; use reth_trie_common::{ + BranchNodeCompact, BranchNodeRef, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, ExtensionNodeRef, + LeafNodeRef, Nibbles, RlpNode, TrieMask, TrieNode, prefix_set::{PrefixSet, PrefixSetMut}, - BranchNodeCompact, BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, RlpNode, TrieMask, - TrieNode, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, }; use smallvec::SmallVec; use tracing::trace; +use super::{ + LeafLookup, LeafLookupError, RevealedSparseNode, SparseTrieErrorKind, SparseTrieInterface, + SparseTrieResult, SparseTrieUpdates, TrieMasks, + provider::{RevealedNode, TrieNodeProvider}, +}; + /// The level below which the sparse trie hashes are calculated in /// [`SerialSparseTrie::update_subtrie_hashes`]. const SPARSE_TRIE_SUBTRIE_HASHES_LEVEL: usize = 2; @@ -160,22 +160,14 @@ impl SparseTrie { /// /// Returns `None` if the trie is blinded. pub const fn as_revealed_ref(&self) -> Option<&T> { - if let Self::Revealed(revealed) = self { - Some(revealed) - } else { - None - } + if let Self::Revealed(revealed) = self { Some(revealed) } else { None } } /// Returns a mutable reference to the underlying revealed sparse trie. /// /// Returns `None` if the trie is blinded. pub fn as_revealed_mut(&mut self) -> Option<&mut T> { - if let Self::Revealed(revealed) = self { - Some(revealed) - } else { - None - } + if let Self::Revealed(revealed) = self { Some(revealed) } else { None } } /// Wipes the trie by removing all nodes and values, @@ -912,11 +904,7 @@ impl SparseTrieInterface for SerialSparseTrie { // Take the current prefix set let mut prefix_set = core::mem::take(&mut self.prefix_set).freeze(); let rlp_node = self.rlp_node_allocate(&mut prefix_set); - if let Some(root_hash) = rlp_node.as_hash() { - root_hash - } else { - keccak256(rlp_node) - } + if let Some(root_hash) = rlp_node.as_hash() { root_hash } else { keccak256(rlp_node) } } fn update_subtrie_hashes(&mut self) { @@ -1930,4 +1918,3 @@ impl SparseTrieUpdates { self.wiped |= other.wiped; } } - From b61ce1f7401a0e1337ebbfcfce925e286e75d6c4 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 16:44:57 +0800 Subject: [PATCH 05/13] chore: group all Optimism-monorepo crates under the op section The reth section now holds only paradigmxyz/reth tag v2.3.0 crates; alloy-op-evm, alloy-op-hardforks, and reth-optimism-chainspec move next to the op-alloy crates they share a source identity (and pin comment) with. --- Cargo.toml | 13 +++++++------ bin/debug-trace-server/Cargo.toml | 2 +- crates/stateless-core/Cargo.toml | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5892eb9..51be8703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,6 @@ alloy-evm = { version = "0.36.0", default-features = false } alloy-genesis = { version = "2.0.5", default-features = false } alloy-hardforks = { version = "0.4.7", default-features = false } alloy-network = "2.0.5" -alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } -alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } alloy-primitives = { version = "1.6.0", default-features = false } alloy-provider = { version = "2.0.5", features = [ "reqwest", @@ -45,17 +43,20 @@ mega-evm = { git = "https://github.com/megaeth-labs/mega-evm.git", branch = "cz/ salt = { git = "https://github.com/megaeth-labs/salt.git", tag = "v1.0.5", default-features = false } # op -# The OP crates are pinned to the same Optimism monorepo revision as mega-evm's +# Every crate below is pinned to the same Optimism monorepo revision as mega-evm's # cz/chore/upgrade-revm-40 branch: crates.io has no op-revm / alloy-op-evm release built -# against revm 40, and every OP crate must share one source identity for OpTxEnvelope, -# receipt, and hardfork types. reth-optimism-chainspec also lives in that monorepo now. +# against revm 40, and all OP crates must share one source identity for OpTxEnvelope, +# receipt, and hardfork types. reth-optimism-chainspec moved from paradigmxyz/reth into +# the monorepo, so it is pinned here with the rest of the OP family. +alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } +reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } # reth reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } -reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism", rev = "f67d87cd5c9992b62d215c043917b94e3f019629", default-features = false } reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } # revm diff --git a/bin/debug-trace-server/Cargo.toml b/bin/debug-trace-server/Cargo.toml index b13bf230..a9ff4849 100644 --- a/bin/debug-trace-server/Cargo.toml +++ b/bin/debug-trace-server/Cargo.toml @@ -12,7 +12,6 @@ exclude.workspace = true alloy-consensus.workspace = true alloy-evm.workspace = true alloy-genesis.workspace = true -alloy-op-evm.workspace = true alloy-primitives.workspace = true alloy-rpc-types-eth.workspace = true alloy-rpc-types-trace.workspace = true @@ -21,6 +20,7 @@ alloy-rpc-types-trace.workspace = true mega-evm.workspace = true # op +alloy-op-evm.workspace = true op-alloy-network.workspace = true op-alloy-rpc-types.workspace = true diff --git a/crates/stateless-core/Cargo.toml b/crates/stateless-core/Cargo.toml index 6060eb0a..6b2fc652 100644 --- a/crates/stateless-core/Cargo.toml +++ b/crates/stateless-core/Cargo.toml @@ -14,8 +14,6 @@ alloy-eips.workspace = true alloy-evm.workspace = true alloy-genesis.workspace = true alloy-hardforks = { workspace = true, default-features = false } -alloy-op-evm.workspace = true -alloy-op-hardforks = { workspace = true, default-features = false } alloy-primitives.workspace = true alloy-rlp.workspace = true alloy-rpc-types-eth.workspace = true @@ -26,12 +24,14 @@ mega-evm = { workspace = true, default-features = false } salt = { workspace = true, default-features = false } # op +alloy-op-evm.workspace = true +alloy-op-hardforks = { workspace = true, default-features = false } op-alloy-consensus.workspace = true op-alloy-rpc-types.workspace = true +reth-optimism-chainspec.workspace = true # reth reth-ethereum-forks.workspace = true -reth-optimism-chainspec.workspace = true reth-trie-common.workspace = true # revm From e4a6c0d037bf9c2a67a8709a8c4718ab50128726 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 17:33:10 +0800 Subject: [PATCH 06/13] refactor: verify withdrawal MPT witnesses via HashBuilder linearization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the vendored reth-1.6 serial sparse trie (~1.9k lines) with a ~250-line linearization of the witnessed storage trie into alloy_trie::HashBuilder — the primitive reth's own eth_getProof generator is built on. Surviving and updated leaves stream as add_leaf (leaf paths are the hashed slots and never change); unwitnessed subtrees stream as add_branch (position and hash invariant under edits elsewhere); every structural change — extension splits, branch collapses, new branches — falls out of the stream shape inside the builder, so the module carries no trie mutation code at all. A pre-pass with no updates must reproduce storage_root exactly, self-checking the walk on every witness before its post-state root is trusted. Updates the witness cannot prove (descent into an unwitnessed subtree, a collapse adopting an unwitnessed node) fail closed, matching reth-1.6 behavior. Drops the reth-trie-common, smallvec, and auto_impl dependencies; adds tests for collapse merging, unwitnessed-survivor collapse, unwitnessed-region descent, and diverging-removal no-ops. --- AGENTS.md | 2 +- Cargo.lock | 27 +- Cargo.toml | 3 - README.md | 2 +- crates/stateless-core/Cargo.toml | 6 +- crates/stateless-core/src/lib.rs | 3 - crates/stateless-core/src/withdrawals/mod.rs | 532 ++++- .../src/withdrawals/sparse_trie/errors.rs | 83 - .../src/withdrawals/sparse_trie/mod.rs | 15 - .../src/withdrawals/sparse_trie/provider.rs | 88 - .../src/withdrawals/sparse_trie/traits.rs | 324 --- .../src/withdrawals/sparse_trie/trie.rs | 1920 ----------------- 12 files changed, 419 insertions(+), 2586 deletions(-) delete mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/errors.rs delete mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/mod.rs delete mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/provider.rs delete mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/traits.rs delete mode 100644 crates/stateless-core/src/withdrawals/sparse_trie/trie.rs diff --git a/AGENTS.md b/AGENTS.md index 7916f3bb..d0d84eda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ The background chain-sync prefetch routes by freshness against the last observed | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay (generic over the `BlockInput` projection) | | `crates/stateless-core/src/evm_database.rs` | WitnessDatabase implementing `revm::DatabaseRef` | | `crates/stateless-core/src/db.rs` | Shared storage traits (`ContractStore`, `ChainStore`) + `StoreError` / `StoreResult` | -| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (sparse trie vendored from reth v1.6.0) | +| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | RPC client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | RpcMethod, RpcMetrics, RpcClientConfig | diff --git a/Cargo.lock b/Cargo.lock index 25f58714..aba09cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4827,29 +4827,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "reth-trie-common" -version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-trie", - "arrayvec", - "bytes", - "derive_more", - "itertools 0.14.0", - "nybbles", - "reth-codecs", - "reth-primitives-traits", - "revm-database", - "serde", - "serde_with", -] - [[package]] name = "reth-zstd-compressors" version = "0.4.2" @@ -5892,7 +5869,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", - "auto_impl", + "alloy-trie", "bincode", "dyn-clone", "eyre", @@ -5904,13 +5881,11 @@ dependencies = [ "op-alloy-rpc-types", "reth-ethereum-forks", "reth-optimism-chainspec", - "reth-trie-common", "revm", "rustc-hash", "salt", "serde", "serde_json", - "smallvec", "stateless-test-utils", "thiserror 2.0.20", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 51be8703..d6387ea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,14 +57,12 @@ reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism # reth reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } -reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v2.3.0", default-features = false } # revm revm = { version = "40.0.3", default-features = false } revm-inspectors = { version = "0.40.1", features = ["std", "js-tracer"], default-features = false } # misc -auto_impl = { version = "1.3", default-features = false } base64 = { version = "0.22", default-features = false } bincode = { version = "2.0", features = ["serde", "alloc"], default-features = false } bytes = "1.11" @@ -103,7 +101,6 @@ rustc-hash = { version = "2.1", default-features = false } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } sha2 = { version = "0.10", default-features = false } -smallvec = { version = "1.15", features = ["const_new"], default-features = false } tempfile = { version = "3.27", default-features = false } thiserror = { version = "2.0", default-features = false } tokio = { version = "1.51", features = ["rt-multi-thread", "signal"], default-features = false } diff --git a/README.md b/README.md index f3f26fb0..9dd457fc 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ The pipeline is configured via `PipelineConfig` and customized through trait imp | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay | | `crates/stateless-core/src/db.rs` | Shared storage traits: `ChainStore`, `ContractStore`, `StoreError` (scenario stores live in their binaries) | | `crates/stateless-core/src/evm_database.rs` | `WitnessDatabase` implementing `revm::DatabaseRef` | -| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (sparse trie vendored from reth v1.6.0) | +| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and bounded `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | `RpcClient`: multi-endpoint HTTP client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | `RpcMethod`, `RpcMetrics`, `RpcClientConfig` | diff --git a/crates/stateless-core/Cargo.toml b/crates/stateless-core/Cargo.toml index 6b2fc652..1b83ee39 100644 --- a/crates/stateless-core/Cargo.toml +++ b/crates/stateless-core/Cargo.toml @@ -18,6 +18,7 @@ alloy-primitives.workspace = true alloy-rlp.workspace = true alloy-rpc-types-eth.workspace = true alloy-serde.workspace = true +alloy-trie.workspace = true # mega mega-evm = { workspace = true, default-features = false } @@ -32,13 +33,11 @@ reth-optimism-chainspec.workspace = true # reth reth-ethereum-forks.workspace = true -reth-trie-common.workspace = true # revm revm.workspace = true # misc -auto_impl.workspace = true dyn-clone.workspace = true eyre = { workspace = true, optional = true } hashbrown.workspace = true @@ -47,7 +46,6 @@ num_cpus = { workspace = true, optional = true } rustc-hash = { workspace = true, default-features = false } serde.workspace = true serde_json.workspace = true -smallvec.workspace = true thiserror = { workspace = true, default-features = false } tokio = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } @@ -77,12 +75,12 @@ std = [ "alloy-rlp/std", "alloy-rpc-types-eth/std", "alloy-serde/std", + "alloy-trie/std", "mega-evm/default", "op-alloy-consensus/std", "op-alloy-rpc-types/std", "reth-ethereum-forks/std", "reth-optimism-chainspec/std", - "reth-trie-common/std", "revm/default", "rustc-hash/std", "salt/default", diff --git a/crates/stateless-core/src/lib.rs b/crates/stateless-core/src/lib.rs index 2d3068bd..48b6609e 100644 --- a/crates/stateless-core/src/lib.rs +++ b/crates/stateless-core/src/lib.rs @@ -21,9 +21,6 @@ #[cfg(not(feature = "std"))] extern crate alloc as std; -// The vendored sparse-trie code (`withdrawals::sparse_trie`) uses `alloc::` paths verbatim. -extern crate alloc; - pub mod chain_spec; pub mod light_witness; pub use light_witness::{LightWitness, LightWitnessExecutor, LightWitnessFromSalt}; diff --git a/crates/stateless-core/src/withdrawals/mod.rs b/crates/stateless-core/src/withdrawals/mod.rs index ce88a0d9..461a2f76 100644 --- a/crates/stateless-core/src/withdrawals/mod.rs +++ b/crates/stateless-core/src/withdrawals/mod.rs @@ -5,18 +5,34 @@ //! storage updates from block execution, it cryptographically proves the storage root //! transition is valid. //! -//! The trie machinery lives in [`sparse_trie`], vendored from reth v1.6.0: the witness -//! format is frozen upstream and can contain extension nodes whose child branch is -//! absent (a minimal exclusion proof), a shape reth v2.3.0's V2 sparse tries cannot -//! represent. - -// Vendored verbatim (see its module docs): unused API surface and pre-let-chains -// style are expected, so both lints are silenced instead of editing the copy. -#[allow(dead_code, clippy::collapsible_if)] -mod sparse_trie; +//! # Approach +//! +//! The witness is the union of `eth_getProof`-style minimal per-slot proofs for the +//! contract's storage trie: RLP nodes along each touched slot's inclusion or exclusion +//! path, and nothing else. In particular, an exclusion path may end at an extension +//! node whose child branch is absent — splitting that extension on insert never reads +//! the child. +//! +//! Verification linearizes the witnessed trie into [`alloy_trie::HashBuilder`] — the +//! same primitive reth's own proof generator is built on — as a sorted stream of: +//! +//! - `add_leaf` for every surviving or updated leaf (leaf paths are the hashed slots and never +//! change), and +//! - `add_branch` for every unwitnessed subtree (its position and hash are invariant under edits +//! elsewhere). +//! +//! All structural changes — extension splits, branch collapses, new branches — fall +//! out of the stream shape inside the builder, so no trie mutation code exists here. +//! +//! Two passes run per verification. The pre-pass linearizes the witness with **no** +//! updates and must reproduce `storage_root` bit-for-bit, self-checking the walk on +//! the very witness being verified; the post-pass applies the updates and must +//! reproduce the header's `withdrawals_root`. Updates that cannot be proven against +//! the witness (descent into an unwitnessed subtree, or a branch collapse adopting an +//! unwitnessed node) fail closed, matching the behavior of the reth v1.6.0 sparse +//! trie this module previously relied on. use std::{ - collections::VecDeque, string::{String, ToString}, vec::Vec, }; @@ -24,14 +40,13 @@ use std::{ use alloy_consensus::Header; use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256, map::B256Map}; use alloy_rlp::Decodable; -use reth_trie_common::{EMPTY_ROOT_HASH, Nibbles, RlpNode, TrieNode}; +use alloy_trie::{ + EMPTY_ROOT_HASH, HashBuilder, Nibbles, + nodes::{RlpNode, TrieNode}, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use self::sparse_trie::{ - SerialSparseTrie, SparseTrie, SparseTrieInterface, TrieMasks, provider::DefaultTrieNodeProvider, -}; - /// L2 contract `L2ToL1MessagePasser`, storing commitments to withdrawal transactions. pub const ADDRESS_L2_TO_L1_MESSAGE_PASSER: Address = address!("0x4200000000000000000000000000000000000016"); @@ -52,6 +67,15 @@ pub enum WithdrawalValidationError { PostStateRootMismatch { expected: B256, actual: B256 }, } +fn trie_error(msg: impl ToString) -> WithdrawalValidationError { + WithdrawalValidationError::TrieOperationFailed(msg.to_string()) +} + +/// A pending slot update: `Some(rlp)` inserts or overwrites the leaf, `None` removes it. +type LeafUpdate = Option>; +/// Sorted (by path) pending updates, sliced narrower at every recursion step. +type Updates = [(Nibbles, LeafUpdate)]; + /// Pre-state witness for the L2ToL1MessagePasser contract storage trie. /// /// Contains the storage root and trie nodes needed to verify storage state @@ -72,9 +96,9 @@ impl MptWitness { /// /// # Process /// - /// 1. Reveals the storage trie via BFS from `storage_root`, verifies the pre-state root - /// 2. Applies storage updates (inserts non-zero, removes zero values) - /// 3. Computes the post-state root, verifies against `header.withdrawals_root` + /// 1. Linearizes the witness with no updates; the rebuilt root must equal `storage_root` + /// 2. Linearizes it again with the updates applied (inserts non-zero, removes zero values) + /// 3. The rebuilt post-state root must equal `header.withdrawals_root` /// /// # Arguments /// @@ -86,7 +110,7 @@ impl MptWitness { /// * `MissingWithdrawalsRoot` - Header lacks `withdrawals_root` field /// * `PreStateRootMismatch` - Witness doesn't match expected pre-state root /// * `PostStateRootMismatch` - Computed post-state doesn't match header - /// * `TrieOperationFailed` - Trie reveal/update/removal failed + /// * `TrieOperationFailed` - Malformed witness node, or an update the witness cannot prove pub fn verify( &self, header: &Header, @@ -95,12 +119,25 @@ impl MptWitness { let expected_post_root = header.withdrawals_root.ok_or(WithdrawalValidationError::MissingWithdrawalsRoot)?; - let mut trie = self.reveal()?; + let mut updates: Vec<(Nibbles, LeafUpdate)> = storage_updates + .into_iter() + .map(|(slot, value)| { + let update = + (!value.is_zero()).then(|| alloy_rlp::encode_fixed_size(&value).to_vec()); + (Nibbles::unpack(slot), update) + }) + .collect(); + updates.sort_unstable_by_key(|(path, _)| *path); + + let linearizer = Linearizer { + nodes: self.state.iter().map(|node| (keccak256(node.as_ref()), node)).collect(), + }; - // Verify the pre-state root. `root()` returns `None` when the trie is still - // blind, which happens when `storage_root` is non-empty but the witness lacks - // the root node; the check below catches that as a mismatch. - let pre_root = trie.root().unwrap_or(EMPTY_ROOT_HASH); + // Pre-state: rebuilding the witnessed trie with no updates must reproduce + // `storage_root` exactly. Besides authenticating the witness, this self-checks + // the linearization walk on this very witness before its post-state output is + // trusted. + let pre_root = linearizer.compute_root(self.storage_root, &[])?; if pre_root != self.storage_root { return Err(WithdrawalValidationError::PreStateRootMismatch { expected: self.storage_root, @@ -108,21 +145,7 @@ impl MptWitness { }); } - // Apply storage updates from block execution - for (slot, value) in storage_updates { - let nibbles = Nibbles::unpack(slot); - if !value.is_zero() { - let encoded = alloy_rlp::encode_fixed_size(&value).to_vec(); - trie.update_leaf(nibbles, encoded, DefaultTrieNodeProvider) - .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - } else { - trie.remove_leaf(&nibbles, DefaultTrieNodeProvider) - .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - } - } - - // Verify the post-state root (same blind-trie caveat as the pre-state). - let post_root = trie.root().unwrap_or(EMPTY_ROOT_HASH); + let post_root = linearizer.compute_root(self.storage_root, &updates)?; if post_root != expected_post_root { return Err(WithdrawalValidationError::PostStateRootMismatch { expected: expected_post_root, @@ -132,68 +155,276 @@ impl MptWitness { Ok(()) } +} - /// Reveals the witness into a sparse storage trie via BFS from `storage_root`. - /// - /// Nodes referenced by hash but absent from the witness stay blind — legal for - /// subtrees the block does not touch (and, for extension children, even for - /// inserts that split the extension). An empty `storage_root` yields a - /// revealed-empty trie so that first-ever withdrawals can insert into it. - fn reveal(&self) -> Result { - if self.storage_root == EMPTY_ROOT_HASH { - return Ok(SparseTrie::revealed_empty()); +/// Streams the (post-update) content of a witnessed storage trie into a +/// [`HashBuilder`] in sorted order. +struct Linearizer<'a> { + /// Content-addressed witness nodes: `keccak(rlp) → rlp`. + nodes: B256Map<&'a Bytes>, +} + +impl Linearizer<'_> { + /// Rebuilds the trie root that results from applying `updates` to the witnessed + /// trie rooted at `root`. + fn compute_root( + &self, + root: B256, + updates: &Updates, + ) -> Result { + let mut hb = HashBuilder::default(); + if root == EMPTY_ROOT_HASH { + // Nothing exists: inserts materialize, removals of absent slots are no-ops. + for (path, update) in updates { + if let Some(value) = update { + hb.add_leaf(*path, value); + } + } + } else if self.nodes.contains_key(&root) { + self.emit_subtree(&mut hb, &RlpNode::word_rlp(&root), Nibbles::default(), updates)?; + } else { + // The root node is absent, so the witness proves nothing; report the + // reconstruction as empty and let the pre-state check flag the mismatch. + return Ok(EMPTY_ROOT_HASH); } + Ok(hb.root()) + } - let witness_map: B256Map<&Bytes> = - self.state.iter().map(|node| (keccak256(node.as_ref()), node)).collect(); + /// Decodes the trie node behind `node_ref`: from the witness for hash references + /// (`None` when absent), or in place for inline (< 32 byte) nodes. + fn resolve(&self, node_ref: &RlpNode) -> Result, WithdrawalValidationError> { + let bytes: &[u8] = match node_ref.as_hash() { + Some(hash) => match self.nodes.get(&hash) { + Some(bytes) => bytes, + None => return Ok(None), + }, + None => node_ref, + }; + TrieNode::decode(&mut &bytes[..]).map(Some).map_err(trie_error) + } - let trie_err = |e: sparse_trie::SparseTrieError| { - WithdrawalValidationError::TrieOperationFailed(e.to_string()) + /// Emits the final content of the subtree at `path` (referenced by `node_ref`) + /// into `hb`, applying `updates` (all of which lie strictly under `path`). + /// + /// Witnessed nodes are always expanded — leaves become `add_leaf` entries and + /// unwitnessed children become `add_branch` entries — so the pre- and post-state + /// passes exercise the identical walk. + fn emit_subtree( + &self, + hb: &mut HashBuilder, + node_ref: &RlpNode, + path: Nibbles, + updates: &Updates, + ) -> Result<(), WithdrawalValidationError> { + let Some(node) = self.resolve(node_ref)? else { + if updates.is_empty() { + // Untouched subtree: its position and hash are invariant under edits + // elsewhere, so the opaque hash stands in for the whole region. + hb.add_branch( + path, + node_ref.as_hash().expect("inline nodes always resolve"), + false, + ); + return Ok(()); + } + return Err(trie_error(format!("update descends into unwitnessed subtree at {path:?}"))); }; - let mut trie = SparseTrie::::blind(); - let mut queue = VecDeque::from([(self.storage_root, Nibbles::default())]); - while let Some((hash, path)) = queue.pop_front() { - // Nodes referenced by hash but not present stay blind. - let Some(bytes) = witness_map.get(&hash) else { continue }; - let node = TrieNode::decode(&mut &bytes[..]) - .map_err(|e| WithdrawalValidationError::TrieOperationFailed(e.to_string()))?; - - // Queue hash-referenced children; inline children are revealed as part of - // their parent node. - match &node { - TrieNode::Branch(branch) => { - for (idx, maybe_child) in branch.as_ref().children() { - if let Some(child_hash) = maybe_child.and_then(RlpNode::as_hash) { - let mut child_path = path; - child_path.push_unchecked(idx); - queue.push_back((child_hash, child_path)); - } + match node { + TrieNode::EmptyRoot => { + // Valid only as the root of an empty trie, which `compute_root` + // short-circuits; as a child this is a malformed witness. + Err(trie_error(format!("unexpected empty trie node at {path:?}"))) + } + TrieNode::Leaf(leaf) => { + let full = path.join(&leaf.key); + // The leaf is the region's only occupant: a removal of any other path + // is a proven no-op, a write at `full` supersedes the leaf. + let mut finals: Vec<(Nibbles, &[u8])> = Vec::with_capacity(updates.len() + 1); + let mut survivor = Some((full, leaf.value.as_slice())); + for (update_path, update) in updates { + if *update_path == full { + survivor = None; + } + if let Some(value) = update { + finals.push((*update_path, value)); + } + } + finals.extend(survivor); + finals.sort_unstable_by_key(|(path, _)| *path); + for (leaf_path, value) in finals { + hb.add_leaf(leaf_path, value); + } + Ok(()) + } + TrieNode::Extension(ext) => { + let boundary = path.join(&ext.key); + let (before, under, after) = split_at_prefix(updates, &boundary); + // Updates diverging inside the extension key are proven to target + // absent slots: removals are no-ops, inserts become fresh leaves. + // The extension split this implies (and the child hash it reuses) + // falls out of the emitted stream — the child is never read. + for (leaf_path, update) in before { + if let Some(value) = update { + hb.add_leaf(*leaf_path, value); + } + } + self.emit_subtree(hb, &ext.child, boundary, under)?; + for (leaf_path, update) in after { + if let Some(value) = update { + hb.add_leaf(*leaf_path, value); } } - TrieNode::Extension(ext) => { - if let Some(child_hash) = ext.child.as_hash() { - let mut child_path = path; - child_path.extend(&ext.key); - queue.push_back((child_hash, child_path)); + Ok(()) + } + TrieNode::Branch(branch) => { + // Partition updates into per-child regions (contiguous runs, since + // they are sorted and all share `path` as a prefix). + let mut regions: Vec<(u8, Option, &Updates)> = Vec::new(); + let mut rest = updates; + for (idx, child) in branch.as_ref().children() { + let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); + let (region, tail) = rest.split_at(split); + rest = tail; + regions.push((idx, child.cloned(), region)); + } + debug_assert!(rest.is_empty()); + + // A branch that ends up with a single surviving region collapses: the + // survivor merges upward. That is derived by the `HashBuilder` from + // the stream — except when the sole survivor is an untouched + // unwitnessed subtree, whose node kind (and thus merged shape) is + // unknowable. reth v1.6.0's sparse trie failed the same way, via its + // empty node provider. + let mut survivors = 0usize; + let mut sole: Option<&(u8, Option, &Updates)> = None; + for region in ®ions { + if self.region_survives(region, path)? { + survivors += 1; + sole = Some(region); + } + } + if survivors == 1 { + let (idx, child, region) = sole.expect("survivors == 1"); + if region.is_empty() && + let Some(child) = child && + let Some(hash) = child.as_hash() && + !self.nodes.contains_key(&hash) + { + return Err(trie_error(format!( + "branch collapse at {path:?} adopts unwitnessed subtree at child {idx:x}" + ))); } } - TrieNode::Leaf(_) | TrieNode::EmptyRoot => {} + + for (idx, child, region) in regions { + let mut child_path = path; + child_path.push_unchecked(idx); + match child { + Some(child) => self.emit_subtree(hb, &child, child_path, region)?, + None => { + // Empty slot: absence is proven by the branch mask, so + // removals are no-ops and inserts are fresh leaves. + for (leaf_path, update) in region { + if let Some(value) = update { + hb.add_leaf(*leaf_path, value); + } + } + } + } + } + Ok(()) + } + } + } + + /// Whether a branch child region still holds any content after its updates. + fn region_survives( + &self, + (idx, child, region): &(u8, Option, &Updates), + path: Nibbles, + ) -> Result { + // Any insert keeps the region alive; existing content with no updates survives. + if region.iter().any(|(_, update)| update.is_some()) { + return Ok(true); + } + match child { + None => Ok(false), + Some(child) if region.is_empty() => { + let _ = child; + Ok(true) + } + Some(child) => { + let mut child_path = path; + child_path.push_unchecked(*idx); + self.survives_removals(child, child_path, region) } + } + } - // BFS visits parents before children, so the root is always first. - if path.is_empty() { - trie.reveal_root(node, TrieMasks::none(), false).map_err(trie_err)?; - } else { - trie.as_revealed_mut() - .expect("root node is revealed first") - .reveal_node(path, node, TrieMasks::none()) - .map_err(trie_err)?; + /// Whether the subtree at `path` still holds content after applying `removals` + /// (a region whose updates are all `None`). + fn survives_removals( + &self, + node_ref: &RlpNode, + path: Nibbles, + removals: &Updates, + ) -> Result { + debug_assert!(removals.iter().all(|(_, update)| update.is_none())); + if removals.is_empty() { + return Ok(true); + } + let Some(node) = self.resolve(node_ref)? else { + return Err(trie_error(format!( + "removal descends into unwitnessed subtree at {path:?}" + ))); + }; + match node { + TrieNode::EmptyRoot => Ok(false), + TrieNode::Leaf(leaf) => { + let full = path.join(&leaf.key); + Ok(!removals.iter().any(|(p, _)| *p == full)) + } + TrieNode::Extension(ext) => { + let boundary = path.join(&ext.key); + let (_, under, _) = split_at_prefix(removals, &boundary); + self.survives_removals(&ext.child, boundary, under) + } + TrieNode::Branch(branch) => { + let mut rest = removals; + for (idx, child) in branch.as_ref().children() { + let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); + let (region, tail) = rest.split_at(split); + rest = tail; + let Some(child) = child else { continue }; + let mut child_path = path; + child_path.push_unchecked(idx); + if region.is_empty() || self.survives_removals(child, child_path, region)? { + return Ok(true); + } + } + Ok(false) } } + } +} - Ok(trie) +/// Splits sorted `updates` around the region prefixed by `prefix`: +/// `(diverging before, under prefix, diverging after)`. +/// +/// Sound because for 64-nibble update paths and a shorter `prefix`, every path +/// under the prefix compares greater than any diverging-lower path and smaller +/// than any diverging-higher path, so the region is one contiguous run. +fn split_at_prefix<'u>( + updates: &'u Updates, + prefix: &Nibbles, +) -> (&'u Updates, &'u Updates, &'u Updates) { + let lo = updates.partition_point(|(p, _)| p < prefix); + let mut hi = lo; + while hi < updates.len() && updates[hi].0.starts_with(prefix) { + hi += 1; } + (&updates[..lo], &updates[lo..hi], &updates[hi..]) } #[cfg(test)] @@ -203,19 +434,31 @@ mod tests { use WithdrawalValidationError::*; use alloy_primitives::b256; use alloy_rlp::Encodable; - use reth_trie_common::{BranchNode, ExtensionNode, LeafNode}; + use alloy_trie::nodes::{BranchNode, ExtensionNode, LeafNode}; use super::*; const SLOT: B256 = b256!("0x1111111111111111111111111111111111111111111111111111111111111111"); const BOGUS: B256 = b256!("0xdeadbeef00000000000000000000000000000000000000000000000000000000"); + fn encoded(node: &TrieNode) -> Bytes { + let mut rlp = Vec::new(); + node.encode(&mut rlp); + Bytes::from(rlp) + } + + fn value_rlp(value: u64) -> Vec { + alloy_rlp::encode_fixed_size(&U256::from(value)).to_vec() + } + + /// A leaf node holding `value` under the trailing `key` nibbles. + fn leaf_node(key: Nibbles, value: u64) -> TrieNode { + TrieNode::Leaf(LeafNode::new(key, value_rlp(value))) + } + /// One-leaf storage trie for `SLOT → value`: returns (root, [leaf_bytes]). fn leaf(value: u64) -> (B256, Vec) { - let value_rlp = alloy_rlp::encode_fixed_size(&U256::from(value)).to_vec(); - let mut rlp = Vec::new(); - TrieNode::Leaf(LeafNode::new(Nibbles::unpack(SLOT), value_rlp)).encode(&mut rlp); - let bytes = Bytes::from(rlp); + let bytes = encoded(&leaf_node(Nibbles::unpack(SLOT), value)); (keccak256(&bytes), vec![bytes]) } @@ -303,8 +546,6 @@ mod tests { } /// First-ever withdrawal: empty pre-state storage trie, one new non-zero slot. - /// Exercises the `EMPTY_ROOT_HASH` pre-state path where the trie starts - /// revealed-empty so the insert can proceed. #[test] fn empty_pre_state_with_new_leaf() { let (post, _) = leaf(42); @@ -313,9 +554,9 @@ mod tests { /// Regression: an insert that splits an extension node whose child branch is /// absent from the witness (a legal minimal exclusion proof — splitting never - /// reads the child). reth v2.3.0's V2 sparse tries drop such extensions on - /// reveal and then fail the insert with "attempted to update blind node"; - /// the vendored v1.6.0 trie must handle it. + /// reads the child). reth v2.3.0's V2 sparse tries cannot represent this shape; + /// the `HashBuilder` linearization needs only the child hash the extension + /// itself carries. /// /// Trie shape: root extension (key = first 63 nibbles of `SLOT`) → branch with /// leaf children at `SLOT`'s last nibble (1) and at nibble 2. The witness @@ -323,30 +564,22 @@ mod tests { /// extension's first key nibble. #[test] fn insert_splitting_extension_with_absent_child_branch() { - let leaf_node = |value: u64| { - let value_rlp = alloy_rlp::encode_fixed_size(&U256::from(value)).to_vec(); - TrieNode::Leaf(LeafNode::new(Nibbles::default(), value_rlp)) - }; - let rlp_of = |node: &TrieNode| { - let mut buf = Vec::new(); - node.encode(&mut buf); - RlpNode::from_rlp(&buf) - }; + let rlp_of = |node: &TrieNode| RlpNode::from_rlp(&encoded(node)); // Child branch of the extension: two empty-key leaves at nibbles 1 and 2. let branch = TrieNode::Branch(BranchNode::new( - vec![rlp_of(&leaf_node(7)), rlp_of(&leaf_node(8))], + vec![ + rlp_of(&leaf_node(Nibbles::default(), 7)), + rlp_of(&leaf_node(Nibbles::default(), 8)), + ], 0b0110.into(), )); - let mut branch_rlp = Vec::new(); - branch.encode(&mut branch_rlp); - let branch_hash = keccak256(&branch_rlp); + let branch_hash = keccak256(encoded(&branch)); // Root extension covering the first 63 nibbles of SLOT. let ext_key = Nibbles::unpack(SLOT).slice(0..63); let ext = TrieNode::Extension(ExtensionNode::new(ext_key, RlpNode::word_rlp(&branch_hash))); - let mut ext_rlp = Vec::new(); - ext.encode(&mut ext_rlp); + let ext_rlp = encoded(&ext); let pre_root = keccak256(&ext_rlp); // Insert a slot that diverges from the extension key at nibble 0. @@ -359,25 +592,88 @@ mod tests { ext_key.slice(1..), RlpNode::word_rlp(&branch_hash), )); - let new_leaf = TrieNode::Leaf(LeafNode::new( - Nibbles::unpack(diverging_slot).slice(1..), - alloy_rlp::encode_fixed_size(&U256::from(9u64)).to_vec(), - )); + let new_leaf = + TrieNode::Leaf(LeafNode::new(Nibbles::unpack(diverging_slot).slice(1..), value_rlp(9))); let split_branch = TrieNode::Branch(BranchNode::new( vec![rlp_of(&shortened_ext), rlp_of(&new_leaf)], 0b0110.into(), )); - let mut split_rlp = Vec::new(); - split_branch.encode(&mut split_rlp); - let expected_post_root = keccak256(&split_rlp); + let expected_post_root = keccak256(encoded(&split_branch)); // Witness: extension only — the child branch is intentionally absent. run( pre_root, - vec![Bytes::from(ext_rlp)], + vec![ext_rlp], Some(expected_post_root), &[(diverging_slot, U256::from(9u64))], ) .unwrap(); } + + /// A zero-write to a slot that provably diverges inside a witnessed extension + /// key is a no-op: the post root equals the pre root. + #[test] + fn diverging_removal_is_noop() { + let branch = + TrieNode::Branch(BranchNode::new(vec![rlp_of_leaf(7), rlp_of_leaf(8)], 0b0110.into())); + let ext = TrieNode::Extension(ExtensionNode::new( + Nibbles::unpack(SLOT).slice(0..63), + RlpNode::word_rlp(&keccak256(encoded(&branch))), + )); + let ext_rlp = encoded(&ext); + let pre_root = keccak256(&ext_rlp); + let diverging_slot = + b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); + run(pre_root, vec![ext_rlp], Some(pre_root), &[(diverging_slot, U256::ZERO)]).unwrap(); + } + + fn rlp_of_leaf(value: u64) -> RlpNode { + RlpNode::from_rlp(&encoded(&leaf_node(Nibbles::default(), value))) + } + + /// Two-leaf trie under a root branch: (root, branch_rlp, leaf rlps, slots). + fn two_leaf_branch() -> (B256, Vec, B256, B256) { + let slot_b = b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); + let leaf_a = leaf_node(Nibbles::unpack(SLOT).slice(1..), 7); + let leaf_b = leaf_node(Nibbles::unpack(slot_b).slice(1..), 8); + let branch = TrieNode::Branch(BranchNode::new( + vec![RlpNode::from_rlp(&encoded(&leaf_a)), RlpNode::from_rlp(&encoded(&leaf_b))], + 0b0110.into(), + )); + let branch_rlp = encoded(&branch); + let root = keccak256(&branch_rlp); + (root, vec![branch_rlp, encoded(&leaf_a), encoded(&leaf_b)], SLOT, slot_b) + } + + /// Removing one of two leaves collapses the root branch into the surviving + /// leaf, whose node is witnessed — the merged single-leaf root must come out. + #[test] + fn removal_collapse_merges_witnessed_sibling() { + let (root, state, slot_a, slot_b) = two_leaf_branch(); + let expected = keccak256(encoded(&leaf_node(Nibbles::unpack(slot_b), 8))); + run(root, state, Some(expected), &[(slot_a, U256::ZERO)]).unwrap(); + } + + /// Same collapse, but the surviving sibling's node is absent from the witness: + /// its merged shape is unknowable, so verification must fail closed. + #[test] + fn removal_collapse_with_unwitnessed_survivor_errors() { + let (root, mut state, slot_a, _) = two_leaf_branch(); + state.truncate(2); // drop leaf_b — the survivor + assert!(matches!( + run(root, state, Some(root), &[(slot_a, U256::ZERO)]), + Err(TrieOperationFailed(_)) + )); + } + + /// An update descending into an unwitnessed branch child must fail closed. + #[test] + fn update_descending_into_unwitnessed_subtree_errors() { + let (root, mut state, _, slot_b) = two_leaf_branch(); + state.truncate(2); // drop leaf_b; slot_b's region is now opaque + assert!(matches!( + run(root, state, Some(root), &[(slot_b, U256::from(9u64))]), + Err(TrieOperationFailed(_)) + )); + } } diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs b/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs deleted file mode 100644 index d600f0b6..00000000 --- a/crates/stateless-core/src/withdrawals/sparse_trie/errors.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/evm/execution-errors/src/trie.rs -//! (SparseTrieError portion)), trimmed to the serial sparse trie needed by withdrawal MPT witness -//! verification. -//! -//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension -//! nodes into their child branches; an extension whose child branch is absent -//! from the witness (a legal, minimal exclusion proof — the child is never -//! needed to split the extension on insert) cannot be represented and is -//! silently dropped on reveal, breaking withdrawal-slot inserts that split -//! such extensions. The MegaETH witness format is frozen upstream, so the -//! proven v1.6.0 semantics are pinned here instead. -//! -//! Local changes: import paths only (crate-relative + vendored errors); -//! test modules and metrics are not carried over. Do not edit otherwise. - -use alloc::boxed::Box; - -use alloy_primitives::B256; -use reth_trie_common::Nibbles; -use thiserror::Error; - -/// Result type with [`SparseTrieError`] as error. -pub type SparseTrieResult = Result; - -/// Error encountered in `SparseTrie`. -#[derive(Error, Debug)] -#[error(transparent)] -pub struct SparseTrieError(#[from] Box); - -impl> From for SparseTrieError { - #[cold] - fn from(value: T) -> Self { - Self(Box::new(value.into())) - } -} - -impl SparseTrieError { - /// Returns the error kind. - pub const fn kind(&self) -> &SparseTrieErrorKind { - &self.0 - } - - /// Consumes the error and returns the error kind. - pub fn into_kind(self) -> SparseTrieErrorKind { - *self.0 - } -} - -/// [`SparseTrieError`] kind. -#[derive(Error, Debug)] -pub enum SparseTrieErrorKind { - /// Sparse trie is still blind. Thrown on attempt to update it. - #[error("sparse trie is blind")] - Blind, - /// Encountered blinded node on update. - #[error("attempted to update blind node at {path:?}: {hash}")] - BlindedNode { - /// Blind node path. - path: Nibbles, - /// Node hash - hash: B256, - }, - /// Encountered unexpected node at path when revealing. - #[error("encountered an invalid node at path {path:?} when revealing: {node:?}")] - Reveal { - /// Path to the node. - path: Nibbles, - /// Node that was at the path when revealing. - node: Box, - }, - /// RLP error. - #[error(transparent)] - Rlp(#[from] alloy_rlp::Error), - /// Node not found in provider during revealing. - #[error("node {path:?} not found in provider during removal")] - NodeNotFoundInProvider { - /// Path to the missing node. - path: Nibbles, - }, - /// Other. - #[error(transparent)] - Other(#[from] Box), -} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs b/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs deleted file mode 100644 index 92906fed..00000000 --- a/crates/stateless-core/src/withdrawals/sparse_trie/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Serial sparse MPT vendored from reth v1.6.0 for withdrawal witness verification. -//! -//! See the module docs in [`trie`] for why this is vendored rather than using -//! `reth-trie-sparse` v2.3.0. - -mod errors; -pub use errors::*; - -pub mod provider; - -mod traits; -pub use traits::*; - -mod trie; -pub use trie::*; diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs b/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs deleted file mode 100644 index d0895f56..00000000 --- a/crates/stateless-core/src/withdrawals/sparse_trie/provider.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/provider.rs), trimmed to the -//! serial sparse trie needed by withdrawal MPT witness verification. -//! -//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension -//! nodes into their child branches; an extension whose child branch is absent -//! from the witness (a legal, minimal exclusion proof — the child is never -//! needed to split the extension on insert) cannot be represented and is -//! silently dropped on reveal, breaking withdrawal-slot inserts that split -//! such extensions. The MegaETH witness format is frozen upstream, so the -//! proven v1.6.0 semantics are pinned here instead. -//! -//! Local changes: import paths only (crate-relative + vendored errors); -//! test modules and metrics are not carried over. Do not edit otherwise. - -//! Traits and default implementations related to retrieval of blinded trie nodes. - -use alloy_primitives::{B256, Bytes}; -use reth_trie_common::{Nibbles, TrieMask}; - -use super::SparseTrieError; - -/// Factory for instantiating trie node providers. -#[auto_impl::auto_impl(&)] -pub trait TrieNodeProviderFactory { - /// Type capable of fetching blinded account nodes. - type AccountNodeProvider: TrieNodeProvider; - /// Type capable of fetching blinded storage nodes. - type StorageNodeProvider: TrieNodeProvider; - - /// Returns blinded account node provider. - fn account_node_provider(&self) -> Self::AccountNodeProvider; - - /// Returns blinded storage node provider. - fn storage_node_provider(&self, account: B256) -> Self::StorageNodeProvider; -} - -/// Revealed blinded trie node. -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct RevealedNode { - /// Raw trie node. - pub node: Bytes, - /// Branch node tree mask, if any. - pub tree_mask: Option, - /// Branch node hash mask, if any. - pub hash_mask: Option, -} - -/// Trie node provider for retrieving trie nodes. -#[auto_impl::auto_impl(&)] -pub trait TrieNodeProvider { - /// Retrieve trie node by path. - fn trie_node(&self, path: &Nibbles) -> Result, SparseTrieError>; -} - -/// Default trie node provider factory that creates [`DefaultTrieNodeProviderFactory`]. -#[derive(PartialEq, Eq, Clone, Default, Debug)] -pub struct DefaultTrieNodeProviderFactory; - -impl TrieNodeProviderFactory for DefaultTrieNodeProviderFactory { - type AccountNodeProvider = DefaultTrieNodeProvider; - type StorageNodeProvider = DefaultTrieNodeProvider; - - fn account_node_provider(&self) -> Self::AccountNodeProvider { - DefaultTrieNodeProvider - } - - fn storage_node_provider(&self, _account: B256) -> Self::StorageNodeProvider { - DefaultTrieNodeProvider - } -} - -/// Default trie node provider that always returns `Ok(None)`. -#[derive(PartialEq, Eq, Clone, Default, Debug)] -pub struct DefaultTrieNodeProvider; - -impl TrieNodeProvider for DefaultTrieNodeProvider { - fn trie_node(&self, _path: &Nibbles) -> Result, SparseTrieError> { - Ok(None) - } -} - -/// Right pad the path with 0s and return as [`B256`]. -#[inline] -pub fn pad_path_to_key(path: &Nibbles) -> B256 { - let mut padded = path.pack(); - padded.resize(32, 0); - B256::from_slice(&padded) -} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs b/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs deleted file mode 100644 index 1892c7d3..00000000 --- a/crates/stateless-core/src/withdrawals/sparse_trie/traits.rs +++ /dev/null @@ -1,324 +0,0 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/traits.rs), trimmed to the -//! serial sparse trie needed by withdrawal MPT witness verification. -//! -//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension -//! nodes into their child branches; an extension whose child branch is absent -//! from the witness (a legal, minimal exclusion proof — the child is never -//! needed to split the extension on insert) cannot be represented and is -//! silently dropped on reveal, breaking withdrawal-slot inserts that split -//! such extensions. The MegaETH witness format is frozen upstream, so the -//! proven v1.6.0 semantics are pinned here instead. -//! -//! Local changes: import paths only (crate-relative + vendored errors); -//! test modules and metrics are not carried over. Do not edit otherwise. - -//! Traits for sparse trie implementations. - -use alloc::{borrow::Cow, vec, vec::Vec}; -use core::fmt::Debug; - -use alloy_primitives::{ - B256, - map::{HashMap, HashSet}, -}; -use reth_trie_common::{BranchNodeCompact, Nibbles, TrieMask, TrieNode}; - -use super::{SparseTrieResult, provider::TrieNodeProvider}; - -/// Trait defining common operations for revealed sparse trie implementations. -/// -/// This trait abstracts over different sparse trie implementations (serial vs parallel) -/// while providing a unified interface for the core trie operations needed by the -/// [`crate::SparseTrie`] enum. -pub trait SparseTrieInterface: Sized + Debug + Send + Sync { - /// Configures the trie to have the given root node revealed. - /// - /// # Arguments - /// - /// * `root` - The root node to reveal - /// * `masks` - Trie masks for root branch node - /// * `retain_updates` - Whether to track updates - /// - /// # Returns - /// - /// Self if successful, or an error if revealing fails. - /// - /// # Panics - /// - /// May panic if the trie is not new/cleared, and has already revealed nodes. - fn with_root( - self, - root: TrieNode, - masks: TrieMasks, - retain_updates: bool, - ) -> SparseTrieResult; - - /// Configures the trie to retain information about updates. - /// - /// If `retain_updates` is true, the trie will record branch node updates - /// and deletions. This information can be used to efficiently update - /// an external database. - /// - /// # Arguments - /// - /// * `retain_updates` - Whether to track updates - /// - /// # Returns - /// - /// Self for method chaining. - fn with_updates(self, retain_updates: bool) -> Self; - - /// Reserves capacity for additional trie nodes. - /// - /// # Arguments - /// - /// * `additional` - The number of additional trie nodes to reserve capacity for. - fn reserve_nodes(&mut self, _additional: usize) {} - - /// The single-node version of `reveal_nodes`. - /// - /// # Returns - /// - /// `Ok(())` if successful, or an error if the node was not revealed. - fn reveal_node( - &mut self, - path: Nibbles, - node: TrieNode, - masks: TrieMasks, - ) -> SparseTrieResult<()> { - self.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]) - } - - /// Reveals one or more trie nodes if they have not been revealed before. - /// - /// This function decodes trie nodes and inserts them into the trie structure. It handles - /// different node types (leaf, extension, branch) by appropriately adding them to the trie and - /// recursively revealing their children. - /// - /// # Arguments - /// - /// * `nodes` - The nodes to be revealed, each having a path and optional set of branch node - /// masks. The nodes will be unsorted. - /// - /// # Returns - /// - /// `Ok(())` if successful, or an error if any of the nodes was not revealed. - fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()>; - - /// Updates the value of a leaf node at the specified path. - /// - /// If the leaf doesn't exist, it will be created. - /// If it does exist, its value will be updated. - /// - /// # Arguments - /// - /// * `full_path` - The full path to the leaf - /// * `value` - The new value for the leaf - /// * `provider` - The trie provider for resolving missing nodes - /// - /// # Returns - /// - /// `Ok(())` if successful, or an error if the update failed. - fn update_leaf( - &mut self, - full_path: Nibbles, - value: Vec, - provider: P, - ) -> SparseTrieResult<()>; - - /// Removes a leaf node at the specified path. - /// - /// This will also handle collapsing the trie structure as needed - /// (e.g., removing branch nodes that become unnecessary). - /// - /// # Arguments - /// - /// * `full_path` - The full path to the leaf to remove - /// * `provider` - The trie node provider for resolving missing nodes - /// - /// # Returns - /// - /// `Ok(())` if successful, or an error if the removal failed. - fn remove_leaf( - &mut self, - full_path: &Nibbles, - provider: P, - ) -> SparseTrieResult<()>; - - /// Calculates and returns the root hash of the trie. - /// - /// This processes any dirty nodes by updating their RLP encodings - /// and returns the root hash. - /// - /// # Returns - /// - /// The root hash of the trie. - fn root(&mut self) -> B256; - - /// Recalculates and updates the RLP hashes of subtries deeper than a certain level. The level - /// is defined in the implementation. - /// - /// The root node is considered to be at level 0. This method is useful for optimizing - /// hash recalculations after localized changes to the trie structure. - fn update_subtrie_hashes(&mut self); - - /// Retrieves a reference to the leaf value at the specified path. - /// - /// # Arguments - /// - /// * `full_path` - The full path to the leaf value - /// - /// # Returns - /// - /// A reference to the leaf value stored at the given full path, if it is revealed. - /// - /// Note: a value can exist in the full trie and this function still returns `None` - /// because the value has not been revealed. - /// - /// Hence a `None` indicates two possibilities: - /// - The value does not exists in the trie, so it cannot be revealed - /// - The value has not yet been revealed. In order to determine which is true, one would need - /// an exclusion proof. - fn get_leaf_value(&self, full_path: &Nibbles) -> Option<&Vec>; - - /// Attempts to find a leaf node at the specified path. - /// - /// This method traverses the trie from the root down to the given path, checking - /// if a leaf exists at that path. It can be used to verify the existence of a leaf - /// or to generate an exclusion proof (proof that a leaf does not exist). - /// - /// # Parameters - /// - /// - `full_path`: The path to search for. - /// - `expected_value`: Optional expected value. If provided, will verify the leaf value - /// matches. - /// - /// # Returns - /// - /// - `Ok(LeafLookup::Exists)` if the leaf exists with the expected value. - /// - `Ok(LeafLookup::NonExistent)` if the leaf definitely does not exist (exclusion proof). - /// - `Err(LeafLookupError)` if the search encountered a blinded node or found a different - /// value. - fn find_leaf( - &self, - full_path: &Nibbles, - expected_value: Option<&Vec>, - ) -> Result; - - /// Returns a reference to the current sparse trie updates. - /// - /// If no updates have been made/recorded, returns an empty update set. - fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates>; - - /// Consumes and returns the currently accumulated trie updates. - /// - /// This is useful when you want to apply the updates to an external database - /// and then start tracking a new set of updates. - /// - /// # Returns - /// - /// The accumulated updates, or an empty set if updates weren't being tracked. - fn take_updates(&mut self) -> SparseTrieUpdates; - - /// Removes all nodes and values from the trie, resetting it to a blank state - /// with only an empty root node. This is used when a storage root is deleted. - /// - /// This should not be used when intending to reuse the trie for a fresh account/storage root; - /// use `clear` for that. - /// - /// Note: All previously tracked changes to the trie are also removed. - fn wipe(&mut self); - - /// This clears all data structures in the sparse trie, keeping the backing data structures - /// allocated. A [`crate::SparseNode::Empty`] is inserted at the root. - /// - /// This is useful for reusing the trie without needing to reallocate memory. - fn clear(&mut self); -} - -/// Struct for passing around branch node mask information. -/// -/// Branch nodes can have up to 16 children (one for each nibble). -/// The masks represent which children are stored in different ways: -/// - `hash_mask`: Indicates which children are stored as hashes in the database -/// - `tree_mask`: Indicates which children are complete subtrees stored in the database -/// -/// These masks are essential for efficient trie traversal and serialization, as they -/// determine how nodes should be encoded and stored on disk. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct TrieMasks { - /// Branch node hash mask, if any. - /// - /// When a bit is set, the corresponding child node's hash is stored in the trie. - /// - /// This mask enables selective hashing of child nodes. - pub hash_mask: Option, - /// Branch node tree mask, if any. - /// - /// When a bit is set, the corresponding child subtree is stored in the database. - pub tree_mask: Option, -} - -impl TrieMasks { - /// Helper function, returns both fields `hash_mask` and `tree_mask` as [`None`] - pub const fn none() -> Self { - Self { hash_mask: None, tree_mask: None } - } -} - -/// Tracks modifications to the sparse trie structure. -/// -/// Maintains references to both modified and pruned/removed branches, enabling -/// one to make batch updates to a persistent database. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct SparseTrieUpdates { - /// Collection of updated intermediate nodes indexed by full path. - pub updated_nodes: HashMap, - /// Collection of removed intermediate nodes indexed by full path. - pub removed_nodes: HashSet, - /// Flag indicating whether the trie was wiped. - pub wiped: bool, -} - -/// Error type for a leaf lookup operation -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LeafLookupError { - /// The path leads to a blinded node, cannot determine if leaf exists. - /// This means the witness is not complete. - BlindedNode { - /// Path to the blinded node. - path: Nibbles, - /// Hash of the blinded node. - hash: B256, - }, - /// The path leads to a leaf with a different value than expected. - /// This means the witness is malformed. - ValueMismatch { - /// Path to the leaf. - path: Nibbles, - /// Expected value. - expected: Option>, - /// Actual value found. - actual: Vec, - }, -} - -/// Success value for a leaf lookup operation -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LeafLookup { - /// Leaf exists with expected value. - Exists, - /// Leaf does not exist (exclusion proof found). - NonExistent, -} - -/// Carries all information needed by a sparse trie to reveal a particular node. -#[derive(Debug, PartialEq, Eq)] -pub struct RevealedSparseNode { - /// Path of the node. - pub path: Nibbles, - /// The node itself. - pub node: TrieNode, - /// Tree and hash masks for the node, if known. - pub masks: TrieMasks, -} diff --git a/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs b/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs deleted file mode 100644 index 3cfc5ae8..00000000 --- a/crates/stateless-core/src/withdrawals/sparse_trie/trie.rs +++ /dev/null @@ -1,1920 +0,0 @@ -//! Vendored from reth v1.6.0 (commit d8451e54e, crates/trie/sparse/src/trie.rs (non-test portion)), -//! trimmed to the serial sparse trie needed by withdrawal MPT witness verification. -//! -//! reth v2.3.0's sparse tries moved to a V2 node model that merges extension -//! nodes into their child branches; an extension whose child branch is absent -//! from the witness (a legal, minimal exclusion proof — the child is never -//! needed to split the extension on insert) cannot be represented and is -//! silently dropped on reveal, breaking withdrawal-slot inserts that split -//! such extensions. The MegaETH witness format is frozen upstream, so the -//! proven v1.6.0 semantics are pinned here instead. -//! -//! Local changes: import paths only (crate-relative + vendored errors); -//! test modules and metrics are not carried over. Do not edit otherwise. - -use alloc::{ - borrow::Cow, - boxed::Box, - fmt, - string::{String, ToString}, - vec, - vec::Vec, -}; - -use alloy_primitives::{ - B256, hex, keccak256, - map::{Entry, HashMap, HashSet}, -}; -use alloy_rlp::Decodable; -use reth_trie_common::{ - BranchNodeCompact, BranchNodeRef, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, ExtensionNodeRef, - LeafNodeRef, Nibbles, RlpNode, TrieMask, TrieNode, - prefix_set::{PrefixSet, PrefixSetMut}, -}; -use smallvec::SmallVec; -use tracing::trace; - -use super::{ - LeafLookup, LeafLookupError, RevealedSparseNode, SparseTrieErrorKind, SparseTrieInterface, - SparseTrieResult, SparseTrieUpdates, TrieMasks, - provider::{RevealedNode, TrieNodeProvider}, -}; - -/// The level below which the sparse trie hashes are calculated in -/// [`SerialSparseTrie::update_subtrie_hashes`]. -const SPARSE_TRIE_SUBTRIE_HASHES_LEVEL: usize = 2; - -/// A sparse trie that is either in a "blind" state (no nodes are revealed, root node hash is -/// unknown) or in a "revealed" state (root node has been revealed and the trie can be updated). -/// -/// In blind mode the trie does not contain any decoded node data, which saves memory but -/// prevents direct access to node contents. The revealed mode stores decoded nodes along -/// with additional information such as values, allowing direct manipulation. -/// -/// The sparse trie design is optimised for: -/// 1. Memory efficiency - only revealed nodes are loaded into memory -/// 2. Update tracking - changes to the trie structure can be tracked and selectively persisted -/// 3. Incremental operations - nodes can be revealed as needed without loading the entire trie. -/// This is what gives rise to the notion of a "sparse" trie. -#[derive(PartialEq, Eq, Debug)] -pub enum SparseTrie { - /// The trie is blind -- no nodes have been revealed - /// - /// This is the default state. In this state, the trie cannot be directly queried or modified - /// until nodes are revealed. - /// - /// In this state the `SparseTrie` can optionally carry with it a cleared `SerialSparseTrie`. - /// This allows for reusing the trie's allocations between payload executions. - Blind(Option>), - /// Some nodes in the Trie have been revealed. - /// - /// In this state, the trie can be queried and modified for the parts - /// that have been revealed. Other parts remain blind and require revealing - /// before they can be accessed. - Revealed(Box), -} - -impl Default for SparseTrie { - fn default() -> Self { - Self::Blind(None) - } -} - -impl SparseTrie { - /// Creates a new revealed but empty sparse trie with `SparseNode::Empty` as root node. - /// - /// # Examples - /// - /// ```ignore - /// use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; - /// - /// let trie = SparseTrie::::revealed_empty(); - /// assert!(!trie.is_blind()); - /// ``` - pub fn revealed_empty() -> Self { - Self::Revealed(Box::default()) - } - - /// Reveals the root node, converting a blind trie into a revealed one. - /// - /// If the trie is blinded, its root node is replaced with `root`. - /// - /// The `masks` are used to determine how the node's children are stored. - /// The `retain_updates` flag controls whether changes to the trie structure - /// should be tracked. - /// - /// # Returns - /// - /// A mutable reference to the underlying [`SparseTrieInterface`]. - pub fn reveal_root( - &mut self, - root: TrieNode, - masks: TrieMasks, - retain_updates: bool, - ) -> SparseTrieResult<&mut T> { - // if `Blind`, we initialize the revealed trie with the given root node, using a - // pre-allocated trie if available. - if self.is_blind() { - let mut revealed_trie = if let Self::Blind(Some(cleared_trie)) = core::mem::take(self) { - cleared_trie - } else { - Box::default() - }; - - *revealed_trie = revealed_trie.with_root(root, masks, retain_updates)?; - *self = Self::Revealed(revealed_trie); - } - - Ok(self.as_revealed_mut().unwrap()) - } -} - -impl SparseTrie { - /// Creates a new blind sparse trie. - /// - /// # Examples - /// - /// ```ignore - /// use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; - /// - /// let trie = SparseTrie::::blind(); - /// assert!(trie.is_blind()); - /// let trie = SparseTrie::::default(); - /// assert!(trie.is_blind()); - /// ``` - pub const fn blind() -> Self { - Self::Blind(None) - } - - /// Returns `true` if the sparse trie has no revealed nodes. - pub const fn is_blind(&self) -> bool { - matches!(self, Self::Blind(_)) - } - - /// Returns `true` if the sparse trie is revealed. - pub const fn is_revealed(&self) -> bool { - matches!(self, Self::Revealed(_)) - } - - /// Returns an immutable reference to the underlying revealed sparse trie. - /// - /// Returns `None` if the trie is blinded. - pub const fn as_revealed_ref(&self) -> Option<&T> { - if let Self::Revealed(revealed) = self { Some(revealed) } else { None } - } - - /// Returns a mutable reference to the underlying revealed sparse trie. - /// - /// Returns `None` if the trie is blinded. - pub fn as_revealed_mut(&mut self) -> Option<&mut T> { - if let Self::Revealed(revealed) = self { Some(revealed) } else { None } - } - - /// Wipes the trie by removing all nodes and values, - /// and resetting the trie to only contain an empty root node. - /// - /// Note: This method will error if the trie is blinded. - pub fn wipe(&mut self) -> SparseTrieResult<()> { - let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; - revealed.wipe(); - Ok(()) - } - - /// Calculates the root hash of the trie. - /// - /// This will update any remaining dirty nodes before computing the root hash. - /// "dirty" nodes are nodes that need their hashes to be recomputed because one or more of their - /// children's hashes have changed. - /// - /// # Returns - /// - /// - `Some(B256)` with the calculated root hash if the trie is revealed. - /// - `None` if the trie is still blind. - pub fn root(&mut self) -> Option { - Some(self.as_revealed_mut()?.root()) - } - - /// Returns the root hash along with any accumulated update information. - /// - /// This is useful for when you need both the root hash and information about - /// what nodes were modified, which can be used to efficiently update - /// an external database. - /// - /// # Returns - /// - /// An `Option` tuple consisting of: - /// - The trie root hash (`B256`). - /// - A [`SparseTrieUpdates`] structure containing information about updated nodes. - /// - `None` if the trie is still blind. - pub fn root_with_updates(&mut self) -> Option<(B256, SparseTrieUpdates)> { - let revealed = self.as_revealed_mut()?; - Some((revealed.root(), revealed.take_updates())) - } - - /// Returns a [`SparseTrie::Blind`] based on this one. If this instance was revealed, or was - /// itself a `Blind` with a pre-allocated [`SparseTrieInterface`], this will return - /// a `Blind` carrying a cleared pre-allocated [`SparseTrieInterface`]. - pub fn clear(self) -> Self { - match self { - Self::Blind(_) => self, - Self::Revealed(mut trie) => { - trie.clear(); - Self::Blind(Some(trie)) - } - } - } - - /// Updates (or inserts) a leaf at the given key path with the specified RLP-encoded value. - /// - /// # Errors - /// - /// Returns an error if the trie is still blind, or if the update fails. - pub fn update_leaf( - &mut self, - path: Nibbles, - value: Vec, - provider: impl TrieNodeProvider, - ) -> SparseTrieResult<()> { - let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; - revealed.update_leaf(path, value, provider)?; - Ok(()) - } - - /// Removes a leaf node at the specified key path. - /// - /// # Errors - /// - /// Returns an error if the trie is still blind, or if the leaf cannot be removed - pub fn remove_leaf( - &mut self, - path: &Nibbles, - provider: impl TrieNodeProvider, - ) -> SparseTrieResult<()> { - let revealed = self.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?; - revealed.remove_leaf(path, provider)?; - Ok(()) - } -} - -/// The representation of revealed sparse trie. -/// -/// The revealed sparse trie contains the actual trie structure with nodes, values, and -/// tracking for changes. It supports operations like inserting, updating, and removing -/// nodes. -/// -/// -/// ## Invariants -/// -/// - The root node is always present in `nodes` collection. -/// - Each leaf entry in `nodes` collection must have a corresponding entry in `values` collection. -/// The opposite is also true. -/// - All keys in `values` collection are full leaf paths. -#[derive(Clone, PartialEq, Eq)] -pub struct SerialSparseTrie { - /// Map from a path (nibbles) to its corresponding sparse trie node. - /// This contains all of the revealed nodes in trie. - nodes: HashMap, - /// When a branch is set, the corresponding child subtree is stored in the database. - branch_node_tree_masks: HashMap, - /// When a bit is set, the corresponding child is stored as a hash in the database. - branch_node_hash_masks: HashMap, - /// Map from leaf key paths to their values. - /// All values are stored here instead of directly in leaf nodes. - values: HashMap>, - /// Set of prefixes (key paths) that have been marked as updated. - /// This is used to track which parts of the trie need to be recalculated. - prefix_set: PrefixSetMut, - /// Optional tracking of trie updates for later use. - updates: Option, - /// Reusable buffer for RLP encoding of nodes. - rlp_buf: Vec, -} - -impl fmt::Debug for SerialSparseTrie { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SerialSparseTrie") - .field("nodes", &self.nodes) - .field("branch_tree_masks", &self.branch_node_tree_masks) - .field("branch_hash_masks", &self.branch_node_hash_masks) - .field("values", &self.values) - .field("prefix_set", &self.prefix_set) - .field("updates", &self.updates) - .field("rlp_buf", &hex::encode(&self.rlp_buf)) - .finish_non_exhaustive() - } -} - -/// Turns a [`Nibbles`] into a [`String`] by concatenating each nibbles' hex character. -fn encode_nibbles(nibbles: &Nibbles) -> String { - let encoded = hex::encode(nibbles.pack()); - encoded[..nibbles.len()].to_string() -} - -impl fmt::Display for SerialSparseTrie { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // This prints the trie in preorder traversal, using a stack - let mut stack = Vec::new(); - let mut visited = HashSet::new(); - - // 4 spaces as indent per level - const INDENT: &str = " "; - - // Track both path and depth - stack.push((Nibbles::default(), self.nodes_ref().get(&Nibbles::default()).unwrap(), 0)); - - while let Some((path, node, depth)) = stack.pop() { - if !visited.insert(path) { - continue; - } - - // Add indentation if alternate flag (#) is set - if f.alternate() { - write!(f, "{}", INDENT.repeat(depth))?; - } - - let packed_path = if depth == 0 { String::from("Root") } else { encode_nibbles(&path) }; - - match node { - SparseNode::Empty | SparseNode::Hash(_) => { - writeln!(f, "{packed_path} -> {node:?}")?; - } - SparseNode::Leaf { key, .. } => { - // we want to append the key to the path - let mut full_path = path; - full_path.extend(key); - let packed_path = encode_nibbles(&full_path); - - writeln!(f, "{packed_path} -> {node:?}")?; - } - SparseNode::Extension { key, .. } => { - writeln!(f, "{packed_path} -> {node:?}")?; - - // push the child node onto the stack with increased depth - let mut child_path = path; - child_path.extend(key); - if let Some(child_node) = self.nodes_ref().get(&child_path) { - stack.push((child_path, child_node, depth + 1)); - } - } - SparseNode::Branch { state_mask, .. } => { - writeln!(f, "{packed_path} -> {node:?}")?; - - for i in CHILD_INDEX_RANGE.rev() { - if state_mask.is_bit_set(i) { - let mut child_path = path; - child_path.push_unchecked(i); - if let Some(child_node) = self.nodes_ref().get(&child_path) { - stack.push((child_path, child_node, depth + 1)); - } - } - } - } - } - } - - Ok(()) - } -} - -impl Default for SerialSparseTrie { - fn default() -> Self { - Self { - nodes: HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]), - branch_node_tree_masks: HashMap::default(), - branch_node_hash_masks: HashMap::default(), - values: HashMap::default(), - prefix_set: PrefixSetMut::default(), - updates: None, - rlp_buf: Vec::new(), - } - } -} - -impl SparseTrieInterface for SerialSparseTrie { - fn with_root( - mut self, - root: TrieNode, - masks: TrieMasks, - retain_updates: bool, - ) -> SparseTrieResult { - self = self.with_updates(retain_updates); - - // A fresh/cleared `SerialSparseTrie` has a `SparseNode::Empty` at its root. Delete that - // so we can reveal the new root node. - let path = Nibbles::default(); - let _removed_root = self.nodes.remove(&path).expect("root node should exist"); - debug_assert_eq!(_removed_root, SparseNode::Empty); - - self.reveal_node(path, root, masks)?; - Ok(self) - } - - fn with_updates(mut self, retain_updates: bool) -> Self { - if retain_updates { - self.updates = Some(SparseTrieUpdates::default()); - } - self - } - - fn reserve_nodes(&mut self, additional: usize) { - self.nodes.reserve(additional); - } - fn reveal_node( - &mut self, - path: Nibbles, - node: TrieNode, - masks: TrieMasks, - ) -> SparseTrieResult<()> { - // If the node is already revealed and it's not a hash node, do nothing. - if self.nodes.get(&path).is_some_and(|node| !node.is_hash()) { - return Ok(()) - } - - if let Some(tree_mask) = masks.tree_mask { - self.branch_node_tree_masks.insert(path, tree_mask); - } - if let Some(hash_mask) = masks.hash_mask { - self.branch_node_hash_masks.insert(path, hash_mask); - } - - match node { - TrieNode::EmptyRoot => { - // For an empty root, ensure that we are at the root path. - debug_assert!(path.is_empty()); - self.nodes.insert(path, SparseNode::Empty); - } - TrieNode::Branch(branch) => { - // For a branch node, iterate over all potential children - let mut stack_ptr = branch.as_ref().first_child_index(); - for idx in CHILD_INDEX_RANGE { - if branch.state_mask.is_bit_set(idx) { - let mut child_path = path; - child_path.push_unchecked(idx); - // Reveal each child node or hash it has - self.reveal_node_or_hash(child_path, &branch.stack[stack_ptr])?; - stack_ptr += 1; - } - } - // Update the branch node entry in the nodes map, handling cases where a blinded - // node is now replaced with a revealed node. - match self.nodes.entry(path) { - Entry::Occupied(mut entry) => match entry.get() { - // Replace a hash node with a fully revealed branch node. - SparseNode::Hash(hash) => { - entry.insert(SparseNode::Branch { - state_mask: branch.state_mask, - // Memoize the hash of a previously blinded node in a new branch - // node. - hash: Some(*hash), - store_in_db_trie: Some( - masks.hash_mask.is_some_and(|mask| !mask.is_empty()) || - masks.tree_mask.is_some_and(|mask| !mask.is_empty()), - ), - }); - } - // Branch node already exists, or an extension node was placed where a - // branch node was before. - SparseNode::Branch { .. } | SparseNode::Extension { .. } => {} - // All other node types can't be handled. - node @ (SparseNode::Empty | SparseNode::Leaf { .. }) => { - return Err(SparseTrieErrorKind::Reveal { - path: *entry.key(), - node: Box::new(node.clone()), - } - .into()) - } - }, - Entry::Vacant(entry) => { - entry.insert(SparseNode::new_branch(branch.state_mask)); - } - } - } - TrieNode::Extension(ext) => match self.nodes.entry(path) { - Entry::Occupied(mut entry) => match entry.get() { - // Replace a hash node with a revealed extension node. - SparseNode::Hash(hash) => { - let mut child_path = *entry.key(); - child_path.extend(&ext.key); - entry.insert(SparseNode::Extension { - key: ext.key, - // Memoize the hash of a previously blinded node in a new extension - // node. - hash: Some(*hash), - store_in_db_trie: None, - }); - self.reveal_node_or_hash(child_path, &ext.child)?; - } - // Extension node already exists, or an extension node was placed where a branch - // node was before. - SparseNode::Extension { .. } | SparseNode::Branch { .. } => {} - // All other node types can't be handled. - node @ (SparseNode::Empty | SparseNode::Leaf { .. }) => { - return Err(SparseTrieErrorKind::Reveal { - path: *entry.key(), - node: Box::new(node.clone()), - } - .into()) - } - }, - Entry::Vacant(entry) => { - let mut child_path = *entry.key(); - child_path.extend(&ext.key); - entry.insert(SparseNode::new_ext(ext.key)); - self.reveal_node_or_hash(child_path, &ext.child)?; - } - }, - TrieNode::Leaf(leaf) => match self.nodes.entry(path) { - Entry::Occupied(mut entry) => match entry.get() { - // Replace a hash node with a revealed leaf node and store leaf node value. - SparseNode::Hash(hash) => { - let mut full = *entry.key(); - full.extend(&leaf.key); - self.values.insert(full, leaf.value.clone()); - entry.insert(SparseNode::Leaf { - key: leaf.key, - // Memoize the hash of a previously blinded node in a new leaf - // node. - hash: Some(*hash), - }); - } - // Left node already exists. - SparseNode::Leaf { .. } => {} - // All other node types can't be handled. - node @ (SparseNode::Empty | - SparseNode::Extension { .. } | - SparseNode::Branch { .. }) => { - return Err(SparseTrieErrorKind::Reveal { - path: *entry.key(), - node: Box::new(node.clone()), - } - .into()) - } - }, - Entry::Vacant(entry) => { - let mut full = *entry.key(); - full.extend(&leaf.key); - entry.insert(SparseNode::new_leaf(leaf.key)); - self.values.insert(full, leaf.value.clone()); - } - }, - } - - Ok(()) - } - - fn reveal_nodes(&mut self, mut nodes: Vec) -> SparseTrieResult<()> { - nodes.sort_unstable_by_key(|node| node.path); - for node in nodes { - self.reveal_node(node.path, node.node, node.masks)?; - } - Ok(()) - } - - fn update_leaf( - &mut self, - full_path: Nibbles, - value: Vec, - provider: P, - ) -> SparseTrieResult<()> { - self.prefix_set.insert(full_path); - let existing = self.values.insert(full_path, value); - if existing.is_some() { - // trie structure unchanged, return immediately - return Ok(()) - } - - let mut current = Nibbles::default(); - while let Some(node) = self.nodes.get_mut(¤t) { - match node { - SparseNode::Empty => { - *node = SparseNode::new_leaf(full_path); - break - } - &mut SparseNode::Hash(hash) => { - return Err(SparseTrieErrorKind::BlindedNode { path: current, hash }.into()) - } - SparseNode::Leaf { key: current_key, .. } => { - current.extend(current_key); - - // this leaf is being updated - if current == full_path { - unreachable!("we already checked leaf presence in the beginning"); - } - - // find the common prefix - let common = current.common_prefix_length(&full_path); - - // update existing node - let new_ext_key = current.slice(current.len() - current_key.len()..common); - *node = SparseNode::new_ext(new_ext_key); - - // create a branch node and corresponding leaves - self.nodes.reserve(3); - self.nodes.insert( - current.slice(..common), - SparseNode::new_split_branch( - current.get_unchecked(common), - full_path.get_unchecked(common), - ), - ); - self.nodes.insert( - full_path.slice(..=common), - SparseNode::new_leaf(full_path.slice(common + 1..)), - ); - self.nodes.insert( - current.slice(..=common), - SparseNode::new_leaf(current.slice(common + 1..)), - ); - - break; - } - SparseNode::Extension { key, .. } => { - current.extend(key); - - if !full_path.starts_with(¤t) { - // find the common prefix - let common = current.common_prefix_length(&full_path); - *key = current.slice(current.len() - key.len()..common); - - // If branch node updates retention is enabled, we need to query the - // extension node child to later set the hash mask for a parent branch node - // correctly. - if self.updates.is_some() { - // Check if the extension node child is a hash that needs to be revealed - if self.nodes.get(¤t).unwrap().is_hash() { - if let Some(RevealedNode { node, tree_mask, hash_mask }) = - provider.trie_node(¤t)? - { - let decoded = TrieNode::decode(&mut &node[..])?; - trace!( - target: "trie::sparse", - ?current, - ?decoded, - ?tree_mask, - ?hash_mask, - "Revealing extension node child", - ); - self.reveal_node( - current, - decoded, - TrieMasks { hash_mask, tree_mask }, - )?; - } - } - } - - // create state mask for new branch node - // NOTE: this might overwrite the current extension node - self.nodes.reserve(3); - let branch = SparseNode::new_split_branch( - current.get_unchecked(common), - full_path.get_unchecked(common), - ); - self.nodes.insert(current.slice(..common), branch); - - // create new leaf - let new_leaf = SparseNode::new_leaf(full_path.slice(common + 1..)); - self.nodes.insert(full_path.slice(..=common), new_leaf); - - // recreate extension to previous child if needed - let key = current.slice(common + 1..); - if !key.is_empty() { - self.nodes.insert(current.slice(..=common), SparseNode::new_ext(key)); - } - - break; - } - } - SparseNode::Branch { state_mask, .. } => { - let nibble = full_path.get_unchecked(current.len()); - current.push_unchecked(nibble); - if !state_mask.is_bit_set(nibble) { - state_mask.set_bit(nibble); - let new_leaf = SparseNode::new_leaf(full_path.slice(current.len()..)); - self.nodes.insert(current, new_leaf); - break; - } - } - }; - } - - Ok(()) - } - - fn remove_leaf( - &mut self, - full_path: &Nibbles, - provider: P, - ) -> SparseTrieResult<()> { - if self.values.remove(full_path).is_none() { - if let Some(&SparseNode::Hash(hash)) = self.nodes.get(full_path) { - // Leaf is present in the trie, but it's blinded. - return Err(SparseTrieErrorKind::BlindedNode { path: *full_path, hash }.into()) - } - - trace!(target: "trie::sparse", ?full_path, "Leaf node is not present in the trie"); - // Leaf is not present in the trie. - return Ok(()) - } - self.prefix_set.insert(*full_path); - - // If the path wasn't present in `values`, we still need to walk the trie and ensure that - // there is no node at the path. When a leaf node is a blinded `Hash`, it will have an entry - // in `nodes`, but not in the `values`. - - let mut removed_nodes = self.take_nodes_for_path(full_path)?; - // Pop the first node from the stack which is the leaf node we want to remove. - let mut child = removed_nodes.pop().expect("leaf exists"); - #[cfg(debug_assertions)] - { - let mut child_path = child.path; - let SparseNode::Leaf { key, .. } = &child.node else { panic!("expected leaf node") }; - child_path.extend(key); - assert_eq!(&child_path, full_path); - } - - // If we don't have any other removed nodes, insert an empty node at the root. - if removed_nodes.is_empty() { - debug_assert!(self.nodes.is_empty()); - self.nodes.insert(Nibbles::default(), SparseNode::Empty); - - return Ok(()) - } - - // Walk the stack of removed nodes from the back and re-insert them back into the trie, - // adjusting the node type as needed. - while let Some(removed_node) = removed_nodes.pop() { - let removed_path = removed_node.path; - - let new_node = match &removed_node.node { - SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), - &SparseNode::Hash(hash) => { - return Err(SparseTrieErrorKind::BlindedNode { path: removed_path, hash }.into()) - } - SparseNode::Leaf { .. } => { - unreachable!("we already popped the leaf node") - } - SparseNode::Extension { key, .. } => { - // If the node is an extension node, we need to look at its child to see if we - // need to merge them. - match &child.node { - SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), - &SparseNode::Hash(hash) => { - return Err( - SparseTrieErrorKind::BlindedNode { path: child.path, hash }.into() - ) - } - // For a leaf node, we collapse the extension node into a leaf node, - // extending the key. While it's impossible to encounter an extension node - // followed by a leaf node in a complete trie, it's possible here because we - // could have downgraded the extension node's child into a leaf node from - // another node type. - SparseNode::Leaf { key: leaf_key, .. } => { - self.nodes.remove(&child.path); - - let mut new_key = *key; - new_key.extend(leaf_key); - SparseNode::new_leaf(new_key) - } - // For an extension node, we collapse them into one extension node, - // extending the key - SparseNode::Extension { key: extension_key, .. } => { - self.nodes.remove(&child.path); - - let mut new_key = *key; - new_key.extend(extension_key); - SparseNode::new_ext(new_key) - } - // For a branch node, we just leave the extension node as-is. - SparseNode::Branch { .. } => removed_node.node, - } - } - &SparseNode::Branch { mut state_mask, hash: _, store_in_db_trie: _ } => { - // If the node is a branch node, we need to check the number of children left - // after deleting the child at the given nibble. - - if let Some(removed_nibble) = removed_node.unset_branch_nibble { - state_mask.unset_bit(removed_nibble); - } - - // If only one child is left set in the branch node, we need to collapse it. - if state_mask.count_bits() == 1 { - let child_nibble = - state_mask.first_set_bit_index().expect("state mask is not empty"); - - // Get full path of the only child node left. - let mut child_path = removed_path; - child_path.push_unchecked(child_nibble); - - trace!(target: "trie::sparse", ?removed_path, ?child_path, "Branch node has only one child"); - - if self.nodes.get(&child_path).unwrap().is_hash() { - trace!(target: "trie::sparse", ?child_path, "Retrieving remaining blinded branch child"); - if let Some(RevealedNode { node, tree_mask, hash_mask }) = - provider.trie_node(&child_path)? - { - let decoded = TrieNode::decode(&mut &node[..])?; - trace!( - target: "trie::sparse", - ?child_path, - ?decoded, - ?tree_mask, - ?hash_mask, - "Revealing remaining blinded branch child" - ); - self.reveal_node( - child_path, - decoded, - TrieMasks { hash_mask, tree_mask }, - )?; - } - } - - // Get the only child node. - let child = self.nodes.get(&child_path).unwrap(); - - let mut delete_child = false; - let new_node = match child { - SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), - &SparseNode::Hash(hash) => { - return Err(SparseTrieErrorKind::BlindedNode { - path: child_path, - hash, - } - .into()) - } - // If the only child is a leaf node, we downgrade the branch node into a - // leaf node, prepending the nibble to the key, and delete the old - // child. - SparseNode::Leaf { key, .. } => { - delete_child = true; - - let mut new_key = Nibbles::from_nibbles_unchecked([child_nibble]); - new_key.extend(key); - SparseNode::new_leaf(new_key) - } - // If the only child node is an extension node, we downgrade the branch - // node into an even longer extension node, prepending the nibble to the - // key, and delete the old child. - SparseNode::Extension { key, .. } => { - delete_child = true; - - let mut new_key = Nibbles::from_nibbles_unchecked([child_nibble]); - new_key.extend(key); - SparseNode::new_ext(new_key) - } - // If the only child is a branch node, we downgrade the current branch - // node into a one-nibble extension node. - SparseNode::Branch { .. } => { - SparseNode::new_ext(Nibbles::from_nibbles_unchecked([child_nibble])) - } - }; - - if delete_child { - self.nodes.remove(&child_path); - } - - if let Some(updates) = self.updates.as_mut() { - updates.updated_nodes.remove(&removed_path); - updates.removed_nodes.insert(removed_path); - } - - new_node - } - // If more than one child is left set in the branch, we just re-insert it as-is. - else { - SparseNode::new_branch(state_mask) - } - } - }; - - child = RemovedSparseNode { - path: removed_path, - node: new_node.clone(), - unset_branch_nibble: None, - }; - trace!(target: "trie::sparse", ?removed_path, ?new_node, "Re-inserting the node"); - self.nodes.insert(removed_path, new_node); - } - - Ok(()) - } - - fn root(&mut self) -> B256 { - // Take the current prefix set - let mut prefix_set = core::mem::take(&mut self.prefix_set).freeze(); - let rlp_node = self.rlp_node_allocate(&mut prefix_set); - if let Some(root_hash) = rlp_node.as_hash() { root_hash } else { keccak256(rlp_node) } - } - - fn update_subtrie_hashes(&mut self) { - self.update_rlp_node_level(SPARSE_TRIE_SUBTRIE_HASHES_LEVEL); - } - - fn get_leaf_value(&self, full_path: &Nibbles) -> Option<&Vec> { - self.values.get(full_path) - } - - fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates> { - self.updates.as_ref().map_or(Cow::Owned(SparseTrieUpdates::default()), Cow::Borrowed) - } - - fn take_updates(&mut self) -> SparseTrieUpdates { - self.updates.take().unwrap_or_default() - } - - fn wipe(&mut self) { - self.nodes = HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]); - self.values = HashMap::default(); - self.prefix_set = PrefixSetMut::all(); - self.updates = self.updates.is_some().then(SparseTrieUpdates::wiped); - } - - fn clear(&mut self) { - self.nodes.clear(); - self.nodes.insert(Nibbles::default(), SparseNode::Empty); - - self.branch_node_tree_masks.clear(); - self.branch_node_hash_masks.clear(); - self.values.clear(); - self.prefix_set.clear(); - self.updates = None; - self.rlp_buf.clear(); - } - - fn find_leaf( - &self, - full_path: &Nibbles, - expected_value: Option<&Vec>, - ) -> Result { - // Helper function to check if a value matches the expected value - fn check_value_match( - actual_value: &Vec, - expected_value: Option<&Vec>, - path: &Nibbles, - ) -> Result<(), LeafLookupError> { - if let Some(expected) = expected_value { - if actual_value != expected { - return Err(LeafLookupError::ValueMismatch { - path: *path, - expected: Some(expected.clone()), - actual: actual_value.clone(), - }); - } - } - Ok(()) - } - - let mut current = Nibbles::default(); // Start at the root - - // Inclusion proof - // - // First, do a quick check if the value exists in our values map. - // We assume that if there exists a leaf node, then its value will - // be in the `values` map. - if let Some(actual_value) = self.values.get(full_path) { - // We found the leaf, check if the value matches (if expected value was provided) - check_value_match(actual_value, expected_value, full_path)?; - return Ok(LeafLookup::Exists); - } - - // If the value does not exist in the `values` map, then this means that the leaf either: - // - Does not exist in the trie - // - Is missing from the witness - // We traverse the trie to find the location where this leaf would have been, showing - // that it is not in the trie. Or we find a blinded node, showing that the witness is - // not complete. - while current.len() < full_path.len() { - match self.nodes.get(¤t) { - Some(SparseNode::Empty) | None => { - // None implies no node is at the current path (even in the full trie) - // Empty node means there is a node at this path and it is "Empty" - return Ok(LeafLookup::NonExistent); - } - Some(&SparseNode::Hash(hash)) => { - // We hit a blinded node - cannot determine if leaf exists - return Err(LeafLookupError::BlindedNode { path: current, hash }); - } - Some(SparseNode::Leaf { key, .. }) => { - // We found a leaf node before reaching our target depth - current.extend(key); - if ¤t == full_path { - // This should have been handled by our initial values map check - if let Some(value) = self.values.get(full_path) { - check_value_match(value, expected_value, full_path)?; - return Ok(LeafLookup::Exists); - } - } - - // The leaf node's path doesn't match our target path, - // providing an exclusion proof - return Ok(LeafLookup::NonExistent); - } - Some(SparseNode::Extension { key, .. }) => { - // Temporarily append the extension key to `current` - let saved_len = current.len(); - current.extend(key); - - if full_path.len() < current.len() || !full_path.starts_with(¤t) { - current.truncate(saved_len); // restore - return Ok(LeafLookup::NonExistent); - } - // Prefix matched, so we keep walking with the longer `current`. - } - Some(SparseNode::Branch { state_mask, .. }) => { - // Check if branch has a child at the next nibble in our path - let nibble = full_path.get_unchecked(current.len()); - if !state_mask.is_bit_set(nibble) { - // No child at this nibble - exclusion proof - return Ok(LeafLookup::NonExistent); - } - - // Continue down the branch - current.push_unchecked(nibble); - } - } - } - - // We've traversed to the end of the path and didn't find a leaf - // Check if there's a node exactly at our target path - match self.nodes.get(full_path) { - Some(SparseNode::Leaf { key, .. }) if key.is_empty() => { - // We found a leaf with an empty key (exact match) - // This should be handled by the values map check above - if let Some(value) = self.values.get(full_path) { - check_value_match(value, expected_value, full_path)?; - return Ok(LeafLookup::Exists); - } - } - Some(&SparseNode::Hash(hash)) => { - return Err(LeafLookupError::BlindedNode { path: *full_path, hash }); - } - _ => { - // No leaf at exactly the target path - return Ok(LeafLookup::NonExistent); - } - } - - // If we get here, there's no leaf at the target path - Ok(LeafLookup::NonExistent) - } -} - -impl SerialSparseTrie { - /// Creates a new revealed sparse trie from the given root node. - /// - /// This function initializes the internal structures and then reveals the root. - /// It is a convenient method to create a trie when you already have the root node available. - /// - /// # Arguments - /// - /// * `root` - The root node of the trie - /// * `masks` - Trie masks for root branch node - /// * `retain_updates` - Whether to track updates - /// - /// # Returns - /// - /// Self if successful, or an error if revealing fails. - pub fn from_root( - root: TrieNode, - masks: TrieMasks, - retain_updates: bool, - ) -> SparseTrieResult { - Self::default().with_root(root, masks, retain_updates) - } - - /// Returns a reference to the current sparse trie updates. - /// - /// If no updates have been made/recorded, returns an empty update set. - pub fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates> { - self.updates.as_ref().map_or(Cow::Owned(SparseTrieUpdates::default()), Cow::Borrowed) - } - - /// Returns an immutable reference to all nodes in the sparse trie. - pub const fn nodes_ref(&self) -> &HashMap { - &self.nodes - } - - /// Reveals either a node or its hash placeholder based on the provided child data. - /// - /// When traversing the trie, we often encounter references to child nodes that - /// are either directly embedded or represented by their hash. This method - /// handles both cases: - /// - /// 1. If the child data represents a hash (32+1=33 bytes), store it as a hash node - /// 2. Otherwise, decode the data as a [`TrieNode`] and recursively reveal it using - /// `reveal_node` - /// - /// # Returns - /// - /// Returns `Ok(())` if successful, or an error if the node cannot be revealed. - /// - /// # Error Handling - /// - /// Will error if there's a conflict between a new hash node and an existing one - /// at the same path - fn reveal_node_or_hash(&mut self, path: Nibbles, child: &[u8]) -> SparseTrieResult<()> { - if child.len() == B256::len_bytes() + 1 { - let hash = B256::from_slice(&child[1..]); - match self.nodes.entry(path) { - Entry::Occupied(entry) => match entry.get() { - // Hash node with a different hash can't be handled. - SparseNode::Hash(previous_hash) if previous_hash != &hash => { - return Err(SparseTrieErrorKind::Reveal { - path: *entry.key(), - node: Box::new(SparseNode::Hash(hash)), - } - .into()) - } - _ => {} - }, - Entry::Vacant(entry) => { - entry.insert(SparseNode::Hash(hash)); - } - } - return Ok(()) - } - - self.reveal_node(path, TrieNode::decode(&mut &child[..])?, TrieMasks::none()) - } - - /// Traverse the trie from the root down to the leaf at the given path, - /// removing and collecting all nodes along that path. - /// - /// This helper function is used during leaf removal to extract the nodes of the trie - /// that will be affected by the deletion. These nodes are then re-inserted and modified - /// as needed (collapsing extension nodes etc) given that the leaf has now been removed. - /// - /// # Returns - /// - /// Returns a vector of [`RemovedSparseNode`] representing the nodes removed during the - /// traversal. - /// - /// # Errors - /// - /// Returns an error if a blinded node or an empty node is encountered unexpectedly, - /// as these prevent proper removal of the leaf. - fn take_nodes_for_path(&mut self, path: &Nibbles) -> SparseTrieResult> { - let mut current = Nibbles::default(); // Start traversal from the root - let mut nodes = Vec::new(); // Collect traversed nodes - - while let Some(node) = self.nodes.remove(¤t) { - match &node { - SparseNode::Empty => return Err(SparseTrieErrorKind::Blind.into()), - &SparseNode::Hash(hash) => { - return Err(SparseTrieErrorKind::BlindedNode { path: current, hash }.into()) - } - SparseNode::Leaf { key: _key, .. } => { - // Leaf node is always the one that we're deleting, and no other leaf nodes can - // be found during traversal. - - #[cfg(debug_assertions)] - { - let mut current = current; - current.extend(_key); - assert_eq!(¤t, path); - } - - nodes.push(RemovedSparseNode { - path: current, - node, - unset_branch_nibble: None, - }); - break - } - SparseNode::Extension { key, .. } => { - #[cfg(debug_assertions)] - { - let mut current = current; - current.extend(key); - assert!( - path.starts_with(¤t), - "path: {path:?}, current: {current:?}, key: {key:?}", - ); - } - - let path = current; - current.extend(key); - nodes.push(RemovedSparseNode { path, node, unset_branch_nibble: None }); - } - SparseNode::Branch { state_mask, .. } => { - let nibble = path.get_unchecked(current.len()); - debug_assert!( - state_mask.is_bit_set(nibble), - "current: {current:?}, path: {path:?}, nibble: {nibble:?}, state_mask: {state_mask:?}", - ); - - // If the branch node has a child that is a leaf node that we're removing, - // we need to unset this nibble. - // Any other branch nodes will not require unsetting the nibble, because - // deleting one leaf node can not remove the whole path - // where the branch node is located. - let mut child_path = current; - child_path.push_unchecked(nibble); - let unset_branch_nibble = self - .nodes - .get(&child_path) - .is_some_and(move |node| match node { - SparseNode::Leaf { key, .. } => { - // Get full path of the leaf node - child_path.extend(key); - &child_path == path - } - _ => false, - }) - .then_some(nibble); - - nodes.push(RemovedSparseNode { path: current, node, unset_branch_nibble }); - - current.push_unchecked(nibble); - } - } - } - - Ok(nodes) - } - - /// Recalculates and updates the RLP hashes of nodes deeper than or equal to the specified - /// `depth`. - /// - /// The root node is considered to be at level 0. This method is useful for optimizing - /// hash recalculations after localized changes to the trie structure: - /// - /// This function identifies all nodes that have changed (based on the prefix set) at the given - /// depth and recalculates their RLP representation. - pub fn update_rlp_node_level(&mut self, depth: usize) { - // Take the current prefix set - let mut prefix_set = core::mem::take(&mut self.prefix_set).freeze(); - let mut buffers = RlpNodeBuffers::default(); - - // Get the nodes that have changed at the given depth. - let (targets, new_prefix_set) = self.get_changed_nodes_at_depth(&mut prefix_set, depth); - // Update the prefix set to the prefix set of the nodes that still need to be updated. - self.prefix_set = new_prefix_set; - - trace!(target: "trie::sparse", ?depth, ?targets, "Updating nodes at depth"); - - let mut temp_rlp_buf = core::mem::take(&mut self.rlp_buf); - for (level, path) in targets { - buffers.path_stack.push(RlpNodePathStackItem { - level, - path, - is_in_prefix_set: Some(true), - }); - self.rlp_node(&mut prefix_set, &mut buffers, &mut temp_rlp_buf); - } - self.rlp_buf = temp_rlp_buf; - } - - /// Returns a list of (level, path) tuples identifying the nodes that have changed at the - /// specified depth, along with a new prefix set for the paths above the provided depth that - /// remain unchanged. - /// - /// Leaf nodes with a depth less than `depth` are returned too. - /// - /// This method helps optimize hash recalculations by identifying which specific - /// nodes need to be updated at each level of the trie. - /// - /// # Parameters - /// - /// - `prefix_set`: The current prefix set tracking which paths need updates. - /// - `depth`: The minimum depth (relative to the root) to include nodes in the targets. - /// - /// # Returns - /// - /// A tuple containing: - /// - A vector of `(level, Nibbles)` pairs for nodes that require updates at or below the - /// specified depth. - /// - A `PrefixSetMut` containing paths shallower than the specified depth that still need to be - /// tracked for future updates. - fn get_changed_nodes_at_depth( - &self, - prefix_set: &mut PrefixSet, - depth: usize, - ) -> (Vec<(usize, Nibbles)>, PrefixSetMut) { - let mut unchanged_prefix_set = PrefixSetMut::default(); - let mut paths = Vec::from([(Nibbles::default(), 0)]); - let mut targets = Vec::new(); - - while let Some((mut path, level)) = paths.pop() { - match self.nodes.get(&path).unwrap() { - SparseNode::Empty | SparseNode::Hash(_) => {} - SparseNode::Leaf { key: _, hash } => { - if hash.is_some() && !prefix_set.contains(&path) { - continue - } - - targets.push((level, path)); - } - SparseNode::Extension { key, hash, store_in_db_trie: _ } => { - if hash.is_some() && !prefix_set.contains(&path) { - continue - } - - if level >= depth { - targets.push((level, path)); - } else { - unchanged_prefix_set.insert(path); - - path.extend(key); - paths.push((path, level + 1)); - } - } - SparseNode::Branch { state_mask, hash, store_in_db_trie: _ } => { - if hash.is_some() && !prefix_set.contains(&path) { - continue - } - - if level >= depth { - targets.push((level, path)); - } else { - unchanged_prefix_set.insert(path); - - for bit in CHILD_INDEX_RANGE.rev() { - if state_mask.is_bit_set(bit) { - let mut child_path = path; - child_path.push_unchecked(bit); - paths.push((child_path, level + 1)); - } - } - } - } - } - } - - (targets, unchanged_prefix_set) - } - - /// Look up or calculate the RLP of the node at the root path. - /// - /// # Panics - /// - /// If the node at provided path does not exist. - pub fn rlp_node_allocate(&mut self, prefix_set: &mut PrefixSet) -> RlpNode { - let mut buffers = RlpNodeBuffers::new_with_root_path(); - let mut temp_rlp_buf = core::mem::take(&mut self.rlp_buf); - let result = self.rlp_node(prefix_set, &mut buffers, &mut temp_rlp_buf); - self.rlp_buf = temp_rlp_buf; - - result - } - - /// Looks up or computes the RLP encoding of the node specified by the current - /// path in the provided buffers. - /// - /// The function uses a stack (`RlpNodeBuffers::path_stack`) to track the traversal and - /// accumulate RLP encodings. - /// - /// # Parameters - /// - /// - `prefix_set`: The set of trie paths that need their nodes updated. - /// - `buffers`: The reusable buffers for stack management and temporary RLP values. - /// - /// # Panics - /// - /// If the node at provided path does not exist. - pub fn rlp_node( - &mut self, - prefix_set: &mut PrefixSet, - buffers: &mut RlpNodeBuffers, - rlp_buf: &mut Vec, - ) -> RlpNode { - let _starting_path = buffers.path_stack.last().map(|item| item.path); - - 'main: while let Some(RlpNodePathStackItem { level, path, mut is_in_prefix_set }) = - buffers.path_stack.pop() - { - let node = self.nodes.get_mut(&path).unwrap(); - trace!( - target: "trie::sparse", - ?_starting_path, - ?level, - ?path, - ?is_in_prefix_set, - ?node, - "Popped node from path stack" - ); - - // Check if the path is in the prefix set. - // First, check the cached value. If it's `None`, then check the prefix set, and update - // the cached value. - let mut prefix_set_contains = - |path: &Nibbles| *is_in_prefix_set.get_or_insert_with(|| prefix_set.contains(path)); - - let (rlp_node, node_type) = match node { - SparseNode::Empty => (RlpNode::word_rlp(&EMPTY_ROOT_HASH), SparseNodeType::Empty), - SparseNode::Hash(hash) => (RlpNode::word_rlp(hash), SparseNodeType::Hash), - SparseNode::Leaf { key, hash } => { - let mut path = path; - path.extend(key); - if let Some(hash) = hash.filter(|_| !prefix_set_contains(&path)) { - (RlpNode::word_rlp(&hash), SparseNodeType::Leaf) - } else { - let value = self.values.get(&path).unwrap(); - rlp_buf.clear(); - let rlp_node = LeafNodeRef { key, value }.rlp(rlp_buf); - *hash = rlp_node.as_hash(); - (rlp_node, SparseNodeType::Leaf) - } - } - SparseNode::Extension { key, hash, store_in_db_trie } => { - let mut child_path = path; - child_path.extend(key); - if let Some((hash, store_in_db_trie)) = - hash.zip(*store_in_db_trie).filter(|_| !prefix_set_contains(&path)) - { - ( - RlpNode::word_rlp(&hash), - SparseNodeType::Extension { store_in_db_trie: Some(store_in_db_trie) }, - ) - } else if buffers.rlp_node_stack.last().is_some_and(|e| e.path == child_path) { - let RlpNodeStackItem { - path: _, - rlp_node: child, - node_type: child_node_type, - } = buffers.rlp_node_stack.pop().unwrap(); - rlp_buf.clear(); - let rlp_node = ExtensionNodeRef::new(key, &child).rlp(rlp_buf); - *hash = rlp_node.as_hash(); - - let store_in_db_trie_value = child_node_type.store_in_db_trie(); - - trace!( - target: "trie::sparse", - ?path, - ?child_path, - ?child_node_type, - "Extension node" - ); - - *store_in_db_trie = store_in_db_trie_value; - - ( - rlp_node, - SparseNodeType::Extension { - // Inherit the `store_in_db_trie` flag from the child node, which is - // always the branch node - store_in_db_trie: store_in_db_trie_value, - }, - ) - } else { - // need to get rlp node for child first - buffers.path_stack.extend([ - RlpNodePathStackItem { level, path, is_in_prefix_set }, - RlpNodePathStackItem { - level: level + 1, - path: child_path, - is_in_prefix_set: None, - }, - ]); - continue - } - } - SparseNode::Branch { state_mask, hash, store_in_db_trie } => { - if let Some((hash, store_in_db_trie)) = - hash.zip(*store_in_db_trie).filter(|_| !prefix_set_contains(&path)) - { - buffers.rlp_node_stack.push(RlpNodeStackItem { - path, - rlp_node: RlpNode::word_rlp(&hash), - node_type: SparseNodeType::Branch { - store_in_db_trie: Some(store_in_db_trie), - }, - }); - continue - } - let retain_updates = self.updates.is_some() && prefix_set_contains(&path); - - buffers.branch_child_buf.clear(); - // Walk children in a reverse order from `f` to `0`, so we pop the `0` first - // from the stack and keep walking in the sorted order. - for bit in CHILD_INDEX_RANGE.rev() { - if state_mask.is_bit_set(bit) { - let mut child = path; - child.push_unchecked(bit); - buffers.branch_child_buf.push(child); - } - } - - buffers - .branch_value_stack_buf - .resize(buffers.branch_child_buf.len(), Default::default()); - let mut added_children = false; - - let mut tree_mask = TrieMask::default(); - let mut hash_mask = TrieMask::default(); - let mut hashes = Vec::new(); - for (i, child_path) in buffers.branch_child_buf.iter().enumerate() { - if buffers.rlp_node_stack.last().is_some_and(|e| &e.path == child_path) { - let RlpNodeStackItem { - path: _, - rlp_node: child, - node_type: child_node_type, - } = buffers.rlp_node_stack.pop().unwrap(); - - // Update the masks only if we need to retain trie updates - if retain_updates { - // SAFETY: it's a child, so it's never empty - let last_child_nibble = child_path.last().unwrap(); - - // Determine whether we need to set trie mask bit. - let should_set_tree_mask_bit = if let Some(store_in_db_trie) = - child_node_type.store_in_db_trie() - { - // A branch or an extension node explicitly set the - // `store_in_db_trie` flag - store_in_db_trie - } else { - // A blinded node has the tree mask bit set - child_node_type.is_hash() && - self.branch_node_tree_masks.get(&path).is_some_and( - |mask| mask.is_bit_set(last_child_nibble), - ) - }; - if should_set_tree_mask_bit { - tree_mask.set_bit(last_child_nibble); - } - - // Set the hash mask. If a child node is a revealed branch node OR - // is a blinded node that has its hash mask bit set according to the - // database, set the hash mask bit and save the hash. - let hash = child.as_hash().filter(|_| { - child_node_type.is_branch() || - (child_node_type.is_hash() && - self.branch_node_hash_masks - .get(&path) - .is_some_and(|mask| { - mask.is_bit_set(last_child_nibble) - })) - }); - if let Some(hash) = hash { - hash_mask.set_bit(last_child_nibble); - hashes.push(hash); - } - } - - // Insert children in the resulting buffer in a normal order, - // because initially we iterated in reverse. - // SAFETY: i < len and len is never 0 - let original_idx = buffers.branch_child_buf.len() - i - 1; - buffers.branch_value_stack_buf[original_idx] = child; - added_children = true; - } else { - debug_assert!(!added_children); - buffers.path_stack.push(RlpNodePathStackItem { - level, - path, - is_in_prefix_set, - }); - buffers.path_stack.extend(buffers.branch_child_buf.drain(..).map( - |path| RlpNodePathStackItem { - level: level + 1, - path, - is_in_prefix_set: None, - }, - )); - continue 'main - } - } - - trace!( - target: "trie::sparse", - ?path, - ?tree_mask, - ?hash_mask, - "Branch node masks" - ); - - rlp_buf.clear(); - let branch_node_ref = - BranchNodeRef::new(&buffers.branch_value_stack_buf, *state_mask); - let rlp_node = branch_node_ref.rlp(rlp_buf); - *hash = rlp_node.as_hash(); - - // Save a branch node update only if it's not a root node, and we need to - // persist updates. - let store_in_db_trie_value = if let Some(updates) = - self.updates.as_mut().filter(|_| retain_updates && !path.is_empty()) - { - let store_in_db_trie = !tree_mask.is_empty() || !hash_mask.is_empty(); - if store_in_db_trie { - // Store in DB trie if there are either any children that are stored in - // the DB trie, or any children represent hashed values - hashes.reverse(); - let branch_node = BranchNodeCompact::new( - *state_mask, - tree_mask, - hash_mask, - hashes, - hash.filter(|_| path.is_empty()), - ); - updates.updated_nodes.insert(path, branch_node); - } else if self - .branch_node_tree_masks - .get(&path) - .is_some_and(|mask| !mask.is_empty()) || - self.branch_node_hash_masks - .get(&path) - .is_some_and(|mask| !mask.is_empty()) - { - // If new tree and hash masks are empty, but previously they weren't, we - // need to remove the node update and add the node itself to the list of - // removed nodes. - updates.updated_nodes.remove(&path); - updates.removed_nodes.insert(path); - } else if self - .branch_node_tree_masks - .get(&path) - .is_none_or(|mask| mask.is_empty()) && - self.branch_node_hash_masks - .get(&path) - .is_none_or(|mask| mask.is_empty()) - { - // If new tree and hash masks are empty, and they were previously empty - // as well, we need to remove the node update. - updates.updated_nodes.remove(&path); - } - - store_in_db_trie - } else { - false - }; - *store_in_db_trie = Some(store_in_db_trie_value); - - ( - rlp_node, - SparseNodeType::Branch { store_in_db_trie: Some(store_in_db_trie_value) }, - ) - } - }; - - trace!( - target: "trie::sparse", - ?_starting_path, - ?level, - ?path, - ?node, - ?node_type, - ?is_in_prefix_set, - "Added node to rlp node stack" - ); - - buffers.rlp_node_stack.push(RlpNodeStackItem { path, rlp_node, node_type }); - } - - debug_assert_eq!(buffers.rlp_node_stack.len(), 1); - buffers.rlp_node_stack.pop().unwrap().rlp_node - } -} - -/// Enum representing sparse trie node type. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SparseNodeType { - /// Empty trie node. - Empty, - /// A placeholder that stores only the hash for a node that has not been fully revealed. - Hash, - /// Sparse leaf node. - Leaf, - /// Sparse extension node. - Extension { - /// A flag indicating whether the extension node should be stored in the database. - store_in_db_trie: Option, - }, - /// Sparse branch node. - Branch { - /// A flag indicating whether the branch node should be stored in the database. - store_in_db_trie: Option, - }, -} - -impl SparseNodeType { - /// Returns true if the node is a hash node. - pub const fn is_hash(&self) -> bool { - matches!(self, Self::Hash) - } - - /// Returns true if the node is a branch node. - pub const fn is_branch(&self) -> bool { - matches!(self, Self::Branch { .. }) - } - - /// Returns true if the node should be stored in the database. - pub const fn store_in_db_trie(&self) -> Option { - match *self { - Self::Extension { store_in_db_trie } | Self::Branch { store_in_db_trie } => { - store_in_db_trie - } - _ => None, - } - } -} - -/// Enum representing trie nodes in sparse trie. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SparseNode { - /// Empty trie node. - Empty, - /// The hash of the node that was not revealed. - Hash(B256), - /// Sparse leaf node with remaining key suffix. - Leaf { - /// Remaining key suffix for the leaf node. - key: Nibbles, - /// Pre-computed hash of the sparse node. - /// Can be reused unless this trie path has been updated. - hash: Option, - }, - /// Sparse extension node with key. - Extension { - /// The key slice stored by this extension node. - key: Nibbles, - /// Pre-computed hash of the sparse node. - /// Can be reused unless this trie path has been updated. - /// - /// If [`None`], then the value is not known and should be calculated from scratch. - hash: Option, - /// Pre-computed flag indicating whether the trie node should be stored in the database. - /// Can be reused unless this trie path has been updated. - /// - /// If [`None`], then the value is not known and should be calculated from scratch. - store_in_db_trie: Option, - }, - /// Sparse branch node with state mask. - Branch { - /// The bitmask representing children present in the branch node. - state_mask: TrieMask, - /// Pre-computed hash of the sparse node. - /// Can be reused unless this trie path has been updated. - /// - /// If [`None`], then the value is not known and should be calculated from scratch. - hash: Option, - /// Pre-computed flag indicating whether the trie node should be stored in the database. - /// Can be reused unless this trie path has been updated. - /// - /// If [`None`], then the value is not known and should be calculated from scratch. - store_in_db_trie: Option, - }, -} - -impl SparseNode { - /// Create new sparse node from [`TrieNode`]. - pub fn from_node(node: TrieNode) -> Self { - match node { - TrieNode::EmptyRoot => Self::Empty, - TrieNode::Leaf(leaf) => Self::new_leaf(leaf.key), - TrieNode::Extension(ext) => Self::new_ext(ext.key), - TrieNode::Branch(branch) => Self::new_branch(branch.state_mask), - } - } - - /// Create new [`SparseNode::Branch`] from state mask. - pub const fn new_branch(state_mask: TrieMask) -> Self { - Self::Branch { state_mask, hash: None, store_in_db_trie: None } - } - - /// Create new [`SparseNode::Branch`] with two bits set. - pub const fn new_split_branch(bit_a: u8, bit_b: u8) -> Self { - let state_mask = TrieMask::new( - // set bits for both children - (1u16 << bit_a) | (1u16 << bit_b), - ); - Self::Branch { state_mask, hash: None, store_in_db_trie: None } - } - - /// Create new [`SparseNode::Extension`] from the key slice. - pub const fn new_ext(key: Nibbles) -> Self { - Self::Extension { key, hash: None, store_in_db_trie: None } - } - - /// Create new [`SparseNode::Leaf`] from leaf key and value. - pub const fn new_leaf(key: Nibbles) -> Self { - Self::Leaf { key, hash: None } - } - - /// Returns `true` if the node is a hash node. - pub const fn is_hash(&self) -> bool { - matches!(self, Self::Hash(_)) - } - - /// Returns the hash of the node if it exists. - pub const fn hash(&self) -> Option { - match self { - Self::Empty => None, - Self::Hash(hash) => Some(*hash), - Self::Leaf { hash, .. } | Self::Extension { hash, .. } | Self::Branch { hash, .. } => { - *hash - } - } - } - - /// Sets the hash of the node for testing purposes. - /// - /// For [`SparseNode::Empty`] and [`SparseNode::Hash`] nodes, this method does nothing. - #[cfg(test)] - pub const fn set_hash(&mut self, new_hash: Option) { - match self { - Self::Empty | Self::Hash(_) => { - // Cannot set hash for Empty or Hash nodes - } - Self::Leaf { hash, .. } | Self::Extension { hash, .. } | Self::Branch { hash, .. } => { - *hash = new_hash; - } - } - } -} - -/// A helper struct used to store information about a node that has been removed -/// during a deletion operation. -#[derive(Debug)] -struct RemovedSparseNode { - /// The path at which the node was located. - path: Nibbles, - /// The removed node - node: SparseNode, - /// For branch nodes, an optional nibble that should be unset due to the node being removed. - /// - /// During leaf deletion, this identifies the specific branch nibble path that - /// connects to the leaf being deleted. Then when restructuring the trie after deletion, - /// this nibble position will be cleared from the branch node's to - /// indicate that the child no longer exists. - /// - /// This is only set for branch nodes that have a direct path to the leaf being deleted. - unset_branch_nibble: Option, -} - -/// Collection of reusable buffers for [`SerialSparseTrie::rlp_node`] calculations. -/// -/// These buffers reduce allocations when computing RLP representations during trie updates. -#[derive(Debug, Default)] -pub struct RlpNodeBuffers { - /// Stack of RLP node paths - path_stack: Vec, - /// Stack of RLP nodes - rlp_node_stack: Vec, - /// Reusable branch child path - branch_child_buf: SmallVec<[Nibbles; 16]>, - /// Reusable branch value stack - branch_value_stack_buf: SmallVec<[RlpNode; 16]>, -} - -impl RlpNodeBuffers { - /// Creates a new instance of buffers with the root path on the stack. - fn new_with_root_path() -> Self { - Self { - path_stack: vec![RlpNodePathStackItem { - level: 0, - path: Nibbles::default(), - is_in_prefix_set: None, - }], - rlp_node_stack: Vec::new(), - branch_child_buf: SmallVec::<[Nibbles; 16]>::new_const(), - branch_value_stack_buf: SmallVec::<[RlpNode; 16]>::new_const(), - } - } -} - -/// RLP node path stack item. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RlpNodePathStackItem { - /// Level at which the node is located. Higher numbers correspond to lower levels in the trie. - pub level: usize, - /// Path to the node. - pub path: Nibbles, - /// Whether the path is in the prefix set. If [`None`], then unknown yet. - pub is_in_prefix_set: Option, -} - -/// RLP node stack item. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RlpNodeStackItem { - /// Path to the node. - pub path: Nibbles, - /// RLP node. - pub rlp_node: RlpNode, - /// Type of the node. - pub node_type: SparseNodeType, -} - -impl SparseTrieUpdates { - /// Create new wiped sparse trie updates. - pub fn wiped() -> Self { - Self { wiped: true, ..Default::default() } - } - - /// Clears the updates, but keeps the backing data structures allocated. - /// - /// Sets `wiped` to `false`. - pub fn clear(&mut self) { - self.updated_nodes.clear(); - self.removed_nodes.clear(); - self.wiped = false; - } - - /// Extends the updates with another set of updates. - pub fn extend(&mut self, other: Self) { - self.updated_nodes.extend(other.updated_nodes); - self.removed_nodes.extend(other.removed_nodes); - self.wiped |= other.wiped; - } -} From 4239705134d5bb4995881b24caae8910e50c350d Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 17:50:06 +0800 Subject: [PATCH 07/13] refactor: apply review cleanups across the upgrade branch - withdrawals: skip the post-pass when a block carries no withdrawal updates; factor the branch-child partition into child_regions/ChildRegion shared by emission and survival walks; drop the dead empty-region arm; slice-pattern the sole-survivor check; add_insert_leaves helper for the four insert-only loops; flatten withdrawals/ back to withdrawals.rs; dedupe test fixtures (TrieNode::rlp, SLOT_B, ext_with_absent_branch). - debug-trace-server: reject the unsupported erc7562Tracer at the RequestShape::classify gate (-32602 before any block data is fetched), the executor arms staying as unreachable backstops; drop per-tx EvmState clones the revm-40 API made avoidable (block/tx prestate, mux); take tracing inspectors by mem::replace instead of cloning their recorded trace arenas. - executor: project slot_num from header.slot_number like every other BlockEnv field instead of pinning 0. --- AGENTS.md | 4 +- README.md | 4 +- bin/debug-trace-server/src/response_cache.rs | 17 +- bin/debug-trace-server/src/rpc_service.rs | 19 +- .../src/tracing_executor.rs | 91 ++++--- crates/stateless-core/src/executor.rs | 5 +- .../{withdrawals/mod.rs => withdrawals.rs} | 248 +++++++++--------- 7 files changed, 199 insertions(+), 189 deletions(-) rename crates/stateless-core/src/{withdrawals/mod.rs => withdrawals.rs} (78%) diff --git a/AGENTS.md b/AGENTS.md index d0d84eda..40729c1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,7 +112,7 @@ Inbound JSON-RPC batch entries execute concurrently as independent runtime tasks HTTP responses negotiate gzip/zstd compression per request via `Accept-Encoding` (kill switch: `--response-compression-disabled`); bodies under 4 KiB stay identity, the stack order lives in `http_middleware()` (compression outside the size/timing layers keeps `x-response-size` at the uncompressed size and `x-execution-time-ns` free of compression CPU), and response-cache hits re-compress per hit. Body-streaming cost is metered by the outermost middleware layer (`debug_trace_body_cpu_time_seconds` / `debug_trace_wire_bytes_total`, labeled by encoding), since every request-scoped measurement is sealed before the body streams. The response cache is keyed by `(resource, block hash, typed tracer variant)`; by-number handlers resolve number → canonical hash before the lookup (local `CANONICAL_CHAIN` first, upstream fallback). -It only stores idempotent request shapes: the five supported built-in tracers (keyed by their parsed typed `tracerConfig`) and the bare default struct-logger request; JS tracers, `muxTracer`, the unimplemented `erc7562Tracer` (rejected with a request error), and struct-logger requests with non-default flags bypass it entirely, and a type-malformed `tracerConfig` on call/prestate/flatCall is rejected with `-32602`. +It only stores idempotent request shapes: the five supported built-in tracers (keyed by their parsed typed `tracerConfig`) and the bare default struct-logger request; JS tracers, `muxTracer`, and struct-logger requests with non-default flags bypass it entirely; the unimplemented `erc7562Tracer` and a type-malformed `tracerConfig` on call/prestate/flatCall are rejected with `-32602` before any block data is fetched. Canonical-hash resolution reads the bounded `CANONICAL_CHAIN` window first (local cache mode), then a bounded in-memory memo of upstream-resolved bindings (`--canonical-hash-memo-capacity`; only depth-final heights are memoized, so a memoized binding can no longer reorg), and only then upstream — so historical heights resolve locally after first touch within a process lifetime. The memo's depth gate learns the tip from the window and `latest`-tag lookups, falling back to a throttled upstream `eth_blockNumber` seed (stateless mode with numeric-only traffic); chain sync clears the memo on stale resets and on reorgs at least the safety depth deep. Below the response cache, a bounded in-memory `BlockData` cache keyed by block hash (`--block-data-cache-max-size`, default 1GB, 0 disables) fronts the DB and RPC tiers; block-number lookups resolve number → hash before touching it, so canonicality is never cached and it needs no reorg invalidation. @@ -129,7 +129,7 @@ The background chain-sync prefetch routes by freshness against the last observed | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay (generic over the `BlockInput` projection) | | `crates/stateless-core/src/evm_database.rs` | WitnessDatabase implementing `revm::DatabaseRef` | | `crates/stateless-core/src/db.rs` | Shared storage traits (`ContractStore`, `ChainStore`) + `StoreError` / `StoreResult` | -| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | +| `crates/stateless-core/src/withdrawals.rs` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | RPC client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | RpcMethod, RpcMetrics, RpcClientConfig | diff --git a/README.md b/README.md index 9dd457fc..243e265c 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The HTTP response cache is keyed by `(resource, block hash, tracer variant)` — Entries hold the reply's raw JSON bytes, serialized exactly once from the trace output and spliced verbatim into every response — a hit shares the bytes by `Arc` instead of re-parsing and re-serializing the JSON tree. By-number requests resolve their canonical hash before the lookup (local index, then an in-memory memo, then upstream `eth_getHeaderByNumber`), so a reorged height resolves to the new hash and misses cleanly. Cacheable shapes are the five built-in tracers (`callTracer`, `prestateTracer`, `4byteTracer`, `noopTracer`, `flatCallTracer`, keyed by their parsed, typed `tracerConfig` — equivalent configs collapse onto one entry) and the bare default struct-logger request (no tracer, no `tracerConfig`, default flags). -JS tracers, `muxTracer`, the unimplemented `erc7562Tracer` (rejected with a request error), and struct-logger requests with non-default flags bypass the cache and are recomputed on every request. +JS tracers, `muxTracer`, and struct-logger requests with non-default flags bypass the cache and are recomputed on every request; the unimplemented `erc7562Tracer` is rejected with `-32602` before any block data is fetched. A type-malformed `tracerConfig` on a config-reading builtin (`callTracer`/`prestateTracer`/`flatCallTracer`) is rejected with `-32602 invalid params` instead of being silently traced with default settings. Disable the cache with `--response-cache-disabled`; `--response-cache-estimated-items` must be at least 1 (the old `=0` disable convention is rejected at startup). @@ -273,7 +273,7 @@ The pipeline is configured via `PipelineConfig` and customized through trait imp | `crates/stateless-core/src/executor.rs` | Block validation and EVM replay | | `crates/stateless-core/src/db.rs` | Shared storage traits: `ChainStore`, `ContractStore`, `StoreError` (scenario stores live in their binaries) | | `crates/stateless-core/src/evm_database.rs` | `WitnessDatabase` implementing `revm::DatabaseRef` | -| `crates/stateless-core/src/withdrawals/` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | +| `crates/stateless-core/src/withdrawals.rs` | Withdrawal MPT witness verification (witness linearized into `alloy_trie::HashBuilder`) | | `crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs` | Shared redb tables, helpers, serialization, and bounded `ContractCache` | | `crates/stateless-common/src/rpc_client.rs` | `RpcClient`: multi-endpoint HTTP client for blocks, witnesses, and bytecode | | `crates/stateless-common/src/metrics.rs` | `RpcMethod`, `RpcMetrics`, `RpcClientConfig` | diff --git a/bin/debug-trace-server/src/response_cache.rs b/bin/debug-trace-server/src/response_cache.rs index bef1b81f..deb17033 100644 --- a/bin/debug-trace-server/src/response_cache.rs +++ b/bin/debug-trace-server/src/response_cache.rs @@ -182,6 +182,12 @@ pub enum RequestShape { /// The deserialization failure. error: serde_json::Error, }, + /// A builtin tracer this server does not implement: the request must be rejected + /// before any block data is fetched or executed. + Unsupported { + /// Shape label of the unsupported tracer. + label: &'static str, + }, } impl RequestShape { @@ -193,7 +199,8 @@ impl RequestShape { /// /// Bypassed: JS tracers (the response depends on the tracer source, which has no place /// in a bounded key), `muxTracer`, and struct-logger requests with non-default - /// `opts.config`. + /// `opts.config`. The unimplemented `erc7562Tracer` classifies as [`Self::Unsupported`] + /// and is rejected at the gate. pub fn classify(opts: &GethDebugTracingOptions) -> Self { use alloy_rpc_types_trace::geth::{GethDebugBuiltInTracerType, GethDebugTracerType}; @@ -235,9 +242,9 @@ impl RequestShape { // Exhaustive on purpose: a future alloy builtin variant must make an // explicit cache-whitelist decision here instead of silently bypassing. GethDebugBuiltInTracerType::MuxTracer => Self::Bypass("mux_tracer"), - // Not implemented by the tracing executor (rejected there with a - // request error), so it must not claim a cache slot. - GethDebugBuiltInTracerType::Erc7562Tracer => Self::Bypass("erc7562_tracer"), + GethDebugBuiltInTracerType::Erc7562Tracer => { + Self::Unsupported { label: "erc7562_tracer" } + } }, Some(GethDebugTracerType::JsTracer(_)) => Self::Bypass("js_tracer"), } @@ -263,7 +270,7 @@ impl RequestShape { match self { Self::Cacheable(variant) => variant.label(), Self::Bypass(label) => label, - Self::InvalidTracerConfig { label, .. } => label, + Self::InvalidTracerConfig { label, .. } | Self::Unsupported { label } => label, } } diff --git a/bin/debug-trace-server/src/rpc_service.rs b/bin/debug-trace-server/src/rpc_service.rs index 48fe4aa0..09bcd44f 100644 --- a/bin/debug-trace-server/src/rpc_service.rs +++ b/bin/debug-trace-server/src/rpc_service.rs @@ -338,18 +338,25 @@ fn invalid_params_err(msg: String) -> jsonrpsee::types::ErrorObjectOwned { } /// Classifies `opts`, records the request-shape metric, and rejects a type-malformed -/// `tracerConfig` on a config-reading builtin with `-32602 invalid params` — before any -/// block data is fetched or executed. Returns the cache variant (`Some` only for -/// cacheable shapes). +/// `tracerConfig` on a config-reading builtin — or an unsupported tracer — with +/// `-32602 invalid params`, before any block data is fetched or executed. Returns the +/// cache variant (`Some` only for cacheable shapes). fn classify_and_gate( method_name: &'static str, opts: &GethDebugTracingOptions, ) -> Result, jsonrpsee::types::ErrorObjectOwned> { let shape = RequestShape::classify(opts); metrics::record_request_shape(method_name, shape.label()); - if let RequestShape::InvalidTracerConfig { label, error } = &shape { - metrics::record_rpc_error(method_name); - return Err(invalid_params_err(format!("invalid tracerConfig for {label}: {error}"))); + match &shape { + RequestShape::InvalidTracerConfig { label, error } => { + metrics::record_rpc_error(method_name); + return Err(invalid_params_err(format!("invalid tracerConfig for {label}: {error}"))); + } + RequestShape::Unsupported { label } => { + metrics::record_rpc_error(method_name); + return Err(invalid_params_err(format!("unsupported tracer: {label}"))); + } + _ => {} } Ok(shape.cache_variant()) } diff --git a/bin/debug-trace-server/src/tracing_executor.rs b/bin/debug-trace-server/src/tracing_executor.rs index e992fcfb..59441221 100644 --- a/bin/debug-trace-server/src/tracing_executor.rs +++ b/bin/debug-trace-server/src/tracing_executor.rs @@ -310,11 +310,11 @@ fn trace_block_with_tracing_inspector( match executor.run_transaction(recovered_tx) { Ok(outcome) => { - let state_changes = outcome.inner.state.clone(); + let result_and_state = outcome.inner.result_and_state; let trace_result = match tracer { TracerKind::Call(call_config) => { - let gas_used = outcome.inner.result.tx_gas_used(); + let gas_used = result_and_state.result.tx_gas_used(); let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx.inner.gas_limit()); let frame = @@ -322,7 +322,6 @@ fn trace_block_with_tracing_inspector( GethTrace::from(frame) } TracerKind::PreState(prestate_config) => { - let result_and_state = outcome.inner.result_and_state; executor.inspector_mut().set_transaction_gas_limit(tx.inner.gas_limit()); let frame_result = { let db = executor.evm.db(); @@ -337,7 +336,11 @@ fn trace_block_with_tracing_inspector( Ok(frame) => { let final_frame = if prestate_config.is_diff_mode() { let db = executor.evm.db(); - add_accessed_unchanged_accounts(frame, &state_changes, db) + add_accessed_unchanged_accounts( + frame, + &result_and_state.state, + db, + ) } else { frame }; @@ -348,18 +351,19 @@ fn trace_block_with_tracing_inspector( } TracerKind::FlatCall(_) => { let info = tx_info_at(block, tx, index); - let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = executor - .inspector() - .clone() - .with_transaction_gas_limit(tx.inner.gas_limit()) - .into_parity_builder() - .into_localized_transaction_traces(info); + // Taking the inspector (rather than cloning its recorded trace + // arena) doubles as the per-tx reset below. + let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = + core::mem::replace(executor.inspector_mut(), tracer.create_inspector()) + .with_transaction_gas_limit(tx.inner.gas_limit()) + .into_parity_builder() + .into_localized_transaction_traces(info); GethTrace::from(frame) } TracerKind::Default(opts) => { - let gas_used = outcome.inner.result.tx_gas_used(); + let gas_used = result_and_state.result.tx_gas_used(); let return_value = - outcome.inner.result.output().cloned().unwrap_or_default(); + result_and_state.result.output().cloned().unwrap_or_default(); let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx.inner.gas_limit()); let frame = inspector.geth_builder().geth_traces( @@ -377,7 +381,7 @@ fn trace_block_with_tracing_inspector( results.push(TraceResult::Success { result: trace_result, tx_hash: Some(tx_hash) }); *executor.inspector_mut() = tracer.create_inspector(); - executor.evm.db_mut().commit(state_changes); + executor.evm.db_mut().commit(result_and_state.state); } Err(e) => return Err(tx_replay_failure(index, tx_hash, e)), } @@ -419,7 +423,6 @@ fn trace_tx_with_tracing_inspector( Ok(frame.into()) } TracerKind::PreState(prestate_config) => { - let state_changes = outcome.inner.state.clone(); let result_and_state = outcome.inner.result_and_state; executor.inspector_mut().set_transaction_gas_limit(tx_gas_limit); @@ -437,7 +440,7 @@ fn trace_tx_with_tracing_inspector( Ok(frame) => { let final_frame = if prestate_config.is_diff_mode() { let db = executor.evm.db(); - add_accessed_unchanged_accounts(frame, &state_changes, db) + add_accessed_unchanged_accounts(frame, &result_and_state.state, db) } else { frame }; @@ -448,12 +451,13 @@ fn trace_tx_with_tracing_inspector( } TracerKind::FlatCall(_) => { let info = tx_info_at(block, target_tx, tx_index); - let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = executor - .inspector() - .clone() - .with_transaction_gas_limit(tx_gas_limit) - .into_parity_builder() - .into_localized_transaction_traces(info); + // Take the inspector rather than cloning its recorded trace arena; this + // executor serves no further transactions. + let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = + core::mem::replace(executor.inspector_mut(), tracer.create_inspector()) + .with_transaction_gas_limit(tx_gas_limit) + .into_parity_builder() + .into_localized_transaction_traces(info); Ok(frame.into()) } TracerKind::Default(opts) => { @@ -463,9 +467,8 @@ fn trace_tx_with_tracing_inspector( let inspector = executor.inspector_mut(); inspector.set_transaction_gas_limit(tx_gas_limit); let frame = inspector.geth_builder().geth_traces(gas_used, return_value, opts.config); - // alloy-rpc-types-trace 2.x serializes `returnValue` with a "0x" prefix on - // both sides, so the frame is emitted as-is (the 1.x era needed a - // prefix-stripping shim to match mega-reth). + // `returnValue` is emitted as-is; see the block-level match for why no + // prefix shim is needed on alloy-rpc-types-trace 2.x. Ok(frame.into()) } } @@ -582,7 +585,6 @@ pub fn trace_block( match executor.run_transaction(recovered_tx) { Ok(outcome) => { let result_and_state = outcome.inner.result_and_state; - let state_changes = result_and_state.state.clone(); let mux_result = { let db = executor.evm.db(); @@ -592,7 +594,7 @@ pub fn trace_block( *executor.inspector_mut() = MuxInspector::try_from_config(mux_config.clone()).unwrap(); - executor.evm.db_mut().commit(state_changes); + executor.evm.db_mut().commit(result_and_state.state); match mux_result { Ok(frame) => { @@ -619,10 +621,9 @@ pub fn trace_block( results } - // Not implemented (request-attributable, like an unknown JS tracer - // before alloy knew the name): supporting it means wiring the - // erc7562 inspector config through `TracerKind` and the response - // cache — its own change, not part of the dependency upgrade. + // Unreachable in practice: RPC handlers reject unsupported tracers via + // `RequestShape::classify` (-32602) before reaching the executor. Kept + // as a backstop so the match stays exhaustive. GethDebugBuiltInTracerType::Erc7562Tracer => { return Err(request_error("Unsupported tracer", "erc7562Tracer")); } @@ -831,7 +832,7 @@ pub fn trace_transaction( .map_err(|e| frame_build_error("MuxFrame creation failed", e).into()) } - // Not implemented; see the block-level match for rationale. + // Unreachable backstop; see the block-level match for rationale. GethDebugBuiltInTracerType::Erc7562Tracer => { Err(request_error("Unsupported tracer", "erc7562Tracer")) } @@ -898,15 +899,16 @@ pub fn parity_trace_block( match executor.run_transaction(recovered_tx) { Ok(outcome) => { let state_changes = outcome.inner.result_and_state.state; - let traces = executor - .inspector() - .clone() - .into_parity_builder() - .into_localized_transaction_traces(info); + // Taking the inspector (rather than cloning its recorded trace arena) + // doubles as the per-tx reset. + let traces = core::mem::replace( + executor.inspector_mut(), + TracingInspector::new(TracingInspectorConfig::default_parity()), + ) + .into_parity_builder() + .into_localized_transaction_traces(info); all_traces.extend(traces); - *executor.inspector_mut() = - TracingInspector::new(TracingInspectorConfig::default_parity()); executor.evm.db_mut().commit(state_changes); } Err(e) => { @@ -952,11 +954,14 @@ pub fn parity_trace_transaction( match executor.run_transaction(recovered_tx) { Ok(_outcome) => { - let traces = executor - .inspector() - .clone() - .into_parity_builder() - .into_localized_transaction_traces(info); + // Take the inspector rather than cloning its recorded trace arena; this + // executor serves no further transactions. + let traces = core::mem::replace( + executor.inspector_mut(), + TracingInspector::new(TracingInspectorConfig::default_parity()), + ) + .into_parity_builder() + .into_localized_transaction_traces(info); Ok(traces) } Err(e) => Err(ValidationError::BlockReplayFailed(e).into()), diff --git a/crates/stateless-core/src/executor.rs b/crates/stateless-core/src/executor.rs index f46b5f14..1a91475d 100644 --- a/crates/stateless-core/src/executor.rs +++ b/crates/stateless-core/src/executor.rs @@ -278,9 +278,8 @@ pub fn create_evm_env( difficulty: header.difficulty, prevrandao: Some(header.mix_hash), blob_excess_gas_and_price: None, - // EIP-7843 slot number. MegaETH has not activated Amsterdam, so headers carry - // no slot number and this stays 0 (the upstream `unwrap_or_default()` path). - slot_num: 0, + // EIP-7843 slot number: absent until Amsterdam activates (mirrors upstream). + slot_num: header.slot_number.unwrap_or_default(), }; if let Some(excess_blob_gas) = header.excess_blob_gas { diff --git a/crates/stateless-core/src/withdrawals/mod.rs b/crates/stateless-core/src/withdrawals.rs similarity index 78% rename from crates/stateless-core/src/withdrawals/mod.rs rename to crates/stateless-core/src/withdrawals.rs index 461a2f76..11bf795f 100644 --- a/crates/stateless-core/src/withdrawals/mod.rs +++ b/crates/stateless-core/src/withdrawals.rs @@ -42,7 +42,7 @@ use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256, map::B256 use alloy_rlp::Decodable; use alloy_trie::{ EMPTY_ROOT_HASH, HashBuilder, Nibbles, - nodes::{RlpNode, TrieNode}, + nodes::{BranchNode, RlpNode, TrieNode}, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -145,7 +145,12 @@ impl MptWitness { }); } - let post_root = linearizer.compute_root(self.storage_root, &updates)?; + // With no updates the post-state is the pre-state just verified above. + let post_root = if updates.is_empty() { + pre_root + } else { + linearizer.compute_root(self.storage_root, &updates)? + }; if post_root != expected_post_root { return Err(WithdrawalValidationError::PostStateRootMismatch { expected: expected_post_root, @@ -175,11 +180,7 @@ impl Linearizer<'_> { let mut hb = HashBuilder::default(); if root == EMPTY_ROOT_HASH { // Nothing exists: inserts materialize, removals of absent slots are no-ops. - for (path, update) in updates { - if let Some(value) = update { - hb.add_leaf(*path, value); - } - } + add_insert_leaves(&mut hb, updates); } else if self.nodes.contains_key(&root) { self.emit_subtree(&mut hb, &RlpNode::word_rlp(&root), Nibbles::default(), updates)?; } else { @@ -264,31 +265,13 @@ impl Linearizer<'_> { // absent slots: removals are no-ops, inserts become fresh leaves. // The extension split this implies (and the child hash it reuses) // falls out of the emitted stream — the child is never read. - for (leaf_path, update) in before { - if let Some(value) = update { - hb.add_leaf(*leaf_path, value); - } - } + add_insert_leaves(hb, before); self.emit_subtree(hb, &ext.child, boundary, under)?; - for (leaf_path, update) in after { - if let Some(value) = update { - hb.add_leaf(*leaf_path, value); - } - } + add_insert_leaves(hb, after); Ok(()) } TrieNode::Branch(branch) => { - // Partition updates into per-child regions (contiguous runs, since - // they are sorted and all share `path` as a prefix). - let mut regions: Vec<(u8, Option, &Updates)> = Vec::new(); - let mut rest = updates; - for (idx, child) in branch.as_ref().children() { - let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); - let (region, tail) = rest.split_at(split); - rest = tail; - regions.push((idx, child.cloned(), region)); - } - debug_assert!(rest.is_empty()); + let regions = child_regions(&branch, &path, updates); // A branch that ends up with a single surviving region collapses: the // survivor merges upward. That is derived by the `HashBuilder` from @@ -296,40 +279,33 @@ impl Linearizer<'_> { // unwitnessed subtree, whose node kind (and thus merged shape) is // unknowable. reth v1.6.0's sparse trie failed the same way, via its // empty node provider. - let mut survivors = 0usize; - let mut sole: Option<&(u8, Option, &Updates)> = None; + let mut survivors = Vec::new(); for region in ®ions { if self.region_survives(region, path)? { - survivors += 1; - sole = Some(region); + survivors.push(region); } } - if survivors == 1 { - let (idx, child, region) = sole.expect("survivors == 1"); - if region.is_empty() && - let Some(child) = child && - let Some(hash) = child.as_hash() && - !self.nodes.contains_key(&hash) - { - return Err(trie_error(format!( - "branch collapse at {path:?} adopts unwitnessed subtree at child {idx:x}" - ))); - } + if let [sole] = survivors[..] && + sole.updates.is_empty() && + let Some(child) = &sole.child && + let Some(hash) = child.as_hash() && + !self.nodes.contains_key(&hash) + { + return Err(trie_error(format!( + "branch collapse at {path:?} adopts unwitnessed subtree at child {:x}", + sole.idx + ))); } - for (idx, child, region) in regions { + for region in regions { let mut child_path = path; - child_path.push_unchecked(idx); - match child { - Some(child) => self.emit_subtree(hb, &child, child_path, region)?, + child_path.push_unchecked(region.idx); + match region.child { + Some(child) => self.emit_subtree(hb, &child, child_path, region.updates)?, None => { // Empty slot: absence is proven by the branch mask, so // removals are no-ops and inserts are fresh leaves. - for (leaf_path, update) in region { - if let Some(value) = update { - hb.add_leaf(*leaf_path, value); - } - } + add_insert_leaves(hb, region.updates); } } } @@ -341,23 +317,20 @@ impl Linearizer<'_> { /// Whether a branch child region still holds any content after its updates. fn region_survives( &self, - (idx, child, region): &(u8, Option, &Updates), + region: &ChildRegion<'_>, path: Nibbles, ) -> Result { - // Any insert keeps the region alive; existing content with no updates survives. - if region.iter().any(|(_, update)| update.is_some()) { + // Any insert keeps the region alive; untouched existing content survives + // (`survives_removals` returns true immediately for an empty update set). + if region.updates.iter().any(|(_, update)| update.is_some()) { return Ok(true); } - match child { + match ®ion.child { None => Ok(false), - Some(child) if region.is_empty() => { - let _ = child; - Ok(true) - } Some(child) => { let mut child_path = path; - child_path.push_unchecked(*idx); - self.survives_removals(child, child_path, region) + child_path.push_unchecked(region.idx); + self.survives_removals(child, child_path, region.updates) } } } @@ -391,15 +364,8 @@ impl Linearizer<'_> { self.survives_removals(&ext.child, boundary, under) } TrieNode::Branch(branch) => { - let mut rest = removals; - for (idx, child) in branch.as_ref().children() { - let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); - let (region, tail) = rest.split_at(split); - rest = tail; - let Some(child) = child else { continue }; - let mut child_path = path; - child_path.push_unchecked(idx); - if region.is_empty() || self.survives_removals(child, child_path, region)? { + for region in child_regions(&branch, &path, removals) { + if self.region_survives(®ion, path)? { return Ok(true); } } @@ -409,6 +375,46 @@ impl Linearizer<'_> { } } +/// One branch child slot together with the updates that fall under it. +struct ChildRegion<'a> { + /// The child's nibble index within the branch. + idx: u8, + /// The child reference from the branch stack; `None` for an empty slot. + child: Option, + /// The updates whose paths run through this slot. + updates: &'a Updates, +} + +/// Partitions `updates` (sorted, all lying under `path`) into the branch's 16 child +/// regions. Each region is a contiguous run because the paths are sorted and grouped +/// by their nibble at depth `path.len()`. +fn child_regions<'a>( + branch: &BranchNode, + path: &Nibbles, + updates: &'a Updates, +) -> Vec> { + let mut regions = Vec::with_capacity(16); + let mut rest = updates; + for (idx, child) in branch.as_ref().children() { + let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); + let (region, tail) = rest.split_at(split); + rest = tail; + regions.push(ChildRegion { idx, child: child.cloned(), updates: region }); + } + debug_assert!(rest.is_empty()); + regions +} + +/// Emits the inserted leaves of `updates` into `hb`. Only valid where the removals in +/// `updates` are proven no-ops (their target region is known to hold no such leaves). +fn add_insert_leaves(hb: &mut HashBuilder, updates: &Updates) { + for (path, update) in updates { + if let Some(value) = update { + hb.add_leaf(*path, value); + } + } +} + /// Splits sorted `updates` around the region prefixed by `prefix`: /// `(diverging before, under prefix, diverging after)`. /// @@ -439,6 +445,9 @@ mod tests { use super::*; const SLOT: B256 = b256!("0x1111111111111111111111111111111111111111111111111111111111111111"); + /// A second slot diverging from `SLOT` at the first nibble. + const SLOT_B: B256 = + b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); const BOGUS: B256 = b256!("0xdeadbeef00000000000000000000000000000000000000000000000000000000"); fn encoded(node: &TrieNode) -> Bytes { @@ -447,6 +456,10 @@ mod tests { Bytes::from(rlp) } + fn rlp_of(node: &TrieNode) -> RlpNode { + node.rlp(&mut Vec::new()) + } + fn value_rlp(value: u64) -> Vec { alloy_rlp::encode_fixed_size(&U256::from(value)).to_vec() } @@ -564,104 +577,83 @@ mod tests { /// extension's first key nibble. #[test] fn insert_splitting_extension_with_absent_child_branch() { - let rlp_of = |node: &TrieNode| RlpNode::from_rlp(&encoded(node)); - - // Child branch of the extension: two empty-key leaves at nibbles 1 and 2. - let branch = TrieNode::Branch(BranchNode::new( - vec![ - rlp_of(&leaf_node(Nibbles::default(), 7)), - rlp_of(&leaf_node(Nibbles::default(), 8)), - ], - 0b0110.into(), - )); - let branch_hash = keccak256(encoded(&branch)); - - // Root extension covering the first 63 nibbles of SLOT. - let ext_key = Nibbles::unpack(SLOT).slice(0..63); - let ext = TrieNode::Extension(ExtensionNode::new(ext_key, RlpNode::word_rlp(&branch_hash))); - let ext_rlp = encoded(&ext); - let pre_root = keccak256(&ext_rlp); - - // Insert a slot that diverges from the extension key at nibble 0. - let diverging_slot = - b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); + let (pre_root, witness, branch_hash) = ext_with_absent_branch(); // Expected post-state: branch at the root splitting nibble 1 (old subtree, // reached via the shortened extension) from nibble 2 (the new leaf). + let ext_key = Nibbles::unpack(SLOT).slice(0..63); let shortened_ext = TrieNode::Extension(ExtensionNode::new( ext_key.slice(1..), RlpNode::word_rlp(&branch_hash), )); - let new_leaf = - TrieNode::Leaf(LeafNode::new(Nibbles::unpack(diverging_slot).slice(1..), value_rlp(9))); + let new_leaf = leaf_node(Nibbles::unpack(SLOT_B).slice(1..), 9); let split_branch = TrieNode::Branch(BranchNode::new( vec![rlp_of(&shortened_ext), rlp_of(&new_leaf)], 0b0110.into(), )); let expected_post_root = keccak256(encoded(&split_branch)); - // Witness: extension only — the child branch is intentionally absent. - run( - pre_root, - vec![ext_rlp], - Some(expected_post_root), - &[(diverging_slot, U256::from(9u64))], - ) - .unwrap(); + run(pre_root, witness, Some(expected_post_root), &[(SLOT_B, U256::from(9u64))]).unwrap(); } /// A zero-write to a slot that provably diverges inside a witnessed extension /// key is a no-op: the post root equals the pre root. #[test] fn diverging_removal_is_noop() { - let branch = - TrieNode::Branch(BranchNode::new(vec![rlp_of_leaf(7), rlp_of_leaf(8)], 0b0110.into())); + let (pre_root, witness, _) = ext_with_absent_branch(); + run(pre_root, witness, Some(pre_root), &[(SLOT_B, U256::ZERO)]).unwrap(); + } + + /// Root extension (key = first 63 nibbles of `SLOT`) whose child branch — two + /// empty-key leaves at nibbles 1 and 2 — is intentionally absent from the + /// witness: returns `(pre_root, witness, branch_hash)`. + fn ext_with_absent_branch() -> (B256, Vec, B256) { + let branch = TrieNode::Branch(BranchNode::new( + vec![ + rlp_of(&leaf_node(Nibbles::default(), 7)), + rlp_of(&leaf_node(Nibbles::default(), 8)), + ], + 0b0110.into(), + )); + let branch_hash = keccak256(encoded(&branch)); let ext = TrieNode::Extension(ExtensionNode::new( Nibbles::unpack(SLOT).slice(0..63), - RlpNode::word_rlp(&keccak256(encoded(&branch))), + RlpNode::word_rlp(&branch_hash), )); let ext_rlp = encoded(&ext); - let pre_root = keccak256(&ext_rlp); - let diverging_slot = - b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); - run(pre_root, vec![ext_rlp], Some(pre_root), &[(diverging_slot, U256::ZERO)]).unwrap(); - } - - fn rlp_of_leaf(value: u64) -> RlpNode { - RlpNode::from_rlp(&encoded(&leaf_node(Nibbles::default(), value))) + (keccak256(&ext_rlp), vec![ext_rlp], branch_hash) } - /// Two-leaf trie under a root branch: (root, branch_rlp, leaf rlps, slots). - fn two_leaf_branch() -> (B256, Vec, B256, B256) { - let slot_b = b256!("0x2111111111111111111111111111111111111111111111111111111111111111"); + /// Two-leaf trie: root branch holding `SLOT` under nibble 1 and `SLOT_B` under + /// nibble 2, all nodes witnessed. Returns `(root, witness)`. + fn two_leaf_branch() -> (B256, Vec) { let leaf_a = leaf_node(Nibbles::unpack(SLOT).slice(1..), 7); - let leaf_b = leaf_node(Nibbles::unpack(slot_b).slice(1..), 8); + let leaf_b = leaf_node(Nibbles::unpack(SLOT_B).slice(1..), 8); let branch = TrieNode::Branch(BranchNode::new( - vec![RlpNode::from_rlp(&encoded(&leaf_a)), RlpNode::from_rlp(&encoded(&leaf_b))], + vec![rlp_of(&leaf_a), rlp_of(&leaf_b)], 0b0110.into(), )); let branch_rlp = encoded(&branch); - let root = keccak256(&branch_rlp); - (root, vec![branch_rlp, encoded(&leaf_a), encoded(&leaf_b)], SLOT, slot_b) + (keccak256(&branch_rlp), vec![branch_rlp, encoded(&leaf_a), encoded(&leaf_b)]) } /// Removing one of two leaves collapses the root branch into the surviving /// leaf, whose node is witnessed — the merged single-leaf root must come out. #[test] fn removal_collapse_merges_witnessed_sibling() { - let (root, state, slot_a, slot_b) = two_leaf_branch(); - let expected = keccak256(encoded(&leaf_node(Nibbles::unpack(slot_b), 8))); - run(root, state, Some(expected), &[(slot_a, U256::ZERO)]).unwrap(); + let (root, state) = two_leaf_branch(); + let expected = keccak256(encoded(&leaf_node(Nibbles::unpack(SLOT_B), 8))); + run(root, state, Some(expected), &[(SLOT, U256::ZERO)]).unwrap(); } /// Same collapse, but the surviving sibling's node is absent from the witness: /// its merged shape is unknowable, so verification must fail closed. #[test] fn removal_collapse_with_unwitnessed_survivor_errors() { - let (root, mut state, slot_a, _) = two_leaf_branch(); + let (root, mut state) = two_leaf_branch(); state.truncate(2); // drop leaf_b — the survivor assert!(matches!( - run(root, state, Some(root), &[(slot_a, U256::ZERO)]), + run(root, state, Some(root), &[(SLOT, U256::ZERO)]), Err(TrieOperationFailed(_)) )); } @@ -669,10 +661,10 @@ mod tests { /// An update descending into an unwitnessed branch child must fail closed. #[test] fn update_descending_into_unwitnessed_subtree_errors() { - let (root, mut state, _, slot_b) = two_leaf_branch(); - state.truncate(2); // drop leaf_b; slot_b's region is now opaque + let (root, mut state) = two_leaf_branch(); + state.truncate(2); // drop leaf_b; SLOT_B's region is now opaque assert!(matches!( - run(root, state, Some(root), &[(slot_b, U256::from(9u64))]), + run(root, state, Some(root), &[(SLOT_B, U256::from(9u64))]), Err(TrieOperationFailed(_)) )); } From 2f94dee17945177dd05eb4e6e4b97d068c69f69c Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 18:04:22 +0800 Subject: [PATCH 08/13] docs: trim the withdrawals module doc to its purpose --- crates/stateless-core/src/withdrawals.rs | 32 +++--------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/crates/stateless-core/src/withdrawals.rs b/crates/stateless-core/src/withdrawals.rs index 11bf795f..b4190ca4 100644 --- a/crates/stateless-core/src/withdrawals.rs +++ b/crates/stateless-core/src/withdrawals.rs @@ -3,34 +3,10 @@ //! This module verifies storage state transitions for the L2ToL1MessagePasser contract, //! which stores commitments to withdrawal transactions. Given a pre-state witness and //! storage updates from block execution, it cryptographically proves the storage root -//! transition is valid. -//! -//! # Approach -//! -//! The witness is the union of `eth_getProof`-style minimal per-slot proofs for the -//! contract's storage trie: RLP nodes along each touched slot's inclusion or exclusion -//! path, and nothing else. In particular, an exclusion path may end at an extension -//! node whose child branch is absent — splitting that extension on insert never reads -//! the child. -//! -//! Verification linearizes the witnessed trie into [`alloy_trie::HashBuilder`] — the -//! same primitive reth's own proof generator is built on — as a sorted stream of: -//! -//! - `add_leaf` for every surviving or updated leaf (leaf paths are the hashed slots and never -//! change), and -//! - `add_branch` for every unwitnessed subtree (its position and hash are invariant under edits -//! elsewhere). -//! -//! All structural changes — extension splits, branch collapses, new branches — fall -//! out of the stream shape inside the builder, so no trie mutation code exists here. -//! -//! Two passes run per verification. The pre-pass linearizes the witness with **no** -//! updates and must reproduce `storage_root` bit-for-bit, self-checking the walk on -//! the very witness being verified; the post-pass applies the updates and must -//! reproduce the header's `withdrawals_root`. Updates that cannot be proven against -//! the witness (descent into an unwitnessed subtree, or a branch collapse adopting an -//! unwitnessed node) fail closed, matching the behavior of the reth v1.6.0 sparse -//! trie this module previously relied on. +//! transition is valid: the witnessed trie is rebuilt with [`alloy_trie::HashBuilder`] +//! once without updates (must reproduce `storage_root`) and once with them (must +//! reproduce the header's `withdrawals_root`); updates the witness cannot prove fail +//! closed. use std::{ string::{String, ToString}, From 139551939631e563ed20d9c299b7adeb4c67464b Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 18:19:37 +0800 Subject: [PATCH 09/13] chore(r2): move the stateless-r2 reqwest pin to 0.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mega-reth's upstream/reth workspace — the branch this upgrade wave targets — is on reqwest 0.13, so the API-coupled pin follows (rustls-tls feature is named rustls since 0.13; http2 note stays valid, the feature is opt-in now). The lockfile drops to a single reqwest 0.13.4. --- Cargo.lock | 79 +++------------------------------- crates/stateless-r2/Cargo.toml | 15 +++---- 2 files changed, 13 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 536d0f42..dfdae966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,7 +406,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.13.4", + "reqwest", "serde", "serde_json", "thiserror 2.0.20", @@ -450,7 +450,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest 0.13.4", + "reqwest", "serde", "serde_json", "tokio", @@ -687,7 +687,7 @@ dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest 0.13.4", + "reqwest", "serde_json", "tower", "tracing", @@ -2090,7 +2090,7 @@ dependencies = [ "quick_cache", "rayon", "redb", - "reqwest 0.13.4", + "reqwest", "revm", "revm-inspectors", "serde", @@ -2925,7 +2925,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -4575,44 +4574,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - [[package]] name = "reqwest" version = "0.13.4" @@ -5203,7 +5164,6 @@ checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -5289,12 +5249,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "ryu-js" version = "1.0.2" @@ -5563,18 +5517,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_with" version = "3.16.0" @@ -5837,7 +5779,7 @@ dependencies = [ "kanal", "op-alloy-network", "op-alloy-rpc-types", - "reqwest 0.13.4", + "reqwest", "revm", "rolling-file", "salt", @@ -5921,7 +5863,7 @@ dependencies = [ "hex", "hmac", "percent-encoding", - "reqwest 0.12.24", + "reqwest", "sha2 0.10.9", "stateless-test-utils", "tokio", @@ -6732,15 +6674,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/stateless-r2/Cargo.toml b/crates/stateless-r2/Cargo.toml index 16ce247d..bd2d5437 100644 --- a/crates/stateless-r2/Cargo.toml +++ b/crates/stateless-r2/Cargo.toml @@ -17,14 +17,13 @@ hex = { workspace = true, features = ["alloc"] } hmac.workspace = true percent-encoding.workspace = true # Pinned explicitly (not workspace-inherited): `put_object` exposes `&reqwest::Client`, so the -# reqwest major is part of this crate's API and must match mega-reth's workspace (0.12). Bumping -# it is a coordinated two-repo change that a workspace-wide bump must not ride over — this -# workspace itself moved to 0.13 (alloy 2.x transports), which is fine: no reqwest types -# cross between this crate and the alloy-facing code. -# No `http2` feature: R2's bare S3 endpoint negotiates HTTP/1.1 only (ALPN-verified live), -# so h2 buys these paths nothing today. Revisit alongside the R2 custom-domain work, which -# does ride an h2-capable stack. -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +# reqwest major is part of this crate's API and must match mega-reth's workspace (0.13 on its +# reth-2.3 `upstream/reth` branch). Bumping it is a coordinated two-repo change that a +# workspace-wide bump must not ride over. +# No `http2` feature (opt-in since 0.13): R2's bare S3 endpoint negotiates HTTP/1.1 only +# (ALPN-verified live), so h2 buys these paths nothing today. Revisit alongside the R2 +# custom-domain work, which does ride an h2-capable stack. +reqwest = { version = "0.13", default-features = false, features = ["rustls"] } sha2.workspace = true tokio = { workspace = true, features = ["sync", "time"] } tracing = { workspace = true, features = ["std"] } From d27bac710fc0430867ae00dda8cc356d5b2b76ff Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 18:38:59 +0800 Subject: [PATCH 10/13] fix(no-std): import format via the alloc-as-std alias in withdrawals The no_std build (--no-default-features) has no prelude format!; import it explicitly like evm_database.rs does. Fixes the no-std and coverage CI jobs, which both compile stateless-core without default features. --- crates/stateless-core/src/withdrawals.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/stateless-core/src/withdrawals.rs b/crates/stateless-core/src/withdrawals.rs index b4190ca4..28672329 100644 --- a/crates/stateless-core/src/withdrawals.rs +++ b/crates/stateless-core/src/withdrawals.rs @@ -9,6 +9,7 @@ //! closed. use std::{ + format, string::{String, ToString}, vec::Vec, }; From e9c3a32c35f5d10ce196c48aa86074e7aae302ad Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Mon, 10 Aug 2026 19:03:37 +0800 Subject: [PATCH 11/13] docs: drop stale pre-upgrade version numbers from comments --- crates/stateless-common/Cargo.toml | 2 +- crates/stateless-core/src/light_witness.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/stateless-common/Cargo.toml b/crates/stateless-common/Cargo.toml index 7a88942d..d1ba1357 100644 --- a/crates/stateless-common/Cargo.toml +++ b/crates/stateless-common/Cargo.toml @@ -36,7 +36,7 @@ clap.workspace = true eyre.workspace = true fastrand = { workspace = true, features = ["std"] } futures.workspace = true -# Also enables gzip/brotli on the shared reqwest 0.12 client (Cargo feature unification) for witness/data fetches. +# Also enables gzip/brotli on the shared reqwest client (Cargo feature unification) for witness/data fetches. reqwest = { workspace = true, features = ["gzip", "brotli"] } rolling-file.workspace = true serde.workspace = true diff --git a/crates/stateless-core/src/light_witness.rs b/crates/stateless-core/src/light_witness.rs index 035e200f..dd9c0dda 100644 --- a/crates/stateless-core/src/light_witness.rs +++ b/crates/stateless-core/src/light_witness.rs @@ -50,8 +50,9 @@ pub struct LightWitness { /// Bucket subtree levels (same as SaltProof.levels). /// /// Routed through [`salt::fx_hashmap_serde`] so we don't have to enable - /// `hashbrown/serde` (which transitively pulls in `serde_core 1.0.221+` - /// and breaks downstream `alloy-tx-macros 1.0.23`). + /// `hashbrown/serde` (introduced when its transitive `serde_core 1.0.221+` + /// broke the then-current `alloy-tx-macros 1.0.23`; the indirection stays + /// so the workspace keeps control over what `hashbrown` pulls in). #[serde(with = "salt::fx_hashmap_serde")] pub levels: FxHashMap, } From 99485a5d0cbc4bce46067fcc2b7419e94d3ae869 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Tue, 11 Aug 2026 08:55:16 +0800 Subject: [PATCH 12/13] refactor: second review pass over the upgrade branch - tracing_executor: extract the four TracerKind frame builders into one extract_trace_frame! macro shared by the block and tx paths (the twin matches this PR had to edit in lockstep six times); the FlatCall take now really is the per-tx reset (guarded tail); mux inspectors reset from a pristine clone instead of re-cloning + re-parsing the config each tx (also removes the only dispatcher unwrap); erc7562 rejection has one home in builtin_tracer_kind and the dispatchers route it there; preceding-tx replay in the tx-level and parity paths seeds a none-config inspector instead of recording every step just to discard it; drop a dead duplicate binding. - rpc_service: classify_and_gate dedups its rejection tail behind an exhaustive match (a future RequestShape variant must make an explicit gate decision); new test pins the -32602 for unsupported tracers. - response_cache: the label-conformance test now covers the Unsupported variant. - withdrawals: the leaf arm reuses sortedness instead of collect+sort; the split_at_prefix upper bound is a second partition_point. --- bin/debug-trace-server/src/response_cache.rs | 8 + bin/debug-trace-server/src/rpc_service.rs | 32 +- .../src/tracing_executor.rs | 303 ++++++++---------- crates/stateless-core/src/withdrawals.rs | 32 +- 4 files changed, 170 insertions(+), 205 deletions(-) diff --git a/bin/debug-trace-server/src/response_cache.rs b/bin/debug-trace-server/src/response_cache.rs index deb17033..bb984913 100644 --- a/bin/debug-trace-server/src/response_cache.rs +++ b/bin/debug-trace-server/src/response_cache.rs @@ -779,6 +779,14 @@ mod tests { let shape = RequestShape::classify(&malformed); assert_eq!(shape.label(), "call_tracer"); assert!(shape.cache_variant().is_none()); + + // The unsupported erc7562Tracer likewise: registered label, neither cacheable + // nor bypass (the gate rejects it before execution). + let shape = + RequestShape::classify(&builtin_opts(GethDebugBuiltInTracerType::Erc7562Tracer)); + assert_eq!(shape.label(), "erc7562_tracer"); + assert!(crate::metrics::REQUEST_SHAPES.contains(&shape.label())); + assert!(shape.cache_variant().is_none()); } /// Key identity (Eq + Hash, exercised through a `HashSet`) discriminates on every diff --git a/bin/debug-trace-server/src/rpc_service.rs b/bin/debug-trace-server/src/rpc_service.rs index 09bcd44f..be9d3711 100644 --- a/bin/debug-trace-server/src/rpc_service.rs +++ b/bin/debug-trace-server/src/rpc_service.rs @@ -347,18 +347,17 @@ fn classify_and_gate( ) -> Result, jsonrpsee::types::ErrorObjectOwned> { let shape = RequestShape::classify(opts); metrics::record_request_shape(method_name, shape.label()); - match &shape { + // Exhaustive on purpose, like `classify` itself: a future shape variant must make + // an explicit gate decision here instead of silently passing through. + let message = match &shape { + RequestShape::Cacheable(_) | RequestShape::Bypass(_) => return Ok(shape.cache_variant()), RequestShape::InvalidTracerConfig { label, error } => { - metrics::record_rpc_error(method_name); - return Err(invalid_params_err(format!("invalid tracerConfig for {label}: {error}"))); + format!("invalid tracerConfig for {label}: {error}") } - RequestShape::Unsupported { label } => { - metrics::record_rpc_error(method_name); - return Err(invalid_params_err(format!("unsupported tracer: {label}"))); - } - _ => {} - } - Ok(shape.cache_variant()) + RequestShape::Unsupported { label } => format!("unsupported tracer: {label}"), + }; + metrics::record_rpc_error(method_name); + Err(invalid_params_err(message)) } /// Maps a [`DataProviderError`] to a JSON-RPC error object. @@ -1126,6 +1125,19 @@ mod tests { assert!(classify_and_gate("test_method", &opts).unwrap().is_some()); } + #[test] + fn unsupported_tracer_maps_to_invalid_params() { + let opts: GethDebugTracingOptions = + serde_json::from_value(serde_json::json!({"tracer": "erc7562Tracer"})).unwrap(); + let err = classify_and_gate("test_method", &opts).unwrap_err(); + assert_eq!(err.code(), jsonrpsee::types::error::INVALID_PARAMS_CODE); + assert!( + err.message().contains("unsupported tracer: erc7562_tracer"), + "message: {}", + err.message(), + ); + } + #[test] fn test_timed_alias_duplicate_rejected() { // Registering the same alias twice should fail diff --git a/bin/debug-trace-server/src/tracing_executor.rs b/bin/debug-trace-server/src/tracing_executor.rs index 4aea12cc..e5f96f6a 100644 --- a/bin/debug-trace-server/src/tracing_executor.rs +++ b/bin/debug-trace-server/src/tracing_executor.rs @@ -34,7 +34,6 @@ use alloy_rpc_types_trace::{ FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, GethDebugTracingOptions, GethTrace, NoopFrame, TraceResult, call::{CallConfig, FlatCallConfig}, - mux::MuxConfig, pre_state::{AccountState, PreStateConfig, PreStateFrame}, }, parity::LocalizedTransactionTrace, @@ -227,27 +226,19 @@ fn tx_replay_failure( ValidationError::BlockReplayFailed(e) } -/// Builds a [`MuxInspector`] from an already-parsed config (request-attributable on -/// failure). -fn mux_inspector(config: MuxConfig) -> Result { - // Since alloy 2.x, an unknown tracer name inside a mux config parses (as a JS - // tracer key) and is only rejected here by the inspector — still config-attributable, - // so the context matches the serde-failure wording. - MuxInspector::try_from_config(config) - .map_err(|e| request_error("invalid muxTracer tracerConfig", e)) -} - /// Parses the mux `tracerConfig` and builds its inspector — the one home for the mux -/// construction path shared by the block and transaction dispatchers. -fn mux_config_and_inspector( - tracer_config: &GethDebugTracerConfig, -) -> Result<(MuxConfig, MuxInspector), TraceError> { +/// construction path shared by the block and transaction dispatchers. Per-tx resets +/// clone the pristine inspector instead of re-parsing the config. +fn mux_inspector(tracer_config: &GethDebugTracerConfig) -> Result { let config = tracer_config .clone() .into_mux_config() .map_err(|e| request_error("invalid muxTracer tracerConfig", e))?; - let inspector = mux_inspector(config.clone())?; - Ok((config, inspector)) + // Since alloy 2.x, an unknown tracer name inside a mux config parses (as a JS + // tracer key) and is only rejected here by the inspector — still config-attributable, + // so the context matches the serde-failure wording. + MuxInspector::try_from_config(config) + .map_err(|e| request_error("invalid muxTracer tracerConfig", e)) } /// Parses a config-reading builtin's `tracerConfig` into its [`TracerKind`] — the one @@ -277,14 +268,17 @@ fn builtin_tracer_kind( .into_flat_call_config() .map(TracerKind::FlatCall) .map_err(|e| request_error("invalid flatCallTracer tracerConfig", e)), + // Unsupported: normally rejected by `RequestShape::classify` (-32602) before + // the executor runs; the dispatchers route it here so the message has one home. + GethDebugBuiltInTracerType::Erc7562Tracer => { + Err(request_error("Unsupported tracer", "erc7562Tracer")) + } // Not TracingInspector tracers: the dispatchers execute these in their own arms - // (mux via `mux_config_and_inspector`; the unsupported erc7562Tracer is - // rejected there) and never route them here; kept total — a request error - // rather than a panic. + // (mux via `mux_inspector`) and never route them here; kept total — a request + // error rather than a panic. GethDebugBuiltInTracerType::FourByteTracer | GethDebugBuiltInTracerType::NoopTracer | - GethDebugBuiltInTracerType::MuxTracer | - GethDebugBuiltInTracerType::Erc7562Tracer => { + GethDebugBuiltInTracerType::MuxTracer => { Err(request_error("tracer takes no typed tracerConfig here", builtin)) } } @@ -331,6 +325,78 @@ macro_rules! replay_preceding_txs { } // TracingInspector-based helpers (shared by Call, PreState, FlatCall, Default) +/// Extracts the traced transaction's frame from the executor's inspector — the one +/// home for the four `TracerKind` frame builders, shared by the block and +/// single-transaction paths (whose executor types are macro-local, like +/// [`setup_executor!`]'s). +/// +/// Evaluates to `Result`. The `FlatCall` arm takes the +/// inspector by value (leaving a fresh one behind, which doubles as its per-tx +/// reset); every other arm leaves the recorded inspector in place for the caller to +/// reset. `$info` is only evaluated by the `FlatCall` arm. +macro_rules! extract_trace_frame { + ($executor:ident, $tracer:expr, $result_and_state:expr, $tx_gas_limit:expr, $info:expr) => {{ + let result_and_state = &$result_and_state; + match $tracer { + TracerKind::Call(call_config) => { + let gas_used = result_and_state.result.tx_gas_used(); + let inspector = $executor.inspector_mut(); + inspector.set_transaction_gas_limit($tx_gas_limit); + Ok(GethTrace::from( + inspector.geth_builder().geth_call_traces(*call_config, gas_used), + )) + } + TracerKind::PreState(prestate_config) => { + $executor.inspector_mut().set_transaction_gas_limit($tx_gas_limit); + let frame_result = { + let db = $executor.evm.db(); + let inspector = $executor.inspector(); + inspector.geth_builder().geth_prestate_traces( + result_and_state, + prestate_config, + db, + ) + }; + match frame_result { + Ok(frame) => { + let final_frame = if prestate_config.is_diff_mode() { + let db = $executor.evm.db(); + add_accessed_unchanged_accounts(frame, &result_and_state.state, db) + } else { + frame + }; + Ok(GethTrace::from(final_frame)) + } + Err(e) => Err(frame_build_error("PreState trace failed", e)), + } + } + TracerKind::FlatCall(_) => { + // Take the inspector rather than cloning its recorded trace arena. + let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = + core::mem::replace($executor.inspector_mut(), $tracer.create_inspector()) + .with_transaction_gas_limit($tx_gas_limit) + .into_parity_builder() + .into_localized_transaction_traces($info); + Ok(GethTrace::from(frame)) + } + TracerKind::Default(opts) => { + let gas_used = result_and_state.result.tx_gas_used(); + let return_value = result_and_state.result.output().cloned().unwrap_or_default(); + let inspector = $executor.inspector_mut(); + inspector.set_transaction_gas_limit($tx_gas_limit); + // alloy-rpc-types-trace 2.x serializes `returnValue` with a "0x" prefix + // on both sides, so the frame is emitted as-is (the 1.x era needed a + // prefix-stripping shim to match mega-reth). + Ok(GethTrace::from(inspector.geth_builder().geth_traces( + gas_used, + return_value, + opts.config, + ))) + } + } + }}; +} + /// Traces all transactions in a block using a `TracingInspector`-based tracer. /// /// Handles executor creation, pre-execution, tx loop, trace extraction, inspector @@ -354,76 +420,21 @@ fn trace_block_with_tracing_inspector( match executor.run_transaction(recovered_tx) { Ok(outcome) => { let result_and_state = outcome.inner.result_and_state; - - let trace_result = match tracer { - TracerKind::Call(call_config) => { - let gas_used = result_and_state.result.tx_gas_used(); - let inspector = executor.inspector_mut(); - inspector.set_transaction_gas_limit(tx.inner.gas_limit()); - let frame = - inspector.geth_builder().geth_call_traces(*call_config, gas_used); - GethTrace::from(frame) - } - TracerKind::PreState(prestate_config) => { - executor.inspector_mut().set_transaction_gas_limit(tx.inner.gas_limit()); - let frame_result = { - let db = executor.evm.db(); - let inspector = executor.inspector(); - inspector.geth_builder().geth_prestate_traces( - &result_and_state, - prestate_config, - db, - ) - }; - match frame_result { - Ok(frame) => { - let final_frame = if prestate_config.is_diff_mode() { - let db = executor.evm.db(); - add_accessed_unchanged_accounts( - frame, - &result_and_state.state, - db, - ) - } else { - frame - }; - GethTrace::from(final_frame) - } - Err(e) => return Err(frame_build_error("PreState trace failed", e)), - } - } - TracerKind::FlatCall(_) => { - let info = tx_info_at(block, tx, index); - // Taking the inspector (rather than cloning its recorded trace - // arena) doubles as the per-tx reset below. - let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = - core::mem::replace(executor.inspector_mut(), tracer.create_inspector()) - .with_transaction_gas_limit(tx.inner.gas_limit()) - .into_parity_builder() - .into_localized_transaction_traces(info); - GethTrace::from(frame) - } - TracerKind::Default(opts) => { - let gas_used = result_and_state.result.tx_gas_used(); - let return_value = - result_and_state.result.output().cloned().unwrap_or_default(); - let inspector = executor.inspector_mut(); - inspector.set_transaction_gas_limit(tx.inner.gas_limit()); - let frame = inspector.geth_builder().geth_traces( - gas_used, - return_value, - opts.config, - ); - // alloy-rpc-types-trace 2.x serializes `returnValue` with a "0x" - // prefix on both sides, so the frame is emitted as-is (the 1.x - // era needed a prefix-stripping shim to match mega-reth). - GethTrace::from(frame) - } - }; + let trace_result = extract_trace_frame!( + executor, + tracer, + result_and_state, + tx.inner.gas_limit(), + tx_info_at(block, tx, index) + )?; results.push(TraceResult::Success { result: trace_result, tx_hash: Some(tx_hash) }); - *executor.inspector_mut() = tracer.create_inspector(); + // The FlatCall arm already left a fresh inspector behind when it took + // the recorded one. + if !matches!(tracer, TracerKind::FlatCall(_)) { + *executor.inspector_mut() = tracer.create_inspector(); + } executor.evm.db_mut().commit(result_and_state.state); } Err(e) => return Err(tx_replay_failure(index, tx_hash, e)), @@ -434,8 +445,8 @@ fn trace_block_with_tracing_inspector( /// Traces a single transaction using a `TracingInspector`-based tracer. /// -/// Creates executor, applies pre-execution, replays preceding txs, resets -/// inspector, runs the target tx, and extracts the trace. +/// Creates executor, applies pre-execution, replays preceding txs (without +/// recording), installs the real tracer, runs the target tx, and extracts the trace. fn trace_tx_with_tracing_inspector( env: &TracingEnv<'_>, block: &Block, @@ -445,7 +456,9 @@ fn trace_tx_with_tracing_inspector( tx_index: usize, tracer: &TracerKind, ) -> Result { - setup_executor!(env, state, tracer.create_inspector() => executor); + // Preceding transactions only rebuild state; a none-config inspector skips + // recording their every step just to discard it. + setup_executor!(env, state, TracingInspector::new(TracingInspectorConfig::none()) => executor); replay_preceding_txs!(executor, env, tx_index); *executor.inspector_mut() = tracer.create_inspector(); @@ -457,64 +470,14 @@ fn trace_tx_with_tracing_inspector( let outcome = executor.run_transaction(recovered_target).map_err(ValidationError::BlockReplayFailed)?; - match tracer { - TracerKind::Call(call_config) => { - let gas_used = outcome.inner.result.tx_gas_used(); - let inspector = executor.inspector_mut(); - inspector.set_transaction_gas_limit(tx_gas_limit); - let frame = inspector.geth_builder().geth_call_traces(*call_config, gas_used); - Ok(frame.into()) - } - TracerKind::PreState(prestate_config) => { - let result_and_state = outcome.inner.result_and_state; - - executor.inspector_mut().set_transaction_gas_limit(tx_gas_limit); - let frame_result = { - let db = executor.evm.db(); - let inspector = executor.inspector(); - inspector.geth_builder().geth_prestate_traces( - &result_and_state, - prestate_config, - db, - ) - }; - - match frame_result { - Ok(frame) => { - let final_frame = if prestate_config.is_diff_mode() { - let db = executor.evm.db(); - add_accessed_unchanged_accounts(frame, &result_and_state.state, db) - } else { - frame - }; - Ok(final_frame.into()) - } - Err(e) => Err(frame_build_error("PreState trace failed", e).into()), - } - } - TracerKind::FlatCall(_) => { - let info = tx_info_at(block, target_tx, tx_index); - // Take the inspector rather than cloning its recorded trace arena; this - // executor serves no further transactions. - let frame: alloy_rpc_types_trace::geth::call::FlatCallFrame = - core::mem::replace(executor.inspector_mut(), tracer.create_inspector()) - .with_transaction_gas_limit(tx_gas_limit) - .into_parity_builder() - .into_localized_transaction_traces(info); - Ok(frame.into()) - } - TracerKind::Default(opts) => { - let gas_used = outcome.inner.result.tx_gas_used(); - let return_value = outcome.inner.result.output().cloned().unwrap_or_default(); - - let inspector = executor.inspector_mut(); - inspector.set_transaction_gas_limit(tx_gas_limit); - let frame = inspector.geth_builder().geth_traces(gas_used, return_value, opts.config); - // `returnValue` is emitted as-is; see the block-level match for why no - // prefix shim is needed on alloy-rpc-types-trace 2.x. - Ok(frame.into()) - } - } + extract_trace_frame!( + executor, + tracer, + outcome.inner.result_and_state, + tx_gas_limit, + tx_info_at(block, target_tx, tx_index) + ) + .map_err(TraceError::from) } // Public API - Geth-style Tracing @@ -554,10 +517,12 @@ pub fn trace_block( .collect(), // Strict parse in `builtin_tracer_kind` — see its doc for why the - // executor re-rejects what the boundary gate already screens. + // executor re-rejects what the boundary gate already screens. The + // unsupported erc7562Tracer errors there before any execution. GethDebugBuiltInTracerType::CallTracer | GethDebugBuiltInTracerType::PreStateTracer | - GethDebugBuiltInTracerType::FlatCallTracer => trace_block_with_tracing_inspector( + GethDebugBuiltInTracerType::FlatCallTracer | + GethDebugBuiltInTracerType::Erc7562Tracer => trace_block_with_tracing_inspector( &env, block, &mut state, @@ -590,7 +555,8 @@ pub fn trace_block( } GethDebugBuiltInTracerType::MuxTracer => { - let (mux_config, inspector) = mux_config_and_inspector(tracer_config)?; + let inspector = mux_inspector(tracer_config)?; + let template = inspector.clone(); setup_executor!(&env, &mut state, inspector => executor); @@ -611,8 +577,7 @@ pub fn trace_block( inspector.try_into_mux_frame(&result_and_state, db, info) }; - *executor.inspector_mut() = - MuxInspector::try_from_config(mux_config.clone()).unwrap(); + *executor.inspector_mut() = template.clone(); executor.evm.db_mut().commit(result_and_state.state); match mux_result { @@ -639,13 +604,6 @@ pub fn trace_block( } results } - - // Unreachable in practice: RPC handlers reject unsupported tracers via - // `RequestShape::classify` (-32602) before reaching the executor. Kept - // as a backstop so the match stays exhaustive. - GethDebugBuiltInTracerType::Erc7562Tracer => { - return Err(request_error("Unsupported tracer", "erc7562Tracer")); - } }, GethDebugTracerType::JsTracer(code) => { @@ -766,7 +724,6 @@ pub fn trace_transaction( let mut state = State::builder().with_database_ref(&cache_db).build(); let GethDebugTracingOptions { tracer, tracer_config, .. } = &opts; - let target_tx = &env.transactions[tx_index]; let recovered_target = &target_tx.inner.inner; let info = tx_info_at(block, target_tx, tx_index); @@ -778,10 +735,12 @@ pub fn trace_transaction( } // Strict parse in `builtin_tracer_kind` — see its doc for why the - // executor re-rejects what the boundary gate already screens. + // executor re-rejects what the boundary gate already screens. The + // unsupported erc7562Tracer errors there before any execution. GethDebugBuiltInTracerType::CallTracer | GethDebugBuiltInTracerType::PreStateTracer | - GethDebugBuiltInTracerType::FlatCallTracer => trace_tx_with_tracing_inspector( + GethDebugBuiltInTracerType::FlatCallTracer | + GethDebugBuiltInTracerType::Erc7562Tracer => trace_tx_with_tracing_inspector( &env, block, &mut state, @@ -803,12 +762,13 @@ pub fn trace_transaction( } GethDebugBuiltInTracerType::MuxTracer => { - let (mux_config, inspector) = mux_config_and_inspector(tracer_config)?; + let inspector = mux_inspector(tracer_config)?; + let template = inspector.clone(); setup_executor!(&env, &mut state, inspector => executor); replay_preceding_txs!(executor, &env, tx_index); - *executor.inspector_mut() = mux_inspector(mux_config)?; + *executor.inspector_mut() = template; let outcome = executor .run_transaction(recovered_target) @@ -824,11 +784,6 @@ pub fn trace_transaction( .map(|frame| frame.into()) .map_err(|e| frame_build_error("MuxFrame creation failed", e).into()) } - - // Unreachable backstop; see the block-level match for rationale. - GethDebugBuiltInTracerType::Erc7562Tracer => { - Err(request_error("Unsupported tracer", "erc7562Tracer")) - } }, GethDebugTracerType::JsTracer(code) => { @@ -935,7 +890,9 @@ pub fn parity_trace_transaction( let cache_db = CacheDB::new(&witness_db); let mut state = State::builder().with_database_ref(&cache_db).build(); - let inspector = TracingInspector::new(TracingInspectorConfig::default_parity()); + // Preceding transactions only rebuild state; a none-config inspector skips + // recording their every step just to discard it. + let inspector = TracingInspector::new(TracingInspectorConfig::none()); setup_executor!(&env, &mut state, inspector => executor); replay_preceding_txs!(executor, &env, tx_index); diff --git a/crates/stateless-core/src/withdrawals.rs b/crates/stateless-core/src/withdrawals.rs index 28672329..4b331ff7 100644 --- a/crates/stateless-core/src/withdrawals.rs +++ b/crates/stateless-core/src/withdrawals.rs @@ -217,22 +217,14 @@ impl Linearizer<'_> { TrieNode::Leaf(leaf) => { let full = path.join(&leaf.key); // The leaf is the region's only occupant: a removal of any other path - // is a proven no-op, a write at `full` supersedes the leaf. - let mut finals: Vec<(Nibbles, &[u8])> = Vec::with_capacity(updates.len() + 1); - let mut survivor = Some((full, leaf.value.as_slice())); - for (update_path, update) in updates { - if *update_path == full { - survivor = None; - } - if let Some(value) = update { - finals.push((*update_path, value)); - } - } - finals.extend(survivor); - finals.sort_unstable_by_key(|(path, _)| *path); - for (leaf_path, value) in finals { - hb.add_leaf(leaf_path, value); + // is a proven no-op, and a write at `full` supersedes the leaf (its + // replacement, if any, is the first entry of `rest`). + let (before, rest) = updates.split_at(updates.partition_point(|(p, _)| p < &full)); + add_insert_leaves(hb, before); + if rest.first().is_none_or(|(p, _)| p != &full) { + hb.add_leaf(full, &leaf.value); } + add_insert_leaves(hb, rest); Ok(()) } TrieNode::Extension(ext) => { @@ -395,18 +387,14 @@ fn add_insert_leaves(hb: &mut HashBuilder, updates: &Updates) { /// Splits sorted `updates` around the region prefixed by `prefix`: /// `(diverging before, under prefix, diverging after)`. /// -/// Sound because for 64-nibble update paths and a shorter `prefix`, every path -/// under the prefix compares greater than any diverging-lower path and smaller -/// than any diverging-higher path, so the region is one contiguous run. +/// The prefixed region is one contiguous run in sorted order, so two partition +/// points delimit it. fn split_at_prefix<'u>( updates: &'u Updates, prefix: &Nibbles, ) -> (&'u Updates, &'u Updates, &'u Updates) { let lo = updates.partition_point(|(p, _)| p < prefix); - let mut hi = lo; - while hi < updates.len() && updates[hi].0.starts_with(prefix) { - hi += 1; - } + let hi = lo + updates[lo..].partition_point(|(p, _)| p.starts_with(prefix)); (&updates[..lo], &updates[lo..hi], &updates[hi..]) } From 2f90a7b3ed7d14f81e2c55d9e35619f95c4990fd Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Wed, 12 Aug 2026 15:38:10 +0800 Subject: [PATCH 13/13] fix(db): self-validate CONTRACTS rows on read instead of erroring revm-bytecode 11 reordered Bytecode's serde enum, so rows written by pre-upgrade binaries no longer decode (or, for 19-byte codes, silently mis-decode into EIP-7702 delegations to garbage addresses). Treat decode failures and keccak mismatches as cache misses: both binaries re-fetch misses through the hash-verified RPC tier and overwrite the stale row, so an in-place upgrade degrades to a one-time re-fetch instead of halting the node. Regression tests pin real revm-bytecode 6.2.2 row encodings for both failure shapes. --- AGENTS.md | 2 +- Cargo.lock | 1 + README.md | 1 + bin/debug-trace-server/src/server_db.rs | 6 +- bin/stateless-validator/src/validator_db.rs | 16 +- crates/stateless-db/Cargo.toml | 1 + crates/stateless-db/src/cache.rs | 4 +- crates/stateless-db/src/helpers.rs | 153 +++++++++++++++++++- 8 files changed, 167 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 40729c1c..08c3a92f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ The pipeline owns the reorg seams: `DivergenceLookups` (the bisection contract) - **`ANCHOR_BLOCK`** — Trusted starting point (block number, hash, state root, withdrawals root). - **`CANONICAL_CHAIN`** — Validated chain progression (block number → hash, state root, withdrawals root); a bounded contiguous window in both binaries, and exactly what reorg bisection trusts. -- **`CONTRACTS`** — Persistent tier of the contract bytecode cache (code hash → bincode+lz4 bytecode). The in-memory tier is the bounded `ContractCache` on top. +- **`CONTRACTS`** — Persistent tier of the contract bytecode cache (code hash → bincode+lz4 bytecode). The in-memory tier is the bounded `ContractCache` on top. Rows are self-validating on read: decode failures and keccak mismatches surface as misses (re-fetched from RPC and overwritten), not errors, so encoding drift across upgrades never bricks a data dir. - **`GENESIS_CONFIG`** — Hardfork activation rules (validator only). - **`BLOCK_DATA`** — Full block content (trace server only). - **`WITNESSES`** — Light witness data (trace server only). diff --git a/Cargo.lock b/Cargo.lock index dfdae966..f9394138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5851,6 +5851,7 @@ dependencies = [ "serde_json", "stateless-core", "tempfile", + "tracing", ] [[package]] diff --git a/README.md b/README.md index 243e265c..04f27190 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,7 @@ The validator and trace server each have their own `redb`-backed database, shari | `BLOCK_RECORDS` | `(BlockNumber, BlockHash)` | `()` | Trace server (pruning index) | In both binaries, `CANONICAL_CHAIN` is a bounded contiguous window — exactly the range reorg bisection trusts. +`CONTRACTS` rows are self-validating on read: an entry that fails to decode or whose bytecode no longer hashes back to its key (disk corruption, or a `Bytecode` encoding change in a dependency upgrade) is treated as a cache miss and transparently re-fetched from RPC, so existing data dirs survive release upgrades without manual clearing. ### SALT Witness Cryptography diff --git a/bin/debug-trace-server/src/server_db.rs b/bin/debug-trace-server/src/server_db.rs index deb7d39f..a8500e52 100644 --- a/bin/debug-trace-server/src/server_db.rs +++ b/bin/debug-trace-server/src/server_db.rs @@ -428,9 +428,11 @@ mod tests { fn test_server_db_contract_codes() { let (_dir, db) = temp_server_db(); - let hash1 = B256::from([1u8; 32]); - let hash2 = B256::from([2u8; 32]); let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); + // The read path re-checks that stored bytecode hashes back to its key, so the + // stored entry must be keyed by its real code hash. + let hash1 = bytecode.hash_slow(); + let hash2 = B256::from([2u8; 32]); ContractStore::add_contracts(&db, &[(hash1, bytecode.clone())]).unwrap(); diff --git a/bin/stateless-validator/src/validator_db.rs b/bin/stateless-validator/src/validator_db.rs index 0545b68b..f95e797b 100644 --- a/bin/stateless-validator/src/validator_db.rs +++ b/bin/stateless-validator/src/validator_db.rs @@ -220,13 +220,15 @@ mod tests { fn test_contracts_roundtrip() { let (_dir, store) = temp_store(); - let hash1 = B256::from([1u8; 32]); - let hash2 = B256::from([2u8; 32]); - let hash3 = B256::from([3u8; 32]); - let bytecode1 = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); let bytecode2 = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x01])); + // The read path re-checks that stored bytecode hashes back to its key, so + // stored entries must be keyed by their real code hashes. + let hash1 = bytecode1.hash_slow(); + let hash2 = bytecode2.hash_slow(); + let hash3 = B256::from([3u8; 32]); + store.add_contracts(&[(hash1, bytecode1.clone()), (hash2, bytecode2.clone())]).unwrap(); let (found, missing) = store.get_contracts(&[hash1, hash2, hash3]).unwrap(); @@ -276,8 +278,8 @@ mod tests { let (_dir, store) = temp_store(); let cache = ContractCache::new(Arc::new(store)); - let hash = B256::from([1u8; 32]); let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); + let hash = bytecode.hash_slow(); cache.insert(&[(hash, bytecode.clone())]).unwrap(); @@ -292,8 +294,10 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let db_path = dir.path().join("test.redb"); - let hash = B256::from([1u8; 32]); let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); + // The disk tier re-checks that stored bytecode hashes back to its key, so the + // stored entry must be keyed by its real code hash. + let hash = bytecode.hash_slow(); { let store = ValidatorDB::new(&db_path).unwrap(); diff --git a/crates/stateless-db/Cargo.toml b/crates/stateless-db/Cargo.toml index 4ab65c80..a7778aa2 100644 --- a/crates/stateless-db/Cargo.toml +++ b/crates/stateless-db/Cargo.toml @@ -29,6 +29,7 @@ quick_cache.workspace = true redb.workspace = true serde.workspace = true serde_json.workspace = true +tracing.workspace = true [dev-dependencies] # misc diff --git a/crates/stateless-db/src/cache.rs b/crates/stateless-db/src/cache.rs index a05b3e58..afcdaeb0 100644 --- a/crates/stateless-db/src/cache.rs +++ b/crates/stateless-db/src/cache.rs @@ -121,7 +121,9 @@ impl ContractCache { /// /// Returns `(found, missing)`. Memory hits are trusted and not re-verified; the /// caller should use a verified RPC fetch (`RpcClient::get_codes(.., verify=true)`) - /// to populate the cache so all entries arrive pre-verified. + /// to populate the cache so all entries arrive pre-verified. The disk tier + /// self-validates on read: a stale or corrupt row surfaces as missing (see + /// `read_contracts`), and the caller's re-fetch + insert overwrites it. pub fn get(&self, hashes: &[B256]) -> StoreResult { let mut found: HashMap = HashMap::default(); found.reserve(hashes.len()); diff --git a/crates/stateless-db/src/helpers.rs b/crates/stateless-db/src/helpers.rs index 2b8d3c14..143d63f2 100644 --- a/crates/stateless-db/src/helpers.rs +++ b/crates/stateless-db/src/helpers.rs @@ -4,6 +4,7 @@ use alloy_primitives::{B256, BlockHash, BlockNumber, map::HashMap}; use redb::{ReadableDatabase, ReadableTable}; use revm::state::Bytecode; use stateless_core::db::{BlockMeta, ContractLookup, StoreResult, StoreResultExt}; +use tracing::warn; use crate::{ serialize::{decode_from_slice, encode_to_vec}, @@ -174,6 +175,13 @@ pub fn write_reset_to_anchor(database: &Database, anchor: &BlockMeta) -> StoreRe } /// Retrieves cached contract bytecodes. Returns `(found, missing)`. +/// +/// The disk tier is self-validating: a row that no longer decodes (serde layout drift in +/// an upgraded dependency, on-disk corruption) or whose decoded bytecode no longer hashes +/// back to its key is reported as missing instead of failing the lookup. Both binaries +/// re-fetch misses through the hash-verified RPC tier and write the result back over the +/// stale row, so such rows degrade to a one-time re-fetch instead of halting the node. +/// Storage-engine errors still propagate. pub fn read_contracts(database: &Database, hashes: &[B256]) -> StoreResult { let read_txn = database.begin_read().store_err()?; let Some(table) = open_read_table(&read_txn, CONTRACTS)? else { @@ -184,12 +192,29 @@ pub fn read_contracts(database: &Database, hashes: &[B256]) -> StoreResult { - let bytecode: Bytecode = decode_from_slice(data.value().as_slice())?; + let Some(data) = table.get(hash.0).store_err()? else { + missing.push(hash); + continue; + }; + match decode_from_slice::(data.value().as_slice()) { + Ok(bytecode) if bytecode.hash_slow() == hash => { found.insert(hash, bytecode); } - None => missing.push(hash), + Ok(_) => { + warn!( + code_hash = %hash, + "stored contract bytecode does not hash back to its key; treating as a miss" + ); + missing.push(hash); + } + Err(error) => { + warn!( + code_hash = %hash, + %error, + "stored contract bytecode failed to decode; treating as a miss" + ); + missing.push(hash); + } } } @@ -215,9 +240,10 @@ pub fn write_add_contracts(database: &Database, codes: &[(B256, Bytecode)]) -> S #[cfg(test)] mod tests { - use alloy_primitives::Bytes; + use alloy_primitives::{Bytes, b256, keccak256}; use super::*; + use crate::serialize::BINCODE_LZ4_MARKER; fn temp_db() -> (tempfile::TempDir, Database) { let dir = tempfile::tempdir().unwrap(); @@ -287,12 +313,18 @@ mod tests { assert_eq!(read_block_hash(&db, 5).unwrap(), None); } + /// Keyed the same way the write path verifies: `Bytecode::hash_slow()`. + fn contract_entry(code: &'static [u8]) -> (B256, Bytecode) { + let bytecode = Bytecode::new_raw(Bytes::from_static(code)); + (bytecode.hash_slow(), bytecode) + } + #[test] fn contracts_roundtrip_and_missing_report() { let (_dir, db) = temp_db(); - let a = (B256::from([1u8; 32]), Bytecode::new_raw(Bytes::from_static(&[0x60]))); - let b = (B256::from([2u8; 32]), Bytecode::new_raw(Bytes::from_static(&[0x61]))); + let a = contract_entry(&[0x60]); + let b = contract_entry(&[0x61]); let missing_hash = B256::from([3u8; 32]); write_add_contracts(&db, &[]).unwrap(); @@ -304,4 +336,111 @@ mod tests { assert_eq!(found[&a.0].bytes_slice(), a.1.bytes_slice()); assert_eq!(found[&b.0].bytes_slice(), b.1.bytes_slice()); } + + /// Inserts a raw pre-encoded row into CONTRACTS, bypassing `write_add_contracts` — + /// stands in for a row written by an older binary (or corrupted on disk). + fn insert_raw_contract_row(db: &Database, key: B256, row: Vec) { + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(CONTRACTS).unwrap(); + table.insert(key.0, row).unwrap(); + } + write_txn.commit().unwrap(); + } + + /// Wraps raw bincode bytes into the stored row format (marker byte + lz4), exactly as + /// `encode_to_vec` would, without re-encoding the payload. + fn to_stored_row(bincode_bytes: &[u8]) -> Vec { + let mut row = vec![BINCODE_LZ4_MARKER]; + row.extend(lz4_flex::compress_prepend_size(bincode_bytes)); + row + } + + // The two fixtures below are `bincode::serde::encode_to_vec(&Bytecode::new_raw(code), + // bincode::config::standard())` captured under revm-bytecode 6.2.2 (revm 27.x), whose + // derived serde enum was ordered `Eip7702 = 0, LegacyAnalyzed = 1`. revm-bytecode 11.x + // serializes through `BytecodeSerde`, ordered `LegacyAnalyzed = 0, Eip7702 = 1`, so + // rows persisted by pre-upgrade binaries carry the wrong variant tag today. + + /// revm-bytecode 6.2.2 encoding of `Bytecode::new_raw(&[0x60, 0x80, 0x60, 0x40, 0x52])`. + /// Under 11.x its `0x01` tag selects `Eip7702`, whose 20-byte address read fails against + /// the 6-byte (stop-padded) bytecode field: the typical decode-failure shape. + const REVM6_ROW_TYPICAL: &[u8] = &[ + 0x01, 0x06, 0x60, 0x80, 0x60, 0x40, 0x52, 0x00, 0x05, 0x13, 0x62, 0x69, 0x74, 0x76, 0x65, + 0x63, 0x3a, 0x3a, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x3a, 0x4c, 0x73, 0x62, 0x30, 0x08, + 0x00, 0x05, 0x01, 0x00, + ]; + const REVM6_ROW_TYPICAL_CODE: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52]; + + /// revm-bytecode 6.2.2 encoding of `Bytecode::new_raw(&[0x5f; 19])`. The 19-byte code is + /// stop-padded to a 20-byte field, so under 11.x it decodes *successfully* into an + /// `Eip7702` delegation to a garbage address — only the hash re-check catches it. + const REVM6_ROW_MISDECODES: &[u8] = &[ + 0x01, 0x14, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, + 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x00, 0x13, 0x13, 0x62, 0x69, 0x74, 0x76, 0x65, 0x63, + 0x3a, 0x3a, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x3a, 0x4c, 0x73, 0x62, 0x30, 0x08, 0x00, + 0x13, 0x03, 0x00, 0x00, 0x00, + ]; + const REVM6_ROW_MISDECODES_CODE: &[u8] = &[0x5f; 19]; + + #[test] + fn stale_revm6_contract_row_is_a_miss_not_an_error() { + let (_dir, db) = temp_db(); + + // The pre-upgrade key convention (6.2.2 `hash_slow()`, captured alongside the + // fixture) is still plain keccak256 of the original bytecode — the key itself + // survives the upgrade, only the value layout drifted. + let hash = keccak256(REVM6_ROW_TYPICAL_CODE); + assert_eq!(hash, b256!("1c3374235d773b2189aed115aa13143020fcdbbe86e38f358cf3e4771b2f0244")); + let row = to_stored_row(REVM6_ROW_TYPICAL); + assert!( + decode_from_slice::(&row).is_err(), + "fixture must fail to decode under the current Bytecode serde layout", + ); + insert_raw_contract_row(&db, hash, row); + + let (found, missing) = read_contracts(&db, &[hash]).unwrap(); + assert!(found.is_empty()); + assert_eq!(missing, vec![hash], "stale row must surface as a miss, not an error"); + + // The caller's miss handling re-fetches and writes back; the row then heals. + let refetched = Bytecode::new_raw(Bytes::from_static(REVM6_ROW_TYPICAL_CODE)); + write_add_contracts(&db, &[(hash, refetched.clone())]).unwrap(); + let (found, missing) = read_contracts(&db, &[hash]).unwrap(); + assert!(missing.is_empty()); + assert_eq!(found[&hash].bytes_slice(), refetched.bytes_slice()); + } + + #[test] + fn stale_revm6_contract_row_misdecoding_as_eip7702_is_a_miss() { + let (_dir, db) = temp_db(); + + let hash = keccak256(REVM6_ROW_MISDECODES_CODE); + let row = to_stored_row(REVM6_ROW_MISDECODES); + // This row *does* decode under the current layout — into an EIP-7702 delegation + // to a garbage address — so only the hash re-check can reject it. + let misdecoded = decode_from_slice::(&row) + .expect("fixture must silently decode under the current Bytecode serde layout"); + assert_ne!(misdecoded.hash_slow(), hash); + insert_raw_contract_row(&db, hash, row); + + let (found, missing) = read_contracts(&db, &[hash]).unwrap(); + assert!(found.is_empty()); + assert_eq!(missing, vec![hash], "mis-decoded row must surface as a miss"); + } + + #[test] + fn hash_mismatched_contract_row_is_a_miss() { + let (_dir, db) = temp_db(); + + // A row that decodes fine but was stored under the wrong key (key corruption, or + // any future silent mis-decode) is rejected by the hash re-check. + let wrong_key = B256::from([7u8; 32]); + let bytecode = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00])); + write_add_contracts(&db, &[(wrong_key, bytecode)]).unwrap(); + + let (found, missing) = read_contracts(&db, &[wrong_key]).unwrap(); + assert!(found.is_empty()); + assert_eq!(missing, vec![wrong_key]); + } }