diff --git a/.config/nextest.toml b/.config/nextest.toml index 26b4a000b9..7aab60242c 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -15,3 +15,7 @@ slow-timeout = { period = "2m", terminate-after = 10 } [[profile.default.overrides]] filter = "binary(e2e_testsuite)" slow-timeout = { period = "2m", terminate-after = 3 } + +[[profile.default.overrides]] +filter = "package(reth-era) and binary(it)" +slow-timeout = { period = "2m", terminate-after = 10 } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eed64b157f..5e334d13c6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,5 +40,6 @@ crates/tasks/ @mattsse crates/tokio-util/ @fgimenez crates/transaction-pool/ @mattsse @yongkangc crates/trie/ @Rjected @shekhirin @mediocregopher +bin/reth-bench-compare/ @mediocregopher @shekhirin @yongkangc etc/ @Rjected @shekhirin .github/ @gakonst @DaniPopes diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index cbf3535218..edd8eadba8 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -78,4 +78,4 @@ jobs: with: cache-on-failure: true - name: run era1 files integration tests - run: cargo nextest run --package reth-era --test it -- --ignored + run: cargo nextest run --release --package reth-era --test it -- --ignored diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4f122da68c..e6d4ee4d50 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -92,7 +92,12 @@ jobs: run: .github/assets/check_rv32imac.sh crate-checks: + name: crate-checks (${{ matrix.partition }}/${{ matrix.total_partitions }}) runs-on: ubuntu-latest + strategy: + matrix: + partition: [1, 2] + total_partitions: [2] timeout-minutes: 30 steps: - uses: actions/checkout@v5 @@ -102,7 +107,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - - run: cargo hack check --workspace + - run: cargo hack check --workspace --partition ${{ matrix.partition }}/${{ matrix.total_partitions }} msrv: name: MSRV diff --git a/Cargo.lock b/Cargo.lock index 93b1e3e704..d64833a027 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6068f356948cd84b5ad9ac30c50478e433847f14a50714d2b68f15d052724049" +checksum = "4bc32535569185cbcb6ad5fa64d989a47bccb9a08e27284b1f2a3ccf16e6d010" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -112,8 +112,8 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-eips", "alloy-primitives", @@ -139,8 +139,8 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-eips", @@ -153,8 +153,8 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -249,8 +249,8 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -275,8 +275,8 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.23.1" -source = "git+https://github.com/Rimeeeeee/evm?branch=new-approach4#b6ebb0c9e89e9f3cf871916aa30c89d630d76592" +version = "0.24.2" +source = "git+https://github.com/Rimeeeeee/evm?branch=new-approach4#93e65625dd142a72290d53ec10fe36c8e7ab7074" dependencies = [ "alloy-consensus", "alloy-eips", @@ -288,8 +288,7 @@ dependencies = [ "alloy-sol-types", "auto_impl", "derive_more", - "op-alloy-consensus", - "op-alloy-rpc-types-engine", + "op-alloy", "op-revm", "revm", "thiserror 2.0.17", @@ -298,8 +297,8 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-eips", "alloy-primitives", @@ -338,8 +337,8 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -352,8 +351,8 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -377,8 +376,8 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-eips", @@ -389,8 +388,8 @@ dependencies = [ [[package]] name = "alloy-op-evm" -version = "0.23.1" -source = "git+https://github.com/Rimeeeeee/evm?branch=new-approach4#b6ebb0c9e89e9f3cf871916aa30c89d630d76592" +version = "0.24.2" +source = "git+https://github.com/Rimeeeeee/evm?branch=new-approach4#93e65625dd142a72290d53ec10fe36c8e7ab7074" dependencies = [ "alloy-consensus", "alloy-eips", @@ -398,7 +397,7 @@ dependencies = [ "alloy-op-hardforks", "alloy-primitives", "auto_impl", - "op-alloy-consensus", + "op-alloy", "op-revm", "revm", "thiserror 2.0.17", @@ -425,14 +424,15 @@ checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "arbitrary", + "borsh", "bytes", "cfg-if", "const-hex", "derive_more", "foldhash 0.2.0", "getrandom 0.3.4", - "hashbrown 0.16.0", - "indexmap 2.12.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -449,8 +449,8 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-chains", "alloy-consensus", @@ -493,8 +493,8 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -536,8 +536,8 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -561,8 +561,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", @@ -573,8 +573,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-admin" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -584,8 +584,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -595,8 +595,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -605,8 +605,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-eips", "alloy-primitives", @@ -624,8 +624,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "derive_more", @@ -635,8 +635,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-eips", @@ -655,8 +655,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -676,8 +676,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-mev" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-eips", @@ -690,8 +690,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -703,8 +703,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -714,8 +714,8 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "arbitrary", @@ -725,8 +725,8 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-primitives", "async-trait", @@ -739,8 +739,8 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-consensus", "alloy-network", @@ -778,7 +778,7 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.12.0", + "indexmap 2.12.1", "proc-macro-error2", "proc-macro2", "quote", @@ -827,8 +827,8 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -849,8 +849,8 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -863,8 +863,8 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -882,8 +882,8 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -919,8 +919,8 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.1.0" -source = "git+https://github.com/Soubhik-10/alloy?branch=bal#934c73a831219307679f0d9ce31e4530502d12dc" +version = "1.1.2" +source = "git+https://github.com/Soubhik-10/alloy?branch=bal#84c32ee38e42335fd6a50e5c12fc01accd1eab87" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -975,22 +975,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1702,7 +1702,7 @@ dependencies = [ "boa_interner", "boa_macros", "boa_string", - "indexmap 2.12.0", + "indexmap 2.12.1", "num-bigint", "rustc-hash", ] @@ -1732,9 +1732,9 @@ dependencies = [ "futures-channel", "futures-concurrency", "futures-lite 2.6.1", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "icu_normalizer", - "indexmap 2.12.0", + "indexmap 2.12.1", "intrusive-collections", "itertools 0.14.0", "num-bigint", @@ -1767,7 +1767,7 @@ checksum = "f1179f690cbfcbe5364cceee5f1cb577265bb6f07b0be6f210aabe270adcf9da" dependencies = [ "boa_macros", "boa_string", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "thin-vec", ] @@ -1779,8 +1779,8 @@ checksum = "9626505d33dc63d349662437297df1d3afd9d5fc4a2b3ad34e5e1ce879a78848" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.16.0", - "indexmap 2.12.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "once_cell", "phf", "rustc-hash", @@ -1953,9 +1953,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -2144,9 +2144,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -2154,9 +2154,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -2634,9 +2634,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -3203,7 +3203,7 @@ dependencies = [ [[package]] name = "ef-test-runner" -version = "1.9.2" +version = "1.9.3" dependencies = [ "clap", "ef-tests", @@ -3211,7 +3211,7 @@ dependencies = [ [[package]] name = "ef-tests" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -3690,7 +3690,7 @@ dependencies = [ [[package]] name = "example-full-contract-state" -version = "1.9.2" +version = "1.9.3" dependencies = [ "eyre", "reth-ethereum", @@ -3829,7 +3829,7 @@ dependencies = [ [[package]] name = "exex-subscription" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "clap", @@ -4191,9 +4191,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -4346,7 +4346,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -4404,14 +4404,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -4622,9 +4623,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -4677,9 +4678,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -4907,13 +4908,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -4965,9 +4966,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.2" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "e8732d3774162a0851e3f2b150eb98f31a9885dd75985099421d393385a01dfd" dependencies = [ "console", "once_cell", @@ -5685,7 +5686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "metrics", "metrics-util", "quanta", @@ -5717,7 +5718,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.15.5", - "indexmap 2.12.0", + "indexmap 2.12.1", "metrics", "ordered-float", "quanta", @@ -6143,10 +6144,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] -name = "op-alloy-consensus" -version = "0.22.1" +name = "op-alloy" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d7ec388eb83a3e6c71774131dbbb2ba9c199b6acac7dce172ed8de2f819e91" +checksum = "c3b13412d297c1f9341f678b763750b120a73ffe998fa54a94d6eda98449e7ca" +dependencies = [ + "op-alloy-consensus", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", +] + +[[package]] +name = "op-alloy-consensus" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726da827358a547be9f1e37c2a756b9e3729cb0350f43408164794b370cad8ae" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6170,9 +6184,9 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" -version = "0.22.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "979fe768bbb571d1d0bd7f84bc35124243b4db17f944b94698872a4701e743a0" +checksum = "f63f27e65be273ec8fcb0b6af0fd850b550979465ab93423705ceb3dfddbd2ab" dependencies = [ "alloy-consensus", "alloy-network", @@ -6185,10 +6199,25 @@ dependencies = [ ] [[package]] -name = "op-alloy-rpc-jsonrpsee" -version = "0.22.1" +name = "op-alloy-provider" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdbb3c0453fe2605fb008851ea0b45f3f2ba607722c9f2e4ffd7198958ce501" +checksum = "a71456699aa256dc20119736422ad9a44da8b9585036117afb936778122093b9" +dependencies = [ + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-engine", + "alloy-transport", + "async-trait", + "op-alloy-rpc-types-engine", +] + +[[package]] +name = "op-alloy-rpc-jsonrpsee" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef9114426b16172254555aad34a8ea96c01895e40da92f5d12ea680a1baeaa7" dependencies = [ "alloy-primitives", "jsonrpsee", @@ -6196,9 +6225,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.22.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc252b5fa74dbd33aa2f9a40e5ff9cfe34ed2af9b9b235781bc7cc8ec7d6aca8" +checksum = "562dd4462562c41f9fdc4d860858c40e14a25df7f983ae82047f15f08fce4d19" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6216,9 +6245,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" -version = "0.22.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1abe694cd6718b8932da3f824f46778be0f43289e4103c88abc505c63533a04" +checksum = "d8f24b8cb66e4b33e6c9e508bf46b8ecafc92eadd0b93fedd306c0accb477657" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6238,7 +6267,7 @@ dependencies = [ [[package]] name = "op-reth" -version = "1.9.2" +version = "1.9.3" dependencies = [ "clap", "reth-cli-util", @@ -6486,9 +6515,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", @@ -7254,7 +7283,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", "memchr", ] @@ -7309,13 +7338,13 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-rpc-types", "aquamarine", @@ -7362,7 +7391,7 @@ dependencies = [ [[package]] name = "reth-basic-payload-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7385,7 +7414,7 @@ dependencies = [ [[package]] name = "reth-bench" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-json-rpc", @@ -7424,7 +7453,7 @@ dependencies = [ [[package]] name = "reth-bench-compare" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-provider", @@ -7450,7 +7479,7 @@ dependencies = [ [[package]] name = "reth-chain-state" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7482,7 +7511,7 @@ dependencies = [ [[package]] name = "reth-chainspec" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7502,7 +7531,7 @@ dependencies = [ [[package]] name = "reth-cli" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-genesis", "clap", @@ -7515,7 +7544,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7597,7 +7626,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" -version = "1.9.2" +version = "1.9.3" dependencies = [ "reth-tasks", "tokio", @@ -7606,7 +7635,7 @@ dependencies = [ [[package]] name = "reth-cli-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7626,7 +7655,7 @@ dependencies = [ [[package]] name = "reth-codecs" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7650,7 +7679,7 @@ dependencies = [ [[package]] name = "reth-codecs-derive" -version = "1.9.2" +version = "1.9.3" dependencies = [ "proc-macro2", "quote", @@ -7660,7 +7689,7 @@ dependencies = [ [[package]] name = "reth-config" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "eyre", @@ -7669,6 +7698,7 @@ dependencies = [ "reth-network-types", "reth-prune-types", "reth-stages-types", + "reth-static-file-types", "serde", "tempfile", "toml", @@ -7677,7 +7707,7 @@ dependencies = [ [[package]] name = "reth-consensus" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7689,7 +7719,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7705,7 +7735,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7730,7 +7760,7 @@ dependencies = [ [[package]] name = "reth-db" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7764,7 +7794,7 @@ dependencies = [ [[package]] name = "reth-db-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -7794,7 +7824,7 @@ dependencies = [ [[package]] name = "reth-db-common" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -7824,7 +7854,7 @@ dependencies = [ [[package]] name = "reth-db-models" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7840,7 +7870,7 @@ dependencies = [ [[package]] name = "reth-discv4" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7866,7 +7896,7 @@ dependencies = [ [[package]] name = "reth-discv5" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7891,7 +7921,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-primitives", @@ -7919,7 +7949,7 @@ dependencies = [ [[package]] name = "reth-downloaders" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7957,7 +7987,7 @@ dependencies = [ [[package]] name = "reth-e2e-test-utils" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8014,7 +8044,7 @@ dependencies = [ [[package]] name = "reth-ecies" -version = "1.9.2" +version = "1.9.3" dependencies = [ "aes", "alloy-primitives", @@ -8042,7 +8072,7 @@ dependencies = [ [[package]] name = "reth-engine-local" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8065,7 +8095,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8089,7 +8119,7 @@ dependencies = [ [[package]] name = "reth-engine-service" -version = "1.9.2" +version = "1.9.3" dependencies = [ "futures", "pin-project", @@ -8118,7 +8148,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8189,7 +8219,7 @@ dependencies = [ [[package]] name = "reth-engine-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -8217,7 +8247,7 @@ dependencies = [ [[package]] name = "reth-era" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8239,7 +8269,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "bytes", @@ -8247,6 +8277,7 @@ dependencies = [ "futures", "futures-util", "reqwest", + "reth-era", "reth-fs-util", "sha2", "tempfile", @@ -8256,7 +8287,7 @@ dependencies = [ [[package]] name = "reth-era-utils" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8282,7 +8313,7 @@ dependencies = [ [[package]] name = "reth-errors" -version = "1.9.2" +version = "1.9.3" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -8292,7 +8323,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -8330,7 +8361,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -8355,7 +8386,7 @@ dependencies = [ [[package]] name = "reth-ethereum" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", @@ -8395,7 +8426,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" -version = "1.9.2" +version = "1.9.3" dependencies = [ "clap", "eyre", @@ -8417,7 +8448,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8434,7 +8465,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8452,7 +8483,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -8465,7 +8496,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8493,7 +8524,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8520,7 +8551,7 @@ dependencies = [ [[package]] name = "reth-etl" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "rayon", @@ -8530,7 +8561,7 @@ dependencies = [ [[package]] name = "reth-evm" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8554,7 +8585,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8579,7 +8610,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-evm", "alloy-primitives", @@ -8591,7 +8622,7 @@ dependencies = [ [[package]] name = "reth-execution-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8611,7 +8642,7 @@ dependencies = [ [[package]] name = "reth-exex" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8655,7 +8686,7 @@ dependencies = [ [[package]] name = "reth-exex-test-utils" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "eyre", @@ -8686,7 +8717,7 @@ dependencies = [ [[package]] name = "reth-exex-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8703,7 +8734,7 @@ dependencies = [ [[package]] name = "reth-fs-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "serde", "serde_json", @@ -8712,7 +8743,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8745,7 +8776,7 @@ dependencies = [ [[package]] name = "reth-ipc" -version = "1.9.2" +version = "1.9.3" dependencies = [ "bytes", "futures", @@ -8767,7 +8798,7 @@ dependencies = [ [[package]] name = "reth-libmdbx" -version = "1.9.2" +version = "1.9.3" dependencies = [ "bitflags 2.10.0", "byteorder", @@ -8785,7 +8816,7 @@ dependencies = [ [[package]] name = "reth-mdbx-sys" -version = "1.9.2" +version = "1.9.3" dependencies = [ "bindgen 0.71.1", "cc", @@ -8793,7 +8824,7 @@ dependencies = [ [[package]] name = "reth-metrics" -version = "1.9.2" +version = "1.9.3" dependencies = [ "futures", "metrics", @@ -8804,14 +8835,15 @@ dependencies = [ [[package]] name = "reth-net-banlist" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", + "ipnet", ] [[package]] name = "reth-net-nat" -version = "1.9.2" +version = "1.9.3" dependencies = [ "futures-util", "if-addrs", @@ -8825,7 +8857,7 @@ dependencies = [ [[package]] name = "reth-network" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8885,7 +8917,7 @@ dependencies = [ [[package]] name = "reth-network-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8909,7 +8941,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8931,7 +8963,7 @@ dependencies = [ [[package]] name = "reth-network-peers" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8948,7 +8980,7 @@ dependencies = [ [[package]] name = "reth-network-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -8961,7 +8993,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" -version = "1.9.2" +version = "1.9.3" dependencies = [ "anyhow", "bincode 1.3.3", @@ -8979,7 +9011,7 @@ dependencies = [ [[package]] name = "reth-node-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -9002,7 +9034,7 @@ dependencies = [ [[package]] name = "reth-node-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9019,7 +9051,6 @@ dependencies = [ "reth-basic-payload-builder", "reth-chain-state", "reth-chainspec", - "reth-cli-util", "reth-config", "reth-consensus", "reth-consensus-debug-client", @@ -9073,7 +9104,7 @@ dependencies = [ [[package]] name = "reth-node-core" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9085,6 +9116,7 @@ dependencies = [ "eyre", "futures", "humantime", + "ipnet", "proptest", "rand 0.9.2", "reth-chainspec", @@ -9097,11 +9129,13 @@ dependencies = [ "reth-engine-local", "reth-engine-primitives", "reth-ethereum-forks", + "reth-net-banlist", "reth-net-nat", "reth-network", "reth-network-p2p", "reth-network-peers", "reth-primitives-traits", + "reth-provider", "reth-prune-types", "reth-rpc-convert", "reth-rpc-eth-types", @@ -9127,7 +9161,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-contract", @@ -9139,6 +9173,7 @@ dependencies = [ "alloy-rpc-types-beacon", "alloy-rpc-types-engine", "alloy-rpc-types-eth", + "alloy-rpc-types-trace", "alloy-signer", "alloy-sol-types", "eyre", @@ -9171,16 +9206,19 @@ dependencies = [ "reth-rpc-eth-types", "reth-rpc-server-types", "reth-tasks", + "reth-testing-utils", "reth-tracing", "reth-transaction-pool", "revm", + "serde", "serde_json", + "similar-asserts", "tokio", ] [[package]] name = "reth-node-ethstats" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9203,7 +9241,7 @@ dependencies = [ [[package]] name = "reth-node-events" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9226,7 +9264,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" -version = "1.9.2" +version = "1.9.3" dependencies = [ "eyre", "http", @@ -9248,7 +9286,7 @@ dependencies = [ [[package]] name = "reth-node-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "reth-chainspec", "reth-db-api", @@ -9259,7 +9297,7 @@ dependencies = [ [[package]] name = "reth-op" -version = "1.9.2" +version = "1.9.3" dependencies = [ "reth-chainspec", "reth-cli-util", @@ -9299,7 +9337,7 @@ dependencies = [ [[package]] name = "reth-optimism-chainspec" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -9327,7 +9365,7 @@ dependencies = [ [[package]] name = "reth-optimism-cli" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9376,7 +9414,7 @@ dependencies = [ [[package]] name = "reth-optimism-consensus" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -9407,7 +9445,7 @@ dependencies = [ [[package]] name = "reth-optimism-evm" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9436,25 +9474,24 @@ dependencies = [ [[package]] name = "reth-optimism-flashblocks" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", "alloy-rpc-types-engine", - "alloy-serde", "brotli", "derive_more", "eyre", "futures-util", "metrics", + "op-alloy-rpc-types-engine", "reth-chain-state", "reth-engine-primitives", "reth-errors", "reth-evm", "reth-execution-types", "reth-metrics", - "reth-optimism-evm", "reth-optimism-payload-builder", "reth-optimism-primitives", "reth-payload-primitives", @@ -9464,7 +9501,6 @@ dependencies = [ "reth-storage-api", "reth-tasks", "ringbuffer", - "serde", "serde_json", "test-case", "tokio", @@ -9475,7 +9511,7 @@ dependencies = [ [[package]] name = "reth-optimism-forks" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-op-hardforks", "alloy-primitives", @@ -9485,7 +9521,7 @@ dependencies = [ [[package]] name = "reth-optimism-node" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -9543,7 +9579,7 @@ dependencies = [ [[package]] name = "reth-optimism-payload-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9582,7 +9618,7 @@ dependencies = [ [[package]] name = "reth-optimism-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9609,7 +9645,7 @@ dependencies = [ [[package]] name = "reth-optimism-rpc" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9671,7 +9707,7 @@ dependencies = [ [[package]] name = "reth-optimism-storage" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "reth-codecs", @@ -9683,7 +9719,7 @@ dependencies = [ [[package]] name = "reth-optimism-txpool" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9720,7 +9756,7 @@ dependencies = [ [[package]] name = "reth-payload-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9740,7 +9776,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "pin-project", "reth-payload-primitives", @@ -9751,7 +9787,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -9773,7 +9809,7 @@ dependencies = [ [[package]] name = "reth-payload-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9782,7 +9818,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -9791,7 +9827,7 @@ dependencies = [ [[package]] name = "reth-primitives" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9813,7 +9849,7 @@ dependencies = [ [[package]] name = "reth-primitives-traits" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9850,7 +9886,7 @@ dependencies = [ [[package]] name = "reth-provider" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9899,7 +9935,7 @@ dependencies = [ [[package]] name = "reth-prune" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9930,11 +9966,11 @@ dependencies = [ [[package]] name = "reth-prune-db" -version = "1.9.2" +version = "1.9.3" [[package]] name = "reth-prune-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "arbitrary", @@ -9953,7 +9989,7 @@ dependencies = [ [[package]] name = "reth-ress-protocol" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9979,7 +10015,7 @@ dependencies = [ [[package]] name = "reth-ress-provider" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10005,7 +10041,7 @@ dependencies = [ [[package]] name = "reth-revm" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10019,7 +10055,7 @@ dependencies = [ [[package]] name = "reth-rpc" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -10102,7 +10138,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-genesis", @@ -10129,7 +10165,7 @@ dependencies = [ [[package]] name = "reth-rpc-api-testing-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10148,7 +10184,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-network", @@ -10203,9 +10239,10 @@ dependencies = [ [[package]] name = "reth-rpc-convert" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", + "alloy-evm", "alloy-json-rpc", "alloy-network", "alloy-primitives", @@ -10217,20 +10254,18 @@ dependencies = [ "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types", - "op-revm", "reth-ethereum-primitives", "reth-evm", "reth-optimism-primitives", "reth-primitives-traits", "reth-storage-api", - "revm-context", "serde_json", "thiserror 2.0.17", ] [[package]] name = "reth-rpc-e2e-tests" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-genesis", "alloy-rpc-types-engine", @@ -10250,7 +10285,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10285,7 +10320,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -10328,7 +10363,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10375,7 +10410,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-rpc-types-engine", "http", @@ -10392,7 +10427,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10407,7 +10442,7 @@ dependencies = [ [[package]] name = "reth-stages" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10464,7 +10499,7 @@ dependencies = [ [[package]] name = "reth-stages-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10473,7 +10508,10 @@ dependencies = [ "auto_impl", "futures-util", "metrics", + "reth-chainspec", "reth-consensus", + "reth-db", + "reth-db-api", "reth-errors", "reth-metrics", "reth-network-p2p", @@ -10493,7 +10531,7 @@ dependencies = [ [[package]] name = "reth-stages-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "arbitrary", @@ -10509,7 +10547,7 @@ dependencies = [ [[package]] name = "reth-stateless" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10536,7 +10574,7 @@ dependencies = [ [[package]] name = "reth-static-file" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "assert_matches", @@ -10559,7 +10597,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "clap", @@ -10573,7 +10611,7 @@ dependencies = [ [[package]] name = "reth-storage-api" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10596,7 +10634,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10611,7 +10649,7 @@ dependencies = [ [[package]] name = "reth-storage-rpc-provider" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10640,7 +10678,7 @@ dependencies = [ [[package]] name = "reth-tasks" -version = "1.9.2" +version = "1.9.3" dependencies = [ "auto_impl", "dyn-clone", @@ -10657,12 +10695,13 @@ dependencies = [ [[package]] name = "reth-testing-utils" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-genesis", "alloy-primitives", + "alloy-rlp", "rand 0.8.5", "rand 0.9.2", "reth-ethereum-primitives", @@ -10672,7 +10711,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" -version = "1.9.2" +version = "1.9.3" dependencies = [ "tokio", "tokio-stream", @@ -10681,7 +10720,7 @@ dependencies = [ [[package]] name = "reth-tracing" -version = "1.9.2" +version = "1.9.3" dependencies = [ "clap", "eyre", @@ -10697,7 +10736,7 @@ dependencies = [ [[package]] name = "reth-tracing-otlp" -version = "1.9.2" +version = "1.9.3" dependencies = [ "clap", "eyre", @@ -10713,7 +10752,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10761,7 +10800,7 @@ dependencies = [ [[package]] name = "reth-trie" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10794,7 +10833,7 @@ dependencies = [ [[package]] name = "reth-trie-common" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -10827,7 +10866,7 @@ dependencies = [ [[package]] name = "reth-trie-db" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10852,7 +10891,7 @@ dependencies = [ [[package]] name = "reth-trie-parallel" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -10882,7 +10921,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -10915,7 +10954,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse-parallel" -version = "1.9.2" +version = "1.9.3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -10944,7 +10983,7 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" -version = "1.9.2" +version = "1.9.3" dependencies = [ "zstd", ] @@ -11306,6 +11345,7 @@ dependencies = [ "ark-ff 0.3.0", "ark-ff 0.4.2", "ark-ff 0.5.0", + "borsh", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", @@ -11753,7 +11793,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "memchr", "ryu", @@ -11795,15 +11835,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.1.0", "serde_core", @@ -11814,9 +11854,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -12672,7 +12712,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -12686,7 +12726,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", "winnow", @@ -12753,7 +12793,7 @@ dependencies = [ "futures-core", "futures-util", "hdrhistogram", - "indexmap 2.12.0", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper", @@ -12957,9 +12997,9 @@ dependencies = [ [[package]] name = "tracy-client" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef54005d3d760186fd662dad4b7bb27ecd5531cdef54d1573ebd3f20a9205ed7" +checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" dependencies = [ "loom", "once_cell", @@ -12969,9 +13009,9 @@ dependencies = [ [[package]] name = "tracy-client-sys" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319c70195101a93f56db4c74733e272d720768e13471f400c78406a326b172b0" +checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" dependencies = [ "cc", "windows-targets 0.52.6", @@ -14177,18 +14217,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 28641a77eb..e95baa993d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "1.9.2" +version = "1.9.3" edition = "2024" rust-version = "1.88" license = "MIT OR Apache-2.0" @@ -483,7 +483,7 @@ revm-inspectors = "0.32.0" alloy-primitives = { version = "1.4.1", default-features = false, features = ["map-foldhash"] } alloy-chains = { version = "0.2.5", default-features = false } -alloy-evm = { version = "0.23.1", default-features = false } +alloy-evm = { version = "0.24.2", default-features = false } alloy-dyn-abi = "1.4.1" alloy-eip2124 = { version = "0.2.0", default-features = false } alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] } @@ -493,35 +493,35 @@ alloy-trie = { version = "0.9.1", default-features = false } alloy-hardforks = "0.4.4" -alloy-consensus = { version = "1.1.0", default-features = false } -alloy-contract = { version = "1.1.0", default-features = false } -alloy-eips = { version = "1.1.0", default-features = false } -alloy-genesis = { version = "1.1.0", default-features = false } -alloy-json-rpc = { version = "1.1.0", default-features = false } -alloy-network = { version = "1.1.0", default-features = false } -alloy-network-primitives = { version = "1.1.0", default-features = false } -alloy-provider = { version = "1.1.0", features = ["reqwest"], default-features = false } -alloy-pubsub = { version = "1.1.0", default-features = false } -alloy-rpc-client = { version = "1.1.0", default-features = false } -alloy-rpc-types = { version = "1.1.0", features = ["eth"], default-features = false } -alloy-rpc-types-admin = { version = "1.1.0", default-features = false } -alloy-rpc-types-anvil = { version = "1.1.0", default-features = false } -alloy-rpc-types-beacon = { version = "1.1.0", default-features = false } -alloy-rpc-types-debug = { version = "1.1.0", default-features = false } -alloy-rpc-types-engine = { version = "1.1.0", default-features = false } -alloy-rpc-types-eth = { version = "1.1.0", default-features = false } -alloy-rpc-types-mev = { version = "1.1.0", default-features = false } -alloy-rpc-types-trace = { version = "1.1.0", default-features = false } -alloy-rpc-types-txpool = { version = "1.1.0", default-features = false } -alloy-serde = { version = "1.1.0", default-features = false } -alloy-signer = { version = "1.1.0", default-features = false } -alloy-signer-local = { version = "1.1.0", default-features = false } -alloy-transport = { version = "1.1.0" } -alloy-transport-http = { version = "1.1.0", features = ["reqwest-rustls-tls"], default-features = false } -alloy-transport-ipc = { version = "1.1.0", default-features = false } -alloy-transport-ws = { version = "1.1.0", default-features = false } +alloy-consensus = { version = "1.1.2", default-features = false } +alloy-contract = { version = "1.1.2", default-features = false } +alloy-eips = { version = "1.1.2", default-features = false } +alloy-genesis = { version = "1.1.2", default-features = false } +alloy-json-rpc = { version = "1.1.2", default-features = false } +alloy-network = { version = "1.1.2", default-features = false } +alloy-network-primitives = { version = "1.1.2", default-features = false } +alloy-provider = { version = "1.1.2", features = ["reqwest"], default-features = false } +alloy-pubsub = { version = "1.1.2", default-features = false } +alloy-rpc-client = { version = "1.1.2", default-features = false } +alloy-rpc-types = { version = "1.1.2", features = ["eth"], default-features = false } +alloy-rpc-types-admin = { version = "1.1.2", default-features = false } +alloy-rpc-types-anvil = { version = "1.1.2", default-features = false } +alloy-rpc-types-beacon = { version = "1.1.2", default-features = false } +alloy-rpc-types-debug = { version = "1.1.2", default-features = false } +alloy-rpc-types-engine = { version = "1.1.2", default-features = false } +alloy-rpc-types-eth = { version = "1.1.2", default-features = false } +alloy-rpc-types-mev = { version = "1.1.2", default-features = false } +alloy-rpc-types-trace = { version = "1.1.2", default-features = false } +alloy-rpc-types-txpool = { version = "1.1.2", default-features = false } +alloy-serde = { version = "1.1.2", default-features = false } +alloy-signer = { version = "1.1.2", default-features = false } +alloy-signer-local = { version = "1.1.2", default-features = false } +alloy-transport = { version = "1.1.2" } +alloy-transport-http = { version = "1.1.2", features = ["reqwest-rustls-tls"], default-features = false } +alloy-transport-ipc = { version = "1.1.2", default-features = false } +alloy-transport-ws = { version = "1.1.2", default-features = false } # op -alloy-op-evm = { version = "0.23.1", default-features = false } +alloy-op-evm = { version = "0.24.2", default-features = false } alloy-op-hardforks = "0.4.3" op-alloy-rpc-types = { version = "0.22.0", default-features = false } op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false } @@ -728,6 +728,8 @@ vergen = "9.0.4" visibility = "0.1.1" walkdir = "2.3.3" vergen-git2 = "1.0.5" +# networking +ipnet = "2.11" [patch.crates-io] alloy-consensus = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" } diff --git a/bin/reth-bench-compare/src/benchmark.rs b/bin/reth-bench-compare/src/benchmark.rs index e1b971f579..ba6eaea176 100644 --- a/bin/reth-bench-compare/src/benchmark.rs +++ b/bin/reth-bench-compare/src/benchmark.rs @@ -103,7 +103,8 @@ impl BenchmarkRunner { cmd.args(["--wait-time", wait_time]); } - cmd.stdout(std::process::Stdio::piped()) + cmd.env("RUST_LOG_STYLE", "never") + .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); @@ -190,7 +191,8 @@ impl BenchmarkRunner { cmd.args(["--wait-time", wait_time]); } - cmd.stdout(std::process::Stdio::piped()) + cmd.env("RUST_LOG_STYLE", "never") + .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); diff --git a/bin/reth-bench-compare/src/cli.rs b/bin/reth-bench-compare/src/cli.rs index a494b1aec4..89d3d874cd 100644 --- a/bin/reth-bench-compare/src/cli.rs +++ b/bin/reth-bench-compare/src/cli.rs @@ -134,6 +134,16 @@ pub(crate) struct Args { #[command(flatten)] pub traces: TraceArgs, + /// Maximum queue size for OTLP Batch Span Processor (traces). + /// Higher values prevent trace drops when benchmarking many blocks. + #[arg( + long, + value_name = "OTLP_BUFFER_SIZE", + default_value = "32768", + help_heading = "Tracing" + )] + pub otlp_max_queue_size: usize, + /// Additional arguments to pass to baseline reth node command /// /// Example: `--baseline-args "--debug.tip 0xabc..."` diff --git a/bin/reth-bench-compare/src/comparison.rs b/bin/reth-bench-compare/src/comparison.rs index dd160ac555..ad6d801e7c 100644 --- a/bin/reth-bench-compare/src/comparison.rs +++ b/bin/reth-bench-compare/src/comparison.rs @@ -6,6 +6,7 @@ use csv::Reader; use eyre::{eyre, Result, WrapErr}; use serde::{Deserialize, Serialize}; use std::{ + cmp::Ordering, collections::HashMap, fs, path::{Path, PathBuf}, @@ -36,6 +37,7 @@ pub(crate) struct BenchmarkResults { #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CombinedLatencyRow { pub block_number: u64, + pub transaction_count: u64, pub gas_used: u64, pub new_payload_latency: u128, } @@ -44,17 +46,26 @@ pub(crate) struct CombinedLatencyRow { #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct TotalGasRow { pub block_number: u64, + pub transaction_count: u64, pub gas_used: u64, pub time: u128, } -/// Summary statistics for a benchmark run +/// Summary statistics for a benchmark run. +/// +/// Latencies are derived from per-block `engine_newPayload` timings (converted from µs to ms): +/// - `mean_new_payload_latency_ms`: arithmetic mean latency across blocks. +/// - `median_new_payload_latency_ms`: p50 latency across blocks. +/// - `p90_new_payload_latency_ms` / `p99_new_payload_latency_ms`: tail latencies across blocks. #[derive(Debug, Clone, Serialize)] pub(crate) struct BenchmarkSummary { pub total_blocks: u64, pub total_gas_used: u64, pub total_duration_ms: u128, - pub avg_new_payload_latency_ms: f64, + pub mean_new_payload_latency_ms: f64, + pub median_new_payload_latency_ms: f64, + pub p90_new_payload_latency_ms: f64, + pub p99_new_payload_latency_ms: f64, pub gas_per_second: f64, pub blocks_per_second: f64, pub min_block_number: u64, @@ -80,10 +91,26 @@ pub(crate) struct RefInfo { pub end_timestamp: Option>, } -/// Summary of the comparison between references +/// Summary of the comparison between references. +/// +/// Percent deltas are `(feature - baseline) / baseline * 100`: +/// - `new_payload_latency_p50_change_percent` / p90 / p99: percent changes of the respective +/// per-block percentiles. +/// - `per_block_latency_change_mean_percent` / `per_block_latency_change_median_percent` are the +/// mean and median of per-block percent deltas (feature vs baseline), capturing block-level +/// drift. +/// - `new_payload_total_latency_change_percent` is the percent change of the total newPayload time +/// across the run. +/// +/// Positive means slower/higher; negative means faster/lower. #[derive(Debug, Serialize)] pub(crate) struct ComparisonSummary { - pub new_payload_latency_change_percent: f64, + pub per_block_latency_change_mean_percent: f64, + pub per_block_latency_change_median_percent: f64, + pub new_payload_total_latency_change_percent: f64, + pub new_payload_latency_p50_change_percent: f64, + pub new_payload_latency_p90_change_percent: f64, + pub new_payload_latency_p99_change_percent: f64, pub gas_per_second_change_percent: f64, pub blocks_per_second_change_percent: f64, } @@ -92,6 +119,8 @@ pub(crate) struct ComparisonSummary { #[derive(Debug, Serialize)] pub(crate) struct BlockComparison { pub block_number: u64, + pub transaction_count: u64, + pub gas_used: u64, pub baseline_new_payload_latency: u128, pub feature_new_payload_latency: u128, pub new_payload_latency_change_percent: f64, @@ -184,10 +213,12 @@ impl ComparisonGenerator { let feature = self.feature_results.as_ref().ok_or_else(|| eyre!("Feature results not loaded"))?; - // Generate comparison - let comparison_summary = - self.calculate_comparison_summary(&baseline.summary, &feature.summary)?; let per_block_comparisons = self.calculate_per_block_comparisons(baseline, feature)?; + let comparison_summary = self.calculate_comparison_summary( + &baseline.summary, + &feature.summary, + &per_block_comparisons, + )?; let report = ComparisonReport { timestamp: self.timestamp.clone(), @@ -277,7 +308,11 @@ impl ComparisonGenerator { Ok(rows) } - /// Calculate summary statistics for a benchmark run + /// Calculate summary statistics for a benchmark run. + /// + /// Computes latency statistics from per-block `new_payload_latency` values in `combined_data` + /// (converting from µs to ms), and throughput metrics using the total run duration from + /// `total_gas_data`. Percentiles (p50/p90/p99) use linear interpolation on sorted latencies. fn calculate_summary( &self, combined_data: &[CombinedLatencyRow], @@ -292,9 +327,16 @@ impl ComparisonGenerator { let total_duration_ms = total_gas_data.last().unwrap().time / 1000; // Convert microseconds to milliseconds - let avg_new_payload_latency_ms: f64 = - combined_data.iter().map(|r| r.new_payload_latency as f64 / 1000.0).sum::() / - total_blocks as f64; + let latencies_ms: Vec = + combined_data.iter().map(|r| r.new_payload_latency as f64 / 1000.0).collect(); + let mean_new_payload_latency_ms: f64 = + latencies_ms.iter().sum::() / total_blocks as f64; + + let mut sorted_latencies_ms = latencies_ms; + sorted_latencies_ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + let median_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.5); + let p90_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.9); + let p99_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.99); let total_duration_seconds = total_duration_ms as f64 / 1000.0; let gas_per_second = if total_duration_seconds > f64::EPSILON { @@ -316,7 +358,10 @@ impl ComparisonGenerator { total_blocks, total_gas_used, total_duration_ms, - avg_new_payload_latency_ms, + mean_new_payload_latency_ms, + median_new_payload_latency_ms, + p90_new_payload_latency_ms, + p99_new_payload_latency_ms, gas_per_second, blocks_per_second, min_block_number, @@ -329,6 +374,7 @@ impl ComparisonGenerator { &self, baseline: &BenchmarkSummary, feature: &BenchmarkSummary, + per_block_comparisons: &[BlockComparison], ) -> Result { let calc_percent_change = |baseline: f64, feature: f64| -> f64 { if baseline.abs() > f64::EPSILON { @@ -338,10 +384,43 @@ impl ComparisonGenerator { } }; + let per_block_percent_changes: Vec = + per_block_comparisons.iter().map(|c| c.new_payload_latency_change_percent).collect(); + let per_block_latency_change_mean_percent = if per_block_percent_changes.is_empty() { + 0.0 + } else { + per_block_percent_changes.iter().sum::() / per_block_percent_changes.len() as f64 + }; + let per_block_latency_change_median_percent = if per_block_percent_changes.is_empty() { + 0.0 + } else { + let mut sorted = per_block_percent_changes; + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + percentile(&sorted, 0.5) + }; + + let baseline_total_latency_ms = + baseline.mean_new_payload_latency_ms * baseline.total_blocks as f64; + let feature_total_latency_ms = + feature.mean_new_payload_latency_ms * feature.total_blocks as f64; + let new_payload_total_latency_change_percent = + calc_percent_change(baseline_total_latency_ms, feature_total_latency_ms); + Ok(ComparisonSummary { - new_payload_latency_change_percent: calc_percent_change( - baseline.avg_new_payload_latency_ms, - feature.avg_new_payload_latency_ms, + per_block_latency_change_mean_percent, + per_block_latency_change_median_percent, + new_payload_total_latency_change_percent, + new_payload_latency_p50_change_percent: calc_percent_change( + baseline.median_new_payload_latency_ms, + feature.median_new_payload_latency_ms, + ), + new_payload_latency_p90_change_percent: calc_percent_change( + baseline.p90_new_payload_latency_ms, + feature.p90_new_payload_latency_ms, + ), + new_payload_latency_p99_change_percent: calc_percent_change( + baseline.p99_new_payload_latency_ms, + feature.p99_new_payload_latency_ms, ), gas_per_second_change_percent: calc_percent_change( baseline.gas_per_second, @@ -378,6 +457,8 @@ impl ComparisonGenerator { let comparison = BlockComparison { block_number: feature_row.block_number, + transaction_count: feature_row.transaction_count, + gas_used: feature_row.gas_used, baseline_new_payload_latency: baseline_row.new_payload_latency, feature_new_payload_latency: feature_row.new_payload_latency, new_payload_latency_change_percent: calc_percent_change( @@ -443,9 +524,38 @@ impl ComparisonGenerator { let summary = &report.comparison_summary; println!("Performance Changes:"); - println!(" NewPayload Latency: {:+.2}%", summary.new_payload_latency_change_percent); - println!(" Gas/Second: {:+.2}%", summary.gas_per_second_change_percent); - println!(" Blocks/Second: {:+.2}%", summary.blocks_per_second_change_percent); + println!( + " NewPayload Latency per-block mean change: {:+.2}%", + summary.per_block_latency_change_mean_percent + ); + println!( + " NewPayload Latency per-block median change: {:+.2}%", + summary.per_block_latency_change_median_percent + ); + println!( + " Total newPayload time change: {:+.2}%", + summary.new_payload_total_latency_change_percent + ); + println!( + " NewPayload Latency p50: {:+.2}%", + summary.new_payload_latency_p50_change_percent + ); + println!( + " NewPayload Latency p90: {:+.2}%", + summary.new_payload_latency_p90_change_percent + ); + println!( + " NewPayload Latency p99: {:+.2}%", + summary.new_payload_latency_p99_change_percent + ); + println!( + " Gas/Second: {:+.2}%", + summary.gas_per_second_change_percent + ); + println!( + " Blocks/Second: {:+.2}%", + summary.blocks_per_second_change_percent + ); println!(); println!("Baseline Summary:"); @@ -458,7 +568,14 @@ impl ComparisonGenerator { baseline.total_gas_used, baseline.total_duration_ms as f64 / 1000.0 ); - println!(" Avg NewPayload: {:.2}ms", baseline.avg_new_payload_latency_ms); + println!(" NewPayload latency (ms):"); + println!( + " mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}", + baseline.mean_new_payload_latency_ms, + baseline.median_new_payload_latency_ms, + baseline.p90_new_payload_latency_ms, + baseline.p99_new_payload_latency_ms + ); if let (Some(start), Some(end)) = (&report.baseline.start_timestamp, &report.baseline.end_timestamp) { @@ -480,7 +597,14 @@ impl ComparisonGenerator { feature.total_gas_used, feature.total_duration_ms as f64 / 1000.0 ); - println!(" Avg NewPayload: {:.2}ms", feature.avg_new_payload_latency_ms); + println!(" NewPayload latency (ms):"); + println!( + " mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}", + feature.mean_new_payload_latency_ms, + feature.median_new_payload_latency_ms, + feature.p90_new_payload_latency_ms, + feature.p99_new_payload_latency_ms + ); if let (Some(start), Some(end)) = (&report.feature.start_timestamp, &report.feature.end_timestamp) { @@ -493,3 +617,29 @@ impl ComparisonGenerator { println!(); } } + +/// Calculate percentile using linear interpolation on a sorted slice. +/// +/// Computes `rank = percentile × (n - 1)` where n is the array length. If the rank falls +/// between two indices, linearly interpolates between those values. For example, with 100 values, +/// p90 computes rank = 0.9 × 99 = 89.1, then returns `values[89] × 0.9 + values[90] × 0.1`. +/// +/// Returns 0.0 for empty input. +fn percentile(sorted_values: &[f64], percentile: f64) -> f64 { + if sorted_values.is_empty() { + return 0.0; + } + + let clamped = percentile.clamp(0.0, 1.0); + let max_index = sorted_values.len() - 1; + let rank = clamped * max_index as f64; + let lower = rank.floor() as usize; + let upper = rank.ceil() as usize; + + if lower == upper { + sorted_values[lower] + } else { + let weight = rank - lower as f64; + sorted_values[lower].mul_add(1.0 - weight, sorted_values[upper] * weight) + } +} diff --git a/bin/reth-bench-compare/src/node.rs b/bin/reth-bench-compare/src/node.rs index 5f6012b04b..c401199daa 100644 --- a/bin/reth-bench-compare/src/node.rs +++ b/bin/reth-bench-compare/src/node.rs @@ -30,6 +30,7 @@ pub(crate) struct NodeManager { additional_reth_args: Vec, comparison_dir: Option, tracing_endpoint: Option, + otlp_max_queue_size: usize, } impl NodeManager { @@ -46,6 +47,7 @@ impl NodeManager { additional_reth_args: args.reth_args.clone(), comparison_dir: None, tracing_endpoint: args.traces.otlp.as_ref().map(|u| u.to_string()), + otlp_max_queue_size: args.otlp_max_queue_size, } } @@ -203,6 +205,9 @@ impl NodeManager { cmd.arg("--"); cmd.args(reth_args); + // Set environment variable to disable log styling + cmd.env("RUST_LOG_STYLE", "never"); + Ok(cmd) } @@ -210,17 +215,22 @@ impl NodeManager { fn create_direct_command(&self, reth_args: &[String]) -> Command { let binary_path = &reth_args[0]; - if self.use_sudo { + let mut cmd = if self.use_sudo { info!("Starting reth node with sudo..."); - let mut cmd = Command::new("sudo"); - cmd.args(reth_args); - cmd + let mut sudo_cmd = Command::new("sudo"); + sudo_cmd.args(reth_args); + sudo_cmd } else { info!("Starting reth node..."); - let mut cmd = Command::new(binary_path); - cmd.args(&reth_args[1..]); // Skip the binary path since it's the command - cmd - } + let mut reth_cmd = Command::new(binary_path); + reth_cmd.args(&reth_args[1..]); // Skip the binary path since it's the command + reth_cmd + }; + + // Set environment variable to disable log styling + cmd.env("RUST_LOG_STYLE", "never"); + + cmd } /// Start a reth node using the specified binary path and return the process handle @@ -259,7 +269,9 @@ impl NodeManager { // Set high queue size to prevent trace dropping during benchmarks if self.tracing_endpoint.is_some() { - cmd.env("OTEL_BLRP_MAX_QUEUE_SIZE", "10000"); + cmd.env("OTEL_BSP_MAX_QUEUE_SIZE", self.otlp_max_queue_size.to_string()); // Traces + cmd.env("OTEL_BLRP_MAX_QUEUE_SIZE", "10000"); // Logs + // Set service name to differentiate baseline vs feature runs in Jaeger cmd.env("OTEL_SERVICE_NAME", format!("reth-{}", ref_type)); } @@ -485,6 +497,9 @@ impl NodeManager { cmd.args(["to-block", &block_number.to_string()]); + // Set environment variable to disable log styling + cmd.env("RUST_LOG_STYLE", "never"); + // Debug log the command debug!("Executing reth unwind command: {:?}", cmd); diff --git a/bin/reth-bench/src/bench/new_payload_fcu.rs b/bin/reth-bench/src/bench/new_payload_fcu.rs index 1d1bf59b36..5760184b7f 100644 --- a/bin/reth-bench/src/bench/new_payload_fcu.rs +++ b/bin/reth-bench/src/bench/new_payload_fcu.rs @@ -79,22 +79,13 @@ impl Command { break; } }; - let header = block.header.clone(); - let (version, params) = match block_to_new_payload(block, is_optimism) { - Ok(result) => result, - Err(e) => { - tracing::error!("Failed to convert block to new payload: {e}"); - let _ = error_sender.send(e); - break; - } - }; - let head_block_hash = header.hash; - let safe_block_hash = - block_provider.get_block_by_number(header.number.saturating_sub(32).into()); + let head_block_hash = block.header.hash; + let safe_block_hash = block_provider + .get_block_by_number(block.header.number.saturating_sub(32).into()); - let finalized_block_hash = - block_provider.get_block_by_number(header.number.saturating_sub(64).into()); + let finalized_block_hash = block_provider + .get_block_by_number(block.header.number.saturating_sub(64).into()); let (safe, finalized) = tokio::join!(safe_block_hash, finalized_block_hash,); @@ -110,14 +101,7 @@ impl Command { next_block += 1; if let Err(e) = sender - .send(( - header, - version, - params, - head_block_hash, - safe_block_hash, - finalized_block_hash, - )) + .send((block, head_block_hash, safe_block_hash, finalized_block_hash)) .await { tracing::error!("Failed to send block data: {e}"); @@ -131,15 +115,16 @@ impl Command { let total_benchmark_duration = Instant::now(); let mut total_wait_time = Duration::ZERO; - while let Some((header, version, params, head, safe, finalized)) = { + while let Some((block, head, safe, finalized)) = { let wait_start = Instant::now(); let result = receiver.recv().await; total_wait_time += wait_start.elapsed(); result } { // just put gas used here - let gas_used = header.gas_used; - let block_number = header.number; + let gas_used = block.header.gas_used; + let block_number = block.header.number; + let transaction_count = block.transactions.len() as u64; debug!(target: "reth-bench", ?block_number, "Sending payload",); @@ -150,6 +135,7 @@ impl Command { finalized_block_hash: finalized, }; + let (version, params) = block_to_new_payload(block, is_optimism)?; let start = Instant::now(); call_new_payload(&auth_provider, version, params).await?; @@ -160,8 +146,13 @@ impl Command { // calculate the total duration and the fcu latency, record let total_latency = start.elapsed(); let fcu_latency = total_latency - new_payload_result.latency; - let combined_result = - CombinedResult { block_number, new_payload_result, fcu_latency, total_latency }; + let combined_result = CombinedResult { + block_number, + transaction_count, + new_payload_result, + fcu_latency, + total_latency, + }; // current duration since the start of the benchmark minus the time // waiting for blocks @@ -174,7 +165,8 @@ impl Command { tokio::time::sleep(self.wait_time).await; // record the current result - let gas_row = TotalGasRow { block_number, gas_used, time: current_duration }; + let gas_row = + TotalGasRow { block_number, transaction_count, gas_used, time: current_duration }; results.push((gas_row, combined_result)); } diff --git a/bin/reth-bench/src/bench/new_payload_only.rs b/bin/reth-bench/src/bench/new_payload_only.rs index 3dfa619ec7..748ac999a9 100644 --- a/bin/reth-bench/src/bench/new_payload_only.rs +++ b/bin/reth-bench/src/bench/new_payload_only.rs @@ -72,19 +72,9 @@ impl Command { break; } }; - let header = block.header.clone(); - - let (version, params) = match block_to_new_payload(block, is_optimism) { - Ok(result) => result, - Err(e) => { - tracing::error!("Failed to convert block to new payload: {e}"); - let _ = error_sender.send(e); - break; - } - }; next_block += 1; - if let Err(e) = sender.send((header, version, params)).await { + if let Err(e) = sender.send(block).await { tracing::error!("Failed to send block data: {e}"); break; } @@ -96,23 +86,24 @@ impl Command { let total_benchmark_duration = Instant::now(); let mut total_wait_time = Duration::ZERO; - while let Some((header, version, params)) = { + while let Some(block) = { let wait_start = Instant::now(); let result = receiver.recv().await; total_wait_time += wait_start.elapsed(); result } { - // just put gas used here - let gas_used = header.gas_used; - - let block_number = header.number; + let block_number = block.header.number; + let transaction_count = block.transactions.len() as u64; + let gas_used = block.header.gas_used; debug!( target: "reth-bench", - number=?header.number, + number=?block.header.number, "Sending payload to engine", ); + let (version, params) = block_to_new_payload(block, is_optimism)?; + let start = Instant::now(); call_new_payload(&auth_provider, version, params).await?; @@ -124,7 +115,8 @@ impl Command { let current_duration = total_benchmark_duration.elapsed() - total_wait_time; // record the current result - let row = TotalGasRow { block_number, gas_used, time: current_duration }; + let row = + TotalGasRow { block_number, transaction_count, gas_used, time: current_duration }; results.push((row, new_payload_result)); } diff --git a/bin/reth-bench/src/bench/output.rs b/bin/reth-bench/src/bench/output.rs index 794cd2768d..17e9ad4a7a 100644 --- a/bin/reth-bench/src/bench/output.rs +++ b/bin/reth-bench/src/bench/output.rs @@ -67,6 +67,8 @@ impl Serialize for NewPayloadResult { pub(crate) struct CombinedResult { /// The block number of the block being processed. pub(crate) block_number: u64, + /// The number of transactions in the block. + pub(crate) transaction_count: u64, /// The `newPayload` result. pub(crate) new_payload_result: NewPayloadResult, /// The latency of the `forkchoiceUpdated` call. @@ -108,10 +110,11 @@ impl Serialize for CombinedResult { let fcu_latency = self.fcu_latency.as_micros(); let new_payload_latency = self.new_payload_result.latency.as_micros(); let total_latency = self.total_latency.as_micros(); - let mut state = serializer.serialize_struct("CombinedResult", 5)?; + let mut state = serializer.serialize_struct("CombinedResult", 6)?; // flatten the new payload result because this is meant for CSV writing state.serialize_field("block_number", &self.block_number)?; + state.serialize_field("transaction_count", &self.transaction_count)?; state.serialize_field("gas_used", &self.new_payload_result.gas_used)?; state.serialize_field("new_payload_latency", &new_payload_latency)?; state.serialize_field("fcu_latency", &fcu_latency)?; @@ -125,6 +128,8 @@ impl Serialize for CombinedResult { pub(crate) struct TotalGasRow { /// The block number of the block being processed. pub(crate) block_number: u64, + /// The number of transactions in the block. + pub(crate) transaction_count: u64, /// The total gas used in the block. pub(crate) gas_used: u64, /// Time since the start of the benchmark. @@ -172,8 +177,9 @@ impl Serialize for TotalGasRow { { // convert the time to microseconds let time = self.time.as_micros(); - let mut state = serializer.serialize_struct("TotalGasRow", 3)?; + let mut state = serializer.serialize_struct("TotalGasRow", 4)?; state.serialize_field("block_number", &self.block_number)?; + state.serialize_field("transaction_count", &self.transaction_count)?; state.serialize_field("gas_used", &self.gas_used)?; state.serialize_field("time", &time)?; state.end() @@ -188,7 +194,12 @@ mod tests { #[test] fn test_write_total_gas_row_csv() { - let row = TotalGasRow { block_number: 1, gas_used: 1_000, time: Duration::from_secs(1) }; + let row = TotalGasRow { + block_number: 1, + transaction_count: 10, + gas_used: 1_000, + time: Duration::from_secs(1), + }; let mut writer = Writer::from_writer(vec![]); writer.serialize(row).unwrap(); @@ -198,11 +209,11 @@ mod tests { let mut result = result.as_slice().lines(); // assert header - let expected_first_line = "block_number,gas_used,time"; + let expected_first_line = "block_number,transaction_count,gas_used,time"; let first_line = result.next().unwrap().unwrap(); assert_eq!(first_line, expected_first_line); - let expected_second_line = "1,1000,1000000"; + let expected_second_line = "1,10,1000,1000000"; let second_line = result.next().unwrap().unwrap(); assert_eq!(second_line, expected_second_line); } diff --git a/crates/chain-state/src/in_memory.rs b/crates/chain-state/src/in_memory.rs index a6c8553810..cf1ddf413b 100644 --- a/crates/chain-state/src/in_memory.rs +++ b/crates/chain-state/src/in_memory.rs @@ -17,7 +17,7 @@ use reth_primitives_traits::{ SignedTransaction, }; use reth_storage_api::StateProviderBox; -use reth_trie::{updates::TrieUpdates, HashedPostState}; +use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted}; use std::{collections::BTreeMap, sync::Arc, time::Instant}; use tokio::sync::{broadcast, watch}; @@ -725,10 +725,10 @@ pub struct ExecutedBlock { pub recovered_block: Arc>, /// Block's execution outcome. pub execution_output: Arc>, - /// Block's hashed state. - pub hashed_state: Arc, - /// Trie updates that result from calculating the state root for the block. - pub trie_updates: Arc, + /// Block's sorted hashed state. + pub hashed_state: Arc, + /// Sorted trie updates that result from calculating the state root for the block. + pub trie_updates: Arc, } impl Default for ExecutedBlock { @@ -763,13 +763,13 @@ impl ExecutedBlock { /// Returns a reference to the hashed state result of the execution outcome #[inline] - pub fn hashed_state(&self) -> &HashedPostState { + pub fn hashed_state(&self) -> &HashedPostStateSorted { &self.hashed_state } /// Returns a reference to the trie updates resulting from the execution outcome #[inline] - pub fn trie_updates(&self) -> &TrieUpdates { + pub fn trie_updates(&self) -> &TrieUpdatesSorted { &self.trie_updates } @@ -875,8 +875,8 @@ mod tests { StateProofProvider, StateProvider, StateRootProvider, StorageRootProvider, }; use reth_trie::{ - AccountProof, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, - StorageProof, TrieInput, + updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof, + MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, }; fn create_mock_state( diff --git a/crates/chain-state/src/memory_overlay.rs b/crates/chain-state/src/memory_overlay.rs index 254edb248b..b248bc2e73 100644 --- a/crates/chain-state/src/memory_overlay.rs +++ b/crates/chain-state/src/memory_overlay.rs @@ -53,7 +53,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> { /// Return lazy-loaded trie state aggregated from in-memory blocks. fn trie_input(&self) -> &TrieInput { self.trie_input.get_or_init(|| { - TrieInput::from_blocks( + TrieInput::from_blocks_sorted( self.in_memory .iter() .rev() diff --git a/crates/chain-state/src/test_utils.rs b/crates/chain-state/src/test_utils.rs index ad9902c600..0af657c5c7 100644 --- a/crates/chain-state/src/test_utils.rs +++ b/crates/chain-state/src/test_utils.rs @@ -23,7 +23,7 @@ use reth_primitives_traits::{ SignedTransaction, }; use reth_storage_api::NodePrimitivesProvider; -use reth_trie::{root::state_root_unhashed, updates::TrieUpdates, HashedPostState}; +use reth_trie::{root::state_root_unhashed, updates::TrieUpdatesSorted, HashedPostStateSorted}; use revm_database::BundleState; use revm_state::AccountInfo; use std::{ @@ -217,8 +217,8 @@ impl TestBlockBuilder { block_number, vec![Requests::default()], )), - hashed_state: Arc::new(HashedPostState::default()), - trie_updates: Arc::new(TrieUpdates::default()), + hashed_state: Arc::new(HashedPostStateSorted::default()), + trie_updates: Arc::new(TrieUpdatesSorted::default()), } } diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index ca18ffc34f..a679da5b66 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -1399,72 +1399,72 @@ Post-merge hard forks (timestamp based): &[ ( EthereumHardfork::Frontier, - ForkId { hash: ForkHash([0xfc, 0x64, 0xec, 0x04]), next: 1150000 }, + ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 }, ), ( EthereumHardfork::Homestead, - ForkId { hash: ForkHash([0x97, 0xc2, 0xc3, 0x4c]), next: 1920000 }, + ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 }, ), ( EthereumHardfork::Dao, - ForkId { hash: ForkHash([0x91, 0xd1, 0xf9, 0x48]), next: 2463000 }, + ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 }, ), ( EthereumHardfork::Tangerine, - ForkId { hash: ForkHash([0x7a, 0x64, 0xda, 0x13]), next: 2675000 }, + ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 }, ), ( EthereumHardfork::SpuriousDragon, - ForkId { hash: ForkHash([0x3e, 0xdd, 0x5b, 0x10]), next: 4370000 }, + ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 }, ), ( EthereumHardfork::Byzantium, - ForkId { hash: ForkHash([0xa0, 0x0b, 0xc3, 0x24]), next: 7280000 }, + ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 }, ), ( EthereumHardfork::Constantinople, - ForkId { hash: ForkHash([0x66, 0x8d, 0xb0, 0xaf]), next: 9069000 }, + ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 }, ), ( EthereumHardfork::Petersburg, - ForkId { hash: ForkHash([0x66, 0x8d, 0xb0, 0xaf]), next: 9069000 }, + ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 }, ), ( EthereumHardfork::Istanbul, - ForkId { hash: ForkHash([0x87, 0x9d, 0x6e, 0x30]), next: 9200000 }, + ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 }, ), ( EthereumHardfork::MuirGlacier, - ForkId { hash: ForkHash([0xe0, 0x29, 0xe9, 0x91]), next: 12244000 }, + ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 }, ), ( EthereumHardfork::Berlin, - ForkId { hash: ForkHash([0x0e, 0xb4, 0x40, 0xf6]), next: 12965000 }, + ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 }, ), ( EthereumHardfork::London, - ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 13773000 }, + ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 }, ), ( EthereumHardfork::ArrowGlacier, - ForkId { hash: ForkHash([0x20, 0xc3, 0x27, 0xfc]), next: 15050000 }, + ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 }, ), ( EthereumHardfork::GrayGlacier, - ForkId { hash: ForkHash([0xf0, 0xaf, 0xd0, 0xe3]), next: 1681338455 }, + ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 }, ), ( EthereumHardfork::Shanghai, - ForkId { hash: ForkHash([0xdc, 0xe9, 0x6c, 0x2d]), next: 1710338135 }, + ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 }, ), ( EthereumHardfork::Cancun, - ForkId { hash: ForkHash([0x9f, 0x3d, 0x22, 0x54]), next: 1746612311 }, + ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 }, ), ( EthereumHardfork::Prague, ForkId { - hash: ForkHash([0xc3, 0x76, 0xcf, 0x8b]), + hash: ForkHash(hex!("0xc376cf8b")), next: mainnet::MAINNET_OSAKA_TIMESTAMP, }, ), @@ -1479,60 +1479,60 @@ Post-merge hard forks (timestamp based): &[ ( EthereumHardfork::Frontier, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Homestead, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Tangerine, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::SpuriousDragon, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Byzantium, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Constantinople, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Petersburg, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Istanbul, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Berlin, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::London, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( EthereumHardfork::Paris, - ForkId { hash: ForkHash([0xb9, 0x6c, 0xbd, 0x13]), next: 1677557088 }, + ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 }, ), ( EthereumHardfork::Shanghai, - ForkId { hash: ForkHash([0xf7, 0xf9, 0xbc, 0x08]), next: 1706655072 }, + ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 }, ), ( EthereumHardfork::Cancun, - ForkId { hash: ForkHash([0x88, 0xcf, 0x81, 0xd9]), next: 1741159776 }, + ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 }, ), ( EthereumHardfork::Prague, ForkId { - hash: ForkHash([0xed, 0x88, 0xb5, 0xfd]), + hash: ForkHash(hex!("0xed88b5fd")), next: sepolia::SEPOLIA_OSAKA_TIMESTAMP, }, ), @@ -1547,71 +1547,71 @@ Post-merge hard forks (timestamp based): &[ ( Head { number: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xfc, 0x64, 0xec, 0x04]), next: 1150000 }, + ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 }, ), ( Head { number: 1150000, ..Default::default() }, - ForkId { hash: ForkHash([0x97, 0xc2, 0xc3, 0x4c]), next: 1920000 }, + ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 }, ), ( Head { number: 1920000, ..Default::default() }, - ForkId { hash: ForkHash([0x91, 0xd1, 0xf9, 0x48]), next: 2463000 }, + ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 }, ), ( Head { number: 2463000, ..Default::default() }, - ForkId { hash: ForkHash([0x7a, 0x64, 0xda, 0x13]), next: 2675000 }, + ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 }, ), ( Head { number: 2675000, ..Default::default() }, - ForkId { hash: ForkHash([0x3e, 0xdd, 0x5b, 0x10]), next: 4370000 }, + ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 }, ), ( Head { number: 4370000, ..Default::default() }, - ForkId { hash: ForkHash([0xa0, 0x0b, 0xc3, 0x24]), next: 7280000 }, + ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 }, ), ( Head { number: 7280000, ..Default::default() }, - ForkId { hash: ForkHash([0x66, 0x8d, 0xb0, 0xaf]), next: 9069000 }, + ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 }, ), ( Head { number: 9069000, ..Default::default() }, - ForkId { hash: ForkHash([0x87, 0x9d, 0x6e, 0x30]), next: 9200000 }, + ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 }, ), ( Head { number: 9200000, ..Default::default() }, - ForkId { hash: ForkHash([0xe0, 0x29, 0xe9, 0x91]), next: 12244000 }, + ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 }, ), ( Head { number: 12244000, ..Default::default() }, - ForkId { hash: ForkHash([0x0e, 0xb4, 0x40, 0xf6]), next: 12965000 }, + ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 }, ), ( Head { number: 12965000, ..Default::default() }, - ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 13773000 }, + ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 }, ), ( Head { number: 13773000, ..Default::default() }, - ForkId { hash: ForkHash([0x20, 0xc3, 0x27, 0xfc]), next: 15050000 }, + ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 }, ), ( Head { number: 15050000, ..Default::default() }, - ForkId { hash: ForkHash([0xf0, 0xaf, 0xd0, 0xe3]), next: 1681338455 }, + ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 }, ), // First Shanghai block ( Head { number: 20000000, timestamp: 1681338455, ..Default::default() }, - ForkId { hash: ForkHash([0xdc, 0xe9, 0x6c, 0x2d]), next: 1710338135 }, + ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 }, ), // First Cancun block ( Head { number: 20000001, timestamp: 1710338135, ..Default::default() }, - ForkId { hash: ForkHash([0x9f, 0x3d, 0x22, 0x54]), next: 1746612311 }, + ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 }, ), // First Prague block ( Head { number: 20000004, timestamp: 1746612311, ..Default::default() }, ForkId { - hash: ForkHash([0xc3, 0x76, 0xcf, 0x8b]), + hash: ForkHash(hex!("0xc376cf8b")), next: mainnet::MAINNET_OSAKA_TIMESTAMP, }, ), @@ -1638,13 +1638,13 @@ Post-merge hard forks (timestamp based): &[ ( Head { number: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xbe, 0xf7, 0x1d, 0x30]), next: 1742999832 }, + ForkId { hash: ForkHash(hex!("0xbef71d30")), next: 1742999832 }, ), // First Prague block ( Head { number: 0, timestamp: 1742999833, ..Default::default() }, ForkId { - hash: ForkHash([0x09, 0x29, 0xe2, 0x4e]), + hash: ForkHash(hex!("0x0929e24e")), next: hoodi::HOODI_OSAKA_TIMESTAMP, }, ), @@ -1671,43 +1671,43 @@ Post-merge hard forks (timestamp based): &[ ( Head { number: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xc6, 0x1a, 0x60, 0x98]), next: 1696000704 }, + ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 }, ), // First MergeNetsplit block ( Head { number: 123, ..Default::default() }, - ForkId { hash: ForkHash([0xc6, 0x1a, 0x60, 0x98]), next: 1696000704 }, + ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 }, ), // Last MergeNetsplit block ( Head { number: 123, timestamp: 1696000703, ..Default::default() }, - ForkId { hash: ForkHash([0xc6, 0x1a, 0x60, 0x98]), next: 1696000704 }, + ForkId { hash: ForkHash(hex!("0xc61a6098")), next: 1696000704 }, ), // First Shanghai block ( Head { number: 123, timestamp: 1696000704, ..Default::default() }, - ForkId { hash: ForkHash([0xfd, 0x4f, 0x01, 0x6b]), next: 1707305664 }, + ForkId { hash: ForkHash(hex!("0xfd4f016b")), next: 1707305664 }, ), // Last Shanghai block ( Head { number: 123, timestamp: 1707305663, ..Default::default() }, - ForkId { hash: ForkHash([0xfd, 0x4f, 0x01, 0x6b]), next: 1707305664 }, + ForkId { hash: ForkHash(hex!("0xfd4f016b")), next: 1707305664 }, ), // First Cancun block ( Head { number: 123, timestamp: 1707305664, ..Default::default() }, - ForkId { hash: ForkHash([0x9b, 0x19, 0x2a, 0xd0]), next: 1740434112 }, + ForkId { hash: ForkHash(hex!("0x9b192ad0")), next: 1740434112 }, ), // Last Cancun block ( Head { number: 123, timestamp: 1740434111, ..Default::default() }, - ForkId { hash: ForkHash([0x9b, 0x19, 0x2a, 0xd0]), next: 1740434112 }, + ForkId { hash: ForkHash(hex!("0x9b192ad0")), next: 1740434112 }, ), // First Prague block ( Head { number: 123, timestamp: 1740434112, ..Default::default() }, ForkId { - hash: ForkHash([0xdf, 0xbd, 0x9b, 0xed]), + hash: ForkHash(hex!("0xdfbd9bed")), next: holesky::HOLESKY_OSAKA_TIMESTAMP, }, ), @@ -1734,45 +1734,45 @@ Post-merge hard forks (timestamp based): &[ ( Head { number: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( Head { number: 1735370, ..Default::default() }, - ForkId { hash: ForkHash([0xfe, 0x33, 0x66, 0xe7]), next: 1735371 }, + ForkId { hash: ForkHash(hex!("0xfe3366e7")), next: 1735371 }, ), ( Head { number: 1735371, ..Default::default() }, - ForkId { hash: ForkHash([0xb9, 0x6c, 0xbd, 0x13]), next: 1677557088 }, + ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 }, ), ( Head { number: 1735372, timestamp: 1677557087, ..Default::default() }, - ForkId { hash: ForkHash([0xb9, 0x6c, 0xbd, 0x13]), next: 1677557088 }, + ForkId { hash: ForkHash(hex!("0xb96cbd13")), next: 1677557088 }, ), // First Shanghai block ( Head { number: 1735373, timestamp: 1677557088, ..Default::default() }, - ForkId { hash: ForkHash([0xf7, 0xf9, 0xbc, 0x08]), next: 1706655072 }, + ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 }, ), // Last Shanghai block ( Head { number: 1735374, timestamp: 1706655071, ..Default::default() }, - ForkId { hash: ForkHash([0xf7, 0xf9, 0xbc, 0x08]), next: 1706655072 }, + ForkId { hash: ForkHash(hex!("0xf7f9bc08")), next: 1706655072 }, ), // First Cancun block ( Head { number: 1735375, timestamp: 1706655072, ..Default::default() }, - ForkId { hash: ForkHash([0x88, 0xcf, 0x81, 0xd9]), next: 1741159776 }, + ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 }, ), // Last Cancun block ( Head { number: 1735376, timestamp: 1741159775, ..Default::default() }, - ForkId { hash: ForkHash([0x88, 0xcf, 0x81, 0xd9]), next: 1741159776 }, + ForkId { hash: ForkHash(hex!("0x88cf81d9")), next: 1741159776 }, ), // First Prague block ( Head { number: 1735377, timestamp: 1741159776, ..Default::default() }, ForkId { - hash: ForkHash([0xed, 0x88, 0xb5, 0xfd]), + hash: ForkHash(hex!("0xed88b5fd")), next: sepolia::SEPOLIA_OSAKA_TIMESTAMP, }, ), @@ -1798,7 +1798,7 @@ Post-merge hard forks (timestamp based): &DEV, &[( Head { number: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x0b, 0x1a, 0x4e, 0xf7]), next: 0 }, + ForkId { hash: ForkHash(hex!("0x0b1a4ef7")), next: 0 }, )], ) } @@ -1814,128 +1814,128 @@ Post-merge hard forks (timestamp based): &[ ( Head { number: 0, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xfc, 0x64, 0xec, 0x04]), next: 1150000 }, + ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 }, ), // Unsynced ( Head { number: 1149999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xfc, 0x64, 0xec, 0x04]), next: 1150000 }, + ForkId { hash: ForkHash(hex!("0xfc64ec04")), next: 1150000 }, ), // Last Frontier block ( Head { number: 1150000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x97, 0xc2, 0xc3, 0x4c]), next: 1920000 }, + ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 }, ), // First Homestead block ( Head { number: 1919999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x97, 0xc2, 0xc3, 0x4c]), next: 1920000 }, + ForkId { hash: ForkHash(hex!("0x97c2c34c")), next: 1920000 }, ), // Last Homestead block ( Head { number: 1920000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x91, 0xd1, 0xf9, 0x48]), next: 2463000 }, + ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 }, ), // First DAO block ( Head { number: 2462999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x91, 0xd1, 0xf9, 0x48]), next: 2463000 }, + ForkId { hash: ForkHash(hex!("0x91d1f948")), next: 2463000 }, ), // Last DAO block ( Head { number: 2463000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x7a, 0x64, 0xda, 0x13]), next: 2675000 }, + ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 }, ), // First Tangerine block ( Head { number: 2674999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x7a, 0x64, 0xda, 0x13]), next: 2675000 }, + ForkId { hash: ForkHash(hex!("0x7a64da13")), next: 2675000 }, ), // Last Tangerine block ( Head { number: 2675000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x3e, 0xdd, 0x5b, 0x10]), next: 4370000 }, + ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 }, ), // First Spurious block ( Head { number: 4369999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x3e, 0xdd, 0x5b, 0x10]), next: 4370000 }, + ForkId { hash: ForkHash(hex!("0x3edd5b10")), next: 4370000 }, ), // Last Spurious block ( Head { number: 4370000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xa0, 0x0b, 0xc3, 0x24]), next: 7280000 }, + ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 }, ), // First Byzantium block ( Head { number: 7279999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xa0, 0x0b, 0xc3, 0x24]), next: 7280000 }, + ForkId { hash: ForkHash(hex!("0xa00bc324")), next: 7280000 }, ), // Last Byzantium block ( Head { number: 7280000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x66, 0x8d, 0xb0, 0xaf]), next: 9069000 }, + ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 }, ), // First and last Constantinople, first Petersburg block ( Head { number: 9068999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x66, 0x8d, 0xb0, 0xaf]), next: 9069000 }, + ForkId { hash: ForkHash(hex!("0x668db0af")), next: 9069000 }, ), // Last Petersburg block ( Head { number: 9069000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x87, 0x9d, 0x6e, 0x30]), next: 9200000 }, + ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 }, ), // First Istanbul and first Muir Glacier block ( Head { number: 9199999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x87, 0x9d, 0x6e, 0x30]), next: 9200000 }, + ForkId { hash: ForkHash(hex!("0x879d6e30")), next: 9200000 }, ), // Last Istanbul and first Muir Glacier block ( Head { number: 9200000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xe0, 0x29, 0xe9, 0x91]), next: 12244000 }, + ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 }, ), // First Muir Glacier block ( Head { number: 12243999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xe0, 0x29, 0xe9, 0x91]), next: 12244000 }, + ForkId { hash: ForkHash(hex!("0xe029e991")), next: 12244000 }, ), // Last Muir Glacier block ( Head { number: 12244000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x0e, 0xb4, 0x40, 0xf6]), next: 12965000 }, + ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 }, ), // First Berlin block ( Head { number: 12964999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x0e, 0xb4, 0x40, 0xf6]), next: 12965000 }, + ForkId { hash: ForkHash(hex!("0x0eb440f6")), next: 12965000 }, ), // Last Berlin block ( Head { number: 12965000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 13773000 }, + ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 }, ), // First London block ( Head { number: 13772999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 13773000 }, + ForkId { hash: ForkHash(hex!("0xb715077d")), next: 13773000 }, ), // Last London block ( Head { number: 13773000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x20, 0xc3, 0x27, 0xfc]), next: 15050000 }, + ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 }, ), // First Arrow Glacier block ( Head { number: 15049999, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0x20, 0xc3, 0x27, 0xfc]), next: 15050000 }, + ForkId { hash: ForkHash(hex!("0x20c327fc")), next: 15050000 }, ), // Last Arrow Glacier block ( Head { number: 15050000, timestamp: 0, ..Default::default() }, - ForkId { hash: ForkHash([0xf0, 0xaf, 0xd0, 0xe3]), next: 1681338455 }, + ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 }, ), // First Gray Glacier block ( Head { number: 19999999, timestamp: 1667999999, ..Default::default() }, - ForkId { hash: ForkHash([0xf0, 0xaf, 0xd0, 0xe3]), next: 1681338455 }, + ForkId { hash: ForkHash(hex!("0xf0afd0e3")), next: 1681338455 }, ), // Last Gray Glacier block ( Head { number: 20000000, timestamp: 1681338455, ..Default::default() }, - ForkId { hash: ForkHash([0xdc, 0xe9, 0x6c, 0x2d]), next: 1710338135 }, + ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 }, ), // Last Shanghai block ( Head { number: 20000001, timestamp: 1710338134, ..Default::default() }, - ForkId { hash: ForkHash([0xdc, 0xe9, 0x6c, 0x2d]), next: 1710338135 }, + ForkId { hash: ForkHash(hex!("0xdce96c2d")), next: 1710338135 }, ), // First Cancun block ( Head { number: 20000002, timestamp: 1710338135, ..Default::default() }, - ForkId { hash: ForkHash([0x9f, 0x3d, 0x22, 0x54]), next: 1746612311 }, + ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 }, ), // Last Cancun block ( Head { number: 20000003, timestamp: 1746612310, ..Default::default() }, - ForkId { hash: ForkHash([0x9f, 0x3d, 0x22, 0x54]), next: 1746612311 }, + ForkId { hash: ForkHash(hex!("0x9f3d2254")), next: 1746612311 }, ), // First Prague block ( Head { number: 20000004, timestamp: 1746612311, ..Default::default() }, ForkId { - hash: ForkHash([0xc3, 0x76, 0xcf, 0x8b]), + hash: ForkHash(hex!("0xc376cf8b")), next: mainnet::MAINNET_OSAKA_TIMESTAMP, }, ), @@ -2405,7 +2405,7 @@ Post-merge hard forks (timestamp based): let chainspec = ChainSpec::from(genesis); // make sure we are at ForkHash("bc0c2605") with Head post-cancun - let expected_forkid = ForkId { hash: ForkHash([0xbc, 0x0c, 0x26, 0x05]), next: 0 }; + let expected_forkid = ForkId { hash: ForkHash(hex!("0xbc0c2605")), next: 0 }; let got_forkid = chainspec.fork_id(&Head { number: 73, timestamp: 840, ..Default::default() }); @@ -2515,7 +2515,7 @@ Post-merge hard forks (timestamp based): assert_eq!(genesis_hash, expected_hash); // check that the forkhash is correct - let expected_forkhash = ForkHash(hex!("8062457a")); + let expected_forkhash = ForkHash(hex!("0x8062457a")); assert_eq!(ForkHash::from(genesis_hash), expected_forkhash); } diff --git a/crates/cli/commands/src/common.rs b/crates/cli/commands/src/common.rs index 4d18d81184..2fb1f71086 100644 --- a/crates/cli/commands/src/common.rs +++ b/crates/cli/commands/src/common.rs @@ -7,7 +7,7 @@ use reth_cli::chainspec::ChainSpecParser; use reth_config::{config::EtlConfig, Config}; use reth_consensus::noop::NoopConsensus; use reth_db::{init_db, open_db_read_only, DatabaseEnv}; -use reth_db_common::init::init_genesis; +use reth_db_common::init::init_genesis_with_settings; use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader}; use reth_eth_wire::NetPrimitivesFor; use reth_evm::{noop::NoopEvmConfig, ConfigureEvm}; @@ -17,7 +17,7 @@ use reth_node_builder::{ Node, NodeComponents, NodeComponentsBuilder, NodeTypes, NodeTypesWithDBAdapter, }; use reth_node_core::{ - args::{DatabaseArgs, DatadirArgs}, + args::{DatabaseArgs, DatadirArgs, StaticFilesArgs}, dirs::{ChainPath, DataDirPath}, }; use reth_provider::{ @@ -57,6 +57,10 @@ pub struct EnvironmentArgs { /// All database related arguments #[command(flatten)] pub db: DatabaseArgs, + + /// All static files related arguments + #[command(flatten)] + pub static_files: StaticFilesArgs, } impl EnvironmentArgs { @@ -97,16 +101,16 @@ impl EnvironmentArgs { Arc::new(init_db(db_path, self.db.database_args())?), StaticFileProvider::read_write(sf_path)?, ), - AccessRights::RO => ( + AccessRights::RO | AccessRights::RoInconsistent => ( Arc::new(open_db_read_only(&db_path, self.db.database_args())?), StaticFileProvider::read_only(sf_path, false)?, ), }; - let provider_factory = self.create_provider_factory(&config, db, sfp)?; + let provider_factory = self.create_provider_factory(&config, db, sfp, access)?; if access.is_read_write() { debug!(target: "reth::cli", chain=%self.chain.chain(), genesis=?self.chain.genesis_hash(), "Initializing genesis"); - init_genesis(&provider_factory)?; + init_genesis_with_settings(&provider_factory, self.static_files.to_settings())?; } Ok(Environment { config, provider_factory, data_dir }) @@ -122,11 +126,11 @@ impl EnvironmentArgs { config: &Config, db: Arc, static_file_provider: StaticFileProvider, + access: AccessRights, ) -> eyre::Result>>> where C: ChainSpecParser, { - let has_receipt_pruning = config.prune.has_receipts_pruning(); let prune_modes = config.prune.segments.clone(); let factory = ProviderFactory::>>::new( db, @@ -136,9 +140,9 @@ impl EnvironmentArgs { .with_prune_modes(prune_modes.clone()); // Check for consistency between database and static files. - if let Some(unwind_target) = factory - .static_file_provider() - .check_consistency(&factory.provider()?, has_receipt_pruning)? + if !access.is_read_only_inconsistent() && + let Some(unwind_target) = + factory.static_file_provider().check_consistency(&factory.provider()?)? { if factory.db_ref().is_read_only()? { warn!(target: "reth::cli", ?unwind_target, "Inconsistent storage. Restart node to heal."); @@ -199,6 +203,8 @@ pub enum AccessRights { RW, /// Read-only access RO, + /// Read-only access with possibly inconsistent data + RoInconsistent, } impl AccessRights { @@ -206,6 +212,12 @@ impl AccessRights { pub const fn is_read_write(&self) -> bool { matches!(self, Self::RW) } + + /// Returns `true` if it requires read-only access to the environment with possibly inconsistent + /// data. + pub const fn is_read_only_inconsistent(&self) -> bool { + matches!(self, Self::RoInconsistent) + } } /// Helper alias to satisfy `FullNodeTypes` bound on [`Node`] trait generic. diff --git a/crates/cli/commands/src/config_cmd.rs b/crates/cli/commands/src/config_cmd.rs index f3a24e267c..e12f468fac 100644 --- a/crates/cli/commands/src/config_cmd.rs +++ b/crates/cli/commands/src/config_cmd.rs @@ -22,13 +22,14 @@ impl Command { let config = if self.default { Config::default() } else { - let path = self.config.clone().unwrap_or_default(); - // Check if the file exists + let path = match self.config.as_ref() { + Some(path) => path, + None => bail!("No config file provided. Use --config or pass --default"), + }; if !path.exists() { bail!("Config file does not exist: {}", path.display()); } - // Read the configuration file - Config::from_path(&path) + Config::from_path(path) .wrap_err_with(|| format!("Could not load config file: {}", path.display()))? }; println!("{}", toml::to_string_pretty(&config)?); diff --git a/crates/cli/commands/src/db/clear.rs b/crates/cli/commands/src/db/clear.rs index fc3852154f..4ba0a63df8 100644 --- a/crates/cli/commands/src/db/clear.rs +++ b/crates/cli/commands/src/db/clear.rs @@ -6,8 +6,9 @@ use reth_db_api::{ transaction::{DbTx, DbTxMut}, TableViewer, Tables, }; +use reth_db_common::DbTool; use reth_node_builder::NodeTypesWithDB; -use reth_provider::{ProviderFactory, StaticFileProviderFactory}; +use reth_provider::StaticFileProviderFactory; use reth_static_file_types::StaticFileSegment; /// The arguments for the `reth db clear` command @@ -19,16 +20,13 @@ pub struct Command { impl Command { /// Execute `db clear` command - pub fn execute( - self, - provider_factory: ProviderFactory, - ) -> eyre::Result<()> { + pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { match self.subcommand { Subcommands::Mdbx { table } => { - table.view(&ClearViewer { db: provider_factory.db_ref() })? + table.view(&ClearViewer { db: tool.provider_factory.db_ref() })? } Subcommands::StaticFile { segment } => { - let static_file_provider = provider_factory.static_file_provider(); + let static_file_provider = tool.provider_factory.static_file_provider(); let static_files = iter_static_files(static_file_provider.directory())?; if let Some(segment_static_files) = static_files.get(&segment) { diff --git a/crates/cli/commands/src/db/list.rs b/crates/cli/commands/src/db/list.rs index 2540e77c11..5d6c055c94 100644 --- a/crates/cli/commands/src/db/list.rs +++ b/crates/cli/commands/src/db/list.rs @@ -3,7 +3,7 @@ use alloy_primitives::hex; use clap::Parser; use eyre::WrapErr; use reth_chainspec::EthereumHardforks; -use reth_db::DatabaseEnv; +use reth_db::{transaction::DbTx, DatabaseEnv}; use reth_db_api::{database::Database, table::Table, RawValue, TableViewer, Tables}; use reth_db_common::{DbTool, ListFilter}; use reth_node_builder::{NodeTypes, NodeTypesWithDBAdapter}; @@ -96,6 +96,9 @@ impl TableViewer<()> for ListTableViewer<'_, N> { fn view(&self) -> Result<(), Self::Error> { self.tool.provider_factory.db_ref().view(|tx| { + // We may be using the tui for a long time + tx.disable_long_read_transaction_safety(); + let table_db = tx.inner.open_db(Some(self.args.table.name())).wrap_err("Could not open db.")?; let stats = tx.inner.db_stat(&table_db).wrap_err(format!("Could not find table: {}", self.args.table.name()))?; let total_entries = stats.entries(); diff --git a/crates/cli/commands/src/db/mod.rs b/crates/cli/commands/src/db/mod.rs index 1ea66b2f55..9a67bca5be 100644 --- a/crates/cli/commands/src/db/mod.rs +++ b/crates/cli/commands/src/db/mod.rs @@ -14,6 +14,8 @@ mod diff; mod get; mod list; mod repair_trie; +mod settings; +mod static_file_header; mod stats; /// DB List TUI mod tui; @@ -51,16 +53,21 @@ pub enum Subcommands { Clear(clear::Command), /// Verifies trie consistency and outputs any inconsistencies RepairTrie(repair_trie::Command), + /// Reads and displays the static file segment header + StaticFileHeader(static_file_header::Command), /// Lists current and local database versions Version, /// Returns the full database path Path, + /// Manage storage settings + Settings(settings::Command), } -/// `db_ro_exec` opens a database in read-only mode, and then execute with the provided command -macro_rules! db_ro_exec { - ($env:expr, $tool:ident, $N:ident, $command:block) => { - let Environment { provider_factory, .. } = $env.init::<$N>(AccessRights::RO)?; +/// Initializes a provider factory with specified access rights, and then execute with the provided +/// command +macro_rules! db_exec { + ($env:expr, $tool:ident, $N:ident, $access_rights:expr, $command:block) => { + let Environment { provider_factory, .. } = $env.init::<$N>($access_rights)?; let $tool = DbTool::new(provider_factory)?; $command; @@ -88,27 +95,32 @@ impl> Command match self.command { // TODO: We'll need to add this on the DB trait. Subcommands::Stats(command) => { - db_ro_exec!(self.env, tool, N, { + let access_rights = if command.skip_consistency_checks { + AccessRights::RoInconsistent + } else { + AccessRights::RO + }; + db_exec!(self.env, tool, N, access_rights, { command.execute(data_dir, &tool)?; }); } Subcommands::List(command) => { - db_ro_exec!(self.env, tool, N, { + db_exec!(self.env, tool, N, AccessRights::RO, { command.execute(&tool)?; }); } Subcommands::Checksum(command) => { - db_ro_exec!(self.env, tool, N, { + db_exec!(self.env, tool, N, AccessRights::RO, { command.execute(&tool)?; }); } Subcommands::Diff(command) => { - db_ro_exec!(self.env, tool, N, { + db_exec!(self.env, tool, N, AccessRights::RO, { command.execute(&tool)?; }); } Subcommands::Get(command) => { - db_ro_exec!(self.env, tool, N, { + db_exec!(self.env, tool, N, AccessRights::RO, { command.execute(&tool)?; }); } @@ -130,19 +142,26 @@ impl> Command } } - let Environment { provider_factory, .. } = self.env.init::(AccessRights::RW)?; - let tool = DbTool::new(provider_factory)?; - tool.drop(db_path, static_files_path, exex_wal_path)?; + db_exec!(self.env, tool, N, AccessRights::RW, { + tool.drop(db_path, static_files_path, exex_wal_path)?; + }); } Subcommands::Clear(command) => { - let Environment { provider_factory, .. } = self.env.init::(AccessRights::RW)?; - command.execute(provider_factory)?; + db_exec!(self.env, tool, N, AccessRights::RW, { + command.execute(&tool)?; + }); } Subcommands::RepairTrie(command) => { let access_rights = if command.dry_run { AccessRights::RO } else { AccessRights::RW }; - let Environment { provider_factory, .. } = self.env.init::(access_rights)?; - command.execute(provider_factory)?; + db_exec!(self.env, tool, N, access_rights, { + command.execute(&tool)?; + }); + } + Subcommands::StaticFileHeader(command) => { + db_exec!(self.env, tool, N, AccessRights::RoInconsistent, { + command.execute(&tool)?; + }); } Subcommands::Version => { let local_db_version = match get_db_version(&db_path) { @@ -162,6 +181,11 @@ impl> Command Subcommands::Path => { println!("{}", db_path.display()); } + Subcommands::Settings(command) => { + db_exec!(self.env, tool, N, command.access_rights(), { + command.execute(&tool)?; + }); + } } Ok(()) diff --git a/crates/cli/commands/src/db/repair_trie.rs b/crates/cli/commands/src/db/repair_trie.rs index f7dea67b76..ade18a3ff8 100644 --- a/crates/cli/commands/src/db/repair_trie.rs +++ b/crates/cli/commands/src/db/repair_trie.rs @@ -5,8 +5,9 @@ use reth_db_api::{ tables, transaction::{DbTx, DbTxMut}, }; +use reth_db_common::DbTool; use reth_node_builder::NodeTypesWithDB; -use reth_provider::{providers::ProviderNodeTypes, ProviderFactory, StageCheckpointReader}; +use reth_provider::{providers::ProviderNodeTypes, StageCheckpointReader}; use reth_stages::StageId; use reth_trie::{ verify::{Output, Verifier}, @@ -29,23 +30,20 @@ pub struct Command { impl Command { /// Execute `db repair-trie` command - pub fn execute( - self, - provider_factory: ProviderFactory, - ) -> eyre::Result<()> { + pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { if self.dry_run { - verify_only(provider_factory)? + verify_only(tool)? } else { - verify_and_repair(provider_factory)? + verify_and_repair(tool)? } Ok(()) } } -fn verify_only(provider_factory: ProviderFactory) -> eyre::Result<()> { +fn verify_only(tool: &DbTool) -> eyre::Result<()> { // Get a database transaction directly from the database - let db = provider_factory.db_ref(); + let db = tool.provider_factory.db_ref(); let mut tx = db.tx()?; tx.disable_long_read_transaction_safety(); @@ -114,11 +112,9 @@ fn verify_checkpoints(provider: impl StageCheckpointReader) -> eyre::Result<()> Ok(()) } -fn verify_and_repair( - provider_factory: ProviderFactory, -) -> eyre::Result<()> { +fn verify_and_repair(tool: &DbTool) -> eyre::Result<()> { // Get a read-write database provider - let mut provider_rw = provider_factory.provider_rw()?; + let mut provider_rw = tool.provider_factory.provider_rw()?; // Check that a pipeline sync isn't in progress. verify_checkpoints(provider_rw.as_ref())?; diff --git a/crates/cli/commands/src/db/settings.rs b/crates/cli/commands/src/db/settings.rs new file mode 100644 index 0000000000..6b5169aeda --- /dev/null +++ b/crates/cli/commands/src/db/settings.rs @@ -0,0 +1,109 @@ +//! `reth db settings` command for managing storage settings + +use clap::{ArgAction, Parser, Subcommand}; +use reth_db_common::DbTool; +use reth_provider::{ + providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory, MetadataProvider, + MetadataWriter, StorageSettings, +}; + +use crate::common::AccessRights; + +/// `reth db settings` subcommand +#[derive(Debug, Parser)] +pub struct Command { + #[command(subcommand)] + command: Subcommands, +} + +impl Command { + /// Returns database access rights required for the command. + pub fn access_rights(&self) -> AccessRights { + match self.command { + Subcommands::Get => AccessRights::RO, + Subcommands::Set(_) => AccessRights::RW, + } + } +} + +#[derive(Debug, Clone, Copy, Subcommand)] +enum Subcommands { + /// Get current storage settings from database + Get, + /// Set storage settings in database + #[clap(subcommand)] + Set(SetCommand), +} + +/// Set storage settings +#[derive(Debug, Clone, Copy, Subcommand)] +#[clap(rename_all = "snake_case")] +pub enum SetCommand { + /// Store receipts in static files instead of the database + ReceiptsInStaticFiles { + #[clap(action(ArgAction::Set))] + value: bool, + }, +} + +impl Command { + /// Execute the command + pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { + match self.command { + Subcommands::Get => self.get(tool), + Subcommands::Set(cmd) => self.set(cmd, tool), + } + } + + fn get(&self, tool: &DbTool) -> eyre::Result<()> { + // Read storage settings + let provider = tool.provider_factory.provider()?; + let storage_settings = provider.storage_settings()?; + + // Display settings + match storage_settings { + Some(settings) => { + println!("Current storage settings:"); + println!("{settings:#?}"); + } + None => { + println!("No storage settings found."); + } + } + + Ok(()) + } + + fn set(&self, cmd: SetCommand, tool: &DbTool) -> eyre::Result<()> { + // Read storage settings + let provider_rw = tool.provider_factory.database_provider_rw()?; + // Destruct settings struct to not miss adding support for new fields + let settings = provider_rw.storage_settings()?; + if settings.is_none() { + println!("No storage settings found, creating new settings."); + } + + let mut settings @ StorageSettings { receipts_in_static_files: _ } = + settings.unwrap_or_default(); + + // Update the setting based on the key + match cmd { + SetCommand::ReceiptsInStaticFiles { value } => { + if settings.receipts_in_static_files == value { + println!("receipts_in_static_files is already set to {}", value); + return Ok(()); + } + settings.receipts_in_static_files = value; + println!("Set receipts_in_static_files = {}", value); + } + } + + // Write updated settings + provider_rw.write_storage_settings(settings)?; + provider_rw.commit()?; + + println!("Storage settings updated successfully."); + + Ok(()) + } +} diff --git a/crates/cli/commands/src/db/static_file_header.rs b/crates/cli/commands/src/db/static_file_header.rs new file mode 100644 index 0000000000..4c0ff27464 --- /dev/null +++ b/crates/cli/commands/src/db/static_file_header.rs @@ -0,0 +1,63 @@ +use clap::{Parser, Subcommand}; +use reth_db_common::DbTool; +use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory}; +use reth_static_file_types::StaticFileSegment; +use std::path::PathBuf; +use tracing::warn; + +/// The arguments for the `reth db static-file-header` command +#[derive(Parser, Debug)] +pub struct Command { + #[command(subcommand)] + source: Source, +} + +/// Source for locating the static file +#[derive(Subcommand, Debug)] +enum Source { + /// Query by segment and block number + Block { + /// Static file segment + #[arg(value_enum)] + segment: StaticFileSegment, + /// Block number to query + block: u64, + }, + /// Query by path to static file + Path { + /// Path to the static file + path: PathBuf, + }, +} + +impl Command { + /// Execute `db static-file-header` command + pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { + let static_file_provider = tool.provider_factory.static_file_provider(); + if let Err(err) = static_file_provider.check_consistency(&tool.provider_factory.provider()?) + { + warn!("Error checking consistency of static files: {err}"); + } + + // Get the provider based on the source + let provider = match self.source { + Source::Path { path } => { + static_file_provider.get_segment_provider_for_path(&path)?.ok_or_else(|| { + eyre::eyre!("Could not find static file segment for path: {}", path.display()) + })? + } + Source::Block { segment, block } => { + static_file_provider.get_segment_provider(segment, block)? + } + }; + + let header = provider.user_header(); + + println!("Segment: {}", header.segment()); + println!("Expected Block Range: {}", header.expected_block_range()); + println!("Block Range: {:?}", header.block_range()); + println!("Transaction Range: {:?}", header.tx_range()); + + Ok(()) + } +} diff --git a/crates/cli/commands/src/db/stats.rs b/crates/cli/commands/src/db/stats.rs index 2aef43c582..e225b2f991 100644 --- a/crates/cli/commands/src/db/stats.rs +++ b/crates/cli/commands/src/db/stats.rs @@ -18,6 +18,10 @@ use std::{sync::Arc, time::Duration}; #[derive(Parser, Debug)] /// The arguments for the `reth db stats` command pub struct Command { + /// Skip consistency checks for static files. + #[arg(long, default_value_t = false)] + pub(crate) skip_consistency_checks: bool, + /// Show only the total size for static files. #[arg(long, default_value_t = false)] detailed_sizes: bool, diff --git a/crates/cli/commands/src/node.rs b/crates/cli/commands/src/node.rs index 240bb3c289..cba857a3a8 100644 --- a/crates/cli/commands/src/node.rs +++ b/crates/cli/commands/src/node.rs @@ -10,7 +10,7 @@ use reth_node_builder::NodeBuilder; use reth_node_core::{ args::{ DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs, - NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, TxPoolArgs, + NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs, }, node_config::NodeConfig, version, @@ -110,6 +110,10 @@ pub struct NodeCommand, { - tracing::info!(target: "reth::cli", version = ?version::version_metadata().short_version, "Starting reth"); + tracing::info!(target: "reth::cli", version = ?version::version_metadata().short_version, "Starting {}", version::version_metadata().name_client); let Self { datadir, @@ -162,9 +166,10 @@ where db, dev, pruning, - ext, engine, era, + static_files, + ext, } = self; // set up node config @@ -184,6 +189,7 @@ where pruning, engine, era, + static_files, }; let data_dir = node_config.datadir(); diff --git a/crates/cli/commands/src/p2p/bootnode.rs b/crates/cli/commands/src/p2p/bootnode.rs index 8e4fb5ad2d..2a6932351b 100644 --- a/crates/cli/commands/src/p2p/bootnode.rs +++ b/crates/cli/commands/src/p2p/bootnode.rs @@ -60,7 +60,7 @@ impl Command { if self.v5 { info!("Starting discv5"); let config = Config::builder(self.addr).build(); - let (_discv5, updates, _local_enr_discv5) = Discv5::start(&sk, config).await?; + let (_discv5, updates) = Discv5::start(&sk, config).await?; discv5_updates = Some(updates); }; diff --git a/crates/cli/commands/src/p2p/mod.rs b/crates/cli/commands/src/p2p/mod.rs index c72ceca78e..d255f5fd77 100644 --- a/crates/cli/commands/src/p2p/mod.rs +++ b/crates/cli/commands/src/p2p/mod.rs @@ -8,7 +8,7 @@ use backon::{ConstantBuilder, Retryable}; use clap::{Parser, Subcommand}; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; use reth_cli::chainspec::ChainSpecParser; -use reth_cli_util::{get_secret_key, hash_or_num_value_parser}; +use reth_cli_util::hash_or_num_value_parser; use reth_config::Config; use reth_network::{BlockDownloaderProvider, NetworkConfigBuilder}; use reth_network_p2p::bodies::client::BodiesClient; @@ -183,9 +183,7 @@ impl DownloadArgs { config.peers.trusted_nodes_only = self.network.trusted_only; let default_secret_key_path = data_dir.p2p_secret(); - let secret_key_path = - self.network.p2p_secret_key.clone().unwrap_or(default_secret_key_path); - let p2p_secret_key = get_secret_key(&secret_key_path)?; + let p2p_secret_key = self.network.secret_key(default_secret_key_path)?; let rlpx_socket = (self.network.addr, self.network.port).into(); let boot_nodes = self.chain.bootnodes().unwrap_or_default(); diff --git a/crates/cli/commands/src/re_execute.rs b/crates/cli/commands/src/re_execute.rs index 63359e2c06..8223ca0120 100644 --- a/crates/cli/commands/src/re_execute.rs +++ b/crates/cli/commands/src/re_execute.rs @@ -9,6 +9,7 @@ use clap::Parser; use eyre::WrapErr; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; use reth_cli::chainspec::ChainSpecParser; +use reth_cli_util::cancellation::CancellationToken; use reth_consensus::FullConsensus; use reth_evm::{execute::Executor, ConfigureEvm}; use reth_primitives_traits::{format_gas_throughput, BlockBody, GotExpected}; @@ -44,6 +45,10 @@ pub struct Command { /// Number of tasks to run in parallel #[arg(long, default_value = "10")] num_tasks: u64, + + /// Continues with execution when an invalid block is encountered and collects these blocks. + #[arg(long)] + skip_invalid_blocks: bool, } impl Command { @@ -61,11 +66,11 @@ impl { let Environment { provider_factory, .. } = self.env.init::(AccessRights::RO)?; - let provider = provider_factory.database_provider_ro()?; let components = components(provider_factory.chain_spec()); let min_block = self.from; - let best_block = provider.best_block_number()?; + let best_block = DatabaseProviderFactory::database_provider_ro(&provider_factory)? + .best_block_number()?; let mut max_block = best_block; if let Some(to) = self.to { if to > best_block { @@ -95,7 +100,11 @@ impl } }; + let skip_invalid_blocks = self.skip_invalid_blocks; let (stats_tx, mut stats_rx) = mpsc::unbounded_channel(); + let (info_tx, mut info_rx) = mpsc::unbounded_channel(); + let cancellation = CancellationToken::new(); + let _guard = cancellation.drop_guard(); let mut tasks = JoinSet::new(); for i in 0..self.num_tasks { @@ -109,17 +118,40 @@ impl let consensus = components.consensus().clone(); let db_at = db_at.clone(); let stats_tx = stats_tx.clone(); + let info_tx = info_tx.clone(); + let cancellation = cancellation.clone(); tasks.spawn_blocking(move || { let mut executor = evm_config.batch_executor(db_at(start_block - 1)); - for block in start_block..end_block { + let mut executor_created = Instant::now(); + let executor_lifetime = Duration::from_secs(120); + + 'blocks: for block in start_block..end_block { + if cancellation.is_cancelled() { + // exit if the program is being terminated + break + } + let block = provider_factory .recovered_block(block.into(), TransactionVariant::NoHash)? .unwrap(); - let result = executor.execute_one(&block)?; + + let result = match executor.execute_one(&block) { + Ok(result) => result, + Err(err) => { + if skip_invalid_blocks { + executor = evm_config.batch_executor(db_at(block.number())); + let _ = info_tx.send((block, eyre::Report::new(err))); + continue + } + return Err(err.into()) + } + }; if let Err(err) = consensus .validate_block_post_execution(&block, &result) - .wrap_err_with(|| format!("Failed to validate block {}", block.number())) + .wrap_err_with(|| { + format!("Failed to validate block {} {}", block.number(), block.hash()) + }) { let correct_receipts = provider_factory.receipts_by_block(block.number().into())?.unwrap(); @@ -155,6 +187,11 @@ impl }; error!(number=?block.number(), ?mismatch, "Gas usage mismatch"); + if skip_invalid_blocks { + executor = evm_config.batch_executor(db_at(block.number())); + let _ = info_tx.send((block, err)); + continue 'blocks; + } return Err(err); } } else { @@ -166,9 +203,12 @@ impl } let _ = stats_tx.send(block.gas_used()); - // Reset DB once in a while to avoid OOM - if executor.size_hint() > 1_000_000 { + // Reset DB once in a while to avoid OOM or read tx timeouts + if executor.size_hint() > 1_000_000 || + executor_created.elapsed() > executor_lifetime + { executor = evm_config.batch_executor(db_at(block.number())); + executor_created = Instant::now(); } } @@ -183,6 +223,7 @@ impl let mut last_logged_gas = 0; let mut last_logged_blocks = 0; let mut last_logged_time = Instant::now(); + let mut invalid_blocks = Vec::new(); let mut interval = tokio::time::interval(Duration::from_secs(10)); @@ -192,6 +233,10 @@ impl total_executed_blocks += 1; total_executed_gas += gas_used; } + Some((block, err)) = info_rx.recv() => { + error!(?err, block=?block.num_hash(), "Invalid block"); + invalid_blocks.push(block.num_hash()); + } result = tasks.join_next() => { if let Some(result) = result { if matches!(result, Err(_) | Ok(Err(_))) { @@ -222,12 +267,25 @@ impl } } - info!( - start_block = min_block, - end_block = max_block, - throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()), - "Re-executed successfully" - ); + if invalid_blocks.is_empty() { + info!( + start_block = min_block, + end_block = max_block, + %total_executed_blocks, + throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()), + "Re-executed successfully" + ); + } else { + info!( + start_block = min_block, + end_block = max_block, + %total_executed_blocks, + invalid_block_count = invalid_blocks.len(), + ?invalid_blocks, + throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()), + "Re-executed with invalid blocks" + ); + } Ok(()) } diff --git a/crates/cli/commands/src/stage/drop.rs b/crates/cli/commands/src/stage/drop.rs index d2ad088ada..fa4be99cb4 100644 --- a/crates/cli/commands/src/stage/drop.rs +++ b/crates/cli/commands/src/stage/drop.rs @@ -1,10 +1,9 @@ //! Database debugging tool use crate::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs}; use clap::Parser; -use itertools::Itertools; use reth_chainspec::EthChainSpec; use reth_cli::chainspec::ChainSpecParser; -use reth_db::{mdbx::tx::Tx, static_file::iter_static_files, DatabaseError}; +use reth_db::{mdbx::tx::Tx, DatabaseError}; use reth_db_api::{ tables, transaction::{DbTx, DbTxMut}, @@ -15,7 +14,9 @@ use reth_db_common::{ }; use reth_node_api::{HeaderTy, ReceiptTy, TxTy}; use reth_node_core::args::StageEnum; -use reth_provider::{DBProvider, DatabaseProviderFactory, StaticFileProviderFactory, TrieWriter}; +use reth_provider::{ + DBProvider, DatabaseProviderFactory, StaticFileProviderFactory, StaticFileWriter, TrieWriter, +}; use reth_prune::PruneSegment; use reth_stages::StageId; use reth_static_file_types::StaticFileSegment; @@ -47,18 +48,37 @@ impl Command { _ => None, }; - // Delete static file segment data before inserting the genesis header below + // Calling `StaticFileProviderRW::prune_*` will instruct the writer to prune rows only + // when `StaticFileProviderRW::commit` is called. We need to do that instead of + // deleting the jar files, otherwise if the task were to be interrupted after we + // have deleted them, BUT before we have committed the checkpoints to the database, we'd + // lose essential data. if let Some(static_file_segment) = static_file_segment { let static_file_provider = tool.provider_factory.static_file_provider(); - let static_files = iter_static_files(static_file_provider.directory())?; - if let Some(segment_static_files) = static_files.get(&static_file_segment) { - // Delete static files from the highest to the lowest block range - for (block_range, _) in segment_static_files - .iter() - .sorted_by_key(|(block_range, _)| block_range.start()) - .rev() - { - static_file_provider.delete_jar(static_file_segment, block_range.start())?; + if let Some(highest_block) = + static_file_provider.get_highest_static_file_block(static_file_segment) + { + let mut writer = static_file_provider.latest_writer(static_file_segment)?; + + match static_file_segment { + StaticFileSegment::Headers => { + // Prune all headers leaving genesis intact. + writer.prune_headers(highest_block)?; + } + StaticFileSegment::Transactions => { + let to_delete = static_file_provider + .get_highest_static_file_tx(static_file_segment) + .map(|tx| tx + 1) + .unwrap_or_default(); + writer.prune_transactions(to_delete, 0)?; + } + StaticFileSegment::Receipts => { + let to_delete = static_file_provider + .get_highest_static_file_tx(static_file_segment) + .map(|receipt| receipt + 1) + .unwrap_or_default(); + writer.prune_receipts(to_delete, 0)?; + } } } } diff --git a/crates/cli/commands/src/stage/run.rs b/crates/cli/commands/src/stage/run.rs index f25338d30e..202506be9c 100644 --- a/crates/cli/commands/src/stage/run.rs +++ b/crates/cli/commands/src/stage/run.rs @@ -84,6 +84,9 @@ pub struct Command { /// Commits the changes in the database. WARNING: potentially destructive. /// /// Useful when you want to run diagnostics on the database. + /// + /// NOTE: This flag is currently required for the headers, bodies, and execution stages because + /// they use static files and must commit to properly unwind and run. // TODO: We should consider allowing to run hooks at the end of the stage run, // e.g. query the DB size, or any table data. #[arg(long, short)] @@ -105,6 +108,14 @@ impl Comp: CliNodeComponents, F: FnOnce(Arc) -> Comp, { + // Quit early if the stages requires a commit and `--commit` is not provided. + if self.requires_commit() && !self.commit { + return Err(eyre::eyre!( + "The stage {} requires overwriting existing static files and must commit, but `--commit` was not provided. Please pass `--commit` and try again.", + self.stage.to_string() + )); + } + // Raise the fd limit of the process. // Does not do anything on windows. let _ = fdlimit::raise_fd_limit(); @@ -383,4 +394,13 @@ impl Command { pub fn chain_spec(&self) -> Option<&Arc> { Some(&self.env.chain) } + + /// Returns whether or not the configured stage requires committing. + /// + /// This is the case for stages that mainly modify static files, as there is no way to unwind + /// these stages without committing anyways. This is because static files do not have + /// transactions and we cannot change the view of headers without writing. + pub fn requires_commit(&self) -> bool { + matches!(self.stage, StageEnum::Headers | StageEnum::Bodies | StageEnum::Execution) + } } diff --git a/crates/cli/util/src/cancellation.rs b/crates/cli/util/src/cancellation.rs new file mode 100644 index 0000000000..31f3446ef2 --- /dev/null +++ b/crates/cli/util/src/cancellation.rs @@ -0,0 +1,103 @@ +//! Thread-safe cancellation primitives for cooperative task cancellation. + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +/// A thread-safe cancellation token that can be shared across threads. +/// +/// This token allows cooperative cancellation by providing a way to signal +/// cancellation and check cancellation status. The token can be cloned and +/// shared across multiple threads, with all clones sharing the same cancellation state. +/// +/// # Example +/// +/// ``` +/// use reth_cli_util::cancellation::CancellationToken; +/// use std::{thread, time::Duration}; +/// +/// let token = CancellationToken::new(); +/// let worker_token = token.clone(); +/// +/// let handle = thread::spawn(move || { +/// while !worker_token.is_cancelled() { +/// // Do work... +/// thread::sleep(Duration::from_millis(100)); +/// } +/// }); +/// +/// // Cancel from main thread +/// token.cancel(); +/// handle.join().unwrap(); +/// ``` +#[derive(Clone, Debug)] +pub struct CancellationToken { + cancelled: Arc, +} + +impl CancellationToken { + /// Creates a new cancellation token in the non-cancelled state. + pub fn new() -> Self { + Self { cancelled: Arc::new(AtomicBool::new(false)) } + } + + /// Signals cancellation to all holders of this token and its clones. + /// + /// Once cancelled, the token cannot be reset. This operation is thread-safe + /// and can be called multiple times without issue. + pub fn cancel(&self) { + self.cancelled.store(true, Ordering::Release); + } + + /// Checks whether cancellation has been requested. + /// + /// Returns `true` if [`cancel`](Self::cancel) has been called on this token + /// or any of its clones. + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Ordering::Relaxed) + } + + /// Creates a guard that automatically cancels this token when dropped. + /// + /// This is useful for ensuring cancellation happens when a scope exits, + /// either normally or via panic. + /// + /// # Example + /// + /// ``` + /// use reth_cli_util::cancellation::CancellationToken; + /// + /// let token = CancellationToken::new(); + /// { + /// let _guard = token.drop_guard(); + /// assert!(!token.is_cancelled()); + /// // Guard dropped here, triggering cancellation + /// } + /// assert!(token.is_cancelled()); + /// ``` + pub fn drop_guard(&self) -> CancellationGuard { + CancellationGuard { token: self.clone() } + } +} + +impl Default for CancellationToken { + fn default() -> Self { + Self::new() + } +} + +/// A guard that cancels its associated [`CancellationToken`] when dropped. +/// +/// Created by calling [`CancellationToken::drop_guard`]. When this guard is dropped, +/// it automatically calls [`cancel`](CancellationToken::cancel) on the token. +#[derive(Debug)] +pub struct CancellationGuard { + token: CancellationToken, +} + +impl Drop for CancellationGuard { + fn drop(&mut self) { + self.token.cancel(); + } +} diff --git a/crates/cli/util/src/lib.rs b/crates/cli/util/src/lib.rs index 7e0d69c186..12c3cdf1e4 100644 --- a/crates/cli/util/src/lib.rs +++ b/crates/cli/util/src/lib.rs @@ -9,10 +9,11 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod allocator; +pub mod cancellation; /// Helper function to load a secret key from a file. pub mod load_secret_key; -pub use load_secret_key::get_secret_key; +pub use load_secret_key::{get_secret_key, parse_secret_key_from_hex}; /// Cli parsers functions. pub mod parsers; diff --git a/crates/cli/util/src/load_secret_key.rs b/crates/cli/util/src/load_secret_key.rs index 0ca46398f1..64d756cddc 100644 --- a/crates/cli/util/src/load_secret_key.rs +++ b/crates/cli/util/src/load_secret_key.rs @@ -30,6 +30,10 @@ pub enum SecretKeyError { /// Path to the secret key file. secret_file: PathBuf, }, + + /// Invalid hex string format. + #[error("invalid hex string: {0}")] + InvalidHexString(String), } /// Attempts to load a [`SecretKey`] from a specified path. If no file exists there, then it @@ -60,3 +64,75 @@ pub fn get_secret_key(secret_key_path: &Path) -> Result Result { + // Remove "0x" prefix if present + let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); + + // Decode the hex string + let bytes = alloy_primitives::hex::decode(hex_str) + .map_err(|e| SecretKeyError::InvalidHexString(e.to_string()))?; + + // Parse into SecretKey + SecretKey::from_slice(&bytes).map_err(SecretKeyError::SecretKeyDecodeError) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_secret_key_from_hex_without_prefix() { + // Valid 32-byte hex string (64 characters) + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_ok()); + + let secret_key = result.unwrap(); + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex); + } + + #[test] + fn test_parse_secret_key_from_hex_with_0x_prefix() { + // Valid 32-byte hex string with 0x prefix + let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_ok()); + + let secret_key = result.unwrap(); + let expected = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), expected); + } + + #[test] + fn test_parse_secret_key_from_hex_invalid_length() { + // Invalid length (not 32 bytes) + let hex = "4c0883a69102937d"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + } + + #[test] + fn test_parse_secret_key_from_hex_invalid_chars() { + // Invalid hex characters + let hex = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + + if let Err(SecretKeyError::InvalidHexString(_)) = result { + // Expected error type + } else { + panic!("Expected InvalidHexString error"); + } + } + + #[test] + fn test_parse_secret_key_from_hex_empty() { + let hex = ""; + let result = parse_secret_key_from_hex(hex); + assert!(result.is_err()); + } +} diff --git a/crates/cli/util/src/parsers.rs b/crates/cli/util/src/parsers.rs index ddb120452e..dae9f9bc6e 100644 --- a/crates/cli/util/src/parsers.rs +++ b/crates/cli/util/src/parsers.rs @@ -31,6 +31,16 @@ pub fn parse_duration_from_secs_or_ms( } } +/// Helper to format a [Duration] to the format that can be parsed by +/// [`parse_duration_from_secs_or_ms`]. +pub fn format_duration_as_secs_or_ms(duration: Duration) -> String { + if duration.as_millis().is_multiple_of(1000) { + format!("{}", duration.as_secs()) + } else { + format!("{}ms", duration.as_millis()) + } +} + /// Parse [`BlockHashOrNumber`] pub fn hash_or_num_value_parser(value: &str) -> eyre::Result { match B256::from_str(value) { diff --git a/crates/cli/util/src/sigsegv_handler.rs b/crates/cli/util/src/sigsegv_handler.rs index dabbf866ce..78e37cf157 100644 --- a/crates/cli/util/src/sigsegv_handler.rs +++ b/crates/cli/util/src/sigsegv_handler.rs @@ -126,7 +126,8 @@ pub fn install() { libc::sigaltstack(&raw const alt_stack, ptr::null_mut()); let mut sa: libc::sigaction = mem::zeroed(); - sa.sa_sigaction = print_stack_trace as libc::sighandler_t; + sa.sa_sigaction = + print_stack_trace as unsafe extern "C" fn(libc::c_int) as libc::sighandler_t; sa.sa_flags = libc::SA_NODEFER | libc::SA_RESETHAND | libc::SA_ONSTACK; libc::sigemptyset(&raw mut sa.sa_mask); libc::sigaction(libc::SIGSEGV, &raw const sa, ptr::null_mut()); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 65bca13901..6edabeab2b 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -15,6 +15,7 @@ workspace = true reth-network-types.workspace = true reth-prune-types.workspace = true reth-stages-types.workspace = true +reth-static-file-types.workspace = true # serde serde = { workspace = true, optional = true } @@ -22,7 +23,7 @@ humantime-serde = { workspace = true, optional = true } # toml toml = { workspace = true, optional = true } -eyre = { workspace = true, optional = true } +eyre.workspace = true # value objects url.workspace = true @@ -31,7 +32,6 @@ url.workspace = true serde = [ "dep:serde", "dep:toml", - "dep:eyre", "dep:humantime-serde", "reth-network-types/serde", "reth-prune-types/serde", diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index 5ff2431bb5..66c0fdd53e 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -2,7 +2,9 @@ use reth_network_types::{PeersConfig, SessionsConfig}; use reth_prune_types::PruneModes; use reth_stages_types::ExecutionStageThresholds; +use reth_static_file_types::StaticFileSegment; use std::{ + collections::HashMap, path::{Path, PathBuf}, time::Duration, }; @@ -29,6 +31,9 @@ pub struct Config { pub peers: PeersConfig, /// Configuration for peer sessions. pub sessions: SessionsConfig, + /// Configuration for static files. + #[cfg_attr(feature = "serde", serde(default))] + pub static_files: StaticFilesConfig, } impl Config { @@ -411,6 +416,68 @@ impl EtlConfig { } } +/// Static files configuration. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct StaticFilesConfig { + /// Number of blocks per file for each segment. + pub blocks_per_file: BlocksPerFileConfig, +} + +/// Configuration for the number of blocks per file for each segment. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct BlocksPerFileConfig { + /// Number of blocks per file for the headers segment. + pub headers: Option, + /// Number of blocks per file for the transactions segment. + pub transactions: Option, + /// Number of blocks per file for the receipts segment. + pub receipts: Option, +} + +impl StaticFilesConfig { + /// Validates the static files configuration. + /// + /// Returns an error if any blocks per file value is zero. + pub fn validate(&self) -> eyre::Result<()> { + let BlocksPerFileConfig { headers, transactions, receipts } = self.blocks_per_file; + eyre::ensure!(headers != Some(0), "Headers segment blocks per file must be greater than 0"); + eyre::ensure!( + transactions != Some(0), + "Transactions segment blocks per file must be greater than 0" + ); + eyre::ensure!( + receipts != Some(0), + "Receipts segment blocks per file must be greater than 0" + ); + Ok(()) + } + + /// Converts the blocks per file configuration into a [`HashMap`] per segment. + pub fn as_blocks_per_file_map(&self) -> HashMap { + let BlocksPerFileConfig { headers, transactions, receipts } = self.blocks_per_file; + + let mut map = HashMap::new(); + // Iterating over all possible segments allows us to do an exhaustive match here, + // to not forget to configure new segments in the future. + for segment in StaticFileSegment::iter() { + let blocks_per_file = match segment { + StaticFileSegment::Headers => headers, + StaticFileSegment::Transactions => transactions, + StaticFileSegment::Receipts => receipts, + }; + + if let Some(blocks_per_file) = blocks_per_file { + map.insert(segment, blocks_per_file); + } + } + map + } +} + /// History stage configuration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/consensus/common/src/validation.rs b/crates/consensus/common/src/validation.rs index d1cf83dcda..20e43c5241 100644 --- a/crates/consensus/common/src/validation.rs +++ b/crates/consensus/common/src/validation.rs @@ -1,8 +1,6 @@ //! Collection of methods for block validation. -use alloy_consensus::{ - constants::MAXIMUM_EXTRA_DATA_SIZE, BlockHeader as _, Transaction, EMPTY_OMMER_ROOT_HASH, -}; +use alloy_consensus::{BlockHeader as _, Transaction, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams}; use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks}; use reth_consensus::{ConsensusError, TxGasLimitTooHighErr}; @@ -306,9 +304,12 @@ pub fn validate_4844_header_standalone( /// From yellow paper: extraData: An arbitrary byte array containing data relevant to this block. /// This must be 32 bytes or fewer; formally Hx. #[inline] -pub fn validate_header_extra_data(header: &H) -> Result<(), ConsensusError> { +pub fn validate_header_extra_data( + header: &H, + max_size: usize, +) -> Result<(), ConsensusError> { let extra_data_len = header.extra_data().len(); - if extra_data_len > MAXIMUM_EXTRA_DATA_SIZE { + if extra_data_len > max_size { Err(ConsensusError::ExtraDataExceedsMax { len: extra_data_len }) } else { Ok(()) @@ -546,4 +547,21 @@ mod tests { })) ); } + + #[test] + fn validate_header_extra_data_with_custom_limit() { + // Test with default 32 bytes - should pass + let header_32 = Header { extra_data: Bytes::from(vec![0; 32]), ..Default::default() }; + assert!(validate_header_extra_data(&header_32, 32).is_ok()); + + // Test exceeding default - should fail + let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() }; + assert_eq!( + validate_header_extra_data(&header_33, 32), + Err(ConsensusError::ExtraDataExceedsMax { len: 33 }) + ); + + // Test with custom larger limit - should pass + assert!(validate_header_extra_data(&header_33, 64).is_ok()); + } } diff --git a/crates/e2e-test-utils/src/setup_builder.rs b/crates/e2e-test-utils/src/setup_builder.rs index 8de2280fe4..c1eabece9f 100644 --- a/crates/e2e-test-utils/src/setup_builder.rs +++ b/crates/e2e-test-utils/src/setup_builder.rs @@ -4,6 +4,7 @@ //! configurations through closures that modify `NodeConfig` and `TreeConfig`. use crate::{node::NodeTestContext, wallet::Wallet, NodeBuilderHelper, NodeHelperType, TmpDB}; +use futures_util::future::TryJoinAll; use reth_chainspec::EthChainSpec; use reth_engine_local::LocalPayloadAttributesBuilder; use reth_node_builder::{ @@ -15,7 +16,7 @@ use reth_provider::providers::BlockchainProvider; use reth_rpc_server_types::RpcModuleSelection; use reth_tasks::TaskManager; use std::sync::Arc; -use tracing::{span, Level}; +use tracing::{span, Instrument, Level}; /// Type alias for tree config modifier closure type TreeConfigModifier = @@ -122,66 +123,71 @@ where reth_node_api::TreeConfig::default() }; - let mut nodes: Vec> = Vec::with_capacity(self.num_nodes); + let mut nodes = (0..self.num_nodes) + .map(async |idx| { + // Create base node config + let base_config = NodeConfig::new(self.chain_spec.clone()) + .with_network(network_config.clone()) + .with_unused_ports() + .with_rpc( + RpcServerArgs::default() + .with_unused_ports() + .with_http() + .with_http_api(RpcModuleSelection::All), + ); + + // Apply node config modifier if present + let node_config = if let Some(modifier) = &self.node_config_modifier { + modifier(base_config) + } else { + base_config + }; + + let span = span!(Level::INFO, "node", idx); + let node = N::default(); + let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config) + .testing_node(exec.clone()) + .with_types_and_provider::>() + .with_components(node.components_builder()) + .with_add_ons(node.add_ons()) + .launch_with_fn(|builder| { + let launcher = EngineNodeLauncher::new( + builder.task_executor().clone(), + builder.config().datadir(), + tree_config.clone(), + ); + builder.launch_with(launcher) + }) + .instrument(span) + .await?; + + let node = NodeTestContext::new(node, self.attributes_generator).await?; + + let genesis = node.block_hash(0); + node.update_forkchoice(genesis, genesis).await?; + + eyre::Ok(node) + }) + .collect::>() + .await?; for idx in 0..self.num_nodes { - // Create base node config - let base_config = NodeConfig::new(self.chain_spec.clone()) - .with_network(network_config.clone()) - .with_unused_ports() - .with_rpc( - RpcServerArgs::default() - .with_unused_ports() - .with_http() - .with_http_api(RpcModuleSelection::All), - ); - - // Apply node config modifier if present - let node_config = if let Some(modifier) = &self.node_config_modifier { - modifier(base_config) - } else { - base_config - }; - - let span = span!(Level::INFO, "node", idx); - let _enter = span.enter(); - let node = N::default(); - let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config) - .testing_node(exec.clone()) - .with_types_and_provider::>() - .with_components(node.components_builder()) - .with_add_ons(node.add_ons()) - .launch_with_fn(|builder| { - let launcher = EngineNodeLauncher::new( - builder.task_executor().clone(), - builder.config().datadir(), - tree_config.clone(), - ); - builder.launch_with(launcher) - }) - .await?; - - let mut node = NodeTestContext::new(node, self.attributes_generator).await?; - - let genesis = node.block_hash(0); - node.update_forkchoice(genesis, genesis).await?; - + let (prev, current) = nodes.split_at_mut(idx); + let current = current.first_mut().unwrap(); // Connect nodes if requested if self.connect_nodes { - if let Some(previous_node) = nodes.last_mut() { - previous_node.connect(&mut node).await; + if let Some(prev_idx) = idx.checked_sub(1) { + prev[prev_idx].connect(current).await; } // Connect last node with the first if there are more than two if idx + 1 == self.num_nodes && self.num_nodes > 2 && - let Some(first_node) = nodes.first_mut() + let Some(first) = prev.first_mut() { - node.connect(first_node).await; + current.connect(first).await; } } - - nodes.push(node); } Ok((nodes, tasks, Wallet::default().with_chain_id(self.chain_spec.chain().into()))) diff --git a/crates/engine/invalid-block-hooks/src/witness.rs b/crates/engine/invalid-block-hooks/src/witness.rs index 6ff8cb7854..4d81e0bd3f 100644 --- a/crates/engine/invalid-block-hooks/src/witness.rs +++ b/crates/engine/invalid-block-hooks/src/witness.rs @@ -278,7 +278,7 @@ where let bundle_state_sorted = sort_bundle_state_for_comparison(re_executed_state); let output_state_sorted = sort_bundle_state_for_comparison(original_state); let filename = format!("{}.bundle_state.diff", block_prefix); - let diff_path = self.save_diff(filename, &bundle_state_sorted, &output_state_sorted)?; + let diff_path = self.save_diff(filename, &output_state_sorted, &bundle_state_sorted)?; warn!( target: "engine::invalid_block_hooks::witness", @@ -308,13 +308,13 @@ where if let Some((original_updates, original_root)) = trie_updates { if re_executed_root != original_root { let filename = format!("{}.state_root.diff", block_prefix); - let diff_path = self.save_diff(filename, &re_executed_root, &original_root)?; + let diff_path = self.save_diff(filename, &original_root, &re_executed_root)?; warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution"); } if re_executed_root != block.state_root() { let filename = format!("{}.header_state_root.diff", block_prefix); - let diff_path = self.save_diff(filename, &re_executed_root, &block.state_root())?; + let diff_path = self.save_diff(filename, &block.state_root(), &re_executed_root)?; warn!(target: "engine::invalid_block_hooks::witness", header_state_root=?block.state_root(), ?re_executed_root, diff_path = %diff_path.display(), "Re-executed state root does not match block state root"); } diff --git a/crates/engine/primitives/src/config.rs b/crates/engine/primitives/src/config.rs index 0b9b7d9f82..d846268b91 100644 --- a/crates/engine/primitives/src/config.rs +++ b/crates/engine/primitives/src/config.rs @@ -30,7 +30,7 @@ fn default_account_worker_count() -> usize { } /// The size of proof targets chunk to spawn in one multiproof calculation. -pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 10; +pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 60; /// Default number of reserved CPU cores for non-reth processes. /// diff --git a/crates/engine/tree/src/tree/metrics.rs b/crates/engine/tree/src/tree/metrics.rs index b1343f081a..40cf794fa7 100644 --- a/crates/engine/tree/src/tree/metrics.rs +++ b/crates/engine/tree/src/tree/metrics.rs @@ -82,9 +82,12 @@ impl EngineApiMetrics { let tx = tx?; let span = debug_span!(target: "engine::tree", "execute tx", tx_hash=?tx.tx().tx_hash()); - let _enter = span.enter(); + let enter = span.entered(); trace!(target: "engine::tree", "Executing transaction"); - executor.execute_transaction(tx)?; + let gas_used = executor.execute_transaction(tx)?; + + // record the tx gas used + enter.record("gas_used", gas_used); } executor.finish().map(|(evm, result)| (evm.into_db(), result)) }; @@ -248,6 +251,8 @@ pub(crate) struct NewPayloadStatusMetrics { pub(crate) new_payload_total_gas: Histogram, /// The gas per second of valid new payload messages received. pub(crate) new_payload_gas_per_second: Histogram, + /// The gas per second for the last new payload call. + pub(crate) new_payload_gas_per_second_last: Gauge, /// Latency for the new payload calls. pub(crate) new_payload_latency: Histogram, /// Latency for the last new payload call. @@ -271,7 +276,9 @@ impl NewPayloadStatusMetrics { PayloadStatusEnum::Valid => { self.new_payload_valid.increment(1); self.new_payload_total_gas.record(gas_used as f64); - self.new_payload_gas_per_second.record(gas_used as f64 / elapsed.as_secs_f64()); + let gas_per_second = gas_used as f64 / elapsed.as_secs_f64(); + self.new_payload_gas_per_second.record(gas_per_second); + self.new_payload_gas_per_second_last.set(gas_per_second); } PayloadStatusEnum::Syncing => self.new_payload_syncing.increment(1), PayloadStatusEnum::Accepted => self.new_payload_accepted.increment(1), diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index 863d5edec4..772b2d239b 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -1134,6 +1134,15 @@ where if self.engine_kind.is_opstack() || self.config.always_process_payload_attributes_on_canonical_head() { + // We need to effectively unwind the _canonical_ chain to the FCU's head, which is + // part of the canonical chain. We need to update the latest block state to reflect + // the canonical ancestor. This ensures that state providers and the transaction + // pool operate with the correct chain state after forkchoice update processing, and + // new payloads built on the reorg'd head will be added to the tree immediately. + if self.config.unwind_canonical_header() { + self.update_latest_block_to_canonical_ancestor(&canonical_header)?; + } + if let Some(attr) = attrs { debug!(target: "engine::tree", head = canonical_header.number(), "handling payload attributes for canonical head"); // Clone only when we actually need to process the attributes @@ -1145,17 +1154,6 @@ where ); return Ok(Some(TreeOutcome::new(updated))); } - - // At this point, no alternative block has been triggered, so we need effectively - // unwind the _canonical_ chain to the FCU's head, which is part of the canonical - // chain. We need to update the latest block state to reflect the - // canonical ancestor. This ensures that state providers and the - // transaction pool operate with the correct chain state after - // forkchoice update processing. - - if self.config.unwind_canonical_header() { - self.update_latest_block_to_canonical_ancestor(&canonical_header)?; - } } // According to the Engine API specification, client software MAY skip an update of the @@ -1805,8 +1803,8 @@ where Ok(Some(ExecutedBlock { recovered_block: Arc::new(RecoveredBlock::new_sealed(block, senders)), execution_output: Arc::new(execution_output), - hashed_state: Arc::new(hashed_state), - trie_updates: Arc::new(trie_updates.into()), + hashed_state: Arc::new(hashed_state.into_sorted()), + trie_updates: Arc::new(trie_updates), })) } diff --git a/crates/engine/tree/src/tree/payload_processor/configured_sparse_trie.rs b/crates/engine/tree/src/tree/payload_processor/configured_sparse_trie.rs index b587a72139..ced00e9c39 100644 --- a/crates/engine/tree/src/tree/payload_processor/configured_sparse_trie.rs +++ b/crates/engine/tree/src/tree/payload_processor/configured_sparse_trie.rs @@ -1,10 +1,10 @@ //! Configured sparse trie enum for switching between serial and parallel implementations. use alloy_primitives::B256; -use reth_trie::{Nibbles, TrieNode}; +use reth_trie::{Nibbles, ProofTrieNode, TrieMasks, TrieNode}; use reth_trie_sparse::{ errors::SparseTrieResult, provider::TrieNodeProvider, LeafLookup, LeafLookupError, - RevealedSparseNode, SerialSparseTrie, SparseTrieInterface, SparseTrieUpdates, TrieMasks, + SerialSparseTrie, SparseTrieInterface, SparseTrieUpdates, }; use reth_trie_sparse_parallel::ParallelSparseTrie; use std::borrow::Cow; @@ -83,7 +83,7 @@ impl SparseTrieInterface for ConfiguredSparseTrie { } } - fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()> { + fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()> { match self { Self::Serial(trie) => trie.reveal_nodes(nodes), Self::Parallel(trie) => trie.reveal_nodes(nodes), diff --git a/crates/engine/tree/src/tree/payload_processor/mod.rs b/crates/engine/tree/src/tree/payload_processor/mod.rs index 88f17a465a..2e391ba866 100644 --- a/crates/engine/tree/src/tree/payload_processor/mod.rs +++ b/crates/engine/tree/src/tree/payload_processor/mod.rs @@ -46,7 +46,7 @@ use std::{ }, time::Instant, }; -use tracing::{debug, debug_span, instrument, warn}; +use tracing::{debug, debug_span, instrument, warn, Span}; mod configured_sparse_trie; pub mod executor; @@ -209,7 +209,7 @@ where + Send + 'static, { - let span = tracing::Span::current(); + let span = Span::current(); let (to_sparse_trie, sparse_trie_rx) = channel(); // We rely on the cursor factory to provide whatever DB overlay is necessary to see a @@ -249,8 +249,9 @@ where ); // spawn multi-proof task + let parent_span = span.clone(); self.executor.spawn_blocking(move || { - let _enter = span.entered(); + let _enter = parent_span.entered(); multi_proof_task.run(); }); @@ -265,6 +266,7 @@ where prewarm_handle, state_root: Some(state_root_rx), transactions: execution_rx, + _span: span, } } @@ -289,6 +291,7 @@ where prewarm_handle, state_root: None, transactions: execution_rx, + _span: Span::current(), } } @@ -368,9 +371,7 @@ where // spawn pre-warm task { let to_prewarm_task = to_prewarm_task.clone(); - let span = debug_span!(target: "engine::tree::payload_processor", "prewarm task"); self.executor.spawn_blocking(move || { - let _enter = span.entered(); prewarm_task.run(transactions, to_prewarm_task); }); } @@ -434,7 +435,7 @@ where sparse_state_trie, ); - let span = tracing::Span::current(); + let span = Span::current(); self.executor.spawn_blocking(move || { let _enter = span.entered(); @@ -466,10 +467,12 @@ pub struct PayloadHandle { to_multi_proof: Option>, // must include the receiver of the state root wired to the sparse trie prewarm_handle: CacheTaskHandle, - /// Receiver for the state root - state_root: Option>>, /// Stream of block transactions transactions: mpsc::Receiver>, + /// Receiver for the state root + state_root: Option>>, + /// Span for tracing + _span: Span, } impl PayloadHandle { @@ -478,7 +481,12 @@ impl PayloadHandle { /// # Panics /// /// If payload processing was started without background tasks. - #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)] + #[instrument( + level = "debug", + target = "engine::tree::payload_processor", + name = "await_state_root", + skip_all + )] pub fn state_root(&mut self) -> Result { self.state_root .take() diff --git a/crates/engine/tree/src/tree/payload_processor/multiproof.rs b/crates/engine/tree/src/tree/payload_processor/multiproof.rs index 7da199dd63..57c3d193d9 100644 --- a/crates/engine/tree/src/tree/payload_processor/multiproof.rs +++ b/crates/engine/tree/src/tree/payload_processor/multiproof.rs @@ -13,9 +13,8 @@ use metrics::{Gauge, Histogram}; use reth_metrics::Metrics; use reth_revm::state::EvmState; use reth_trie::{ - added_removed_keys::MultiAddedRemovedKeys, prefix_set::TriePrefixSetsMut, - updates::TrieUpdatesSorted, DecodedMultiProof, HashedPostState, HashedPostStateSorted, - HashedStorage, MultiProofTargets, TrieInput, + added_removed_keys::MultiAddedRemovedKeys, DecodedMultiProof, HashedPostState, HashedStorage, + MultiProofTargets, }; use reth_trie_parallel::{ proof::ParallelProof, @@ -27,6 +26,10 @@ use reth_trie_parallel::{ use std::{collections::BTreeMap, ops::DerefMut, sync::Arc, time::Instant}; use tracing::{debug, error, instrument, trace}; +/// The default max targets, for limiting the number of account and storage proof targets to be +/// fetched by a single worker. +const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300; + /// A trie update that can be applied to sparse trie alongside the proofs for touched parts of the /// state. #[derive(Default, Debug)] @@ -56,35 +59,6 @@ impl SparseTrieUpdate { } } -/// Common configuration for multi proof tasks -#[derive(Debug, Clone, Default)] -pub(crate) struct MultiProofConfig { - /// The sorted collection of cached in-memory intermediate trie nodes that - /// can be reused for computation. - pub nodes_sorted: Arc, - /// The sorted in-memory overlay hashed state. - pub state_sorted: Arc, - /// The collection of prefix sets for the computation. Since the prefix sets _always_ - /// invalidate the in-memory nodes, not all keys from `state_sorted` might be present here, - /// if we have cached nodes for them. - pub prefix_sets: Arc, -} - -impl MultiProofConfig { - /// Creates a new state root config from the trie input. - /// - /// This returns a cleared [`TrieInput`] so that we can reuse any allocated space in the - /// [`TrieInput`]. - pub(crate) fn from_input(mut input: TrieInput) -> (TrieInput, Self) { - let config = Self { - nodes_sorted: Arc::new(input.nodes.drain_into_sorted()), - state_sorted: Arc::new(input.state.drain_into_sorted()), - prefix_sets: Arc::new(input.prefix_sets.clone()), - }; - (input.cleared(), config) - } -} - /// Messages used internally by the multi proof task. #[derive(Debug)] pub(super) enum MultiProofMessage { @@ -704,6 +678,10 @@ pub(super) struct MultiProofTask { multiproof_manager: MultiproofManager, /// multi proof task metrics metrics: MultiProofTaskMetrics, + /// If this number is exceeded and chunking is enabled, then this will override whether or not + /// there are any active workers and force chunking across workers. This is to prevent tasks + /// which are very long from hitting a single worker. + max_targets_for_chunking: usize, } impl MultiProofTask { @@ -732,6 +710,7 @@ impl MultiProofTask { proof_result_tx, ), metrics, + max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING, } } @@ -921,10 +900,14 @@ impl MultiProofTask { let mut spawned_proof_targets = MultiProofTargets::default(); + // Chunk regardless if there are many proof targets + let many_proof_targets = + not_fetched_state_update.chunking_length() > self.max_targets_for_chunking; + // Only chunk if multiple account or storage workers are available to take advantage of // parallelism. - let should_chunk = self.multiproof_manager.proof_worker_handle.available_account_workers() > - 1 || + let should_chunk = many_proof_targets || + self.multiproof_manager.proof_worker_handle.available_account_workers() > 1 || self.multiproof_manager.proof_worker_handle.available_storage_workers() > 1; let mut dispatch = |hashed_state_update| { diff --git a/crates/engine/tree/src/tree/payload_processor/prewarm.rs b/crates/engine/tree/src/tree/payload_processor/prewarm.rs index ddbfc0715a..09aec02c6a 100644 --- a/crates/engine/tree/src/tree/payload_processor/prewarm.rs +++ b/crates/engine/tree/src/tree/payload_processor/prewarm.rs @@ -40,7 +40,7 @@ use std::{ }, time::Instant, }; -use tracing::{debug, debug_span, instrument, trace, warn}; +use tracing::{debug, debug_span, instrument, trace, warn, Span}; /// A wrapper for transactions that includes their index in the block. #[derive(Clone)] @@ -87,6 +87,8 @@ where to_multi_proof: Option>, /// Receiver for events produced by tx execution actions_rx: Receiver, + /// Parent span for tracing + parent_span: Span, } impl PrewarmCacheTask @@ -122,6 +124,7 @@ where transaction_count_hint, to_multi_proof, actions_rx, + parent_span: Span::current(), }, actions_tx, ) @@ -140,7 +143,7 @@ where let ctx = self.ctx.clone(); let max_concurrency = self.max_concurrency; let transaction_count_hint = self.transaction_count_hint; - let span = tracing::Span::current(); + let span = Span::current(); self.executor.spawn_blocking(move || { let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: span, "spawn_all").entered(); @@ -284,9 +287,10 @@ where /// This will execute the transactions until all transactions have been processed or the task /// was cancelled. #[instrument( + parent = &self.parent_span, level = "debug", target = "engine::tree::payload_processor::prewarm", - name = "prewarm", + name = "prewarm and caching", skip_all )] pub(super) fn run( @@ -452,7 +456,7 @@ where .entered(); txs.recv() } { - let _enter = + let enter = debug_span!(target: "engine::tree::payload_processor::prewarm", "prewarm tx", index, tx_hash=%tx.tx().tx_hash()) .entered(); @@ -484,7 +488,11 @@ where }; metrics.execution_duration.record(start.elapsed()); - drop(_enter); + // record some basic information about the transactions + enter.record("gas_used", res.result.gas_used()); + enter.record("is_success", res.result.is_success()); + + drop(enter); // If the task was cancelled, stop execution, send an empty result to notify the task, // and exit. diff --git a/crates/engine/tree/src/tree/payload_validator.rs b/crates/engine/tree/src/tree/payload_validator.rs index 91b2a818a5..e42145cf97 100644 --- a/crates/engine/tree/src/tree/payload_validator.rs +++ b/crates/engine/tree/src/tree/payload_validator.rs @@ -5,7 +5,7 @@ use crate::tree::{ error::{InsertBlockError, InsertBlockErrorKind, InsertPayloadError}, executor::WorkloadExecutor, instrumented_state::InstrumentedStateProvider, - payload_processor::{multiproof::MultiProofConfig, PayloadProcessor}, + payload_processor::PayloadProcessor, precompile_cache::{CachedPrecompile, CachedPrecompileMetrics, PrecompileCacheMap}, sparse_trie::StateRootComputeOutcome, EngineApiMetrics, EngineApiTreeState, ExecutionEnv, PayloadHandle, StateProviderBuilder, @@ -38,7 +38,7 @@ use reth_provider::{ StateRootProvider, TrieReader, }; use reth_revm::db::State; -use reth_trie::{updates::TrieUpdates, HashedPostState, TrieInput}; +use reth_trie::{updates::TrieUpdates, HashedPostState, TrieInputSorted}; use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError}; use std::{collections::HashMap, sync::Arc, time::Instant}; use tracing::{debug, debug_span, error, info, instrument, trace, warn}; @@ -121,8 +121,6 @@ where metrics: EngineApiMetrics, /// Validator for the payload. validator: V, - /// A cleared trie input, kept around to be reused so allocations can be minimized. - trie_input: Option, } impl BasicEngineValidator @@ -166,11 +164,11 @@ where invalid_block_hook, metrics: EngineApiMetrics::default(), validator, - trie_input: Default::default(), } } /// Converts a [`BlockOrPayload`] to a recovered block. + #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)] pub fn convert_to_block>>( &self, input: BlockOrPayload, @@ -375,7 +373,7 @@ where debug!( target: "engine::tree::payload_validator", ?strategy, - "Deciding which state root algorithm to run" + "Decided which state root algorithm to run" ); // use prewarming background task @@ -412,7 +410,7 @@ where Err(err) => return self.handle_execution_error(input, err, &parent_block), }; - // after executing the block we can stop executing transactions + // After executing the block we can stop prewarming transactions handle.stop_prewarming_execution(); let block = self.convert_to_block(input)?; @@ -422,10 +420,7 @@ where block ); - debug!(target: "engine::tree::payload_validator", "Calculating block state root"); - let root_time = Instant::now(); - let mut maybe_state_root = None; match strategy { @@ -530,8 +525,8 @@ where Ok(ExecutedBlock { recovered_block: Arc::new(block), execution_output: Arc::new(ExecutionOutcome::from((output, block_num_hash.number))), - hashed_state: Arc::new(hashed_state), - trie_updates: Arc::new(trie_output), + hashed_state: Arc::new(hashed_state.into_sorted()), + trie_updates: Arc::new(trie_output.into_sorted()), }) } @@ -553,6 +548,7 @@ where /// Validate if block is correct and satisfies all the consensus rules that concern the header /// and block body itself. + #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)] fn validate_block_inner(&self, block: &RecoveredBlock) -> Result<(), ConsensusError> { if let Err(e) = self.consensus.validate_header(block.sealed_header()) { error!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {}: {e}", block.hash()); @@ -643,26 +639,24 @@ where hashed_state: &HashedPostState, state: &EngineApiTreeState, ) -> Result<(B256, TrieUpdates), ParallelStateRootError> { - let (mut input, block_hash) = self.compute_trie_input(parent_hash, state, None)?; + let (mut input, block_hash) = self.compute_trie_input(parent_hash, state)?; - // Extend with block we are validating root for. - input.append_ref(hashed_state); + // Extend state overlay with current block's sorted state. + input.prefix_sets.extend(hashed_state.construct_prefix_sets()); + let sorted_hashed_state = hashed_state.clone().into_sorted(); + Arc::make_mut(&mut input.state).extend_ref(&sorted_hashed_state); - // Convert the TrieInput into a MultProofConfig, since everything uses the sorted - // forms of the state/trie fields. - let (_, multiproof_config) = MultiProofConfig::from_input(input); + let TrieInputSorted { nodes, state, prefix_sets: prefix_sets_mut } = input; let factory = OverlayStateProviderFactory::new(self.provider.clone()) .with_block_hash(Some(block_hash)) - .with_trie_overlay(Some(multiproof_config.nodes_sorted)) - .with_hashed_state_overlay(Some(multiproof_config.state_sorted)); + .with_trie_overlay(Some(nodes)) + .with_hashed_state_overlay(Some(state)); // The `hashed_state` argument is already taken into account as part of the overlay, but we // need to use the prefix sets which were generated from it to indicate to the // ParallelStateRoot which parts of the trie need to be recomputed. - let prefix_sets = Arc::into_inner(multiproof_config.prefix_sets) - .expect("MultiProofConfig was never cloned") - .freeze(); + let prefix_sets = prefix_sets_mut.freeze(); ParallelStateRoot::new(factory, prefix_sets).incremental_root_with_updates() } @@ -673,6 +667,7 @@ where /// - parent header validation /// - post-execution consensus validation /// - state-root based post-execution validation + #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)] fn validate_post_execution>>( &self, block: &RecoveredBlock, @@ -692,21 +687,32 @@ where } // now validate against the parent + let _enter = debug_span!(target: "engine::tree::payload_validator", "validate_header_against_parent").entered(); if let Err(e) = self.consensus.validate_header_against_parent(block.sealed_header(), parent_block) { warn!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {} against parent: {e}", block.hash()); return Err(e.into()) } + drop(_enter); + // Validate block post-execution rules + let _enter = + debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution") + .entered(); if let Err(err) = self.consensus.validate_block_post_execution(block, output) { // call post-block hook self.on_invalid_block(parent_block, block, output, None, ctx.state_mut()); return Err(err.into()) } + drop(_enter); + let _enter = + debug_span!(target: "engine::tree::payload_validator", "hashed_post_state").entered(); let hashed_state = self.provider.hashed_post_state(&output.state); + drop(_enter); + let _enter = debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution_with_hashed_state").entered(); if let Err(err) = self.validator.validate_block_post_execution_with_hashed_state(&hashed_state, block) { @@ -758,26 +764,23 @@ where > { match strategy { StateRootStrategy::StateRootTask => { - // get allocated trie input if it exists - let allocated_trie_input = self.trie_input.take(); - // Compute trie input let trie_input_start = Instant::now(); - let (trie_input, block_hash) = - self.compute_trie_input(parent_hash, state, allocated_trie_input)?; + let (trie_input, block_hash) = self.compute_trie_input(parent_hash, state)?; - // Convert the TrieInput into a MultProofConfig, since everything uses the sorted - // forms of the state/trie fields. - let (trie_input, multiproof_config) = MultiProofConfig::from_input(trie_input); - self.trie_input.replace(trie_input); + self.metrics + .block_validation + .trie_input_duration + .record(trie_input_start.elapsed().as_secs_f64()); + + // Create OverlayStateProviderFactory with sorted trie data for multiproofs + let TrieInputSorted { nodes, state, .. } = trie_input; - // Create OverlayStateProviderFactory with the multiproof config, for use with - // multiproofs. let multiproof_provider_factory = OverlayStateProviderFactory::new(self.provider.clone()) .with_block_hash(Some(block_hash)) - .with_trie_overlay(Some(multiproof_config.nodes_sorted)) - .with_hashed_state_overlay(Some(multiproof_config.state_sorted)); + .with_trie_overlay(Some(nodes)) + .with_hashed_state_overlay(Some(state)); // Use state root task only if prefix sets are empty, otherwise proof generation is // too expensive because it requires walking all paths in every proof. @@ -851,23 +854,14 @@ where } /// Determines the state root computation strategy based on configuration. - #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)] - fn plan_state_root_computation(&self) -> StateRootStrategy { - let strategy = if self.config.state_root_fallback() { + const fn plan_state_root_computation(&self) -> StateRootStrategy { + if self.config.state_root_fallback() { StateRootStrategy::Synchronous } else if self.config.use_state_root_task() { StateRootStrategy::StateRootTask } else { StateRootStrategy::Parallel - }; - - debug!( - target: "engine::tree::payload_validator", - ?strategy, - "Planned state root computation strategy" - ); - - strategy + } } /// Called when an invalid block is encountered during validation. @@ -889,14 +883,14 @@ where /// Computes the trie input at the provided parent hash, as well as the block number of the /// highest persisted ancestor. /// - /// The goal of this function is to take in-memory blocks and generate a [`TrieInput`] that - /// serves as an overlay to the database blocks. + /// The goal of this function is to take in-memory blocks and generate a [`TrieInputSorted`] + /// that serves as an overlay to the database blocks. /// /// It works as follows: /// 1. Collect in-memory blocks that are descendants of the provided parent hash using /// [`crate::tree::TreeState::blocks_by_hash`]. This returns the highest persisted ancestor /// hash (`block_hash`) and the list of in-memory descendant blocks. - /// 2. Extend the `TrieInput` with the contents of these in-memory blocks (from oldest to + /// 2. Extend the `TrieInputSorted` with the contents of these in-memory blocks (from oldest to /// newest) to build the overlay state and trie updates that sit on top of the database view /// anchored at `block_hash`. #[instrument( @@ -909,11 +903,7 @@ where &self, parent_hash: B256, state: &EngineApiTreeState, - allocated_trie_input: Option, - ) -> ProviderResult<(TrieInput, B256)> { - // get allocated trie input or use a default trie input - let mut input = allocated_trie_input.unwrap_or_default(); - + ) -> ProviderResult<(TrieInputSorted, B256)> { let (block_hash, blocks) = state.tree_state.blocks_by_hash(parent_hash).unwrap_or_else(|| (parent_hash, vec![])); @@ -923,10 +913,24 @@ where debug!(target: "engine::tree::payload_validator", historical = ?block_hash, blocks = blocks.len(), "Parent found in memory"); } - // Extend with contents of parent in-memory blocks. - input.extend_with_blocks( - blocks.iter().rev().map(|block| (block.hashed_state(), block.trie_updates())), - ); + // Extend with contents of parent in-memory blocks directly in sorted form. + let mut input = TrieInputSorted::default(); + let mut blocks_iter = blocks.iter().rev().peekable(); + + if let Some(first) = blocks_iter.next() { + input.state = Arc::clone(&first.hashed_state); + input.nodes = Arc::clone(&first.trie_updates); + + // Only clone and mutate if there are more in-memory blocks. + if blocks_iter.peek().is_some() { + let state_mut = Arc::make_mut(&mut input.state); + let nodes_mut = Arc::make_mut(&mut input.nodes); + for block in blocks_iter { + state_mut.extend_ref(block.hashed_state()); + nodes_mut.extend_ref(block.trie_updates()); + } + } + } Ok((input, block_hash)) } diff --git a/crates/engine/tree/src/tree/precompile_cache.rs b/crates/engine/tree/src/tree/precompile_cache.rs index 1183dfbe98..fd58eee4d6 100644 --- a/crates/engine/tree/src/tree/precompile_cache.rs +++ b/crates/engine/tree/src/tree/precompile_cache.rs @@ -274,7 +274,12 @@ mod tests { #[test] fn test_precompile_cache_basic() { let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult { - Ok(PrecompileOutput { gas_used: 0, bytes: Bytes::default(), reverted: false }) + Ok(PrecompileOutput { + gas_used: 0, + gas_refunded: 0, + bytes: Bytes::default(), + reverted: false, + }) }) .into(); @@ -283,6 +288,7 @@ mod tests { let output = PrecompileOutput { gas_used: 50, + gas_refunded: 0, bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"), reverted: false, }; @@ -315,6 +321,7 @@ mod tests { Ok(PrecompileOutput { gas_used: 5000, + gas_refunded: 0, bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"), reverted: false, }) @@ -329,6 +336,7 @@ mod tests { Ok(PrecompileOutput { gas_used: 7000, + gas_refunded: 0, bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"), reverted: false, }) diff --git a/crates/engine/tree/src/tree/tests.rs b/crates/engine/tree/src/tree/tests.rs index b22b1c1f69..5c187c9085 100644 --- a/crates/engine/tree/src/tree/tests.rs +++ b/crates/engine/tree/src/tree/tests.rs @@ -826,8 +826,8 @@ fn test_tree_state_on_new_head_deep_fork() { test_harness.tree.state.tree_state.insert_executed(ExecutedBlock { recovered_block: Arc::new(block.clone()), execution_output: Arc::new(ExecutionOutcome::default()), - hashed_state: Arc::new(HashedPostState::default()), - trie_updates: Arc::new(TrieUpdates::default()), + hashed_state: Arc::new(HashedPostState::default().into_sorted()), + trie_updates: Arc::new(TrieUpdates::default().into_sorted()), }); } test_harness.tree.state.tree_state.set_canonical_head(chain_a.last().unwrap().num_hash()); @@ -836,8 +836,8 @@ fn test_tree_state_on_new_head_deep_fork() { test_harness.tree.state.tree_state.insert_executed(ExecutedBlock { recovered_block: Arc::new(block.clone()), execution_output: Arc::new(ExecutionOutcome::default()), - hashed_state: Arc::new(HashedPostState::default()), - trie_updates: Arc::new(TrieUpdates::default()), + hashed_state: Arc::new(HashedPostState::default().into_sorted()), + trie_updates: Arc::new(TrieUpdates::default().into_sorted()), }); } diff --git a/crates/era-downloader/Cargo.toml b/crates/era-downloader/Cargo.toml index 54ae581813..190e533356 100644 --- a/crates/era-downloader/Cargo.toml +++ b/crates/era-downloader/Cargo.toml @@ -15,6 +15,7 @@ alloy-primitives.workspace = true # reth reth-fs-util.workspace = true +reth-era.workspace = true # http bytes.workspace = true diff --git a/crates/era-downloader/src/client.rs b/crates/era-downloader/src/client.rs index 36ed93e1e2..f8ffd55025 100644 --- a/crates/era-downloader/src/client.rs +++ b/crates/era-downloader/src/client.rs @@ -3,14 +3,18 @@ use bytes::Bytes; use eyre::{eyre, OptionExt}; use futures_util::{stream::StreamExt, Stream, TryStreamExt}; use reqwest::{Client, IntoUrl, Url}; +use reth_era::common::file_ops::EraFileType; use sha2::{Digest, Sha256}; use std::{future::Future, path::Path, str::FromStr}; use tokio::{ fs::{self, File}, io::{self, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWriteExt}, - join, try_join, + try_join, }; +/// Downloaded index page filename +const INDEX_HTML_FILE: &str = "index.html"; + /// Accesses the network over HTTP. pub trait HttpClient { /// Makes an HTTP GET request to `url`. Returns a stream of response body bytes. @@ -41,6 +45,7 @@ pub struct EraClient { client: Http, url: Url, folder: Box, + era_type: EraFileType, } impl EraClient { @@ -48,7 +53,8 @@ impl EraClient { /// Constructs [`EraClient`] using `client` to download from `url` into `folder`. pub fn new(client: Http, url: Url, folder: impl Into>) -> Self { - Self { client, url, folder: folder.into() } + let era_type = EraFileType::from_url(url.as_str()); + Self { client, url, folder: folder.into(), era_type } } /// Performs a GET request on `url` and stores the response body into a file located within @@ -92,9 +98,11 @@ impl EraClient { } } - self.assert_checksum(number, actual_checksum?) - .await - .map_err(|e| eyre!("{e} for {file_name} at {}", path.display()))?; + if self.era_type == EraFileType::Era1 { + self.assert_checksum(number, actual_checksum?) + .await + .map_err(|e| eyre!("{e} for {file_name} at {}", path.display()))?; + } } Ok(path.into_boxed_path()) @@ -145,9 +153,11 @@ impl EraClient { pub async fn files_count(&self) -> usize { let mut count = 0usize; + let file_extension = self.era_type.extension().trim_start_matches('.'); + if let Ok(mut dir) = fs::read_dir(&self.folder).await { while let Ok(Some(entry)) = dir.next_entry().await { - if entry.path().extension() == Some("era1".as_ref()) { + if entry.path().extension() == Some(file_extension.as_ref()) { count += 1; } } @@ -156,46 +166,35 @@ impl EraClient { count } - /// Fetches the list of ERA1 files from `url` and stores it in a file located within `folder`. + /// Fetches the list of ERA1/ERA files from `url` and stores it in a file located within + /// `folder`. + /// For era files, checksum.txt file does not exist, so the checksum verification is + /// skipped. pub async fn fetch_file_list(&self) -> eyre::Result<()> { - let (mut index, mut checksums) = try_join!( - self.client.get(self.url.clone()), - self.client.get(self.url.clone().join(Self::CHECKSUMS)?), - )?; - - let index_path = self.folder.to_path_buf().join("index.html"); + let index_path = self.folder.to_path_buf().join(INDEX_HTML_FILE); let checksums_path = self.folder.to_path_buf().join(Self::CHECKSUMS); - let (mut index_file, mut checksums_file) = - try_join!(File::create(&index_path), File::create(&checksums_path))?; - - loop { - let (index, checksums) = join!(index.next(), checksums.next()); - let (index, checksums) = (index.transpose()?, checksums.transpose()?); - - if index.is_none() && checksums.is_none() { - break; - } - let index_file = &mut index_file; - let checksums_file = &mut checksums_file; - + // Only for era1, we download also checksums file + if self.era_type == EraFileType::Era1 { + let checksums_url = self.url.join(Self::CHECKSUMS)?; try_join!( - async move { - if let Some(index) = index { - io::copy(&mut index.as_ref(), index_file).await?; - } - Ok::<(), eyre::Error>(()) - }, - async move { - if let Some(checksums) = checksums { - io::copy(&mut checksums.as_ref(), checksums_file).await?; - } - Ok::<(), eyre::Error>(()) - }, + self.download_file_to_path(self.url.clone(), &index_path), + self.download_file_to_path(checksums_url, &checksums_path) )?; + } else { + // Download only index file + self.download_file_to_path(self.url.clone(), &index_path).await?; } - let file = File::open(&index_path).await?; + // Parse and extract era filenames from index.html + self.extract_era_filenames(&index_path).await?; + + Ok(()) + } + + /// Extracts ERA filenames from `index.html` and writes them to the index file + async fn extract_era_filenames(&self, index_path: &Path) -> eyre::Result<()> { + let file = File::open(index_path).await?; let reader = io::BufReader::new(file); let mut lines = reader.lines(); @@ -203,21 +202,36 @@ impl EraClient { let file = File::create(&path).await?; let mut writer = io::BufWriter::new(file); + let ext = self.era_type.extension(); + let ext_len = ext.len(); + while let Some(line) = lines.next_line().await? { - if let Some(j) = line.find(".era1") && + if let Some(j) = line.find(ext) && let Some(i) = line[..j].rfind(|c: char| !c.is_alphanumeric() && c != '-') { - let era = &line[i + 1..j + 5]; + let era = &line[i + 1..j + ext_len]; writer.write_all(era.as_bytes()).await?; writer.write_all(b"\n").await?; } } + writer.flush().await?; + Ok(()) + } + + // Helper to download a file to a specified path + async fn download_file_to_path(&self, url: Url, path: &Path) -> eyre::Result<()> { + let mut stream = self.client.get(url).await?; + let mut file = File::create(path).await?; + + while let Some(item) = stream.next().await.transpose()? { + io::copy(&mut item.as_ref(), &mut file).await?; + } Ok(()) } - /// Returns ERA1 file name that is ordered at `number`. + /// Returns ERA1/ERA file name that is ordered at `number`. pub async fn number_to_file_name(&self, number: usize) -> eyre::Result> { let path = self.folder.to_path_buf().join("index"); let file = File::open(&path).await?; @@ -235,18 +249,23 @@ impl EraClient { match File::open(path).await { Ok(file) => { - let number = self - .file_name_to_number(name) - .ok_or_else(|| eyre!("Cannot parse ERA number from {name}"))?; + if self.era_type == EraFileType::Era1 { + let number = self + .file_name_to_number(name) + .ok_or_else(|| eyre!("Cannot parse ERA number from {name}"))?; - let actual_checksum = checksum(file).await?; - let is_verified = self.verify_checksum(number, actual_checksum).await?; + let actual_checksum = checksum(file).await?; + let is_verified = self.verify_checksum(number, actual_checksum).await?; - if !is_verified { - fs::remove_file(path).await?; + if !is_verified { + fs::remove_file(path).await?; + } + + Ok(is_verified) + } else { + // For era files, we skip checksum verification, as checksum.txt does not exist + Ok(true) } - - Ok(is_verified) } Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false), Err(e) => Err(e)?, diff --git a/crates/era-downloader/src/fs.rs b/crates/era-downloader/src/fs.rs index 19532f01cf..eaab1f3f4b 100644 --- a/crates/era-downloader/src/fs.rs +++ b/crates/era-downloader/src/fs.rs @@ -12,6 +12,8 @@ pub fn read_dir( start_from: BlockNumber, ) -> eyre::Result> + Send + Sync + 'static + Unpin> { let mut checksums = None; + + // read all the files in the given dir and also read the checksums file let mut entries = fs::read_dir(dir)? .filter_map(|entry| { (|| { @@ -29,6 +31,7 @@ pub fn read_dir( return Ok(Some((number, path.into_boxed_path()))); } } + if path.file_name() == Some("checksums.txt".as_ref()) { let file = fs::open(path)?; let reader = io::BufReader::new(file); @@ -43,9 +46,15 @@ pub fn read_dir( .collect::>>()?; let mut checksums = checksums.ok_or_eyre("Missing file `checksums.txt` in the `dir`")?; + let start_index = start_from as usize / BLOCKS_PER_FILE; + for _ in 0..start_index { + // skip the first entries in the checksums iterator so that both iters align + checksums.next().transpose()?.ok_or_eyre("Got less checksums than ERA files")?; + } + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - Ok(stream::iter(entries.into_iter().skip(start_from as usize / BLOCKS_PER_FILE).map( + Ok(stream::iter(entries.into_iter().skip_while(move |(n, _)| *n < start_index).map( move |(_, path)| { let expected_checksum = checksums.next().transpose()?.ok_or_eyre("Got less checksums than ERA files")?; diff --git a/crates/era-downloader/tests/it/checksums.rs b/crates/era-downloader/tests/it/checksums.rs index 20717bfda0..500b7ed338 100644 --- a/crates/era-downloader/tests/it/checksums.rs +++ b/crates/era-downloader/tests/it/checksums.rs @@ -61,21 +61,21 @@ impl HttpClient for FailingClient { let url = url.into_url().unwrap(); Ok(futures::stream::iter(vec![Ok(match url.as_str() { - "https://mainnet.era1.nimbus.team/" => Bytes::from_static(crate::NIMBUS), - "https://era1.ethportal.net/" => Bytes::from_static(crate::ETH_PORTAL), - "https://era.ithaca.xyz/era1/index.html" => Bytes::from_static(crate::ITHACA), + "https://mainnet.era1.nimbus.team/" => Bytes::from_static(crate::ERA1_NIMBUS), + "https://era1.ethportal.net/" => Bytes::from_static(crate::ERA1_ETH_PORTAL), + "https://era.ithaca.xyz/era1/index.html" => Bytes::from_static(crate::ERA1_ITHACA), "https://mainnet.era1.nimbus.team/checksums.txt" | "https://era1.ethportal.net/checksums.txt" | "https://era.ithaca.xyz/era1/checksums.txt" => Bytes::from_static(CHECKSUMS), "https://era1.ethportal.net/mainnet-00000-5ec1ffb8.era1" | "https://mainnet.era1.nimbus.team/mainnet-00000-5ec1ffb8.era1" | "https://era.ithaca.xyz/era1/mainnet-00000-5ec1ffb8.era1" => { - Bytes::from_static(crate::MAINNET_0) + Bytes::from_static(crate::ERA1_MAINNET_0) } "https://era1.ethportal.net/mainnet-00001-a5364e9a.era1" | "https://mainnet.era1.nimbus.team/mainnet-00001-a5364e9a.era1" | "https://era.ithaca.xyz/era1/mainnet-00001-a5364e9a.era1" => { - Bytes::from_static(crate::MAINNET_1) + Bytes::from_static(crate::ERA1_MAINNET_1) } v => unimplemented!("Unexpected URL \"{v}\""), })])) diff --git a/crates/era-downloader/tests/it/download.rs b/crates/era-downloader/tests/it/download.rs index e7756bfede..bf6b956c69 100644 --- a/crates/era-downloader/tests/it/download.rs +++ b/crates/era-downloader/tests/it/download.rs @@ -10,7 +10,7 @@ use test_case::test_case; #[test_case("https://era1.ethportal.net/"; "ethportal")] #[test_case("https://era.ithaca.xyz/era1/index.html"; "ithaca")] #[tokio::test] -async fn test_getting_file_url_after_fetching_file_list(url: &str) { +async fn test_getting_era1_file_url_after_fetching_file_list(url: &str) { let base_url = Url::from_str(url).unwrap(); let folder = tempdir().unwrap(); let folder = folder.path(); @@ -48,3 +48,19 @@ async fn test_getting_file_after_fetching_file_list(url: &str) { let actual_count = client.files_count().await; assert_eq!(actual_count, expected_count); } + +#[test_case("https://mainnet.era.nimbus.team/"; "nimbus")] +#[tokio::test] +async fn test_getting_era_file_url_after_fetching_file_list(url: &str) { + let base_url = Url::from_str(url).unwrap(); + let folder = tempdir().unwrap(); + let folder = folder.path(); + let client = EraClient::new(StubClient, base_url.clone(), folder); + + client.fetch_file_list().await.unwrap(); + + let expected_url = Some(base_url.join("mainnet-00000-4b363db9.era").unwrap()); + let actual_url = client.url(0).await.unwrap(); + + assert_eq!(actual_url, expected_url); +} diff --git a/crates/era-downloader/tests/it/list.rs b/crates/era-downloader/tests/it/list.rs index 3940fa5d8b..3da10102c3 100644 --- a/crates/era-downloader/tests/it/list.rs +++ b/crates/era-downloader/tests/it/list.rs @@ -10,7 +10,7 @@ use test_case::test_case; #[test_case("https://era1.ethportal.net/"; "ethportal")] #[test_case("https://era.ithaca.xyz/era1/index.html"; "ithaca")] #[tokio::test] -async fn test_getting_file_name_after_fetching_file_list(url: &str) { +async fn test_getting_era1_file_name_after_fetching_file_list(url: &str) { let url = Url::from_str(url).unwrap(); let folder = tempdir().unwrap(); let folder = folder.path(); @@ -23,3 +23,19 @@ async fn test_getting_file_name_after_fetching_file_list(url: &str) { assert_eq!(actual, expected); } + +#[test_case("https://mainnet.era.nimbus.team/"; "nimbus")] +#[tokio::test] +async fn test_getting_era_file_name_after_fetching_file_list(url: &str) { + let url = Url::from_str(url).unwrap(); + let folder = tempdir().unwrap(); + let folder = folder.path(); + let client = EraClient::new(StubClient, url, folder); + + client.fetch_file_list().await.unwrap(); + + let actual = client.number_to_file_name(500).await.unwrap(); + let expected = Some("mainnet-00500-87109713.era".to_owned()); + + assert_eq!(actual, expected); +} diff --git a/crates/era-downloader/tests/it/main.rs b/crates/era-downloader/tests/it/main.rs index 189d95506d..fb5f33ab71 100644 --- a/crates/era-downloader/tests/it/main.rs +++ b/crates/era-downloader/tests/it/main.rs @@ -13,12 +13,20 @@ use futures::Stream; use reqwest::IntoUrl; use reth_era_downloader::HttpClient; -pub(crate) const NIMBUS: &[u8] = include_bytes!("../res/nimbus.html"); -pub(crate) const ETH_PORTAL: &[u8] = include_bytes!("../res/ethportal.html"); -pub(crate) const ITHACA: &[u8] = include_bytes!("../res/ithaca.html"); -pub(crate) const CHECKSUMS: &[u8] = include_bytes!("../res/checksums.txt"); -pub(crate) const MAINNET_0: &[u8] = include_bytes!("../res/mainnet-00000-5ec1ffb8.era1"); -pub(crate) const MAINNET_1: &[u8] = include_bytes!("../res/mainnet-00001-a5364e9a.era1"); +pub(crate) const ERA1_NIMBUS: &[u8] = include_bytes!("../res/era1-nimbus.html"); +pub(crate) const ERA1_ETH_PORTAL: &[u8] = include_bytes!("../res/ethportal.html"); +pub(crate) const ERA1_ITHACA: &[u8] = include_bytes!("../res/era1-ithaca.html"); +pub(crate) const ERA1_CHECKSUMS: &[u8] = include_bytes!("../res/checksums.txt"); +pub(crate) const ERA1_MAINNET_0: &[u8] = + include_bytes!("../res/era1-files/mainnet-00000-5ec1ffb8.era1"); +pub(crate) const ERA1_MAINNET_1: &[u8] = + include_bytes!("../res/era1-files/mainnet-00001-a5364e9a.era1"); + +pub(crate) const ERA_NIMBUS: &[u8] = include_bytes!("../res/era-nimbus.html"); +pub(crate) const ERA_MAINNET_0: &[u8] = + include_bytes!("../res/era-files/mainnet-00000-4b363db9.era"); +pub(crate) const ERA_MAINNET_1: &[u8] = + include_bytes!("../res/era-files/mainnet-00001-40cf2f3c.era"); /// An HTTP client pre-programmed with canned answers to received calls. /// Panics if it receives an unknown call. @@ -33,22 +41,32 @@ impl HttpClient for StubClient { let url = url.into_url().unwrap(); Ok(futures::stream::iter(vec![Ok(match url.as_str() { - "https://mainnet.era1.nimbus.team/" => Bytes::from_static(NIMBUS), - "https://era1.ethportal.net/" => Bytes::from_static(ETH_PORTAL), - "https://era.ithaca.xyz/era1/index.html" => Bytes::from_static(ITHACA), + // Era1 urls + "https://mainnet.era1.nimbus.team/" => Bytes::from_static(ERA1_NIMBUS), + "https://era1.ethportal.net/" => Bytes::from_static(ERA1_ETH_PORTAL), + "https://era.ithaca.xyz/era1/index.html" => Bytes::from_static(ERA1_ITHACA), "https://mainnet.era1.nimbus.team/checksums.txt" | "https://era1.ethportal.net/checksums.txt" | - "https://era.ithaca.xyz/era1/checksums.txt" => Bytes::from_static(CHECKSUMS), + "https://era.ithaca.xyz/era1/checksums.txt" => Bytes::from_static(ERA1_CHECKSUMS), "https://era1.ethportal.net/mainnet-00000-5ec1ffb8.era1" | "https://mainnet.era1.nimbus.team/mainnet-00000-5ec1ffb8.era1" | "https://era.ithaca.xyz/era1/mainnet-00000-5ec1ffb8.era1" => { - Bytes::from_static(MAINNET_0) + Bytes::from_static(ERA1_MAINNET_0) } "https://era1.ethportal.net/mainnet-00001-a5364e9a.era1" | "https://mainnet.era1.nimbus.team/mainnet-00001-a5364e9a.era1" | "https://era.ithaca.xyz/era1/mainnet-00001-a5364e9a.era1" => { - Bytes::from_static(MAINNET_1) + Bytes::from_static(ERA1_MAINNET_1) } + // Era urls + "https://mainnet.era.nimbus.team/" => Bytes::from_static(ERA_NIMBUS), + "https://mainnet.era.nimbus.team/mainnet-00000-4b363db9.era" => { + Bytes::from_static(ERA_MAINNET_0) + } + "https://mainnet.era.nimbus.team/mainnet-00001-40cf2f3c.era" => { + Bytes::from_static(ERA_MAINNET_1) + } + v => unimplemented!("Unexpected URL \"{v}\""), })])) } diff --git a/crates/era-downloader/tests/it/stream.rs b/crates/era-downloader/tests/it/stream.rs index eb7dc2da72..878e580789 100644 --- a/crates/era-downloader/tests/it/stream.rs +++ b/crates/era-downloader/tests/it/stream.rs @@ -34,7 +34,7 @@ async fn test_streaming_files_after_fetching_file_list(url: &str) { } #[tokio::test] -async fn test_streaming_files_after_fetching_file_list_into_missing_folder_fails() { +async fn test_streaming_era1_files_after_fetching_file_list_into_missing_folder_fails() { let base_url = Url::from_str("https://era.ithaca.xyz/era1/index.html").unwrap(); let folder = tempdir().unwrap().path().to_owned(); let client = EraClient::new(StubClient, base_url, folder); @@ -49,3 +49,20 @@ async fn test_streaming_files_after_fetching_file_list_into_missing_folder_fails assert_eq!(actual_error, expected_error); } + +#[tokio::test] +async fn test_streaming_era_files_after_fetching_file_list_into_missing_folder_fails() { + let base_url = Url::from_str("https://mainnet.era.nimbus.team").unwrap(); //TODO: change once ithaca host era files + let folder = tempdir().unwrap().path().to_owned(); + let client = EraClient::new(StubClient, base_url, folder); + + let mut stream = EraStream::new( + client, + EraStreamConfig::default().with_max_files(2).with_max_concurrent_downloads(1), + ); + + let actual_error = stream.next().await.unwrap().unwrap_err().to_string(); + let expected_error = "No such file or directory (os error 2)".to_owned(); + + assert_eq!(actual_error, expected_error); +} diff --git a/crates/era-downloader/tests/res/era-files/mainnet-00000-4b363db9.era b/crates/era-downloader/tests/res/era-files/mainnet-00000-4b363db9.era new file mode 100644 index 0000000000..f2ad6c76f0 --- /dev/null +++ b/crates/era-downloader/tests/res/era-files/mainnet-00000-4b363db9.era @@ -0,0 +1 @@ +c diff --git a/crates/era-downloader/tests/res/era-files/mainnet-00001-40cf2f3c.era b/crates/era-downloader/tests/res/era-files/mainnet-00001-40cf2f3c.era new file mode 100644 index 0000000000..4bcfe98e64 --- /dev/null +++ b/crates/era-downloader/tests/res/era-files/mainnet-00001-40cf2f3c.era @@ -0,0 +1 @@ +d diff --git a/crates/era-downloader/tests/res/era-nimbus.html b/crates/era-downloader/tests/res/era-nimbus.html new file mode 100644 index 0000000000..3420275cc7 --- /dev/null +++ b/crates/era-downloader/tests/res/era-nimbus.html @@ -0,0 +1,1593 @@ + +Index of / + +

Index of /


../
+mainnet-00000-4b363db9.era                         19-Apr-2023 08:01             1810141
+mainnet-00001-40cf2f3c.era                         19-Apr-2023 08:01            18580262
+mainnet-00002-74a3850f.era                         19-Apr-2023 08:01            19565485
+mainnet-00003-76b05bfe.era                         19-Apr-2023 08:01            18660406
+mainnet-00004-f82d21ce.era                         19-Apr-2023 08:01            20093454
+mainnet-00005-4ec633af.era                         19-Apr-2023 08:01            19983422
+mainnet-00006-eef40204.era                         19-Apr-2023 08:01            20641097
+mainnet-00007-ce8f6ccd.era                         19-Apr-2023 08:01            21459536
+mainnet-00008-f0d27f1e.era                         19-Apr-2023 08:01            22035110
+mainnet-00009-9ba3d167.era                         19-Apr-2023 08:01            22813416
+mainnet-00010-fde55062.era                         19-Apr-2023 08:01            23457891
+mainnet-00011-76e38a38.era                         19-Apr-2023 08:01            23862617
+mainnet-00012-7ef05aec.era                         19-Apr-2023 08:01            27179700
+mainnet-00013-691af8c1.era                         19-Apr-2023 08:01            24659214
+mainnet-00014-c17977ec.era                         19-Apr-2023 08:01            25148747
+mainnet-00015-d60a0e74.era                         19-Apr-2023 08:01            25855274
+mainnet-00016-36da78e2.era                         19-Apr-2023 08:01            26889835
+mainnet-00017-e3026c33.era                         19-Apr-2023 08:01            28599421
+mainnet-00018-8fadc278.era                         19-Apr-2023 08:01            29673899
+mainnet-00019-c3c42b77.era                         19-Apr-2023 08:01            30387949
+mainnet-00020-e49f5ec8.era                         19-Apr-2023 08:01            32311398
+mainnet-00021-98fcd379.era                         19-Apr-2023 08:01            31330610
+mainnet-00022-867c2320.era                         19-Apr-2023 08:01            31229986
+mainnet-00023-bbbf8551.era                         19-Apr-2023 08:01            30966415
+mainnet-00024-7e297816.era                         19-Apr-2023 08:01            31298813
+mainnet-00025-266413c7.era                         19-Apr-2023 08:01            32582497
+mainnet-00026-93997190.era                         19-Apr-2023 08:01            31353508
+mainnet-00027-c3e74ac5.era                         19-Apr-2023 08:01            32793045
+mainnet-00028-d390d828.era                         19-Apr-2023 08:01            32716855
+mainnet-00029-053c8d4f.era                         19-Apr-2023 08:01            35018552
+mainnet-00030-ab3e346d.era                         19-Apr-2023 08:01            33153620
+mainnet-00031-6bcae87f.era                         19-Apr-2023 08:01            33928188
+mainnet-00032-22d32f3f.era                         19-Apr-2023 08:01            34686488
+mainnet-00033-e7826225.era                         19-Apr-2023 08:01            35989306
+mainnet-00034-f464bad4.era                         19-Apr-2023 08:01            36129694
+mainnet-00035-e45c1e3e.era                         19-Apr-2023 08:01            36772095
+mainnet-00036-6a92d280.era                         19-Apr-2023 08:01            36576585
+mainnet-00037-9fe02a0d.era                         19-Apr-2023 08:01            38458478
+mainnet-00038-4ec47bd9.era                         19-Apr-2023 08:01            38371832
+mainnet-00039-dda536eb.era                         19-Apr-2023 08:01            40574896
+mainnet-00040-e67676e4.era                         19-Apr-2023 08:01            42854476
+mainnet-00041-20ca9f8a.era                         19-Apr-2023 08:01            41948268
+mainnet-00042-5c04d415.era                         19-Apr-2023 08:01            42616685
+mainnet-00043-98c36e1d.era                         19-Apr-2023 08:01            43400431
+mainnet-00044-d8066c38.era                         19-Apr-2023 08:01            47470998
+mainnet-00045-c8915969.era                         19-Apr-2023 08:01            55899512
+mainnet-00046-078aed7c.era                         19-Apr-2023 08:01            53341912
+mainnet-00047-8ad00df4.era                         19-Apr-2023 08:01            46838488
+mainnet-00048-d1fc0d45.era                         19-Apr-2023 08:01            47427770
+mainnet-00049-2d9b3eca.era                         19-Apr-2023 08:01            48335222
+mainnet-00050-649476e4.era                         19-Apr-2023 08:01            49923077
+mainnet-00051-7b310497.era                         19-Apr-2023 08:01            56562914
+mainnet-00052-7cbff20e.era                         19-Apr-2023 08:01            56649533
+mainnet-00053-9e690c2e.era                         19-Apr-2023 08:01            58669099
+mainnet-00054-f44d18d1.era                         19-Apr-2023 08:01            53642199
+mainnet-00055-3cdac49d.era                         19-Apr-2023 08:01            54674906
+mainnet-00056-9fbb8fc4.era                         19-Apr-2023 08:01            61488692
+mainnet-00057-93102c5a.era                         19-Apr-2023 08:01            63798327
+mainnet-00058-9af69b68.era                         19-Apr-2023 08:01            65633837
+mainnet-00059-c0b2123d.era                         19-Apr-2023 08:01            64031147
+mainnet-00060-202ccf6f.era                         19-Apr-2023 08:01            60016283
+mainnet-00061-bf7adcba.era                         19-Apr-2023 08:01            63529457
+mainnet-00062-54e19978.era                         19-Apr-2023 08:01            69520094
+mainnet-00063-1b090109.era                         19-Apr-2023 08:01            64288584
+mainnet-00064-07dbbdfc.era                         19-Apr-2023 08:01            62462786
+mainnet-00065-3d035c95.era                         19-Apr-2023 08:01            69824227
+mainnet-00066-c9d23f5b.era                         19-Apr-2023 08:01            69701623
+mainnet-00067-2b9c4ff2.era                         19-Apr-2023 08:01            64904839
+mainnet-00068-6c0c35c0.era                         19-Apr-2023 08:01            68077958
+mainnet-00069-fd2e135f.era                         19-Apr-2023 08:01            72932779
+mainnet-00070-df464014.era                         19-Apr-2023 08:01            73158530
+mainnet-00071-b0d5e5de.era                         19-Apr-2023 08:01            72433228
+mainnet-00072-8c994e45.era                         19-Apr-2023 08:01            66222825
+mainnet-00073-5913138f.era                         19-Apr-2023 08:01            67531788
+mainnet-00074-77c2f86c.era                         19-Apr-2023 08:01            66923751
+mainnet-00075-ea3f8b6a.era                         19-Apr-2023 08:01            64831808
+mainnet-00076-105da067.era                         19-Apr-2023 08:01            66729877
+mainnet-00077-7a1c1dbe.era                         19-Apr-2023 08:01            70441150
+mainnet-00078-f43226a1.era                         19-Apr-2023 08:01            70727253
+mainnet-00079-8065f8f5.era                         19-Apr-2023 08:01            67868774
+mainnet-00080-fc93a1b8.era                         19-Apr-2023 08:01            75110643
+mainnet-00081-64301fb7.era                         19-Apr-2023 08:01            71234420
+mainnet-00082-946e696f.era                         19-Apr-2023 08:01            68028413
+mainnet-00083-6e882135.era                         19-Apr-2023 08:01            65251306
+mainnet-00084-ea2f03fa.era                         19-Apr-2023 08:01            63653815
+mainnet-00085-400b6150.era                         19-Apr-2023 08:01            72612475
+mainnet-00086-ff3c5850.era                         19-Apr-2023 08:01            76913640
+mainnet-00087-0f2df0f3.era                         19-Apr-2023 08:01            72822474
+mainnet-00088-a5af5c4d.era                         19-Apr-2023 08:01            69817765
+mainnet-00089-779c474c.era                         19-Apr-2023 08:01            79883924
+mainnet-00090-56a5eb95.era                         19-Apr-2023 08:01            77689567
+mainnet-00091-c8820f5d.era                         19-Apr-2023 08:01            76266230
+mainnet-00092-8071bfe1.era                         19-Apr-2023 08:01            70279875
+mainnet-00093-162845ca.era                         19-Apr-2023 08:01            68271473
+mainnet-00094-ce2b7c1c.era                         19-Apr-2023 08:01            63122737
+mainnet-00095-89870019.era                         19-Apr-2023 08:01            63176783
+mainnet-00096-22d3666f.era                         19-Apr-2023 08:01            62245079
+mainnet-00097-0a12cee7.era                         19-Apr-2023 08:01            66697617
+mainnet-00098-6a323bf4.era                         19-Apr-2023 08:01            72118178
+mainnet-00099-d78a180f.era                         19-Apr-2023 08:01            75283283
+mainnet-00100-9e740416.era                         19-Apr-2023 08:01            79242147
+mainnet-00101-a2ca13d2.era                         19-Apr-2023 08:01            72687766
+mainnet-00102-42aba82e.era                         19-Apr-2023 08:01            66471367
+mainnet-00103-8b284964.era                         19-Apr-2023 08:01            58583319
+mainnet-00104-84866bce.era                         19-Apr-2023 08:01            60069517
+mainnet-00105-5973052c.era                         19-Apr-2023 08:01            57408918
+mainnet-00106-ec3b5974.era                         19-Apr-2023 08:01            60351153
+mainnet-00107-25cb4b21.era                         19-Apr-2023 08:01            60967209
+mainnet-00108-96efb79f.era                         19-Apr-2023 08:01            57472500
+mainnet-00109-40f9ed5f.era                         19-Apr-2023 08:01            63856387
+mainnet-00110-fc30c4dc.era                         19-Apr-2023 08:01            58268620
+mainnet-00111-31fa9e03.era                         19-Apr-2023 08:01            66131359
+mainnet-00112-b9c9c501.era                         19-Apr-2023 08:01            67909807
+mainnet-00113-6b00dbc2.era                         19-Apr-2023 08:01            70192442
+mainnet-00114-40543d05.era                         19-Apr-2023 08:01            67103740
+mainnet-00115-fe01e028.era                         19-Apr-2023 08:01            69615332
+mainnet-00116-9321f262.era                         19-Apr-2023 08:01            70471612
+mainnet-00117-eae18b3b.era                         19-Apr-2023 08:01            68864538
+mainnet-00118-ff07f2e3.era                         19-Apr-2023 08:01            66025557
+mainnet-00119-2d52ef48.era                         19-Apr-2023 08:01            63541726
+mainnet-00120-29f537a3.era                         19-Apr-2023 08:01            64973381
+mainnet-00121-0e6f3faa.era                         19-Apr-2023 08:01            65723355
+mainnet-00122-c8ea3c9f.era                         19-Apr-2023 08:01            64333665
+mainnet-00123-a05d338d.era                         19-Apr-2023 08:01            64879032
+mainnet-00124-961a3ce9.era                         19-Apr-2023 08:01            66202483
+mainnet-00125-5c5e9f9d.era                         19-Apr-2023 08:01            67110402
+mainnet-00126-0920fd3e.era                         19-Apr-2023 08:01            64992073
+mainnet-00127-348c85d7.era                         19-Apr-2023 08:01            68782028
+mainnet-00128-235f3c5d.era                         19-Apr-2023 08:01            69591376
+mainnet-00129-850f303e.era                         19-Apr-2023 08:01            69899092
+mainnet-00130-0f5754a0.era                         19-Apr-2023 08:01            68796462
+mainnet-00131-30551905.era                         19-Apr-2023 08:01            68312081
+mainnet-00132-15ca033c.era                         19-Apr-2023 08:01            71930760
+mainnet-00133-44e4d781.era                         19-Apr-2023 08:01            64469039
+mainnet-00134-24bb219e.era                         19-Apr-2023 08:01            67357374
+mainnet-00135-78db4c95.era                         19-Apr-2023 08:01            67484778
+mainnet-00136-154a8fe6.era                         19-Apr-2023 08:01            64252529
+mainnet-00137-2a63e504.era                         19-Apr-2023 08:01            66294089
+mainnet-00138-8f5c2cc7.era                         19-Apr-2023 08:01            69832669
+mainnet-00139-5188cfb6.era                         19-Apr-2023 08:01            70931933
+mainnet-00140-68d3319a.era                         19-Apr-2023 08:01            67676267
+mainnet-00141-9ab68992.era                         19-Apr-2023 08:01            68506361
+mainnet-00142-9daa1e68.era                         19-Apr-2023 08:01            67403565
+mainnet-00143-ea65b368.era                         19-Apr-2023 08:01            68620832
+mainnet-00144-f0e68195.era                         19-Apr-2023 08:01            70439651
+mainnet-00145-324a9fd9.era                         19-Apr-2023 08:01            74268694
+mainnet-00146-e61beaac.era                         19-Apr-2023 08:02            74104387
+mainnet-00147-3400eac8.era                         19-Apr-2023 08:02            74725735
+mainnet-00148-f2ffe12a.era                         19-Apr-2023 08:02            77682938
+mainnet-00149-941197c0.era                         19-Apr-2023 08:02            79924748
+mainnet-00150-e58cc6d1.era                         19-Apr-2023 08:02            77128487
+mainnet-00151-8b035db0.era                         19-Apr-2023 08:02            73665416
+mainnet-00152-7a1b47f8.era                         19-Apr-2023 08:02            75422717
+mainnet-00153-5b9f5d14.era                         19-Apr-2023 08:02            75991256
+mainnet-00154-5506aec2.era                         19-Apr-2023 08:02            76352743
+mainnet-00155-b187606b.era                         19-Apr-2023 08:02            80814087
+mainnet-00156-d1db3a39.era                         19-Apr-2023 08:02            81125701
+mainnet-00157-cb4a35ef.era                         19-Apr-2023 08:02            80031716
+mainnet-00158-86f4ee82.era                         19-Apr-2023 08:02            78296940
+mainnet-00159-471ea44e.era                         19-Apr-2023 08:02            80665182
+mainnet-00160-64fd0bc0.era                         19-Apr-2023 08:02            81653393
+mainnet-00161-59cfce4a.era                         19-Apr-2023 08:02            83520494
+mainnet-00162-b625abf6.era                         19-Apr-2023 08:02            81405682
+mainnet-00163-6a322a25.era                         19-Apr-2023 08:02            81356654
+mainnet-00164-b21c0ea4.era                         19-Apr-2023 08:02            77776375
+mainnet-00165-60fe50c9.era                         19-Apr-2023 08:02            81372959
+mainnet-00166-37f50120.era                         19-Apr-2023 08:02            81159848
+mainnet-00167-d06b616a.era                         19-Apr-2023 08:02            81433352
+mainnet-00168-591f41bd.era                         19-Apr-2023 08:02            81523847
+mainnet-00169-d6df2275.era                         19-Apr-2023 08:02            83528707
+mainnet-00170-e1aa3c1d.era                         19-Apr-2023 08:02            79656576
+mainnet-00171-b38e1a6b.era                         19-Apr-2023 08:02            81003140
+mainnet-00172-2c5d3b59.era                         19-Apr-2023 08:02            83870057
+mainnet-00173-ea310823.era                         19-Apr-2023 08:02            86845777
+mainnet-00174-f1dffe88.era                         19-Apr-2023 08:02            90177937
+mainnet-00175-b7a9160e.era                         19-Apr-2023 08:02            86840082
+mainnet-00176-8c05a49f.era                         19-Apr-2023 08:02            85199741
+mainnet-00177-1ea0ce2d.era                         19-Apr-2023 08:02            84875487
+mainnet-00178-0d0a5290.era                         19-Apr-2023 08:02            90506499
+mainnet-00179-529bdb65.era                         19-Apr-2023 08:02            88375904
+mainnet-00180-3d46655b.era                         19-Apr-2023 08:02            99823718
+mainnet-00181-5b2b8506.era                         19-Apr-2023 08:02            97431627
+mainnet-00182-389855a7.era                         19-Apr-2023 08:02            90997160
+mainnet-00183-7307be7c.era                         19-Apr-2023 08:02            92538927
+mainnet-00184-a0aa399e.era                         19-Apr-2023 08:02            98152875
+mainnet-00185-8e47493a.era                         19-Apr-2023 08:02            93287428
+mainnet-00186-82c6bfa8.era                         19-Apr-2023 08:02            93153188
+mainnet-00187-a05172bc.era                         19-Apr-2023 08:02            94047482
+mainnet-00188-80a8f609.era                         19-Apr-2023 08:02            96532497
+mainnet-00189-1969f732.era                         19-Apr-2023 08:02           102775171
+mainnet-00190-cf755024.era                         19-Apr-2023 08:02            97429581
+mainnet-00191-c4b273af.era                         19-Apr-2023 08:02           103605152
+mainnet-00192-02fe825e.era                         19-Apr-2023 08:02            98221273
+mainnet-00193-bf9be962.era                         19-Apr-2023 08:02            94606338
+mainnet-00194-73ba4d4b.era                         19-Apr-2023 08:02            92119107
+mainnet-00195-c6b10414.era                         19-Apr-2023 08:02            94695632
+mainnet-00196-a0c04a41.era                         19-Apr-2023 08:02            99263490
+mainnet-00197-e326d80f.era                         19-Apr-2023 08:02            90632581
+mainnet-00198-e4b20428.era                         19-Apr-2023 08:02            98177442
+mainnet-00199-1229bb7a.era                         19-Apr-2023 08:02           101276205
+mainnet-00200-1059dbf9.era                         19-Apr-2023 08:02           109459948
+mainnet-00201-7b7bcf26.era                         19-Apr-2023 08:02           103228424
+mainnet-00202-43ff90bf.era                         19-Apr-2023 08:02           100601822
+mainnet-00203-7ced6085.era                         19-Apr-2023 08:02           104361405
+mainnet-00204-aaf7f80a.era                         19-Apr-2023 08:02           102238704
+mainnet-00205-3e062302.era                         19-Apr-2023 08:02           102660932
+mainnet-00206-29475177.era                         19-Apr-2023 08:02           102893233
+mainnet-00207-1cec56ec.era                         19-Apr-2023 08:02           101029314
+mainnet-00208-09589075.era                         19-Apr-2023 08:02           104283853
+mainnet-00209-af8a2b2c.era                         19-Apr-2023 08:02           102192843
+mainnet-00210-53e9f2b8.era                         19-Apr-2023 08:02            98837926
+mainnet-00211-dd4773b5.era                         19-Apr-2023 08:02           100401650
+mainnet-00212-1f505eb5.era                         19-Apr-2023 08:02           106285285
+mainnet-00213-80ebec77.era                         19-Apr-2023 08:02           108087872
+mainnet-00214-0753b745.era                         19-Apr-2023 08:02           107119331
+mainnet-00215-8c624631.era                         19-Apr-2023 08:02           107194383
+mainnet-00216-67e04584.era                         19-Apr-2023 08:02           105244736
+mainnet-00217-529bb4d5.era                         19-Apr-2023 08:02           107292287
+mainnet-00218-918403ea.era                         19-Apr-2023 08:02           108463200
+mainnet-00219-e97456c3.era                         19-Apr-2023 08:02           109578337
+mainnet-00220-1b4181c7.era                         19-Apr-2023 08:02           109367454
+mainnet-00221-bbf1ea31.era                         19-Apr-2023 08:02           115692306
+mainnet-00222-f5c6fc10.era                         19-Apr-2023 08:02           116154257
+mainnet-00223-e3860536.era                         19-Apr-2023 08:02           117562693
+mainnet-00224-47162a88.era                         19-Apr-2023 08:02           121982248
+mainnet-00225-e4b0ee30.era                         19-Apr-2023 08:02           121410422
+mainnet-00226-474f9d41.era                         19-Apr-2023 08:02           120006785
+mainnet-00227-06b92185.era                         19-Apr-2023 08:02           117339637
+mainnet-00228-e5f13f42.era                         19-Apr-2023 08:02           122596886
+mainnet-00229-fc7ce170.era                         19-Apr-2023 08:02           123403174
+mainnet-00230-59598ac3.era                         19-Apr-2023 08:02           131368809
+mainnet-00231-2e78e382.era                         19-Apr-2023 08:02           143342472
+mainnet-00232-af149f76.era                         19-Apr-2023 08:02           122533494
+mainnet-00233-bad539a1.era                         19-Apr-2023 08:02           131088925
+mainnet-00234-911f5e67.era                         19-Apr-2023 08:02           124332893
+mainnet-00235-13d7f81a.era                         19-Apr-2023 08:02           127609612
+mainnet-00236-60ab620b.era                         19-Apr-2023 08:02           132286023
+mainnet-00237-8cd6d6f8.era                         19-Apr-2023 08:02           131961381
+mainnet-00238-dc7f6d7f.era                         19-Apr-2023 08:02           134331881
+mainnet-00239-e4cb8d62.era                         19-Apr-2023 08:02           138355516
+mainnet-00240-47e84e3b.era                         19-Apr-2023 08:02           140431003
+mainnet-00241-e1ba6894.era                         19-Apr-2023 08:02           140508194
+mainnet-00242-ff76e7b7.era                         19-Apr-2023 08:02           136578011
+mainnet-00243-8af7482e.era                         19-Apr-2023 08:02           135003688
+mainnet-00244-0ec22b52.era                         19-Apr-2023 08:02           126396110
+mainnet-00245-c084edd8.era                         19-Apr-2023 08:02           127944080
+mainnet-00246-0596ae6d.era                         19-Apr-2023 08:02           132953959
+mainnet-00247-e066d037.era                         19-Apr-2023 08:02           134959158
+mainnet-00248-859b3a5e.era                         19-Apr-2023 08:02           138410520
+mainnet-00249-28c20404.era                         19-Apr-2023 08:02           138654797
+mainnet-00250-1c525361.era                         19-Apr-2023 08:02           133548500
+mainnet-00251-51ca4809.era                         19-Apr-2023 08:02           130100495
+mainnet-00252-75a660ba.era                         19-Apr-2023 08:02           131712045
+mainnet-00253-87f32efb.era                         19-Apr-2023 08:02           136508760
+mainnet-00254-c2e7d484.era                         19-Apr-2023 08:02           135425750
+mainnet-00255-a7c46790.era                         19-Apr-2023 08:02           131551434
+mainnet-00256-2db6dfeb.era                         19-Apr-2023 08:02           131161922
+mainnet-00257-3dab898e.era                         19-Apr-2023 08:02           136207857
+mainnet-00258-8716b540.era                         19-Apr-2023 08:02           139220947
+mainnet-00259-0c5952dc.era                         19-Apr-2023 08:02           138595540
+mainnet-00260-c7e49e0d.era                         19-Apr-2023 08:03           138043147
+mainnet-00261-9fc5de06.era                         19-Apr-2023 08:03           140066104
+mainnet-00262-7e503b13.era                         19-Apr-2023 08:03           135104819
+mainnet-00263-6f24973b.era                         19-Apr-2023 08:03           131436160
+mainnet-00264-f67a09e3.era                         19-Apr-2023 08:03           137433017
+mainnet-00265-1468e348.era                         19-Apr-2023 08:03           136067719
+mainnet-00266-ad6fcf9c.era                         19-Apr-2023 08:03           138802586
+mainnet-00267-2a973e19.era                         19-Apr-2023 08:03           142173149
+mainnet-00268-80db539b.era                         19-Apr-2023 08:03           138784296
+mainnet-00269-463379f5.era                         19-Apr-2023 08:03           138787503
+mainnet-00270-3d352348.era                         19-Apr-2023 08:03           140145338
+mainnet-00271-99ea5dc6.era                         19-Apr-2023 08:03           142078088
+mainnet-00272-ae99a2a7.era                         19-Apr-2023 08:03           143222093
+mainnet-00273-4fc9d8ff.era                         19-Apr-2023 08:03           143392174
+mainnet-00274-7305fb17.era                         19-Apr-2023 08:03           142417127
+mainnet-00275-fb002049.era                         19-Apr-2023 08:03           141451380
+mainnet-00276-0ab34c16.era                         19-Apr-2023 08:03           142634264
+mainnet-00277-87c2d149.era                         19-Apr-2023 08:03           143249113
+mainnet-00278-e351c112.era                         19-Apr-2023 08:03           142725486
+mainnet-00279-fccc599d.era                         19-Apr-2023 08:03           143806302
+mainnet-00280-6df55667.era                         19-Apr-2023 08:03           143477409
+mainnet-00281-f17a7ec9.era                         19-Apr-2023 08:03           143444436
+mainnet-00282-49e08203.era                         19-Apr-2023 08:03           143611554
+mainnet-00283-409dab3d.era                         19-Apr-2023 08:03           143460220
+mainnet-00284-ccb80e2d.era                         19-Apr-2023 08:03           143031890
+mainnet-00285-74c3f8f9.era                         19-Apr-2023 08:03           143092162
+mainnet-00286-7a0086e7.era                         19-Apr-2023 08:03           143484675
+mainnet-00287-3848b932.era                         19-Apr-2023 08:03           143137543
+mainnet-00288-92c7bf45.era                         19-Apr-2023 08:03           142200815
+mainnet-00289-f2d57377.era                         19-Apr-2023 08:03           140465017
+mainnet-00290-f66bf737.era                         19-Apr-2023 08:03           132901777
+mainnet-00291-5cffd097.era                         19-Apr-2023 08:03           133799498
+mainnet-00292-b54d6727.era                         19-Apr-2023 08:03           140895436
+mainnet-00293-f9cbebe4.era                         19-Apr-2023 08:03           142635649
+mainnet-00294-a52fc044.era                         19-Apr-2023 08:03           142286481
+mainnet-00295-d764c311.era                         19-Apr-2023 08:03           140936625
+mainnet-00296-0b74d6bf.era                         19-Apr-2023 08:03           135067781
+mainnet-00297-2c312b09.era                         19-Apr-2023 08:03           135852621
+mainnet-00298-c3d2f59a.era                         19-Apr-2023 08:03           137782958
+mainnet-00299-9ca1bb8b.era                         19-Apr-2023 08:03           139210195
+mainnet-00300-23835f14.era                         19-Apr-2023 08:03           140775098
+mainnet-00301-94bdd692.era                         19-Apr-2023 08:03           141618192
+mainnet-00302-672d899b.era                         19-Apr-2023 08:03           141714545
+mainnet-00303-ef8bfe3a.era                         19-Apr-2023 08:03           141126500
+mainnet-00304-f423ed3c.era                         19-Apr-2023 08:03           140685485
+mainnet-00305-b8804abf.era                         19-Apr-2023 08:03           138826271
+mainnet-00306-9260a0c6.era                         19-Apr-2023 08:03           142131990
+mainnet-00307-deb43161.era                         19-Apr-2023 08:03           143429675
+mainnet-00308-cacc5c4c.era                         19-Apr-2023 08:03           142623937
+mainnet-00309-1f9d17d1.era                         19-Apr-2023 08:03           143304925
+mainnet-00310-9b2dd0f7.era                         19-Apr-2023 08:03           140975104
+mainnet-00311-74bc9b12.era                         19-Apr-2023 08:03           141169227
+mainnet-00312-039dbadf.era                         19-Apr-2023 08:03           143320041
+mainnet-00313-cb78de56.era                         19-Apr-2023 08:03           144544508
+mainnet-00314-aa045fb3.era                         19-Apr-2023 08:03           144207060
+mainnet-00315-86151ee0.era                         19-Apr-2023 08:03           144284174
+mainnet-00316-a768cb73.era                         19-Apr-2023 08:03           144308418
+mainnet-00317-c55ea3b8.era                         19-Apr-2023 08:03           144725081
+mainnet-00318-a6ca04a3.era                         19-Apr-2023 08:03           143601635
+mainnet-00319-8fa923d9.era                         19-Apr-2023 08:03           144570935
+mainnet-00320-c9dad55c.era                         19-Apr-2023 08:03           140119571
+mainnet-00321-3048f935.era                         19-Apr-2023 08:03           143827715
+mainnet-00322-70f99be0.era                         19-Apr-2023 08:03           145079590
+mainnet-00323-9c951b17.era                         19-Apr-2023 08:03           143859235
+mainnet-00324-41c96e46.era                         19-Apr-2023 08:03           145612087
+mainnet-00325-ee523bad.era                         19-Apr-2023 08:03           144200381
+mainnet-00326-81b666eb.era                         19-Apr-2023 08:03           144883081
+mainnet-00327-8a4ac4bb.era                         19-Apr-2023 08:03           144088378
+mainnet-00328-6a504821.era                         19-Apr-2023 08:03           145246261
+mainnet-00329-d880d95f.era                         19-Apr-2023 08:03           144544227
+mainnet-00330-c47672af.era                         19-Apr-2023 08:03           144395898
+mainnet-00331-92a3286e.era                         19-Apr-2023 08:03           144314761
+mainnet-00332-0e3d1e11.era                         19-Apr-2023 08:03           143410533
+mainnet-00333-1664895f.era                         19-Apr-2023 08:03           141165354
+mainnet-00334-bf5b55e0.era                         19-Apr-2023 08:03           140686127
+mainnet-00335-362a9895.era                         19-Apr-2023 08:03           142357964
+mainnet-00336-16d958db.era                         19-Apr-2023 08:03           143097863
+mainnet-00337-dffe144a.era                         19-Apr-2023 08:03           142880225
+mainnet-00338-fc13e70a.era                         19-Apr-2023 08:03           143369692
+mainnet-00339-50d6189f.era                         19-Apr-2023 08:03           140687599
+mainnet-00340-8e1fa202.era                         19-Apr-2023 08:03           142915993
+mainnet-00341-a9118dca.era                         19-Apr-2023 08:03           144257850
+mainnet-00342-bbf4f40a.era                         19-Apr-2023 08:03           142945729
+mainnet-00343-ae87bbfa.era                         19-Apr-2023 08:03           139730408
+mainnet-00344-97def269.era                         19-Apr-2023 08:03           144287893
+mainnet-00345-ee60fe7f.era                         19-Apr-2023 08:03           145247533
+mainnet-00346-afe75173.era                         19-Apr-2023 08:03           140383275
+mainnet-00347-ebe40c78.era                         19-Apr-2023 08:03           137305156
+mainnet-00348-31ed95ce.era                         19-Apr-2023 08:03           140187571
+mainnet-00349-1f8ba855.era                         19-Apr-2023 08:03           137702054
+mainnet-00350-f6b68695.era                         19-Apr-2023 08:04           142022093
+mainnet-00351-b1c0e38d.era                         19-Apr-2023 08:04           144745315
+mainnet-00352-9989d5e9.era                         19-Apr-2023 08:04           145697985
+mainnet-00353-5ae0ffb9.era                         19-Apr-2023 08:04           144158393
+mainnet-00354-ca0dc4e4.era                         19-Apr-2023 08:04           144468288
+mainnet-00355-43f765be.era                         19-Apr-2023 08:04           143115320
+mainnet-00356-f1bffc9a.era                         19-Apr-2023 08:04           142731615
+mainnet-00357-c542316b.era                         19-Apr-2023 08:04           139120413
+mainnet-00358-cc546403.era                         19-Apr-2023 08:04           135280520
+mainnet-00359-5054fa14.era                         19-Apr-2023 08:04           136014293
+mainnet-00360-08de2754.era                         19-Apr-2023 08:04           134986110
+mainnet-00361-c09d813e.era                         19-Apr-2023 08:04           142068273
+mainnet-00362-91268052.era                         19-Apr-2023 08:04           144734024
+mainnet-00363-b197ad33.era                         19-Apr-2023 08:04           145221207
+mainnet-00364-d873d778.era                         19-Apr-2023 08:04           143957528
+mainnet-00365-e905337e.era                         19-Apr-2023 08:04           144707364
+mainnet-00366-041623bf.era                         19-Apr-2023 08:04           141526500
+mainnet-00367-4027a2bc.era                         19-Apr-2023 08:04           138061319
+mainnet-00368-0962c836.era                         19-Apr-2023 08:04           144491489
+mainnet-00369-6dc5bdb8.era                         19-Apr-2023 08:04           146753546
+mainnet-00370-6ec40871.era                         19-Apr-2023 08:04           144573059
+mainnet-00371-d5b51b1f.era                         19-Apr-2023 08:04           142026136
+mainnet-00372-a40692c4.era                         19-Apr-2023 08:04           144712869
+mainnet-00373-c3baa5c6.era                         19-Apr-2023 08:04           142716858
+mainnet-00374-c79b0c6f.era                         19-Apr-2023 08:04           141000156
+mainnet-00375-f97aae3b.era                         19-Apr-2023 08:04           144907908
+mainnet-00376-d19f272a.era                         19-Apr-2023 08:04           146643752
+mainnet-00377-493ff9fb.era                         19-Apr-2023 08:04           147601635
+mainnet-00378-c0faf344.era                         19-Apr-2023 08:04           147740569
+mainnet-00379-93288ae1.era                         19-Apr-2023 08:04           147116558
+mainnet-00380-20c8d848.era                         19-Apr-2023 08:04           146790818
+mainnet-00381-d4a23bda.era                         19-Apr-2023 08:04           147056664
+mainnet-00382-563fafb7.era                         19-Apr-2023 08:04           145773378
+mainnet-00383-ac873f2c.era                         19-Apr-2023 08:04           145787364
+mainnet-00384-301dfcb3.era                         19-Apr-2023 08:04           143853946
+mainnet-00385-382ffae8.era                         19-Apr-2023 08:04           145489423
+mainnet-00386-93708f54.era                         19-Apr-2023 08:04           146854074
+mainnet-00387-14a9a844.era                         19-Apr-2023 08:04           147725465
+mainnet-00388-4fd885a2.era                         19-Apr-2023 08:04           143902764
+mainnet-00389-a7c98c34.era                         19-Apr-2023 08:04           142402218
+mainnet-00390-a364d119.era                         19-Apr-2023 08:04           142419416
+mainnet-00391-f403cdd3.era                         19-Apr-2023 08:04           145201759
+mainnet-00392-214f6b79.era                         19-Apr-2023 08:04           146785388
+mainnet-00393-8d4ee9ba.era                         19-Apr-2023 08:04           146040091
+mainnet-00394-3c3885bc.era                         19-Apr-2023 08:04           147195813
+mainnet-00395-9239d7ce.era                         19-Apr-2023 08:04           148416246
+mainnet-00396-019b8d1e.era                         19-Apr-2023 08:04           147350365
+mainnet-00397-2ada2dcf.era                         19-Apr-2023 08:04           148159111
+mainnet-00398-2539f1cb.era                         19-Apr-2023 08:04           148741299
+mainnet-00399-ea503937.era                         19-Apr-2023 08:04           148402948
+mainnet-00400-cebcae93.era                         19-Apr-2023 08:04           148454062
+mainnet-00401-904b72bd.era                         19-Apr-2023 08:04           149528573
+mainnet-00402-35c28170.era                         19-Apr-2023 08:04           150243706
+mainnet-00403-bb46bb18.era                         19-Apr-2023 08:04           148118111
+mainnet-00404-882a68f8.era                         19-Apr-2023 08:04           149981367
+mainnet-00405-b25162bf.era                         19-Apr-2023 08:04           149041765
+mainnet-00406-50991f74.era                         19-Apr-2023 08:04           147628707
+mainnet-00407-d485b3b4.era                         19-Apr-2023 08:04           149035343
+mainnet-00408-bce51f59.era                         19-Apr-2023 08:04           148863331
+mainnet-00409-12a8fedb.era                         19-Apr-2023 08:04           149293274
+mainnet-00410-273264a4.era                         19-Apr-2023 08:04           150353388
+mainnet-00411-344d3e70.era                         19-Apr-2023 08:04           150214222
+mainnet-00412-95698adb.era                         19-Apr-2023 08:04           150998512
+mainnet-00413-e622731a.era                         19-Apr-2023 08:04           152868638
+mainnet-00414-00314316.era                         19-Apr-2023 08:04           149856037
+mainnet-00415-9205cd0e.era                         19-Apr-2023 08:04           151302096
+mainnet-00416-211ee89e.era                         19-Apr-2023 08:04           147818765
+mainnet-00417-214f1587.era                         19-Apr-2023 08:04           146519980
+mainnet-00418-984b0fb0.era                         19-Apr-2023 08:04           145008254
+mainnet-00419-be35f9d3.era                         19-Apr-2023 08:04           143471753
+mainnet-00420-716e07c6.era                         19-Apr-2023 08:04           145488915
+mainnet-00421-2d964c44.era                         19-Apr-2023 08:04           145647054
+mainnet-00422-688ff302.era                         19-Apr-2023 08:04           149788343
+mainnet-00423-a8837094.era                         19-Apr-2023 08:04           148359905
+mainnet-00424-66592198.era                         19-Apr-2023 08:04           148839481
+mainnet-00425-f1224927.era                         19-Apr-2023 08:04           147991905
+mainnet-00426-e501e21e.era                         19-Apr-2023 08:04           149688020
+mainnet-00427-7c5a03e6.era                         19-Apr-2023 08:04           141917550
+mainnet-00428-e89501d6.era                         19-Apr-2023 08:04           141561621
+mainnet-00429-b07b7ecd.era                         19-Apr-2023 08:04           132041593
+mainnet-00430-9d576a91.era                         19-Apr-2023 08:04           130907692
+mainnet-00431-a6f08e88.era                         19-Apr-2023 08:04           130563397
+mainnet-00432-02eda247.era                         19-Apr-2023 08:04           141379377
+mainnet-00433-4a10be25.era                         19-Apr-2023 08:04           149681451
+mainnet-00434-daba5aa4.era                         19-Apr-2023 08:04           149216678
+mainnet-00435-46ac713f.era                         19-Apr-2023 08:04           146975846
+mainnet-00436-cf498157.era                         19-Apr-2023 08:04           139380529
+mainnet-00437-bd3d1515.era                         19-Apr-2023 08:04           140259423
+mainnet-00438-3a702b67.era                         19-Apr-2023 08:04           141864937
+mainnet-00439-2cb045ae.era                         19-Apr-2023 08:04           142790480
+mainnet-00440-a03195a1.era                         19-Apr-2023 08:04           143467511
+mainnet-00441-a652d5ff.era                         19-Apr-2023 08:05           144613205
+mainnet-00442-177df8c4.era                         19-Apr-2023 08:05           146002671
+mainnet-00443-9e30c504.era                         19-Apr-2023 08:05           146292377
+mainnet-00444-ad034c6b.era                         19-Apr-2023 08:05           145665055
+mainnet-00445-f92f372c.era                         19-Apr-2023 08:05           147829082
+mainnet-00446-c9ff4460.era                         19-Apr-2023 08:05           148983548
+mainnet-00447-93909b1c.era                         19-Apr-2023 08:05           151487652
+mainnet-00448-3e2020be.era                         19-Apr-2023 08:05           150375082
+mainnet-00449-420aa4e7.era                         19-Apr-2023 08:05           150551309
+mainnet-00450-0139a40c.era                         19-Apr-2023 08:05           152704757
+mainnet-00451-ab0430cb.era                         19-Apr-2023 08:05           152803994
+mainnet-00452-19970c07.era                         19-Apr-2023 08:05           150643202
+mainnet-00453-1d23ae80.era                         19-Apr-2023 08:05           150234390
+mainnet-00454-a203fd6a.era                         19-Apr-2023 08:05           149779940
+mainnet-00455-e83c7f19.era                         19-Apr-2023 08:05           151769936
+mainnet-00456-ed2ed157.era                         19-Apr-2023 08:05           152586963
+mainnet-00457-7647d780.era                         19-Apr-2023 08:05           150812062
+mainnet-00458-d83e98a0.era                         19-Apr-2023 08:05           150456529
+mainnet-00459-4c94657e.era                         19-Apr-2023 08:05           150180357
+mainnet-00460-b3d0336a.era                         19-Apr-2023 08:05           149390443
+mainnet-00461-b9cc17aa.era                         19-Apr-2023 08:05           150521363
+mainnet-00462-84b64267.era                         19-Apr-2023 08:05           150935934
+mainnet-00463-2c3e04f9.era                         19-Apr-2023 08:05           148737199
+mainnet-00464-ce81b446.era                         19-Apr-2023 08:05           148035817
+mainnet-00465-17169abc.era                         19-Apr-2023 08:05           151294041
+mainnet-00466-6f96819e.era                         19-Apr-2023 08:05           149965989
+mainnet-00467-d72171ba.era                         19-Apr-2023 08:05           150079467
+mainnet-00468-288eb7d2.era                         19-Apr-2023 08:05           149299506
+mainnet-00469-ac1bb583.era                         19-Apr-2023 08:05           149766225
+mainnet-00470-17db03ab.era                         19-Apr-2023 08:05           154542826
+mainnet-00471-d8736340.era                         19-Apr-2023 08:05           152828081
+mainnet-00472-c903f576.era                         19-Apr-2023 08:05           149920625
+mainnet-00473-85544b2c.era                         19-Apr-2023 08:05           143204985
+mainnet-00474-b5cb18bf.era                         19-Apr-2023 08:05           143883862
+mainnet-00475-1bcb0b75.era                         19-Apr-2023 08:05           149707407
+mainnet-00476-bd7f073e.era                         19-Apr-2023 08:05           148642578
+mainnet-00477-e687403d.era                         19-Apr-2023 08:05           148540204
+mainnet-00478-e352f77e.era                         19-Apr-2023 08:05           150408471
+mainnet-00479-1d77e810.era                         19-Apr-2023 08:05           149977377
+mainnet-00480-420ecbda.era                         19-Apr-2023 08:05           150153985
+mainnet-00481-3ca53867.era                         19-Apr-2023 08:05           150151322
+mainnet-00482-fa14e924.era                         19-Apr-2023 08:05           150942816
+mainnet-00483-816d1efe.era                         19-Apr-2023 08:05           150443511
+mainnet-00484-c1982d2d.era                         19-Apr-2023 08:05           149366089
+mainnet-00485-d6ea460b.era                         19-Apr-2023 08:05           149470782
+mainnet-00486-e3f47d77.era                         19-Apr-2023 08:05           147843144
+mainnet-00487-ca45547d.era                         19-Apr-2023 08:05           148553021
+mainnet-00488-6aef4d62.era                         19-Apr-2023 08:05           149505209
+mainnet-00489-c227388d.era                         19-Apr-2023 08:05           150393101
+mainnet-00490-b01d5e10.era                         19-Apr-2023 08:05           147078140
+mainnet-00491-05d38617.era                         19-Apr-2023 08:05           148069836
+mainnet-00492-ce9419e1.era                         19-Apr-2023 08:05           150646658
+mainnet-00493-e65f5d9d.era                         19-Apr-2023 08:05           151421913
+mainnet-00494-461696a2.era                         19-Apr-2023 08:05           152059901
+mainnet-00495-bb43374c.era                         19-Apr-2023 08:05           152803741
+mainnet-00496-3e322bc4.era                         19-Apr-2023 08:05           153827569
+mainnet-00497-6c371884.era                         19-Apr-2023 08:05           152946047
+mainnet-00498-37951b8a.era                         19-Apr-2023 08:05           150824865
+mainnet-00499-cf687cf0.era                         19-Apr-2023 08:05           151192378
+mainnet-00500-87109713.era                         19-Apr-2023 08:05           152823056
+mainnet-00501-5cc4a456.era                         19-Apr-2023 08:05           146391337
+mainnet-00502-10e603ed.era                         19-Apr-2023 08:05           148199375
+mainnet-00503-8ef7268b.era                         19-Apr-2023 08:05           151875291
+mainnet-00504-996ab41d.era                         19-Apr-2023 08:05           151763994
+mainnet-00505-f262525d.era                         19-Apr-2023 08:05           150677541
+mainnet-00506-a426c61e.era                         19-Apr-2023 08:05           149249452
+mainnet-00507-2fad0c94.era                         19-Apr-2023 08:05           150760026
+mainnet-00508-04f87fed.era                         19-Apr-2023 08:05           152027833
+mainnet-00509-d86d2672.era                         19-Apr-2023 08:05           151507444
+mainnet-00510-767cf672.era                         19-Apr-2023 08:05           151610239
+mainnet-00511-78a7fd5a.era                         19-Apr-2023 08:05           151887859
+mainnet-00512-99c76049.era                         19-Apr-2023 08:05           151009351
+mainnet-00513-35109a06.era                         19-Apr-2023 08:05           150910062
+mainnet-00514-2e322919.era                         19-Apr-2023 08:05           151409329
+mainnet-00515-b3354541.era                         19-Apr-2023 08:05           146896352
+mainnet-00516-aa99a4c4.era                         19-Apr-2023 08:05           144440605
+mainnet-00517-68961dbb.era                         19-Apr-2023 08:05           151542512
+mainnet-00518-4e267a3a.era                         19-Apr-2023 08:05           149956234
+mainnet-00519-11f02ea8.era                         19-Apr-2023 08:05           152191999
+mainnet-00520-fca505d1.era                         19-Apr-2023 08:05           150897624
+mainnet-00521-ba5a52a0.era                         19-Apr-2023 08:05           151664141
+mainnet-00522-8882f3b8.era                         19-Apr-2023 08:05           147405485
+mainnet-00523-ddc83067.era                         19-Apr-2023 08:05           145993937
+mainnet-00524-5068f888.era                         19-Apr-2023 08:05           147257515
+mainnet-00525-681ebfa9.era                         19-Apr-2023 08:05           148244705
+mainnet-00526-8483ee75.era                         19-Apr-2023 08:05           149990129
+mainnet-00527-051b3b7d.era                         19-Apr-2023 08:06           148358353
+mainnet-00528-e3733d57.era                         19-Apr-2023 08:06           147214678
+mainnet-00529-ac324fb0.era                         19-Apr-2023 08:06           147406673
+mainnet-00530-59213b31.era                         19-Apr-2023 08:06           144574735
+mainnet-00531-3106561c.era                         19-Apr-2023 08:06           141321005
+mainnet-00532-f09fb770.era                         19-Apr-2023 08:06           138975960
+mainnet-00533-d5b6eb9e.era                         19-Apr-2023 08:06           145204913
+mainnet-00534-b5fbeb2f.era                         19-Apr-2023 08:06           147933076
+mainnet-00535-b3845d23.era                         19-Apr-2023 08:06           149434998
+mainnet-00536-5d6d5372.era                         19-Apr-2023 08:06           149924559
+mainnet-00537-636a6c7b.era                         19-Apr-2023 08:06           148891119
+mainnet-00538-0db607c0.era                         19-Apr-2023 08:06           149941116
+mainnet-00539-eccb94a6.era                         19-Apr-2023 08:06           151451856
+mainnet-00540-d293c891.era                         19-Apr-2023 08:06           151222012
+mainnet-00541-baba46b8.era                         19-Apr-2023 08:06           149892687
+mainnet-00542-c9f98e60.era                         19-Apr-2023 08:06           149808275
+mainnet-00543-1bf67198.era                         19-Apr-2023 08:06           149872098
+mainnet-00544-ccb7f2e0.era                         19-Apr-2023 08:06           150620915
+mainnet-00545-89b6b06c.era                         19-Apr-2023 08:06           146478100
+mainnet-00546-4eef8f7f.era                         19-Apr-2023 08:06           140125331
+mainnet-00547-0166dac1.era                         19-Apr-2023 08:06           144301100
+mainnet-00548-fff2c048.era                         19-Apr-2023 08:06           148181648
+mainnet-00549-6da817b9.era                         19-Apr-2023 08:06           139007607
+mainnet-00550-22438f92.era                         19-Apr-2023 08:06           138354832
+mainnet-00551-fac0cbff.era                         19-Apr-2023 08:06           139046367
+mainnet-00552-e0f39697.era                         19-Apr-2023 08:06           140697882
+mainnet-00553-4ec1668f.era                         19-Apr-2023 08:06           139229765
+mainnet-00554-2af21c9d.era                         19-Apr-2023 08:06           140890557
+mainnet-00555-0206a1af.era                         19-Apr-2023 08:06           141196410
+mainnet-00556-c480a1a9.era                         19-Apr-2023 08:06           144861549
+mainnet-00557-690b98ad.era                         19-Apr-2023 08:06           133346780
+mainnet-00558-73deda10.era                         19-Apr-2023 08:06           129442464
+mainnet-00559-e15ed367.era                         19-Apr-2023 08:06           136700182
+mainnet-00560-224c677c.era                         19-Apr-2023 08:06           142617954
+mainnet-00561-15de525a.era                         19-Apr-2023 08:06           135296243
+mainnet-00562-e47bdb20.era                         19-Apr-2023 08:06           133406089
+mainnet-00563-f24d04c9.era                         19-Apr-2023 08:06           144994150
+mainnet-00564-dcd6c750.era                         19-Apr-2023 08:06           146784212
+mainnet-00565-2d7775ea.era                         19-Apr-2023 08:06           147304487
+mainnet-00566-19353898.era                         19-Apr-2023 08:06           137093662
+mainnet-00567-cd412b92.era                         19-Apr-2023 08:06           138876874
+mainnet-00568-c5b1968c.era                         19-Apr-2023 08:06           134999727
+mainnet-00569-ff2f4627.era                         19-Apr-2023 08:06           131216076
+mainnet-00570-891fb8fd.era                         19-Apr-2023 08:06           130411652
+mainnet-00571-a957e865.era                         19-Apr-2023 08:06           136919974
+mainnet-00572-67cc2c10.era                         19-Apr-2023 08:06           136993635
+mainnet-00573-c847a969.era                         19-Apr-2023 08:06           133719903
+mainnet-00574-3c0da77d.era                         19-Apr-2023 08:06           224997856
+mainnet-00575-abe0d5d9.era                         19-Apr-2023 08:06           495576057
+mainnet-00576-18473343.era                         19-Apr-2023 08:06           462703957
+mainnet-00577-3945c8e9.era                         19-Apr-2023 08:06           462701274
+mainnet-00578-1b2761b0.era                         19-Apr-2023 08:06           488690656
+mainnet-00579-505b6eca.era                         19-Apr-2023 08:06           516646616
+mainnet-00580-24505b1b.era                         19-Apr-2023 08:06           540829778
+mainnet-00581-5fed1297.era                         19-Apr-2023 08:06           525912796
+mainnet-00582-a55650a7.era                         19-Apr-2023 08:06           507902313
+mainnet-00583-51fb01b3.era                         19-Apr-2023 08:06           481257827
+mainnet-00584-b9214ce1.era                         19-Apr-2023 08:06           496943155
+mainnet-00585-125a6940.era                         19-Apr-2023 08:07           502140763
+mainnet-00586-865166e0.era                         19-Apr-2023 08:07           506779555
+mainnet-00587-6e183cad.era                         19-Apr-2023 08:07           509832473
+mainnet-00588-c8808368.era                         19-Apr-2023 08:07           503822671
+mainnet-00589-42803687.era                         19-Apr-2023 08:07           500850156
+mainnet-00590-2ed09275.era                         19-Apr-2023 08:07           494815765
+mainnet-00591-563fe434.era                         19-Apr-2023 08:07           495012154
+mainnet-00592-769bda94.era                         19-Apr-2023 08:07           508855516
+mainnet-00593-11494dbb.era                         19-Apr-2023 08:07           509268079
+mainnet-00594-38249b5c.era                         19-Apr-2023 08:07           502077066
+mainnet-00595-418d306c.era                         19-Apr-2023 08:07           432608543
+mainnet-00596-0c8f6483.era                         19-Apr-2023 08:07           412850034
+mainnet-00597-f8f5e231.era                         19-Apr-2023 08:07           448546679
+mainnet-00598-c1955d2f.era                         19-Apr-2023 08:07           445687915
+mainnet-00599-2de31eb5.era                         19-Apr-2023 08:07           487034006
+mainnet-00600-e031a37d.era                         19-Apr-2023 08:07           457977414
+mainnet-00601-336cddc5.era                         19-Apr-2023 08:07           433884715
+mainnet-00602-a9694dc1.era                         19-Apr-2023 08:07           456087586
+mainnet-00603-3f1f76df.era                         19-Apr-2023 08:07           479079484
+mainnet-00604-15aadeb5.era                         19-Apr-2023 08:07           498414286
+mainnet-00605-46375bab.era                         19-Apr-2023 08:07           511955699
+mainnet-00606-6ca5d9c2.era                         19-Apr-2023 08:07           486082684
+mainnet-00607-62b47765.era                         19-Apr-2023 08:07           449335714
+mainnet-00608-d6a7c702.era                         19-Apr-2023 08:07           482482451
+mainnet-00609-73a22c92.era                         19-Apr-2023 08:07           499823134
+mainnet-00610-85001de6.era                         19-Apr-2023 08:08           549047929
+mainnet-00611-48491d55.era                         19-Apr-2023 08:08           564984931
+mainnet-00612-b6190577.era                         19-Apr-2023 08:08           543066354
+mainnet-00613-b2392947.era                         19-Apr-2023 08:08           520443934
+mainnet-00614-01adaa2b.era                         19-Apr-2023 08:08           515626872
+mainnet-00615-a16e6eab.era                         19-Apr-2023 08:08           529367293
+mainnet-00616-fef77a50.era                         19-Apr-2023 08:08           528334097
+mainnet-00617-453b53bf.era                         19-Apr-2023 08:08           534903189
+mainnet-00618-6e7d7dd3.era                         19-Apr-2023 08:08           540347892
+mainnet-00619-e9e5ecbb.era                         19-Apr-2023 08:08           536071235
+mainnet-00620-fb989681.era                         19-Apr-2023 08:08           522349450
+mainnet-00621-1639ac56.era                         19-Apr-2023 08:08           562031375
+mainnet-00622-7bc85a3d.era                         19-Apr-2023 08:08           631897088
+mainnet-00623-69d46abc.era                         19-Apr-2023 08:08           647883493
+mainnet-00624-a53a4850.era                         19-Apr-2023 08:08           596921421
+mainnet-00625-26c77f05.era                         19-Apr-2023 08:08           574738057
+mainnet-00626-8b8dc42a.era                         19-Apr-2023 08:08           527717095
+mainnet-00627-f6bd90de.era                         19-Apr-2023 08:08           563400942
+mainnet-00628-5bff8aa6.era                         19-Apr-2023 08:08           541930570
+mainnet-00629-0b6885ea.era                         19-Apr-2023 08:08           522505585
+mainnet-00630-43736ddb.era                         19-Apr-2023 08:08           531768041
+mainnet-00631-dbbf2183.era                         19-Apr-2023 08:08           512125978
+mainnet-00632-b8b89228.era                         19-Apr-2023 08:08           560138303
+mainnet-00633-c5cdb62b.era                         19-Apr-2023 08:08           572780751
+mainnet-00634-0226338b.era                         19-Apr-2023 08:09           565225106
+mainnet-00635-b817d4a1.era                         19-Apr-2023 08:09           564228603
+mainnet-00636-dd28d082.era                         19-Apr-2023 08:09           528129901
+mainnet-00637-3bd698db.era                         19-Apr-2023 08:09           519734299
+mainnet-00638-357ca82b.era                         19-Apr-2023 08:09           507251686
+mainnet-00639-8c7ad2cb.era                         19-Apr-2023 08:09           524765203
+mainnet-00640-50649802.era                         19-Apr-2023 08:09           548440213
+mainnet-00641-7fc62116.era                         19-Apr-2023 08:09           553277962
+mainnet-00642-7f6f0637.era                         19-Apr-2023 08:09           537226667
+mainnet-00643-b6a63b71.era                         19-Apr-2023 08:09           527018749
+mainnet-00644-5f8f506f.era                         19-Apr-2023 08:09           499443483
+mainnet-00645-2188864c.era                         19-Apr-2023 08:09           521221458
+mainnet-00646-a8314126.era                         19-Apr-2023 08:09           535934162
+mainnet-00647-d4209a57.era                         19-Apr-2023 08:09           545777379
+mainnet-00648-ad59a24b.era                         19-Apr-2023 08:09           532293474
+mainnet-00649-b5d83959.era                         19-Apr-2023 08:09           582735939
+mainnet-00650-4f59b460.era                         19-Apr-2023 08:09           517927212
+mainnet-00651-302c1ff2.era                         19-Apr-2023 08:09           517587788
+mainnet-00652-c0c2fcc3.era                         19-Apr-2023 08:09           571705282
+mainnet-00653-2cddb94b.era                         19-Apr-2023 08:09           590869496
+mainnet-00654-f0dfb557.era                         19-Apr-2023 08:09           595213879
+mainnet-00655-3a14592b.era                         19-Apr-2023 08:09           580015510
+mainnet-00656-8f9e0955.era                         19-Apr-2023 08:09           570332276
+mainnet-00657-3e4f8b1a.era                         19-Apr-2023 08:09           513264967
+mainnet-00658-83771ab5.era                         19-Apr-2023 08:09           525798501
+mainnet-00659-74f019cd.era                         19-Apr-2023 08:09           588220728
+mainnet-00660-67813bd4.era                         19-Apr-2023 08:09           573837245
+mainnet-00661-b86f6535.era                         19-Apr-2023 08:09           586427151
+mainnet-00662-7015d148.era                         19-Apr-2023 08:09           541604335
+mainnet-00663-f9a7edf9.era                         19-Apr-2023 08:09           511863060
+mainnet-00664-1084e515.era                         19-Apr-2023 08:09           533555817
+mainnet-00665-dfb16823.era                         19-Apr-2023 08:10           534989343
+mainnet-00666-9175daf6.era                         19-Apr-2023 08:10           563532091
+mainnet-00667-16a5a652.era                         19-Apr-2023 08:10           554848282
+mainnet-00668-fb425f19.era                         19-Apr-2023 08:10           552626889
+mainnet-00669-08edc329.era                         19-Apr-2023 08:10           478996212
+mainnet-00670-b1cd7944.era                         19-Apr-2023 08:10           498479485
+mainnet-00671-c8db5713.era                         19-Apr-2023 08:10           537942060
+mainnet-00672-3a3d1895.era                         19-Apr-2023 08:10           566016132
+mainnet-00673-5b921f7d.era                         19-Apr-2023 08:10           569669917
+mainnet-00674-1d7aad1a.era                         19-Apr-2023 08:10           583186972
+mainnet-00675-f4170ce0.era                         19-Apr-2023 08:10           561058464
+mainnet-00676-079d079f.era                         19-Apr-2023 08:10           609422904
+mainnet-00677-06c9d971.era                         19-Apr-2023 08:10           608321167
+mainnet-00678-d5de9dd7.era                         19-Apr-2023 08:10           595027792
+mainnet-00679-e69f92b7.era                         19-Apr-2023 08:10           641362841
+mainnet-00680-23976593.era                         19-Apr-2023 08:10           638607520
+mainnet-00681-fdb5c50a.era                         19-Apr-2023 08:11           637711640
+mainnet-00682-477ffb23.era                         19-Apr-2023 08:11           626984513
+mainnet-00683-7546da48.era                         19-Apr-2023 08:11           626170878
+mainnet-00684-cfdf8415.era                         19-Apr-2023 08:11           547182052
+mainnet-00685-08ee0616.era                         19-Apr-2023 08:11           508113029
+mainnet-00686-d844b1a7.era                         19-Apr-2023 08:11           501837161
+mainnet-00687-9e3e819b.era                         19-Apr-2023 08:12           504437678
+mainnet-00688-7b9ed838.era                         19-Apr-2023 08:12           472423545
+mainnet-00689-20b01cfd.era                         19-Apr-2023 08:12           504885883
+mainnet-00690-35c5500a.era                         19-Apr-2023 08:12           525538491
+mainnet-00691-89e7ee6c.era                         19-Apr-2023 08:12           549451456
+mainnet-00692-0d6d6c62.era                         19-Apr-2023 08:12           537954528
+mainnet-00693-fae1772e.era                         19-Apr-2023 08:13           502106544
+mainnet-00694-9042daf2.era                         19-Apr-2023 08:13           500180256
+mainnet-00695-c41fe322.era                         19-Apr-2023 08:13           533667519
+mainnet-00696-955bb127.era                         19-Apr-2023 08:13           525940145
+mainnet-00697-90342a2c.era                         19-Apr-2023 08:13           553934927
+mainnet-00698-a3c2b085.era                         19-Apr-2023 08:13           553493293
+mainnet-00699-6ba2e1a0.era                         19-Apr-2023 08:14           533630560
+mainnet-00700-77a2ed91.era                         19-Apr-2023 08:14           531067147
+mainnet-00701-2e20b09f.era                         23-Oct-2024 08:52           541655768
+mainnet-00702-438ec0d0.era                         19-Apr-2023 08:14           564032434
+mainnet-00703-9fedf535.era                         19-Apr-2023 08:14           573063844
+mainnet-00704-7f2b27b3.era                         19-Apr-2023 08:14           614527103
+mainnet-00705-b4707b23.era                         19-Apr-2023 08:15           546234999
+mainnet-00706-1539f181.era                         19-Apr-2023 08:15           519406975
+mainnet-00707-c160b799.era                         23-Oct-2024 08:52           562424018
+mainnet-00708-61a7c610.era                         19-Apr-2023 08:15           576833673
+mainnet-00709-b93cbd7d.era                         23-Oct-2024 08:52           603511712
+mainnet-00710-3d5b2574.era                         19-Apr-2023 08:15           609292836
+mainnet-00711-363514ef.era                         19-Apr-2023 08:15           608722863
+mainnet-00712-0331da6c.era                         19-Apr-2023 08:15           595185240
+mainnet-00713-bd93ffab.era                         19-Apr-2023 08:16           636461431
+mainnet-00714-3e95b296.era                         19-Apr-2023 08:16           653322133
+mainnet-00715-8e899bb6.era                         19-Apr-2023 08:16           660796370
+mainnet-00716-6ed75e01.era                         19-Apr-2023 08:16           661301244
+mainnet-00717-66e0f690.era                         19-Apr-2023 08:16           664040750
+mainnet-00718-f89a9b7a.era                         19-Apr-2023 08:17           586820944
+mainnet-00719-a7400052.era                         23-Oct-2024 08:52           603755469
+mainnet-00720-b0e85a12.era                         19-Apr-2023 08:17           629426399
+mainnet-00721-650423f0.era                         19-Apr-2023 08:17           613986390
+mainnet-00722-7b469136.era                         19-Apr-2023 08:17           604280079
+mainnet-00723-f8c04369.era                         19-Apr-2023 08:17           595563889
+mainnet-00724-d31afe74.era                         19-Apr-2023 08:18           566112731
+mainnet-00725-8c727b38.era                         23-Oct-2024 08:52           553276601
+mainnet-00726-411eb77c.era                         19-Apr-2023 08:18           583149242
+mainnet-00727-73a7d7a3.era                         19-Apr-2023 08:18           601017473
+mainnet-00728-cbd21a79.era                         19-Apr-2023 08:18           613201260
+mainnet-00729-040e23ac.era                         19-Apr-2023 08:18           638976201
+mainnet-00730-e0ca27c6.era                         19-Apr-2023 08:19           594243162
+mainnet-00731-f32383b2.era                         23-Oct-2024 08:53           578920126
+mainnet-00732-262b0f21.era                         19-Apr-2023 08:19           622691357
+mainnet-00733-7fe3d657.era                         19-Apr-2023 08:19           659616995
+mainnet-00734-c13a4eee.era                         19-Apr-2023 08:19           670471994
+mainnet-00735-10e158b0.era                         19-Apr-2023 08:19           673008215
+mainnet-00736-32bcbf43.era                         19-Apr-2023 08:20           671270573
+mainnet-00737-9f6aeaf8.era                         19-Apr-2023 08:20           650370129
+mainnet-00738-b5d4ed5a.era                         19-Apr-2023 08:20           694578721
+mainnet-00739-846cfb2e.era                         19-Apr-2023 08:20           734879617
+mainnet-00740-e34eaf91.era                         19-Apr-2023 08:21           730321149
+mainnet-00741-52aa7e72.era                         19-Apr-2023 08:21           963813783
+mainnet-00742-7c475e71.era                         19-Apr-2023 08:21           770905783
+mainnet-00743-b64ecc18.era                         19-Apr-2023 08:21           721802029
+mainnet-00744-892c5327.era                         19-Apr-2023 08:22           757312802
+mainnet-00745-a2d9ed02.era                         19-Apr-2023 08:22           748437207
+mainnet-00746-f37e1ddf.era                         19-Apr-2023 08:22           748192145
+mainnet-00747-29066c92.era                         19-Apr-2023 08:23           781979215
+mainnet-00748-53908038.era                         19-Apr-2023 08:23           723698704
+mainnet-00749-684cd2fd.era                         19-Apr-2023 08:23           691045939
+mainnet-00750-7a7fd2fa.era                         19-Apr-2023 08:23           748106287
+mainnet-00751-4da2b768.era                         19-Apr-2023 08:24           773658982
+mainnet-00752-1a7df1c9.era                         19-Apr-2023 08:24           788092168
+mainnet-00753-9bcde975.era                         19-Apr-2023 08:24           785977479
+mainnet-00754-b82788ae.era                         19-Apr-2023 08:24           760093020
+mainnet-00755-717aa839.era                         19-Apr-2023 08:25           771476233
+mainnet-00756-0a25493c.era                         19-Apr-2023 08:25           783171376
+mainnet-00757-b67cf3b9.era                         19-Apr-2023 08:25           803741519
+mainnet-00758-a7552bde.era                         19-Apr-2023 08:26           786381467
+mainnet-00759-b17d8602.era                         23-Oct-2024 08:53           767495598
+mainnet-00760-346c731f.era                         08-May-2023 08:15           811732314
+mainnet-00761-3a46750f.era                         08-May-2023 08:15           769654991
+mainnet-00762-a9f3023b.era                         08-May-2023 08:15           799164656
+mainnet-00763-e5b74b59.era                         08-May-2023 08:15           819312199
+mainnet-00764-72c1ffea.era                         08-May-2023 08:15           785236015
+mainnet-00765-a619d810.era                         08-May-2023 08:16           769443931
+mainnet-00766-5eff964b.era                         08-May-2023 08:16           699339779
+mainnet-00767-d7e72c09.era                         08-May-2023 08:16           718356544
+mainnet-00768-62b1606b.era                         08-May-2023 08:16           685941561
+mainnet-00769-f5bd9239.era                         08-May-2023 08:16           741102370
+mainnet-00770-a8170629.era                         08-May-2023 08:16           732232248
+mainnet-00771-55405ee2.era                         08-May-2023 08:16           766719961
+mainnet-00772-ce838529.era                         08-May-2023 08:16           696550803
+mainnet-00773-d12bf8fe.era                         08-May-2023 08:16           638004868
+mainnet-00774-1c623f9f.era                         08-May-2023 08:17           701550521
+mainnet-00775-053da11e.era                         08-May-2023 08:17           637850206
+mainnet-00776-b15a45f0.era                         08-May-2023 08:17           711435225
+mainnet-00777-0a7de38a.era                         08-May-2023 08:17           696817462
+mainnet-00778-9429b8ba.era                         08-May-2023 08:17           640102478
+mainnet-00779-7db8d45b.era                         23-Oct-2024 08:53           660963999
+mainnet-00780-bb546fec.era                         23-Oct-2024 08:53           603726762
+mainnet-00781-6e8fa554.era                         23-Oct-2024 09:53           636061107
+mainnet-00782-886fa978.era                         26-Jun-2023 07:54           698085639
+mainnet-00783-0b819fcd.era                         22-May-2023 04:08           704508612
+mainnet-00784-11892522.era                         22-May-2023 04:08           747974111
+mainnet-00785-c976cc65.era                         22-May-2023 04:08           742100420
+mainnet-00786-2d6a0e03.era                         22-May-2023 04:08           716484326
+mainnet-00787-1b6c901d.era                         22-May-2023 04:08           733218903
+mainnet-00788-b468a0f5.era                         22-May-2023 04:08           790372282
+mainnet-00789-23095abc.era                         22-May-2023 04:08           791636791
+mainnet-00790-a0a71e45.era                         22-May-2023 04:08           739323435
+mainnet-00791-d5d807ad.era                         12-Jun-2023 04:21           736458814
+mainnet-00792-80e46420.era                         12-Jun-2023 04:21           741688861
+mainnet-00793-71fb8913.era                         12-Jun-2023 04:21           734708420
+mainnet-00794-d84d734d.era                         12-Jun-2023 04:21           727843645
+mainnet-00795-d2b2cac5.era                         12-Jun-2023 04:21           788850468
+mainnet-00796-2e644178.era                         12-Jun-2023 04:22           831325414
+mainnet-00797-61418b9a.era                         12-Jun-2023 04:22           826661366
+mainnet-00798-3a33e200.era                         12-Jun-2023 04:22           853766445
+mainnet-00799-4a58ec2a.era                         12-Jun-2023 04:22           787522763
+mainnet-00800-c6e8340a.era                         12-Jun-2023 04:22           815304803
+mainnet-00801-687f5501.era                         12-Jun-2023 04:22           808641205
+mainnet-00802-34f0c2a1.era                         12-Jun-2023 04:22           732331389
+mainnet-00803-358c03f1.era                         12-Jun-2023 04:22           792362847
+mainnet-00804-5b8ccf94.era                         12-Jun-2023 04:22           797548028
+mainnet-00805-eb7f02fa.era                         12-Jun-2023 04:22           843160589
+mainnet-00806-47ac87d9.era                         12-Jun-2023 04:22           827918117
+mainnet-00807-a4160018.era                         12-Jun-2023 04:22           776330252
+mainnet-00808-077bbbae.era                         26-Jun-2023 07:54           787782664
+mainnet-00809-fc590b46.era                         19-Jun-2023 07:25           774657962
+mainnet-00810-9fad0ce8.era                         19-Jun-2023 07:25           806249847
+mainnet-00811-d96da8b7.era                         19-Jun-2023 07:25           817295735
+mainnet-00812-f681f446.era                         19-Jun-2023 07:25           851551213
+mainnet-00813-a9f60881.era                         19-Jun-2023 07:25           847493844
+mainnet-00814-ee562d37.era                         19-Jun-2023 07:26           867699331
+mainnet-00815-b3c51faa.era                         26-Jun-2023 07:54           801853524
+mainnet-00816-c14b6750.era                         26-Jun-2023 07:54           826454169
+mainnet-00817-e24e816a.era                         26-Jun-2023 07:54           720958246
+mainnet-00818-ccbb3d6a.era                         26-Jun-2023 07:54           786693207
+mainnet-00819-ff60322a.era                         26-Jun-2023 07:54           843663424
+mainnet-00820-30a75a75.era                         26-Jun-2023 07:54           846784334
+mainnet-00821-0efa1511.era                         03-Jul-2023 06:02           834993174
+mainnet-00822-a21cddaf.era                         03-Jul-2023 06:02           794724029
+mainnet-00823-305cdcc3.era                         03-Jul-2023 06:02           791244731
+mainnet-00824-a45f43f3.era                         03-Jul-2023 06:03           830374545
+mainnet-00825-e20a0f4b.era                         24-Jul-2023 06:52           870819813
+mainnet-00826-f1765519.era                         24-Jul-2023 06:52           889904073
+mainnet-00827-dcef9fdb.era                         24-Jul-2023 06:53           849788323
+mainnet-00828-a511170d.era                         24-Jul-2023 06:53           847219888
+mainnet-00829-b32a05eb.era                         24-Jul-2023 06:53           800364820
+mainnet-00830-1855aad1.era                         24-Jul-2023 06:53           823425048
+mainnet-00831-c898b382.era                         24-Jul-2023 06:53           796463498
+mainnet-00832-59d05cb1.era                         24-Jul-2023 06:53           609795206
+mainnet-00833-6d17cb6a.era                         24-Jul-2023 06:53           743365995
+mainnet-00834-38769a0b.era                         24-Jul-2023 06:54           833196365
+mainnet-00835-0f60db4d.era                         24-Jul-2023 06:54           817385230
+mainnet-00836-5d08ee64.era                         24-Jul-2023 06:54           858996938
+mainnet-00837-12cdd8fb.era                         24-Jul-2023 06:54           842887915
+mainnet-00838-cb7a0ebe.era                         24-Jul-2023 06:54           861567126
+mainnet-00839-a1090041.era                         24-Jul-2023 06:54           886454606
+mainnet-00840-6c22994f.era                         24-Jul-2023 06:54           894801269
+mainnet-00841-5da55cc8.era                         24-Jul-2023 06:55           874944599
+mainnet-00842-6b3ce9b1.era                         24-Jul-2023 06:55           869624758
+mainnet-00843-34484521.era                         24-Jul-2023 06:55           878901464
+mainnet-00844-df554ead.era                         24-Jul-2023 06:55           888929443
+mainnet-00845-3cf3fd9c.era                         24-Jul-2023 06:55           865263984
+mainnet-00846-f15d5d20.era                         28-Aug-2023 05:54           800513291
+mainnet-00847-1750bf54.era                         28-Aug-2023 05:55           863504338
+mainnet-00848-e05afc8d.era                         28-Aug-2023 05:55           865939528
+mainnet-00849-ea0365ee.era                         28-Aug-2023 05:55           936527182
+mainnet-00850-8edb5d38.era                         28-Aug-2023 05:55           961064417
+mainnet-00851-9c79c091.era                         28-Aug-2023 05:55           960493868
+mainnet-00852-bc820b3a.era                         28-Aug-2023 05:56           920878392
+mainnet-00853-41123418.era                         28-Aug-2023 05:56           874193728
+mainnet-00854-95c53704.era                         28-Aug-2023 05:56          1013047460
+mainnet-00855-9ba9bed7.era                         28-Aug-2023 05:56          1002801327
+mainnet-00856-36db113f.era                         28-Aug-2023 05:56           930772310
+mainnet-00857-81495dc4.era                         28-Aug-2023 05:57           928767906
+mainnet-00858-61cad6c7.era                         28-Aug-2023 05:57          1024750253
+mainnet-00859-71624f28.era                         28-Aug-2023 05:57           948010475
+mainnet-00860-c23f7553.era                         28-Aug-2023 05:57           893907661
+mainnet-00861-071d021f.era                         28-Aug-2023 05:57          1032824345
+mainnet-00862-b91d3e3a.era                         28-Aug-2023 05:58          1027170593
+mainnet-00863-eb429e36.era                         28-Aug-2023 05:58          1016463348
+mainnet-00864-dc3b08a7.era                         28-Aug-2023 05:58           998284761
+mainnet-00865-c813618a.era                         28-Aug-2023 05:58           925250701
+mainnet-00866-25d91f5b.era                         28-Aug-2023 05:58           925584230
+mainnet-00867-252a551c.era                         28-Aug-2023 05:59          1004699688
+mainnet-00868-f1e84f1d.era                         28-Aug-2023 05:59          1113213581
+mainnet-00869-3f1d459f.era                         28-Aug-2023 05:59           988767048
+mainnet-00870-f2f2246b.era                         28-Aug-2023 05:59           992239416
+mainnet-00871-95e94b4f.era                         28-Aug-2023 06:00           973954184
+mainnet-00872-c4a496bb.era                         28-Aug-2023 06:00           989147665
+mainnet-00873-bd6c439c.era                         28-Aug-2023 06:00           993197217
+mainnet-00874-af19b48f.era                         28-Aug-2023 06:00           947911538
+mainnet-00875-0304307b.era                         28-Aug-2023 06:01           986503543
+mainnet-00876-03608796.era                         28-Aug-2023 06:01           977040266
+mainnet-00877-ce9dc617.era                         28-Aug-2023 06:01          1008605054
+mainnet-00878-2464e02b.era                         28-Aug-2023 06:01           948756741
+mainnet-00879-749540de.era                         04-Sep-2023 05:20           987031605
+mainnet-00880-b8a32060.era                         04-Sep-2023 05:21           975224999
+mainnet-00881-9a729fe9.era                         04-Sep-2023 05:21           960216853
+mainnet-00882-e883a202.era                         04-Sep-2023 05:21           998614925
+mainnet-00883-1902db49.era                         04-Sep-2023 05:21           931229313
+mainnet-00884-76aca2de.era                         04-Sep-2023 05:21           929360587
+mainnet-00885-707e06be.era                         11-Sep-2023 06:18           969228491
+mainnet-00886-c13b6b5d.era                         11-Sep-2023 06:18           939747038
+mainnet-00887-858429f8.era                         11-Sep-2023 06:18          1012058655
+mainnet-00888-1f9352e9.era                         11-Sep-2023 06:19           957342620
+mainnet-00889-49737ed7.era                         11-Sep-2023 06:19           980547424
+mainnet-00890-3916ff3c.era                         11-Sep-2023 06:19           991893360
+mainnet-00891-67728d98.era                         11-Sep-2023 06:19          1038889719
+mainnet-00892-b232c174.era                         18-Sep-2023 03:34          1086005596
+mainnet-00893-f90a505f.era                         18-Sep-2023 03:34          1082256932
+mainnet-00894-eabfbb06.era                         18-Sep-2023 03:34          1088774632
+mainnet-00895-79bf766b.era                         18-Sep-2023 03:34          1096573668
+mainnet-00896-173310df.era                         18-Sep-2023 03:35          1068353154
+mainnet-00897-5e9be9f3.era                         18-Sep-2023 03:35          1042973196
+mainnet-00898-2553521a.era                         25-Sep-2023 10:56          1067119795
+mainnet-00899-1c8b3d15.era                         25-Sep-2023 10:56          1089136275
+mainnet-00900-00d57a1e.era                         25-Sep-2023 10:56          1058179549
+mainnet-00901-79958936.era                         25-Sep-2023 10:57          1060800841
+mainnet-00902-ffaab08b.era                         25-Sep-2023 10:57          1014455552
+mainnet-00903-8f050474.era                         25-Sep-2023 10:57           994441909
+mainnet-00904-f604b6ff.era                         02-Oct-2023 01:52          1070009820
+mainnet-00905-c52945c7.era                         02-Oct-2023 01:53          1061634407
+mainnet-00906-061e9667.era                         02-Oct-2023 01:53          1117623482
+mainnet-00907-f9b9dd13.era                         02-Oct-2023 01:53          1108227037
+mainnet-00908-6f17480f.era                         02-Oct-2023 01:54          1083971925
+mainnet-00909-fc28b7d0.era                         02-Oct-2023 01:54          1104203793
+mainnet-00910-9c549b9e.era                         09-Oct-2023 07:35          1072357272
+mainnet-00911-0088390c.era                         09-Oct-2023 07:35          1017560114
+mainnet-00912-1e0ddb7e.era                         09-Oct-2023 07:35          1034319951
+mainnet-00913-82c83bad.era                         09-Oct-2023 07:36          1022458198
+mainnet-00914-fa6f3f38.era                         09-Oct-2023 07:36          1063728193
+mainnet-00915-48eea553.era                         09-Oct-2023 07:36          1051356032
+mainnet-00916-86c78c5b.era                         16-Oct-2023 06:29          1063966623
+mainnet-00917-2e0a1b0c.era                         16-Oct-2023 06:29          1069504396
+mainnet-00918-7b79051b.era                         16-Oct-2023 06:29          1066731226
+mainnet-00919-99b2052e.era                         16-Oct-2023 06:29           960708423
+mainnet-00920-43591dfc.era                         16-Oct-2023 06:30           947801041
+mainnet-00921-7a3c340e.era                         16-Oct-2023 06:30           933190266
+mainnet-00922-7377475a.era                         23-Oct-2023 08:24           981113494
+mainnet-00923-69445c1a.era                         23-Oct-2023 08:24           992049368
+mainnet-00924-cd9bfdda.era                         23-Oct-2023 08:24           989082425
+mainnet-00925-67f73440.era                         23-Oct-2023 08:24           958030180
+mainnet-00926-8d621baa.era                         23-Oct-2023 08:24           945233290
+mainnet-00927-9dda355f.era                         23-Oct-2023 08:24           974668748
+mainnet-00928-2d6ca1ee.era                         30-Oct-2023 02:05          1037368243
+mainnet-00929-db3e0a27.era                         30-Oct-2023 02:05          1034099267
+mainnet-00930-cfe42bfa.era                         30-Oct-2023 02:05          1012153045
+mainnet-00931-29738dd7.era                         30-Oct-2023 02:06           934396133
+mainnet-00932-ea471bbf.era                         30-Oct-2023 02:06           973080485
+mainnet-00933-7c28d556.era                         30-Oct-2023 02:06           986514501
+mainnet-00934-f54a6eb5.era                         06-Nov-2023 10:58           999590868
+mainnet-00935-88c8a776.era                         06-Nov-2023 10:58           962033821
+mainnet-00936-a31a161e.era                         06-Nov-2023 10:58          1022777754
+mainnet-00937-cf684168.era                         06-Nov-2023 10:58           999231170
+mainnet-00938-1c7dff58.era                         06-Nov-2023 10:58          1008648251
+mainnet-00939-17bc73f0.era                         06-Nov-2023 10:59          1062577401
+mainnet-00940-737e1682.era                         06-Nov-2023 10:59          1077754675
+mainnet-00941-491476a9.era                         13-Nov-2023 04:38          1038034285
+mainnet-00942-07a53155.era                         13-Nov-2023 04:38          1091532410
+mainnet-00943-48878dcc.era                         13-Nov-2023 04:38          1094341742
+mainnet-00944-9a036eae.era                         13-Nov-2023 04:38          1032689234
+mainnet-00945-5d2237ec.era                         13-Nov-2023 04:39           964070069
+mainnet-00946-c7db75bc.era                         13-Nov-2023 04:39          1012973886
+mainnet-00947-a6a6e278.era                         20-Nov-2023 02:21           899045300
+mainnet-00948-03585a93.era                         20-Nov-2023 02:21           967830037
+mainnet-00949-1e333c28.era                         20-Nov-2023 02:21          1015957385
+mainnet-00950-b3ae9cd0.era                         20-Nov-2023 02:21          1051942437
+mainnet-00951-9765163a.era                         20-Nov-2023 02:22          1095175148
+mainnet-00952-72cece82.era                         20-Nov-2023 02:22          1156790471
+mainnet-00953-0670fc06.era                         27-Nov-2023 05:26          1069401319
+mainnet-00954-505fa2ad.era                         27-Nov-2023 05:26          1039394824
+mainnet-00955-0d1d3fcb.era                         27-Nov-2023 05:26          1002735038
+mainnet-00956-9be3f767.era                         27-Nov-2023 05:27          1060109728
+mainnet-00957-338ee481.era                         27-Nov-2023 05:27          1116358753
+mainnet-00958-7325f013.era                         27-Nov-2023 05:27          1319322371
+mainnet-00959-1d52ab18.era                         04-Dec-2023 00:50          1156181406
+mainnet-00960-71d67d25.era                         04-Dec-2023 00:50           985678190
+mainnet-00961-e17aec30.era                         04-Dec-2023 00:50           939138939
+mainnet-00962-822fd3d5.era                         04-Dec-2023 00:51           920138170
+mainnet-00963-c2681306.era                         04-Dec-2023 00:51           921805727
+mainnet-00964-b3d63aab.era                         04-Dec-2023 00:51          1004504014
+mainnet-00965-211032a7.era                         11-Dec-2023 06:41          1000881013
+mainnet-00966-44a0d993.era                         11-Dec-2023 06:41          1059634868
+mainnet-00967-5cc56804.era                         11-Dec-2023 06:42          1072877088
+mainnet-00968-126d53bd.era                         11-Dec-2023 06:42          1011939882
+mainnet-00969-28edd9b3.era                         11-Dec-2023 06:42          1021787209
+mainnet-00970-e10ce061.era                         11-Dec-2023 06:42          1118532402
+mainnet-00971-ce26da07.era                         18-Dec-2023 00:23          1084072118
+mainnet-00972-0591765a.era                         18-Dec-2023 00:24          1154294323
+mainnet-00973-dd3c8ad9.era                         18-Dec-2023 00:24          1210776835
+mainnet-00974-b444ff6d.era                         18-Dec-2023 00:24          1093759377
+mainnet-00975-cbe02fd8.era                         18-Dec-2023 00:24          1261295440
+mainnet-00976-8c7535a1.era                         18-Dec-2023 00:25          1449783125
+mainnet-00977-f4e2ae07.era                         25-Dec-2023 05:37          1098889569
+mainnet-00978-07d55b2d.era                         25-Dec-2023 05:37          1230591248
+mainnet-00979-cd58b1ff.era                         25-Dec-2023 05:37          1119476799
+mainnet-00980-e5b66b9b.era                         25-Dec-2023 05:37          1123494252
+mainnet-00981-a46114eb.era                         25-Dec-2023 05:38          1096917394
+mainnet-00982-c1ac696b.era                         25-Dec-2023 05:38          1141784163
+mainnet-00983-49be149e.era                         25-Dec-2023 05:38          1051124967
+mainnet-00984-ff09ec7c.era                         01-Jan-2024 05:37          1001802855
+mainnet-00985-9b837cb4.era                         01-Jan-2024 05:37          1170029356
+mainnet-00986-8476c96b.era                         01-Jan-2024 05:38          1188343960
+mainnet-00987-942cbb5e.era                         01-Jan-2024 05:38          1071867821
+mainnet-00988-3e89d0df.era                         01-Jan-2024 05:38          1121008991
+mainnet-00989-98aed740.era                         01-Jan-2024 05:38          1149541746
+mainnet-00990-1b287ed5.era                         08-Jan-2024 09:58          1212922019
+mainnet-00991-b38b7ba6.era                         08-Jan-2024 09:58          1240474182
+mainnet-00992-67d3603a.era                         08-Jan-2024 09:58          1133538391
+mainnet-00993-a640b201.era                         08-Jan-2024 09:58          1158266109
+mainnet-00994-583f5d58.era                         08-Jan-2024 09:59          1186421664
+mainnet-00995-d773e72c.era                         08-Jan-2024 09:59          1274344444
+mainnet-00996-e2c4f84b.era                         15-Jan-2024 11:03          1173040509
+mainnet-00997-524f39b6.era                         15-Jan-2024 11:04          1132459955
+mainnet-00998-139c3883.era                         15-Jan-2024 11:04          1103865541
+mainnet-00999-e3244e9f.era                         15-Jan-2024 11:04          1104175515
+mainnet-01000-910e16c9.era                         15-Jan-2024 11:05          1147011072
+mainnet-01001-8299ddd3.era                         15-Jan-2024 11:05          1107449257
+mainnet-01002-59aa0262.era                         22-Jan-2024 08:51          1115920291
+mainnet-01003-b2c4b5dd.era                         22-Jan-2024 08:51          1116754609
+mainnet-01004-5a2289c2.era                         22-Jan-2024 08:52          1018162781
+mainnet-01005-f8b3b4ac.era                         22-Jan-2024 08:52           953066997
+mainnet-01006-8a6c02e1.era                         22-Jan-2024 08:52          1038274490
+mainnet-01007-0c1f2eed.era                         22-Jan-2024 08:52          1086183652
+mainnet-01008-eade9397.era                         29-Jan-2024 08:34          1076999731
+mainnet-01009-0316802b.era                         29-Jan-2024 08:34          1119043127
+mainnet-01010-0ea1ad78.era                         29-Jan-2024 08:34          1146914051
+mainnet-01011-aafe82a9.era                         29-Jan-2024 08:34          1064045432
+mainnet-01012-1c42bdef.era                         29-Jan-2024 08:35          1092291197
+mainnet-01013-9ff54825.era                         29-Jan-2024 08:35          1078983389
+mainnet-01014-e6c61a31.era                         29-Jan-2024 08:35          1142431766
+mainnet-01015-bfc01f9e.era                         05-Feb-2024 03:18          1153173765
+mainnet-01016-fc3f4ab7.era                         05-Feb-2024 03:18          1189501622
+mainnet-01017-8cd8ff7d.era                         05-Feb-2024 03:18          1123925962
+mainnet-01018-8c39c89c.era                         05-Feb-2024 03:18          1028958177
+mainnet-01019-ca20443e.era                         05-Feb-2024 03:19          1062677717
+mainnet-01020-50403439.era                         05-Feb-2024 03:19          1067064150
+mainnet-01021-d749f8c9.era                         12-Feb-2024 05:50          1165956249
+mainnet-01022-acdc946a.era                         12-Feb-2024 05:50          1054510834
+mainnet-01023-26670576.era                         12-Feb-2024 05:50          1003807420
+mainnet-01024-13bd2903.era                         12-Feb-2024 05:51           889313434
+mainnet-01025-19f6a400.era                         12-Feb-2024 05:51           929681985
+mainnet-01026-58b77a49.era                         12-Feb-2024 05:51          1019166538
+mainnet-01027-96b7bd78.era                         19-Feb-2024 08:58          1063375691
+mainnet-01028-50b3c58d.era                         19-Feb-2024 08:59          1233880239
+mainnet-01029-36c00fed.era                         19-Feb-2024 08:59          1248655386
+mainnet-01030-a8554c62.era                         19-Feb-2024 08:59          1102776535
+mainnet-01031-dbdc62f7.era                         19-Feb-2024 09:00          1174094509
+mainnet-01032-c8f1bcb1.era                         19-Feb-2024 09:00          1222359858
+mainnet-01033-63bf38fa.era                         26-Feb-2024 03:47          1189562472
+mainnet-01034-fbe9d06e.era                         26-Feb-2024 03:48          1257320716
+mainnet-01035-e94d1a72.era                         26-Feb-2024 03:48          1379355048
+mainnet-01036-118b7186.era                         26-Feb-2024 03:48          1192744870
+mainnet-01037-b28ab8ba.era                         26-Feb-2024 03:48          1263436212
+mainnet-01038-a78b7ddf.era                         26-Feb-2024 03:49          1359703022
+mainnet-01039-fde0699a.era                         04-Mar-2024 00:07          1370184165
+mainnet-01040-7d19530c.era                         04-Mar-2024 00:07          1225905386
+mainnet-01041-398d3b46.era                         04-Mar-2024 00:07          1337371389
+mainnet-01042-6364282f.era                         04-Mar-2024 00:07          1351185031
+mainnet-01043-a46193fd.era                         04-Mar-2024 00:07          1235985744
+mainnet-01044-f5ec475d.era                         04-Mar-2024 00:08          1248444632
+mainnet-01045-77d3f886.era                         11-Mar-2024 02:50          1177619028
+mainnet-01046-b3802cd3.era                         11-Mar-2024 02:51          1133430936
+mainnet-01047-0064fb20.era                         11-Mar-2024 02:51          1164750331
+mainnet-01048-808a4bf2.era                         11-Mar-2024 02:51          1088832842
+mainnet-01049-cc6db2c3.era                         11-Mar-2024 02:51          1057909023
+mainnet-01050-d1e77acc.era                         11-Mar-2024 02:51          1103115299
+mainnet-01051-7e12d77c.era                         18-Mar-2024 11:44          1075508844
+mainnet-01052-66aad1b6.era                         18-Mar-2024 11:44           996908733
+mainnet-01053-c4ee701c.era                         18-Mar-2024 11:45          1133066463
+mainnet-01054-b0075cb6.era                         18-Mar-2024 11:45           925022806
+mainnet-01055-ab2e55bf.era                         18-Mar-2024 11:45           736320894
+mainnet-01056-44385cac.era                         18-Mar-2024 11:45           774599828
+mainnet-01057-ac140e39.era                         18-Mar-2024 11:46           791355015
+mainnet-01058-dd2d45b7.era                         25-Mar-2024 08:06           754659888
+mainnet-01059-b2739649.era                         25-Mar-2024 08:07           806029022
+mainnet-01060-b72f0847.era                         25-Mar-2024 08:07           777128548
+mainnet-01061-36d851e1.era                         25-Mar-2024 08:07           806056001
+mainnet-01062-1ee83eae.era                         25-Mar-2024 08:07           824723494
+mainnet-01063-f6982971.era                         25-Mar-2024 08:07           833108880
+mainnet-01064-cba7f482.era                         01-Apr-2024 07:36           752937909
+mainnet-01065-1bfae1c4.era                         01-Apr-2024 07:36           709545628
+mainnet-01066-67f8cc77.era                         01-Apr-2024 07:37           700367434
+mainnet-01067-8ad9d021.era                         01-Apr-2024 07:37           718495314
+mainnet-01068-aea34cd4.era                         01-Apr-2024 07:37           722445322
+mainnet-01069-2a00f16a.era                         01-Apr-2024 07:37           696927002
+mainnet-01070-7616e3e2.era                         08-Apr-2024 03:51           661305125
+mainnet-01071-5304c6f7.era                         08-Apr-2024 03:51           681721953
+mainnet-01072-cb2a78df.era                         08-Apr-2024 03:52           660376625
+mainnet-01073-116cb348.era                         08-Apr-2024 03:52           667976000
+mainnet-01074-7c32320f.era                         08-Apr-2024 03:52           669149664
+mainnet-01075-9388a8c8.era                         08-Apr-2024 03:52           682192221
+mainnet-01076-7feb4130.era                         15-Apr-2024 06:45           676327136
+mainnet-01077-653370f5.era                         15-Apr-2024 06:45           665052760
+mainnet-01078-f5d64a2d.era                         15-Apr-2024 06:46           685971594
+mainnet-01079-92178e12.era                         15-Apr-2024 06:46           689813342
+mainnet-01080-3840e823.era                         15-Apr-2024 06:46           671252009
+mainnet-01081-fe751ced.era                         15-Apr-2024 06:46           672663854
+mainnet-01082-d14bce2a.era                         22-Apr-2024 08:17           684595860
+mainnet-01083-36c7d5b3.era                         22-Apr-2024 08:18           677460672
+mainnet-01084-18abd21f.era                         22-Apr-2024 08:18           656516511
+mainnet-01085-451ccf54.era                         22-Apr-2024 08:18           649935189
+mainnet-01086-902886db.era                         22-Apr-2024 08:18           648818639
+mainnet-01087-0d0e1330.era                         22-Apr-2024 08:19           638849696
+mainnet-01088-533d2a23.era                         29-Apr-2024 07:48           653432323
+mainnet-01089-93a1b601.era                         29-Apr-2024 07:49           639223723
+mainnet-01090-c4a3317c.era                         29-Apr-2024 07:49           643593872
+mainnet-01091-8e5210eb.era                         29-Apr-2024 07:49           659733631
+mainnet-01092-de317889.era                         29-Apr-2024 07:49           658703258
+mainnet-01093-b66a32cc.era                         29-Apr-2024 07:50           635378988
+mainnet-01094-1c6cb5a6.era                         29-Apr-2024 07:50           632085657
+mainnet-01095-0efdad09.era                         06-May-2024 08:44           601696789
+mainnet-01096-51bf07d3.era                         06-May-2024 08:44           667843969
+mainnet-01097-800c6f04.era                         06-May-2024 08:45           697226159
+mainnet-01098-52038cdd.era                         06-May-2024 08:45           637486045
+mainnet-01099-d2f087ab.era                         06-May-2024 08:45           659658736
+mainnet-01100-bd968114.era                         06-May-2024 08:45           641014331
+mainnet-01101-f2c1ae87.era                         13-May-2024 04:19           657663713
+mainnet-01102-e0f64801.era                         13-May-2024 04:20           669176240
+mainnet-01103-0ffe706a.era                         13-May-2024 04:20           650879139
+mainnet-01104-66ed6c0e.era                         13-May-2024 04:20           663051687
+mainnet-01105-ca7a5d0d.era                         13-May-2024 04:20           686841543
+mainnet-01106-a64ae290.era                         13-May-2024 04:21           619378837
+mainnet-01107-919c04f5.era                         20-May-2024 05:41           634477029
+mainnet-01108-db5b9be9.era                         20-May-2024 05:41           670717322
+mainnet-01109-28a2b519.era                         20-May-2024 05:41           661703006
+mainnet-01110-4ef3e14d.era                         20-May-2024 05:41           646417627
+mainnet-01111-a6faa027.era                         20-May-2024 05:42           643799941
+mainnet-01112-7045f707.era                         20-May-2024 05:42           634822727
+mainnet-01113-f38e96e1.era                         27-May-2024 04:56           629626563
+mainnet-01114-745ba36a.era                         27-May-2024 04:56           649652452
+mainnet-01115-1e8137f1.era                         27-May-2024 04:56           626563621
+mainnet-01116-40c11f73.era                         27-May-2024 04:57           646270036
+mainnet-01117-86a1e9f3.era                         27-May-2024 04:57           636281800
+mainnet-01118-8446391b.era                         27-May-2024 04:57           616328870
+mainnet-01119-9b6036f6.era                         03-Jun-2024 01:13           614347594
+mainnet-01120-bc33b830.era                         03-Jun-2024 01:14           516340226
+mainnet-01121-1b0f199a.era                         03-Jun-2024 01:14           523538313
+mainnet-01122-7365c3e8.era                         03-Jun-2024 01:14           527781479
+mainnet-01123-d026cb41.era                         03-Jun-2024 01:14           531013189
+mainnet-01124-6d94e6fd.era                         03-Jun-2024 01:14           525258226
+mainnet-01125-c344b70a.era                         10-Jun-2024 09:17           511847554
+mainnet-01126-630e4d70.era                         10-Jun-2024 09:17           523258708
+mainnet-01127-503d3141.era                         10-Jun-2024 09:18           520110861
+mainnet-01128-94aa3597.era                         10-Jun-2024 09:18           514680738
+mainnet-01129-8d1137bc.era                         10-Jun-2024 09:18           533373684
+mainnet-01130-70bf6a5c.era                         10-Jun-2024 09:18           540196002
+mainnet-01131-8aa44d86.era                         10-Jun-2024 09:18           527483662
+mainnet-01132-29970b3e.era                         17-Jun-2024 02:47           523725133
+mainnet-01133-7cbcb319.era                         17-Jun-2024 02:47           541541070
+mainnet-01134-7db2e27a.era                         17-Jun-2024 02:47           562506272
+mainnet-01135-84cd50a5.era                         17-Jun-2024 02:47           552204307
+mainnet-01136-569f04e1.era                         17-Jun-2024 02:48           542145875
+mainnet-01137-4bdc6ceb.era                         24-Jun-2024 09:11           530103758
+mainnet-01138-59d38f35.era                         24-Jun-2024 09:11           533616930
+mainnet-01139-97e78606.era                         24-Jun-2024 09:11           541950632
+mainnet-01140-f70d4869.era                         24-Jun-2024 09:12           545440235
+mainnet-01141-3a675f56.era                         24-Jun-2024 09:12           613182585
+mainnet-01142-fe5edaaa.era                         24-Jun-2024 09:12           545238653
+mainnet-01143-592c3728.era                         24-Jun-2024 09:12           519071817
+mainnet-01144-4c5f788a.era                         01-Jul-2024 00:39           544451793
+mainnet-01145-2756d4d2.era                         01-Jul-2024 00:39           537632071
+mainnet-01146-6959479b.era                         01-Jul-2024 00:39           546297936
+mainnet-01147-34a0baee.era                         01-Jul-2024 00:39           536273900
+mainnet-01148-551205ba.era                         01-Jul-2024 00:39           542295574
+mainnet-01149-90fb3502.era                         01-Jul-2024 00:40           514738750
+mainnet-01150-00aa0b36.era                         08-Jul-2024 03:46           521268023
+mainnet-01151-3aa871c3.era                         08-Jul-2024 03:46           525584362
+mainnet-01152-8e9d366a.era                         08-Jul-2024 03:46           521008287
+mainnet-01153-7665603b.era                         08-Jul-2024 03:46           544170610
+mainnet-01154-caeecd33.era                         08-Jul-2024 03:47           543122360
+mainnet-01155-23db87d4.era                         08-Jul-2024 03:47           512978887
+mainnet-01156-4786c4bc.era                         15-Jul-2024 03:33           525873269
+mainnet-01157-b4c73df5.era                         15-Jul-2024 03:33           524868204
+mainnet-01158-d19719f0.era                         15-Jul-2024 03:33           516751860
+mainnet-01159-8831acbd.era                         15-Jul-2024 03:33           524962479
+mainnet-01160-f6be8d9e.era                         15-Jul-2024 03:33           517287594
+mainnet-01161-bfe066df.era                         15-Jul-2024 03:34           515757018
+mainnet-01162-f387c372.era                         22-Jul-2024 00:09           538835657
+mainnet-01163-8433aafb.era                         22-Jul-2024 00:09           530313058
+mainnet-01164-9d014848.era                         22-Jul-2024 00:09           532985237
+mainnet-01165-9fbc60d2.era                         22-Jul-2024 00:09           533554844
+mainnet-01166-6e39e80c.era                         22-Jul-2024 00:10           525881605
+mainnet-01167-6610feb0.era                         22-Jul-2024 00:10           516969710
+mainnet-01168-d1cd2453.era                         29-Jul-2024 03:22           515827057
+mainnet-01169-d560f6b7.era                         29-Jul-2024 03:22           532758376
+mainnet-01170-e9018088.era                         29-Jul-2024 03:22           536228278
+mainnet-01171-a0885e7f.era                         29-Jul-2024 03:22           530304553
+mainnet-01172-dfdf959b.era                         29-Jul-2024 03:23           526140374
+mainnet-01173-4b4146f3.era                         29-Jul-2024 03:23           503233838
+mainnet-01174-c7cd4e84.era                         05-Aug-2024 05:27           503774924
+mainnet-01175-809f889c.era                         05-Aug-2024 05:28           522347812
+mainnet-01176-9de72b35.era                         05-Aug-2024 05:28           555634120
+mainnet-01177-c1897063.era                         05-Aug-2024 05:28           529343591
+mainnet-01178-49dbac0e.era                         05-Aug-2024 05:28           533915470
+mainnet-01179-0f0f90a4.era                         05-Aug-2024 05:28           534239576
+mainnet-01180-11a0ad82.era                         05-Aug-2024 05:29           512692069
+mainnet-01181-1b9fd83b.era                         12-Aug-2024 02:50           544589550
+mainnet-01182-9e3856d0.era                         12-Aug-2024 02:50           534654676
+mainnet-01183-595cb34b.era                         12-Aug-2024 02:50           520551561
+mainnet-01184-419044cb.era                         12-Aug-2024 02:50           520280614
+mainnet-01185-e6cfcca2.era                         12-Aug-2024 02:50           518934027
+mainnet-01186-c53bfdf9.era                         12-Aug-2024 02:51           496384503
+mainnet-01187-9bc14ac4.era                         19-Aug-2024 01:43           516388771
+mainnet-01188-a5ddb290.era                         19-Aug-2024 01:43           521884036
+mainnet-01189-3a26c876.era                         19-Aug-2024 01:43           540705955
+mainnet-01190-40f5ebfa.era                         19-Aug-2024 01:43           532503784
+mainnet-01191-61a46948.era                         19-Aug-2024 01:44           529324058
+mainnet-01192-941f360d.era                         19-Aug-2024 01:44           517036066
+mainnet-01193-413ffb33.era                         26-Aug-2024 05:30           551007340
+mainnet-01194-d3531f76.era                         26-Aug-2024 05:30           540890048
+mainnet-01195-47e5d536.era                         26-Aug-2024 05:30           525899104
+mainnet-01196-86d2ceb8.era                         26-Aug-2024 05:30           532120678
+mainnet-01197-93dfc40a.era                         26-Aug-2024 05:31           519303955
+mainnet-01198-7fa25a94.era                         26-Aug-2024 05:31           524389543
+mainnet-01199-5049e4ae.era                         02-Sep-2024 06:01           524249268
+mainnet-01200-4e63ffad.era                         02-Sep-2024 06:01           532869857
+mainnet-01201-d9ca43da.era                         02-Sep-2024 06:01           532060938
+mainnet-01202-5780d8ad.era                         02-Sep-2024 06:02           526098735
+mainnet-01203-9939c48f.era                         02-Sep-2024 06:02           524114507
+mainnet-01204-4621397b.era                         02-Sep-2024 06:02           500686660
+mainnet-01205-2550cacc.era                         09-Sep-2024 10:56           498991304
+mainnet-01206-343645e1.era                         09-Sep-2024 10:56           500950462
+mainnet-01207-4c36f600.era                         09-Sep-2024 10:56           515988706
+mainnet-01208-f34ed4d3.era                         09-Sep-2024 10:56           494922791
+mainnet-01209-22a39c65.era                         09-Sep-2024 10:57           514352398
+mainnet-01210-f8451c68.era                         09-Sep-2024 10:57           508333824
+mainnet-01211-1370f37f.era                         09-Sep-2024 10:57           506672973
+mainnet-01212-01268972.era                         16-Sep-2024 00:14           513177285
+mainnet-01213-011ff1c2.era                         16-Sep-2024 00:14           517325501
+mainnet-01214-6f717be4.era                         16-Sep-2024 00:14           513394492
+mainnet-01215-f7934ece.era                         16-Sep-2024 00:14           522925047
+mainnet-01216-1d0de988.era                         16-Sep-2024 00:15           521437045
+mainnet-01217-d1819216.era                         23-Sep-2024 09:04           516928102
+mainnet-01218-c4d201d4.era                         23-Sep-2024 09:04           526662767
+mainnet-01219-d82f4773.era                         23-Sep-2024 09:04           530528035
+mainnet-01220-c2eb4581.era                         23-Sep-2024 09:04           511189094
+mainnet-01221-f5353213.era                         23-Sep-2024 09:05           518598665
+mainnet-01222-2c5cc193.era                         23-Sep-2024 09:05           506732438
+mainnet-01223-8178f64b.era                         23-Sep-2024 09:05           509569001
+mainnet-01224-f30e85cd.era                         30-Sep-2024 11:37           504192678
+mainnet-01225-c9e90f34.era                         30-Sep-2024 11:37           501253711
+mainnet-01226-92be86f6.era                         30-Sep-2024 11:38           510124386
+mainnet-01227-497bcaf6.era                         30-Sep-2024 11:38           523961290
+mainnet-01228-f9381eb9.era                         30-Sep-2024 11:38           539758958
+mainnet-01229-a55063a3.era                         30-Sep-2024 11:38           539554804
+mainnet-01230-8d00c7e5.era                         07-Oct-2024 05:52           531327691
+mainnet-01231-58c274da.era                         07-Oct-2024 05:52           544205842
+mainnet-01232-f330b8d6.era                         07-Oct-2024 05:52           544514121
+mainnet-01233-53b80a54.era                         07-Oct-2024 05:52           543739427
+mainnet-01234-e97a19c9.era                         07-Oct-2024 05:53           537416447
+mainnet-01235-339204b8.era                         07-Oct-2024 05:53           510147622
+mainnet-01236-d5cfd238.era                         14-Oct-2024 11:39           512310651
+mainnet-01237-523a46c8.era                         14-Oct-2024 11:39           510561192
+mainnet-01238-63d410bd.era                         14-Oct-2024 11:39           507750978
+mainnet-01239-85077afb.era                         14-Oct-2024 11:40           514157553
+mainnet-01240-98ed1e81.era                         14-Oct-2024 11:40           513472871
+mainnet-01241-55252f8c.era                         14-Oct-2024 11:40           516565794
+mainnet-01242-dbecf326.era                         21-Oct-2024 04:43           512202461
+mainnet-01243-97e754b1.era                         21-Oct-2024 04:44           541157141
+mainnet-01244-d519e3a1.era                         21-Oct-2024 04:44           531181802
+mainnet-01245-8946bc4b.era                         21-Oct-2024 04:44           528295056
+mainnet-01246-952e7792.era                         21-Oct-2024 04:44           530989066
+mainnet-01247-6216c622.era                         21-Oct-2024 04:45           530583386
+mainnet-01248-faaeff0d.era                         23-Oct-2024 08:53           531889406
+mainnet-01249-23b3ce4f.era                         23-Oct-2024 09:15           540485342
+mainnet-01250-1df3dd00.era                         11-Nov-2024 02:42           527369240
+mainnet-01251-f7979d7c.era                         11-Nov-2024 02:42           519849366
+mainnet-01252-fd301068.era                         11-Nov-2024 02:42           539671179
+mainnet-01253-5c81a85e.era                         11-Nov-2024 02:42           520225292
+mainnet-01254-07b72fc2.era                         11-Nov-2024 02:42           489438274
+mainnet-01255-bc622b19.era                         11-Nov-2024 02:43           521904618
+mainnet-01256-ca7f996f.era                         11-Nov-2024 02:43           525840073
+mainnet-01257-caa30ab5.era                         11-Nov-2024 02:43           515452960
+mainnet-01258-f4f77fad.era                         11-Nov-2024 02:43           516313554
+mainnet-01259-a3365829.era                         11-Nov-2024 02:43           518067981
+mainnet-01260-e615f43c.era                         11-Nov-2024 02:44           531296276
+mainnet-01261-c092f651.era                         11-Nov-2024 02:44           549562097
+mainnet-01262-bcb8e013.era                         11-Nov-2024 02:44           533996323
+mainnet-01263-8274d1c5.era                         11-Nov-2024 02:44           547942816
+mainnet-01264-dcfa40c5.era                         11-Nov-2024 02:44           551316400
+mainnet-01265-e6064f87.era                         11-Nov-2024 02:45           543235949
+mainnet-01266-2f91212f.era                         11-Nov-2024 02:45           543702811
+mainnet-01267-e3ddc749.era                         18-Nov-2024 10:58           548089699
+mainnet-01268-ae094943.era                         18-Nov-2024 10:58           550180215
+mainnet-01269-af18ad45.era                         18-Nov-2024 10:58           545423537
+mainnet-01270-79fe06aa.era                         18-Nov-2024 10:59           543892466
+mainnet-01271-f43df910.era                         18-Nov-2024 10:59           545007035
+mainnet-01272-6cabb907.era                         18-Nov-2024 10:59           545999688
+mainnet-01273-53499c35.era                         25-Nov-2024 03:29           547941951
+mainnet-01274-42094f2a.era                         25-Nov-2024 03:29           545993070
+mainnet-01275-5dd68a9e.era                         25-Nov-2024 03:29           546352518
+mainnet-01276-c0474572.era                         25-Nov-2024 03:29           548203168
+mainnet-01277-18129ea3.era                         25-Nov-2024 03:29           561082149
+mainnet-01278-e9618417.era                         25-Nov-2024 03:30           554107010
+mainnet-01279-e84bfdac.era                         02-Dec-2024 04:37           557070417
+mainnet-01280-ccf8abdb.era                         02-Dec-2024 04:37           556983443
+mainnet-01281-e3ec741a.era                         02-Dec-2024 04:37           557638070
+mainnet-01282-316b4c12.era                         02-Dec-2024 04:37           566574873
+mainnet-01283-fc7fe135.era                         02-Dec-2024 04:37           554917172
+mainnet-01284-73c540a7.era                         02-Dec-2024 04:38           552828735
+mainnet-01285-274813ee.era                         23-Dec-2024 10:26           560002327
+mainnet-01286-e427d790.era                         23-Dec-2024 10:26           570673150
+mainnet-01287-8c566618.era                         23-Dec-2024 10:26           567255015
+mainnet-01288-970f9ef7.era                         23-Dec-2024 10:26           567912612
+mainnet-01289-68bbb574.era                         23-Dec-2024 10:27           567201986
+mainnet-01290-490e2c60.era                         23-Dec-2024 10:27           562055587
+mainnet-01291-724f5f50.era                         23-Dec-2024 10:27           565601793
+mainnet-01292-26ab701d.era                         23-Dec-2024 10:27           589837713
+mainnet-01293-9938cd29.era                         23-Dec-2024 10:28           562768281
+mainnet-01294-5009f134.era                         23-Dec-2024 10:28           568454475
+mainnet-01295-d8b9456e.era                         23-Dec-2024 10:28           557069963
+mainnet-01296-3d2e50fe.era                         23-Dec-2024 10:28           561916952
+mainnet-01297-4302ec9b.era                         23-Dec-2024 10:28           567810864
+mainnet-01298-65ac3ba4.era                         23-Dec-2024 10:29           566069376
+mainnet-01299-514a418b.era                         23-Dec-2024 10:29           584705352
+mainnet-01300-12b1cd58.era                         23-Dec-2024 10:29           568493215
+mainnet-01301-58ffae02.era                         23-Dec-2024 10:29           579609259
+mainnet-01302-ca8b3d23.era                         23-Dec-2024 10:30           567219105
+mainnet-01303-03d3c637.era                         23-Dec-2024 10:30           568036747
+mainnet-01304-597fb2d2.era                         30-Dec-2024 05:29           563733928
+mainnet-01305-e82f75eb.era                         30-Dec-2024 05:29           555917872
+mainnet-01306-e8862af0.era                         30-Dec-2024 05:29           543175566
+mainnet-01307-4ba10bd7.era                         30-Dec-2024 05:30           564966234
+mainnet-01308-41b2ae42.era                         30-Dec-2024 05:30           557444885
+mainnet-01309-b2fb856e.era                         30-Dec-2024 05:30           569689776
+mainnet-01310-717a92d9.era                         06-Jan-2025 04:29           575208992
+mainnet-01311-6d0f76d6.era                         06-Jan-2025 04:29           584106233
+mainnet-01312-0b3eef3d.era                         06-Jan-2025 04:29           480548102
+mainnet-01313-2e64295b.era                         06-Jan-2025 04:29           547496404
+mainnet-01314-8c677941.era                         06-Jan-2025 04:29           541972973
+mainnet-01315-e647d21d.era                         06-Jan-2025 04:30           537968123
+mainnet-01316-9d9bb53a.era                         13-Jan-2025 04:01           553204829
+mainnet-01317-92c1e9ab.era                         13-Jan-2025 04:01           555690541
+mainnet-01318-11ba37ba.era                         13-Jan-2025 04:01           552768393
+mainnet-01319-db8841f4.era                         13-Jan-2025 04:01           557195550
+mainnet-01320-59f1c8c0.era                         13-Jan-2025 04:02           550178409
+mainnet-01321-9d82e6dc.era                         13-Jan-2025 04:02           541631688
+mainnet-01322-6c292b86.era                         20-Jan-2025 07:02           561491048
+mainnet-01323-dbc11560.era                         20-Jan-2025 07:02           554842380
+mainnet-01324-c2221df7.era                         20-Jan-2025 07:02           567272046
+mainnet-01325-749bed39.era                         20-Jan-2025 07:02           581203487
+mainnet-01326-570a6902.era                         20-Jan-2025 07:03           569407995
+mainnet-01327-4ac78245.era                         20-Jan-2025 07:03           560130705
+mainnet-01328-362692bf.era                         27-Jan-2025 11:26           577840598
+mainnet-01329-a11e7e24.era                         27-Jan-2025 11:26           569096842
+mainnet-01330-32b046f2.era                         27-Jan-2025 11:27           571271651
+mainnet-01331-ab4b8cea.era                         27-Jan-2025 11:27           575520039
+mainnet-01332-0c621ccf.era                         27-Jan-2025 11:27           599223183
+mainnet-01333-201cd44e.era                         27-Jan-2025 11:28           599620747
+mainnet-01334-4a479a19.era                         27-Jan-2025 11:28           568687552
+mainnet-01335-381db7ff.era                         03-Feb-2025 11:38           589146849
+mainnet-01336-edbac0a0.era                         03-Feb-2025 11:38           559994615
+mainnet-01337-343a22ff.era                         03-Feb-2025 11:38           560642758
+mainnet-01338-7e85e340.era                         03-Feb-2025 11:39           560552238
+mainnet-01339-75d1c621.era                         03-Feb-2025 11:39           552870501
+mainnet-01340-429201f5.era                         03-Feb-2025 11:39           553881347
+mainnet-01341-91372206.era                         06-Apr-2025 12:52           584070656
+mainnet-01342-beed409a.era                         06-Apr-2025 12:53           613520760
+mainnet-01343-b6f6a159.era                         06-Apr-2025 12:53           596395049
+mainnet-01344-8f5487d6.era                         06-Apr-2025 12:53           603330358
+mainnet-01345-7cabd53f.era                         06-Apr-2025 12:53           575770089
+mainnet-01346-303a52b7.era                         06-Apr-2025 12:53           565215275
+mainnet-01347-3d3f2c2b.era                         06-Apr-2025 12:53           605011651
+mainnet-01348-f64fe31a.era                         06-Apr-2025 12:53           609676486
+mainnet-01349-6d74bea8.era                         06-Apr-2025 12:53           609390464
+mainnet-01350-62701c9b.era                         06-Apr-2025 12:53           620215784
+mainnet-01351-feaf38ed.era                         06-Apr-2025 12:54           599392384
+mainnet-01352-97b8f658.era                         06-Apr-2025 12:54           560595833
+mainnet-01353-4cd4ab3a.era                         06-Apr-2025 12:54           587156401
+mainnet-01354-797b839b.era                         06-Apr-2025 12:54           607490669
+mainnet-01355-7d84720c.era                         06-Apr-2025 12:54           583647685
+mainnet-01356-85bdb143.era                         06-Apr-2025 12:54           593565225
+mainnet-01357-8bf81664.era                         06-Apr-2025 12:54           635102447
+mainnet-01358-b6685f0a.era                         06-Apr-2025 12:54           558985768
+mainnet-01359-e79eb257.era                         06-Apr-2025 12:55           616124362
+mainnet-01360-8b6c330c.era                         06-Apr-2025 12:55           632658989
+mainnet-01361-8e5394d0.era                         06-Apr-2025 12:55           661831649
+mainnet-01362-fff3d653.era                         06-Apr-2025 12:55           658971016
+mainnet-01363-4548746f.era                         06-Apr-2025 12:55           686452559
+mainnet-01364-d6a5e2d9.era                         06-Apr-2025 12:55           665147593
+mainnet-01365-377bad7f.era                         06-Apr-2025 12:55           617378921
+mainnet-01366-d8771d2f.era                         06-Apr-2025 12:55           667725984
+mainnet-01367-e58a6300.era                         06-Apr-2025 12:56           635922575
+mainnet-01368-1ad08afe.era                         06-Apr-2025 12:56           638802018
+mainnet-01369-6eb67655.era                         06-Apr-2025 12:56           643699872
+mainnet-01370-de9fb9f0.era                         06-Apr-2025 12:56           593040714
+mainnet-01371-ab2dbf41.era                         06-Apr-2025 12:56           703847915
+mainnet-01372-8d09eb00.era                         06-Apr-2025 12:56           707898839
+mainnet-01373-7fdd8c83.era                         06-Apr-2025 12:56           667039238
+mainnet-01374-9fff40c4.era                         06-Apr-2025 12:57           629666777
+mainnet-01375-ecc5376f.era                         06-Apr-2025 12:57           628056691
+mainnet-01376-745e17d7.era                         06-Apr-2025 12:57           530928011
+mainnet-01377-60b83f19.era                         06-Apr-2025 12:57           590097206
+mainnet-01378-571bb72e.era                         06-Apr-2025 12:57           620967618
+mainnet-01379-a41cab48.era                         06-Apr-2025 12:57           642269678
+mainnet-01380-6e279ed9.era                         06-Apr-2025 12:57           613440985
+mainnet-01381-4005cb17.era                         06-Apr-2025 12:57           634050200
+mainnet-01382-5f9d6c83.era                         06-Apr-2025 12:57           540254112
+mainnet-01383-daa6e61b.era                         06-Apr-2025 12:58           578923306
+mainnet-01384-720d50f2.era                         06-Apr-2025 12:58           709219856
+mainnet-01385-cce9515a.era                         06-Apr-2025 12:58           651555256
+mainnet-01386-1dcb024a.era                         06-Apr-2025 12:58           668712337
+mainnet-01387-d94a5dfa.era                         06-Apr-2025 12:58           680710876
+mainnet-01388-dfc329a8.era                         06-Apr-2025 12:58           668537632
+mainnet-01389-dd7ff8fc.era                         06-Apr-2025 12:58           650269127
+mainnet-01390-427f2bda.era                         14-Apr-2025 09:54           679348752
+mainnet-01391-54f9e228.era                         14-Apr-2025 09:55           724231691
+mainnet-01392-bc55bc76.era                         14-Apr-2025 09:55           662825273
+mainnet-01393-35550d53.era                         14-Apr-2025 09:55           611592141
+mainnet-01394-cc3f3451.era                         14-Apr-2025 09:55           559951662
+mainnet-01395-d11df78c.era                         14-Apr-2025 09:55           528602637
+mainnet-01396-af34f6ac.era                         14-Apr-2025 09:55           636722722
+mainnet-01397-0e9fe959.era                         14-Apr-2025 09:55           624047192
+mainnet-01398-4737632a.era                         14-Apr-2025 09:55           625220963
+mainnet-01399-a2a5d13d.era                         14-Apr-2025 09:55           608433250
+mainnet-01400-1546988d.era                         14-Apr-2025 09:56           573436131
+mainnet-01401-95ccdec7.era                         14-Apr-2025 09:56           556839696
+mainnet-01402-89879954.era                         05-May-2025 08:01           587819452
+mainnet-01403-fa4d83f9.era                         05-May-2025 08:02           608654180
+mainnet-01404-bf0d1bbb.era                         05-May-2025 08:03           645193927
+mainnet-01405-af3f0440.era                         05-May-2025 08:03           768950346
+mainnet-01406-cf9da469.era                         05-May-2025 08:04           670386062
+mainnet-01407-2099e5d4.era                         05-May-2025 08:05           529249674
+mainnet-01408-ab7b93b7.era                         28-Apr-2025 01:51           545897544
+mainnet-01409-f68d5c35.era                         28-Apr-2025 01:51           611928991
+mainnet-01410-bf961c25.era                         28-Apr-2025 01:52           873112623
+mainnet-01411-92f93908.era                         28-Apr-2025 01:52           656827871
+mainnet-01412-7cd3a771.era                         28-Apr-2025 01:52           680537261
+mainnet-01413-01118cd1.era                         28-Apr-2025 01:52           656467793
+mainnet-01414-49e4139b.era                         06-May-2025 08:47           617730158
+mainnet-01415-f08713a7.era                         05-May-2025 06:33           649952485
+mainnet-01416-f420a3d7.era                         05-May-2025 06:33           645972098
+mainnet-01417-66e4d377.era                         05-May-2025 06:33           688283748
+mainnet-01418-a8467695.era                         05-May-2025 06:34           618649783
+mainnet-01419-5115573b.era                         05-May-2025 06:34           539700184
+mainnet-01420-38049ee3.era                         05-May-2025 06:34           559203395
+mainnet-01421-67215600.era                         12-May-2025 10:09           587519805
+mainnet-01422-566301aa.era                         12-May-2025 10:09           621356800
+mainnet-01423-d22ac6b8.era                         12-May-2025 10:10           501011043
+mainnet-01424-efb3fed0.era                         12-May-2025 10:10           547732826
+mainnet-01425-472ebfbc.era                         12-May-2025 10:10           508308324
+mainnet-01426-91c877bd.era                         12-May-2025 10:10           500999212
+mainnet-01427-a9a10326.era                         19-May-2025 05:31           511750853
+mainnet-01428-5d919b2e.era                         19-May-2025 05:31           518481386
+mainnet-01429-951203d8.era                         19-May-2025 05:31           515173938
+mainnet-01430-51f933fc.era                         19-May-2025 05:32           510685780
+mainnet-01431-fa942f94.era                         19-May-2025 05:32           506642295
+mainnet-01432-a0634156.era                         19-May-2025 05:32           477210982
+mainnet-01433-ed3ba647.era                         26-May-2025 02:13           506643773
+mainnet-01434-80a6e7c7.era                         26-May-2025 02:13           512992126
+mainnet-01435-12527fcc.era                         26-May-2025 02:14           508901306
+mainnet-01436-96e5270c.era                         26-May-2025 02:14           510014514
+mainnet-01437-4dd8dc46.era                         26-May-2025 02:14           505884905
+mainnet-01438-b7868426.era                         26-May-2025 02:14           485054203
+mainnet-01439-e67413f7.era                         02-Jun-2025 03:09           507507669
+mainnet-01440-5726ebd1.era                         02-Jun-2025 03:09           508670677
+mainnet-01441-8ed2e608.era                         02-Jun-2025 03:10           513917269
+mainnet-01442-91b4aac0.era                         02-Jun-2025 03:10           509790376
+mainnet-01443-8ac15c95.era                         02-Jun-2025 03:10           537040991
+mainnet-01444-a9085580.era                         02-Jun-2025 03:10           523360692
+mainnet-01445-11a035bf.era                         09-Jun-2025 04:38           511355933
+mainnet-01446-8710b8df.era                         09-Jun-2025 04:38           515325340
+mainnet-01447-042230e7.era                         09-Jun-2025 04:38           506901702
+mainnet-01448-7cded86d.era                         09-Jun-2025 04:38           514607852
+mainnet-01449-1aacbb08.era                         09-Jun-2025 04:39           509106002
+mainnet-01450-bf830a58.era                         09-Jun-2025 04:39           503644503
+mainnet-01451-c8ad2f2e.era                         16-Jun-2025 05:26           512352082
+mainnet-01452-cb2a3d0d.era                         16-Jun-2025 05:26           533889445
+mainnet-01453-96b64568.era                         16-Jun-2025 05:26           521220735
+mainnet-01454-b9341b25.era                         16-Jun-2025 05:27           524077240
+mainnet-01455-74a1f4c5.era                         16-Jun-2025 05:27           525673121
+mainnet-01456-08e96075.era                         16-Jun-2025 05:27           497184635
+mainnet-01457-3c8f6ef7.era                         23-Jun-2025 06:23           488037068
+mainnet-01458-5d746467.era                         23-Jun-2025 06:23           532077341
+mainnet-01459-41f6f1cf.era                         23-Jun-2025 06:23           531398644
+mainnet-01460-4aedf6f7.era                         23-Jun-2025 06:23           520741368
+mainnet-01461-9fc63162.era                         23-Jun-2025 06:23           521918826
+mainnet-01462-a65baa7d.era                         23-Jun-2025 06:24           504989708
+mainnet-01463-71cd98bb.era                         23-Jun-2025 06:24           502781197
+mainnet-01464-65d55b17.era                         30-Jun-2025 04:26           518142408
+mainnet-01465-ec236506.era                         30-Jun-2025 04:26           593335981
+mainnet-01466-1b573477.era                         30-Jun-2025 04:26           577705534
+mainnet-01467-373863fb.era                         30-Jun-2025 04:26           545465708
+mainnet-01468-3186aea5.era                         30-Jun-2025 04:27           509381337
+mainnet-01469-cb46aff4.era                         30-Jun-2025 04:27           491401479
+mainnet-01470-87d4bf6c.era                         07-Jul-2025 02:50           531057855
+mainnet-01471-c1909436.era                         07-Jul-2025 02:50           539567050
+mainnet-01472-6c492b68.era                         07-Jul-2025 02:50           532392791
+mainnet-01473-6a7890dc.era                         07-Jul-2025 02:50           537249038
+mainnet-01474-bd1b54bd.era                         07-Jul-2025 02:51           504579030
+mainnet-01475-0753f1d0.era                         07-Jul-2025 02:51           485973803
+mainnet-01476-ebebbd78.era                         14-Jul-2025 03:40           522532725
+mainnet-01477-3d63c65e.era                         14-Jul-2025 03:40           533358429
+mainnet-01478-6266ab2a.era                         14-Jul-2025 03:41           537264749
+mainnet-01479-19fbd7cf.era                         14-Jul-2025 03:41           541010085
+mainnet-01480-73549677.era                         14-Jul-2025 03:41           543304813
+mainnet-01481-3ea05fea.era                         14-Jul-2025 03:41           522750008
+mainnet-01482-5c2a345e.era                         21-Jul-2025 01:17           531863029
+mainnet-01483-3fc48cd4.era                         21-Jul-2025 01:17           537582790
+mainnet-01484-f8aa993b.era                         21-Jul-2025 01:17           537786130
+mainnet-01485-5c02747c.era                         21-Jul-2025 01:17           542329685
+mainnet-01486-ef245ecc.era                         21-Jul-2025 01:18           548678220
+mainnet-01487-c4bcbb92.era                         21-Jul-2025 01:18           532909638
+mainnet-01488-4283192d.era                         28-Jul-2025 04:49           551272646
+mainnet-01489-382b8330.era                         28-Jul-2025 04:49           619160048
+mainnet-01490-55e9e78e.era                         28-Jul-2025 04:49           616392325
+mainnet-01491-221c924e.era                         28-Jul-2025 04:50           617171399
+mainnet-01492-b7151c51.era                         28-Jul-2025 04:50           609245440
+mainnet-01493-76fa01be.era                         28-Jul-2025 04:50           555349101
+mainnet-01494-e6fa99c8.era                         04-Aug-2025 07:55           598647842
+mainnet-01495-57543dcf.era                         04-Aug-2025 07:55           614211341
+mainnet-01496-c23b15bb.era                         04-Aug-2025 07:55           589009100
+mainnet-01497-a07e2a40.era                         04-Aug-2025 07:55           572260943
+mainnet-01498-4c868960.era                         04-Aug-2025 07:56           629190715
+mainnet-01499-6a6a20ff.era                         04-Aug-2025 07:56           590685890
+mainnet-01500-057b6b26.era                         04-Aug-2025 07:56           556171743
+mainnet-01501-f6f87b12.era                         11-Aug-2025 08:49           597333684
+mainnet-01502-d4ffeca9.era                         11-Aug-2025 08:49           667039182
+mainnet-01503-dab29b31.era                         11-Aug-2025 08:49           625552613
+mainnet-01504-212f7e5d.era                         11-Aug-2025 08:49           639827455
+mainnet-01505-7dcbdfcc.era                         11-Aug-2025 08:50           630276257
+mainnet-01506-4781865b.era                         11-Aug-2025 08:50           617615852
+mainnet-01507-71a0d0ed.era                         18-Aug-2025 02:39           616329885
+mainnet-01508-25703831.era                         18-Aug-2025 02:40           626754368
+mainnet-01509-8e00415c.era                         18-Aug-2025 02:40           634867114
+mainnet-01510-2e4b9a11.era                         18-Aug-2025 02:40           641222043
+mainnet-01511-da173f04.era                         18-Aug-2025 02:40           614196467
+mainnet-01512-0a49b96e.era                         18-Aug-2025 02:41           592189452
+mainnet-01513-df34c479.era                         25-Aug-2025 09:53           613528021
+mainnet-01514-5edd8c1a.era                         25-Aug-2025 09:53           633913654
+mainnet-01515-11eb351b.era                         25-Aug-2025 09:53           617540268
+mainnet-01516-8670fdc7.era                         25-Aug-2025 09:54           608231671
+mainnet-01517-77bf4dd2.era                         25-Aug-2025 09:54           628744521
+mainnet-01518-fe90de5a.era                         25-Aug-2025 09:54           598496583
+mainnet-01519-ce776aa1.era                         01-Sep-2025 08:26           640576960
+mainnet-01520-32722bfb.era                         01-Sep-2025 08:27           717198343
+mainnet-01521-0e71ad4d.era                         01-Sep-2025 08:27           760259031
+mainnet-01522-50583e4f.era                         01-Sep-2025 08:27           675462440
+mainnet-01523-910ad536.era                         01-Sep-2025 08:28           613764353
+mainnet-01524-ea8f0b5e.era                         01-Sep-2025 08:28           575625087
+mainnet-01525-d62de99b.era                         08-Sep-2025 01:43           669359119
+mainnet-01526-391b5e5a.era                         08-Sep-2025 01:44           653964184
+mainnet-01527-01489b26.era                         08-Sep-2025 01:44           675542055
+mainnet-01528-e9bc5b30.era                         08-Sep-2025 01:44           711891996
+mainnet-01529-3798eb16.era                         08-Sep-2025 01:44           706257201
+mainnet-01530-0accb22b.era                         08-Sep-2025 01:45           653919202
+mainnet-01531-8d911990.era                         15-Sep-2025 07:55           650968739
+mainnet-01532-f43a2fb4.era                         15-Sep-2025 07:55           709114767
+mainnet-01533-ab0d4f67.era                         15-Sep-2025 07:55           668699214
+mainnet-01534-5f9378bf.era                         15-Sep-2025 07:56           676501449
+mainnet-01535-80360bd2.era                         15-Sep-2025 07:56           674695008
+mainnet-01536-0e0b783e.era                         15-Sep-2025 07:56           638043802
+mainnet-01537-96d14cf5.era                         15-Sep-2025 07:57           606845395
+mainnet-01538-2c9f9367.era                         22-Sep-2025 05:55           662375155
+mainnet-01539-c5fd437f.era                         22-Sep-2025 05:55           648392361
+mainnet-01540-3118fe4e.era                         22-Sep-2025 05:55           650881186
+mainnet-01541-44b4fcdd.era                         22-Sep-2025 05:56           643553635
+mainnet-01542-462370e1.era                         22-Sep-2025 05:56           650929390
+mainnet-01543-eb979c5c.era                         22-Sep-2025 05:56           580550163
+mainnet-01544-03c55703.era                         29-Sep-2025 04:05           635803552
+mainnet-01545-b9858945.era                         29-Sep-2025 04:06           660884266
+mainnet-01546-fbb1d2b1.era                         29-Sep-2025 04:06           642186847
+mainnet-01547-80f87b05.era                         29-Sep-2025 04:06           640847272
+mainnet-01548-296c79ff.era                         29-Sep-2025 04:06           665760701
+mainnet-01549-009bc358.era                         29-Sep-2025 04:07           611997495
+mainnet-01550-cee52de0.era                         06-Oct-2025 02:26           638819034
+mainnet-01551-4f9d2662.era                         06-Oct-2025 02:26           644511364
+mainnet-01552-39e1ab4f.era                         06-Oct-2025 02:26           652348099
+mainnet-01553-a4438806.era                         06-Oct-2025 02:26           642283660
+mainnet-01554-a1d21712.era                         06-Oct-2025 02:27           609334280
+mainnet-01555-0f6bb064.era                         06-Oct-2025 02:27           580638372
+mainnet-01556-4874cc8c.era                         13-Oct-2025 10:53           606856176
+mainnet-01557-169c265e.era                         13-Oct-2025 10:53           652740240
+mainnet-01558-d2a546b1.era                         13-Oct-2025 10:53           629692121
+mainnet-01559-0297ac02.era                         13-Oct-2025 10:54           628998706
+mainnet-01560-60670459.era                         13-Oct-2025 10:54           651533419
+mainnet-01561-1c0d3738.era                         13-Oct-2025 10:54           633349455
+mainnet-01562-19638cd7.era                         20-Oct-2025 08:51           630590339
+mainnet-01563-83425e17.era                         20-Oct-2025 08:52           634230055
+mainnet-01564-be3fb544.era                         20-Oct-2025 08:53           633587281
+mainnet-01565-70a30ac2.era                         20-Oct-2025 08:54           657461325
+mainnet-01566-889bd753.era                         20-Oct-2025 08:55           642401052
+mainnet-01567-5dab1697.era                         20-Oct-2025 08:55           617456928
+mainnet-01568-151718cf.era                         22-Oct-2025 13:10           586683223
+mainnet-01569-a8d582c2.era                         22-Oct-2025 13:10           589552271
+mainnet-01570-52db1f8f.era                         27-Oct-2025 05:26           611340268
+mainnet-01571-871da102.era                         27-Oct-2025 05:26           594561772
+mainnet-01572-e04875db.era                         27-Oct-2025 05:27           579162292
+mainnet-01573-75eddab6.era                         27-Oct-2025 05:27           522297441
+mainnet-01574-f80ba4cd.era                         03-Nov-2025 03:34           618565234
+mainnet-01575-1fc558d2.era                         03-Nov-2025 03:34           620994360
+mainnet-01576-e32a9cb4.era                         03-Nov-2025 03:34           617223135
+mainnet-01577-9c577cdb.era                         03-Nov-2025 03:34           649445555
+mainnet-01578-5f70f2e2.era                         03-Nov-2025 03:35           656459253
+mainnet-01579-ab2ba57f.era                         03-Nov-2025 03:35           568585524
+mainnet-01580-bedd6a6e.era                         10-Nov-2025 01:14           604540958
+mainnet-01581-82073d28.era                         10-Nov-2025 01:14           669364186
+mainnet-01582-f32c6826.era                         10-Nov-2025 01:15           636571133
+mainnet-01583-cbb3b80b.era                         10-Nov-2025 01:15           625837882
+mainnet-01584-357790d1.era                         10-Nov-2025 01:15           615795824
+mainnet-01585-99fd7574.era                         10-Nov-2025 01:15           645204626
+mainnet-01586-3a3b5e27.era                         10-Nov-2025 01:16           617120440
+

+ diff --git a/crates/era-downloader/tests/res/mainnet-00000-5ec1ffb8.era1 b/crates/era-downloader/tests/res/era1-files/mainnet-00000-5ec1ffb8.era1 similarity index 100% rename from crates/era-downloader/tests/res/mainnet-00000-5ec1ffb8.era1 rename to crates/era-downloader/tests/res/era1-files/mainnet-00000-5ec1ffb8.era1 diff --git a/crates/era-downloader/tests/res/mainnet-00001-a5364e9a.era1 b/crates/era-downloader/tests/res/era1-files/mainnet-00001-a5364e9a.era1 similarity index 100% rename from crates/era-downloader/tests/res/mainnet-00001-a5364e9a.era1 rename to crates/era-downloader/tests/res/era1-files/mainnet-00001-a5364e9a.era1 diff --git a/crates/era-downloader/tests/res/ithaca.html b/crates/era-downloader/tests/res/era1-ithaca.html similarity index 100% rename from crates/era-downloader/tests/res/ithaca.html rename to crates/era-downloader/tests/res/era1-ithaca.html diff --git a/crates/era-downloader/tests/res/nimbus.html b/crates/era-downloader/tests/res/era1-nimbus.html similarity index 100% rename from crates/era-downloader/tests/res/nimbus.html rename to crates/era-downloader/tests/res/era1-nimbus.html diff --git a/crates/era/src/common/file_ops.rs b/crates/era/src/common/file_ops.rs index 752f5b66fb..1a1e1defb7 100644 --- a/crates/era/src/common/file_ops.rs +++ b/crates/era/src/common/file_ops.rs @@ -122,3 +122,45 @@ impl> FileWriter for T { Self::create(path, file) } } + +/// Era file type identifier +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EraFileType { + /// Consensus layer ERA file, `.era` + /// Contains beacon blocks and states + Era, + /// Execution layer ERA1 file, `.era1` + /// Contains execution blocks pre-merge + Era1, +} + +impl EraFileType { + /// Get the file extension for this type, dot included + pub const fn extension(&self) -> &'static str { + match self { + Self::Era => ".era", + Self::Era1 => ".era1", + } + } + + /// Detect file type from a filename + pub fn from_filename(filename: &str) -> Option { + if filename.ends_with(".era") { + Some(Self::Era) + } else if filename.ends_with(".era1") { + Some(Self::Era1) + } else { + None + } + } + + /// Detect file type from URL + /// By default, it assumes `Era` type + pub fn from_url(url: &str) -> Self { + if url.contains("era1") { + Self::Era1 + } else { + Self::Era + } + } +} diff --git a/crates/era/src/era1/types/execution.rs b/crates/era/src/era1/types/execution.rs index b207bfb03c..b7ce19618e 100644 --- a/crates/era/src/era1/types/execution.rs +++ b/crates/era/src/era1/types/execution.rs @@ -53,22 +53,18 @@ //! ## [`CompressedReceipts`] //! //! ```rust -//! use alloy_consensus::ReceiptWithBloom; +//! use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom}; //! use reth_era::{common::decode::DecodeCompressed, era1::types::execution::CompressedReceipts}; -//! use reth_ethereum_primitives::{Receipt, TxType}; //! -//! let receipt = Receipt { -//! tx_type: TxType::Legacy, -//! success: true, -//! cumulative_gas_used: 21000, -//! logs: vec![], -//! }; -//! let receipt_with_bloom = ReceiptWithBloom { receipt, logs_bloom: Default::default() }; +//! let receipt = +//! Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 21000, logs: vec![] }; +//! let receipt_with_bloom = ReceiptWithBloom::new(receipt, Default::default()); +//! let enveloped_receipt = ReceiptEnvelope::Legacy(receipt_with_bloom); //! // Compress the receipt: rlp encoding and snappy compression -//! let compressed_receipt_data = CompressedReceipts::from_encodable(&receipt_with_bloom)?; +//! let compressed_receipt_data = CompressedReceipts::from_encodable(&enveloped_receipt)?; //! // Get raw receipt by decoding and decompressing compressed and encoded receipt -//! let decompressed_receipt = compressed_receipt_data.decode::()?; -//! assert_eq!(decompressed_receipt.receipt.cumulative_gas_used, 21000); +//! let decompressed_receipt = compressed_receipt_data.decode::()?; +//! assert_eq!(decompressed_receipt.cumulative_gas_used(), 21000); //! # Ok::<(), reth_era::e2s::error::E2sError>(()) //! `````` @@ -703,8 +699,8 @@ mod tests { .expect("Failed to compress receipt list"); // Decode the compressed receipts back - // Note: most likely the decoding for real era files will be done to reach - // `Vec`` + // Note: For real ERA1 files, use `Vec` before Era ~1520 or use + // `Vec` after this era let decoded_receipts: Vec = compressed_receipts.decode().expect("Failed to decode compressed receipt list"); diff --git a/crates/era/tests/it/dd.rs b/crates/era/tests/it/dd.rs index 9c1e5d163e..85cc0b1fb9 100644 --- a/crates/era/tests/it/dd.rs +++ b/crates/era/tests/it/dd.rs @@ -14,138 +14,146 @@ use reth_era::{ use reth_ethereum_primitives::TransactionSigned; use std::io::Cursor; -use crate::{open_test_file, Era1TestDownloader, ERA1_MAINNET_FILES_NAMES, MAINNET}; +use crate::{Era1TestDownloader, MAINNET}; -#[tokio::test(flavor = "multi_thread")] -#[ignore = "download intensive"] -async fn test_mainnet_era1_only_file_decompression_and_decoding() -> eyre::Result<()> { - let downloader = Era1TestDownloader::new().await.expect("Failed to create downloader"); +// Helper function to test decompression and decoding for a specific file +async fn test_file_decompression( + downloader: &Era1TestDownloader, + filename: &str, +) -> eyre::Result<()> { + println!("\nTesting file: {filename}"); + let file = downloader.open_era1_file(filename, MAINNET).await?; - for &filename in &ERA1_MAINNET_FILES_NAMES { - println!("\nTesting file: {filename}"); - let file = open_test_file(filename, &downloader, MAINNET).await?; + // Test block decompression across different positions in the file + let test_block_indices = [ + 0, // First block + file.group.blocks.len() / 2, // Middle block + file.group.blocks.len() - 1, // Last block + ]; - // Test block decompression across different positions in the file - let test_block_indices = [ - 0, // First block - file.group.blocks.len() / 2, // Middle block - file.group.blocks.len() - 1, // Last block - ]; + for &block_idx in &test_block_indices { + let block = &file.group.blocks[block_idx]; + let block_number = file.group.block_index.starting_number() + block_idx as u64; - for &block_idx in &test_block_indices { - let block = &file.group.blocks[block_idx]; - let block_number = file.group.block_index.starting_number() + block_idx as u64; + println!( + "\n Testing block {}, compressed body size: {} bytes", + block_number, + block.body.data.len() + ); - println!( - "\n Testing block {}, compressed body size: {} bytes", - block_number, - block.body.data.len() - ); + // Test header decompression and decoding + let header_data = block.header.decompress()?; + assert!( + !header_data.is_empty(), + "Block {block_number} header decompression should produce non-empty data" + ); - // Test header decompression and decoding - let header_data = block.header.decompress()?; - assert!( - !header_data.is_empty(), - "Block {block_number} header decompression should produce non-empty data" - ); + let header = block.header.decode_header()?; + assert_eq!(header.number, block_number, "Decoded header should have correct block number"); + println!("Header decompression and decoding successful"); - let header = block.header.decode_header()?; - assert_eq!( - header.number, block_number, - "Decoded header should have correct block number" - ); - println!("Header decompression and decoding successful"); + // Test body decompression + let body_data = block.body.decompress()?; + assert!( + !body_data.is_empty(), + "Block {block_number} body decompression should produce non-empty data" + ); + println!("Body decompression successful ({} bytes)", body_data.len()); - // Test body decompression - let body_data = block.body.decompress()?; - assert!( - !body_data.is_empty(), - "Block {block_number} body decompression should produce non-empty data" - ); - println!("Body decompression successful ({} bytes)", body_data.len()); - - let decoded_body: BlockBody = - CompressedBody::decode_body_from_decompressed::( - &body_data, - ) + let decoded_body: BlockBody = + CompressedBody::decode_body_from_decompressed::(&body_data) .expect("Failed to decode body"); - println!( - "Body decoding successful: {} transactions, {} ommers, withdrawals: {}", - decoded_body.transactions.len(), - decoded_body.ommers.len(), - decoded_body.withdrawals.is_some() - ); + println!( + "Body decoding successful: {} transactions, {} ommers, withdrawals: {}", + decoded_body.transactions.len(), + decoded_body.ommers.len(), + decoded_body.withdrawals.is_some() + ); - // Test receipts decompression - let receipts_data = block.receipts.decompress()?; - assert!( - !receipts_data.is_empty(), - "Block {block_number} receipts decompression should produce non-empty data" - ); - println!("Receipts decompression successful ({} bytes)", receipts_data.len()); + // Test receipts decompression + let receipts_data = block.receipts.decompress()?; + assert!( + !receipts_data.is_empty(), + "Block {block_number} receipts decompression should produce non-empty data" + ); + println!("Receipts decompression successful ({} bytes)", receipts_data.len()); - assert!( - block.total_difficulty.value > U256::ZERO, - "Block {block_number} should have non-zero difficulty" - ); - println!("Total difficulty verified: {}", block.total_difficulty.value); - } + assert!( + block.total_difficulty.value > U256::ZERO, + "Block {block_number} should have non-zero difficulty" + ); + println!("Total difficulty verified: {}", block.total_difficulty.value); + } - // Test round-trip serialization - println!("\n Testing data preservation roundtrip..."); - let mut buffer = Vec::new(); - { - let mut writer = Era1Writer::new(&mut buffer); - writer.write_file(&file)?; - } + // Test round-trip serialization + println!("\n Testing data preservation roundtrip..."); + let mut buffer = Vec::new(); + { + let mut writer = Era1Writer::new(&mut buffer); + writer.write_file(&file)?; + } - // Read back from buffer - let reader = Era1Reader::new(Cursor::new(&buffer)); - let read_back_file = reader.read(file.id.network_name.clone())?; + // Read back from buffer + let reader = Era1Reader::new(Cursor::new(&buffer)); + let read_back_file = reader.read(file.id.network_name.clone())?; - // Verify basic properties are preserved - assert_eq!(file.id.network_name, read_back_file.id.network_name); - assert_eq!(file.id.start_block, read_back_file.id.start_block); - assert_eq!(file.group.blocks.len(), read_back_file.group.blocks.len()); - assert_eq!(file.group.accumulator.root, read_back_file.group.accumulator.root); + // Verify basic properties are preserved + assert_eq!(file.id.network_name, read_back_file.id.network_name); + assert_eq!(file.id.start_block, read_back_file.id.start_block); + assert_eq!(file.group.blocks.len(), read_back_file.group.blocks.len()); + assert_eq!(file.group.accumulator.root, read_back_file.group.accumulator.root); - // Test data preservation for some blocks - for &idx in &test_block_indices { - let original_block = &file.group.blocks[idx]; - let read_back_block = &read_back_file.group.blocks[idx]; - let block_number = file.group.block_index.starting_number() + idx as u64; + // Test data preservation for some blocks + for &idx in &test_block_indices { + let original_block = &file.group.blocks[idx]; + let read_back_block = &read_back_file.group.blocks[idx]; + let block_number = file.group.block_index.starting_number() + idx as u64; - println!("Block {block_number} details:"); - println!(" Header size: {} bytes", original_block.header.data.len()); - println!(" Body size: {} bytes", original_block.body.data.len()); - println!(" Receipts size: {} bytes", original_block.receipts.data.len()); + println!("Block {block_number} details:"); + println!(" Header size: {} bytes", original_block.header.data.len()); + println!(" Body size: {} bytes", original_block.body.data.len()); + println!(" Receipts size: {} bytes", original_block.receipts.data.len()); - // Test that decompressed data is identical - assert_eq!( - original_block.header.decompress()?, - read_back_block.header.decompress()?, - "Header data should be identical for block {block_number}" - ); + // Test that decompressed data is identical + assert_eq!( + original_block.header.decompress()?, + read_back_block.header.decompress()?, + "Header data should be identical for block {block_number}" + ); - assert_eq!( - original_block.body.decompress()?, - read_back_block.body.decompress()?, - "Body data should be identical for block {block_number}" - ); + assert_eq!( + original_block.body.decompress()?, + read_back_block.body.decompress()?, + "Body data should be identical for block {block_number}" + ); - assert_eq!( - original_block.receipts.decompress()?, - read_back_block.receipts.decompress()?, - "Receipts data should be identical for block {block_number}" - ); + assert_eq!( + original_block.receipts.decompress()?, + read_back_block.receipts.decompress()?, + "Receipts data should be identical for block {block_number}" + ); - assert_eq!( - original_block.total_difficulty.value, read_back_block.total_difficulty.value, - "Total difficulty should be identical for block {block_number}" - ); - } + assert_eq!( + original_block.total_difficulty.value, read_back_block.total_difficulty.value, + "Total difficulty should be identical for block {block_number}" + ); } Ok(()) } + +#[test_case::test_case("mainnet-00000-5ec1ffb8.era1"; "era_dd_mainnet_0")] +#[test_case::test_case("mainnet-00003-d8b8a40b.era1"; "era_dd_mainnet_3")] +#[test_case::test_case("mainnet-00151-e322efe1.era1"; "era_dd_mainnet_151")] +#[test_case::test_case("mainnet-00293-0d6c5812.era1"; "era_dd_mainnet_293")] +#[test_case::test_case("mainnet-00443-ea71b6f9.era1"; "era_dd_mainnet_443")] +#[test_case::test_case("mainnet-01367-d7efc68f.era1"; "era_dd_mainnet_1367")] +#[test_case::test_case("mainnet-01610-99fdde4b.era1"; "era_dd_mainnet_1610")] +#[test_case::test_case("mainnet-01895-3f81607c.era1"; "era_dd_mainnet_1895")] +#[tokio::test(flavor = "multi_thread")] +#[ignore = "download intensive"] +async fn test_mainnet_era1_file_decompression_and_decoding(filename: &str) -> eyre::Result<()> { + let downloader = Era1TestDownloader::new().await?; + test_file_decompression(&downloader, filename).await +} diff --git a/crates/era/tests/it/main.rs b/crates/era/tests/it/main.rs index 9750e7b10b..625f24a5ec 100644 --- a/crates/era/tests/it/main.rs +++ b/crates/era/tests/it/main.rs @@ -15,7 +15,7 @@ use reth_era::{ use reth_era_downloader::EraClient; use std::{ collections::HashMap, - path::{Path, PathBuf}, + path::PathBuf, str::FromStr, sync::{Arc, Mutex}, }; @@ -63,7 +63,7 @@ const ERA1_SEPOLIA_FILES_NAMES: [&str; 4] = [ "sepolia-00000-643a00f7.era1", "sepolia-00074-0e81003c.era1", "sepolia-00173-b6924da5.era1", - "sepolia-00182-a4f0a8a1.era1 ", + "sepolia-00182-a4f0a8a1.era1", ]; /// Utility for downloading `.era1` files for tests @@ -157,18 +157,3 @@ impl Era1TestDownloader { Era1Reader::open(&path, network).map_err(|e| eyre!("Failed to open Era1 file: {e}")) } } - -/// Open a test file by name, -/// downloading only if it is necessary -async fn open_test_file( - file_path: &str, - downloader: &Era1TestDownloader, - network: &str, -) -> Result { - let filename = Path::new(file_path) - .file_name() - .and_then(|os_str| os_str.to_str()) - .ok_or_else(|| eyre!("Invalid file path: {}", file_path))?; - - downloader.open_era1_file(filename, network).await -} diff --git a/crates/era/tests/it/roundtrip.rs b/crates/era/tests/it/roundtrip.rs index 56f5ac20cd..cbc51e111c 100644 --- a/crates/era/tests/it/roundtrip.rs +++ b/crates/era/tests/it/roundtrip.rs @@ -7,8 +7,7 @@ //! - Writing the data back to a new file //! - Confirming that all original data is preserved throughout the process -use alloy_consensus::{BlockBody, BlockHeader, Header, ReceiptWithBloom}; -use rand::{prelude::IndexedRandom, rng}; +use alloy_consensus::{BlockBody, BlockHeader, Header, ReceiptEnvelope}; use reth_era::{ common::file_ops::{EraFileFormat, StreamReader, StreamWriter}, e2s::types::IndexEntry, @@ -25,9 +24,7 @@ use reth_era::{ use reth_ethereum_primitives::TransactionSigned; use std::io::Cursor; -use crate::{ - Era1TestDownloader, ERA1_MAINNET_FILES_NAMES, ERA1_SEPOLIA_FILES_NAMES, MAINNET, SEPOLIA, -}; +use crate::{Era1TestDownloader, MAINNET, SEPOLIA}; // Helper function to test roundtrip compression/encoding for a specific file async fn test_file_roundtrip( @@ -152,10 +149,9 @@ async fn test_file_roundtrip( ); // Decode receipts - let original_receipts_decoded = - original_block.receipts.decode::>()?; + let original_receipts_decoded = original_block.receipts.decode::>()?; let roundtrip_receipts_decoded = - roundtrip_block.receipts.decode::>()?; + roundtrip_block.receipts.decode::>()?; assert_eq!( original_receipts_decoded, roundtrip_receipts_decoded, @@ -256,35 +252,27 @@ async fn test_file_roundtrip( Ok(()) } +#[test_case::test_case("mainnet-00000-5ec1ffb8.era1"; "era_mainnet_0")] +#[test_case::test_case("mainnet-00151-e322efe1.era1"; "era_mainnet_151")] +#[test_case::test_case("mainnet-01367-d7efc68f.era1"; "era_mainnet_1367")] +#[test_case::test_case("mainnet-01895-3f81607c.era1"; "era_mainnet_1895")] #[tokio::test(flavor = "multi_thread")] #[ignore = "download intensive"] -async fn test_roundtrip_compression_encoding_mainnet() -> eyre::Result<()> { +async fn test_roundtrip_compression_encoding_mainnet(filename: &str) -> eyre::Result<()> { + let downloader = Era1TestDownloader::new().await?; + test_file_roundtrip(&downloader, filename, MAINNET).await +} + +#[test_case::test_case("sepolia-00000-643a00f7.era1"; "era_sepolia_0")] +#[test_case::test_case("sepolia-00074-0e81003c.era1"; "era_sepolia_74")] +#[test_case::test_case("sepolia-00173-b6924da5.era1"; "era_sepolia_173")] +#[test_case::test_case("sepolia-00182-a4f0a8a1.era1"; "era_sepolia_182")] +#[tokio::test(flavor = "multi_thread")] +#[ignore = "download intensive"] +async fn test_roundtrip_compression_encoding_sepolia(filename: &str) -> eyre::Result<()> { let downloader = Era1TestDownloader::new().await?; - let mut rng = rng(); - - // pick 4 random files from the mainnet list - let sample_files: Vec<&str> = - ERA1_MAINNET_FILES_NAMES.choose_multiple(&mut rng, 4).copied().collect(); - - println!("Testing {} randomly selected mainnet files", sample_files.len()); - - for &filename in &sample_files { - test_file_roundtrip(&downloader, filename, MAINNET).await?; - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -#[ignore = "download intensive"] -async fn test_roundtrip_compression_encoding_sepolia() -> eyre::Result<()> { - let downloader = Era1TestDownloader::new().await?; - - // Test all Sepolia files - for &filename in &ERA1_SEPOLIA_FILES_NAMES { - test_file_roundtrip(&downloader, filename, SEPOLIA).await?; - } + test_file_roundtrip(&downloader, filename, SEPOLIA).await?; Ok(()) } diff --git a/crates/ethereum/cli/src/app.rs b/crates/ethereum/cli/src/app.rs index 7a9bad459a..edf648d955 100644 --- a/crates/ethereum/cli/src/app.rs +++ b/crates/ethereum/cli/src/app.rs @@ -10,13 +10,11 @@ use reth_cli_runner::CliRunner; use reth_db::DatabaseEnv; use reth_node_api::NodePrimitives; use reth_node_builder::{NodeBuilder, WithLaunchContext}; -use reth_node_core::args::OtlpInitStatus; use reth_node_ethereum::{consensus::EthBeaconConsensus, EthEvmConfig, EthereumNode}; use reth_node_metrics::recorder::install_prometheus_recorder; use reth_rpc_server_types::RpcModuleValidator; use reth_tracing::{FileWorkerGuard, Layers}; use std::{fmt, sync::Arc}; -use tracing::{info, warn}; /// A wrapper around a parsed CLI that handles command execution. #[derive(Debug)] @@ -107,26 +105,12 @@ where /// Initializes tracing with the configured options. /// - /// If file logging is enabled, this function stores guard to the struct. - /// For gRPC OTLP, it requires tokio runtime context. + /// See [`Cli::init_tracing`] for more information. pub fn init_tracing(&mut self, runner: &CliRunner) -> Result<()> { if self.guard.is_none() { - let mut layers = self.layers.take().unwrap_or_default(); - - let otlp_status = runner.block_on(self.cli.traces.init_otlp_tracing(&mut layers))?; - - self.guard = self.cli.logs.init_tracing_with_layers(layers)?; - info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.cli.logs.log_file_directory); - match otlp_status { - OtlpInitStatus::Started(endpoint) => { - info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.cli.traces.protocol); - } - OtlpInitStatus::NoFeature => { - warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature") - } - OtlpInitStatus::Disabled => {} - } + self.guard = self.cli.init_tracing(runner, self.layers.take().unwrap_or_default())?; } + Ok(()) } } diff --git a/crates/ethereum/cli/src/interface.rs b/crates/ethereum/cli/src/interface.rs index f41143bb4f..c1a0f3f84b 100644 --- a/crates/ethereum/cli/src/interface.rs +++ b/crates/ethereum/cli/src/interface.rs @@ -19,14 +19,14 @@ use reth_db::DatabaseEnv; use reth_node_api::NodePrimitives; use reth_node_builder::{NodeBuilder, WithLaunchContext}; use reth_node_core::{ - args::{LogArgs, TraceArgs}, + args::{LogArgs, OtlpInitStatus, TraceArgs}, version::version_metadata, }; use reth_node_metrics::recorder::install_prometheus_recorder; use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator}; -use reth_tracing::FileWorkerGuard; +use reth_tracing::{FileWorkerGuard, Layers}; use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc}; -use tracing::info; +use tracing::{info, warn}; /// The main reth cli interface. /// @@ -205,8 +205,7 @@ impl self.logs.log_file_directory = self.logs.log_file_directory.join(chain_spec.chain().to_string()); } - let _guard = self.init_tracing()?; - info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory); + let _guard = self.init_tracing(&runner, Layers::new())?; // Install the prometheus recorder to be sure to record all metrics let _ = install_prometheus_recorder(); @@ -219,11 +218,27 @@ impl /// /// If file logging is enabled, this function returns a guard that must be kept alive to ensure /// that all logs are flushed to disk. + /// /// If an OTLP endpoint is specified, it will export metrics to the configured collector. - pub fn init_tracing(&self) -> eyre::Result> { - let layers = reth_tracing::Layers::new(); + pub fn init_tracing( + &mut self, + runner: &CliRunner, + mut layers: Layers, + ) -> eyre::Result> { + let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?; let guard = self.logs.init_tracing_with_layers(layers)?; + info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory); + match otlp_status { + OtlpInitStatus::Started(endpoint) => { + info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol); + } + OtlpInitStatus::NoFeature => { + warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature") + } + OtlpInitStatus::Disabled => {} + } + Ok(guard) } } diff --git a/crates/ethereum/consensus/src/lib.rs b/crates/ethereum/consensus/src/lib.rs index d49123fb62..6dd314b7ff 100644 --- a/crates/ethereum/consensus/src/lib.rs +++ b/crates/ethereum/consensus/src/lib.rs @@ -12,7 +12,7 @@ extern crate alloc; use alloc::{fmt::Debug, sync::Arc}; -use alloy_consensus::EMPTY_OMMER_ROOT_HASH; +use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::eip7840::BlobParams; use reth_chainspec::{EthChainSpec, EthereumHardforks}; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator}; @@ -38,12 +38,25 @@ pub use validation::validate_block_post_execution; pub struct EthBeaconConsensus { /// Configuration chain_spec: Arc, + /// Maximum allowed extra data size in bytes + max_extra_data_size: usize, } impl EthBeaconConsensus { /// Create a new instance of [`EthBeaconConsensus`] pub const fn new(chain_spec: Arc) -> Self { - Self { chain_spec } + Self { chain_spec, max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE } + } + + /// Returns the maximum allowed extra data size. + pub const fn max_extra_data_size(&self) -> usize { + self.max_extra_data_size + } + + /// Sets the maximum allowed extra data size and returns the updated instance. + pub const fn with_max_extra_data_size(mut self, size: usize) -> Self { + self.max_extra_data_size = size; + self } /// Returns the chain spec associated with this consensus engine. @@ -131,7 +144,7 @@ where } } } - validate_header_extra_data(header)?; + validate_header_extra_data(header, self.max_extra_data_size)?; validate_header_gas(header)?; validate_header_base_fee(header, &self.chain_spec)?; diff --git a/crates/ethereum/hardforks/src/display.rs b/crates/ethereum/hardforks/src/display.rs index b01c478df8..9cbd253b28 100644 --- a/crates/ethereum/hardforks/src/display.rs +++ b/crates/ethereum/hardforks/src/display.rs @@ -124,15 +124,17 @@ impl core::fmt::Display for DisplayHardforks { Ok(()) } - format( - "Pre-merge hard forks (block based)", - &self.pre_merge, - self.with_merge.is_empty(), - f, - )?; + if !self.pre_merge.is_empty() { + format( + "Pre-merge hard forks (block based)", + &self.pre_merge, + self.with_merge.is_empty(), + f, + )?; + } if self.with_merge.is_empty() { - if !self.post_merge.is_empty() { + if !self.pre_merge.is_empty() && !self.post_merge.is_empty() { // need an extra line here in case we don't have a merge block (optimism) writeln!(f)?; } diff --git a/crates/ethereum/node/Cargo.toml b/crates/ethereum/node/Cargo.toml index 575934007f..4ea3b8ccb1 100644 --- a/crates/ethereum/node/Cargo.toml +++ b/crates/ethereum/node/Cargo.toml @@ -60,6 +60,7 @@ reth-exex.workspace = true reth-node-core.workspace = true reth-e2e-test-utils.workspace = true reth-tasks.workspace = true +reth-testing-utils.workspace = true alloy-primitives.workspace = true alloy-provider.workspace = true @@ -74,6 +75,9 @@ futures.workspace = true tokio.workspace = true serde_json.workspace = true rand.workspace = true +serde.workspace = true +alloy-rpc-types-trace.workspace = true +similar-asserts.workspace = true [features] default = [] diff --git a/crates/ethereum/node/tests/e2e/main.rs b/crates/ethereum/node/tests/e2e/main.rs index 0ebee83cd5..c2ec66eb94 100644 --- a/crates/ethereum/node/tests/e2e/main.rs +++ b/crates/ethereum/node/tests/e2e/main.rs @@ -5,6 +5,7 @@ mod dev; mod eth; mod p2p; mod pool; +mod prestate; mod rpc; mod utils; diff --git a/crates/ethereum/node/tests/e2e/p2p.rs b/crates/ethereum/node/tests/e2e/p2p.rs index 34a4210538..74266b1675 100644 --- a/crates/ethereum/node/tests/e2e/p2p.rs +++ b/crates/ethereum/node/tests/e2e/p2p.rs @@ -1,10 +1,18 @@ use crate::utils::{advance_with_random_transactions, eth_payload_attributes}; +use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope}; +use alloy_eips::Encodable2718; +use alloy_network::TxSignerSync; use alloy_provider::{Provider, ProviderBuilder}; -use rand::{rngs::StdRng, Rng, SeedableRng}; +use futures::future::JoinAll; +use rand::{rngs::StdRng, seq::IndexedRandom, Rng, SeedableRng}; use reth_chainspec::{ChainSpecBuilder, MAINNET}; -use reth_e2e_test_utils::{setup, setup_engine, transaction::TransactionTestContext}; +use reth_e2e_test_utils::{ + setup, setup_engine, setup_engine_with_connection, transaction::TransactionTestContext, + wallet::Wallet, +}; use reth_node_ethereum::EthereumNode; -use std::sync::Arc; +use reth_rpc_api::EthApiServer; +use std::{sync::Arc, time::Duration}; #[tokio::test] async fn can_sync() -> eyre::Result<()> { @@ -195,3 +203,94 @@ async fn test_reorg_through_backfill() -> eyre::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_tx_propagation() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap()) + .cancun_activated() + .prague_activated() + .build(), + ); + + // Setup wallet + let chain_id = chain_spec.chain().into(); + let wallet = Wallet::new(1).inner; + let mut nonce = 0; + let mut build_tx = || { + let mut tx = TxEip1559 { + chain_id, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + gas_limit: 100_000, + nonce, + ..Default::default() + }; + nonce += 1; + let signature = wallet.sign_transaction_sync(&mut tx).unwrap(); + TxEnvelope::Eip1559(tx.into_signed(signature)) + }; + + // Setup 10 nodes + let (mut nodes, _tasks, _) = setup_engine_with_connection::( + 10, + chain_spec.clone(), + false, + Default::default(), + eth_payload_attributes, + false, + ) + .await?; + + // Connect all nodes to the first one + let (first, rest) = nodes.split_at_mut(1); + for node in rest { + node.connect(&mut first[0]).await; + } + + // Advance all nodes for 1 block so that they don't consider themselves unsynced + let tx = build_tx(); + nodes[0].rpc.inject_tx(tx.encoded_2718().into()).await?; + let payload = nodes[0].advance_block().await?; + nodes[1..] + .iter_mut() + .map(|node| async { + node.submit_payload(payload.clone()).await.unwrap(); + node.sync_to(payload.block().hash()).await.unwrap(); + }) + .collect::>() + .await; + + // Build and send transaction to first node + let tx = build_tx(); + let tx_hash = *tx.tx_hash(); + let _ = nodes[0].rpc.inject_tx(tx.encoded_2718().into()).await?; + + tokio::time::sleep(Duration::from_millis(100)).await; + + // Assert that all nodes have the transaction + for (i, node) in nodes.iter().enumerate() { + assert!( + node.rpc.inner.eth_api().transaction_by_hash(tx_hash).await?.is_some(), + "Node {i} should have the transaction" + ); + } + + // Build and send one more transaction to a random node + let tx = build_tx(); + let tx_hash = *tx.tx_hash(); + let _ = nodes.choose(&mut rand::rng()).unwrap().rpc.inject_tx(tx.encoded_2718().into()).await?; + + tokio::time::sleep(Duration::from_millis(100)).await; + + // Assert that all nodes have the transaction + for node in nodes { + assert!(node.rpc.inner.eth_api().transaction_by_hash(tx_hash).await?.is_some()); + } + + Ok(()) +} diff --git a/crates/ethereum/node/tests/e2e/prestate.rs b/crates/ethereum/node/tests/e2e/prestate.rs new file mode 100644 index 0000000000..6c66f09bf7 --- /dev/null +++ b/crates/ethereum/node/tests/e2e/prestate.rs @@ -0,0 +1,132 @@ +use alloy_eips::BlockId; +use alloy_genesis::{Genesis, GenesisAccount}; +use alloy_primitives::address; +use alloy_provider::ext::DebugApi; +use alloy_rpc_types_eth::{Transaction, TransactionRequest}; +use alloy_rpc_types_trace::geth::{ + AccountState, GethDebugTracingOptions, PreStateConfig, PreStateFrame, +}; +use eyre::{eyre, Result}; +use reth_chainspec::{ChainSpecBuilder, MAINNET}; +use reth_node_builder::{NodeBuilder, NodeHandle}; +use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig}; +use reth_node_ethereum::EthereumNode; +use reth_rpc_server_types::RpcModuleSelection; +use reth_tasks::TaskManager; +use serde::Deserialize; +use std::sync::Arc; + +const PRESTATE_SNAPSHOT: &str = + include_str!("../../../../../testing/prestate/tx-selfdestruct-prestate.json"); + +/// Replays the selfdestruct transaction via `debug_traceCall` and ensures Reth's prestate matches +/// Geth's captured snapshot. +// +#[tokio::test] +async fn debug_trace_call_matches_geth_prestate_snapshot() -> Result<()> { + reth_tracing::init_test_tracing(); + + let mut genesis: Genesis = MAINNET.genesis().clone(); + genesis.coinbase = address!("0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5"); + + let exec = TaskManager::current(); + let exec = exec.executor(); + + let expected_frame = expected_snapshot_frame()?; + let prestate_mode = match &expected_frame { + PreStateFrame::Default(mode) => mode.clone(), + _ => return Err(eyre!("snapshot must contain default prestate frame")), + }; + + genesis.alloc.extend( + prestate_mode + .0 + .clone() + .into_iter() + .map(|(addr, state)| (addr, account_state_to_genesis(state))), + ); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(genesis) + .cancun_activated() + .prague_activated() + .build(), + ); + + let node_config = NodeConfig::test().with_chain(chain_spec).with_rpc( + RpcServerArgs::default() + .with_unused_ports() + .with_http() + .with_http_api(RpcModuleSelection::all_modules().into()), + ); + + let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config) + .testing_node(exec) + .node(EthereumNode::default()) + .launch() + .await?; + + let provider = node.rpc_server_handle().eth_http_provider().unwrap(); + + // + let tx = r#"{ + "type": "0x2", + "chainId": "0x1", + "nonce": "0x39af8", + "gas": "0x249f0", + "maxFeePerGas": "0xc6432e2d7", + "maxPriorityFeePerGas": "0x68889c2b", + "to": "0xc77ad0a71008d7094a62cfbd250a2eb2afdf2776", + "value": "0x0", + "accessList": [], + "input": "0xf3fef3a3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000f6b64", + "r": "0x40ab901a8262d5e6fe9b6513996cd5df412526580cab7410c13acc9dd9f6ec93", + "s": "0x6b76354c8cb1c1d6dbebfd555be9053170f02a648c4b36740e3fd7c6e9499572", + "yParity": "0x1", + "v": "0x1", + "hash": "0x391f4b6a382d3bcc3120adc2ea8c62003e604e487d97281129156fd284a1a89d", + "blockHash": "0xf9b77bcf8c69544304dff34129f3bdc71f00fdf766c1522ed6ac1382726ead82", + "blockNumber": "0x1294fd2", + "transactionIndex": "0x3a", + "from": "0xa7fb5ca286fc3fd67525629048a4de3ba24cba2e", + "gasPrice": "0x7c5bcc0e0" + }"#; + let tx = serde_json::from_str::(tx).unwrap(); + let request = TransactionRequest::from_recovered_transaction(tx.into_recovered()); + + let trace: PreStateFrame = provider + .debug_trace_call_prestate( + request, + BlockId::latest(), + GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()).into(), + ) + .await?; + + similar_asserts::assert_eq!(trace, expected_frame); + + Ok(()) +} + +fn expected_snapshot_frame() -> Result { + #[derive(Deserialize)] + struct Snapshot { + result: serde_json::Value, + } + + let snapshot: Snapshot = serde_json::from_str(PRESTATE_SNAPSHOT)?; + Ok(serde_json::from_value(snapshot.result)?) +} + +fn account_state_to_genesis(value: AccountState) -> GenesisAccount { + let balance = value.balance.unwrap_or_default(); + let code = value.code.filter(|code| !code.is_empty()); + let storage = (!value.storage.is_empty()).then_some(value.storage); + + GenesisAccount::default() + .with_balance(balance) + .with_nonce(value.nonce) + .with_code(code) + .with_storage(storage) +} diff --git a/crates/ethereum/node/tests/e2e/rpc.rs b/crates/ethereum/node/tests/e2e/rpc.rs index f040f44dfd..c149580ca6 100644 --- a/crates/ethereum/node/tests/e2e/rpc.rs +++ b/crates/ethereum/node/tests/e2e/rpc.rs @@ -1,5 +1,6 @@ use crate::utils::eth_payload_attributes; use alloy_eips::{eip2718::Encodable2718, eip7910::EthConfig}; +use alloy_genesis::Genesis; use alloy_primitives::{Address, B256, U256}; use alloy_provider::{network::EthereumWallet, Provider, ProviderBuilder, SendableTx}; use alloy_rpc_types_beacon::relay::{ @@ -11,8 +12,16 @@ use alloy_rpc_types_eth::TransactionRequest; use rand::{rngs::StdRng, Rng, SeedableRng}; use reth_chainspec::{ChainSpecBuilder, EthChainSpec, MAINNET}; use reth_e2e_test_utils::setup_engine; +use reth_network::types::NatResolver; +use reth_node_builder::{NodeBuilder, NodeHandle}; +use reth_node_core::{ + args::{NetworkArgs, RpcServerArgs}, + node_config::NodeConfig, +}; use reth_node_ethereum::EthereumNode; use reth_payload_primitives::BuiltPayload; +use reth_rpc_api::servers::AdminApiServer; +use reth_tasks::TaskManager; use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -329,3 +338,41 @@ async fn test_eth_config() -> eyre::Result<()> { Ok(()) } + +// +#[tokio::test] +async fn test_admin_external_ip() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let exec = TaskManager::current(); + let exec = exec.executor(); + + // Chain spec with test allocs + let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap(); + let chain_spec = + Arc::new(ChainSpecBuilder::default().chain(MAINNET.chain).genesis(genesis).build()); + + let external_ip = "10.64.128.71".parse().unwrap(); + // Node setup + let node_config = NodeConfig::test() + .with_chain(chain_spec) + .with_network( + NetworkArgs::default().with_nat_resolver(NatResolver::ExternalIp(external_ip)), + ) + .with_unused_ports() + .with_rpc(RpcServerArgs::default().with_unused_ports().with_http()); + + let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config) + .testing_node(exec) + .node(EthereumNode::default()) + .launch() + .await?; + + let api = node.add_ons_handle.admin_api(); + + let info = api.node_info().await.unwrap(); + + assert_eq!(info.ip, external_ip); + + Ok(()) +} diff --git a/crates/ethereum/primitives/src/transaction.rs b/crates/ethereum/primitives/src/transaction.rs index f2ec4ad9cd..28782c2ac6 100644 --- a/crates/ethereum/primitives/src/transaction.rs +++ b/crates/ethereum/primitives/src/transaction.rs @@ -8,7 +8,7 @@ use alloy_consensus::{ TxLegacy, TxType, Typed2718, }; use alloy_eips::{ - eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, + eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718}, eip2930::AccessList, eip7702::SignedAuthorization, }; @@ -664,6 +664,12 @@ impl TxHashRef for TransactionSigned { } } +impl IsTyped2718 for TransactionSigned { + fn is_type(type_id: u8) -> bool { + ::is_type(type_id) + } +} + impl SignedTransaction for TransactionSigned {} #[cfg(test)] diff --git a/crates/net/banlist/Cargo.toml b/crates/net/banlist/Cargo.toml index 7afec48d48..f5f885da24 100644 --- a/crates/net/banlist/Cargo.toml +++ b/crates/net/banlist/Cargo.toml @@ -14,3 +14,6 @@ workspace = true [dependencies] # ethereum alloy-primitives.workspace = true + +# networking +ipnet.workspace = true diff --git a/crates/net/banlist/src/lib.rs b/crates/net/banlist/src/lib.rs index 31b779bc8d..402041ed2f 100644 --- a/crates/net/banlist/src/lib.rs +++ b/crates/net/banlist/src/lib.rs @@ -10,7 +10,7 @@ type PeerId = alloy_primitives::B512; -use std::{collections::HashMap, net::IpAddr, time::Instant}; +use std::{collections::HashMap, net::IpAddr, str::FromStr, time::Instant}; /// Determines whether or not the IP is globally routable. /// Should be replaced with [`IpAddr::is_global`](std::net::IpAddr::is_global) once it is stable. @@ -215,3 +215,161 @@ mod tests { assert!(!banlist.is_banned_ip(&ip)); } } + +/// IP filter for restricting network communication to specific IP ranges using CIDR notation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IpFilter { + /// List of allowed IP networks in CIDR notation. + /// If empty, all IPs are allowed. + allowed_networks: Vec, +} + +impl IpFilter { + /// Creates a new IP filter with the given CIDR networks. + /// + /// If the list is empty, all IPs will be allowed. + pub const fn new(allowed_networks: Vec) -> Self { + Self { allowed_networks } + } + + /// Creates an IP filter from a comma-separated list of CIDR networks. + /// + /// # Errors + /// + /// Returns an error if any of the CIDR strings cannot be parsed. + pub fn from_cidr_string(cidrs: &str) -> Result { + if cidrs.is_empty() { + return Ok(Self::allow_all()) + } + + let networks = cidrs + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(ipnet::IpNet::from_str) + .collect::, _>>()?; + + Ok(Self::new(networks)) + } + + /// Creates a filter that allows all IPs. + pub const fn allow_all() -> Self { + Self { allowed_networks: Vec::new() } + } + + /// Checks if the given IP address is allowed by this filter. + /// + /// Returns `true` if the filter is empty (allows all) or if the IP is within + /// any of the allowed networks. + pub fn is_allowed(&self, ip: &IpAddr) -> bool { + // If no restrictions are set, allow all IPs + if self.allowed_networks.is_empty() { + return true + } + + // Check if the IP is within any of the allowed networks + self.allowed_networks.iter().any(|net| net.contains(ip)) + } + + /// Returns `true` if this filter has restrictions (i.e., not allowing all IPs). + pub const fn has_restrictions(&self) -> bool { + !self.allowed_networks.is_empty() + } + + /// Returns the list of allowed networks. + pub fn allowed_networks(&self) -> &[ipnet::IpNet] { + &self.allowed_networks + } +} + +impl Default for IpFilter { + fn default() -> Self { + Self::allow_all() + } +} + +#[cfg(test)] +mod ip_filter_tests { + use super::*; + + #[test] + fn test_allow_all_filter() { + let filter = IpFilter::allow_all(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1]))); + assert!(filter.is_allowed(&IpAddr::from([8, 8, 8, 8]))); + assert!(!filter.has_restrictions()); + } + + #[test] + fn test_single_network_filter() { + let filter = IpFilter::from_cidr_string("192.168.0.0/16").unwrap(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 255, 255]))); + assert!(!filter.is_allowed(&IpAddr::from([192, 169, 1, 1]))); + assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1]))); + assert!(filter.has_restrictions()); + } + + #[test] + fn test_multiple_networks_filter() { + let filter = IpFilter::from_cidr_string("192.168.0.0/16,10.0.0.0/8").unwrap(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + assert!(filter.is_allowed(&IpAddr::from([10, 5, 10, 20]))); + assert!(filter.is_allowed(&IpAddr::from([10, 255, 255, 255]))); + assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1]))); + assert!(!filter.is_allowed(&IpAddr::from([8, 8, 8, 8]))); + } + + #[test] + fn test_ipv6_filter() { + let filter = IpFilter::from_cidr_string("2001:db8::/32").unwrap(); + let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap(); + let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap(); + + assert!(filter.is_allowed(&ipv6_in_range)); + assert!(!filter.is_allowed(&ipv6_out_range)); + } + + #[test] + fn test_mixed_ipv4_ipv6_filter() { + let filter = IpFilter::from_cidr_string("192.168.0.0/16,2001:db8::/32").unwrap(); + + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap(); + assert!(filter.is_allowed(&ipv6_in_range)); + + assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1]))); + let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap(); + assert!(!filter.is_allowed(&ipv6_out_range)); + } + + #[test] + fn test_empty_string() { + let filter = IpFilter::from_cidr_string("").unwrap(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + assert!(!filter.has_restrictions()); + } + + #[test] + fn test_invalid_cidr() { + assert!(IpFilter::from_cidr_string("invalid").is_err()); + assert!(IpFilter::from_cidr_string("192.168.0.0/33").is_err()); + assert!(IpFilter::from_cidr_string("192.168.0.0,10.0.0.0").is_err()); + } + + #[test] + fn test_whitespace_handling() { + let filter = IpFilter::from_cidr_string(" 192.168.0.0/16 , 10.0.0.0/8 ").unwrap(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1]))); + assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1]))); + assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1]))); + } + + #[test] + fn test_single_ip_as_cidr() { + let filter = IpFilter::from_cidr_string("192.168.1.100/32").unwrap(); + assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 100]))); + assert!(!filter.is_allowed(&IpAddr::from([192, 168, 1, 101]))); + } +} diff --git a/crates/net/discv4/src/config.rs b/crates/net/discv4/src/config.rs index ebd1650298..d2fccc7b65 100644 --- a/crates/net/discv4/src/config.rs +++ b/crates/net/discv4/src/config.rs @@ -101,7 +101,7 @@ impl Discv4Config { pub fn resolve_external_ip_interval(&self) -> Option { let resolver = self.external_ip_resolver?; let interval = self.resolve_external_ip_interval?; - Some(ResolveNatInterval::interval(resolver, interval)) + Some(ResolveNatInterval::interval_at(resolver, tokio::time::Instant::now(), interval)) } } @@ -336,4 +336,25 @@ mod tests { .enable_lookup(true) .build(); } + + #[tokio::test] + async fn test_resolve_external_ip_interval_uses_interval_at() { + use reth_net_nat::NatResolver; + use std::net::{IpAddr, Ipv4Addr}; + + let ip_addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)); + + // Create a config with external IP resolver + let mut builder = Discv4Config::builder(); + builder.external_ip_resolver(Some(NatResolver::ExternalIp(ip_addr))); + builder.resolve_external_ip_interval(Some(Duration::from_secs(60 * 5))); + let config = builder.build(); + + // Get the ResolveNatInterval + let mut interval = config.resolve_external_ip_interval().expect("should have interval"); + + // Test that first tick returns immediately (interval_at behavior) + let ip = interval.tick().await; + assert_eq!(ip, Some(ip_addr)); + } } diff --git a/crates/net/discv5/src/lib.rs b/crates/net/discv5/src/lib.rs index 912a4a0846..030599a4cd 100644 --- a/crates/net/discv5/src/lib.rs +++ b/crates/net/discv5/src/lib.rs @@ -75,6 +75,11 @@ pub struct Discv5 { discovered_peer_filter: MustNotIncludeKeys, /// Metrics for underlying [`discv5::Discv5`] node and filtered discovered peers. metrics: Discv5Metrics, + /// Returns the _local_ [`NodeRecord`] this service was started with. + // Note: we must track this separately because the `discv5::Discv5` does not necessarily + // provide this via it's [`local_enr`](discv5::Discv5::local_ner()) This is intended for + // obtaining the port this service was launched at + local_node_record: NodeRecord, } impl Discv5 { @@ -155,22 +160,29 @@ impl Discv5 { enr.try_into().ok() } + /// Returns the local [`Enr`] of the service. + pub fn local_enr(&self) -> Enr { + self.discv5.local_enr() + } + + /// The port the discv5 service is listening on. + pub const fn local_port(&self) -> u16 { + self.local_node_record.udp_port + } + /// Spawns [`discv5::Discv5`]. Returns [`discv5::Discv5`] handle in reth compatible wrapper type /// [`Discv5`], a receiver of [`discv5::Event`]s from the underlying node, and the local /// [`Enr`](discv5::Enr) converted into the reth compatible [`NodeRecord`] type. pub async fn start( sk: &SecretKey, discv5_config: Config, - ) -> Result<(Self, mpsc::Receiver, NodeRecord), Error> { + ) -> Result<(Self, mpsc::Receiver), Error> { // // 1. make local enr from listen config // - let (enr, bc_enr, fork_key, rlpx_ip_mode) = build_local_enr(sk, &discv5_config); + let (enr, local_node_record, fork_key, rlpx_ip_mode) = build_local_enr(sk, &discv5_config); - trace!(target: "net::discv5", - ?enr, - "local ENR" - ); + trace!(target: "net::discv5", ?enr, "local ENR"); // // 2. start discv5 @@ -217,9 +229,15 @@ impl Discv5 { ); Ok(( - Self { discv5, rlpx_ip_mode, fork_key, discovered_peer_filter, metrics }, + Self { + discv5, + rlpx_ip_mode, + fork_key, + discovered_peer_filter, + metrics, + local_node_record, + }, discv5_updates, - bc_enr, )) } @@ -699,12 +717,14 @@ mod test { fork_key: None, discovered_peer_filter: MustNotIncludeKeys::default(), metrics: Discv5Metrics::default(), + local_node_record: NodeRecord::new( + (Ipv4Addr::LOCALHOST, 30303).into(), + PeerId::random(), + ), } } - async fn start_discovery_node( - udp_port_discv5: u16, - ) -> (Discv5, mpsc::Receiver, NodeRecord) { + async fn start_discovery_node(udp_port_discv5: u16) -> (Discv5, mpsc::Receiver) { let secret_key = SecretKey::new(&mut thread_rng()); let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap(); @@ -725,11 +745,11 @@ mod test { // rig test // rig node_1 - let (node_1, mut stream_1, _) = start_discovery_node(30344).await; + let (node_1, mut stream_1) = start_discovery_node(30344).await; let node_1_enr = node_1.with_discv5(|discv5| discv5.local_enr()); // rig node_2 - let (node_2, mut stream_2, _) = start_discovery_node(30355).await; + let (node_2, mut stream_2) = start_discovery_node(30355).await; let node_2_enr = node_2.with_discv5(|discv5| discv5.local_enr()); trace!(target: "net::discv5::test", diff --git a/crates/net/dns/src/lib.rs b/crates/net/dns/src/lib.rs index df597a755e..a7e371fcc5 100644 --- a/crates/net/dns/src/lib.rs +++ b/crates/net/dns/src/lib.rs @@ -583,8 +583,14 @@ mod tests { // await recheck timeout tokio::time::sleep(config.recheck_interval).await; + let mut new_root = root.clone(); + new_root.sequence_number = new_root.sequence_number.saturating_add(1); + new_root.enr_root = "NEW_ENR_ROOT".to_string(); + new_root.sign(&secret_key).unwrap(); + resolver.insert(link.domain.clone(), new_root.to_string()); + let enr = Enr::empty(&secret_key).unwrap(); - resolver.insert(format!("{}.{}", root.enr_root.clone(), link.domain), enr.to_base64()); + resolver.insert(format!("{}.{}", new_root.enr_root.clone(), link.domain), enr.to_base64()); let event = poll_fn(|cx| service.poll(cx)).await; diff --git a/crates/net/dns/src/sync.rs b/crates/net/dns/src/sync.rs index 5b9453959d..0534abb6a1 100644 --- a/crates/net/dns/src/sync.rs +++ b/crates/net/dns/src/sync.rs @@ -102,29 +102,30 @@ impl SyncTree { /// Updates the root and returns what changed pub(crate) fn update_root(&mut self, root: TreeRootEntry) { - let enr = root.enr_root == self.root.enr_root; - let link = root.link_root == self.root.link_root; + let enr_unchanged = root.enr_root == self.root.enr_root; + let link_unchanged = root.link_root == self.root.link_root; self.root = root; self.root_updated = Instant::now(); - let state = match (enr, link) { - (true, true) => { - self.unresolved_nodes.clear(); - self.unresolved_links.clear(); - SyncState::Pending - } - (true, _) => { + let state = match (enr_unchanged, link_unchanged) { + // both unchanged — no resync needed + (true, true) => return, + // only ENR changed + (false, true) => { self.unresolved_nodes.clear(); SyncState::Enr } - (_, true) => { + // only LINK changed + (true, false) => { self.unresolved_links.clear(); SyncState::Link } - _ => { - // unchanged - return + // both changed + (false, false) => { + self.unresolved_nodes.clear(); + self.unresolved_links.clear(); + SyncState::Pending } }; self.sync_state = state; @@ -132,6 +133,7 @@ impl SyncTree { } /// The action to perform by the service +#[derive(Debug)] pub(crate) enum SyncAction { UpdateRoot, Enr(String), @@ -160,3 +162,97 @@ impl ResolveKind { matches!(self, Self::Link) } } + +#[cfg(test)] +mod tests { + use super::*; + use enr::EnrKey; + use secp256k1::rand::thread_rng; + + fn base_root() -> TreeRootEntry { + // taken from existing tests to ensure valid formatting + let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE"; + s.parse::().unwrap() + } + + fn make_tree() -> SyncTree { + let secret_key = SecretKey::new(&mut thread_rng()); + let link = + LinkEntry { domain: "nodes.example.org".to_string(), pubkey: secret_key.public() }; + SyncTree::new(base_root(), link) + } + + fn advance_to_active(tree: &mut SyncTree) { + // Move Pending -> (emit Link) -> Enr, then Enr -> (emit Enr) -> Active + let now = Instant::now(); + let timeout = Duration::from_secs(60 * 60 * 24); + let _ = tree.poll(now, timeout); + let _ = tree.poll(now, timeout); + } + + #[test] + fn update_root_unchanged_no_action_from_active() { + let mut tree = make_tree(); + let now = Instant::now(); + let timeout = Duration::from_secs(60 * 60 * 24); + advance_to_active(&mut tree); + + // same root -> no resync + let same = base_root(); + tree.update_root(same); + assert!(tree.poll(now, timeout).is_none()); + } + + #[test] + fn update_root_only_enr_changed_triggers_enr() { + let mut tree = make_tree(); + advance_to_active(&mut tree); + let mut new_root = base_root(); + new_root.enr_root = "NEW_ENR_ROOT".to_string(); + let now = Instant::now(); + let timeout = Duration::from_secs(60 * 60 * 24); + + tree.update_root(new_root.clone()); + match tree.poll(now, timeout) { + Some(SyncAction::Enr(hash)) => assert_eq!(hash, new_root.enr_root), + other => panic!("expected Enr action, got {:?}", other), + } + } + + #[test] + fn update_root_only_link_changed_triggers_link() { + let mut tree = make_tree(); + advance_to_active(&mut tree); + let mut new_root = base_root(); + new_root.link_root = "NEW_LINK_ROOT".to_string(); + let now = Instant::now(); + let timeout = Duration::from_secs(60 * 60 * 24); + + tree.update_root(new_root.clone()); + match tree.poll(now, timeout) { + Some(SyncAction::Link(hash)) => assert_eq!(hash, new_root.link_root), + other => panic!("expected Link action, got {:?}", other), + } + } + + #[test] + fn update_root_both_changed_triggers_link_then_enr() { + let mut tree = make_tree(); + advance_to_active(&mut tree); + let mut new_root = base_root(); + new_root.enr_root = "NEW_ENR_ROOT".to_string(); + new_root.link_root = "NEW_LINK_ROOT".to_string(); + let now = Instant::now(); + let timeout = Duration::from_secs(60 * 60 * 24); + + tree.update_root(new_root.clone()); + match tree.poll(now, timeout) { + Some(SyncAction::Link(hash)) => assert_eq!(hash, new_root.link_root), + other => panic!("expected first Link action, got {:?}", other), + } + match tree.poll(now, timeout) { + Some(SyncAction::Enr(hash)) => assert_eq!(hash, new_root.enr_root), + other => panic!("expected second Enr action, got {:?}", other), + } + } +} diff --git a/crates/net/eth-wire-types/src/capability.rs b/crates/net/eth-wire-types/src/capability.rs index 3f39bed606..f7cd00671f 100644 --- a/crates/net/eth-wire-types/src/capability.rs +++ b/crates/net/eth-wire-types/src/capability.rs @@ -158,6 +158,15 @@ pub struct Capabilities { } impl Capabilities { + /// Create a new instance from the given vec. + pub fn new(value: Vec) -> Self { + Self { + eth_66: value.iter().any(Capability::is_eth_v66), + eth_67: value.iter().any(Capability::is_eth_v67), + eth_68: value.iter().any(Capability::is_eth_v68), + inner: value, + } + } /// Returns all capabilities. #[inline] pub fn capabilities(&self) -> &[Capability] { @@ -197,12 +206,7 @@ impl Capabilities { impl From> for Capabilities { fn from(value: Vec) -> Self { - Self { - eth_66: value.iter().any(Capability::is_eth_v66), - eth_67: value.iter().any(Capability::is_eth_v67), - eth_68: value.iter().any(Capability::is_eth_v68), - inner: value, - } + Self::new(value) } } diff --git a/crates/net/network-types/src/peers/config.rs b/crates/net/network-types/src/peers/config.rs index 1fe685b0e8..29e4499b40 100644 --- a/crates/net/network-types/src/peers/config.rs +++ b/crates/net/network-types/src/peers/config.rs @@ -7,7 +7,7 @@ use std::{ time::Duration, }; -use reth_net_banlist::BanList; +use reth_net_banlist::{BanList, IpFilter}; use reth_network_peers::{NodeRecord, TrustedPeer}; use tracing::info; @@ -166,6 +166,12 @@ pub struct PeersConfig { /// This acts as an IP based rate limit. #[cfg_attr(feature = "serde", serde(default, with = "humantime_serde"))] pub incoming_ip_throttle_duration: Duration, + /// IP address filter for restricting network connections to specific IP ranges. + /// + /// Similar to geth's --netrestrict flag. If configured, only connections to/from + /// IPs within the specified CIDR ranges will be allowed. + #[cfg_attr(feature = "serde", serde(skip))] + pub ip_filter: IpFilter, } impl Default for PeersConfig { @@ -184,6 +190,7 @@ impl Default for PeersConfig { basic_nodes: Default::default(), max_backoff_count: 5, incoming_ip_throttle_duration: INBOUND_IP_THROTTLE_DURATION, + ip_filter: IpFilter::default(), } } } @@ -301,6 +308,12 @@ impl PeersConfig { Ok(self.with_basic_nodes(nodes)) } + /// Configure the IP filter for restricting network connections to specific IP ranges. + pub fn with_ip_filter(mut self, ip_filter: IpFilter) -> Self { + self.ip_filter = ip_filter; + self + } + /// Returns settings for testing #[cfg(any(test, feature = "test-utils"))] pub fn test() -> Self { diff --git a/crates/net/network/src/builder.rs b/crates/net/network/src/builder.rs index 3f36b1bdc8..97a342a869 100644 --- a/crates/net/network/src/builder.rs +++ b/crates/net/network/src/builder.rs @@ -1,7 +1,5 @@ //! Builder support for configuring the entire setup. -use std::fmt::Debug; - use crate::{ eth_requests::EthRequestHandler, transactions::{ @@ -77,15 +75,7 @@ impl NetworkBuilder { self, pool: Pool, transactions_manager_config: TransactionsManagerConfig, - ) -> NetworkBuilder< - TransactionsManager< - Pool, - N, - NetworkPolicies, - >, - Eth, - N, - > { + ) -> NetworkBuilder, Eth, N> { self.transactions_with_policy( pool, transactions_manager_config, @@ -94,19 +84,12 @@ impl NetworkBuilder { } /// Creates a new [`TransactionsManager`] and wires it to the network. - pub fn transactions_with_policy< - Pool: TransactionPool, - P: TransactionPropagationPolicy + Debug, - >( + pub fn transactions_with_policy( self, pool: Pool, transactions_manager_config: TransactionsManagerConfig, - propagation_policy: P, - ) -> NetworkBuilder< - TransactionsManager>, - Eth, - N, - > { + propagation_policy: impl TransactionPropagationPolicy, + ) -> NetworkBuilder, Eth, N> { let Self { mut network, request_handler, .. } = self; let (tx, rx) = mpsc::unbounded_channel(); network.set_transactions(tx); diff --git a/crates/net/network/src/config.rs b/crates/net/network/src/config.rs index c403bdcb55..047970aac0 100644 --- a/crates/net/network/src/config.rs +++ b/crates/net/network/src/config.rs @@ -6,7 +6,7 @@ use crate::{ transactions::TransactionsManagerConfig, NetworkHandle, NetworkManager, }; -use alloy_primitives::B256; +use alloy_eips::BlockNumHash; use reth_chainspec::{ChainSpecProvider, EthChainSpec, Hardforks}; use reth_discv4::{Discv4Config, Discv4ConfigBuilder, NatResolver, DEFAULT_DISCOVERY_ADDRESS}; use reth_discv5::NetworkStackId; @@ -94,9 +94,9 @@ pub struct NetworkConfig { /// This can be overridden to support custom handshake logic via the /// [`NetworkConfigBuilder`]. pub handshake: Arc, - /// List of block hashes to check for required blocks. + /// List of block number-hash pairs to check for required blocks. /// If non-empty, peers that don't have these blocks will be filtered out. - pub required_block_hashes: Vec, + pub required_block_hashes: Vec, } // === impl NetworkConfig === @@ -225,7 +225,7 @@ pub struct NetworkConfigBuilder { /// . handshake: Arc, /// List of block hashes to check for required blocks. - required_block_hashes: Vec, + required_block_hashes: Vec, /// Optional network id network_id: Option, } @@ -555,7 +555,7 @@ impl NetworkConfigBuilder { } /// Sets the required block hashes for peer filtering. - pub fn required_block_hashes(mut self, hashes: Vec) -> Self { + pub fn required_block_hashes(mut self, hashes: Vec) -> Self { self.required_block_hashes = hashes; self } diff --git a/crates/net/network/src/discovery.rs b/crates/net/network/src/discovery.rs index 9cc3a6249a..3dc409e00b 100644 --- a/crates/net/network/src/discovery.rs +++ b/crates/net/network/src/discovery.rs @@ -25,7 +25,7 @@ use std::{ }; use tokio::{sync::mpsc, task::JoinHandle}; use tokio_stream::{wrappers::ReceiverStream, Stream}; -use tracing::trace; +use tracing::{debug, trace}; /// Default max capacity for cache of discovered peers. /// @@ -95,12 +95,15 @@ impl Discovery { // spawn the service let discv4_service = discv4_service.spawn(); + debug!(target:"net", ?discovery_v4_addr, "started discovery v4"); + Ok((Some(discv4), Some(discv4_updates), Some(discv4_service))) }; let discv5_future = async { let Some(config) = discv5_config else { return Ok::<_, NetworkError>((None, None)) }; - let (discv5, discv5_updates, _local_enr_discv5) = Discv5::start(&sk, config).await?; + let (discv5, discv5_updates) = Discv5::start(&sk, config).await?; + debug!(target:"net", discovery_v5_enr=? discv5.local_enr(), "started discovery v5"); Ok((Some(discv5), Some(discv5_updates.into()))) }; diff --git a/crates/net/network/src/error.rs b/crates/net/network/src/error.rs index 96ba2ff85e..af3fbd1860 100644 --- a/crates/net/network/src/error.rs +++ b/crates/net/network/src/error.rs @@ -113,7 +113,22 @@ impl SessionError for EthStreamError { P2PHandshakeError::HelloNotInHandshake | P2PHandshakeError::NonHelloMessageInHandshake, )) => true, - Self::EthHandshakeError(err) => !matches!(err, EthHandshakeError::NoResponse), + Self::EthHandshakeError(err) => { + #[allow(clippy::match_same_arms)] + match err { + EthHandshakeError::NoResponse => { + // this happens when the conn simply stalled + false + } + EthHandshakeError::InvalidFork(_) => { + // this can occur when the remote or our node is running an outdated client, + // we shouldn't treat this as fatal, because the node can come back online + // with an updated version any time + false + } + _ => true, + } + } _ => false, } } @@ -144,7 +159,22 @@ impl SessionError for EthStreamError { P2PStreamError::MismatchedProtocolVersion { .. } ) } - Self::EthHandshakeError(err) => !matches!(err, EthHandshakeError::NoResponse), + Self::EthHandshakeError(err) => { + #[allow(clippy::match_same_arms)] + match err { + EthHandshakeError::NoResponse => { + // this happens when the conn simply stalled + false + } + EthHandshakeError::InvalidFork(_) => { + // this can occur when the remote or our node is running an outdated client, + // we shouldn't treat this as fatal, because the node can come back online + // with an updated version any time + false + } + _ => true, + } + } _ => false, } } @@ -196,6 +226,11 @@ impl SessionError for EthStreamError { P2PStreamError::PingerError(_) | P2PStreamError::Snap(_), ) => Some(BackoffKind::Medium), + Self::EthHandshakeError(EthHandshakeError::InvalidFork(_)) => { + // the remote can come back online after updating client version, so we can back off + // for a bit + Some(BackoffKind::Medium) + } _ => None, } } diff --git a/crates/net/network/src/fetch/mod.rs b/crates/net/network/src/fetch/mod.rs index 9d603863a9..399f91d12a 100644 --- a/crates/net/network/src/fetch/mod.rs +++ b/crates/net/network/src/fetch/mod.rs @@ -139,8 +139,9 @@ impl StateFetcher { /// Returns the _next_ idle peer that's ready to accept a request, /// prioritizing those with the lowest timeout/latency and those that recently responded with - /// adequate data. - fn next_best_peer(&self) -> Option { + /// adequate data. Additionally, if full blocks are required this prioritizes peers that have + /// full history available + fn next_best_peer(&self, requirement: BestPeerRequirements) -> Option { let mut idle = self.peers.iter().filter(|(_, peer)| peer.state.is_idle()); let mut best_peer = idle.next()?; @@ -152,7 +153,13 @@ impl StateFetcher { continue } - // replace best peer if this peer has better rtt + // replace best peer if this peer meets the requirements better + if maybe_better.1.is_better(best_peer.1, &requirement) { + best_peer = maybe_better; + continue + } + + // replace best peer if this peer has better rtt and both have same range quality if maybe_better.1.timeout() < best_peer.1.timeout() && !maybe_better.1.last_response_likely_bad { @@ -170,9 +177,13 @@ impl StateFetcher { return PollAction::NoRequests } - let Some(peer_id) = self.next_best_peer() else { return PollAction::NoPeersAvailable }; - let request = self.queued_requests.pop_front().expect("not empty"); + let Some(peer_id) = self.next_best_peer(request.best_peer_requirements()) else { + // need to put back the the request + self.queued_requests.push_front(request); + return PollAction::NoPeersAvailable + }; + let request = self.prepare_block_request(peer_id, request); PollAction::Ready(FetchAction::BlockRequest { peer_id, request }) @@ -358,7 +369,6 @@ struct Peer { /// lowest timeout. last_response_likely_bad: bool, /// Tracks the range info for the peer. - #[allow(dead_code)] range_info: Option, } @@ -366,6 +376,74 @@ impl Peer { fn timeout(&self) -> u64 { self.timeout.load(Ordering::Relaxed) } + + /// Returns the earliest block number available from the peer. + fn earliest(&self) -> u64 { + self.range_info.as_ref().map_or(0, |info| info.earliest()) + } + + /// Returns true if the peer has the full history available. + fn has_full_history(&self) -> bool { + self.earliest() == 0 + } + + fn range(&self) -> Option> { + self.range_info.as_ref().map(|info| info.range()) + } + + /// Returns true if this peer has a better range than the other peer for serving the requested + /// range. + /// + /// A peer has a "better range" if: + /// 1. It can fully cover the requested range while the other cannot + /// 2. None can fully cover the range, but this peer has lower start value + /// 3. If a peer doesnt announce a range we assume it has full history, but check the other's + /// range and treat that as better if it can cover the range + fn has_better_range(&self, other: &Self, range: RangeInclusive) -> bool { + let self_range = self.range(); + let other_range = other.range(); + + match (self_range, other_range) { + (Some(self_r), Some(other_r)) => { + // Check if each peer can fully cover the requested range + let self_covers = self_r.contains(range.start()) && self_r.contains(range.end()); + let other_covers = other_r.contains(range.start()) && other_r.contains(range.end()); + + #[allow(clippy::match_same_arms)] + match (self_covers, other_covers) { + (true, false) => true, // Only self covers the range + (false, true) => false, // Only other covers the range + (true, true) => false, // Both cover + (false, false) => { + // neither covers - prefer if peer has lower (better) start range + self_r.start() < other_r.start() + } + } + } + (Some(self_r), None) => { + // Self has range info, other doesn't (treated as full history with unknown latest) + // Self is better only if it covers the range + self_r.contains(range.start()) && self_r.contains(range.end()) + } + (None, Some(other_r)) => { + // Self has no range info (full history), other has range info + // Self is better only if other doesn't cover the range + !(other_r.contains(range.start()) && other_r.contains(range.end())) + } + (None, None) => false, // Neither has range info - no one is better + } + } + + /// Returns true if this peer is better than the other peer based on the given requirements. + fn is_better(&self, other: &Self, requirement: &BestPeerRequirements) -> bool { + match requirement { + BestPeerRequirements::None => false, + BestPeerRequirements::FullBlockRange(range) => { + self.has_better_range(other, range.clone()) + } + BestPeerRequirements::FullBlock => self.has_full_history() && !other.has_full_history(), + } + } } /// Tracks the state of an individual peer @@ -427,7 +505,6 @@ pub(crate) enum DownloadRequest { request: Vec, response: oneshot::Sender>>, priority: Priority, - #[allow(dead_code)] range_hint: Option>, }, } @@ -456,6 +533,20 @@ impl DownloadRequest { const fn is_normal_priority(&self) -> bool { self.get_priority().is_normal() } + + /// Returns the best peer requirements for this request. + fn best_peer_requirements(&self) -> BestPeerRequirements { + match self { + Self::GetBlockHeaders { .. } => BestPeerRequirements::None, + Self::GetBlockBodies { range_hint, .. } => { + if let Some(range) = range_hint { + BestPeerRequirements::FullBlockRange(range.clone()) + } else { + BestPeerRequirements::FullBlock + } + } + } + } } /// An action the syncer can emit. @@ -480,6 +571,16 @@ pub(crate) enum BlockResponseOutcome { BadResponse(PeerId, ReputationChangeKind), } +/// Additional requirements for how to rank peers during selection. +enum BestPeerRequirements { + /// No additional requirements + None, + /// Peer must have this block range available. + FullBlockRange(RangeInclusive), + /// Peer must have full range. + FullBlock, +} + #[cfg(test)] mod tests { use super::*; @@ -536,17 +637,17 @@ mod tests { None, ); - let first_peer = fetcher.next_best_peer().unwrap(); + let first_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap(); assert!(first_peer == peer1 || first_peer == peer2); // Pending disconnect for first_peer fetcher.on_pending_disconnect(&first_peer); // first_peer now isn't idle, so we should get other peer - let second_peer = fetcher.next_best_peer().unwrap(); + let second_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap(); assert!(first_peer == peer1 || first_peer == peer2); assert_ne!(first_peer, second_peer); // without idle peers, returns None fetcher.on_pending_disconnect(&second_peer); - assert_eq!(fetcher.next_best_peer(), None); + assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), None); } #[tokio::test] @@ -588,13 +689,13 @@ mod tests { ); // Must always get peer1 (lowest timeout) - assert_eq!(fetcher.next_best_peer(), Some(peer1)); - assert_eq!(fetcher.next_best_peer(), Some(peer1)); + assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1)); + assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1)); // peer2's timeout changes below peer1's peer2_timeout.store(10, Ordering::Relaxed); // Then we get peer 2 always (now lowest) - assert_eq!(fetcher.next_best_peer(), Some(peer2)); - assert_eq!(fetcher.next_best_peer(), Some(peer2)); + assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2)); + assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2)); } #[tokio::test] @@ -684,4 +785,367 @@ mod tests { assert!(fetcher.peers[&peer_id].state.is_idle()); } + + #[test] + fn test_peer_is_better_none_requirement() { + let peer1 = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 100, B256::random())), + }; + + let peer2 = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 50, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(20)), + last_response_likely_bad: false, + range_info: None, + }; + + // With None requirement, is_better should always return false + assert!(!peer1.is_better(&peer2, &BestPeerRequirements::None)); + assert!(!peer2.is_better(&peer1, &BestPeerRequirements::None)); + } + + #[test] + fn test_peer_is_better_full_block_requirement() { + // Peer with full history (earliest = 0) + let peer_full = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 100, B256::random())), + }; + + // Peer without full history (earliest = 50) + let peer_partial = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(50, 100, B256::random())), + }; + + // Peer without range info (treated as full history) + let peer_no_range = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: None, + }; + + // Peer with full history is better than peer without + assert!(peer_full.is_better(&peer_partial, &BestPeerRequirements::FullBlock)); + assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlock)); + + // Peer without range info (full history) is better than partial + assert!(peer_no_range.is_better(&peer_partial, &BestPeerRequirements::FullBlock)); + assert!(!peer_partial.is_better(&peer_no_range, &BestPeerRequirements::FullBlock)); + + // Both have full history - no improvement + assert!(!peer_full.is_better(&peer_no_range, &BestPeerRequirements::FullBlock)); + assert!(!peer_no_range.is_better(&peer_full, &BestPeerRequirements::FullBlock)); + } + + #[test] + fn test_peer_is_better_full_block_range_requirement() { + let range = RangeInclusive::new(40, 60); + + // Peer that covers the requested range + let peer_covers = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 100, B256::random())), + }; + + // Peer that doesn't cover the range (earliest too high) + let peer_no_cover = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(70, 100, B256::random())), + }; + + // Peer that covers the requested range is better than one that doesn't + assert!(peer_covers + .is_better(&peer_no_cover, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!( + !peer_no_cover.is_better(&peer_covers, &BestPeerRequirements::FullBlockRange(range)) + ); + } + + #[test] + fn test_peer_is_better_both_cover_range() { + let range = RangeInclusive::new(30, 50); + + // Peer with full history that covers the range + let peer_full = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 50, B256::random())), + }; + + // Peer without full history that also covers the range + let peer_partial = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(30, 50, B256::random())), + }; + + // When both cover the range, prefer none + assert!(!peer_full + .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range))); + } + + #[test] + fn test_peer_is_better_lower_start() { + let range = RangeInclusive::new(30, 60); + + // Peer with full history that covers the range + let peer_full = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 50, B256::random())), + }; + + // Peer without full history that also covers the range + let peer_partial = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(30, 50, B256::random())), + }; + + // When both cover the range, prefer lower start value + assert!(peer_full + .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range))); + } + + #[test] + fn test_peer_is_better_neither_covers_range() { + let range = RangeInclusive::new(40, 60); + + // Peer with full history that doesn't cover the range (latest too low) + let peer_full = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 30, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(0, 30, B256::random())), + }; + + // Peer without full history that also doesn't cover the range + let peer_partial = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 30, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(10, 30, B256::random())), + }; + + // When neither covers the range, prefer full history + assert!(peer_full + .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range))); + } + + #[test] + fn test_peer_is_better_no_range_info() { + let range = RangeInclusive::new(40, 60); + + // Peer with range info + let peer_with_range = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(30, 100, B256::random())), + }; + + // Peer without range info + let peer_no_range = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: None, + }; + + // Peer without range info is not better (we prefer peers with known ranges) + assert!(!peer_no_range + .is_better(&peer_with_range, &BestPeerRequirements::FullBlockRange(range.clone()))); + + // Peer with range info is better than peer without + assert!( + peer_with_range.is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range)) + ); + } + + #[test] + fn test_peer_is_better_one_peer_no_range_covers() { + let range = RangeInclusive::new(40, 60); + + // Peer with range info that covers the requested range + let peer_with_range_covers = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(30, 100, B256::random())), + }; + + // Peer without range info (treated as full history with unknown latest) + let peer_no_range = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: None, + }; + + // Peer with range that covers is better than peer without range info + assert!(peer_with_range_covers + .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone()))); + + // Peer without range info is not better when other covers + assert!(!peer_no_range + .is_better(&peer_with_range_covers, &BestPeerRequirements::FullBlockRange(range))); + } + + #[test] + fn test_peer_is_better_one_peer_no_range_doesnt_cover() { + let range = RangeInclusive::new(40, 60); + + // Peer with range info that does NOT cover the requested range (too high) + let peer_with_range_no_cover = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(70, 100, B256::random())), + }; + + // Peer without range info (treated as full history) + let peer_no_range = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: None, + }; + + // Peer with range that doesn't cover is not better + assert!(!peer_with_range_no_cover + .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone()))); + + // Peer without range info (full history) is better when other doesn't cover + assert!(peer_no_range + .is_better(&peer_with_range_no_cover, &BestPeerRequirements::FullBlockRange(range))); + } + + #[test] + fn test_peer_is_better_edge_cases() { + // Test exact range boundaries + let range = RangeInclusive::new(50, 100); + + // Peer that exactly covers the range + let peer_exact = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(50, 100, B256::random())), + }; + + // Peer that's one block short at the start + let peer_short_start = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(51, 100, B256::random())), + }; + + // Peer that's one block short at the end + let peer_short_end = Peer { + state: PeerState::Idle, + best_hash: B256::random(), + best_number: 100, + capabilities: Arc::new(Capabilities::new(vec![])), + timeout: Arc::new(AtomicU64::new(10)), + last_response_likely_bad: false, + range_info: Some(BlockRangeInfo::new(50, 99, B256::random())), + }; + + // Exact coverage is better than short coverage + assert!(peer_exact + .is_better(&peer_short_start, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!(peer_exact + .is_better(&peer_short_end, &BestPeerRequirements::FullBlockRange(range.clone()))); + + // Short coverage is not better than exact coverage + assert!(!peer_short_start + .is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range.clone()))); + assert!( + !peer_short_end.is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range)) + ); + } } diff --git a/crates/net/network/src/lib.rs b/crates/net/network/src/lib.rs index a84168d384..ad63067a51 100644 --- a/crates/net/network/src/lib.rs +++ b/crates/net/network/src/lib.rs @@ -175,6 +175,7 @@ pub use reth_network_p2p as p2p; /// re-export types crates pub mod types { + pub use reth_discv4::NatResolver; pub use reth_eth_wire_types::*; pub use reth_network_types::*; } diff --git a/crates/net/network/src/network.rs b/crates/net/network/src/network.rs index cfc3d56cb2..b9ae7042cf 100644 --- a/crates/net/network/src/network.rs +++ b/crates/net/network/src/network.rs @@ -232,9 +232,25 @@ impl PeersInfo for NetworkHandle { fn local_node_record(&self) -> NodeRecord { if let Some(discv4) = &self.inner.discv4 { + // Note: the discv4 services uses the same `nat` so we can directly return the node + // record here discv4.node_record() - } else if let Some(record) = self.inner.discv5.as_ref().and_then(|d| d.node_record()) { - record + } else if let Some(discv5) = self.inner.discv5.as_ref() { + // for disv5 we must check if we have an external ip configured + if let Some(external) = self.inner.nat.and_then(|nat| nat.as_external_ip()) { + NodeRecord::new((external, discv5.local_port()).into(), *self.peer_id()) + } else { + // use the node record that discv5 tracks or use localhost + self.inner.discv5.as_ref().and_then(|d| d.node_record()).unwrap_or_else(|| { + NodeRecord::new( + (std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), discv5.local_port()) + .into(), + *self.peer_id(), + ) + }) + } + // also use the tcp port + .with_tcp_port(self.inner.listener_address.lock().port()) } else { let external_ip = self.inner.nat.and_then(|nat| nat.as_external_ip()); diff --git a/crates/net/network/src/peers.rs b/crates/net/network/src/peers.rs index e89b1695d9..757ef500b3 100644 --- a/crates/net/network/src/peers.rs +++ b/crates/net/network/src/peers.rs @@ -90,6 +90,8 @@ pub struct PeersManager { net_connection_state: NetworkConnectionState, /// How long to temporarily ban ip on an incoming connection attempt. incoming_ip_throttle_duration: Duration, + /// IP address filter for restricting network connections to specific IP ranges. + ip_filter: reth_net_banlist::IpFilter, } impl PeersManager { @@ -108,6 +110,7 @@ impl PeersManager { basic_nodes, max_backoff_count, incoming_ip_throttle_duration, + ip_filter, } = config; let (manager_tx, handle_rx) = mpsc::unbounded_channel(); let now = Instant::now(); @@ -161,6 +164,7 @@ impl PeersManager { max_backoff_count, net_connection_state: NetworkConnectionState::default(), incoming_ip_throttle_duration, + ip_filter, } } @@ -243,6 +247,12 @@ impl PeersManager { &mut self, addr: IpAddr, ) -> Result<(), InboundConnectionError> { + // Check if the IP is in the allowed ranges (netrestrict) + if !self.ip_filter.is_allowed(&addr) { + trace!(target: "net", ?addr, "Rejecting connection from IP not in allowed ranges"); + return Err(InboundConnectionError::IpBanned) + } + if self.ban_list.is_banned_ip(&addr) { return Err(InboundConnectionError::IpBanned) } @@ -749,7 +759,15 @@ impl PeersManager { addr: PeerAddr, fork_id: Option, ) { - if self.ban_list.is_banned(&peer_id, &addr.tcp().ip()) { + let ip_addr = addr.tcp().ip(); + + // Check if the IP is in the allowed ranges (netrestrict) + if !self.ip_filter.is_allowed(&ip_addr) { + trace!(target: "net", ?peer_id, ?ip_addr, "Skipping peer from IP not in allowed ranges"); + return + } + + if self.ban_list.is_banned(&peer_id, &ip_addr) { return } @@ -830,7 +848,15 @@ impl PeersManager { addr: PeerAddr, fork_id: Option, ) { - if self.ban_list.is_banned(&peer_id, &addr.tcp().ip()) { + let ip_addr = addr.tcp().ip(); + + // Check if the IP is in the allowed ranges (netrestrict) + if !self.ip_filter.is_allowed(&ip_addr) { + trace!(target: "net", ?peer_id, ?ip_addr, "Skipping outbound connection to IP not in allowed ranges"); + return + } + + if self.ban_list.is_banned(&peer_id, &ip_addr) { return } @@ -2899,4 +2925,106 @@ mod tests { let updated_peer = manager.peers.get(&peer_id).unwrap(); assert_eq!(updated_peer.addr.tcp().ip(), updated_ip); } + + #[tokio::test] + async fn test_ip_filter_blocks_inbound_connection() { + use reth_net_banlist::IpFilter; + use std::net::IpAddr; + + // Create a filter that only allows 192.168.0.0/16 + let ip_filter = IpFilter::from_cidr_string("192.168.0.0/16").unwrap(); + let config = PeersConfig::test().with_ip_filter(ip_filter); + let mut peers = PeersManager::new(config); + + // Try to connect from an allowed IP + let allowed_ip: IpAddr = "192.168.1.100".parse().unwrap(); + assert!(peers.on_incoming_pending_session(allowed_ip).is_ok()); + + // Try to connect from a disallowed IP + let disallowed_ip: IpAddr = "10.0.0.1".parse().unwrap(); + assert!(peers.on_incoming_pending_session(disallowed_ip).is_err()); + } + + #[tokio::test] + async fn test_ip_filter_blocks_outbound_connection() { + use reth_net_banlist::IpFilter; + use std::net::SocketAddr; + + // Create a filter that only allows 192.168.0.0/16 + let ip_filter = IpFilter::from_cidr_string("192.168.0.0/16").unwrap(); + let config = PeersConfig::test().with_ip_filter(ip_filter); + let mut peers = PeersManager::new(config); + + let peer_id = PeerId::new([1; 64]); + + // Try to add a peer with an allowed IP + let allowed_addr: SocketAddr = "192.168.1.100:30303".parse().unwrap(); + peers.add_peer(peer_id, PeerAddr::from_tcp(allowed_addr), None); + assert!(peers.peers.contains_key(&peer_id)); + + // Try to add a peer with a disallowed IP + let peer_id2 = PeerId::new([2; 64]); + let disallowed_addr: SocketAddr = "10.0.0.1:30303".parse().unwrap(); + peers.add_peer(peer_id2, PeerAddr::from_tcp(disallowed_addr), None); + assert!(!peers.peers.contains_key(&peer_id2)); + } + + #[tokio::test] + async fn test_ip_filter_ipv6() { + use reth_net_banlist::IpFilter; + use std::net::IpAddr; + + // Create a filter that only allows IPv6 range 2001:db8::/32 + let ip_filter = IpFilter::from_cidr_string("2001:db8::/32").unwrap(); + let config = PeersConfig::test().with_ip_filter(ip_filter); + let mut peers = PeersManager::new(config); + + // Try to connect from an allowed IPv6 address + let allowed_ip: IpAddr = "2001:db8::1".parse().unwrap(); + assert!(peers.on_incoming_pending_session(allowed_ip).is_ok()); + + // Try to connect from a disallowed IPv6 address + let disallowed_ip: IpAddr = "2001:db9::1".parse().unwrap(); + assert!(peers.on_incoming_pending_session(disallowed_ip).is_err()); + } + + #[tokio::test] + async fn test_ip_filter_multiple_ranges() { + use reth_net_banlist::IpFilter; + use std::net::IpAddr; + + // Create a filter that allows multiple ranges + let ip_filter = IpFilter::from_cidr_string("192.168.0.0/16,10.0.0.0/8").unwrap(); + let config = PeersConfig::test().with_ip_filter(ip_filter); + let mut peers = PeersManager::new(config); + + // Try IPs from both allowed ranges + let ip1: IpAddr = "192.168.1.1".parse().unwrap(); + let ip2: IpAddr = "10.5.10.20".parse().unwrap(); + assert!(peers.on_incoming_pending_session(ip1).is_ok()); + assert!(peers.on_incoming_pending_session(ip2).is_ok()); + + // Try IP from disallowed range + let disallowed_ip: IpAddr = "172.16.0.1".parse().unwrap(); + assert!(peers.on_incoming_pending_session(disallowed_ip).is_err()); + } + + #[tokio::test] + async fn test_ip_filter_no_restriction() { + use reth_net_banlist::IpFilter; + use std::net::IpAddr; + + // Create a filter with no restrictions (allow all) + let ip_filter = IpFilter::allow_all(); + let config = PeersConfig::test().with_ip_filter(ip_filter); + let mut peers = PeersManager::new(config); + + // All IPs should be allowed + let ip1: IpAddr = "192.168.1.1".parse().unwrap(); + let ip2: IpAddr = "10.0.0.1".parse().unwrap(); + let ip3: IpAddr = "8.8.8.8".parse().unwrap(); + assert!(peers.on_incoming_pending_session(ip1).is_ok()); + assert!(peers.on_incoming_pending_session(ip2).is_ok()); + assert!(peers.on_incoming_pending_session(ip3).is_ok()); + } } diff --git a/crates/net/network/src/required_block_filter.rs b/crates/net/network/src/required_block_filter.rs index 9c831e2f5d..407384251a 100644 --- a/crates/net/network/src/required_block_filter.rs +++ b/crates/net/network/src/required_block_filter.rs @@ -3,7 +3,7 @@ //! This module provides functionality to filter out peers that don't have //! specific required blocks (primarily used for shadowfork testing). -use alloy_primitives::B256; +use alloy_eips::BlockNumHash; use futures::StreamExt; use reth_eth_wire_types::{GetBlockHeaders, HeadersDirection}; use reth_network_api::{ @@ -16,11 +16,13 @@ use tracing::{debug, info, trace}; /// /// This task listens for new peer sessions and checks if they have the required /// block hashes. Peers that don't have these blocks are banned. +/// +/// This type is mainly used to connect peers on shadow forks (e.g. mainnet shadowfork= pub struct RequiredBlockFilter { /// Network handle for listening to events and managing peer reputation. network: N, - /// List of block hashes that peers must have to be considered valid. - block_hashes: Vec, + /// List of block number-hash pairs that peers must have to be considered valid. + block_num_hashes: Vec, } impl RequiredBlockFilter @@ -28,8 +30,8 @@ where N: NetworkEventListenerProvider + Peers + Clone + Send + Sync + 'static, { /// Creates a new required block peer filter. - pub const fn new(network: N, block_hashes: Vec) -> Self { - Self { network, block_hashes } + pub const fn new(network: N, block_num_hashes: Vec) -> Self { + Self { network, block_num_hashes } } /// Spawns the required block peer filter task. @@ -37,12 +39,12 @@ where /// This task will run indefinitely, monitoring new peer sessions and filtering /// out peers that don't have the required blocks. pub fn spawn(self) { - if self.block_hashes.is_empty() { + if self.block_num_hashes.is_empty() { debug!(target: "net::filter", "No required block hashes configured, skipping peer filtering"); return; } - info!(target: "net::filter", "Starting required block peer filter with {} block hashes", self.block_hashes.len()); + info!(target: "net::filter", "Starting required block peer filter with {} block hashes", self.block_num_hashes.len()); tokio::spawn(async move { self.run().await; @@ -60,10 +62,18 @@ where // Spawn a task to check this peer's blocks let network = self.network.clone(); - let block_hashes = self.block_hashes.clone(); + let block_num_hashes = self.block_num_hashes.clone(); + let peer_block_number = info.status.latest_block.unwrap_or(0); tokio::spawn(async move { - Self::check_peer_blocks(network, peer_id, messages, block_hashes).await; + Self::check_peer_blocks( + network, + peer_id, + messages, + block_num_hashes, + peer_block_number, + ) + .await; }); } } @@ -74,9 +84,19 @@ where network: N, peer_id: reth_network_api::PeerId, messages: reth_network_api::PeerRequestSender>, - block_hashes: Vec, + block_num_hashes: Vec, + latest_peer_block: u64, ) { - for block_hash in block_hashes { + for block_num_hash in block_num_hashes { + // Skip if peer's block number is lower than required, peer might also be syncing and + // still on the same chain. + if block_num_hash.number > 0 && latest_peer_block <= block_num_hash.number { + debug!(target: "net::filter", "Skipping check for block {} - peer {} only at block {}", + block_num_hash.number, peer_id, latest_peer_block); + continue; + } + + let block_hash = block_num_hash.hash; trace!(target: "net::filter", "Checking if peer {} has block {}", peer_id, block_hash); // Create a request for block headers @@ -139,28 +159,35 @@ where #[cfg(test)] mod tests { use super::*; + use alloy_eips::BlockNumHash; use alloy_primitives::{b256, B256}; use reth_network_api::noop::NoopNetwork; #[test] fn test_required_block_filter_creation() { let network = NoopNetwork::default(); - let block_hashes = vec![ - b256!("0x1111111111111111111111111111111111111111111111111111111111111111"), - b256!("0x2222222222222222222222222222222222222222222222222222222222222222"), + let block_num_hashes = vec![ + BlockNumHash::new( + 0, + b256!("0x1111111111111111111111111111111111111111111111111111111111111111"), + ), + BlockNumHash::new( + 23115201, + b256!("0x2222222222222222222222222222222222222222222222222222222222222222"), + ), ]; - let filter = RequiredBlockFilter::new(network, block_hashes.clone()); - assert_eq!(filter.block_hashes.len(), 2); - assert_eq!(filter.block_hashes, block_hashes); + let filter = RequiredBlockFilter::new(network, block_num_hashes.clone()); + assert_eq!(filter.block_num_hashes.len(), 2); + assert_eq!(filter.block_num_hashes, block_num_hashes); } #[test] fn test_required_block_filter_empty_hashes_does_not_spawn() { let network = NoopNetwork::default(); - let block_hashes = vec![]; + let block_num_hashes = vec![]; - let filter = RequiredBlockFilter::new(network, block_hashes); + let filter = RequiredBlockFilter::new(network, block_num_hashes); // This should not panic and should exit early when spawn is called filter.spawn(); } @@ -170,10 +197,10 @@ mod tests { // This test would require a more complex setup with mock network components // For now, we ensure the basic structure is correct let network = NoopNetwork::default(); - let block_hashes = vec![B256::default()]; + let block_num_hashes = vec![BlockNumHash::new(0, B256::default())]; - let filter = RequiredBlockFilter::new(network, block_hashes); + let filter = RequiredBlockFilter::new(network, block_num_hashes); // Verify the filter can be created and basic properties are set - assert_eq!(filter.block_hashes.len(), 1); + assert_eq!(filter.block_num_hashes.len(), 1); } } diff --git a/crates/net/network/src/session/active.rs b/crates/net/network/src/session/active.rs index 0044c1f92e..98ccbad3b9 100644 --- a/crates/net/network/src/session/active.rs +++ b/crates/net/network/src/session/active.rs @@ -237,16 +237,6 @@ impl ActiveSession { self.try_emit_broadcast(PeerMessage::PooledTransactions(msg.into())).into() } EthMessage::NewPooledTransactionHashes68(msg) => { - if msg.hashes.len() != msg.types.len() || msg.hashes.len() != msg.sizes.len() { - return OnIncomingMessageOutcome::BadMessage { - error: EthStreamError::TransactionHashesInvalidLenOfFields { - hashes_len: msg.hashes.len(), - types_len: msg.types.len(), - sizes_len: msg.sizes.len(), - }, - message: EthMessage::NewPooledTransactionHashes68(msg), - } - } self.try_emit_broadcast(PeerMessage::PooledTransactions(msg.into())).into() } EthMessage::GetBlockHeaders(req) => { diff --git a/crates/net/network/src/session/types.rs b/crates/net/network/src/session/types.rs index b73bfe3b99..02297c2146 100644 --- a/crates/net/network/src/session/types.rs +++ b/crates/net/network/src/session/types.rs @@ -11,7 +11,7 @@ use std::{ }, }; -/// Information about the range of blocks available from a peer. +/// Information about the range of full blocks available from a peer. /// /// This represents the announced `eth69` /// [`BlockRangeUpdate`] of a peer. @@ -45,12 +45,12 @@ impl BlockRangeInfo { RangeInclusive::new(earliest, latest) } - /// Returns the earliest block number available from the peer. + /// Returns the earliest full block number available from the peer. pub fn earliest(&self) -> u64 { self.inner.earliest.load(Ordering::Relaxed) } - /// Returns the latest block number available from the peer. + /// Returns the latest full block number available from the peer. pub fn latest(&self) -> u64 { self.inner.latest.load(Ordering::Relaxed) } @@ -60,6 +60,11 @@ impl BlockRangeInfo { *self.inner.latest_hash.read() } + /// Returns true if the peer has the full history available. + pub fn has_full_history(&self) -> bool { + self.earliest() == 0 + } + /// Updates the range information. pub fn update(&self, earliest: u64, latest: u64, latest_hash: B256) { self.inner.earliest.store(earliest, Ordering::Relaxed); diff --git a/crates/net/network/src/test_utils/testnet.rs b/crates/net/network/src/test_utils/testnet.rs index d246689954..aae1f7708e 100644 --- a/crates/net/network/src/test_utils/testnet.rs +++ b/crates/net/network/src/test_utils/testnet.rs @@ -399,13 +399,7 @@ pub struct Peer { #[pin] request_handler: Option>, #[pin] - transactions_manager: Option< - TransactionsManager< - Pool, - EthNetworkPrimitives, - NetworkPolicies, - >, - >, + transactions_manager: Option>, pool: Option, client: C, secret_key: SecretKey, diff --git a/crates/net/network/src/transactions/config.rs b/crates/net/network/src/transactions/config.rs index c34bbecd77..f6b76908df 100644 --- a/crates/net/network/src/transactions/config.rs +++ b/crates/net/network/src/transactions/config.rs @@ -1,4 +1,5 @@ -use std::{fmt::Debug, marker::PhantomData, str::FromStr}; +use core::fmt; +use std::{fmt::Debug, str::FromStr}; use super::{ PeerMetadata, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, @@ -9,11 +10,11 @@ use crate::transactions::constants::tx_fetcher::{ DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, }; +use alloy_eips::eip2718::IsTyped2718; use alloy_primitives::B256; use derive_more::{Constructor, Display}; - use reth_eth_wire::NetworkPrimitives; -use reth_ethereum_primitives::TxType; +use reth_network_types::peers::kind::PeerKind; /// Configuration for managing transactions within the network. #[derive(Debug, Clone)] @@ -26,6 +27,9 @@ pub struct TransactionsManagerConfig { /// How new pending transactions are propagated. #[cfg_attr(feature = "serde", serde(default))] pub propagation_mode: TransactionPropagationMode, + /// Which peers we accept incoming transactions or announcements from. + #[cfg_attr(feature = "serde", serde(default))] + pub ingress_policy: TransactionIngressPolicy, } impl Default for TransactionsManagerConfig { @@ -34,6 +38,7 @@ impl Default for TransactionsManagerConfig { transaction_fetcher_config: TransactionFetcherConfig::default(), max_transactions_seen_by_peer_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, propagation_mode: TransactionPropagationMode::default(), + ingress_policy: TransactionIngressPolicy::default(), } } } @@ -122,17 +127,19 @@ impl Default for TransactionFetcherConfig { } /// A policy defining which peers pending transactions are gossiped to. -pub trait TransactionPropagationPolicy: Send + Sync + Unpin + 'static { +pub trait TransactionPropagationPolicy: + Send + Sync + Unpin + fmt::Debug + 'static +{ /// Filter a given peer based on the policy. /// /// This determines whether transactions can be propagated to this peer. - fn can_propagate(&self, peer: &mut PeerMetadata) -> bool; + fn can_propagate(&self, peer: &mut PeerMetadata) -> bool; /// A callback on the policy when a new peer session is established. - fn on_session_established(&mut self, peer: &mut PeerMetadata); + fn on_session_established(&mut self, peer: &mut PeerMetadata); /// A callback on the policy when a peer session is closed. - fn on_session_closed(&mut self, peer: &mut PeerMetadata); + fn on_session_closed(&mut self, peer: &mut PeerMetadata); } /// Determines which peers pending transactions are propagated to. @@ -150,8 +157,8 @@ pub enum TransactionPropagationKind { None, } -impl TransactionPropagationPolicy for TransactionPropagationKind { - fn can_propagate(&self, peer: &mut PeerMetadata) -> bool { +impl TransactionPropagationPolicy for TransactionPropagationKind { + fn can_propagate(&self, peer: &mut PeerMetadata) -> bool { match self { Self::All => true, Self::Trusted => peer.peer_kind.is_trusted(), @@ -159,9 +166,9 @@ impl TransactionPropagationPolicy for TransactionPropagationKind { } } - fn on_session_established(&mut self, _peer: &mut PeerMetadata) {} + fn on_session_established(&mut self, _peer: &mut PeerMetadata) {} - fn on_session_closed(&mut self, _peer: &mut PeerMetadata) {} + fn on_session_closed(&mut self, _peer: &mut PeerMetadata) {} } impl FromStr for TransactionPropagationKind { @@ -177,6 +184,48 @@ impl FromStr for TransactionPropagationKind { } } +/// Determines which peers we will accept incoming transactions or announcements from. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TransactionIngressPolicy { + /// Accept transactions from any peer. + #[default] + All, + /// Accept transactions only from trusted peers. + Trusted, + /// Drop all incoming transactions. + None, +} + +impl TransactionIngressPolicy { + /// Returns true if the ingress policy allows the provided peer kind. + pub const fn allows(&self, peer_kind: PeerKind) -> bool { + match self { + Self::All => true, + Self::Trusted => peer_kind.is_trusted(), + Self::None => false, + } + } + + /// Returns true if the ingress policy accepts transactions from any peer. + pub const fn allows_all(&self) -> bool { + matches!(self, Self::All) + } +} + +impl FromStr for TransactionIngressPolicy { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "All" | "all" => Ok(Self::All), + "Trusted" | "trusted" => Ok(Self::Trusted), + "None" | "none" => Ok(Self::None), + _ => Err(format!("Invalid transaction ingress policy: {s}")), + } + } +} + /// Defines the outcome of evaluating a transaction against an `AnnouncementFilteringPolicy`. /// /// Dictates how the `TransactionManager` should proceed on an announced transaction. @@ -195,87 +244,65 @@ pub enum AnnouncementAcceptance { /// A policy that defines how to handle incoming transaction announcements, /// particularly concerning transaction types and other announcement metadata. -pub trait AnnouncementFilteringPolicy: Send + Sync + Unpin + 'static { +pub trait AnnouncementFilteringPolicy: + Send + Sync + Unpin + fmt::Debug + 'static +{ /// Decides how to handle a transaction announcement based on its type, hash, and size. fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance; } /// A generic `AnnouncementFilteringPolicy` that enforces strict validation /// of transaction type based on a generic type `T`. -#[derive(Debug, Clone)] -pub struct TypedStrictFilter + Debug + Send + Sync + 'static>(PhantomData); +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct TypedStrictFilter; -impl + Debug + Send + Sync + 'static> Default for TypedStrictFilter { - fn default() -> Self { - Self(PhantomData) - } -} - -impl AnnouncementFilteringPolicy for TypedStrictFilter -where - T: TryFrom + Debug + Send + Sync + Unpin + 'static, - >::Error: Debug, -{ +impl AnnouncementFilteringPolicy for TypedStrictFilter { fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance { - match T::try_from(ty) { - Ok(_valid_type) => AnnouncementAcceptance::Accept, - Err(e) => { - tracing::trace!(target: "net::tx::policy::strict_typed", - type_param = %std::any::type_name::(), - %ty, - %size, - %hash, - error = ?e, - "Invalid or unrecognized transaction type byte. Rejecting entry and recommending peer penalization." - ); - AnnouncementAcceptance::Reject { penalize_peer: true } - } + if N::PooledTransaction::is_type(ty) { + AnnouncementAcceptance::Accept + } else { + tracing::trace!(target: "net::tx::policy::strict_typed", + %ty, + %size, + %hash, + "Invalid or unrecognized transaction type byte. Rejecting entry and recommending peer penalization." + ); + AnnouncementAcceptance::Reject { penalize_peer: true } } } } /// Type alias for a `TypedStrictFilter`. This is the default strict announcement filter. -pub type StrictEthAnnouncementFilter = TypedStrictFilter; +pub type StrictEthAnnouncementFilter = TypedStrictFilter; /// An [`AnnouncementFilteringPolicy`] that permissively handles unknown type bytes /// based on a given type `T` using `T::try_from(u8)`. /// /// If `T::try_from(ty)` succeeds, the announcement is accepted. Otherwise, it's ignored. -#[derive(Debug, Clone)] -pub struct TypedRelaxedFilter + Debug + Send + Sync + 'static>(PhantomData); +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct TypedRelaxedFilter; -impl + Debug + Send + Sync + 'static> Default for TypedRelaxedFilter { - fn default() -> Self { - Self(PhantomData) - } -} - -impl AnnouncementFilteringPolicy for TypedRelaxedFilter -where - T: TryFrom + Debug + Send + Sync + Unpin + 'static, - >::Error: Debug, -{ +impl AnnouncementFilteringPolicy for TypedRelaxedFilter { fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance { - match T::try_from(ty) { - Ok(_valid_type) => AnnouncementAcceptance::Accept, - Err(e) => { - tracing::trace!(target: "net::tx::policy::relaxed_typed", - type_param = %std::any::type_name::(), - %ty, - %size, - %hash, - error = ?e, - "Unknown transaction type byte. Ignoring entry." - ); - AnnouncementAcceptance::Ignore - } + if N::PooledTransaction::is_type(ty) { + AnnouncementAcceptance::Accept + } else { + tracing::trace!(target: "net::tx::policy::relaxed_typed", + %ty, + %size, + %hash, + "Unknown transaction type byte. Ignoring entry." + ); + AnnouncementAcceptance::Ignore } } } /// Type alias for `TypedRelaxedFilter`. This filter accepts known Ethereum transaction types and /// ignores unknown ones without penalizing the peer. -pub type RelaxedEthAnnouncementFilter = TypedRelaxedFilter; +pub type RelaxedEthAnnouncementFilter = TypedRelaxedFilter; #[cfg(test)] mod tests { diff --git a/crates/net/network/src/transactions/mod.rs b/crates/net/network/src/transactions/mod.rs index f4ef42523d..72c704d3ea 100644 --- a/crates/net/network/src/transactions/mod.rs +++ b/crates/net/network/src/transactions/mod.rs @@ -8,19 +8,19 @@ pub mod config; pub mod constants; /// Component responsible for fetching transactions from [`NewPooledTransactionHashes`]. pub mod fetcher; -/// Defines the [`TransactionPolicies`] trait for aggregating transaction-related policies. +/// Defines the traits for transaction-related policies. pub mod policy; pub use self::constants::{ tx_fetcher::DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ, SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, }; -use config::{AnnouncementAcceptance, StrictEthAnnouncementFilter, TransactionPropagationKind}; +use config::AnnouncementAcceptance; pub use config::{ - AnnouncementFilteringPolicy, TransactionFetcherConfig, TransactionPropagationMode, - TransactionPropagationPolicy, TransactionsManagerConfig, + AnnouncementFilteringPolicy, TransactionFetcherConfig, TransactionIngressPolicy, + TransactionPropagationMode, TransactionPropagationPolicy, TransactionsManagerConfig, }; -use policy::{NetworkPolicies, TransactionPolicies}; +use policy::NetworkPolicies; pub(crate) use fetcher::{FetchEvent, TransactionFetcher}; @@ -35,6 +35,7 @@ use crate::{ metrics::{ AnnouncedTxTypesMetrics, TransactionsManagerMetrics, NETWORK_POOL_TRANSACTIONS_SCOPE, }, + transactions::config::{StrictEthAnnouncementFilter, TransactionPropagationKind}, NetworkHandle, TxTypesCounter, }; use alloy_primitives::{TxHash, B256}; @@ -98,8 +99,6 @@ pub struct TransactionsHandle { manager_tx: mpsc::UnboundedSender>, } -/// Implementation of the `TransactionsHandle` API for use in testnet via type -/// [`PeerHandle`](crate::test_utils::PeerHandle). impl TransactionsHandle { fn send(&self, cmd: TransactionsCommand) { let _ = self.manager_tx.send(cmd); @@ -243,7 +242,7 @@ impl TransactionsHandle { /// propagate new transactions over the network. /// /// It can be configured with different policies for transaction propagation and announcement -/// filtering. See [`NetworkPolicies`] and [`TransactionPolicies`] for more details. +/// filtering. See [`NetworkPolicies`] for more details. /// /// ## Network Transaction Processing /// @@ -280,14 +279,7 @@ impl TransactionsHandle { /// Rate limiting via reputation, bad transaction isolation, peer scoring. #[derive(Debug)] #[must_use = "Manager does nothing unless polled."] -pub struct TransactionsManager< - Pool, - N: NetworkPrimitives = EthNetworkPrimitives, - PBundle: TransactionPolicies = NetworkPolicies< - TransactionPropagationKind, - StrictEthAnnouncementFilter, - >, -> { +pub struct TransactionsManager { /// Access to the transaction pool. pool: Pool, /// Network access. @@ -344,20 +336,14 @@ pub struct TransactionsManager< /// How the `TransactionsManager` is configured. config: TransactionsManagerConfig, /// Network Policies - policies: PBundle, + policies: NetworkPolicies, /// `TransactionsManager` metrics metrics: TransactionsManagerMetrics, /// `AnnouncedTxTypes` metrics announced_tx_types_metrics: AnnouncedTxTypesMetrics, } -impl - TransactionsManager< - Pool, - N, - NetworkPolicies, - > -{ +impl TransactionsManager { /// Sets up a new instance. /// /// Note: This expects an existing [`NetworkManager`](crate::NetworkManager) instance. @@ -372,14 +358,15 @@ impl pool, from_network, transactions_manager_config, - NetworkPolicies::default(), + NetworkPolicies::new( + TransactionPropagationKind::default(), + StrictEthAnnouncementFilter::default(), + ), ) } } -impl - TransactionsManager -{ +impl TransactionsManager { /// Sets up a new instance with given the settings. /// /// Note: This expects an existing [`NetworkManager`](crate::NetworkManager) instance. @@ -388,7 +375,7 @@ impl pool: Pool, from_network: mpsc::UnboundedReceiver>, transactions_manager_config: TransactionsManagerConfig, - policies: PBundle, + policies: NetworkPolicies, ) -> Self { let network_events = network.event_listener(); @@ -559,9 +546,7 @@ impl } } -impl - TransactionsManager -{ +impl TransactionsManager { /// Processes a batch import results. fn on_batch_import_result(&mut self, batch_results: Vec>) { for res in batch_results { @@ -828,16 +813,13 @@ impl } } -impl TransactionsManager +impl TransactionsManager where Pool: TransactionPool + Unpin + 'static, - N: NetworkPrimitives< BroadcastedTransaction: SignedTransaction, PooledTransaction: SignedTransaction, > + Unpin, - - PBundle: TransactionPolicies, Pool::Transaction: PoolTransaction, { @@ -1282,10 +1264,25 @@ where } } + /// Returns true if the ingress policy allows processing messages from the given peer. + fn accepts_incoming_from(&self, peer_id: &PeerId) -> bool { + if self.config.ingress_policy.allows_all() { + return true; + } + let Some(peer) = self.peers.get(peer_id) else { + return false; + }; + self.config.ingress_policy.allows(peer.peer_kind()) + } + /// Handles dedicated transaction events related to the `eth` protocol. fn on_network_tx_event(&mut self, event: NetworkTransactionEvent) { match event { NetworkTransactionEvent::IncomingTransactions { peer_id, msg } => { + if !self.accepts_incoming_from(&peer_id) { + trace!(target: "net::tx", peer_id=format!("{peer_id:#}"), policy=?self.config.ingress_policy, "Ignoring full transactions from peer blocked by ingress policy"); + return; + } // ensure we didn't receive any blob transactions as these are disallowed to be // broadcasted in full @@ -1306,6 +1303,10 @@ where } } NetworkTransactionEvent::IncomingPooledTransactionHashes { peer_id, msg } => { + if !self.accepts_incoming_from(&peer_id) { + trace!(target: "net::tx", peer_id=format!("{peer_id:#}"), policy=?self.config.ingress_policy, "Ignoring transaction hashes from peer blocked by ingress policy"); + return; + } self.on_new_pooled_transaction_hashes(peer_id, msg) } NetworkTransactionEvent::GetPooledTransactions { peer_id, request, response } => { @@ -1491,8 +1492,7 @@ impl< BroadcastedTransaction: SignedTransaction, PooledTransaction: SignedTransaction, > + Unpin, - PBundle: TransactionPolicies + Unpin, - > Future for TransactionsManager + > Future for TransactionsManager where Pool::Transaction: PoolTransaction, @@ -1534,7 +1534,19 @@ where SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE, ) { Poll::Ready(count) => { - count == SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE + if count == SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE { + // we filled the entire buffer capacity and need to try again on the next poll + // immediately + true + } else { + // try once more, because mostlikely the channel is now empty and the waker is + // registered if this is pending, if we filled additional hashes, we poll again + // on the next iteration + let limit = + SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE - + new_txs.len(); + this.pending_transactions.poll_recv_many(cx, &mut new_txs, limit).is_ready() + } } Poll::Pending => false, }; @@ -1542,25 +1554,6 @@ where this.on_new_pending_transactions(new_txs); } - // Advance inflight fetch requests (flush transaction fetcher and queue for - // import to pool). - // - // The smallest decodable transaction is an empty legacy transaction, 10 bytes - // (2 MiB / 10 bytes > 200k transactions). - // - // Since transactions aren't validated until they are inserted into the pool, - // this can potentially queue >200k transactions for insertion to pool. More - // if the message size is bigger than the soft limit on a `PooledTransactions` - // response which is 2 MiB. - let maybe_more_tx_fetch_events = metered_poll_nested_stream_with_budget!( - poll_durations.acc_fetch_events, - "net::tx", - "Transaction fetch events stream", - DEFAULT_BUDGET_TRY_DRAIN_STREAM, - this.transaction_fetcher.poll_next_unpin(cx), - |event| this.on_fetch_event(event), - ); - // Advance incoming transaction events (stream new txns/announcements from // network manager and queue for import to pool/fetch txns). // @@ -1584,6 +1577,25 @@ where |event| this.on_network_tx_event(event), ); + // Advance inflight fetch requests (flush transaction fetcher and queue for + // import to pool). + // + // The smallest decodable transaction is an empty legacy transaction, 10 bytes + // (2 MiB / 10 bytes > 200k transactions). + // + // Since transactions aren't validated until they are inserted into the pool, + // this can potentially queue >200k transactions for insertion to pool. More + // if the message size is bigger than the soft limit on a `PooledTransactions` + // response which is 2 MiB. + let mut maybe_more_tx_fetch_events = metered_poll_nested_stream_with_budget!( + poll_durations.acc_fetch_events, + "net::tx", + "Transaction fetch events stream", + DEFAULT_BUDGET_TRY_DRAIN_STREAM, + this.transaction_fetcher.poll_next_unpin(cx), + |event| this.on_fetch_event(event), + ); + // Advance pool imports (flush txns to pool). // // Note, this is done in batches. A batch is filled from one `Transactions` @@ -1615,6 +1627,7 @@ where { if this.has_capacity_for_fetching_pending_hashes() { this.on_fetch_hashes_pending_fetch(); + maybe_more_tx_fetch_events = true; } }, poll_durations.acc_pending_fetch @@ -2896,11 +2909,7 @@ mod tests { let network_handle = network_manager.handle().clone(); let network_service_handle = tokio::spawn(network_manager); - let mut tx_manager = TransactionsManager::< - TestPool, - EthNetworkPrimitives, - NetworkPolicies, - >::with_policy( + let mut tx_manager = TransactionsManager::::with_policy( network_handle.clone(), pool.clone(), from_network_rx, diff --git a/crates/net/network/src/transactions/policy.rs b/crates/net/network/src/transactions/policy.rs index c25b9d9b41..0fbadae037 100644 --- a/crates/net/network/src/transactions/policy.rs +++ b/crates/net/network/src/transactions/policy.rs @@ -1,78 +1,49 @@ use crate::transactions::config::{AnnouncementFilteringPolicy, TransactionPropagationPolicy}; +use reth_eth_wire::NetworkPrimitives; use std::fmt::Debug; -/// A bundle of policies that control the behavior of network components like -/// the [`TransactionsManager`](super::TransactionsManager). -/// -/// This trait allows for different collections of policies to be used interchangeably. -pub trait TransactionPolicies: Send + Sync + Debug + 'static { - /// The type of the policy used for transaction propagation. - type Propagation: TransactionPropagationPolicy; - /// The type of the policy used for filtering transaction announcements. - type Announcement: AnnouncementFilteringPolicy; - - /// Returns a reference to the transaction propagation policy. - fn propagation_policy(&self) -> &Self::Propagation; - - /// Returns a mutable reference to the transaction propagation policy. - fn propagation_policy_mut(&mut self) -> &mut Self::Propagation; - - /// Returns a reference to the announcement filtering policy. - fn announcement_filter(&self) -> &Self::Announcement; -} - /// A container that bundles specific implementations of transaction-related policies, /// -/// This struct implements the [`TransactionPolicies`] trait, providing a complete set of -/// policies required by components like the [`TransactionsManager`](super::TransactionsManager). -/// It holds a specific [`TransactionPropagationPolicy`] and an -/// [`AnnouncementFilteringPolicy`]. -#[derive(Debug, Clone, Default)] -pub struct NetworkPolicies { - propagation: P, - announcement: A, +/// This struct provides a complete set of policies required by components like the +/// [`TransactionsManager`](super::TransactionsManager). It holds a specific +/// [`TransactionPropagationPolicy`] and an [`AnnouncementFilteringPolicy`]. +#[derive(Debug)] +pub struct NetworkPolicies { + propagation: Box>, + announcement: Box>, } -impl NetworkPolicies { +impl NetworkPolicies { /// Creates a new bundle of network policies. - pub const fn new(propagation: P, announcement: A) -> Self { - Self { propagation, announcement } + pub fn new( + propagation: impl TransactionPropagationPolicy, + announcement: impl AnnouncementFilteringPolicy, + ) -> Self { + Self { propagation: Box::new(propagation), announcement: Box::new(announcement) } } /// Returns a new `NetworkPolicies` bundle with the `TransactionPropagationPolicy` replaced. - pub fn with_propagation(self, new_propagation: NewP) -> NetworkPolicies - where - NewP: TransactionPropagationPolicy, - { - NetworkPolicies::new(new_propagation, self.announcement) + pub fn with_propagation(self, new_propagation: impl TransactionPropagationPolicy) -> Self { + Self { propagation: Box::new(new_propagation), announcement: self.announcement } } /// Returns a new `NetworkPolicies` bundle with the `AnnouncementFilteringPolicy` replaced. - pub fn with_announcement(self, new_announcement: NewA) -> NetworkPolicies - where - NewA: AnnouncementFilteringPolicy, - { - NetworkPolicies::new(self.propagation, new_announcement) - } -} - -impl TransactionPolicies for NetworkPolicies -where - P: TransactionPropagationPolicy + Debug, - A: AnnouncementFilteringPolicy + Debug, -{ - type Propagation = P; - type Announcement = A; - - fn propagation_policy(&self) -> &Self::Propagation { - &self.propagation - } - - fn propagation_policy_mut(&mut self) -> &mut Self::Propagation { - &mut self.propagation - } - - fn announcement_filter(&self) -> &Self::Announcement { - &self.announcement + pub fn with_announcement(self, new_announcement: impl AnnouncementFilteringPolicy) -> Self { + Self { propagation: self.propagation, announcement: Box::new(new_announcement) } + } + + /// Returns a reference to the transaction propagation policy. + pub fn propagation_policy(&self) -> &dyn TransactionPropagationPolicy { + &*self.propagation + } + + /// Returns a mutable reference to the transaction propagation policy. + pub fn propagation_policy_mut(&mut self) -> &mut dyn TransactionPropagationPolicy { + &mut *self.propagation + } + + /// Returns a reference to the announcement filtering policy. + pub fn announcement_filter(&self) -> &dyn AnnouncementFilteringPolicy { + &*self.announcement } } diff --git a/crates/net/network/tests/it/txgossip.rs b/crates/net/network/tests/it/txgossip.rs index ed1c2f925d..d0f192cff5 100644 --- a/crates/net/network/tests/it/txgossip.rs +++ b/crates/net/network/tests/it/txgossip.rs @@ -5,7 +5,9 @@ use futures::StreamExt; use reth_ethereum_primitives::TransactionSigned; use reth_network::{ test_utils::{NetworkEventStream, Testnet}, - transactions::config::TransactionPropagationKind, + transactions::config::{ + TransactionIngressPolicy, TransactionPropagationKind, TransactionsManagerConfig, + }, NetworkEvent, NetworkEventListenerProvider, Peers, }; use reth_network_api::{events::PeerEvent, PeerKind, PeersInfo}; @@ -123,6 +125,73 @@ async fn test_tx_propagation_policy_trusted_only() { assert!(buff.contains(&outcome_1.hash)); } +#[tokio::test(flavor = "multi_thread")] +async fn test_tx_ingress_policy_trusted_only() { + reth_tracing::init_test_tracing(); + + let provider = MockEthProvider::default(); + + let tx_manager_config = TransactionsManagerConfig { + ingress_policy: TransactionIngressPolicy::Trusted, + ..Default::default() + }; + + let net = Testnet::create_with(2, provider.clone()).await; + let net = net.with_eth_pool_config(tx_manager_config); + + let handle = net.spawn(); + + // connect all the peers + handle.connect_peers().await; + + let peer_0_handle = &handle.peers()[0]; + let peer_1_handle = &handle.peers()[1]; + + let mut peer0_tx_listener = peer_0_handle.pool().unwrap().pending_transactions_listener(); + + let mut tx_gen = TransactionGenerator::new(rand::rng()); + let tx = tx_gen.gen_eip1559_pooled(); + + // ensure the sender has balance + let sender = tx.sender(); + provider.add_account(sender, ExtendedAccount::new(0, U256::from(100_000_000))); + + // insert the tx in peer1's pool + let outcome_0 = peer_1_handle.pool().unwrap().add_external_transaction(tx).await.unwrap(); + + // ensure tx is not accepted by peer0 + peer0_tx_listener.try_recv().expect_err("Empty"); + + let mut event_stream_0 = NetworkEventStream::new(peer_0_handle.network().event_listener()); + let mut event_stream_1 = NetworkEventStream::new(peer_1_handle.network().event_listener()); + + // disconnect peer1 from peer0 + peer_0_handle.network().remove_peer(*peer_1_handle.peer_id(), PeerKind::Static); + join!(event_stream_0.next_session_closed(), event_stream_1.next_session_closed()); + + // re register peer1 as trusted + peer_0_handle.network().add_trusted_peer(*peer_1_handle.peer_id(), peer_1_handle.local_addr()); + join!(event_stream_0.next_session_established(), event_stream_1.next_session_established()); + + let mut tx_gen = TransactionGenerator::new(rand::rng()); + let tx = tx_gen.gen_eip1559_pooled(); + + // ensure the sender has balance + let sender = tx.sender(); + provider.add_account(sender, ExtendedAccount::new(0, U256::from(100_000_000))); + + // insert pending tx in peer1's pool + let outcome_1 = peer_1_handle.pool().unwrap().add_external_transaction(tx).await.unwrap(); + + // ensure peer0 now receives both pending txs from peer1 (the blocked one and the new one) + let mut buff = Vec::with_capacity(2); + buff.push(peer0_tx_listener.recv().await.unwrap()); + buff.push(peer0_tx_listener.recv().await.unwrap()); + + assert!(buff.contains(&outcome_0.hash)); + assert!(buff.contains(&outcome_1.hash)); +} + #[tokio::test(flavor = "multi_thread")] async fn test_4844_tx_gossip_penalization() { reth_tracing::init_test_tracing(); diff --git a/crates/node/builder/Cargo.toml b/crates/node/builder/Cargo.toml index 8e8774e86c..df89dcfdf5 100644 --- a/crates/node/builder/Cargo.toml +++ b/crates/node/builder/Cargo.toml @@ -15,7 +15,6 @@ workspace = true ## reth reth-chain-state.workspace = true reth-chainspec.workspace = true -reth-cli-util.workspace = true reth-config.workspace = true reth-consensus-debug-client.workspace = true reth-consensus.workspace = true diff --git a/crates/node/builder/src/builder/mod.rs b/crates/node/builder/src/builder/mod.rs index f2886f4756..3727abbc77 100644 --- a/crates/node/builder/src/builder/mod.rs +++ b/crates/node/builder/src/builder/mod.rs @@ -12,7 +12,6 @@ use crate::{ use alloy_eips::eip4844::env_settings::EnvKzgSettings; use futures::Future; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; -use reth_cli_util::get_secret_key; use reth_db_api::{database::Database, database_metrics::DatabaseMetrics}; use reth_exex::ExExContext; use reth_network::{ @@ -36,7 +35,7 @@ use reth_provider::{ use reth_tasks::TaskExecutor; use reth_transaction_pool::{PoolConfig, PoolTransaction, TransactionPool}; use secp256k1::SecretKey; -use std::{fmt::Debug, sync::Arc}; +use std::sync::Arc; use tracing::{info, trace, warn}; pub mod add_ons; @@ -832,7 +831,7 @@ impl BuilderContext { > + Unpin + 'static, Node::Provider: BlockReaderFor, - Policy: TransactionPropagationPolicy + Debug, + Policy: TransactionPropagationPolicy, { let (handle, network, txpool, eth) = builder .transactions_with_policy(pool, tx_config, propagation_policy) @@ -869,9 +868,7 @@ impl BuilderContext { /// Get the network secret from the given data dir fn network_secret(&self, data_dir: &ChainPath) -> eyre::Result { - let network_secret_path = - self.config().network.p2p_secret_key.clone().unwrap_or_else(|| data_dir.p2p_secret()); - let secret_key = get_secret_key(&network_secret_path)?; + let secret_key = self.config().network.secret_key(data_dir.p2p_secret())?; Ok(secret_key) } diff --git a/crates/node/builder/src/launch/common.rs b/crates/node/builder/src/launch/common.rs index a31e649825..651fd3958b 100644 --- a/crates/node/builder/src/launch/common.rs +++ b/crates/node/builder/src/launch/common.rs @@ -42,7 +42,7 @@ use reth_chainspec::{Chain, EthChainSpec, EthereumHardforks}; use reth_config::{config::EtlConfig, PruneConfig}; use reth_consensus::noop::NoopConsensus; use reth_db_api::{database::Database, database_metrics::DatabaseMetrics}; -use reth_db_common::init::{init_genesis, InitStorageError}; +use reth_db_common::init::{init_genesis_with_settings, InitStorageError}; use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader}; use reth_engine_local::MiningMode; use reth_evm::{noop::NoopEvmConfig, ConfigureEvm}; @@ -165,6 +165,9 @@ impl LaunchContext { // Update the config with the command line arguments toml_config.peers.trusted_nodes_only = config.network.trusted_only; + // Merge static file CLI arguments with config file, giving priority to CLI + toml_config.static_files = config.static_files.merge_with_config(toml_config.static_files); + Ok(toml_config) } @@ -464,22 +467,25 @@ where N: ProviderNodeTypes, Evm: ConfigureEvm + 'static, { + // Validate static files configuration + let static_files_config = &self.toml_config().static_files; + static_files_config.validate()?; + + // Apply per-segment blocks_per_file configuration let static_file_provider = StaticFileProviderBuilder::read_write(self.data_dir().static_files())? .with_metrics() + .with_blocks_per_file_for_segments(static_files_config.as_blocks_per_file_map()) .build()?; let factory = ProviderFactory::new(self.right().clone(), self.chain_spec(), static_file_provider)? .with_prune_modes(self.prune_modes()); - let has_receipt_pruning = self.toml_config().prune.has_receipts_pruning(); - // Check for consistency between database and static files. If it fails, it unwinds to // the first block that's consistent between database and static files. - if let Some(unwind_target) = factory - .static_file_provider() - .check_consistency(&factory.provider()?, has_receipt_pruning)? + if let Some(unwind_target) = + factory.static_file_provider().check_consistency(&factory.provider()?)? { // Highly unlikely to happen, and given its destructive nature, it's better to panic // instead. @@ -618,13 +624,19 @@ where /// Convenience function to [`Self::init_genesis`] pub fn with_genesis(self) -> Result { - init_genesis(self.provider_factory())?; + init_genesis_with_settings( + self.provider_factory(), + self.node_config().static_files.to_settings(), + )?; Ok(self) } /// Write the genesis block and state if it has not already been written pub fn init_genesis(&self) -> Result { - init_genesis(self.provider_factory()) + init_genesis_with_settings( + self.provider_factory(), + self.node_config().static_files.to_settings(), + ) } /// Creates a new `WithMeteredProvider` container and attaches it to the diff --git a/crates/node/builder/src/rpc.rs b/crates/node/builder/src/rpc.rs index a66d7b222e..7fb219ff7a 100644 --- a/crates/node/builder/src/rpc.rs +++ b/crates/node/builder/src/rpc.rs @@ -23,7 +23,10 @@ use reth_node_core::{ version::{version_metadata, CLIENT_CODE}, }; use reth_payload_builder::{PayloadBuilderHandle, PayloadStore}; -use reth_rpc::eth::{core::EthRpcConverterFor, DevSigner, EthApiTypes, FullEthApiServer}; +use reth_rpc::{ + eth::{core::EthRpcConverterFor, DevSigner, EthApiTypes, FullEthApiServer}, + AdminApi, +}; use reth_rpc_api::{eth::helpers::EthTransactions, IntoEngineApiRpcModule}; use reth_rpc_builder::{ auth::{AuthRpcModule, AuthServerHandle}, @@ -383,6 +386,21 @@ impl RpcHandle { ) -> &EventSender::Primitives>> { &self.engine_events } + + /// Returns the `EthApi` instance of the rpc server. + pub const fn eth_api(&self) -> &EthApi { + self.rpc_registry.registry.eth_api() + } + + /// Returns an instance of the [`AdminApi`] for the rpc server. + pub fn admin_api( + &self, + ) -> AdminApi::ChainSpec, Node::Pool> + where + ::ChainSpec: EthereumHardforks, + { + self.rpc_registry.registry.admin_api() + } } /// Handle returned when only the regular RPC server (HTTP/WS/IPC) is launched. @@ -973,7 +991,12 @@ where ); let eth_config = config.rpc.eth_config().max_batch_size(config.txpool.max_batch_size()); - let ctx = EthApiCtx { components: &node, config: eth_config, cache }; + let ctx = EthApiCtx { + components: &node, + config: eth_config, + cache, + engine_handle: beacon_engine_handle.clone(), + }; let eth_api = eth_api_builder.build_eth_api(ctx).await?; let auth_config = config.rpc.auth_server_config(jwt_secret)?; @@ -1137,6 +1160,8 @@ pub struct EthApiCtx<'a, N: FullNodeTypes> { pub config: EthConfig, /// Cache for eth state pub cache: EthStateCache>, + /// Handle to the beacon consensus engine + pub engine_handle: ConsensusEngineHandle<::Payload>, } impl<'a, N: FullNodeComponents>> diff --git a/crates/node/core/Cargo.toml b/crates/node/core/Cargo.toml index 1d76786579..45b362a0c6 100644 --- a/crates/node/core/Cargo.toml +++ b/crates/node/core/Cargo.toml @@ -19,6 +19,7 @@ reth-cli-util.workspace = true reth-db = { workspace = true, features = ["mdbx"] } reth-storage-errors.workspace = true reth-storage-api = { workspace = true, features = ["std", "db-api"] } +reth-provider.workspace = true reth-network = { workspace = true, features = ["serde"] } reth-network-p2p.workspace = true reth-rpc-eth-types.workspace = true @@ -30,6 +31,7 @@ reth-config = { workspace = true, features = ["serde"] } reth-discv4.workspace = true reth-discv5.workspace = true reth-net-nat.workspace = true +reth-net-banlist.workspace = true reth-network-peers.workspace = true reth-prune-types.workspace = true reth-stages-types.workspace = true @@ -54,7 +56,7 @@ serde.workspace = true strum = { workspace = true, features = ["derive"] } thiserror.workspace = true url.workspace = true - +ipnet.workspace = true # io dirs-next.workspace = true shellexpand.workspace = true diff --git a/crates/node/core/src/args/database.rs b/crates/node/core/src/args/database.rs index 6384f36a80..298669e198 100644 --- a/crates/node/core/src/args/database.rs +++ b/crates/node/core/src/args/database.rs @@ -33,6 +33,18 @@ pub struct DatabaseArgs { /// The default value is 8TB. #[arg(long = "db.max-size", value_parser = parse_byte_size)] pub max_size: Option, + /// Database page size (e.g., 4KB, 8KB, 16KB). + /// + /// Specifies the page size used by the MDBX database. + /// + /// The page size determines the maximum database size. + /// MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum + /// database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + /// + /// WARNING: This setting is only configurable at database creation; changing + /// it later requires re-syncing. + #[arg(long = "db.page-size", value_parser = parse_byte_size)] + pub page_size: Option, /// Database growth step (e.g., 4GB, 4KB) #[arg(long = "db.growth-step", value_parser = parse_byte_size)] pub growth_step: Option, @@ -73,6 +85,7 @@ impl DatabaseArgs { .with_exclusive(self.exclusive) .with_max_read_transaction_duration(max_read_transaction_duration) .with_geometry_max_size(self.max_size) + .with_geometry_page_size(self.page_size) .with_growth_step(self.growth_step) .with_max_readers(self.max_readers) .with_sync_mode(self.sync_mode) @@ -305,6 +318,41 @@ mod tests { assert!(result.is_err()); } + #[test] + fn test_command_parser_with_valid_page_size_from_str() { + let cmd = CommandParser::::try_parse_from(["reth", "--db.page-size", "8KB"]) + .unwrap(); + assert_eq!(cmd.args.page_size, Some(KILOBYTE * 8)); + + let cmd = CommandParser::::try_parse_from(["reth", "--db.page-size", "1MB"]) + .unwrap(); + assert_eq!(cmd.args.page_size, Some(MEGABYTE)); + + // Test with spaces + let cmd = + CommandParser::::try_parse_from(["reth", "--db.page-size", "16 KB"]) + .unwrap(); + assert_eq!(cmd.args.page_size, Some(KILOBYTE * 16)); + + // Test with just a number (bytes) + let cmd = CommandParser::::try_parse_from(["reth", "--db.page-size", "4096"]) + .unwrap(); + assert_eq!(cmd.args.page_size, Some(KILOBYTE * 4)); + } + + #[test] + fn test_command_parser_with_invalid_page_size() { + // Invalid text + let result = + CommandParser::::try_parse_from(["reth", "--db.page-size", "invalid"]); + assert!(result.is_err()); + + // Invalid unit + let result = + CommandParser::::try_parse_from(["reth", "--db.page-size", "7 ZB"]); + assert!(result.is_err()); + } + #[test] fn test_possible_values() { // Initialize the LogLevelValueParser diff --git a/crates/node/core/src/args/mod.rs b/crates/node/core/src/args/mod.rs index e657d06ef4..8112d733fc 100644 --- a/crates/node/core/src/args/mod.rs +++ b/crates/node/core/src/args/mod.rs @@ -34,7 +34,7 @@ pub use metric::MetricArgs; /// `PayloadBuilderArgs` struct for configuring the payload builder mod payload_builder; -pub use payload_builder::PayloadBuilderArgs; +pub use payload_builder::{DefaultPayloadBuilderValues, PayloadBuilderArgs}; /// Stage related arguments mod stage; @@ -76,5 +76,9 @@ pub use ress_args::RessArgs; mod era; pub use era::{DefaultEraHost, EraArgs, EraSourceArgs}; +/// `StaticFilesArgs` for configuring static files. +mod static_files; +pub use static_files::StaticFilesArgs; + mod error; pub mod types; diff --git a/crates/node/core/src/args/network.rs b/crates/node/core/src/args/network.rs index 4e57839e3e..8faebdc9cc 100644 --- a/crates/node/core/src/args/network.rs +++ b/crates/node/core/src/args/network.rs @@ -1,5 +1,6 @@ //! clap [Args](clap::Args) for network related arguments. +use alloy_eips::BlockNumHash; use alloy_primitives::B256; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, @@ -10,16 +11,18 @@ use std::{ use crate::version::version_metadata; use clap::Args; use reth_chainspec::EthChainSpec; +use reth_cli_util::{get_secret_key, load_secret_key::SecretKeyError}; use reth_config::Config; use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT}; use reth_discv5::{ discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT, DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL, }; +use reth_net_banlist::IpFilter; use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME}; use reth_network::{ transactions::{ - config::TransactionPropagationKind, + config::{TransactionIngressPolicy, TransactionPropagationKind}, constants::{ tx_fetcher::{ DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, @@ -37,6 +40,7 @@ use reth_network::{ }; use reth_network_peers::{mainnet_nodes, TrustedPeer}; use secp256k1::SecretKey; +use std::str::FromStr; use tracing::error; /// Parameters for configuring the network more granularity via CLI @@ -81,9 +85,16 @@ pub struct NetworkArgs { /// /// This will also deterministically set the peer ID. If not specified, it will be set in the /// data dir for the chain being used. - #[arg(long, value_name = "PATH")] + #[arg(long, value_name = "PATH", conflicts_with = "p2p_secret_key_hex")] pub p2p_secret_key: Option, + /// Hex encoded secret key to use for this node. + /// + /// This will also deterministically set the peer ID. Cannot be used together with + /// `--p2p-secret-key`. + #[arg(long, value_name = "HEX", conflicts_with = "p2p_secret_key")] + pub p2p_secret_key_hex: Option, + /// Do not persist peers. #[arg(long, verbatim_doc_comment)] pub no_persist_peers: bool, @@ -162,6 +173,12 @@ pub struct NetworkArgs { #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)] pub tx_propagation_policy: TransactionPropagationKind, + /// Transaction ingress policy + /// + /// Determines which peers' transactions are accepted over P2P. + #[arg(long = "tx-ingress-policy", default_value_t = TransactionIngressPolicy::All)] + pub tx_ingress_policy: TransactionIngressPolicy, + /// Disable transaction pool gossip /// /// Disables gossiping of transactions in the mempool to peers. This can be omitted for @@ -180,14 +197,24 @@ pub struct NetworkArgs { )] pub propagation_mode: TransactionPropagationMode, - /// Comma separated list of required block hashes. + /// Comma separated list of required block hashes or block number=hash pairs. /// Peers that don't have these blocks will be filtered out. - #[arg(long = "required-block-hashes", value_delimiter = ',')] - pub required_block_hashes: Vec, + /// Format: hash or `block_number=hash` (e.g., 23115201=0x1234...) + #[arg(long = "required-block-hashes", value_delimiter = ',', value_parser = parse_block_num_hash)] + pub required_block_hashes: Vec, /// Optional network ID to override the chain specification's network ID for P2P connections #[arg(long)] pub network_id: Option, + + /// Restrict network communication to the given IP networks (CIDR masks). + /// + /// Comma separated list of CIDR network specifications. + /// Only peers with IP addresses within these ranges will be allowed to connect. + /// + /// Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + #[arg(long, value_name = "NETRESTRICT")] + pub netrestrict: Option, } impl NetworkArgs { @@ -230,6 +257,7 @@ impl NetworkArgs { ), max_transactions_seen_by_peer_history: self.max_seen_tx_history, propagation_mode: self.propagation_mode, + ingress_policy: self.tx_ingress_policy, } } @@ -258,11 +286,13 @@ impl NetworkArgs { let peers_file = self.peers_file.clone().unwrap_or(default_peers_file); // Configure peer connections + let ip_filter = self.ip_filter().unwrap_or_default(); let peers_config = config .peers .clone() .with_max_inbound_opt(self.max_inbound_peers) - .with_max_outbound_opt(self.max_outbound_peers); + .with_max_outbound_opt(self.max_outbound_peers) + .with_ip_filter(ip_filter); // Configure basic network stack NetworkConfigBuilder::::new(secret_key) @@ -309,6 +339,12 @@ impl NetworkArgs { self.no_persist_peers.not().then_some(peers_file) } + /// Configures the [`DiscoveryArgs`]. + pub const fn with_discovery(mut self, discovery: DiscoveryArgs) -> Self { + self.discovery = discovery; + self + } + /// Sets the p2p port to zero, to allow the OS to assign a random unused port when /// the network components bind to a socket. pub const fn with_unused_p2p_port(mut self) -> Self { @@ -324,6 +360,12 @@ impl NetworkArgs { self } + /// Configures the [`NatResolver`] + pub const fn with_nat_resolver(mut self, nat: NatResolver) -> Self { + self.nat = nat; + self + } + /// Change networking port numbers based on the instance number, if provided. /// Ports are updated to `previous_value + instance - 1` /// @@ -344,6 +386,36 @@ impl NetworkArgs { ) .await } + + /// Load the p2p secret key from the provided options. + /// + /// If `p2p_secret_key_hex` is provided, it will be used directly. + /// If `p2p_secret_key` is provided, it will be loaded from the file. + /// If neither is provided, the `default_secret_key_path` will be used. + pub fn secret_key( + &self, + default_secret_key_path: PathBuf, + ) -> Result { + if let Some(b256) = &self.p2p_secret_key_hex { + // Use the B256 value directly (already validated as 32 bytes) + SecretKey::from_slice(b256.as_slice()).map_err(SecretKeyError::SecretKeyDecodeError) + } else { + // Load from file (either provided path or default) + let secret_key_path = self.p2p_secret_key.clone().unwrap_or(default_secret_key_path); + get_secret_key(&secret_key_path) + } + } + + /// Creates an IP filter from the netrestrict argument. + /// + /// Returns an error if the CIDR format is invalid. + pub fn ip_filter(&self) -> Result { + if let Some(netrestrict) = &self.netrestrict { + IpFilter::from_cidr_string(netrestrict) + } else { + Ok(IpFilter::allow_all()) + } + } } impl Default for NetworkArgs { @@ -357,6 +429,7 @@ impl Default for NetworkArgs { peers_file: None, identity: version_metadata().p2p_client_version.to_string(), p2p_secret_key: None, + p2p_secret_key_hex: None, no_persist_peers: false, nat: NatResolver::Any, addr: DEFAULT_DISCOVERY_ADDR, @@ -373,10 +446,12 @@ impl Default for NetworkArgs { max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, net_if: None, tx_propagation_policy: TransactionPropagationKind::default(), + tx_ingress_policy: TransactionIngressPolicy::default(), disable_tx_gossip: false, propagation_mode: TransactionPropagationMode::Sqrt, required_block_hashes: vec![], network_id: None, + netrestrict: None, } } } @@ -542,6 +617,12 @@ impl DiscoveryArgs { self } + /// Set the discovery V5 port + pub const fn with_discv5_port(mut self, port: u16) -> Self { + self.discv5_port = port; + self + } + /// Change networking port numbers based on the instance number. /// Ports are updated to `previous_value + instance - 1` /// @@ -576,6 +657,19 @@ impl Default for DiscoveryArgs { } } +/// Parse a block number=hash pair or just a hash into `BlockNumHash` +fn parse_block_num_hash(s: &str) -> Result { + if let Some((num_str, hash_str)) = s.split_once('=') { + let number = num_str.parse().map_err(|_| format!("Invalid block number: {}", num_str))?; + let hash = B256::from_str(hash_str).map_err(|_| format!("Invalid hash: {}", hash_str))?; + Ok(BlockNumHash::new(number, hash)) + } else { + // For backward compatibility, treat as hash-only with number 0 + let hash = B256::from_str(s).map_err(|_| format!("Invalid hash: {}", s))?; + Ok(BlockNumHash::new(0, hash)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -670,17 +764,21 @@ mod tests { let args = CommandParser::::parse_from([ "reth", "--required-block-hashes", - "0x1111111111111111111111111111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222222222222222222222222222", + "0x1111111111111111111111111111111111111111111111111111111111111111,23115201=0x2222222222222222222222222222222222222222222222222222222222222222", ]) .args; assert_eq!(args.required_block_hashes.len(), 2); + // First hash without block number (should default to 0) + assert_eq!(args.required_block_hashes[0].number, 0); assert_eq!( - args.required_block_hashes[0].to_string(), + args.required_block_hashes[0].hash.to_string(), "0x1111111111111111111111111111111111111111111111111111111111111111" ); + // Second with block number=hash format + assert_eq!(args.required_block_hashes[1].number, 23115201); assert_eq!( - args.required_block_hashes[1].to_string(), + args.required_block_hashes[1].hash.to_string(), "0x2222222222222222222222222222222222222222222222222222222222222222" ); } @@ -690,4 +788,141 @@ mod tests { let args = CommandParser::::parse_from(["reth"]).args; assert!(args.required_block_hashes.is_empty()); } + + #[test] + fn test_parse_block_num_hash() { + // Test hash only format + let result = parse_block_num_hash( + "0x1111111111111111111111111111111111111111111111111111111111111111", + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap().number, 0); + + // Test block_number=hash format + let result = parse_block_num_hash( + "23115201=0x2222222222222222222222222222222222222222222222222222222222222222", + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap().number, 23115201); + + // Test invalid formats + assert!(parse_block_num_hash("invalid").is_err()); + assert!(parse_block_num_hash( + "abc=0x1111111111111111111111111111111111111111111111111111111111111111" + ) + .is_err()); + } + + #[test] + fn parse_p2p_secret_key_hex() { + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let expected: B256 = hex.parse().unwrap(); + assert_eq!(args.p2p_secret_key_hex, Some(expected)); + assert_eq!(args.p2p_secret_key, None); + } + + #[test] + fn parse_p2p_secret_key_hex_with_0x_prefix() { + let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let expected: B256 = hex.parse().unwrap(); + assert_eq!(args.p2p_secret_key_hex, Some(expected)); + assert_eq!(args.p2p_secret_key, None); + } + + #[test] + fn test_p2p_secret_key_and_hex_are_mutually_exclusive() { + let result = CommandParser::::try_parse_from([ + "reth", + "--p2p-secret-key", + "/path/to/key", + "--p2p-secret-key-hex", + "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f", + ]); + + assert!(result.is_err()); + } + + #[test] + fn test_secret_key_method_with_hex() { + let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f"; + let args = + CommandParser::::parse_from(["reth", "--p2p-secret-key-hex", hex]).args; + + let temp_dir = std::env::temp_dir(); + let default_path = temp_dir.join("default_key"); + let secret_key = args.secret_key(default_path).unwrap(); + + // Verify the secret key matches the hex input + assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex); + } + + #[test] + fn parse_netrestrict_single_network() { + let args = + CommandParser::::parse_from(["reth", "--netrestrict", "192.168.0.0/16"]) + .args; + + assert_eq!(args.netrestrict, Some("192.168.0.0/16".to_string())); + + let ip_filter = args.ip_filter().unwrap(); + assert!(ip_filter.has_restrictions()); + assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap())); + assert!(!ip_filter.is_allowed(&"10.0.0.1".parse().unwrap())); + } + + #[test] + fn parse_netrestrict_multiple_networks() { + let args = CommandParser::::parse_from([ + "reth", + "--netrestrict", + "192.168.0.0/16,10.0.0.0/8", + ]) + .args; + + assert_eq!(args.netrestrict, Some("192.168.0.0/16,10.0.0.0/8".to_string())); + + let ip_filter = args.ip_filter().unwrap(); + assert!(ip_filter.has_restrictions()); + assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap())); + assert!(ip_filter.is_allowed(&"10.5.10.20".parse().unwrap())); + assert!(!ip_filter.is_allowed(&"172.16.0.1".parse().unwrap())); + } + + #[test] + fn parse_netrestrict_ipv6() { + let args = + CommandParser::::parse_from(["reth", "--netrestrict", "2001:db8::/32"]) + .args; + + let ip_filter = args.ip_filter().unwrap(); + assert!(ip_filter.has_restrictions()); + assert!(ip_filter.is_allowed(&"2001:db8::1".parse().unwrap())); + assert!(!ip_filter.is_allowed(&"2001:db9::1".parse().unwrap())); + } + + #[test] + fn netrestrict_not_set() { + let args = CommandParser::::parse_from(["reth"]).args; + assert_eq!(args.netrestrict, None); + + let ip_filter = args.ip_filter().unwrap(); + assert!(!ip_filter.has_restrictions()); + assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap())); + assert!(ip_filter.is_allowed(&"10.0.0.1".parse().unwrap())); + } + + #[test] + fn netrestrict_invalid_cidr() { + let args = + CommandParser::::parse_from(["reth", "--netrestrict", "invalid-cidr"]) + .args; + + assert!(args.ip_filter().is_err()); + } } diff --git a/crates/node/core/src/args/payload_builder.rs b/crates/node/core/src/args/payload_builder.rs index 181648cc7a..d9ce66499f 100644 --- a/crates/node/core/src/args/payload_builder.rs +++ b/crates/node/core/src/args/payload_builder.rs @@ -1,19 +1,90 @@ use crate::{cli::config::PayloadBuilderConfig, version::default_extra_data}; use alloy_consensus::constants::MAXIMUM_EXTRA_DATA_SIZE; -use alloy_eips::merge::SLOT_DURATION; use clap::{ builder::{RangedU64ValueParser, TypedValueParser}, Arg, Args, Command, }; -use reth_cli_util::{parse_duration_from_secs, parse_duration_from_secs_or_ms}; -use std::{borrow::Cow, ffi::OsStr, time::Duration}; +use reth_cli_util::{ + parse_duration_from_secs, parse_duration_from_secs_or_ms, + parsers::format_duration_as_secs_or_ms, +}; +use std::{borrow::Cow, ffi::OsStr, sync::OnceLock, time::Duration}; + +/// Global static payload builder defaults +static PAYLOAD_BUILDER_DEFAULTS: OnceLock = OnceLock::new(); + +/// Default values for payload builder that can be customized +/// +/// Global defaults can be set via [`DefaultPayloadBuilderValues::try_init`]. +#[derive(Debug, Clone)] +pub struct DefaultPayloadBuilderValues { + /// Default extra data for blocks + extra_data: String, + /// Default interval between payload builds in seconds + interval: String, + /// Default deadline for payload builds in seconds + deadline: String, + /// Default maximum number of concurrent payload building tasks + max_payload_tasks: usize, +} + +impl DefaultPayloadBuilderValues { + /// Initialize the global payload builder defaults with this configuration + pub fn try_init(self) -> Result<(), Self> { + PAYLOAD_BUILDER_DEFAULTS.set(self) + } + + /// Get a reference to the global payload builder defaults + pub fn get_global() -> &'static Self { + PAYLOAD_BUILDER_DEFAULTS.get_or_init(Self::default) + } + + /// Set the default extra data + pub fn with_extra_data(mut self, v: impl Into) -> Self { + self.extra_data = v.into(); + self + } + + /// Set the default interval in seconds + pub fn with_interval(mut self, v: Duration) -> Self { + self.interval = format_duration_as_secs_or_ms(v); + self + } + + /// Set the default deadline in seconds + pub fn with_deadline(mut self, v: u64) -> Self { + self.deadline = format!("{}", v); + self + } + + /// Set the default maximum payload tasks + pub const fn with_max_payload_tasks(mut self, v: usize) -> Self { + self.max_payload_tasks = v; + self + } +} + +impl Default for DefaultPayloadBuilderValues { + fn default() -> Self { + Self { + extra_data: default_extra_data(), + interval: "1".to_string(), + deadline: "12".to_string(), + max_payload_tasks: 3, + } + } +} /// Parameters for configuring the Payload Builder #[derive(Debug, Clone, Args, PartialEq, Eq)] #[command(next_help_heading = "Builder")] pub struct PayloadBuilderArgs { /// Block extra data set by the payload builder. - #[arg(long = "builder.extradata", value_parser = ExtraDataValueParser::default(), default_value_t = default_extra_data())] + #[arg( + long = "builder.extradata", + value_parser = ExtraDataValueParser::default(), + default_value_t = DefaultPayloadBuilderValues::get_global().extra_data.clone() + )] pub extra_data: String, /// Target gas limit for built blocks. @@ -25,15 +96,29 @@ pub struct PayloadBuilderArgs { /// Interval is specified in seconds or in milliseconds if the value ends with `ms`: /// * `50ms` -> 50 milliseconds /// * `1` -> 1 second - #[arg(long = "builder.interval", value_parser = parse_duration_from_secs_or_ms, default_value = "1", value_name = "DURATION")] + #[arg( + long = "builder.interval", + value_parser = parse_duration_from_secs_or_ms, + default_value = DefaultPayloadBuilderValues::get_global().interval.as_str(), + value_name = "DURATION" + )] pub interval: Duration, /// The deadline for when the payload builder job should resolve. - #[arg(long = "builder.deadline", value_parser = parse_duration_from_secs, default_value = "12", value_name = "SECONDS")] + #[arg( + long = "builder.deadline", + value_parser = parse_duration_from_secs, + default_value = DefaultPayloadBuilderValues::get_global().deadline.as_str(), + value_name = "SECONDS" + )] pub deadline: Duration, /// Maximum number of tasks to spawn for building a payload. - #[arg(long = "builder.max-tasks", default_value = "3", value_parser = RangedU64ValueParser::::new().range(1..))] + #[arg( + long = "builder.max-tasks", + value_parser = RangedU64ValueParser::::new().range(1..), + default_value_t = DefaultPayloadBuilderValues::get_global().max_payload_tasks + )] pub max_payload_tasks: usize, /// Maximum number of blobs to include per block. @@ -43,12 +128,13 @@ pub struct PayloadBuilderArgs { impl Default for PayloadBuilderArgs { fn default() -> Self { + let defaults = DefaultPayloadBuilderValues::get_global(); Self { - extra_data: default_extra_data(), - interval: Duration::from_secs(1), + extra_data: defaults.extra_data.clone(), + interval: parse_duration_from_secs_or_ms(defaults.interval.as_str()).unwrap(), gas_limit: None, - deadline: SLOT_DURATION, - max_payload_tasks: 3, + deadline: Duration::from_secs(defaults.deadline.parse().unwrap()), + max_payload_tasks: defaults.max_payload_tasks, max_blobs_per_block: None, } } diff --git a/crates/node/core/src/args/static_files.rs b/crates/node/core/src/args/static_files.rs new file mode 100644 index 0000000000..9de5b14643 --- /dev/null +++ b/crates/node/core/src/args/static_files.rs @@ -0,0 +1,56 @@ +//! clap [Args](clap::Args) for static files configuration + +use clap::Args; +use reth_config::config::{BlocksPerFileConfig, StaticFilesConfig}; +use reth_provider::StorageSettings; + +/// Parameters for static files configuration +#[derive(Debug, Args, PartialEq, Eq, Default, Clone, Copy)] +#[command(next_help_heading = "Static Files")] +pub struct StaticFilesArgs { + /// Number of blocks per file for the headers segment. + #[arg(long = "static-files.blocks-per-file.headers")] + pub blocks_per_file_headers: Option, + + /// Number of blocks per file for the transactions segment. + #[arg(long = "static-files.blocks-per-file.transactions")] + pub blocks_per_file_transactions: Option, + + /// Number of blocks per file for the receipts segment. + #[arg(long = "static-files.blocks-per-file.receipts")] + pub blocks_per_file_receipts: Option, + + /// Store receipts in static files instead of the database. + /// + /// When enabled, receipts will be written to static files on disk instead of the database. + /// + /// Note: This setting can only be configured at genesis initialization. Once + /// the node has been initialized, changing this flag requires re-syncing from scratch. + #[arg(long = "static-files.receipts")] + pub receipts: bool, +} + +impl StaticFilesArgs { + /// Merges the CLI arguments with an existing [`StaticFilesConfig`], giving priority to CLI + /// args. + pub fn merge_with_config(&self, config: StaticFilesConfig) -> StaticFilesConfig { + StaticFilesConfig { + blocks_per_file: BlocksPerFileConfig { + headers: self.blocks_per_file_headers.or(config.blocks_per_file.headers), + transactions: self + .blocks_per_file_transactions + .or(config.blocks_per_file.transactions), + receipts: self.blocks_per_file_receipts.or(config.blocks_per_file.receipts), + }, + } + } + + /// Converts the static files arguments into [`StorageSettings`]. + pub const fn to_settings(&self) -> StorageSettings { + if self.receipts { + StorageSettings::new().with_receipts_in_static_files() + } else { + StorageSettings::legacy() + } + } +} diff --git a/crates/node/core/src/node_config.rs b/crates/node/core/src/node_config.rs index 64b469086e..1d5b1700cb 100644 --- a/crates/node/core/src/node_config.rs +++ b/crates/node/core/src/node_config.rs @@ -3,7 +3,7 @@ use crate::{ args::{ DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, NetworkArgs, PayloadBuilderArgs, - PruningArgs, RpcServerArgs, TxPoolArgs, + PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs, }, dirs::{ChainPath, DataDirPath}, utils::get_single_header, @@ -147,6 +147,9 @@ pub struct NodeConfig { /// All ERA import related arguments with --era prefix pub era: EraArgs, + + /// All static files related arguments + pub static_files: StaticFilesArgs, } impl NodeConfig { @@ -177,6 +180,7 @@ impl NodeConfig { datadir: DatadirArgs::default(), engine: EngineArgs::default(), era: EraArgs::default(), + static_files: StaticFilesArgs::default(), } } @@ -233,6 +237,46 @@ impl NodeConfig { self } + /// Set the [`ChainSpec`] for the node and converts the type to that chainid. + pub fn map_chain(self, chain: impl Into>) -> NodeConfig { + let Self { + datadir, + config, + metrics, + instance, + network, + rpc, + txpool, + builder, + debug, + db, + dev, + pruning, + engine, + era, + static_files, + .. + } = self; + NodeConfig { + datadir, + config, + chain: chain.into(), + metrics, + instance, + network, + rpc, + txpool, + builder, + debug, + db, + dev, + pruning, + engine, + era, + static_files, + } + } + /// Set the metrics address for the node pub fn with_metrics(mut self, metrics: MetricArgs) -> Self { self.metrics = metrics; @@ -499,6 +543,7 @@ impl NodeConfig { pruning: self.pruning, engine: self.engine, era: self.era, + static_files: self.static_files, } } @@ -539,6 +584,7 @@ impl Clone for NodeConfig { datadir: self.datadir.clone(), engine: self.engine.clone(), era: self.era.clone(), + static_files: self.static_files, } } } diff --git a/crates/node/ethstats/src/ethstats.rs b/crates/node/ethstats/src/ethstats.rs index 7592e93ae9..275ef6c8cf 100644 --- a/crates/node/ethstats/src/ethstats.rs +++ b/crates/node/ethstats/src/ethstats.rs @@ -118,7 +118,7 @@ where "Successfully connected to EthStats server at {}", self.credentials.host ); let conn: ConnWrapper = ConnWrapper::new(ws_stream); - *self.conn.write().await = Some(conn.clone()); + *self.conn.write().await = Some(conn); self.login().await?; Ok(()) } @@ -558,24 +558,32 @@ where // Start the read loop in a separate task let read_handle = { - let conn = self.conn.clone(); + let conn_arc = self.conn.clone(); let message_tx = message_tx.clone(); let shutdown_tx = shutdown_tx.clone(); tokio::spawn(async move { loop { - let conn = conn.read().await; - if let Some(conn) = conn.as_ref() { + let conn_guard = conn_arc.read().await; + if let Some(conn) = conn_guard.as_ref() { match conn.read_json().await { Ok(msg) => { if message_tx.send(msg).await.is_err() { break; } } - Err(e) => { - debug!(target: "ethstats", "Read error: {}", e); - break; - } + Err(e) => match e { + crate::error::ConnectionError::Serialization(err) => { + debug!(target: "ethstats", "JSON parse error from stats server: {}", err); + } + other => { + debug!(target: "ethstats", "Read error: {}", other); + drop(conn_guard); + if let Some(conn) = conn_arc.write().await.take() { + let _ = conn.close().await; + } + } + }, } } else { sleep(RECONNECT_INTERVAL).await; @@ -658,10 +666,12 @@ where } // Handle reconnection - _ = reconnect_interval.tick(), if self.conn.read().await.is_none() => { - match self.connect().await { - Ok(_) => info!(target: "ethstats", "Reconnected successfully"), - Err(e) => debug!(target: "ethstats", "Reconnect failed: {}", e), + _ = reconnect_interval.tick() => { + if self.conn.read().await.is_none() { + match self.connect().await { + Ok(_) => info!(target: "ethstats", "Reconnected successfully"), + Err(e) => debug!(target: "ethstats", "Reconnect failed: {}", e), + } } } } diff --git a/crates/optimism/chainspec/res/superchain-configs.tar b/crates/optimism/chainspec/res/superchain-configs.tar index 80345a2843..2ed30f474b 100644 Binary files a/crates/optimism/chainspec/res/superchain-configs.tar and b/crates/optimism/chainspec/res/superchain-configs.tar differ diff --git a/crates/optimism/chainspec/res/superchain_registry_commit b/crates/optimism/chainspec/res/superchain_registry_commit index d37cde1bb4..239646ec04 100644 --- a/crates/optimism/chainspec/res/superchain_registry_commit +++ b/crates/optimism/chainspec/res/superchain_registry_commit @@ -1 +1 @@ -9e3f71cee0e4e2acb4864cb00f5fbee3555d8e9f +59e22d265b7a423b7f51a67a722471a6f3c3cc39 diff --git a/crates/optimism/consensus/src/lib.rs b/crates/optimism/consensus/src/lib.rs index 36033e9d59..b6462ec487 100644 --- a/crates/optimism/consensus/src/lib.rs +++ b/crates/optimism/consensus/src/lib.rs @@ -12,7 +12,9 @@ extern crate alloc; use alloc::{format, sync::Arc}; -use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH}; +use alloy_consensus::{ + constants::MAXIMUM_EXTRA_DATA_SIZE, BlockHeader as _, EMPTY_OMMER_ROOT_HASH, +}; use alloy_primitives::B64; use core::fmt::Debug; use reth_chainspec::EthChainSpec; @@ -46,12 +48,25 @@ pub use error::OpConsensusError; pub struct OpBeaconConsensus { /// Configuration chain_spec: Arc, + /// Maximum allowed extra data size in bytes + max_extra_data_size: usize, } impl OpBeaconConsensus { /// Create a new instance of [`OpBeaconConsensus`] pub const fn new(chain_spec: Arc) -> Self { - Self { chain_spec } + Self { chain_spec, max_extra_data_size: MAXIMUM_EXTRA_DATA_SIZE } + } + + /// Returns the maximum allowed extra data size. + pub const fn max_extra_data_size(&self) -> usize { + self.max_extra_data_size + } + + /// Sets the maximum allowed extra data size and returns the updated instance. + pub const fn with_max_extra_data_size(mut self, size: usize) -> Self { + self.max_extra_data_size = size; + self } } @@ -166,7 +181,7 @@ where // is greater than its parent timestamp. // validate header extra data for all networks post merge - validate_header_extra_data(header)?; + validate_header_extra_data(header, self.max_extra_data_size)?; validate_header_gas(header)?; validate_header_base_fee(header, &self.chain_spec) } diff --git a/crates/optimism/evm/Cargo.toml b/crates/optimism/evm/Cargo.toml index 12390a7ec0..724f8555e0 100644 --- a/crates/optimism/evm/Cargo.toml +++ b/crates/optimism/evm/Cargo.toml @@ -80,4 +80,4 @@ portable = [ "op-revm/portable", "revm/portable", ] -rpc = ["reth-rpc-eth-api", "reth-optimism-primitives/serde", "reth-optimism-primitives/reth-codec"] +rpc = ["reth-rpc-eth-api", "reth-optimism-primitives/serde", "reth-optimism-primitives/reth-codec", "alloy-evm/rpc"] diff --git a/crates/optimism/evm/src/config.rs b/crates/optimism/evm/src/config.rs index 6ae2a91a6c..1f1068c40d 100644 --- a/crates/optimism/evm/src/config.rs +++ b/crates/optimism/evm/src/config.rs @@ -1,6 +1,7 @@ pub use alloy_op_evm::{ spec as revm_spec, spec_by_timestamp_after_bedrock as revm_spec_by_timestamp_after_bedrock, }; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use revm::primitives::{Address, Bytes, B256}; /// Context relevant for execution of a next block w.r.t OP. @@ -35,3 +36,16 @@ impl reth_rpc_eth_api::helpers::pending_block:: } } } + +impl From for OpNextBlockEnvAttributes { + fn from(base: OpFlashblockPayloadBase) -> Self { + Self { + timestamp: base.timestamp, + suggested_fee_recipient: base.fee_recipient, + prev_randao: base.prev_randao, + gas_limit: base.gas_limit, + parent_beacon_block_root: Some(base.parent_beacon_block_root), + extra_data: base.extra_data, + } + } +} diff --git a/crates/optimism/flashblocks/Cargo.toml b/crates/optimism/flashblocks/Cargo.toml index 977e28d37e..3bb0038a51 100644 --- a/crates/optimism/flashblocks/Cargo.toml +++ b/crates/optimism/flashblocks/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] # reth reth-optimism-primitives = { workspace = true, features = ["serde"] } -reth-optimism-evm.workspace = true reth-chain-state = { workspace = true, features = ["serde"] } reth-primitives-traits = { workspace = true, features = ["serde"] } reth-engine-primitives = { workspace = true, features = ["std"] } @@ -30,15 +29,16 @@ reth-metrics.workspace = true # alloy alloy-eips = { workspace = true, features = ["serde"] } -alloy-serde.workspace = true alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types-engine = { workspace = true, features = ["serde"] } alloy-consensus.workspace = true +# op-alloy +op-alloy-rpc-types-engine.workspace = true + # io tokio.workspace = true tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] } -serde.workspace = true serde_json.workspace = true url.workspace = true futures-util.workspace = true diff --git a/crates/optimism/flashblocks/src/consensus.rs b/crates/optimism/flashblocks/src/consensus.rs index 60314d2f6c..65926a4d91 100644 --- a/crates/optimism/flashblocks/src/consensus.rs +++ b/crates/optimism/flashblocks/src/consensus.rs @@ -1,86 +1,271 @@ -use crate::FlashBlockCompleteSequenceRx; +use crate::{FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx}; use alloy_primitives::B256; +use alloy_rpc_types_engine::PayloadStatusEnum; +use op_alloy_rpc_types_engine::OpExecutionData; use reth_engine_primitives::ConsensusEngineHandle; use reth_optimism_payload_builder::OpPayloadTypes; -use reth_payload_primitives::EngineApiMessageVersion; +use reth_payload_primitives::{EngineApiMessageVersion, ExecutionPayload, PayloadTypes}; use ringbuffer::{AllocRingBuffer, RingBuffer}; -use tracing::warn; +use tracing::*; + +/// Cache entry for block information: (block hash, block number, timestamp). +type BlockCacheEntry = (B256, u64, u64); /// Consensus client that sends FCUs and new payloads using blocks from a [`FlashBlockService`] /// /// [`FlashBlockService`]: crate::FlashBlockService #[derive(Debug)] -pub struct FlashBlockConsensusClient { +pub struct FlashBlockConsensusClient

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

, sequence_receiver: FlashBlockCompleteSequenceRx, + /// Caches previous block info for lookup: (block hash, block number, timestamp). + block_hash_buffer: AllocRingBuffer, } -impl FlashBlockConsensusClient { +impl

FlashBlockConsensusClient

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

, sequence_receiver: FlashBlockCompleteSequenceRx, ) -> eyre::Result { - Ok(Self { engine_handle, sequence_receiver }) + // Buffer size of 768 blocks (64 * 12) supports 1s block time chains like Unichain. + // Oversized for 2s block time chains like Base, but acceptable given minimal memory usage. + let block_hash_buffer = AllocRingBuffer::new(768); + Ok(Self { engine_handle, sequence_receiver, block_hash_buffer }) } - /// Get previous block hash using previous block hash buffer. If it isn't available (buffer - /// started more recently than `offset`), return default zero hash - fn get_previous_block_hash( + /// Return the safe and finalized block hash for FCU calls. + /// + /// Safe blocks are considered 32 L1 blocks (approximately 384s at 12s/block) behind the head, + /// and finalized blocks are 64 L1 blocks (approximately 768s) behind the head. This + /// approximation, while not precisely matching the OP stack's derivation, provides + /// sufficient proximity and enables op-reth to sync the chain independently of an op-node. + /// The offset is dynamically adjusted based on the actual block time detected from the + /// buffer. + fn get_safe_and_finalized_block_hash(&self) -> (B256, B256) { + let cached_blocks_count = self.block_hash_buffer.len(); + + // Not enough blocks to determine safe/finalized yet + if cached_blocks_count < 2 { + return (B256::ZERO, B256::ZERO); + } + + // Calculate average block time using block numbers to handle missing blocks correctly. + // By dividing timestamp difference by block number difference, we get accurate block + // time even when blocks are missing from the buffer. + let (_, latest_block_number, latest_timestamp) = + self.block_hash_buffer.get(cached_blocks_count - 1).unwrap(); + let (_, previous_block_number, previous_timestamp) = + self.block_hash_buffer.get(cached_blocks_count - 2).unwrap(); + let timestamp_delta = latest_timestamp.saturating_sub(*previous_timestamp); + let block_number_delta = latest_block_number.saturating_sub(*previous_block_number).max(1); + let block_time_secs = timestamp_delta / block_number_delta; + + // L1 reference: 32 blocks * 12s = 384s for safe, 64 blocks * 12s = 768s for finalized + const SAFE_TIME_SECS: u64 = 384; + const FINALIZED_TIME_SECS: u64 = 768; + + // Calculate how many L2 blocks correspond to these L1 time periods + let safe_block_offset = + (SAFE_TIME_SECS / block_time_secs).min(cached_blocks_count as u64) as usize; + let finalized_block_offset = + (FINALIZED_TIME_SECS / block_time_secs).min(cached_blocks_count as u64) as usize; + + // Get safe hash: offset from end of buffer + let safe_hash = self + .block_hash_buffer + .get(cached_blocks_count.saturating_sub(safe_block_offset)) + .map(|&(hash, _, _)| hash) + .unwrap(); + + // Get finalized hash: offset from end of buffer + let finalized_hash = self + .block_hash_buffer + .get(cached_blocks_count.saturating_sub(finalized_block_offset)) + .map(|&(hash, _, _)| hash) + .unwrap(); + + (safe_hash, finalized_hash) + } + + /// Receive the next flashblock sequence and cache its block information. + /// + /// Returns `None` if receiving fails (error is already logged). + async fn receive_and_cache_sequence(&mut self) -> Option { + match self.sequence_receiver.recv().await { + Ok(sequence) => { + self.block_hash_buffer.push(( + sequence.payload_base().parent_hash, + sequence.block_number(), + sequence.payload_base().timestamp, + )); + Some(sequence) + } + Err(err) => { + error!( + target: "flashblocks", + %err, + "error while fetching flashblock completed sequence", + ); + None + } + } + } + + /// Convert a flashblock sequence to an execution payload. + /// + /// Returns `None` if conversion fails (error is already logged). + fn convert_sequence_to_payload( &self, - previous_block_hashes: &AllocRingBuffer, - offset: usize, - ) -> B256 { - *previous_block_hashes - .len() - .checked_sub(offset) - .and_then(|index| previous_block_hashes.get(index)) - .unwrap_or_default() + sequence: &FlashBlockCompleteSequence, + ) -> Option { + match P::ExecutionData::try_from(sequence) { + Ok(payload) => Some(payload), + Err(err) => { + error!( + target: "flashblocks", + %err, + "error while converting to payload from completed sequence", + ); + None + } + } + } + + /// Submit a new payload to the engine. + /// + /// Returns `Ok(block_hash)` if the payload was accepted, `Err(())` otherwise (errors are + /// logged). + async fn submit_new_payload( + &self, + payload: P::ExecutionData, + sequence: &FlashBlockCompleteSequence, + ) -> Result { + let block_number = payload.block_number(); + let block_hash = payload.block_hash(); + + match self.engine_handle.new_payload(payload).await { + Ok(result) => { + debug!( + target: "flashblocks", + flashblock_count = sequence.count(), + block_number, + %block_hash, + ?result, + "Submitted engine_newPayload", + ); + + if let PayloadStatusEnum::Invalid { validation_error } = result.status { + debug!( + target: "flashblocks", + block_number, + %block_hash, + %validation_error, + "Payload validation error", + ); + return Err(()); + } + + Ok(block_hash) + } + Err(err) => { + error!( + target: "flashblocks", + %err, + block_number, + "Failed to submit new payload", + ); + Err(()) + } + } + } + + /// Submit a forkchoice update to the engine. + async fn submit_forkchoice_update( + &self, + head_block_hash: B256, + sequence: &FlashBlockCompleteSequence, + ) { + let block_number = sequence.block_number(); + let (safe_hash, finalized_hash) = self.get_safe_and_finalized_block_hash(); + let fcu_state = alloy_rpc_types_engine::ForkchoiceState { + head_block_hash, + safe_block_hash: safe_hash, + finalized_block_hash: finalized_hash, + }; + + match self + .engine_handle + .fork_choice_updated(fcu_state, None, EngineApiMessageVersion::V5) + .await + { + Ok(result) => { + debug!( + target: "flashblocks", + flashblock_count = sequence.count(), + block_number, + %head_block_hash, + %safe_hash, + %finalized_hash, + ?result, + "Submitted engine_forkChoiceUpdated", + ) + } + Err(err) => { + error!( + target: "flashblocks", + %err, + block_number, + %head_block_hash, + %safe_hash, + %finalized_hash, + "Failed to submit fork choice update", + ); + } + } } /// Spawn the client to start sending FCUs and new payloads by periodically fetching recent /// blocks. pub async fn run(mut self) { - let mut previous_block_hashes = AllocRingBuffer::new(64); - loop { - match self.sequence_receiver.recv().await { - Ok(sequence) => { - let block_hash = sequence.payload_base().parent_hash; - previous_block_hashes.push(block_hash); + let Some(sequence) = self.receive_and_cache_sequence().await else { + continue; + }; - if sequence.state_root().is_none() { - warn!("Missing state root for the complete sequence") - } + let Some(payload) = self.convert_sequence_to_payload(&sequence) else { + continue; + }; - // Load previous block hashes. We're using (head - 32) and (head - 64) as the - // safe and finalized block hashes. - let safe_block_hash = self.get_previous_block_hash(&previous_block_hashes, 32); - let finalized_block_hash = - self.get_previous_block_hash(&previous_block_hashes, 64); + let Ok(block_hash) = self.submit_new_payload(payload, &sequence).await else { + continue; + }; - let state = alloy_rpc_types_engine::ForkchoiceState { - head_block_hash: block_hash, - safe_block_hash, - finalized_block_hash, - }; - - // Send FCU - let _ = self - .engine_handle - .fork_choice_updated(state, None, EngineApiMessageVersion::V3) - .await; - } - Err(err) => { - warn!( - target: "consensus::flashblock-client", - %err, - "error while fetching flashblock completed sequence" - ); - break; - } - } + self.submit_forkchoice_update(block_hash, &sequence).await; } } } + +impl From<&FlashBlockCompleteSequence> for OpExecutionData { + fn from(sequence: &FlashBlockCompleteSequence) -> Self { + let mut data = Self::from_flashblocks_unchecked(sequence); + // Replace payload's state_root with the calculated one. For flashblocks, there was an + // option to disable state root calculation for blocks, and in that case, the payload's + // state_root will be zero, and we'll need to locally calculate state_root before + // proceeding to call engine_newPayload. + if let Some(execution_outcome) = sequence.execution_outcome() { + let payload = data.payload.as_v1_mut(); + payload.state_root = execution_outcome.state_root; + payload.block_hash = execution_outcome.block_hash; + } + data + } +} diff --git a/crates/optimism/flashblocks/src/lib.rs b/crates/optimism/flashblocks/src/lib.rs index 7220f443cc..74f202aed7 100644 --- a/crates/optimism/flashblocks/src/lib.rs +++ b/crates/optimism/flashblocks/src/lib.rs @@ -11,23 +11,25 @@ use reth_primitives_traits::NodePrimitives; use std::sync::Arc; -pub use payload::{ - ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashBlock, FlashBlockDecoder, - Metadata, -}; -pub use service::{FlashBlockBuildInfo, FlashBlockService}; -pub use ws::{WsConnect, WsFlashBlockStream}; +// Included to enable serde feature for OpReceipt type used transitively +use reth_optimism_primitives as _; mod consensus; pub use consensus::FlashBlockConsensusClient; + mod payload; -pub use payload::PendingFlashBlock; +pub use payload::{FlashBlock, PendingFlashBlock}; + mod sequence; pub use sequence::{FlashBlockCompleteSequence, FlashBlockPendingSequence}; mod service; +pub use service::{FlashBlockBuildInfo, FlashBlockService}; + mod worker; + mod ws; +pub use ws::{WsConnect, WsFlashBlockStream}; /// Receiver of the most recent [`PendingFlashBlock`] built out of [`FlashBlock`]s. /// diff --git a/crates/optimism/flashblocks/src/payload.rs b/crates/optimism/flashblocks/src/payload.rs index 7469538ee3..c7031c1856 100644 --- a/crates/optimism/flashblocks/src/payload.rs +++ b/crates/optimism/flashblocks/src/payload.rs @@ -1,154 +1,11 @@ use alloy_consensus::BlockHeader; -use alloy_eips::eip4895::Withdrawal; -use alloy_primitives::{bytes, Address, Bloom, Bytes, B256, U256}; -use alloy_rpc_types_engine::PayloadId; +use alloy_primitives::B256; use derive_more::Deref; -use reth_optimism_evm::OpNextBlockEnvAttributes; -use reth_optimism_primitives::OpReceipt; use reth_primitives_traits::NodePrimitives; use reth_rpc_eth_types::PendingBlock; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -/// Represents a Flashblock, a real-time block-like structure emitted by the Base L2 chain. -/// -/// A Flashblock provides a snapshot of a block’s effects before finalization, -/// allowing faster insight into state transitions, balance changes, and logs. -/// It includes a diff of the block’s execution and associated metadata. -/// -/// See: [Base Flashblocks Documentation](https://docs.base.org/chain/flashblocks) -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct FlashBlock { - /// The unique payload ID as assigned by the execution engine for this block. - pub payload_id: PayloadId, - /// A sequential index that identifies the order of this Flashblock. - pub index: u64, - /// A subset of block header fields. - pub base: Option, - /// The execution diff representing state transitions and transactions. - pub diff: ExecutionPayloadFlashblockDeltaV1, - /// Additional metadata about the block such as receipts and balances. - pub metadata: Metadata, -} - -impl FlashBlock { - /// Returns the block number of this flashblock. - pub const fn block_number(&self) -> u64 { - self.metadata.block_number - } - - /// Returns the first parent hash of this flashblock. - pub fn parent_hash(&self) -> Option { - Some(self.base.as_ref()?.parent_hash) - } - - /// Returns the receipt for the given transaction hash. - pub fn receipt_by_hash(&self, hash: &B256) -> Option<&OpReceipt> { - self.metadata.receipt_by_hash(hash) - } -} - -/// A trait for decoding flashblocks from bytes. -pub trait FlashBlockDecoder: Send + 'static { - /// Decodes `bytes` into a [`FlashBlock`]. - fn decode(&self, bytes: bytes::Bytes) -> eyre::Result; -} - -/// Default implementation of the decoder. -impl FlashBlockDecoder for () { - fn decode(&self, bytes: bytes::Bytes) -> eyre::Result { - FlashBlock::decode(bytes) - } -} - -/// Provides metadata about the block that may be useful for indexing or analysis. -// Note: this uses mixed camel, snake case: -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct Metadata { - /// The number of the block in the L2 chain. - pub block_number: u64, - /// A map of addresses to their updated balances after the block execution. - /// This represents balance changes due to transactions, rewards, or system transfers. - pub new_account_balances: BTreeMap, - /// Execution receipts for all transactions in the block. - /// Contains logs, gas usage, and other EVM-level metadata. - pub receipts: BTreeMap, -} - -impl Metadata { - /// Returns the receipt for the given transaction hash. - pub fn receipt_by_hash(&self, hash: &B256) -> Option<&OpReceipt> { - self.receipts.get(hash) - } -} - -/// Represents the base configuration of an execution payload that remains constant -/// throughout block construction. This includes fundamental block properties like -/// parent hash, block number, and other header fields that are determined at -/// block creation and cannot be modified. -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)] -pub struct ExecutionPayloadBaseV1 { - /// Ecotone parent beacon block root - pub parent_beacon_block_root: B256, - /// The parent hash of the block. - pub parent_hash: B256, - /// The fee recipient of the block. - pub fee_recipient: Address, - /// The previous randao of the block. - pub prev_randao: B256, - /// The block number. - #[serde(with = "alloy_serde::quantity")] - pub block_number: u64, - /// The gas limit of the block. - #[serde(with = "alloy_serde::quantity")] - pub gas_limit: u64, - /// The timestamp of the block. - #[serde(with = "alloy_serde::quantity")] - pub timestamp: u64, - /// The extra data of the block. - pub extra_data: Bytes, - /// The base fee per gas of the block. - pub base_fee_per_gas: U256, -} - -/// Represents the modified portions of an execution payload within a flashblock. -/// This structure contains only the fields that can be updated during block construction, -/// such as state root, receipts, logs, and new transactions. Other immutable block fields -/// like parent hash and block number are excluded since they remain constant throughout -/// the block's construction. -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)] -pub struct ExecutionPayloadFlashblockDeltaV1 { - /// The state root of the block. - pub state_root: B256, - /// The receipts root of the block. - pub receipts_root: B256, - /// The logs bloom of the block. - pub logs_bloom: Bloom, - /// The gas used of the block. - #[serde(with = "alloy_serde::quantity")] - pub gas_used: u64, - /// The block hash of the block. - pub block_hash: B256, - /// The transactions of the block. - pub transactions: Vec, - /// Array of [`Withdrawal`] enabled with V2 - pub withdrawals: Vec, - /// The withdrawals root of the block. - pub withdrawals_root: B256, -} - -impl From for OpNextBlockEnvAttributes { - fn from(value: ExecutionPayloadBaseV1) -> Self { - Self { - timestamp: value.timestamp, - suggested_fee_recipient: value.fee_recipient, - prev_randao: value.prev_randao, - gas_limit: value.gas_limit, - parent_beacon_block_root: Some(value.parent_beacon_block_root), - extra_data: value.extra_data, - } - } -} +/// Type alias for the Optimism flashblock payload. +pub type FlashBlock = op_alloy_rpc_types_engine::OpFlashblockPayload; /// The pending block built with all received Flashblocks alongside the metadata for the last added /// Flashblock. diff --git a/crates/optimism/flashblocks/src/sequence.rs b/crates/optimism/flashblocks/src/sequence.rs index f2363207e3..409a13f516 100644 --- a/crates/optimism/flashblocks/src/sequence.rs +++ b/crates/optimism/flashblocks/src/sequence.rs @@ -1,9 +1,10 @@ -use crate::{ExecutionPayloadBaseV1, FlashBlock, FlashBlockCompleteSequenceRx}; +use crate::{FlashBlock, FlashBlockCompleteSequenceRx}; use alloy_eips::eip2718::WithEncoded; -use alloy_primitives::B256; +use alloy_primitives::{Bytes, B256}; use alloy_rpc_types_engine::PayloadId; use core::mem; use eyre::{bail, OptionExt}; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use reth_primitives_traits::{Recovered, SignedTransaction}; use std::{collections::BTreeMap, ops::Deref}; use tokio::sync::broadcast; @@ -12,18 +13,24 @@ use tracing::{debug, trace, warn}; /// The size of the broadcast channel for completed flashblock sequences. const FLASHBLOCK_SEQUENCE_CHANNEL_SIZE: usize = 128; +/// Outcome from executing a flashblock sequence. +#[derive(Debug, Clone, Copy)] +pub struct SequenceExecutionOutcome { + /// The block hash of the executed pending block + pub block_hash: B256, + /// Properly computed state root + pub state_root: B256, +} + /// An ordered B-tree keeping the track of a sequence of [`FlashBlock`]s by their indices. #[derive(Debug)] pub struct FlashBlockPendingSequence { /// tracks the individual flashblocks in order - /// - /// With a blocktime of 2s and flashblock tick-rate of 200ms plus one extra flashblock per new - /// pending block, we expect 11 flashblocks per slot. inner: BTreeMap>, /// Broadcasts flashblocks to subscribers. block_broadcaster: broadcast::Sender, - /// Optional properly computed state root for the current sequence. - state_root: Option, + /// Optional execution outcome from building the current sequence. + execution_outcome: Option, } impl FlashBlockPendingSequence @@ -35,7 +42,7 @@ where // Note: if the channel is full, send will not block but rather overwrite the oldest // messages. Order is preserved. let (tx, _) = broadcast::channel(FLASHBLOCK_SEQUENCE_CHANNEL_SIZE); - Self { inner: BTreeMap::new(), block_broadcaster: tx, state_root: None } + Self { inner: BTreeMap::new(), block_broadcaster: tx, execution_outcome: None } } /// Returns the sender half of the [`FlashBlockCompleteSequence`] channel. @@ -52,13 +59,18 @@ where // Clears the state and broadcasts the blocks produced to subscribers. fn clear_and_broadcast_blocks(&mut self) { + if self.inner.is_empty() { + return; + } + let flashblocks = mem::take(&mut self.inner); + let execution_outcome = mem::take(&mut self.execution_outcome); // If there are any subscribers, send the flashblocks to them. if self.block_broadcaster.receiver_count() > 0 { let flashblocks = match FlashBlockCompleteSequence::new( flashblocks.into_iter().map(|block| block.1.into()).collect(), - self.state_root, + execution_outcome, ) { Ok(flashblocks) => flashblocks, Err(err) => { @@ -81,7 +93,7 @@ where /// A [`FlashBlock`] with index 0 resets the set. pub fn insert(&mut self, flashblock: FlashBlock) -> eyre::Result<()> { if flashblock.index == 0 { - trace!(number=%flashblock.block_number(), "Tracking new flashblock sequence"); + trace!(target: "flashblocks", number=%flashblock.block_number(), "Tracking new flashblock sequence"); // Flash block at index zero resets the whole state. self.clear_and_broadcast_blocks(); @@ -92,22 +104,25 @@ where // only insert if we previously received the same block and payload, assume we received // index 0 - let same_block = self.block_number() == Some(flashblock.metadata.block_number); + let same_block = self.block_number() == Some(flashblock.block_number()); let same_payload = self.payload_id() == Some(flashblock.payload_id); if same_block && same_payload { - trace!(number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock"); + trace!(target: "flashblocks", number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock"); self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?); } else { - trace!(number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following"); + trace!(target: "flashblocks", number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following"); } Ok(()) } - /// Set state root - pub const fn set_state_root(&mut self, state_root: Option) { - self.state_root = state_root; + /// Set execution outcome from building the flashblock sequence + pub const fn set_execution_outcome( + &mut self, + execution_outcome: Option, + ) { + self.execution_outcome = execution_outcome; } /// Iterator over sequence of executable transactions. @@ -129,11 +144,11 @@ where /// Returns the first block number pub fn block_number(&self) -> Option { - Some(self.inner.values().next()?.block().metadata.block_number) + Some(self.inner.values().next()?.block().block_number()) } /// Returns the payload base of the first tracked flashblock. - pub fn payload_base(&self) -> Option { + pub fn payload_base(&self) -> Option { self.inner.values().next()?.block().base.clone() } @@ -170,12 +185,12 @@ where /// /// Ensures invariants of a complete flashblocks sequence. /// If this entire sequence of flashblocks was executed on top of latest block, this also includes -/// the computed state root. +/// the execution outcome with block hash and state root. #[derive(Debug, Clone)] pub struct FlashBlockCompleteSequence { inner: Vec, - /// Optional state root for the current sequence - state_root: Option, + /// Optional execution outcome from building the flashblock sequence + execution_outcome: Option, } impl FlashBlockCompleteSequence { @@ -184,7 +199,10 @@ impl FlashBlockCompleteSequence { /// * vector is not empty /// * first flashblock have the base payload /// * sequence of flashblocks is sound (successive index from 0, same payload id, ...) - pub fn new(blocks: Vec, state_root: Option) -> eyre::Result { + pub fn new( + blocks: Vec, + execution_outcome: Option, + ) -> eyre::Result { let first_block = blocks.first().ok_or_eyre("No flashblocks in sequence")?; // Ensure that first flashblock have base @@ -194,21 +212,21 @@ impl FlashBlockCompleteSequence { if !blocks.iter().enumerate().all(|(idx, block)| { idx == block.index as usize && block.payload_id == first_block.payload_id && - block.metadata.block_number == first_block.metadata.block_number + block.block_number() == first_block.block_number() }) { bail!("Flashblock inconsistencies detected in sequence"); } - Ok(Self { inner: blocks, state_root }) + Ok(Self { inner: blocks, execution_outcome }) } /// Returns the block number pub fn block_number(&self) -> u64 { - self.inner.first().unwrap().metadata.block_number + self.inner.first().unwrap().block_number() } /// Returns the payload base of the first flashblock. - pub fn payload_base(&self) -> &ExecutionPayloadBaseV1 { + pub fn payload_base(&self) -> &OpFlashblockPayloadBase { self.inner.first().unwrap().base.as_ref().unwrap() } @@ -222,9 +240,14 @@ impl FlashBlockCompleteSequence { self.inner.last().unwrap() } - /// Returns the state root for the current sequence - pub const fn state_root(&self) -> Option { - self.state_root + /// Returns the execution outcome of the sequence. + pub const fn execution_outcome(&self) -> Option { + self.execution_outcome + } + + /// Returns all transactions from all flashblocks in the sequence + pub fn all_transactions(&self) -> Vec { + self.inner.iter().flat_map(|fb| fb.diff.transactions.iter().cloned()).collect() } } @@ -241,7 +264,7 @@ impl TryFrom> for FlashBlockCompleteSequence { fn try_from(sequence: FlashBlockPendingSequence) -> Result { Self::new( sequence.inner.into_values().map(|block| block.block().clone()).collect::>(), - sequence.state_root, + sequence.execution_outcome, ) } } @@ -297,12 +320,14 @@ impl Deref for PreparedFlashBlock { #[cfg(test)] mod tests { use super::*; - use crate::ExecutionPayloadFlashblockDeltaV1; use alloy_consensus::{ transaction::SignerRecoverable, EthereumTxEnvelope, EthereumTypedTransaction, TxEip1559, }; use alloy_eips::Encodable2718; use alloy_primitives::{hex, Signature, TxKind, U256}; + use op_alloy_rpc_types_engine::{ + OpFlashblockPayload, OpFlashblockPayloadBase, OpFlashblockPayloadDelta, + }; #[test] fn test_sequence_stops_before_gap() { @@ -332,11 +357,11 @@ mod tests { let tx = Recovered::new_unchecked(tx.clone(), tx.recover_signer_unchecked().unwrap()); sequence - .insert(FlashBlock { + .insert(OpFlashblockPayload { payload_id: Default::default(), index: 0, base: None, - diff: ExecutionPayloadFlashblockDeltaV1 { + diff: OpFlashblockPayloadDelta { transactions: vec![tx.encoded_2718().into()], ..Default::default() }, @@ -345,7 +370,7 @@ mod tests { .unwrap(); sequence - .insert(FlashBlock { + .insert(OpFlashblockPayload { payload_id: Default::default(), index: 2, base: None, @@ -367,10 +392,10 @@ mod tests { for idx in 0..10 { sequence - .insert(FlashBlock { + .insert(OpFlashblockPayload { payload_id: Default::default(), index: idx, - base: Some(ExecutionPayloadBaseV1::default()), + base: Some(OpFlashblockPayloadBase::default()), diff: Default::default(), metadata: Default::default(), }) @@ -385,10 +410,10 @@ mod tests { // Let's insert a new flashblock with index 0 sequence - .insert(FlashBlock { + .insert(OpFlashblockPayload { payload_id: Default::default(), index: 0, - base: Some(ExecutionPayloadBaseV1::default()), + base: Some(OpFlashblockPayloadBase::default()), diff: Default::default(), metadata: Default::default(), }) diff --git a/crates/optimism/flashblocks/src/service.rs b/crates/optimism/flashblocks/src/service.rs index f5d4a4a810..82a62c9e01 100644 --- a/crates/optimism/flashblocks/src/service.rs +++ b/crates/optimism/flashblocks/src/service.rs @@ -1,13 +1,14 @@ use crate::{ - sequence::FlashBlockPendingSequence, + sequence::{FlashBlockPendingSequence, SequenceExecutionOutcome}, worker::{BuildArgs, FlashBlockBuilder}, - ExecutionPayloadBaseV1, FlashBlock, FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx, - InProgressFlashBlockRx, PendingFlashBlock, + FlashBlock, FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx, InProgressFlashBlockRx, + PendingFlashBlock, }; use alloy_eips::eip2718::WithEncoded; use alloy_primitives::B256; use futures_util::{FutureExt, Stream, StreamExt}; -use metrics::Histogram; +use metrics::{Gauge, Histogram}; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use reth_chain_state::{CanonStateNotification, CanonStateNotifications, CanonStateSubscriptions}; use reth_evm::ConfigureEvm; use reth_metrics::Metrics; @@ -29,7 +30,8 @@ use tokio::{ }; use tracing::{debug, trace, warn}; -pub(crate) const FB_STATE_ROOT_FROM_INDEX: usize = 9; +/// 200 ms flashblock time. +pub(crate) const FLASHBLOCK_BLOCK_TIME: u64 = 200; /// The `FlashBlockService` maintains an in-memory [`PendingFlashBlock`] built out of a sequence of /// [`FlashBlock`]s. @@ -37,7 +39,7 @@ pub(crate) const FB_STATE_ROOT_FROM_INDEX: usize = 9; pub struct FlashBlockService< N: NodePrimitives, S, - EvmConfig: ConfigureEvm, + EvmConfig: ConfigureEvm + Unpin>, Provider, > { rx: S, @@ -59,7 +61,7 @@ pub struct FlashBlockService< in_progress_tx: watch::Sender>, /// `FlashBlock` service's metrics metrics: FlashBlockServiceMetrics, - /// Enable state root calculation from flashblock with index [`FB_STATE_ROOT_FROM_INDEX`] + /// Enable state root calculation compute_state_root: bool, } @@ -67,7 +69,7 @@ impl FlashBlockService where N: NodePrimitives, S: Stream> + Unpin + 'static, - EvmConfig: ConfigureEvm + Unpin> + EvmConfig: ConfigureEvm + Unpin> + Clone + 'static, Provider: StateProviderFactory @@ -137,12 +139,14 @@ where /// Note: this should be spawned pub async fn run(mut self, tx: tokio::sync::watch::Sender>>) { while let Some(block) = self.next().await { - if let Ok(block) = block.inspect_err(|e| tracing::error!("{e}")) { - let _ = tx.send(block).inspect_err(|e| tracing::error!("{e}")); + if let Ok(block) = block.inspect_err(|e| tracing::error!(target: "flashblocks", "{e}")) + { + let _ = + tx.send(block).inspect_err(|e| tracing::error!(target: "flashblocks", "{e}")); } } - warn!("Flashblock service has stopped"); + warn!(target: "flashblocks", "Flashblock service has stopped"); } /// Notifies all subscribers about the received flashblock @@ -165,6 +169,7 @@ where > { let Some(base) = self.blocks.payload_base() else { trace!( + target: "flashblocks", flashblock_number = ?self.blocks.block_number(), count = %self.blocks.count(), "Missing flashblock payload base" @@ -173,22 +178,55 @@ where return None }; + let Some(latest) = self.builder.provider().latest_header().ok().flatten() else { + trace!(target: "flashblocks", "No latest header found"); + return None + }; + // attempt an initial consecutive check - if let Some(latest) = self.builder.provider().latest_header().ok().flatten() && - latest.hash() != base.parent_hash - { - trace!(flashblock_parent=?base.parent_hash, flashblock_number=base.block_number, local_latest=?latest.num_hash(), "Skipping non consecutive build attempt"); + if latest.hash() != base.parent_hash { + trace!(target: "flashblocks", flashblock_parent=?base.parent_hash, flashblock_number=base.block_number, local_latest=?latest.num_hash(), "Skipping non consecutive build attempt"); return None } let Some(last_flashblock) = self.blocks.last_flashblock() else { - trace!(flashblock_number = ?self.blocks.block_number(), count = %self.blocks.count(), "Missing last flashblock"); + trace!(target: "flashblocks", flashblock_number = ?self.blocks.block_number(), count = %self.blocks.count(), "Missing last flashblock"); return None }; - // Check if state root must be computed - let compute_state_root = - self.compute_state_root && self.blocks.index() >= Some(FB_STATE_ROOT_FROM_INDEX as u64); + // Auto-detect when to compute state root: only if the builder didn't provide it (sent + // B256::ZERO) and we're near the expected final flashblock index. + // + // Background: Each block period receives multiple flashblocks at regular intervals. + // The sequencer sends an initial "base" flashblock at index 0 when a new block starts, + // then subsequent flashblocks are produced every FLASHBLOCK_BLOCK_TIME intervals (200ms). + // + // Examples with different block times: + // - Base (2s blocks): expect 2000ms / 200ms = 10 intervals → Flashblocks: index 0 (base) + // + indices 1-10 = potentially 11 total + // + // - Unichain (1s blocks): expect 1000ms / 200ms = 5 intervals → Flashblocks: index 0 (base) + // + indices 1-5 = potentially 6 total + // + // Why compute at N-1 instead of N: + // 1. Timing variance in flashblock producing time may mean only N flashblocks were produced + // instead of N+1 (missing the final one). Computing at N-1 ensures we get the state root + // for most common cases. + // + // 2. The +1 case (index 0 base + N intervals): If all N+1 flashblocks do arrive, we'll + // still calculate state root for flashblock N, which sacrifices a little performance but + // still ensures correctness for common cases. + // + // Note: Pathological cases may result in fewer flashblocks than expected (e.g., builder + // downtime, flashblock execution exceeding timing budget). When this occurs, we won't + // compute the state root, causing FlashblockConsensusClient to lack precomputed state for + // engine_newPayload. This is safe: we still have op-node as backstop to maintain + // chain progression. + let block_time_ms = (base.timestamp - latest.timestamp()) * 1000; + let expected_final_flashblock = block_time_ms / FLASHBLOCK_BLOCK_TIME; + let compute_state_root = self.compute_state_root && + last_flashblock.diff.state_root.is_zero() && + self.blocks.index() >= Some(expected_final_flashblock.saturating_sub(1)); Some(BuildArgs { base, @@ -228,7 +266,7 @@ impl Stream for FlashBlockService> + Unpin + 'static, - EvmConfig: ConfigureEvm + Unpin> + EvmConfig: ConfigureEvm + Unpin> + Clone + 'static, Provider: StateProviderFactory @@ -264,8 +302,15 @@ where if let Some((now, result)) = result { match result { Ok(Some((new_pending, cached_reads))) => { - // update state root of the current sequence - this.blocks.set_state_root(new_pending.computed_state_root()); + // update execution outcome of the current sequence + let execution_outcome = + new_pending.computed_state_root().map(|state_root| { + SequenceExecutionOutcome { + block_hash: new_pending.block().hash(), + state_root, + } + }); + this.blocks.set_execution_outcome(execution_outcome); // built a new pending block this.current = Some(new_pending.clone()); @@ -275,7 +320,9 @@ where let elapsed = now.elapsed(); this.metrics.execution_duration.record(elapsed.as_secs_f64()); + trace!( + target: "flashblocks", parent_hash = %new_pending.block().parent_hash(), block_number = new_pending.block().number(), flash_blocks = this.blocks.count(), @@ -290,7 +337,7 @@ where } Err(err) => { // we can ignore this error - debug!(%err, "failed to execute flashblock"); + debug!(target: "flashblocks", %err, "failed to execute flashblock"); } } } @@ -305,7 +352,9 @@ where } match this.blocks.insert(flashblock) { Ok(_) => this.rebuild = true, - Err(err) => debug!(%err, "Failed to prepare flashblock"), + Err(err) => { + debug!(target: "flashblocks", %err, "Failed to prepare flashblock") + } } } Err(err) => return Poll::Ready(Some(Err(err))), @@ -320,6 +369,7 @@ where } && let Some(current) = this.on_new_tip(state) { trace!( + target: "flashblocks", parent_hash = %current.block().parent_hash(), block_number = current.block().number(), "Clearing current flashblock on new canonical block" @@ -341,6 +391,9 @@ where index: args.last_flashblock_index, block_number: args.base.block_number, }; + // Record current block and index metrics + this.metrics.current_block_height.set(fb_info.block_number as f64); + this.metrics.current_index.set(fb_info.index as f64); // Signal that a flashblock build has started with build metadata let _ = this.in_progress_tx.send(Some(fb_info)); let (tx, rx) = oneshot::channel(); @@ -381,4 +434,8 @@ struct FlashBlockServiceMetrics { last_flashblock_length: Histogram, /// The duration applying flashblock state changes in seconds. execution_duration: Histogram, + /// Current block height. + current_block_height: Gauge, + /// Current flashblock index. + current_index: Gauge, } diff --git a/crates/optimism/flashblocks/src/worker.rs b/crates/optimism/flashblocks/src/worker.rs index 8cf7777f6a..f689c29dc0 100644 --- a/crates/optimism/flashblocks/src/worker.rs +++ b/crates/optimism/flashblocks/src/worker.rs @@ -1,6 +1,7 @@ -use crate::{ExecutionPayloadBaseV1, PendingFlashBlock}; +use crate::PendingFlashBlock; use alloy_eips::{eip2718::WithEncoded, BlockNumberOrTag}; use alloy_primitives::B256; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; use reth_chain_state::{CanonStateSubscriptions, ExecutedBlock}; use reth_errors::RethError; use reth_evm::{ @@ -38,7 +39,7 @@ impl FlashBlockBuilder { } pub(crate) struct BuildArgs { - pub(crate) base: ExecutionPayloadBaseV1, + pub(crate) base: OpFlashblockPayloadBase, pub(crate) transactions: I, pub(crate) cached_state: Option<(B256, CachedReads)>, pub(crate) last_flashblock_index: u64, @@ -49,7 +50,7 @@ pub(crate) struct BuildArgs { impl FlashBlockBuilder where N: NodePrimitives, - EvmConfig: ConfigureEvm + Unpin>, + EvmConfig: ConfigureEvm + Unpin>, Provider: StateProviderFactory + CanonStateSubscriptions + BlockReaderIdExt< @@ -60,14 +61,14 @@ where > + Unpin, { /// Returns the [`PendingFlashBlock`] made purely out of transactions and - /// [`ExecutionPayloadBaseV1`] in `args`. + /// [`OpFlashblockPayloadBase`] in `args`. /// /// Returns `None` if the flashblock doesn't attach to the latest header. pub(crate) fn execute>>>( &self, mut args: BuildArgs, ) -> eyre::Result, CachedReads)>> { - trace!("Attempting new pending block from flashblocks"); + trace!(target: "flashblocks", "Attempting new pending block from flashblocks"); let latest = self .provider @@ -76,7 +77,7 @@ where let latest_hash = latest.hash(); if args.base.parent_hash != latest_hash { - trace!(flashblock_parent = ?args.base.parent_hash, local_latest=?latest.num_hash(),"Skipping non consecutive flashblock"); + trace!(target: "flashblocks", flashblock_parent = ?args.base.parent_hash, local_latest=?latest.num_hash(),"Skipping non consecutive flashblock"); // doesn't attach to the latest block return Ok(None) } @@ -106,6 +107,7 @@ where // if the real state root should be computed let BlockBuilderOutcome { execution_result, block, hashed_state, .. } = if args.compute_state_root { + trace!(target: "flashblocks", "Computing block state root"); builder.finish(&state_provider)? } else { builder.finish(NoopProvider::default())? @@ -123,7 +125,7 @@ where ExecutedBlock { recovered_block: block.into(), execution_output: Arc::new(execution_outcome), - hashed_state: Arc::new(hashed_state), + hashed_state: Arc::new(hashed_state.into_sorted()), trie_updates: Arc::default(), }, ); diff --git a/crates/optimism/flashblocks/src/ws/decoding.rs b/crates/optimism/flashblocks/src/ws/decoding.rs index 267f79cf19..64d96dc5e3 100644 --- a/crates/optimism/flashblocks/src/ws/decoding.rs +++ b/crates/optimism/flashblocks/src/ws/decoding.rs @@ -1,52 +1,29 @@ -use crate::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashBlock, Metadata}; +use crate::FlashBlock; use alloy_primitives::bytes::Bytes; -use alloy_rpc_types_engine::PayloadId; -use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, io}; +use std::io; -/// Internal helper for decoding -#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize)] -struct FlashblocksPayloadV1 { - /// The payload id of the flashblock - pub payload_id: PayloadId, - /// The index of the flashblock in the block - pub index: u64, - /// The base execution payload configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub base: Option, - /// The delta/diff containing modified portions of the execution payload - pub diff: ExecutionPayloadFlashblockDeltaV1, - /// Additional metadata associated with the flashblock - pub metadata: serde_json::Value, +/// A trait for decoding flashblocks from bytes. +pub trait FlashBlockDecoder: Send + 'static { + /// Decodes `bytes` into a [`FlashBlock`]. + fn decode(&self, bytes: Bytes) -> eyre::Result; } -impl FlashBlock { - /// Decodes `bytes` into [`FlashBlock`]. - /// - /// This function is specific to the Base Optimism websocket encoding. - /// - /// It is assumed that the `bytes` are encoded in JSON and optionally compressed using brotli. - /// Whether the `bytes` is compressed or not is determined by looking at the first - /// non ascii-whitespace character. - pub(crate) fn decode(bytes: Bytes) -> eyre::Result { - let bytes = try_parse_message(bytes)?; - - let payload: FlashblocksPayloadV1 = serde_json::from_slice(&bytes) - .map_err(|e| eyre::eyre!("failed to parse message: {e}"))?; - - let metadata: Metadata = serde_json::from_value(payload.metadata) - .map_err(|e| eyre::eyre!("failed to parse message metadata: {e}"))?; - - Ok(Self { - payload_id: payload.payload_id, - index: payload.index, - base: payload.base, - diff: payload.diff, - metadata, - }) +/// Default implementation of the decoder. +impl FlashBlockDecoder for () { + fn decode(&self, bytes: Bytes) -> eyre::Result { + decode_flashblock(bytes) } } +pub(crate) fn decode_flashblock(bytes: Bytes) -> eyre::Result { + let bytes = crate::ws::decoding::try_parse_message(bytes)?; + + let payload: FlashBlock = + serde_json::from_slice(&bytes).map_err(|e| eyre::eyre!("failed to parse message: {e}"))?; + + Ok(payload) +} + /// Maps `bytes` into a potentially different [`Bytes`]. /// /// If the bytes start with a "{" character, prepended by any number of ASCII-whitespaces, diff --git a/crates/optimism/flashblocks/src/ws/mod.rs b/crates/optimism/flashblocks/src/ws/mod.rs index 2b82089931..8c8a591089 100644 --- a/crates/optimism/flashblocks/src/ws/mod.rs +++ b/crates/optimism/flashblocks/src/ws/mod.rs @@ -1,4 +1,6 @@ pub use stream::{WsConnect, WsFlashBlockStream}; mod decoding; +pub(crate) use decoding::FlashBlockDecoder; + mod stream; diff --git a/crates/optimism/flashblocks/src/ws/stream.rs b/crates/optimism/flashblocks/src/ws/stream.rs index 64cf6f718e..e46fd6d747 100644 --- a/crates/optimism/flashblocks/src/ws/stream.rs +++ b/crates/optimism/flashblocks/src/ws/stream.rs @@ -1,4 +1,4 @@ -use crate::{FlashBlock, FlashBlockDecoder}; +use crate::{ws::FlashBlockDecoder, FlashBlock}; use futures_util::{ stream::{SplitSink, SplitStream}, FutureExt, Sink, Stream, StreamExt, @@ -126,7 +126,9 @@ where } Ok(Message::Ping(bytes)) => this.ping(bytes), Ok(Message::Close(frame)) => this.close(frame), - Ok(msg) => debug!("Received unexpected message: {:?}", msg), + Ok(msg) => { + debug!(target: "flashblocks", "Received unexpected message: {:?}", msg) + } Err(err) => return Poll::Ready(Some(Err(err.into()))), } } @@ -238,7 +240,6 @@ impl WsConnect for WsConnector { #[cfg(test)] mod tests { use super::*; - use crate::ExecutionPayloadBaseV1; use alloy_primitives::bytes::Bytes; use brotli::enc::BrotliEncoderParams; use std::{future, iter}; @@ -449,23 +450,7 @@ mod tests { } fn flashblock() -> FlashBlock { - FlashBlock { - payload_id: Default::default(), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: Default::default(), - parent_hash: Default::default(), - fee_recipient: Default::default(), - prev_randao: Default::default(), - block_number: 0, - gas_limit: 0, - timestamp: 0, - extra_data: Default::default(), - base_fee_per_gas: Default::default(), - }), - diff: Default::default(), - metadata: Default::default(), - } + Default::default() } #[test_case::test_case(to_json_message(Message::Binary); "json binary")] diff --git a/crates/optimism/node/src/args.rs b/crates/optimism/node/src/args.rs index 4e9bb2ce7c..9d27c99320 100644 --- a/crates/optimism/node/src/args.rs +++ b/crates/optimism/node/src/args.rs @@ -74,6 +74,14 @@ pub struct RollupArgs { /// block tag will use the pending state based on flashblocks. #[arg(long)] pub flashblocks_url: Option, + + /// Enable flashblock consensus client to drive the chain forward + /// + /// When enabled, the flashblock consensus client will process flashblock sequences and submit + /// them to the engine API to advance the chain. + /// Requires `flashblocks_url` to be set. + #[arg(long, default_value_t = false, requires = "flashblocks_url")] + pub flashblock_consensus: bool, } impl Default for RollupArgs { @@ -90,6 +98,7 @@ impl Default for RollupArgs { historical_rpc: None, min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, } } } diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 65055eb671..c177051677 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -194,6 +194,7 @@ impl OpNode { .with_min_suggested_priority_fee(self.args.min_suggested_priority_fee) .with_historical_rpc(self.args.historical_rpc.clone()) .with_flashblocks(self.args.flashblocks_url.clone()) + .with_flashblock_consensus(self.args.flashblock_consensus) } /// Instantiates the [`ProviderFactoryBuilder`] for an opstack node. @@ -695,6 +696,8 @@ pub struct OpAddOnsBuilder { tokio_runtime: Option, /// A URL pointing to a secure websocket service that streams out flashblocks. flashblocks_url: Option, + /// Enable flashblock consensus client to drive chain forward. + flashblock_consensus: bool, } impl Default for OpAddOnsBuilder { @@ -711,6 +714,7 @@ impl Default for OpAddOnsBuilder { rpc_middleware: Identity::new(), tokio_runtime: None, flashblocks_url: None, + flashblock_consensus: false, } } } @@ -779,6 +783,7 @@ impl OpAddOnsBuilder { tokio_runtime, _nt, flashblocks_url, + flashblock_consensus, .. } = self; OpAddOnsBuilder { @@ -793,6 +798,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + flashblock_consensus, } } @@ -801,6 +807,12 @@ impl OpAddOnsBuilder { self.flashblocks_url = flashblocks_url; self } + + /// With a flashblock consensus client to drive chain forward. + pub const fn with_flashblock_consensus(mut self, flashblock_consensus: bool) -> Self { + self.flashblock_consensus = flashblock_consensus; + self + } } impl OpAddOnsBuilder { @@ -826,6 +838,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + flashblock_consensus, .. } = self; @@ -835,7 +848,8 @@ impl OpAddOnsBuilder { .with_sequencer(sequencer_url.clone()) .with_sequencer_headers(sequencer_headers.clone()) .with_min_suggested_priority_fee(min_suggested_priority_fee) - .with_flashblocks(flashblocks_url), + .with_flashblocks(flashblocks_url) + .with_flashblock_consensus(flashblock_consensus), PVB::default(), EB::default(), EVB::default(), diff --git a/crates/optimism/node/src/rpc.rs b/crates/optimism/node/src/rpc.rs index db811a7f92..b87800a54e 100644 --- a/crates/optimism/node/src/rpc.rs +++ b/crates/optimism/node/src/rpc.rs @@ -13,7 +13,7 @@ //! components::ComponentsBuilder, //! hooks::OnComponentInitializedHook, //! rpc::{EthApiBuilder, EthApiCtx}, -//! LaunchContext, NodeConfig, RethFullAdapter, +//! ConsensusEngineHandle, LaunchContext, NodeConfig, RethFullAdapter, //! }; //! use reth_optimism_chainspec::OP_SEPOLIA; //! use reth_optimism_evm::OpEvmConfig; @@ -67,7 +67,14 @@ //! config.cache, //! node.task_executor().clone(), //! ); -//! let ctx = EthApiCtx { components: node.node_adapter(), config, cache }; +//! // Create a dummy beacon engine handle for offline mode +//! let (tx, _) = tokio::sync::mpsc::unbounded_channel(); +//! let ctx = EthApiCtx { +//! components: node.node_adapter(), +//! config, +//! cache, +//! engine_handle: ConsensusEngineHandle::new(tx), +//! }; //! let eth_api = OpEthApiBuilder::::default().build_eth_api(ctx).await.unwrap(); //! //! // build `trace` namespace API diff --git a/crates/optimism/node/tests/it/main.rs b/crates/optimism/node/tests/it/main.rs index fbd49d4c1c..fa173e499f 100644 --- a/crates/optimism/node/tests/it/main.rs +++ b/crates/optimism/node/tests/it/main.rs @@ -4,4 +4,6 @@ mod builder; mod priority; +mod rpc; + const fn main() {} diff --git a/crates/optimism/node/tests/it/rpc.rs b/crates/optimism/node/tests/it/rpc.rs new file mode 100644 index 0000000000..8869975ea9 --- /dev/null +++ b/crates/optimism/node/tests/it/rpc.rs @@ -0,0 +1,41 @@ +//! RPC integration tests. + +use reth_network::types::NatResolver; +use reth_node_builder::{NodeBuilder, NodeHandle}; +use reth_node_core::{ + args::{NetworkArgs, RpcServerArgs}, + node_config::NodeConfig, +}; +use reth_optimism_chainspec::BASE_MAINNET; +use reth_optimism_node::OpNode; +use reth_rpc_api::servers::AdminApiServer; +use reth_tasks::TaskManager; + +// +#[tokio::test] +async fn test_admin_external_ip() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let exec = TaskManager::current(); + let exec = exec.executor(); + + let external_ip = "10.64.128.71".parse().unwrap(); + // Node setup + let node_config = NodeConfig::test() + .map_chain(BASE_MAINNET.clone()) + .with_network( + NetworkArgs::default().with_nat_resolver(NatResolver::ExternalIp(external_ip)), + ) + .with_rpc(RpcServerArgs::default().with_unused_ports().with_http()); + + let NodeHandle { node, node_exit_future: _ } = + NodeBuilder::new(node_config).testing_node(exec).node(OpNode::default()).launch().await?; + + let api = node.add_ons_handle.admin_api(); + + let info = api.node_info().await.unwrap(); + + assert_eq!(info.ip, external_ip); + + Ok(()) +} diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index 1f7c0c00f9..e44e28fc8f 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -386,6 +386,7 @@ impl OpBuilder<'_, Txs> { let executed: BuiltPayloadExecutedBlock = BuiltPayloadExecutedBlock { recovered_block: Arc::new(block), execution_output: Arc::new(execution_outcome), + // Keep unsorted; conversion to sorted happens when needed downstream hashed_state: either::Either::Left(Arc::new(hashed_state)), trie_updates: either::Either::Left(Arc::new(trie_updates)), }; diff --git a/crates/optimism/payload/src/payload.rs b/crates/optimism/payload/src/payload.rs index 7e3a365327..3f7b3d401e 100644 --- a/crates/optimism/payload/src/payload.rs +++ b/crates/optimism/payload/src/payload.rs @@ -343,6 +343,9 @@ where /// Generates the payload id for the configured payload from the [`OpPayloadAttributes`]. /// /// Returns an 8-byte identifier by hashing the payload components with sha256 hash. +/// +/// Note: This must be updated whenever the [`OpPayloadAttributes`] changes for a hardfork. +/// See also pub fn payload_id_optimism( parent: &B256, attributes: &OpPayloadAttributes, @@ -388,6 +391,10 @@ pub fn payload_id_optimism( hasher.update(eip_1559_params.as_slice()); } + if let Some(min_base_fee) = attributes.min_base_fee { + hasher.update(min_base_fee.to_be_bytes()); + } + let mut out = hasher.finalize(); out[0] = payload_version; @@ -476,6 +483,37 @@ mod tests { ); } + #[test] + fn test_payload_id_parity_op_geth_jovian() { + // + let expected = + PayloadId::new(FixedBytes::<8>::from_str("0x046c65ffc4d659ec").unwrap().into()); + let attrs = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 1728933301, + prev_randao: b256!("0x9158595abbdab2c90635087619aa7042bbebe47642dfab3c9bfb934f6b082765"), + suggested_fee_recipient: address!("0x4200000000000000000000000000000000000011"), + withdrawals: Some([].into()), + parent_beacon_block_root: b256!("0x8fe0193b9bf83cb7e5a08538e494fecc23046aab9a497af3704f4afdae3250ff").into(), + }, + transactions: Some([bytes!("7ef8f8a0dc19cfa777d90980e4875d0a548a881baaa3f83f14d1bc0d3038bc329350e54194deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000f424000000000000000000000000300000000670d6d890000000000000125000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000014bf9181db6e381d4384bbf69c48b0ee0eed23c6ca26143c6d2544f9d39997a590000000000000000000000007f83d659683caf2767fd3c720981d51f5bc365bc")].into()), + no_tx_pool: None, + gas_limit: Some(30000000), + eip_1559_params: None, + min_base_fee: Some(100), + }; + + // Reth's `PayloadId` should match op-geth's `PayloadId`. This fails + assert_eq!( + expected, + payload_id_optimism( + &b256!("0x3533bf30edaf9505d0810bf475cbe4e5f4b9889904b9845e83efdeab4e92eb1e"), + &attrs, + EngineApiMessageVersion::V4 as u8 + ) + ); + } + #[test] fn test_get_extra_data_post_holocene() { let attributes: OpPayloadBuilderAttributes = diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index 30257049c2..ef83fe3ddb 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -14,8 +14,6 @@ workspace = true [dependencies] # reth reth-primitives-traits = { workspace = true, features = ["op"] } -reth-codecs = { workspace = true, optional = true, features = ["op"] } -reth-zstd-compressors = { workspace = true, optional = true } # ethereum alloy-primitives.workspace = true @@ -27,17 +25,15 @@ alloy-rlp.workspace = true op-alloy-consensus.workspace = true # codec -bytes = { workspace = true, optional = true } -modular-bitfield = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_with = { workspace = true, optional = true } -# test -arbitrary = { workspace = true, features = ["derive"], optional = true } - [dev-dependencies] reth-codecs = { workspace = true, features = ["test-utils", "op"] } +bytes.workspace = true +modular-bitfield.workspace = true +reth-zstd-compressors.workspace = true rand.workspace = true arbitrary.workspace = true rstest.workspace = true @@ -53,41 +49,35 @@ secp256k1 = { workspace = true, features = ["rand"] } default = ["std"] std = [ "reth-primitives-traits/std", - "reth-codecs?/std", "alloy-consensus/std", "alloy-primitives/std", "serde?/std", - "bytes?/std", "alloy-rlp/std", - "reth-zstd-compressors?/std", "op-alloy-consensus/std", "serde_json/std", "serde_with?/std", "alloy-eips/std", "secp256k1/std", + "bytes/std", + "reth-zstd-compressors/std", ] alloy-compat = ["op-alloy-consensus/alloy-compat"] reth-codec = [ - "dep:reth-codecs", "std", "reth-primitives-traits/reth-codec", - "reth-codecs?/op", - "dep:bytes", - "dep:modular-bitfield", - "dep:reth-zstd-compressors", ] serde = [ "dep:serde", "reth-primitives-traits/serde", "alloy-primitives/serde", "alloy-consensus/serde", - "bytes?/serde", - "reth-codecs?/serde", "op-alloy-consensus/serde", "alloy-eips/serde", "rand/serde", "rand_08/serde", "secp256k1/serde", + "bytes/serde", + "reth-codecs/serde", ] serde-bincode-compat = [ "serde", @@ -99,11 +89,10 @@ serde-bincode-compat = [ ] arbitrary = [ "std", - "dep:arbitrary", "reth-primitives-traits/arbitrary", - "reth-codecs?/arbitrary", "op-alloy-consensus/arbitrary", "alloy-consensus/arbitrary", "alloy-primitives/arbitrary", "alloy-eips/arbitrary", + "reth-codecs/arbitrary", ] diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index 8100d70c91..f58f73a6e8 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -20,7 +20,8 @@ pub mod transaction; pub use transaction::*; mod receipt; -pub use receipt::{DepositReceipt, OpReceipt}; +pub use op_alloy_consensus::OpReceipt; +pub use receipt::DepositReceipt; /// Optimism-specific block type. pub type OpBlock = alloy_consensus::Block; @@ -44,6 +45,6 @@ impl reth_primitives_traits::NodePrimitives for OpPrimitives { /// Bincode-compatible serde implementations. #[cfg(feature = "serde-bincode-compat")] pub mod serde_bincode_compat { - pub use super::receipt::serde_bincode_compat::*; - pub use op_alloy_consensus::serde_bincode_compat::*; + pub use super::receipt::serde_bincode_compat::OpReceipt as LocalOpReceipt; + pub use op_alloy_consensus::serde_bincode_compat::OpReceipt; } diff --git a/crates/optimism/primitives/src/receipt.rs b/crates/optimism/primitives/src/receipt.rs index 74f21eab11..5880b37b24 100644 --- a/crates/optimism/primitives/src/receipt.rs +++ b/crates/optimism/primitives/src/receipt.rs @@ -9,409 +9,9 @@ use alloy_eips::{ }; use alloy_primitives::{Bloom, Log}; use alloy_rlp::{BufMut, Decodable, Encodable, Header}; -use op_alloy_consensus::{OpDepositReceipt, OpTxType}; +use op_alloy_consensus::{OpDepositReceipt, OpReceipt, OpTxType}; use reth_primitives_traits::InMemorySize; -/// Typed ethereum transaction receipt. -/// Receipt containing result of transaction execution. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[cfg_attr(feature = "reth-codec", reth_codecs::add_arbitrary_tests(rlp))] -pub enum OpReceipt { - /// Legacy receipt - Legacy(Receipt), - /// EIP-2930 receipt - Eip2930(Receipt), - /// EIP-1559 receipt - Eip1559(Receipt), - /// EIP-7702 receipt - Eip7702(Receipt), - /// Deposit receipt - Deposit(OpDepositReceipt), -} - -impl OpReceipt { - /// Returns [`OpTxType`] of the receipt. - pub const fn tx_type(&self) -> OpTxType { - match self { - Self::Legacy(_) => OpTxType::Legacy, - Self::Eip2930(_) => OpTxType::Eip2930, - Self::Eip1559(_) => OpTxType::Eip1559, - Self::Eip7702(_) => OpTxType::Eip7702, - Self::Deposit(_) => OpTxType::Deposit, - } - } - - /// Returns inner [`Receipt`], - pub const fn as_receipt(&self) -> &Receipt { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt, - Self::Deposit(receipt) => &receipt.inner, - } - } - - /// Returns a mutable reference to the inner [`Receipt`], - pub const fn as_receipt_mut(&mut self) -> &mut Receipt { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt, - Self::Deposit(receipt) => &mut receipt.inner, - } - } - - /// Consumes this and returns the inner [`Receipt`]. - pub fn into_receipt(self) -> Receipt { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt, - Self::Deposit(receipt) => receipt.inner, - } - } - - /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header. - pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt.rlp_encoded_fields_length_with_bloom(bloom), - Self::Deposit(receipt) => receipt.rlp_encoded_fields_length_with_bloom(bloom), - } - } - - /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header. - pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt.rlp_encode_fields_with_bloom(bloom, out), - Self::Deposit(receipt) => receipt.rlp_encode_fields_with_bloom(bloom, out), - } - } - - /// Returns RLP header for inner encoding. - pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header { - Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) } - } - - /// Returns RLP header for inner encoding without bloom. - pub fn rlp_header_inner_without_bloom(&self) -> Header { - Header { list: true, payload_length: self.rlp_encoded_fields_length_without_bloom() } - } - - /// RLP-decodes the receipt from the provided buffer. This does not expect a type byte or - /// network header. - pub fn rlp_decode_inner( - buf: &mut &[u8], - tx_type: OpTxType, - ) -> alloy_rlp::Result> { - match tx_type { - OpTxType::Legacy => { - let ReceiptWithBloom { receipt, logs_bloom } = - RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; - Ok(ReceiptWithBloom { receipt: Self::Legacy(receipt), logs_bloom }) - } - OpTxType::Eip2930 => { - let ReceiptWithBloom { receipt, logs_bloom } = - RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; - Ok(ReceiptWithBloom { receipt: Self::Eip2930(receipt), logs_bloom }) - } - OpTxType::Eip1559 => { - let ReceiptWithBloom { receipt, logs_bloom } = - RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; - Ok(ReceiptWithBloom { receipt: Self::Eip1559(receipt), logs_bloom }) - } - OpTxType::Eip7702 => { - let ReceiptWithBloom { receipt, logs_bloom } = - RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; - Ok(ReceiptWithBloom { receipt: Self::Eip7702(receipt), logs_bloom }) - } - OpTxType::Deposit => { - let ReceiptWithBloom { receipt, logs_bloom } = - RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; - Ok(ReceiptWithBloom { receipt: Self::Deposit(receipt), logs_bloom }) - } - } - } - - /// RLP-encodes receipt fields without an RLP header. - pub fn rlp_encode_fields_without_bloom(&self, out: &mut dyn BufMut) { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => { - receipt.status.encode(out); - receipt.cumulative_gas_used.encode(out); - receipt.logs.encode(out); - } - Self::Deposit(receipt) => { - receipt.inner.status.encode(out); - receipt.inner.cumulative_gas_used.encode(out); - receipt.inner.logs.encode(out); - if let Some(nonce) = receipt.deposit_nonce { - nonce.encode(out); - } - if let Some(version) = receipt.deposit_receipt_version { - version.encode(out); - } - } - } - } - - /// Returns length of RLP-encoded receipt fields without an RLP header. - pub fn rlp_encoded_fields_length_without_bloom(&self) -> usize { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => { - receipt.status.length() + - receipt.cumulative_gas_used.length() + - receipt.logs.length() - } - Self::Deposit(receipt) => { - receipt.inner.status.length() + - receipt.inner.cumulative_gas_used.length() + - receipt.inner.logs.length() + - receipt.deposit_nonce.map_or(0, |nonce| nonce.length()) + - receipt.deposit_receipt_version.map_or(0, |version| version.length()) - } - } - } - - /// RLP-decodes the receipt from the provided buffer without bloom. - pub fn rlp_decode_inner_without_bloom( - buf: &mut &[u8], - tx_type: OpTxType, - ) -> alloy_rlp::Result { - let header = Header::decode(buf)?; - if !header.list { - return Err(alloy_rlp::Error::UnexpectedString); - } - - let remaining = buf.len(); - let status = Decodable::decode(buf)?; - let cumulative_gas_used = Decodable::decode(buf)?; - let logs = Decodable::decode(buf)?; - - let mut deposit_nonce = None; - let mut deposit_receipt_version = None; - - // For deposit receipts, try to decode nonce and version if they exist - if tx_type == OpTxType::Deposit && buf.len() + header.payload_length > remaining { - deposit_nonce = Some(Decodable::decode(buf)?); - if buf.len() + header.payload_length > remaining { - deposit_receipt_version = Some(Decodable::decode(buf)?); - } - } - - if buf.len() + header.payload_length != remaining { - return Err(alloy_rlp::Error::UnexpectedLength); - } - - match tx_type { - OpTxType::Legacy => Ok(Self::Legacy(Receipt { status, cumulative_gas_used, logs })), - OpTxType::Eip2930 => Ok(Self::Eip2930(Receipt { status, cumulative_gas_used, logs })), - OpTxType::Eip1559 => Ok(Self::Eip1559(Receipt { status, cumulative_gas_used, logs })), - OpTxType::Eip7702 => Ok(Self::Eip7702(Receipt { status, cumulative_gas_used, logs })), - OpTxType::Deposit => Ok(Self::Deposit(OpDepositReceipt { - inner: Receipt { status, cumulative_gas_used, logs }, - deposit_nonce, - deposit_receipt_version, - })), - } - } -} - -impl Eip2718EncodableReceipt for OpReceipt { - fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize { - !self.tx_type().is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload() - } - - fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) { - if !self.tx_type().is_legacy() { - out.put_u8(self.tx_type() as u8); - } - self.rlp_header_inner(bloom).encode(out); - self.rlp_encode_fields(bloom, out); - } -} - -impl RlpEncodableReceipt for OpReceipt { - fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize { - let mut len = self.eip2718_encoded_length_with_bloom(bloom); - if !self.tx_type().is_legacy() { - len += Header { - list: false, - payload_length: self.eip2718_encoded_length_with_bloom(bloom), - } - .length(); - } - - len - } - - fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) { - if !self.tx_type().is_legacy() { - Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) } - .encode(out); - } - self.eip2718_encode_with_bloom(bloom, out); - } -} - -impl RlpDecodableReceipt for OpReceipt { - fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result> { - let header_buf = &mut &**buf; - let header = Header::decode(header_buf)?; - - // Legacy receipt, reuse initial buffer without advancing - if header.list { - return Self::rlp_decode_inner(buf, OpTxType::Legacy) - } - - // Otherwise, advance the buffer and try decoding type flag followed by receipt - *buf = *header_buf; - - let remaining = buf.len(); - let tx_type = OpTxType::decode(buf)?; - let this = Self::rlp_decode_inner(buf, tx_type)?; - - if buf.len() + header.payload_length != remaining { - return Err(alloy_rlp::Error::UnexpectedLength); - } - - Ok(this) - } -} - -impl Encodable2718 for OpReceipt { - fn encode_2718_len(&self) -> usize { - !self.tx_type().is_legacy() as usize + - self.rlp_header_inner_without_bloom().length_with_payload() - } - - fn encode_2718(&self, out: &mut dyn BufMut) { - if !self.tx_type().is_legacy() { - out.put_u8(self.tx_type() as u8); - } - self.rlp_header_inner_without_bloom().encode(out); - self.rlp_encode_fields_without_bloom(out); - } -} - -impl Decodable2718 for OpReceipt { - fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { - Ok(Self::rlp_decode_inner_without_bloom(buf, OpTxType::try_from(ty)?)?) - } - - fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { - Ok(Self::rlp_decode_inner_without_bloom(buf, OpTxType::Legacy)?) - } -} - -impl Encodable for OpReceipt { - fn encode(&self, out: &mut dyn BufMut) { - self.network_encode(out); - } - - fn length(&self) -> usize { - self.network_len() - } -} - -impl Decodable for OpReceipt { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - Ok(Self::network_decode(buf)?) - } -} - -impl TxReceipt for OpReceipt { - type Log = Log; - - fn status_or_post_state(&self) -> Eip658Value { - self.as_receipt().status_or_post_state() - } - - fn status(&self) -> bool { - self.as_receipt().status() - } - - fn bloom(&self) -> Bloom { - self.as_receipt().bloom() - } - - fn cumulative_gas_used(&self) -> u64 { - self.as_receipt().cumulative_gas_used() - } - - fn logs(&self) -> &[Log] { - self.as_receipt().logs() - } - - fn into_logs(self) -> Vec { - match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip7702(receipt) => receipt.logs, - Self::Deposit(receipt) => receipt.inner.logs, - } - } -} - -impl Typed2718 for OpReceipt { - fn ty(&self) -> u8 { - self.tx_type().into() - } -} - -impl IsTyped2718 for OpReceipt { - fn is_type(type_id: u8) -> bool { - ::is_type(type_id) - } -} - -impl InMemorySize for OpReceipt { - fn size(&self) -> usize { - self.as_receipt().size() - } -} - -impl From for OpReceipt { - fn from(envelope: op_alloy_consensus::OpReceiptEnvelope) -> Self { - match envelope { - op_alloy_consensus::OpReceiptEnvelope::Legacy(receipt) => Self::Legacy(receipt.receipt), - op_alloy_consensus::OpReceiptEnvelope::Eip2930(receipt) => { - Self::Eip2930(receipt.receipt) - } - op_alloy_consensus::OpReceiptEnvelope::Eip1559(receipt) => { - Self::Eip1559(receipt.receipt) - } - op_alloy_consensus::OpReceiptEnvelope::Eip7702(receipt) => { - Self::Eip7702(receipt.receipt) - } - op_alloy_consensus::OpReceiptEnvelope::Deposit(receipt) => { - Self::Deposit(OpDepositReceipt { - deposit_nonce: receipt.receipt.deposit_nonce, - deposit_receipt_version: receipt.receipt.deposit_receipt_version, - inner: receipt.receipt.inner, - }) - } - } - } -} - /// Trait for deposit receipt. pub trait DepositReceipt: reth_primitives_traits::Receipt { /// Converts a `Receipt` into a mutable Optimism deposit receipt. @@ -437,100 +37,6 @@ impl DepositReceipt for OpReceipt { } } -#[cfg(feature = "reth-codec")] -mod compact { - use super::*; - use alloc::borrow::Cow; - use reth_codecs::Compact; - - #[derive(reth_codecs::CompactZstd)] - #[reth_zstd( - compressor = reth_zstd_compressors::RECEIPT_COMPRESSOR, - decompressor = reth_zstd_compressors::RECEIPT_DECOMPRESSOR - )] - struct CompactOpReceipt<'a> { - tx_type: OpTxType, - success: bool, - cumulative_gas_used: u64, - #[expect(clippy::owned_cow)] - logs: Cow<'a, Vec>, - deposit_nonce: Option, - deposit_receipt_version: Option, - } - - impl<'a> From<&'a OpReceipt> for CompactOpReceipt<'a> { - fn from(receipt: &'a OpReceipt) -> Self { - Self { - tx_type: receipt.tx_type(), - success: receipt.status(), - cumulative_gas_used: receipt.cumulative_gas_used(), - logs: Cow::Borrowed(&receipt.as_receipt().logs), - deposit_nonce: if let OpReceipt::Deposit(receipt) = receipt { - receipt.deposit_nonce - } else { - None - }, - deposit_receipt_version: if let OpReceipt::Deposit(receipt) = receipt { - receipt.deposit_receipt_version - } else { - None - }, - } - } - } - - impl From> for OpReceipt { - fn from(receipt: CompactOpReceipt<'_>) -> Self { - let CompactOpReceipt { - tx_type, - success, - cumulative_gas_used, - logs, - deposit_nonce, - deposit_receipt_version, - } = receipt; - - let inner = - Receipt { status: success.into(), cumulative_gas_used, logs: logs.into_owned() }; - - match tx_type { - OpTxType::Legacy => Self::Legacy(inner), - OpTxType::Eip2930 => Self::Eip2930(inner), - OpTxType::Eip1559 => Self::Eip1559(inner), - OpTxType::Eip7702 => Self::Eip7702(inner), - OpTxType::Deposit => Self::Deposit(OpDepositReceipt { - inner, - deposit_nonce, - deposit_receipt_version, - }), - } - } - } - - impl Compact for OpReceipt { - fn to_compact(&self, buf: &mut B) -> usize - where - B: bytes::BufMut + AsMut<[u8]>, - { - CompactOpReceipt::from(self).to_compact(buf) - } - - fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { - let (receipt, buf) = CompactOpReceipt::from_compact(buf, len); - (receipt.into(), buf) - } - } - - #[cfg(test)] - #[test] - fn test_ensure_backwards_compatibility() { - use reth_codecs::{test_utils::UnusedBits, validate_bitflag_backwards_compat}; - - assert_eq!(CompactOpReceipt::bitflag_encoded_bytes(), 2); - validate_bitflag_backwards_compat!(CompactOpReceipt<'_>, UnusedBits::NotZero); - } -} - #[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] pub(super) mod serde_bincode_compat { use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -540,17 +46,21 @@ pub(super) mod serde_bincode_compat { /// /// Intended to use with the [`serde_with::serde_as`] macro in the following way: /// ```rust - /// use reth_optimism_primitives::{serde_bincode_compat, OpReceipt}; + /// use reth_optimism_primitives::OpReceipt; + /// use reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat; /// use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// use serde_with::serde_as; /// /// #[serde_as] /// #[derive(Serialize, Deserialize)] /// struct Data { - /// #[serde_as(as = "serde_bincode_compat::OpReceipt<'_>")] + /// #[serde_as( + /// as = "reth_primitives_traits::serde_bincode_compat::BincodeReprFor<'_, OpReceipt>" + /// )] /// receipt: OpReceipt, /// } /// ``` + #[allow(rustdoc::private_doc_tests)] #[derive(Debug, Serialize, Deserialize)] pub enum OpReceipt<'a> { /// Legacy receipt @@ -609,18 +119,6 @@ pub(super) mod serde_bincode_compat { } } - impl reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat for super::OpReceipt { - type BincodeRepr<'a> = OpReceipt<'a>; - - fn as_repr(&self) -> Self::BincodeRepr<'_> { - self.into() - } - - fn from_repr(repr: Self::BincodeRepr<'_>) -> Self { - repr.into() - } - } - #[cfg(test)] mod tests { use crate::{receipt::serde_bincode_compat, OpReceipt}; diff --git a/crates/optimism/primitives/src/transaction/signed.rs b/crates/optimism/primitives/src/transaction/signed.rs index 820cc11271..fc2f63abd8 100644 --- a/crates/optimism/primitives/src/transaction/signed.rs +++ b/crates/optimism/primitives/src/transaction/signed.rs @@ -9,7 +9,7 @@ use alloy_consensus::{ Typed2718, }; use alloy_eips::{ - eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, + eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718}, eip2930::AccessList, eip7702::SignedAuthorization, }; @@ -148,6 +148,12 @@ impl TxHashRef for OpTransactionSigned { } } +impl IsTyped2718 for OpTransactionSigned { + fn is_type(type_id: u8) -> bool { + ::is_type(type_id) + } +} + impl SignedTransaction for OpTransactionSigned { fn recalculate_hash(&self) -> B256 { keccak256(self.encoded_2718()) diff --git a/crates/optimism/rpc/src/error.rs b/crates/optimism/rpc/src/error.rs index 2b5962460d..b457ce9d9c 100644 --- a/crates/optimism/rpc/src/error.rs +++ b/crates/optimism/rpc/src/error.rs @@ -1,6 +1,7 @@ //! RPC errors specific to OP. use alloy_json_rpc::ErrorPayload; +use alloy_primitives::Bytes; use alloy_rpc_types_eth::{error::EthRpcErrorCode, BlockError}; use alloy_transport::{RpcError, TransportErrorKind}; use jsonrpsee_types::error::{INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE}; @@ -8,7 +9,10 @@ use op_revm::{OpHaltReason, OpTransactionError}; use reth_evm::execute::ProviderError; use reth_optimism_evm::OpBlockExecutionError; use reth_rpc_eth_api::{AsEthApiError, EthTxEnvError, TransactionConversionError}; -use reth_rpc_eth_types::{error::api::FromEvmHalt, EthApiError}; +use reth_rpc_eth_types::{ + error::api::{FromEvmHalt, FromRevert}, + EthApiError, +}; use reth_rpc_server_types::result::{internal_rpc_err, rpc_err}; use revm::context_interface::result::{EVMError, InvalidTransaction}; use std::{convert::Infallible, fmt::Display}; @@ -194,6 +198,12 @@ impl FromEvmHalt for OpEthApiError { } } +impl FromRevert for OpEthApiError { + fn from_revert(output: Bytes) -> Self { + Self::Eth(EthApiError::from_revert(output)) + } +} + impl From for OpEthApiError { fn from(value: TransactionConversionError) -> Self { Self::Eth(EthApiError::from(value)) diff --git a/crates/optimism/rpc/src/eth/mod.rs b/crates/optimism/rpc/src/eth/mod.rs index 8adbee93ad..f9b6195133 100644 --- a/crates/optimism/rpc/src/eth/mod.rs +++ b/crates/optimism/rpc/src/eth/mod.rs @@ -16,6 +16,7 @@ use alloy_consensus::BlockHeader; use alloy_primitives::{B256, U256}; use eyre::WrapErr; use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpFlashblockPayloadBase; pub use receipt::{OpReceiptBuilder, OpReceiptFieldsBuilder}; use reqwest::Url; use reth_chainspec::{EthereumHardforks, Hardforks}; @@ -23,8 +24,9 @@ use reth_evm::ConfigureEvm; use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, NodeTypes}; use reth_node_builder::rpc::{EthApiBuilder, EthApiCtx}; use reth_optimism_flashblocks::{ - ExecutionPayloadBaseV1, FlashBlockBuildInfo, FlashBlockCompleteSequenceRx, FlashBlockRx, - FlashBlockService, FlashblocksListeners, PendingBlockRx, PendingFlashBlock, WsFlashBlockStream, + FlashBlockBuildInfo, FlashBlockCompleteSequence, FlashBlockCompleteSequenceRx, + FlashBlockConsensusClient, FlashBlockRx, FlashBlockService, FlashblocksListeners, + PendingBlockRx, PendingFlashBlock, WsFlashBlockStream, }; use reth_rpc::eth::core::EthApiInner; use reth_rpc_eth_api::{ @@ -398,6 +400,12 @@ pub struct OpEthApiBuilder { /// /// [flashblocks]: reth_optimism_flashblocks flashblocks_url: Option, + /// Enable flashblock consensus client to drive the chain forward. + /// + /// When enabled, flashblock sequences are submitted to the engine API via + /// `newPayload` and `forkchoiceUpdated` calls, advancing the canonical chain state. + /// Requires `flashblocks_url` to be set. + flashblock_consensus: bool, /// Marker for network types. _nt: PhantomData, } @@ -409,6 +417,7 @@ impl Default for OpEthApiBuilder { sequencer_headers: Vec::new(), min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, _nt: PhantomData, } } @@ -422,6 +431,7 @@ impl OpEthApiBuilder { sequencer_headers: Vec::new(), min_suggested_priority_fee: 1_000_000, flashblocks_url: None, + flashblock_consensus: false, _nt: PhantomData, } } @@ -449,6 +459,12 @@ impl OpEthApiBuilder { self.flashblocks_url = flashblocks_url; self } + + /// With flashblock consensus client enabled to drive chain forward + pub const fn with_flashblock_consensus(mut self, flashblock_consensus: bool) -> Self { + self.flashblock_consensus = flashblock_consensus; + self + } } impl EthApiBuilder for OpEthApiBuilder @@ -456,10 +472,18 @@ where N: FullNodeComponents< Evm: ConfigureEvm< NextBlockEnvCtx: BuildPendingEnv> - + From + + From + Unpin, >, - Types: NodeTypes, + Types: NodeTypes< + ChainSpec: Hardforks + EthereumHardforks, + Payload: reth_node_api::PayloadTypes< + ExecutionData: for<'a> TryFrom< + &'a FlashBlockCompleteSequence, + Error: std::fmt::Display, + >, + >, + >, >, NetworkT: RpcTypes, OpRpcConvert: RpcConvert, @@ -474,6 +498,7 @@ where sequencer_headers, min_suggested_priority_fee, flashblocks_url, + flashblock_consensus, .. } = self; let rpc_converter = @@ -500,14 +525,23 @@ where ctx.components.evm_config().clone(), ctx.components.provider().clone(), ctx.components.task_executor().clone(), - ); + ) + .compute_state_root(flashblock_consensus); // enable state root calculation if flashblock_consensus if enabled. let flashblocks_sequence = service.block_sequence_broadcaster().clone(); let received_flashblocks = service.flashblocks_broadcaster().clone(); let in_progress_rx = service.subscribe_in_progress(); - ctx.components.task_executor().spawn(Box::pin(service.run(tx))); + if flashblock_consensus { + info!(target: "reth::cli", "Launching FlashBlockConsensusClient"); + let flashblock_client = FlashBlockConsensusClient::new( + ctx.engine_handle.clone(), + flashblocks_sequence.subscribe(), + )?; + ctx.components.task_executor().spawn(Box::pin(flashblock_client.run())); + } + Some(FlashblocksListeners::new( pending_rx, flashblocks_sequence, diff --git a/crates/optimism/rpc/src/historical.rs b/crates/optimism/rpc/src/historical.rs index 736d962b6d..6037da4fe7 100644 --- a/crates/optimism/rpc/src/historical.rs +++ b/crates/optimism/rpc/src/historical.rs @@ -5,8 +5,9 @@ use alloy_eips::BlockId; use alloy_json_rpc::{RpcRecv, RpcSend}; use alloy_primitives::{BlockNumber, B256}; use alloy_rpc_client::RpcClient; +use jsonrpsee::BatchResponseBuilder; use jsonrpsee_core::{ - middleware::{Batch, Notification, RpcServiceT}, + middleware::{Batch, BatchEntry, Notification, RpcServiceT}, server::MethodResponse, }; use jsonrpsee_types::{Params, Request}; @@ -122,8 +123,14 @@ impl HistoricalRpcService { impl RpcServiceT for HistoricalRpcService where - S: RpcServiceT + Send + Sync + Clone + 'static, - + S: RpcServiceT< + MethodResponse = MethodResponse, + BatchResponse = MethodResponse, + NotificationResponse = MethodResponse, + > + Send + + Sync + + Clone + + 'static, P: BlockReaderIdExt + TransactionsProvider + Send + Sync + Clone + 'static, { type MethodResponse = S::MethodResponse; @@ -145,8 +152,64 @@ where }) } - fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { - self.inner.batch(req) + fn batch<'a>( + &self, + mut req: Batch<'a>, + ) -> impl Future + Send + 'a { + let this = self.clone(); + let historical = self.historical.clone(); + + async move { + let mut needs_forwarding = false; + for entry in req.iter_mut() { + if let Ok(BatchEntry::Call(call)) = entry && + historical.should_forward_request(call) + { + needs_forwarding = true; + break; + } + } + + if !needs_forwarding { + // no call needs to be forwarded and we can simply perform this batch request + return this.inner.batch(req).await; + } + + // the entire response is checked above so we can assume that these don't exceed + let mut batch_rp = BatchResponseBuilder::new_with_limit(usize::MAX); + let mut got_notification = false; + + for batch_entry in req { + match batch_entry { + Ok(BatchEntry::Call(req)) => { + let rp = this.call(req).await; + if let Err(err) = batch_rp.append(rp) { + return err; + } + } + Ok(BatchEntry::Notification(n)) => { + got_notification = true; + this.notification(n).await; + } + Err(err) => { + let (err, id) = err.into_parts(); + let rp = MethodResponse::error(id, err); + if let Err(err) = batch_rp.append(rp) { + return err; + } + } + } + } + + // If the batch is empty and we got a notification, we return an empty response. + if batch_rp.is_empty() && got_notification { + MethodResponse::notification() + } + // An empty batch is regarded as an invalid request here. + else { + MethodResponse::from_batch(batch_rp.finish()) + } + } } fn notification<'a>( @@ -171,21 +234,23 @@ impl

HistoricalRpcInner

where P: BlockReaderIdExt + TransactionsProvider + Send + Sync + Clone, { - /// Checks if a request should be forwarded to the historical endpoint and returns - /// the response if it was forwarded. - async fn maybe_forward_request(&self, req: &Request<'_>) -> Option { - let should_forward = match req.method_name() { + /// Checks if a request should be forwarded to the historical endpoint (synchronous check). + fn should_forward_request(&self, req: &Request<'_>) -> bool { + match req.method_name() { "debug_traceTransaction" | "eth_getTransactionByHash" | "eth_getTransactionReceipt" | "eth_getRawTransactionByHash" => self.should_forward_transaction(req), method => self.should_forward_block_request(method, req), - }; + } + } - if should_forward { + /// Checks if a request should be forwarded to the historical endpoint and returns + /// the response if it was forwarded. + async fn maybe_forward_request(&self, req: &Request<'_>) -> Option { + if self.should_forward_request(req) { return self.forward_to_historical(req).await } - None } diff --git a/crates/payload/basic/src/better_payload_emitter.rs b/crates/payload/basic/src/better_payload_emitter.rs index a6fcaa08ec..216995ed9b 100644 --- a/crates/payload/basic/src/better_payload_emitter.rs +++ b/crates/payload/basic/src/better_payload_emitter.rs @@ -3,8 +3,8 @@ use reth_payload_builder::PayloadBuilderError; use std::sync::Arc; use tokio::sync::broadcast; -/// Emits events when a better payload is built. Delegates the actual payload building -/// to an inner [`PayloadBuilder`]. +/// Emits events when a payload is built (both `Better` and `Freeze` outcomes). +/// Delegates the actual payload building to an inner [`PayloadBuilder`]. #[derive(Debug, Clone)] pub struct BetterPayloadEmitter { better_payloads_tx: broadcast::Sender>, @@ -16,7 +16,8 @@ where PB: PayloadBuilder, { /// Create a new [`BetterPayloadEmitter`] with the given inner payload builder. - /// Owns the sender half of a broadcast channel that emits the better payloads. + /// Owns the sender half of a broadcast channel that emits payloads when they are built + /// (for both `Better` and `Freeze` outcomes). pub const fn new( better_payloads_tx: broadcast::Sender>, inner: PB, @@ -38,9 +39,14 @@ where args: BuildArguments, ) -> Result, PayloadBuilderError> { match self.inner.try_build(args) { - Ok(BuildOutcome::Better { payload, cached_reads }) => { - let _ = self.better_payloads_tx.send(Arc::new(payload.clone())); - Ok(BuildOutcome::Better { payload, cached_reads }) + Ok(res) => { + // Emit payload for both Better and Freeze outcomes, as both represent valid + // payloads that should be available to subscribers (e.g., for + // insertion into engine service). + if let Some(payload) = res.payload().cloned() { + let _ = self.better_payloads_tx.send(Arc::new(payload)); + } + Ok(res) } res => res, } diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index aa2b1f6680..84a4f64a4e 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -706,7 +706,7 @@ pub enum BuildOutcome { } impl BuildOutcome { - /// Consumes the type and returns the payload if the outcome is `Better`. + /// Consumes the type and returns the payload if the outcome is `Better` or `Freeze`. pub fn into_payload(self) -> Option { match self { Self::Better { payload, .. } | Self::Freeze(payload) => Some(payload), @@ -714,11 +714,24 @@ impl BuildOutcome { } } + /// Consumes the type and returns the payload if the outcome is `Better` or `Freeze`. + pub const fn payload(&self) -> Option<&Payload> { + match self { + Self::Better { payload, .. } | Self::Freeze(payload) => Some(payload), + _ => None, + } + } + /// Returns true if the outcome is `Better`. pub const fn is_better(&self) -> bool { matches!(self, Self::Better { .. }) } + /// Returns true if the outcome is `Freeze`. + pub const fn is_frozen(&self) -> bool { + matches!(self, Self::Freeze { .. }) + } + /// Returns true if the outcome is `Aborted`. pub const fn is_aborted(&self) -> bool { matches!(self, Self::Aborted { .. }) diff --git a/crates/payload/builder/src/service.rs b/crates/payload/builder/src/service.rs index f3f1b03ab2..507b302651 100644 --- a/crates/payload/builder/src/service.rs +++ b/crates/payload/builder/src/service.rs @@ -441,7 +441,7 @@ where this.metrics.inc_initiated_jobs(); new_job = true; this.payload_jobs.push((job, id)); - this.payload_events.send(Events::Attributes(attr.clone())).ok(); + this.payload_events.send(Events::Attributes(attr)).ok(); } Err(err) => { this.metrics.inc_failed_jobs(); diff --git a/crates/payload/primitives/src/lib.rs b/crates/payload/primitives/src/lib.rs index c6db7fbfd9..45cd40010f 100644 --- a/crates/payload/primitives/src/lib.rs +++ b/crates/payload/primitives/src/lib.rs @@ -13,7 +13,6 @@ extern crate alloc; -use crate::alloc::string::ToString; use alloy_primitives::Bytes; use reth_chainspec::EthereumHardforks; use reth_primitives_traits::{NodePrimitives, SealedBlock}; @@ -535,21 +534,19 @@ pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjec let mut last_request_type = None; for request in requests { if request.len() <= 1 { - return Err(EngineObjectValidationError::InvalidParams( - "EmptyExecutionRequest".to_string().into(), - )) + return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into())) } let request_type = request[0]; if Some(request_type) < last_request_type { return Err(EngineObjectValidationError::InvalidParams( - "OutOfOrderExecutionRequest".to_string().into(), + "OutOfOrderExecutionRequest".into(), )) } if Some(request_type) == last_request_type { return Err(EngineObjectValidationError::InvalidParams( - "DuplicatedExecutionRequestType".to_string().into(), + "DuplicatedExecutionRequestType".into(), )) } diff --git a/crates/payload/primitives/src/traits.rs b/crates/payload/primitives/src/traits.rs index fdc078887d..a95a7209e9 100644 --- a/crates/payload/primitives/src/traits.rs +++ b/crates/payload/primitives/src/traits.rs @@ -42,17 +42,21 @@ pub struct BuiltPayloadExecutedBlock { impl BuiltPayloadExecutedBlock { /// Converts this into an [`reth_chain_state::ExecutedBlock`]. /// - /// If the hashed state or trie updates are in sorted form, they will be converted - /// back to their unsorted representations. + /// Ensures hashed state and trie updates are in their sorted representations + /// as required by `reth_chain_state::ExecutedBlock`. pub fn into_executed_payload(self) -> reth_chain_state::ExecutedBlock { let hashed_state = match self.hashed_state { - Either::Left(unsorted) => unsorted, - Either::Right(sorted) => Arc::new(Arc::unwrap_or_clone(sorted).into()), + // Convert unsorted to sorted + Either::Left(unsorted) => Arc::new(Arc::unwrap_or_clone(unsorted).into_sorted()), + // Already sorted + Either::Right(sorted) => sorted, }; let trie_updates = match self.trie_updates { - Either::Left(unsorted) => unsorted, - Either::Right(sorted) => Arc::new(Arc::unwrap_or_clone(sorted).into()), + // Convert unsorted to sorted + Either::Left(unsorted) => Arc::new(Arc::unwrap_or_clone(unsorted).into_sorted()), + // Already sorted + Either::Right(sorted) => sorted, }; reth_chain_state::ExecutedBlock { diff --git a/crates/primitives-traits/src/constants/gas_units.rs b/crates/primitives-traits/src/constants/gas_units.rs index e311e34d0a..986f207034 100644 --- a/crates/primitives-traits/src/constants/gas_units.rs +++ b/crates/primitives-traits/src/constants/gas_units.rs @@ -10,10 +10,14 @@ pub const MEGAGAS: u64 = KILOGAS * 1_000; /// Represents one Gigagas, or `1_000_000_000` gas. pub const GIGAGAS: u64 = MEGAGAS * 1_000; +/// Represents one Teragas, or `1_000_000_000_000` gas. +pub const TERAGAS: u64 = GIGAGAS * 1_000; + /// Returns a formatted gas throughput log, showing either: /// * "Kgas/s", or 1,000 gas per second /// * "Mgas/s", or 1,000,000 gas per second /// * "Ggas/s", or 1,000,000,000 gas per second +/// * "Tgas/s", or 1,000,000,000,000 gas per second /// /// Depending on the magnitude of the gas throughput. pub fn format_gas_throughput(gas: u64, execution_duration: Duration) -> String { @@ -22,8 +26,10 @@ pub fn format_gas_throughput(gas: u64, execution_duration: Duration) -> String { format!("{:.2}Kgas/second", gas_per_second / KILOGAS as f64) } else if gas_per_second < GIGAGAS as f64 { format!("{:.2}Mgas/second", gas_per_second / MEGAGAS as f64) - } else { + } else if gas_per_second < TERAGAS as f64 { format!("{:.2}Ggas/second", gas_per_second / GIGAGAS as f64) + } else { + format!("{:.2}Tgas/second", gas_per_second / TERAGAS as f64) } } @@ -31,6 +37,7 @@ pub fn format_gas_throughput(gas: u64, execution_duration: Duration) -> String { /// * "Kgas", or 1,000 gas /// * "Mgas", or 1,000,000 gas /// * "Ggas", or 1,000,000,000 gas +/// * "Tgas", or 1,000,000,000,000 gas /// /// Depending on the magnitude of gas. pub fn format_gas(gas: u64) -> String { @@ -39,8 +46,10 @@ pub fn format_gas(gas: u64) -> String { format!("{:.2}Kgas", gas / KILOGAS as f64) } else if gas < GIGAGAS as f64 { format!("{:.2}Mgas", gas / MEGAGAS as f64) - } else { + } else if gas < TERAGAS as f64 { format!("{:.2}Ggas", gas / GIGAGAS as f64) + } else { + format!("{:.2}Tgas", gas / TERAGAS as f64) } } @@ -65,6 +74,10 @@ mod tests { let gas = 100_000_000_000; let gas_unit = format_gas(gas); assert_eq!(gas_unit, "100.00Ggas"); + + let gas = 100_000_000_000_000; + let gas_unit = format_gas(gas); + assert_eq!(gas_unit, "100.00Tgas"); } #[test] diff --git a/crates/primitives-traits/src/serde_bincode_compat.rs b/crates/primitives-traits/src/serde_bincode_compat.rs index e5c849ae84..2752dcfc83 100644 --- a/crates/primitives-traits/src/serde_bincode_compat.rs +++ b/crates/primitives-traits/src/serde_bincode_compat.rs @@ -346,4 +346,17 @@ mod block_bincode { repr.into() } } + + #[cfg(feature = "op")] + impl super::SerdeBincodeCompat for op_alloy_consensus::OpReceipt { + type BincodeRepr<'a> = op_alloy_consensus::serde_bincode_compat::OpReceipt<'a>; + + fn as_repr(&self) -> Self::BincodeRepr<'_> { + self.into() + } + + fn from_repr(repr: Self::BincodeRepr<'_>) -> Self { + repr.into() + } + } } diff --git a/crates/primitives-traits/src/size.rs b/crates/primitives-traits/src/size.rs index 82c8b5d9c4..e2343cfb95 100644 --- a/crates/primitives-traits/src/size.rs +++ b/crates/primitives-traits/src/size.rs @@ -148,6 +148,18 @@ mod op { } } + impl InMemorySize for op_alloy_consensus::OpReceipt { + fn size(&self) -> usize { + match self { + Self::Legacy(receipt) | + Self::Eip2930(receipt) | + Self::Eip1559(receipt) | + Self::Eip7702(receipt) => receipt.size(), + Self::Deposit(receipt) => receipt.size(), + } + } + } + impl InMemorySize for op_alloy_consensus::OpTypedTransaction { fn size(&self) -> usize { match self { diff --git a/crates/primitives-traits/src/transaction/signed.rs b/crates/primitives-traits/src/transaction/signed.rs index 08a6758d8d..a6212a6c68 100644 --- a/crates/primitives-traits/src/transaction/signed.rs +++ b/crates/primitives-traits/src/transaction/signed.rs @@ -6,7 +6,7 @@ use alloy_consensus::{ transaction::{Recovered, RlpEcdsaEncodableTx, SignerRecoverable, TxHashRef}, EthereumTxEnvelope, SignableTransaction, }; -use alloy_eips::eip2718::{Decodable2718, Encodable2718}; +use alloy_eips::eip2718::{Decodable2718, Encodable2718, IsTyped2718}; use alloy_primitives::{keccak256, Address, Signature, B256}; use alloy_rlp::{Decodable, Encodable}; use core::hash::Hash; @@ -46,6 +46,7 @@ pub trait SignedTransaction: + InMemorySize + SignerRecoverable + TxHashRef + + IsTyped2718 { /// Returns whether this transaction type can be __broadcasted__ as full transaction over the /// network. diff --git a/crates/prune/prune/src/builder.rs b/crates/prune/prune/src/builder.rs index 78283710e1..9e451ea49f 100644 --- a/crates/prune/prune/src/builder.rs +++ b/crates/prune/prune/src/builder.rs @@ -7,7 +7,7 @@ use reth_primitives_traits::NodePrimitives; use reth_provider::{ providers::StaticFileProvider, BlockReader, ChainStateBlockReader, DBProvider, DatabaseProviderFactory, NodePrimitivesProvider, PruneCheckpointReader, PruneCheckpointWriter, - StaticFileProviderFactory, + StaticFileProviderFactory, StorageSettingsCache, }; use reth_prune_types::PruneModes; use std::time::Duration; @@ -80,6 +80,7 @@ impl PrunerBuilder { + PruneCheckpointReader + BlockReader + ChainStateBlockReader + + StorageSettingsCache + StaticFileProviderFactory< Primitives: NodePrimitives, >, @@ -112,7 +113,8 @@ impl PrunerBuilder { + BlockReader + ChainStateBlockReader + PruneCheckpointWriter - + PruneCheckpointReader, + + PruneCheckpointReader + + StorageSettingsCache, { let segments = SegmentSet::::from_components(static_file_provider, self.segments); diff --git a/crates/prune/prune/src/db_ext.rs b/crates/prune/prune/src/db_ext.rs index 63ab87c446..09224a795f 100644 --- a/crates/prune/prune/src/db_ext.rs +++ b/crates/prune/prune/src/db_ext.rs @@ -19,11 +19,12 @@ pub(crate) trait DbTxPruneExt: DbTxMut { mut delete_callback: impl FnMut(TableRow), ) -> Result<(usize, bool), DatabaseError> { let mut cursor = self.cursor_write::()?; - let mut keys = keys.into_iter(); + let mut keys = keys.into_iter().peekable(); let mut deleted_entries = 0; - for key in &mut keys { + let mut done = true; + while keys.peek().is_some() { if limiter.is_limit_reached() { debug!( target: "providers::db", @@ -33,9 +34,11 @@ pub(crate) trait DbTxPruneExt: DbTxMut { table = %T::NAME, "Pruning limit reached" ); + done = false; break } + let key = keys.next().expect("peek() said Some"); let row = cursor.seek_exact(key)?; if let Some(row) = row { cursor.delete_current()?; @@ -45,7 +48,6 @@ pub(crate) trait DbTxPruneExt: DbTxMut { } } - let done = keys.next().is_none(); Ok((deleted_entries, done)) } @@ -124,3 +126,158 @@ pub(crate) trait DbTxPruneExt: DbTxMut { } impl DbTxPruneExt for Tx where Tx: DbTxMut {} + +#[cfg(test)] +mod tests { + use super::DbTxPruneExt; + use crate::PruneLimiter; + use reth_db_api::tables; + use reth_primitives_traits::SignerRecoverable; + use reth_provider::{DBProvider, DatabaseProviderFactory}; + use reth_stages::test_utils::{StorageKind, TestStageDB}; + use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams}; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; + + struct CountingIter { + data: Vec, + calls: Arc, + } + + impl CountingIter { + fn new(data: Vec, calls: Arc) -> Self { + Self { data, calls } + } + } + + struct CountingIntoIter { + inner: std::vec::IntoIter, + calls: Arc, + } + + impl Iterator for CountingIntoIter { + type Item = u64; + fn next(&mut self) -> Option { + let res = self.inner.next(); + self.calls.fetch_add(1, Ordering::SeqCst); + res + } + } + + impl IntoIterator for CountingIter { + type Item = u64; + type IntoIter = CountingIntoIter; + fn into_iter(self) -> Self::IntoIter { + CountingIntoIter { inner: self.data.into_iter(), calls: self.calls } + } + } + + #[test] + fn prune_table_with_iterator_early_exit_does_not_overconsume() { + let db = TestStageDB::default(); + let mut rng = generators::rng(); + + let blocks = random_block_range( + &mut rng, + 1..=3, + BlockRangeParams { + parent: Some(alloy_primitives::B256::ZERO), + tx_count: 2..3, + ..Default::default() + }, + ); + db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks"); + + let mut tx_senders = Vec::new(); + for block in &blocks { + tx_senders.reserve_exact(block.transaction_count()); + for transaction in &block.body().transactions { + tx_senders.push(( + tx_senders.len() as u64, + transaction.recover_signer().expect("recover signer"), + )); + } + } + let total = tx_senders.len(); + db.insert_transaction_senders(tx_senders).expect("insert transaction senders"); + + let provider = db.factory.database_provider_rw().unwrap(); + + let calls = Arc::new(AtomicUsize::new(0)); + let keys: Vec = (0..total as u64).collect(); + let counting_iter = CountingIter::new(keys, calls.clone()); + + let mut limiter = PruneLimiter::default().set_deleted_entries_limit(2); + + let (pruned, done) = provider + .tx_ref() + .prune_table_with_iterator::( + counting_iter, + &mut limiter, + |_| {}, + ) + .expect("prune"); + + assert_eq!(pruned, 2); + assert!(!done); + assert_eq!(calls.load(Ordering::SeqCst), pruned + 1); + + provider.commit().expect("commit"); + assert_eq!(db.table::().unwrap().len(), total - 2); + } + + #[test] + fn prune_table_with_iterator_consumes_to_end_reports_done() { + let db = TestStageDB::default(); + let mut rng = generators::rng(); + + let blocks = random_block_range( + &mut rng, + 1..=2, + BlockRangeParams { + parent: Some(alloy_primitives::B256::ZERO), + tx_count: 1..2, + ..Default::default() + }, + ); + db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks"); + + let mut tx_senders = Vec::new(); + for block in &blocks { + for transaction in &block.body().transactions { + tx_senders.push(( + tx_senders.len() as u64, + transaction.recover_signer().expect("recover signer"), + )); + } + } + let total = tx_senders.len(); + db.insert_transaction_senders(tx_senders).expect("insert transaction senders"); + + let provider = db.factory.database_provider_rw().unwrap(); + + let calls = Arc::new(AtomicUsize::new(0)); + let keys: Vec = (0..total as u64).collect(); + let counting_iter = CountingIter::new(keys, calls.clone()); + + let mut limiter = PruneLimiter::default().set_deleted_entries_limit(usize::MAX); + + let (pruned, done) = provider + .tx_ref() + .prune_table_with_iterator::( + counting_iter, + &mut limiter, + |_| {}, + ) + .expect("prune"); + + assert_eq!(pruned, total); + assert!(done); + assert_eq!(calls.load(Ordering::SeqCst), total + 1); + + provider.commit().expect("commit"); + assert_eq!(db.table::().unwrap().len(), 0); + } +} diff --git a/crates/prune/prune/src/segments/mod.rs b/crates/prune/prune/src/segments/mod.rs index f917c78ea9..86e8597088 100644 --- a/crates/prune/prune/src/segments/mod.rs +++ b/crates/prune/prune/src/segments/mod.rs @@ -4,8 +4,14 @@ mod user; use crate::{PruneLimiter, PrunerError}; use alloy_primitives::{BlockNumber, TxNumber}; -use reth_provider::{errors::provider::ProviderResult, BlockReader, PruneCheckpointWriter}; -use reth_prune_types::{PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, SegmentOutput}; +use reth_provider::{ + errors::provider::ProviderResult, BlockReader, PruneCheckpointWriter, StaticFileProviderFactory, +}; +use reth_prune_types::{ + PruneCheckpoint, PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput, + SegmentOutputCheckpoint, +}; +use reth_static_file_types::StaticFileSegment; pub use set::SegmentSet; use std::{fmt::Debug, ops::RangeInclusive}; use tracing::error; @@ -14,6 +20,39 @@ pub use user::{ SenderRecovery, StorageHistory, TransactionLookup, }; +/// Prunes data from static files for a given segment. +/// +/// This is a generic helper function used by both receipts and bodies pruning +/// when data is stored in static files. +pub(crate) fn prune_static_files( + provider: &Provider, + input: PruneInput, + segment: StaticFileSegment, +) -> Result +where + Provider: StaticFileProviderFactory, +{ + let deleted_headers = + provider.static_file_provider().delete_segment_below_block(segment, input.to_block + 1)?; + + if deleted_headers.is_empty() { + return Ok(SegmentOutput::done()) + } + + let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range()); + + let pruned = tx_ranges.clone().map(|range| range.len()).sum::() as usize; + + Ok(SegmentOutput { + progress: PruneProgress::Finished, + pruned, + checkpoint: Some(SegmentOutputCheckpoint { + block_number: Some(input.to_block), + tx_number: tx_ranges.map(|range| range.end()).max(), + }), + }) +} + /// A segment represents a pruning of some portion of the data. /// /// Segments are called from [`Pruner`](crate::Pruner) with the following lifecycle: diff --git a/crates/prune/prune/src/segments/receipts.rs b/crates/prune/prune/src/segments/receipts.rs index 5e9d39aca1..30915f89b2 100644 --- a/crates/prune/prune/src/segments/receipts.rs +++ b/crates/prune/prune/src/segments/receipts.rs @@ -3,15 +3,21 @@ //! - [`crate::segments::user::Receipts`] is responsible for pruning receipts according to the //! user-configured settings (for example, on a full node or with a custom prune config) -use crate::{db_ext::DbTxPruneExt, segments::PruneInput, PrunerError}; +use crate::{ + db_ext::DbTxPruneExt, + segments::{self, PruneInput}, + PrunerError, +}; use reth_db_api::{table::Value, tables, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ - errors::provider::ProviderResult, BlockReader, DBProvider, NodePrimitivesProvider, - PruneCheckpointWriter, TransactionsProvider, + errors::provider::ProviderResult, BlockReader, DBProvider, EitherWriter, + NodePrimitivesProvider, PruneCheckpointWriter, StaticFileProviderFactory, StorageSettingsCache, + TransactionsProvider, }; use reth_prune_types::{PruneCheckpoint, PruneSegment, SegmentOutput, SegmentOutputCheckpoint}; -use tracing::trace; +use reth_static_file_types::StaticFileSegment; +use tracing::{debug, trace}; pub(crate) fn prune( provider: &Provider, @@ -21,8 +27,17 @@ where Provider: DBProvider + TransactionsProvider + BlockReader + + StorageSettingsCache + + StaticFileProviderFactory + NodePrimitivesProvider>, { + if EitherWriter::receipts_destination(provider).is_static_file() { + debug!(target: "pruner", "Pruning receipts from static files."); + return segments::prune_static_files(provider, input, StaticFileSegment::Receipts) + } + debug!(target: "pruner", "Pruning receipts from database."); + + // Original database implementation for when receipts are not on static files (old nodes) let tx_range = match input.get_next_tx_num_range(provider)? { Some(range) => range, None => { @@ -98,8 +113,14 @@ mod tests { use std::ops::Sub; #[test] - fn prune() { - let db = TestStageDB::default(); + fn prune_legacy() { + let mut db = TestStageDB::default(); + // Configure the factory to use database for receipts by enabling receipt pruning. + // This ensures EitherWriter::receipts_destination returns Database instead of StaticFile. + db.factory = db.factory.with_prune_modes(reth_prune_types::PruneModes { + receipts: Some(PruneMode::Full), + ..Default::default() + }); let mut rng = generators::rng(); let blocks = random_block_range( diff --git a/crates/prune/prune/src/segments/set.rs b/crates/prune/prune/src/segments/set.rs index 7ae9e044e2..479ab4f25b 100644 --- a/crates/prune/prune/src/segments/set.rs +++ b/crates/prune/prune/src/segments/set.rs @@ -7,7 +7,7 @@ use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ providers::StaticFileProvider, BlockReader, ChainStateBlockReader, DBProvider, - PruneCheckpointReader, PruneCheckpointWriter, StaticFileProviderFactory, + PruneCheckpointReader, PruneCheckpointWriter, StaticFileProviderFactory, StorageSettingsCache, }; use reth_prune_types::PruneModes; @@ -51,7 +51,8 @@ where + PruneCheckpointWriter + PruneCheckpointReader + BlockReader - + ChainStateBlockReader, + + ChainStateBlockReader + + StorageSettingsCache, { /// Creates a [`SegmentSet`] from an existing components, such as [`StaticFileProvider`] and /// [`PruneModes`]. diff --git a/crates/prune/prune/src/segments/user/bodies.rs b/crates/prune/prune/src/segments/user/bodies.rs index 0a6a432754..4d0dca41b1 100644 --- a/crates/prune/prune/src/segments/user/bodies.rs +++ b/crates/prune/prune/src/segments/user/bodies.rs @@ -1,11 +1,9 @@ use crate::{ - segments::{PruneInput, Segment}, + segments::{self, PruneInput, Segment}, PrunerError, }; use reth_provider::{BlockReader, StaticFileProviderFactory}; -use reth_prune_types::{ - PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint, -}; +use reth_prune_types::{PruneMode, PrunePurpose, PruneSegment, SegmentOutput}; use reth_static_file_types::StaticFileSegment; /// Segment responsible for pruning transactions in static files. @@ -40,26 +38,7 @@ where } fn prune(&self, provider: &Provider, input: PruneInput) -> Result { - let deleted_headers = provider - .static_file_provider() - .delete_segment_below_block(StaticFileSegment::Transactions, input.to_block + 1)?; - - if deleted_headers.is_empty() { - return Ok(SegmentOutput::done()) - } - - let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range()); - - let pruned = tx_ranges.clone().map(|range| range.len()).sum::() as usize; - - Ok(SegmentOutput { - progress: PruneProgress::Finished, - pruned, - checkpoint: Some(SegmentOutputCheckpoint { - block_number: Some(input.to_block), - tx_number: tx_ranges.map(|range| range.end()).max(), - }), - }) + segments::prune_static_files(provider, input, StaticFileSegment::Transactions) } } @@ -149,7 +128,7 @@ mod tests { let static_provider = factory.static_file_provider(); assert_eq!( - static_provider.get_lowest_static_file_block(StaticFileSegment::Transactions), + static_provider.get_lowest_range_end(StaticFileSegment::Transactions), test_case.expected_lowest_block ); assert_eq!( @@ -279,7 +258,10 @@ mod tests { // Set up initial state if provided if let Some(initial_range) = initial_range { *writer.user_header_mut() = SegmentHeader::new( - initial_range, + // Expected block range needs to have a fixed size that's determined by the + // provider itself + static_provider + .find_fixed_range(StaticFileSegment::Transactions, initial_range.start()), Some(initial_range), Some(initial_range), StaticFileSegment::Transactions, @@ -291,25 +273,21 @@ mod tests { // Verify initial state assert_eq!( - static_provider.get_lowest_static_file_block(StaticFileSegment::Transactions), + static_provider.get_lowest_range_end(StaticFileSegment::Transactions), expected_before_update, "Test case {}: Initial min_block mismatch", idx ); - // Update to new range - *writer.user_header_mut() = SegmentHeader::new( - updated_range, - Some(updated_range), - Some(updated_range), - StaticFileSegment::Transactions, - ); + // Update to new block and tx ranges + writer.user_header_mut().set_block_range(updated_range.start(), updated_range.end()); + writer.user_header_mut().set_tx_range(updated_range.start(), updated_range.end()); writer.inner().set_dirty(); writer.commit().unwrap(); // update_index is called inside // Verify min_block was updated (not stuck at stale value) assert_eq!( - static_provider.get_lowest_static_file_block(StaticFileSegment::Transactions), + static_provider.get_lowest_range_end(StaticFileSegment::Transactions), Some(expected_after_update), "Test case {}: min_block should be updated to {} (not stuck at stale value)", idx, diff --git a/crates/prune/prune/src/segments/user/receipts.rs b/crates/prune/prune/src/segments/user/receipts.rs index 03faddc1d5..9f193b4ca3 100644 --- a/crates/prune/prune/src/segments/user/receipts.rs +++ b/crates/prune/prune/src/segments/user/receipts.rs @@ -6,7 +6,7 @@ use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ errors::provider::ProviderResult, BlockReader, DBProvider, NodePrimitivesProvider, - PruneCheckpointWriter, TransactionsProvider, + PruneCheckpointWriter, StaticFileProviderFactory, StorageSettingsCache, TransactionsProvider, }; use reth_prune_types::{PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, SegmentOutput}; use tracing::instrument; @@ -28,6 +28,8 @@ where + PruneCheckpointWriter + TransactionsProvider + BlockReader + + StorageSettingsCache + + StaticFileProviderFactory + NodePrimitivesProvider>, { fn segment(&self) -> PruneSegment { diff --git a/crates/ress/protocol/src/types.rs b/crates/ress/protocol/src/types.rs index 45ef3604a7..2c474ca79d 100644 --- a/crates/ress/protocol/src/types.rs +++ b/crates/ress/protocol/src/types.rs @@ -1,7 +1,16 @@ use alloy_primitives::bytes::{Buf, BufMut}; use alloy_rlp::{Decodable, Encodable}; -/// Node type variant. +/// Represents the type of node in the RESS protocol. +/// +/// This enum is used during the handshake phase to identify whether a peer is a stateless +/// or stateful node. The node type determines which connections are valid: +/// - Stateless ↔ Stateless: valid +/// - Stateless ↔ Stateful: valid +/// - Stateful ↔ Stateful: invalid +/// +/// Use [`is_valid_connection`](Self::is_valid_connection) to check if a connection between +/// two node types is allowed. #[repr(u8)] #[derive(PartialEq, Eq, Copy, Clone, Debug)] #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] diff --git a/crates/ress/provider/src/lib.rs b/crates/ress/provider/src/lib.rs index d986eb9e95..ecfdb3c4d8 100644 --- a/crates/ress/provider/src/lib.rs +++ b/crates/ress/provider/src/lib.rs @@ -156,11 +156,12 @@ where // NOTE: there might be a race condition where target ancestor hash gets evicted from the // database. let witness_state_provider = self.provider.state_by_block_hash(ancestor_hash)?; - let mut trie_input = TrieInput::default(); - for block in executed_ancestors.into_iter().rev() { - let trie_updates = block.trie_updates.as_ref(); - trie_input.append_cached_ref(trie_updates, &block.hashed_state); - } + let trie_input = TrieInput::from_blocks_sorted( + executed_ancestors + .iter() + .rev() + .map(|block| (block.hashed_state.as_ref(), block.trie_updates.as_ref())), + ); let mut hashed_state = db.into_state(); hashed_state.extend(record.hashed_state); diff --git a/crates/revm/src/cached.rs b/crates/revm/src/cached.rs index d40e814c12..a802b5a0e1 100644 --- a/crates/revm/src/cached.rs +++ b/crates/revm/src/cached.rs @@ -24,6 +24,7 @@ use revm::{bytecode::Bytecode, state::AccountInfo, Database, DatabaseRef}; /// let db = cached_reads.as_db_mut(db); /// // this is `Database` and can be used to build a payload, it never commits to `CachedReads` or the underlying database, but all reads from the underlying database are cached in `CachedReads`. /// // Subsequent payload build attempts can use cached reads and avoid hitting the underlying database. +/// // Note: `cached_reads` must outlive `db` to satisfy lifetime requirements. /// let state = State::builder().with_database(db).build(); /// } /// ``` @@ -71,6 +72,10 @@ impl CachedReads { } /// A [Database] that caches reads inside [`CachedReads`]. +/// +/// The lifetime parameter `'a` is tied to the lifetime of the underlying [`CachedReads`] instance. +/// This ensures that the cache remains valid for the entire duration this wrapper is used. +/// The original [`CachedReads`] must outlive this wrapper to prevent use-after-free. #[derive(Debug)] pub struct CachedReadsDbMut<'a, DB> { /// The cache of reads. @@ -158,6 +163,11 @@ impl Database for CachedReadsDbMut<'_, DB> { /// /// This is intended to be used as the [`DatabaseRef`] for /// `revm::db::State` for repeated payload build jobs. +/// +/// The lifetime parameter `'a` matches the lifetime of the underlying [`CachedReadsDbMut`], +/// which in turn is tied to the [`CachedReads`] cache. [`RefCell`] is used here to provide +/// interior mutability for the [`DatabaseRef`] trait (which requires `&self`), while the +/// lifetime ensures the cache remains valid throughout the wrapper's usage. #[derive(Debug)] pub struct CachedReadsDBRef<'a, DB> { /// The inner cache reads db mut. diff --git a/crates/rpc/rpc-builder/src/config.rs b/crates/rpc/rpc-builder/src/config.rs index 4d57bdec7d..6198b0ee0a 100644 --- a/crates/rpc/rpc-builder/src/config.rs +++ b/crates/rpc/rpc-builder/src/config.rs @@ -190,6 +190,13 @@ impl RethRpcServerConfig for RpcServerArgs { ); } + if self.ws_api.is_some() && !self.ws { + warn!( + target: "reth::cli", + "The --ws.api flag is set but --ws is not enabled. WS RPC API will not be exposed." + ); + } + if self.http { let socket_address = SocketAddr::new(self.http_addr, self.http_port); config = config diff --git a/crates/rpc/rpc-builder/src/lib.rs b/crates/rpc/rpc-builder/src/lib.rs index 6bd4223f60..6e373e614a 100644 --- a/crates/rpc/rpc-builder/src/lib.rs +++ b/crates/rpc/rpc-builder/src/lib.rs @@ -99,7 +99,7 @@ pub use eth::EthHandlers; // Rpc server metrics mod metrics; use crate::middleware::RethRpcMiddleware; -pub use metrics::{MeteredRequestFuture, RpcRequestMetricsService}; +pub use metrics::{MeteredBatchRequestsFuture, MeteredRequestFuture, RpcRequestMetricsService}; use reth_chain_state::CanonStateSubscriptions; use reth_rpc::eth::sim_bundle::EthSimBundle; diff --git a/crates/rpc/rpc-builder/src/metrics.rs b/crates/rpc/rpc-builder/src/metrics.rs index f32d90ed09..56bb9a313c 100644 --- a/crates/rpc/rpc-builder/src/metrics.rs +++ b/crates/rpc/rpc-builder/src/metrics.rs @@ -62,7 +62,7 @@ impl RpcRequestMetrics { Self::new(module, RpcTransport::WebSocket) } - /// Creates a new instance of the metrics layer for Ws. + /// Creates a new instance of the metrics layer for Ipc. pub(crate) fn ipc(module: &RpcModule<()>) -> Self { Self::new(module, RpcTransport::Ipc) } @@ -127,7 +127,20 @@ where } fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { - self.inner.batch(req) + self.metrics.inner.connection_metrics.batches_started_total.increment(1); + + for batch_entry in req.iter().flatten() { + let method_name = batch_entry.method_name(); + if let Some(call_metrics) = self.metrics.inner.call_metrics.get(method_name) { + call_metrics.started_total.increment(1); + } + } + + MeteredBatchRequestsFuture { + fut: self.inner.batch(req), + started_at: Instant::now(), + metrics: self.metrics.clone(), + } } fn notification<'a>( @@ -194,6 +207,42 @@ impl> Future for MeteredRequestFuture { } } +/// Response future to update the metrics for a batch of request/response pairs. +#[pin_project::pin_project] +pub struct MeteredBatchRequestsFuture { + #[pin] + fut: F, + /// time when the batch request started + started_at: Instant, + /// metrics for the batch + metrics: RpcRequestMetrics, +} + +impl std::fmt::Debug for MeteredBatchRequestsFuture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("MeteredBatchRequestsFuture") + } +} + +impl Future for MeteredBatchRequestsFuture +where + F: Future, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let res = this.fut.poll(cx); + + if res.is_ready() { + let elapsed = this.started_at.elapsed().as_secs_f64(); + this.metrics.inner.connection_metrics.batches_finished_total.increment(1); + this.metrics.inner.connection_metrics.batch_response_time_seconds.record(elapsed); + } + res + } +} + /// The transport protocol used for the RPC connection. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) enum RpcTransport { @@ -232,6 +281,12 @@ struct RpcServerConnectionMetrics { requests_finished_total: Counter, /// Response for a single request/response pair request_time_seconds: Histogram, + /// The number of batch requests started + batches_started_total: Counter, + /// The number of batch requests finished + batches_finished_total: Counter, + /// Response time for a batch request + batch_response_time_seconds: Histogram, } /// Metrics for the RPC calls diff --git a/crates/rpc/rpc-convert/Cargo.toml b/crates/rpc/rpc-convert/Cargo.toml index af43e9c54a..53b8d0541e 100644 --- a/crates/rpc/rpc-convert/Cargo.toml +++ b/crates/rpc/rpc-convert/Cargo.toml @@ -25,16 +25,13 @@ alloy-signer.workspace = true alloy-consensus.workspace = true alloy-network.workspace = true alloy-json-rpc.workspace = true +alloy-evm = { workspace = true, features = ["rpc"] } # optimism op-alloy-consensus = { workspace = true, optional = true } op-alloy-rpc-types = { workspace = true, optional = true } op-alloy-network = { workspace = true, optional = true } reth-optimism-primitives = { workspace = true, optional = true } -op-revm = { workspace = true, optional = true } - -# revm -revm-context.workspace = true # io jsonrpsee-types.workspace = true @@ -56,7 +53,7 @@ op = [ "dep:op-alloy-network", "dep:reth-optimism-primitives", "dep:reth-storage-api", - "dep:op-revm", "reth-evm/op", "reth-primitives-traits/op", + "alloy-evm/op", ] diff --git a/crates/rpc/rpc-convert/src/fees.rs b/crates/rpc/rpc-convert/src/fees.rs deleted file mode 100644 index 46f8fc8c20..0000000000 --- a/crates/rpc/rpc-convert/src/fees.rs +++ /dev/null @@ -1,281 +0,0 @@ -use alloy_primitives::{B256, U256}; -use std::cmp::min; -use thiserror::Error; - -/// Helper type for representing the fees of a `TransactionRequest` -#[derive(Debug)] -pub struct CallFees { - /// EIP-1559 priority fee - pub max_priority_fee_per_gas: Option, - /// Unified gas price setting - /// - /// Will be the configured `basefee` if unset in the request - /// - /// `gasPrice` for legacy, - /// `maxFeePerGas` for EIP-1559 - pub gas_price: U256, - /// Max Fee per Blob gas for EIP-4844 transactions - pub max_fee_per_blob_gas: Option, -} - -impl CallFees { - /// Ensures the fields of a `TransactionRequest` are not conflicting. - /// - /// # EIP-4844 transactions - /// - /// Blob transactions have an additional fee parameter `maxFeePerBlobGas`. - /// If the `maxFeePerBlobGas` or `blobVersionedHashes` are set we treat it as an EIP-4844 - /// transaction. - /// - /// Note: Due to the `Default` impl of [`BlockEnv`] (Some(0)) this assumes the `block_blob_fee` - /// is always `Some` - /// - /// ## Notable design decisions - /// - /// For compatibility reasons, this contains several exceptions when fee values are validated: - /// - If both `maxFeePerGas` and `maxPriorityFeePerGas` are set to `0` they are treated as - /// missing values, bypassing fee checks wrt. `baseFeePerGas`. - /// - /// This mirrors geth's behaviour when transaction requests are executed: - /// - /// [`BlockEnv`]: revm_context::BlockEnv - pub fn ensure_fees( - call_gas_price: Option, - call_max_fee: Option, - call_priority_fee: Option, - block_base_fee: U256, - blob_versioned_hashes: Option<&[B256]>, - max_fee_per_blob_gas: Option, - block_blob_fee: Option, - ) -> Result { - /// Get the effective gas price of a transaction as specfified in EIP-1559 with relevant - /// checks. - fn get_effective_gas_price( - max_fee_per_gas: Option, - max_priority_fee_per_gas: Option, - block_base_fee: U256, - ) -> Result { - match max_fee_per_gas { - Some(max_fee) => { - let max_priority_fee_per_gas = max_priority_fee_per_gas.unwrap_or(U256::ZERO); - - // only enforce the fee cap if provided input is not zero - if !(max_fee.is_zero() && max_priority_fee_per_gas.is_zero()) && - max_fee < block_base_fee - { - // `base_fee_per_gas` is greater than the `max_fee_per_gas` - return Err(CallFeesError::FeeCapTooLow) - } - if max_fee < max_priority_fee_per_gas { - return Err( - // `max_priority_fee_per_gas` is greater than the `max_fee_per_gas` - CallFeesError::TipAboveFeeCap, - ) - } - // ref - Ok(min( - max_fee, - block_base_fee - .checked_add(max_priority_fee_per_gas) - .ok_or(CallFeesError::TipVeryHigh)?, - )) - } - None => Ok(block_base_fee - .checked_add(max_priority_fee_per_gas.unwrap_or(U256::ZERO)) - .ok_or(CallFeesError::TipVeryHigh)?), - } - } - - let has_blob_hashes = - blob_versioned_hashes.as_ref().map(|blobs| !blobs.is_empty()).unwrap_or(false); - - match (call_gas_price, call_max_fee, call_priority_fee, max_fee_per_blob_gas) { - (gas_price, None, None, None) => { - // either legacy transaction or no fee fields are specified - // when no fields are specified, set gas price to zero - let gas_price = gas_price.unwrap_or(U256::ZERO); - Ok(Self { - gas_price, - max_priority_fee_per_gas: None, - max_fee_per_blob_gas: has_blob_hashes.then_some(block_blob_fee).flatten(), - }) - } - (None, max_fee_per_gas, max_priority_fee_per_gas, None) => { - // request for eip-1559 transaction - let effective_gas_price = get_effective_gas_price( - max_fee_per_gas, - max_priority_fee_per_gas, - block_base_fee, - )?; - let max_fee_per_blob_gas = has_blob_hashes.then_some(block_blob_fee).flatten(); - - Ok(Self { - gas_price: effective_gas_price, - max_priority_fee_per_gas, - max_fee_per_blob_gas, - }) - } - (None, max_fee_per_gas, max_priority_fee_per_gas, Some(max_fee_per_blob_gas)) => { - // request for eip-4844 transaction - let effective_gas_price = get_effective_gas_price( - max_fee_per_gas, - max_priority_fee_per_gas, - block_base_fee, - )?; - // Ensure blob_hashes are present - if !has_blob_hashes { - // Blob transaction but no blob hashes - return Err(CallFeesError::BlobTransactionMissingBlobHashes) - } - - Ok(Self { - gas_price: effective_gas_price, - max_priority_fee_per_gas, - max_fee_per_blob_gas: Some(max_fee_per_blob_gas), - }) - } - _ => { - // this fallback covers incompatible combinations of fields - Err(CallFeesError::ConflictingFeeFieldsInRequest) - } - } - } -} - -/// Error coming from decoding and validating transaction request fees. -#[derive(Debug, Error)] -pub enum CallFeesError { - /// Thrown when a call or transaction request (`eth_call`, `eth_estimateGas`, - /// `eth_sendTransaction`) contains conflicting fields (legacy, EIP-1559) - #[error("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")] - ConflictingFeeFieldsInRequest, - /// Thrown post London if the transaction's fee is less than the base fee of the block - #[error("max fee per gas less than block base fee")] - FeeCapTooLow, - /// Thrown to ensure no one is able to specify a transaction with a tip higher than the total - /// fee cap. - #[error("max priority fee per gas higher than max fee per gas")] - TipAboveFeeCap, - /// A sanity error to avoid huge numbers specified in the tip field. - #[error("max priority fee per gas higher than 2^256-1")] - TipVeryHigh, - /// Blob transaction has no versioned hashes - #[error("blob transaction missing blob hashes")] - BlobTransactionMissingBlobHashes, -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_consensus::constants::GWEI_TO_WEI; - - #[test] - fn test_ensure_0_fallback() { - let CallFees { gas_price, .. } = - CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO)) - .unwrap(); - assert!(gas_price.is_zero()); - } - - #[test] - fn test_ensure_max_fee_0_exception() { - let CallFees { gas_price, .. } = - CallFees::ensure_fees(None, Some(U256::ZERO), None, U256::from(99), None, None, None) - .unwrap(); - assert!(gas_price.is_zero()); - } - - #[test] - fn test_blob_fees() { - let CallFees { gas_price, max_fee_per_blob_gas, .. } = - CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO)) - .unwrap(); - assert!(gas_price.is_zero()); - assert_eq!(max_fee_per_blob_gas, None); - - let CallFees { gas_price, max_fee_per_blob_gas, .. } = CallFees::ensure_fees( - None, - None, - None, - U256::from(99), - Some(&[B256::from(U256::ZERO)]), - None, - Some(U256::from(99)), - ) - .unwrap(); - assert!(gas_price.is_zero()); - assert_eq!(max_fee_per_blob_gas, Some(U256::from(99))); - } - - #[test] - fn test_eip_1559_fees() { - let CallFees { gas_price, .. } = CallFees::ensure_fees( - None, - Some(U256::from(25 * GWEI_TO_WEI)), - Some(U256::from(15 * GWEI_TO_WEI)), - U256::from(15 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ) - .unwrap(); - assert_eq!(gas_price, U256::from(25 * GWEI_TO_WEI)); - - let CallFees { gas_price, .. } = CallFees::ensure_fees( - None, - Some(U256::from(25 * GWEI_TO_WEI)), - Some(U256::from(5 * GWEI_TO_WEI)), - U256::from(15 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ) - .unwrap(); - assert_eq!(gas_price, U256::from(20 * GWEI_TO_WEI)); - - let CallFees { gas_price, .. } = CallFees::ensure_fees( - None, - Some(U256::from(30 * GWEI_TO_WEI)), - Some(U256::from(30 * GWEI_TO_WEI)), - U256::from(15 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ) - .unwrap(); - assert_eq!(gas_price, U256::from(30 * GWEI_TO_WEI)); - - let call_fees = CallFees::ensure_fees( - None, - Some(U256::from(30 * GWEI_TO_WEI)), - Some(U256::from(31 * GWEI_TO_WEI)), - U256::from(15 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ); - assert!(call_fees.is_err()); - - let call_fees = CallFees::ensure_fees( - None, - Some(U256::from(5 * GWEI_TO_WEI)), - Some(U256::from(GWEI_TO_WEI)), - U256::from(15 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ); - assert!(call_fees.is_err()); - - let call_fees = CallFees::ensure_fees( - None, - Some(U256::MAX), - Some(U256::MAX), - U256::from(5 * GWEI_TO_WEI), - None, - None, - Some(U256::ZERO), - ); - assert!(call_fees.is_err()); - } -} diff --git a/crates/rpc/rpc-convert/src/lib.rs b/crates/rpc/rpc-convert/src/lib.rs index 9844b17b60..0d33251ce0 100644 --- a/crates/rpc/rpc-convert/src/lib.rs +++ b/crates/rpc/rpc-convert/src/lib.rs @@ -11,19 +11,19 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod block; -mod fees; pub mod receipt; mod rpc; pub mod transaction; pub use block::TryFromBlockResponse; -pub use fees::{CallFees, CallFeesError}; pub use receipt::TryFromReceiptResponse; pub use rpc::*; pub use transaction::{ - EthTxEnvError, IntoRpcTx, RpcConvert, RpcConverter, TransactionConversionError, - TryFromTransactionResponse, TryIntoSimTx, TxInfoMapper, + RpcConvert, RpcConverter, TransactionConversionError, TryFromTransactionResponse, TryIntoSimTx, + TxInfoMapper, }; +pub use alloy_evm::rpc::{CallFees, CallFeesError, EthTxEnvError, TryIntoTxEnv}; + #[cfg(feature = "op")] pub use transaction::op::*; diff --git a/crates/rpc/rpc-convert/src/transaction.rs b/crates/rpc/rpc-convert/src/transaction.rs index 6766ec43fb..f07d96aeb5 100644 --- a/crates/rpc/rpc-convert/src/transaction.rs +++ b/crates/rpc/rpc-convert/src/transaction.rs @@ -1,28 +1,20 @@ //! Compatibility functions for rpc `Transaction` type. use crate::{ - fees::{CallFees, CallFeesError}, - RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes, SignableTxRequest, + RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes, SignableTxRequest, TryIntoTxEnv, }; use alloy_consensus::{ error::ValueError, transaction::Recovered, EthereumTxEnvelope, Sealable, TxEip4844, }; use alloy_network::Network; -use alloy_primitives::{Address, TxKind, U256}; -use alloy_rpc_types_eth::{ - request::{TransactionInputError, TransactionRequest}, - Transaction, TransactionInfo, -}; +use alloy_primitives::{Address, U256}; +use alloy_rpc_types_eth::{request::TransactionRequest, Transaction, TransactionInfo}; use core::error; use dyn_clone::DynClone; -use reth_evm::{ - revm::context_interface::{either::Either, Block}, - BlockEnvFor, ConfigureEvm, EvmEnvFor, TxEnvFor, -}; +use reth_evm::{BlockEnvFor, ConfigureEvm, EvmEnvFor, TxEnvFor}; use reth_primitives_traits::{ BlockTy, HeaderTy, NodePrimitives, SealedBlock, SealedHeader, SealedHeaderFor, TransactionMeta, TxTy, }; -use revm_context::{BlockEnv, CfgEnv, TxEnv}; use std::{convert::Infallible, error::Error, fmt::Debug, marker::PhantomData}; use thiserror::Error; @@ -462,7 +454,7 @@ where tx_req: TxReq, evm_env: &EvmEnvFor, ) -> Result, Self::Error> { - tx_req.try_into_tx_env(&evm_env.cfg_env, &evm_env.block_env) + tx_req.try_into_tx_env(evm_env) } } @@ -491,122 +483,6 @@ where } } -/// Converts `self` into `T`. -/// -/// Should create an executable transaction environment using [`TransactionRequest`]. -pub trait TryIntoTxEnv { - /// An associated error that can occur during the conversion. - type Err; - - /// Performs the conversion. - fn try_into_tx_env( - self, - cfg_env: &CfgEnv, - block_env: &BlockEnv, - ) -> Result; -} - -/// An Ethereum specific transaction environment error than can occur during conversion from -/// [`TransactionRequest`]. -#[derive(Debug, Error)] -pub enum EthTxEnvError { - /// Error while decoding or validating transaction request fees. - #[error(transparent)] - CallFees(#[from] CallFeesError), - /// Both data and input fields are set and not equal. - #[error(transparent)] - Input(#[from] TransactionInputError), -} - -impl TryIntoTxEnv for TransactionRequest { - type Err = EthTxEnvError; - - fn try_into_tx_env( - self, - cfg_env: &CfgEnv, - block_env: &BlockEnv, - ) -> Result { - // Ensure that if versioned hashes are set, they're not empty - if self.blob_versioned_hashes.as_ref().is_some_and(|hashes| hashes.is_empty()) { - return Err(CallFeesError::BlobTransactionMissingBlobHashes.into()) - } - - let tx_type = self.minimal_tx_type() as u8; - - let Self { - from, - to, - gas_price, - max_fee_per_gas, - max_priority_fee_per_gas, - gas, - value, - input, - nonce, - access_list, - chain_id, - blob_versioned_hashes, - max_fee_per_blob_gas, - authorization_list, - transaction_type: _, - sidecar: _, - } = self; - - let CallFees { max_priority_fee_per_gas, gas_price, max_fee_per_blob_gas } = - CallFees::ensure_fees( - gas_price.map(U256::from), - max_fee_per_gas.map(U256::from), - max_priority_fee_per_gas.map(U256::from), - U256::from(block_env.basefee), - blob_versioned_hashes.as_deref(), - max_fee_per_blob_gas.map(U256::from), - block_env.blob_gasprice().map(U256::from), - )?; - - let gas_limit = gas.unwrap_or( - // Use maximum allowed gas limit. The reason for this - // is that both Erigon and Geth use pre-configured gas cap even if - // it's possible to derive the gas limit from the block: - // - block_env.gas_limit, - ); - - let chain_id = chain_id.unwrap_or(cfg_env.chain_id); - - let caller = from.unwrap_or_default(); - - let nonce = nonce.unwrap_or_default(); - - let env = TxEnv { - tx_type, - gas_limit, - nonce, - caller, - gas_price: gas_price.saturating_to(), - gas_priority_fee: max_priority_fee_per_gas.map(|v| v.saturating_to()), - kind: to.unwrap_or(TxKind::Create), - value: value.unwrap_or_default(), - data: input.try_into_unique_input().map_err(EthTxEnvError::from)?.unwrap_or_default(), - chain_id: Some(chain_id), - access_list: access_list.unwrap_or_default(), - // EIP-4844 fields - blob_hashes: blob_versioned_hashes.unwrap_or_default(), - max_fee_per_blob_gas: max_fee_per_blob_gas - .map(|v| v.saturating_to()) - .unwrap_or_default(), - // EIP-7702 fields - authorization_list: authorization_list - .unwrap_or_default() - .into_iter() - .map(Either::Left) - .collect(), - }; - - Ok(env) - } -} - /// Conversion into transaction RPC response failed. #[derive(Debug, Clone, Error)] #[error("Failed to convert transaction into RPC response: {0}")] @@ -990,13 +866,12 @@ where pub mod op { use super::*; use alloy_consensus::SignableTransaction; - use alloy_primitives::{Address, Bytes, Signature}; + use alloy_signer::Signature; use op_alloy_consensus::{ transaction::{OpDepositInfo, OpTransactionInfo}, OpTxEnvelope, }; use op_alloy_rpc_types::OpTransactionRequest; - use op_revm::OpTransaction; use reth_optimism_primitives::DepositReceipt; use reth_primitives_traits::SignedTransaction; use reth_storage_api::{errors::ProviderError, ReceiptProvider}; @@ -1054,22 +929,6 @@ pub mod op { Ok(tx.into_signed(signature).into()) } } - - impl TryIntoTxEnv> for OpTransactionRequest { - type Err = EthTxEnvError; - - fn try_into_tx_env( - self, - cfg_env: &CfgEnv, - block_env: &BlockEnv, - ) -> Result, Self::Err> { - Ok(OpTransaction { - base: self.as_ref().clone().try_into_tx_env(cfg_env, block_env)?, - enveloped_tx: Some(Bytes::new()), - deposit: Default::default(), - }) - } - } } /// Trait for converting network transaction responses to primitive transaction types. @@ -1146,8 +1005,6 @@ mod transaction_response_tests { #[cfg(feature = "op")] mod op { use super::*; - use crate::transaction::TryIntoTxEnv; - use revm_context::{BlockEnv, CfgEnv}; #[test] fn test_optimism_transaction_conversion() { @@ -1180,23 +1037,5 @@ mod transaction_response_tests { assert!(result.is_ok()); } - - #[test] - fn test_op_into_tx_env() { - use op_alloy_rpc_types::OpTransactionRequest; - use op_revm::{transaction::OpTxTr, OpSpecId}; - use revm_context::Transaction; - - let s = r#"{"from":"0x0000000000000000000000000000000000000000","to":"0x6d362b9c3ab68c0b7c79e8a714f1d7f3af63655f","input":"0x1626ba7ec8ee0d506e864589b799a645ddb88b08f5d39e8049f9f702b3b61fa15e55fc73000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000550000002d6db27c52e3c11c1cf24072004ac75cba49b25bf45f513902e469755e1f3bf2ca8324ad16930b0a965c012a24bb1101f876ebebac047bd3b6bf610205a27171eaaeffe4b5e5589936f4e542d637b627311b0000000000000000000000","data":"0x1626ba7ec8ee0d506e864589b799a645ddb88b08f5d39e8049f9f702b3b61fa15e55fc73000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000550000002d6db27c52e3c11c1cf24072004ac75cba49b25bf45f513902e469755e1f3bf2ca8324ad16930b0a965c012a24bb1101f876ebebac047bd3b6bf610205a27171eaaeffe4b5e5589936f4e542d637b627311b0000000000000000000000","chainId":"0x7a69"}"#; - - let req: OpTransactionRequest = serde_json::from_str(s).unwrap(); - - let cfg = CfgEnv::::default(); - let block_env = BlockEnv::default(); - let tx_env = req.try_into_tx_env(&cfg, &block_env).unwrap(); - assert_eq!(tx_env.gas_limit(), block_env.gas_limit); - assert_eq!(tx_env.gas_price(), 0); - assert!(tx_env.enveloped_tx().unwrap().is_empty()); - } } } diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 750a969a41..7fa99849ee 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -711,9 +711,9 @@ where hashes: Vec, ) -> EngineApiResult { let start = Instant::now(); - let res = Self::get_payload_bodies_by_hash_v1(self, hashes); + let res = Self::get_payload_bodies_by_hash_v1(self, hashes).await; self.inner.metrics.latency.get_payload_bodies_by_hash_v1.record(start.elapsed()); - res.await + res } /// Validates the `engine_forkchoiceUpdated` payload attributes and executes the forkchoice diff --git a/crates/rpc/rpc-eth-api/src/core.rs b/crates/rpc/rpc-eth-api/src/core.rs index 4e0afbf6ab..9d8af4b803 100644 --- a/crates/rpc/rpc-eth-api/src/core.rs +++ b/crates/rpc/rpc-eth-api/src/core.rs @@ -18,7 +18,7 @@ use alloy_serde::JsonStorageKey; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use reth_primitives_traits::TxTy; use reth_rpc_convert::RpcTxReq; -use reth_rpc_eth_types::FillTransactionResult; +use reth_rpc_eth_types::FillTransaction; use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult}; use tracing::trace; @@ -242,7 +242,7 @@ pub trait EthApi< /// Fills the defaults on a given unsigned transaction. #[method(name = "fillTransaction")] - async fn fill_transaction(&self, request: TxReq) -> RpcResult>; + async fn fill_transaction(&self, request: TxReq) -> RpcResult>; /// Simulate arbitrary number of transactions at an arbitrary blockchain index, with the /// optionality of state overrides @@ -703,7 +703,7 @@ where async fn fill_transaction( &self, request: RpcTxReq, - ) -> RpcResult>> { + ) -> RpcResult>> { trace!(target: "rpc::eth", ?request, "Serving eth_fillTransaction"); Ok(EthTransactions::fill_transaction(self, request).await?) } diff --git a/crates/rpc/rpc-eth-api/src/helpers/call.rs b/crates/rpc/rpc-eth-api/src/helpers/call.rs index 14b145317b..0242b5939d 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/call.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/call.rs @@ -29,17 +29,14 @@ use reth_revm::{database::StateProviderDatabase, db::State}; use reth_rpc_convert::{RpcConvert, RpcTxReq}; use reth_rpc_eth_types::{ cache::db::{StateCacheDbRefMutWrapper, StateProviderTraitObjWrapper}, - error::{api::FromEvmHalt, ensure_success, FromEthApiError}, + error::FromEthApiError, simulate::{self, EthSimulateError}, - EthApiError, RevertError, StateCacheDb, + EthApiError, StateCacheDb, }; use reth_storage_api::{BlockIdReader, ProviderTx}; use revm::{ context::Block, - context_interface::{ - result::{ExecutionResult, ResultAndState}, - Transaction, - }, + context_interface::{result::ResultAndState, Transaction}, database::bal::BalDatabaseError, Database, DatabaseCommit, }; @@ -227,7 +224,7 @@ pub trait EthCall: EstimateCall + Call + LoadPendingBlock + LoadBlock + FullEthA let res = self.transact_call_at(request, block_number.unwrap_or_default(), overrides).await?; - ensure_success(res.result) + Self::Error::ensure_success(res.result) } } @@ -344,7 +341,7 @@ pub trait EthCall: EstimateCall + Call + LoadPendingBlock + LoadBlock + FullEthA }, )?; - match ensure_success::<_, Self::Error>(res.result) { + match Self::Error::ensure_success(res.result) { Ok(output) => { bundle_results .push(EthCallResponse { value: Some(output), error: None }); @@ -447,46 +444,22 @@ pub trait EthCall: EstimateCall + Call + LoadPendingBlock + LoadBlock + FullEthA let result = this.inspect(&mut db, evm_env.clone(), tx_env.clone(), &mut inspector)?; let access_list = inspector.into_access_list(); + let gas_used = result.result.gas_used(); tx_env.set_access_list(access_list.clone()); - match result.result { - ExecutionResult::Halt { reason, gas_used } => { - let error = - Some(Self::Error::from_evm_halt(reason, tx_env.gas_limit()).to_string()); - return Ok(AccessListResult { - access_list, - gas_used: U256::from(gas_used), - error, - }) - } - ExecutionResult::Revert { output, gas_used } => { - let error = Some(RevertError::new(output).to_string()); - return Ok(AccessListResult { - access_list, - gas_used: U256::from(gas_used), - error, - }) - } - ExecutionResult::Success { .. } => {} - }; + if let Err(err) = Self::Error::ensure_success(result.result) { + return Ok(AccessListResult { + access_list, + gas_used: U256::from(gas_used), + error: Some(err.to_string()), + }); + } // transact again to get the exact gas used - let gas_limit = tx_env.gas_limit(); let result = this.transact(&mut db, evm_env, tx_env)?; - let res = match result.result { - ExecutionResult::Halt { reason, gas_used } => { - let error = Some(Self::Error::from_evm_halt(reason, gas_limit).to_string()); - AccessListResult { access_list, gas_used: U256::from(gas_used), error } - } - ExecutionResult::Revert { output, gas_used } => { - let error = Some(RevertError::new(output).to_string()); - AccessListResult { access_list, gas_used: U256::from(gas_used), error } - } - ExecutionResult::Success { gas_used, .. } => { - AccessListResult { access_list, gas_used: U256::from(gas_used), error: None } - } - }; + let gas_used = result.result.gas_used(); + let error = Self::Error::ensure_success(result.result).err().map(|e| e.to_string()); - Ok(res) + Ok(AccessListResult { access_list, gas_used: U256::from(gas_used), error }) }) } } diff --git a/crates/rpc/rpc-eth-api/src/helpers/estimate.rs b/crates/rpc/rpc-eth-api/src/helpers/estimate.rs index ea6173f4c9..fd61458fd7 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/estimate.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/estimate.rs @@ -2,6 +2,7 @@ use super::{Call, LoadPendingBlock}; use crate::{AsEthApiError, FromEthApiError, IntoEthApiError}; +use alloy_consensus::constants::KECCAK_EMPTY; use alloy_evm::overrides::apply_state_overrides; use alloy_network::TransactionBuilder; use alloy_primitives::{TxKind, U256}; @@ -10,14 +11,19 @@ use futures::Future; use reth_chainspec::MIN_TRANSACTION_GAS; use reth_errors::ProviderError; use reth_evm::{ConfigureEvm, Database, Evm, EvmEnvFor, EvmFor, TransactionEnv, TxEnvFor}; -use reth_revm::{database::StateProviderDatabase, db::State}; +use reth_revm::{ + database::{EvmStateProvider, StateProviderDatabase}, + db::State, +}; use reth_rpc_convert::{RpcConvert, RpcTxReq}; use reth_rpc_eth_types::{ - error::{api::FromEvmHalt, FromEvmError}, - EthApiError, RevertError, RpcInvalidTransactionError, + error::{ + api::{FromEvmHalt, FromRevert}, + FromEvmError, + }, + EthApiError, RpcInvalidTransactionError, }; use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO}; -use reth_storage_api::StateProvider; use revm::{ context::Block, context_interface::{result::ExecutionResult, Transaction}, @@ -46,7 +52,7 @@ pub trait EstimateCall: Call { state_override: Option, ) -> Result where - S: StateProvider, + S: EvmStateProvider, { // Disabled because eth_estimateGas is sometimes used with eoa senders // See @@ -98,10 +104,14 @@ pub trait EstimateCall: Call { // Check if this is a basic transfer (no input data to account with no code) let is_basic_transfer = if tx_env.input().is_empty() && - let TxKind::Call(to) = tx_env.kind() && - let Ok(code) = db.database.account_code(&to) + let TxKind::Call(to) = tx_env.kind() { - code.map(|code| code.is_empty()).unwrap_or(true) + match db.database.basic_account(&to) { + Ok(Some(account)) => { + account.bytecode_hash.is_none() || account.bytecode_hash == Some(KECCAK_EMPTY) + } + _ => true, + } } else { false }; @@ -181,7 +191,7 @@ pub trait EstimateCall: Call { Self::map_out_of_gas_err(&mut evm, tx_env, max_gas_limit) } else { // the transaction did revert - Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into_eth_err()) + Err(Self::Error::from_revert(output)) } } }; @@ -325,7 +335,7 @@ pub trait EstimateCall: Call { } ExecutionResult::Revert { output, .. } => { // reverted again after bumping the limit - Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into_eth_err()) + Err(Self::Error::from_revert(output)) } ExecutionResult::Halt { reason, .. } => { Err(Self::Error::from_evm_halt(reason, req_gas_limit)) diff --git a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs index 604dccd70a..03ad730220 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs @@ -376,8 +376,8 @@ pub trait LoadPendingBlock: Ok(ExecutedBlock { recovered_block: block.into(), execution_output: Arc::new(execution_outcome), - hashed_state: Arc::new(hashed_state), - trie_updates: Arc::new(trie_updates), + hashed_state: Arc::new(hashed_state.into_sorted()), + trie_updates: Arc::new(trie_updates.into_sorted()), }) } } @@ -420,8 +420,28 @@ impl BuildPendingEnv for NextBlockEnvAttributes { suggested_fee_recipient: parent.beneficiary(), prev_randao: B256::random(), gas_limit: parent.gas_limit(), - parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO), + parent_beacon_block_root: parent.parent_beacon_block_root(), withdrawals: parent.withdrawals_root().map(|_| Default::default()), } } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Header; + use alloy_primitives::B256; + use reth_primitives_traits::SealedHeader; + + #[test] + fn pending_env_keeps_parent_beacon_root() { + let mut header = Header::default(); + let beacon_root = B256::repeat_byte(0x42); + header.parent_beacon_block_root = Some(beacon_root); + let sealed = SealedHeader::new(header, B256::ZERO); + + let attrs = NextBlockEnvAttributes::build_pending_env(&sealed); + + assert_eq!(attrs.parent_beacon_block_root, Some(beacon_root)); + } +} diff --git a/crates/rpc/rpc-eth-api/src/helpers/transaction.rs b/crates/rpc/rpc-eth-api/src/helpers/transaction.rs index 2b1f3d0533..2f6c3674ed 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/transaction.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/transaction.rs @@ -24,7 +24,7 @@ use reth_rpc_convert::{transaction::RpcConvert, RpcTxReq}; use reth_rpc_eth_types::{ utils::{binary_search, recover_raw_transaction}, EthApiError::{self, TransactionConfirmationTimeout}, - FillTransactionResult, SignError, TransactionSource, + FillTransaction, SignError, TransactionSource, }; use reth_storage_api::{ BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider, @@ -450,7 +450,7 @@ pub trait EthTransactions: LoadTransaction { fn fill_transaction( &self, mut request: RpcTxReq, - ) -> impl Future>, Self::Error>> + Send + ) -> impl Future>, Self::Error>> + Send where Self: EthApiSpec + LoadBlock + EstimateCall + LoadFee, { @@ -511,7 +511,7 @@ pub trait EthTransactions: LoadTransaction { let raw = tx.encoded_2718().into(); - Ok(FillTransactionResult { raw, tx }) + Ok(FillTransaction { raw, tx }) } } diff --git a/crates/rpc/rpc-eth-types/src/error/api.rs b/crates/rpc/rpc-eth-types/src/error/api.rs index 77dcccb3bd..1942d22ee3 100644 --- a/crates/rpc/rpc-eth-types/src/error/api.rs +++ b/crates/rpc/rpc-eth-types/src/error/api.rs @@ -1,11 +1,12 @@ //! Helper traits to wrap generic l1 errors, in network specific error type configured in //! `reth_rpc_eth_api::EthApiTypes`. -use crate::EthApiError; +use crate::{EthApiError, RevertError}; +use alloy_primitives::Bytes; use reth_errors::ProviderError; use reth_evm::{ConfigureEvm, EvmErrorFor, HaltReasonFor}; use reth_revm::db::bal::BalDatabaseError; -use revm::context_interface::result::HaltReason; +use revm::{context::result::ExecutionResult, context_interface::result::HaltReason}; use super::RpcInvalidTransactionError; @@ -84,17 +85,32 @@ impl AsEthApiError for EthApiError { /// Helper trait to convert from revm errors. pub trait FromEvmError: - From>> + FromEvmHalt> + From>> + + FromEvmHalt> + + FromRevert { /// Converts from EVM error to this type. fn from_evm_err(err: EvmErrorFor>) -> Self { err.into() } + + /// Ensures the execution result is successful or returns an error, + fn ensure_success(result: ExecutionResult>) -> Result { + match result { + ExecutionResult::Success { output, .. } => Ok(output.into_data()), + ExecutionResult::Revert { output, .. } => Err(Self::from_revert(output)), + ExecutionResult::Halt { reason, gas_used } => { + Err(Self::from_evm_halt(reason, gas_used)) + } + } + } } impl FromEvmError for T where - T: From>> + FromEvmHalt>, + T: From>> + + FromEvmHalt> + + FromRevert, Evm: ConfigureEvm, { } @@ -110,3 +126,17 @@ impl FromEvmHalt for EthApiError { RpcInvalidTransactionError::halt(halt, gas_limit).into() } } + +/// Helper trait to construct errors from unexpected reverts. +pub trait FromRevert { + /// Constructs an error from revert bytes. + /// + /// This is only invoked when revert was unexpected (`eth_call`, `eth_estimateGas`, etc). + fn from_revert(output: Bytes) -> Self; +} + +impl FromRevert for EthApiError { + fn from_revert(output: Bytes) -> Self { + RpcInvalidTransactionError::Revert(RevertError::new(output)).into() + } +} diff --git a/crates/rpc/rpc-eth-types/src/error/mod.rs b/crates/rpc/rpc-eth-types/src/error/mod.rs index acde78e1cf..317bfbc0fa 100644 --- a/crates/rpc/rpc-eth-types/src/error/mod.rs +++ b/crates/rpc/rpc-eth-types/src/error/mod.rs @@ -1,7 +1,6 @@ //! Implementation specific Errors for the `eth_` namespace. pub mod api; -use crate::error::api::FromEvmHalt; use alloy_eips::BlockId; use alloy_evm::{call::CallError, overrides::StateOverrideError}; use alloy_primitives::{Address, Bytes, B256, U256}; @@ -23,7 +22,7 @@ use reth_transaction_pool::error::{ }; use revm::{ context_interface::result::{ - EVMError, ExecutionResult, HaltReason, InvalidHeader, InvalidTransaction, OutOfGasError, + EVMError, HaltReason, InvalidHeader, InvalidTransaction, OutOfGasError, }, state::bal::BalError, }; @@ -1078,19 +1077,19 @@ pub enum SignError { NoChainId, } -/// Converts the evm [`ExecutionResult`] into a result where `Ok` variant is the output bytes if it -/// is [`ExecutionResult::Success`]. -pub fn ensure_success + FromEthApiError>( - result: ExecutionResult, -) -> Result { - match result { - ExecutionResult::Success { output, .. } => Ok(output.into_data()), - ExecutionResult::Revert { output, .. } => { - Err(Error::from_eth_err(RpcInvalidTransactionError::Revert(RevertError::new(output)))) - } - ExecutionResult::Halt { reason, gas_used } => Err(Error::from_evm_halt(reason, gas_used)), - } -} +// /// Converts the evm [`ExecutionResult`] into a result where `Ok` variant is the output bytes if +// it /// is [`ExecutionResult::Success`]. +// pub fn ensure_success + FromEthApiError>( +// result: ExecutionResult, +// ) -> Result { +// match result { +// ExecutionResult::Success { output, .. } => Ok(output.into_data()), +// ExecutionResult::Revert { output, .. } => { +// +// Err(Error::from_eth_err(RpcInvalidTransactionError::Revert(RevertError::new(output)))) } +// ExecutionResult::Halt { reason, gas_used } => Err(Error::from_evm_halt(reason, +// gas_used)), } +// } impl From> for EthApiError where diff --git a/crates/rpc/rpc-eth-types/src/lib.rs b/crates/rpc/rpc-eth-types/src/lib.rs index 7378ad9962..8d829aebf4 100644 --- a/crates/rpc/rpc-eth-types/src/lib.rs +++ b/crates/rpc/rpc-eth-types/src/lib.rs @@ -23,6 +23,7 @@ pub mod transaction; pub mod tx_forward; pub mod utils; +pub use alloy_rpc_types_eth::FillTransaction; pub use builder::config::{EthConfig, EthFilterConfig}; pub use cache::{ config::EthStateCacheConfig, db::StateCacheDb, multi_consumer::MultiConsumerLruCache, @@ -35,5 +36,5 @@ pub use gas_oracle::{ }; pub use id_provider::EthSubscriptionIdProvider; pub use pending_block::{PendingBlock, PendingBlockEnv, PendingBlockEnvOrigin}; -pub use transaction::{FillTransactionResult, TransactionSource}; +pub use transaction::TransactionSource; pub use tx_forward::ForwardConfig; diff --git a/crates/rpc/rpc-eth-types/src/simulate.rs b/crates/rpc/rpc-eth-types/src/simulate.rs index ec63443da3..ce3c713af1 100644 --- a/crates/rpc/rpc-eth-types/src/simulate.rs +++ b/crates/rpc/rpc-eth-types/src/simulate.rs @@ -2,10 +2,10 @@ use crate::{ error::{ - api::{FromEthApiError, FromEvmHalt}, - ToRpcError, + api::{FromEthApiError, FromEvmHalt, FromRevert}, + FromEvmError, ToRpcError, }, - EthApiError, RevertError, + EthApiError, }; use alloy_consensus::{transaction::TxHashRef, BlockHeader, Transaction as _}; use alloy_eips::eip2718::WithEncoded; @@ -17,7 +17,7 @@ use alloy_rpc_types_eth::{ use jsonrpsee_types::ErrorObject; use reth_evm::{ execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor}, - Evm, + Evm, HaltReasonFor, }; use reth_primitives_traits::{BlockBody as _, BlockTy, NodePrimitives, Recovered, RecoveredBlock}; use reth_rpc_convert::{RpcBlock, RpcConvert, RpcTxReq}; @@ -186,14 +186,14 @@ where } /// Handles outputs of the calls execution and builds a [`SimulatedBlock`]. -pub fn build_simulated_block( +pub fn build_simulated_block( block: RecoveredBlock>, - results: Vec>, + results: Vec>>, txs_kind: BlockTransactionsKind, tx_resp_builder: &T, ) -> Result>, T::Error> where - T: RpcConvert>, + T: RpcConvert>, { let mut calls: Vec = Vec::with_capacity(results.len()); @@ -214,12 +214,12 @@ where } } ExecutionResult::Revert { output, gas_used } => { - let error = RevertError::new(output.clone()); + let error = T::Error::from_revert(output.clone()); SimCallResult { return_data: output, error: Some(SimulateError { - code: error.error_code(), message: error.to_string(), + code: error.into().code(), }), gas_used, status: false, diff --git a/crates/rpc/rpc-eth-types/src/transaction.rs b/crates/rpc/rpc-eth-types/src/transaction.rs index 3d099f0118..de3323d61e 100644 --- a/crates/rpc/rpc-eth-types/src/transaction.rs +++ b/crates/rpc/rpc-eth-types/src/transaction.rs @@ -2,21 +2,11 @@ //! //! Transaction wrapper that labels transaction with its origin. -use alloy_primitives::{Bytes, B256}; +use alloy_primitives::B256; use alloy_rpc_types_eth::TransactionInfo; use reth_ethereum_primitives::TransactionSigned; use reth_primitives_traits::{NodePrimitives, Recovered, SignedTransaction}; use reth_rpc_convert::{RpcConvert, RpcTransaction}; -use serde::{Deserialize, Serialize}; - -/// Response type for `eth_fillTransaction` RPC method. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FillTransactionResult { - /// RLP-encoded transaction bytes - pub raw: Bytes, - /// Filled transaction object - pub tx: T, -} /// Represents from where a transaction was fetched. #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/crates/rpc/rpc/src/debug.rs b/crates/rpc/rpc/src/debug.rs index 44167bc7a9..35c3f07a41 100644 --- a/crates/rpc/rpc/src/debug.rs +++ b/crates/rpc/rpc/src/debug.rs @@ -270,6 +270,10 @@ where /// The `debug_traceCall` method lets you run an `eth_call` within the context of the given /// block execution using the final state of parent block as the base. /// + /// If `tx_index` is provided in opts, the call will be traced at the state after executing + /// transactions up to the specified index within the block (0-indexed). + /// If not provided, then uses the post-state (default behavior). + /// /// Differences compare to `eth_call`: /// - `debug_traceCall` executes with __enabled__ basefee check, `eth_call` does not: pub async fn debug_trace_call( @@ -280,9 +284,20 @@ where ) -> Result { let at = block_id.unwrap_or_default(); let GethDebugTracingCallOptions { - tracing_options, state_overrides, block_overrides, .. + tracing_options, + state_overrides, + block_overrides, + tx_index, } = opts; let overrides = EvmOverrides::new(state_overrides, block_overrides.map(Box::new)); + + // Check if we need to replay transactions for a specific tx_index + if let Some(tx_idx) = tx_index { + return self + .debug_trace_call_at_tx_index(call, at, tx_idx as usize, tracing_options, overrides) + .await; + } + let GethDebugTracingOptions { config, tracer, tracer_config, .. } = tracing_options; let this = self.clone(); @@ -493,6 +508,74 @@ where Ok(frame.into()) } + /// Helper method to execute `debug_trace_call` at a specific transaction index within a block. + /// This replays transactions up to the specified index, then executes the trace call in that + /// state. + async fn debug_trace_call_at_tx_index( + &self, + call: RpcTxReq, + block_id: BlockId, + tx_index: usize, + tracing_options: GethDebugTracingOptions, + overrides: EvmOverrides, + ) -> Result { + // Get the target block to check transaction count + let block = self + .eth_api() + .recovered_block(block_id) + .await? + .ok_or(EthApiError::HeaderNotFound(block_id))?; + + if tx_index >= block.transaction_count() { + // tx_index out of bounds + return Err(EthApiError::InvalidParams(format!( + "tx_index {} out of bounds for block with {} transactions", + tx_index, + block.transaction_count() + )) + .into()) + } + + let (evm_env, _) = self.eth_api().evm_env_at(block.hash().into()).await?; + + // execute after the parent block, replaying `tx_index` transactions + let state_at = block.parent_hash().into(); + + let this = self.clone(); + self.eth_api() + .spawn_with_state_at_block(state_at, move |state| { + let mut db = + State::builder().with_database(StateProviderDatabase::new(state)).build(); + + // 1. apply pre-execution changes + this.eth_api().apply_pre_execution_changes(&block, &mut db, &evm_env)?; + + // 2. replay the required number of transactions + for tx in block.transactions_recovered().take(tx_index) { + let tx_env = this.eth_api().evm_config().tx_env(tx); + let res = this.eth_api().transact(&mut db, evm_env.clone(), tx_env)?; + db.commit(res.state); + } + + // 3. now execute the trace call on this state + let (call_evm_env, call_tx_env) = + this.eth_api().prepare_call_env(evm_env, call, &mut db, overrides)?; + + // Execute the trace call using trace_transaction + let (trace, _) = this.trace_transaction( + &tracing_options, + call_evm_env, + call_tx_env, + &mut db, + None, // No transaction context for call tracing + &mut None, + )?; + + Ok(trace) + }) + .await + } + /// The `debug_traceCallMany` method lets you run an `eth_callMany` within the context of the /// given block execution using the first n transactions in the given block as base. /// Each following bundle increments block number by 1 and block timestamp by 12 seconds diff --git a/crates/rpc/rpc/src/eth/filter.rs b/crates/rpc/rpc/src/eth/filter.rs index f0dac574c3..d538f919cf 100644 --- a/crates/rpc/rpc/src/eth/filter.rs +++ b/crates/rpc/rpc/src/eth/filter.rs @@ -1138,7 +1138,7 @@ impl< let expected_next = last_header.number() + 1; if peeked.number() != expected_next { - debug!( + trace!( target: "rpc::eth::filter", last_block = last_header.number(), next_block = peeked.number(), diff --git a/crates/stages/api/Cargo.toml b/crates/stages/api/Cargo.toml index c8eb81289d..6cdf45f790 100644 --- a/crates/stages/api/Cargo.toml +++ b/crates/stages/api/Cargo.toml @@ -43,6 +43,9 @@ auto_impl.workspace = true [dev-dependencies] assert_matches.workspace = true +reth-chainspec.workspace = true +reth-db = { workspace = true, features = ["test-utils"] } +reth-db-api.workspace = true reth-provider = { workspace = true, features = ["test-utils"] } tokio = { workspace = true, features = ["sync", "rt-multi-thread"] } tokio-stream.workspace = true @@ -50,9 +53,12 @@ reth-testing-utils.workspace = true [features] test-utils = [ + "reth-chainspec/test-utils", "reth-consensus/test-utils", + "reth-db-api/test-utils", + "reth-db/test-utils", "reth-network-p2p/test-utils", + "reth-primitives-traits/test-utils", "reth-provider/test-utils", "reth-stages-types/test-utils", - "reth-primitives-traits/test-utils", ] diff --git a/crates/stages/api/src/pipeline/mod.rs b/crates/stages/api/src/pipeline/mod.rs index e8542c36da..9b13badc76 100644 --- a/crates/stages/api/src/pipeline/mod.rs +++ b/crates/stages/api/src/pipeline/mod.rs @@ -315,7 +315,8 @@ impl Pipeline { // attempt to proceed with a finalized block which has been unwinded let _locked_sf_producer = self.static_file_producer.lock(); - let mut provider_rw = self.provider_factory.database_provider_rw()?; + let mut provider_rw = + self.provider_factory.database_provider_rw()?.disable_long_read_transaction_safety(); for stage in unwind_pipeline { let stage_id = stage.id(); diff --git a/crates/stages/api/src/stage.rs b/crates/stages/api/src/stage.rs index beb08f62a6..f9a5c1e8b3 100644 --- a/crates/stages/api/src/stage.rs +++ b/crates/stages/api/src/stage.rs @@ -1,12 +1,13 @@ use crate::{error::StageError, StageCheckpoint, StageId}; use alloy_primitives::{BlockNumber, TxNumber}; -use reth_provider::{BlockReader, ProviderError}; +use reth_provider::{BlockReader, ProviderError, StaticFileProviderFactory, StaticFileSegment}; use std::{ cmp::{max, min}, future::{poll_fn, Future}, ops::{Range, RangeInclusive}, task::{Context, Poll}, }; +use tracing::instrument; /// Stage execution input, see [`Stage::execute`]. #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] @@ -17,6 +18,26 @@ pub struct ExecInput { pub checkpoint: Option, } +/// Return type for [`ExecInput::next_block_range_with_threshold`]. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct BlockRangeOutput { + /// The block range to execute. + pub block_range: RangeInclusive, + /// Whether this is the final range to execute. + pub is_final_range: bool, +} + +/// Return type for [`ExecInput::next_block_range_with_transaction_threshold`]. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TransactionRangeOutput { + /// The transaction range to execute. + pub tx_range: Range, + /// The block range to execute. + pub block_range: RangeInclusive, + /// Whether this is the final range to execute. + pub is_final_range: bool, +} + impl ExecInput { /// Return the checkpoint of the stage or default. pub fn checkpoint(&self) -> StageCheckpoint { @@ -42,8 +63,7 @@ impl ExecInput { /// Return next block range that needs to be executed. pub fn next_block_range(&self) -> RangeInclusive { - let (range, _) = self.next_block_range_with_threshold(u64::MAX); - range + self.next_block_range_with_threshold(u64::MAX).block_range } /// Return true if this is the first block range to execute. @@ -52,11 +72,7 @@ impl ExecInput { } /// Return the next block range to execute. - /// Return pair of the block range and if this is final block range. - pub fn next_block_range_with_threshold( - &self, - threshold: u64, - ) -> (RangeInclusive, bool) { + pub fn next_block_range_with_threshold(&self, threshold: u64) -> BlockRangeOutput { let current_block = self.checkpoint(); let start = current_block.block_number + 1; let target = self.target(); @@ -64,23 +80,46 @@ impl ExecInput { let end = min(target, current_block.block_number.saturating_add(threshold)); let is_final_range = end == target; - (start..=end, is_final_range) + BlockRangeOutput { block_range: start..=end, is_final_range } } /// Return the next block range determined the number of transactions within it. /// This function walks the block indices until either the end of the range is reached or /// the number of transactions exceeds the threshold. + /// + /// Returns [`None`] if no transactions are found for the current execution input. + #[instrument(level = "debug", target = "sync::stages", skip(provider), ret)] pub fn next_block_range_with_transaction_threshold( &self, provider: &Provider, tx_threshold: u64, - ) -> Result<(Range, RangeInclusive, bool), StageError> + ) -> Result, StageError> where - Provider: BlockReader, + Provider: StaticFileProviderFactory + BlockReader, { - let start_block = self.next_block(); + // Get lowest available block number for transactions + let Some(lowest_transactions_block) = + provider.static_file_provider().get_lowest_range_start(StaticFileSegment::Transactions) + else { + return Ok(None) + }; + + // We can only process transactions that have associated static files, so we cap the start + // block by lowest available block number. + // + // Certain transactions may not have associated static files when user deletes them + // manually. In that case, we can't process them, and need to adjust the start block + // accordingly. + let start_block = self.next_block().max(lowest_transactions_block); let target_block = self.target(); + // If the start block is greater than the target, then there's no transactions to process + // and we return early. It's possible to trigger this scenario when running `reth + // stage run` manually for a range of transactions that doesn't exist. + if start_block > target_block { + return Ok(None) + } + let start_block_body = provider .block_body_indices(start_block)? .ok_or(ProviderError::BlockBodyIndicesNotFound(start_block))?; @@ -95,7 +134,7 @@ impl ExecInput { if all_tx_cnt == 0 { // if there is no more transaction return back. - return Ok((first_tx_num..first_tx_num, start_block..=target_block, true)) + return Ok(None) } // get block of this tx @@ -116,7 +155,11 @@ impl ExecInput { }; let tx_range = first_tx_num..next_tx_num; - Ok((tx_range, start_block..=end_block, is_final_range)) + Ok(Some(TransactionRangeOutput { + tx_range, + block_range: start_block..=end_block, + is_final_range, + })) } } @@ -277,3 +320,164 @@ pub trait StageExt: Stage { } impl + ?Sized> StageExt for S {} + +#[cfg(test)] +mod tests { + use reth_chainspec::MAINNET; + use reth_db::test_utils::{create_test_rw_db, create_test_static_files_dir}; + use reth_db_api::{models::StoredBlockBodyIndices, tables, transaction::DbTxMut}; + use reth_provider::{ + test_utils::MockNodeTypesWithDB, ProviderFactory, StaticFileProviderBuilder, + StaticFileProviderFactory, StaticFileSegment, + }; + use reth_stages_types::StageCheckpoint; + use reth_testing_utils::generators::{self, random_signed_tx}; + + use crate::ExecInput; + + #[test] + fn test_exec_input_next_block_range_with_transaction_threshold() { + let mut rng = generators::rng(); + let provider_factory = ProviderFactory::::new( + create_test_rw_db(), + MAINNET.clone(), + StaticFileProviderBuilder::read_write(create_test_static_files_dir().0.keep()) + .unwrap() + .with_blocks_per_file(1) + .build() + .unwrap(), + ) + .unwrap(); + + // Without checkpoint, without transactions in static files + { + let exec_input = ExecInput { target: Some(100), checkpoint: None }; + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap(); + assert!(range_output.is_none()); + } + + // With checkpoint at block 10, without transactions in static files + { + let exec_input = + ExecInput { target: Some(1), checkpoint: Some(StageCheckpoint::new(10)) }; + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap(); + assert!(range_output.is_none()); + } + + // Without checkpoint, with transactions in static files starting from block 1 + { + let exec_input = ExecInput { target: Some(1), checkpoint: None }; + + let mut provider_rw = provider_factory.provider_rw().unwrap(); + provider_rw + .tx_mut() + .put::( + 1, + StoredBlockBodyIndices { first_tx_num: 0, tx_count: 2 }, + ) + .unwrap(); + let mut writer = + provider_rw.get_static_file_writer(0, StaticFileSegment::Transactions).unwrap(); + writer.increment_block(0).unwrap(); + writer.increment_block(1).unwrap(); + writer.append_transaction(0, &random_signed_tx(&mut rng)).unwrap(); + writer.append_transaction(1, &random_signed_tx(&mut rng)).unwrap(); + drop(writer); + provider_rw.commit().unwrap(); + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap() + .unwrap(); + assert_eq!(range_output.tx_range, 0..2); + assert_eq!(range_output.block_range, 1..=1); + assert!(range_output.is_final_range); + } + + // With checkpoint at block 1, with transactions in static files starting from block 1 + { + let exec_input = + ExecInput { target: Some(2), checkpoint: Some(StageCheckpoint::new(1)) }; + + let mut provider_rw = provider_factory.provider_rw().unwrap(); + provider_rw + .tx_mut() + .put::( + 2, + StoredBlockBodyIndices { first_tx_num: 2, tx_count: 1 }, + ) + .unwrap(); + let mut writer = + provider_rw.get_static_file_writer(1, StaticFileSegment::Transactions).unwrap(); + writer.increment_block(2).unwrap(); + writer.append_transaction(2, &random_signed_tx(&mut rng)).unwrap(); + drop(writer); + provider_rw.commit().unwrap(); + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap() + .unwrap(); + assert_eq!(range_output.tx_range, 2..3); + assert_eq!(range_output.block_range, 2..=2); + assert!(range_output.is_final_range); + } + + // Without checkpoint, with transactions in static files starting from block 2 + { + let exec_input = ExecInput { target: Some(2), checkpoint: None }; + + provider_factory + .static_file_provider() + .delete_jar(StaticFileSegment::Transactions, 0) + .unwrap(); + provider_factory + .static_file_provider() + .delete_jar(StaticFileSegment::Transactions, 1) + .unwrap(); + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap() + .unwrap(); + assert_eq!(range_output.tx_range, 2..3); + assert_eq!(range_output.block_range, 2..=2); + assert!(range_output.is_final_range); + } + + // Without checkpoint, with transactions in static files starting from block 2 + { + let exec_input = + ExecInput { target: Some(3), checkpoint: Some(StageCheckpoint::new(2)) }; + + let mut provider_rw = provider_factory.provider_rw().unwrap(); + provider_rw + .tx_mut() + .put::( + 3, + StoredBlockBodyIndices { first_tx_num: 3, tx_count: 1 }, + ) + .unwrap(); + let mut writer = + provider_rw.get_static_file_writer(1, StaticFileSegment::Transactions).unwrap(); + writer.increment_block(3).unwrap(); + writer.append_transaction(3, &random_signed_tx(&mut rng)).unwrap(); + drop(writer); + provider_rw.commit().unwrap(); + + let range_output = exec_input + .next_block_range_with_transaction_threshold(&provider_factory, 10) + .unwrap() + .unwrap(); + assert_eq!(range_output.tx_range, 3..4); + assert_eq!(range_output.block_range, 3..=3); + assert!(range_output.is_final_range); + } + } +} diff --git a/crates/stages/stages/src/stages/execution.rs b/crates/stages/stages/src/stages/execution.rs index 1666e79baf..8dd76e56b9 100644 --- a/crates/stages/stages/src/stages/execution.rs +++ b/crates/stages/stages/src/stages/execution.rs @@ -11,9 +11,9 @@ use reth_exex::{ExExManagerHandle, ExExNotification, ExExNotificationSource}; use reth_primitives_traits::{format_gas_throughput, BlockBody, NodePrimitives}; use reth_provider::{ providers::{StaticFileProvider, StaticFileWriter}, - BlockHashReader, BlockReader, DBProvider, ExecutionOutcome, HeaderProvider, + BlockHashReader, BlockReader, DBProvider, EitherWriter, ExecutionOutcome, HeaderProvider, LatestStateProviderRef, OriginalValuesKnown, ProviderError, StateWriter, - StaticFileProviderFactory, StatsReader, TransactionVariant, + StaticFileProviderFactory, StatsReader, StorageSettingsCache, TransactionVariant, }; use reth_revm::database::StateProviderDatabase; use reth_stages_api::{ @@ -185,11 +185,15 @@ where unwind_to: Option, ) -> Result<(), StageError> where - Provider: StaticFileProviderFactory + DBProvider + BlockReader + HeaderProvider, + Provider: StaticFileProviderFactory + + DBProvider + + BlockReader + + HeaderProvider + + StorageSettingsCache, { - // If there's any receipts pruning configured, receipts are written directly to database and - // inconsistencies are expected. - if provider.prune_modes_ref().has_receipts_pruning() { + // On old nodes, if there's any receipts pruning configured, receipts are written directly + // to database and inconsistencies are expected. + if EitherWriter::receipts_destination(provider).is_database() { return Ok(()) } @@ -205,16 +209,23 @@ where .map(|num| num + 1) .unwrap_or(0); + // Get highest block number in static files for receipts + let static_file_block_num = static_file_provider + .get_highest_static_file_block(StaticFileSegment::Receipts) + .unwrap_or(0); + // Check if we had any unexpected shutdown after committing to static files, but // NOT committing to database. - match next_static_file_receipt_num.cmp(&next_receipt_num) { + match static_file_block_num.cmp(&checkpoint) { // It can be equal when it's a chain of empty blocks, but we still need to update the // last block in the range. Ordering::Greater | Ordering::Equal => { let mut static_file_producer = static_file_provider.latest_writer(StaticFileSegment::Receipts)?; - static_file_producer - .prune_receipts(next_static_file_receipt_num - next_receipt_num, checkpoint)?; + static_file_producer.prune_receipts( + next_static_file_receipt_num.saturating_sub(next_receipt_num), + checkpoint, + )?; // Since this is a database <-> static file inconsistency, we commit the change // straight away. static_file_producer.commit()?; @@ -222,19 +233,14 @@ where Ordering::Less => { // If we are already in the process of unwind, this might be fine because we will // fix the inconsistency right away. - if let Some(unwind_to) = unwind_to { - let next_receipt_num_after_unwind = provider - .block_body_indices(unwind_to)? - .map(|b| b.next_tx_num()) - .ok_or(ProviderError::BlockBodyIndicesNotFound(unwind_to))?; - - if next_receipt_num_after_unwind > next_static_file_receipt_num { - // This means we need a deeper unwind. - } else { - return Ok(()) - } + if let Some(unwind_to) = unwind_to && + unwind_to <= static_file_block_num + { + return Ok(()) } + // Otherwise, this is a real inconsistency - database has more blocks than static + // files return Err(missing_static_data_error( next_static_file_receipt_num.saturating_sub(1), &static_file_provider, @@ -259,7 +265,8 @@ where Primitives: NodePrimitives, > + StatsReader + BlockHashReader - + StateWriter::Receipt>, + + StateWriter::Receipt> + + StorageSettingsCache, { /// Return the id of the stage fn id(&self) -> StageId { @@ -665,7 +672,7 @@ mod tests { use assert_matches::assert_matches; use reth_chainspec::ChainSpecBuilder; use reth_db_api::{ - models::AccountBeforeTx, + models::{metadata::StorageSettings, AccountBeforeTx}, transaction::{DbTx, DbTxMut}, }; use reth_ethereum_consensus::EthBeaconConsensus; @@ -679,6 +686,7 @@ mod tests { use reth_prune::PruneModes; use reth_prune_types::{PruneMode, ReceiptsLogPruneConfig}; use reth_stages_api::StageUnitCheckpoint; + use reth_testing_utils::generators; use std::collections::BTreeMap; fn stage() -> ExecutionStage { @@ -1236,4 +1244,64 @@ mod tests { ] ); } + + #[test] + fn test_ensure_consistency_with_skipped_receipts() { + // Test that ensure_consistency allows the case where receipts are intentionally + // skipped. When receipts are skipped, blocks are still incremented in static files + // but no receipt data is written. + + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::new().with_receipts_in_static_files()); + + // Setup with block 1 + let provider_rw = factory.database_provider_rw().unwrap(); + let mut rng = generators::rng(); + let genesis = generators::random_block(&mut rng, 0, Default::default()); + provider_rw.insert_block(genesis.try_recover().unwrap()).expect("failed to insert genesis"); + let block = generators::random_block( + &mut rng, + 1, + generators::BlockParams { tx_count: Some(2), ..Default::default() }, + ); + provider_rw.insert_block(block.try_recover().unwrap()).expect("failed to insert block"); + + let static_file_provider = provider_rw.static_file_provider(); + static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap().commit().unwrap(); + + // Simulate skipped receipts: increment block in receipts static file but don't write + // receipts + { + let mut receipts_writer = + static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap(); + receipts_writer.increment_block(0).unwrap(); + receipts_writer.increment_block(1).unwrap(); + receipts_writer.commit().unwrap(); + } // Explicitly drop receipts_writer here + + provider_rw.commit().expect("failed to commit"); + + // Verify blocks are incremented but no receipts written + assert_eq!( + factory + .static_file_provider() + .get_highest_static_file_block(StaticFileSegment::Receipts), + Some(1) + ); + assert_eq!( + factory.static_file_provider().get_highest_static_file_tx(StaticFileSegment::Receipts), + None + ); + + // Create execution stage + let stage = stage(); + + // Run ensure_consistency - should NOT error + // Block numbers match (both at 1), but tx numbers don't (database has txs, static files + // don't) This is fine - receipts are being skipped + let provider = factory.provider().unwrap(); + stage + .ensure_consistency(&provider, 1, None) + .expect("ensure_consistency should succeed when receipts are intentionally skipped"); + } } diff --git a/crates/stages/stages/src/stages/mod.rs b/crates/stages/stages/src/stages/mod.rs index 58fa7cfb32..4434f153c8 100644 --- a/crates/stages/stages/src/stages/mod.rs +++ b/crates/stages/stages/src/stages/mod.rs @@ -303,7 +303,6 @@ mod tests { db: &TestStageDB, prune_count: usize, segment: StaticFileSegment, - is_full_node: bool, expected: Option, ) { // We recreate the static file provider, since consistency heals are done on fetching the @@ -330,7 +329,7 @@ mod tests { static_file_provider = StaticFileProvider::read_write(static_file_provider.path()).unwrap(); assert!(matches!( static_file_provider - .check_consistency(&db.factory.database_provider_ro().unwrap(), is_full_node,), + .check_consistency(&db.factory.database_provider_ro().unwrap()), Ok(e) if e == expected )); } @@ -352,7 +351,7 @@ mod tests { assert!(matches!( db.factory .static_file_provider() - .check_consistency(&db.factory.database_provider_ro().unwrap(), false,), + .check_consistency(&db.factory.database_provider_ro().unwrap(),), Ok(e) if e == expected )); } @@ -385,7 +384,7 @@ mod tests { assert!(matches!( db.factory .static_file_provider() - .check_consistency(&db.factory.database_provider_ro().unwrap(), false), + .check_consistency(&db.factory.database_provider_ro().unwrap()), Ok(e) if e == expected )); } @@ -396,36 +395,40 @@ mod tests { let db_provider = db.factory.database_provider_ro().unwrap(); assert!(matches!( - db.factory.static_file_provider().check_consistency(&db_provider, false), + db.factory.static_file_provider().check_consistency(&db_provider), Ok(None) )); } #[test] fn test_consistency_no_commit_prune() { - let db = seed_data(90).unwrap(); - let full_node = true; - let archive_node = !full_node; + // Test full node with receipt pruning + let mut db_full = seed_data(90).unwrap(); + db_full.factory = db_full.factory.with_prune_modes(PruneModes { + receipts: Some(PruneMode::Before(1)), + ..Default::default() + }); // Full node does not use receipts, therefore doesn't check for consistency on receipts // segment - simulate_behind_checkpoint_corruption(&db, 1, StaticFileSegment::Receipts, full_node, None); + simulate_behind_checkpoint_corruption(&db_full, 1, StaticFileSegment::Receipts, None); + + // Test archive node without receipt pruning + let db_archive = seed_data(90).unwrap(); // there are 2 to 3 transactions per block. however, if we lose one tx, we need to unwind to // the previous block. simulate_behind_checkpoint_corruption( - &db, + &db_archive, 1, StaticFileSegment::Receipts, - archive_node, Some(PipelineTarget::Unwind(88)), ); simulate_behind_checkpoint_corruption( - &db, + &db_archive, 3, StaticFileSegment::Headers, - archive_node, Some(PipelineTarget::Unwind(86)), ); } diff --git a/crates/stages/stages/src/stages/prune.rs b/crates/stages/stages/src/stages/prune.rs index f6fb7f90ae..1180407555 100644 --- a/crates/stages/stages/src/stages/prune.rs +++ b/crates/stages/stages/src/stages/prune.rs @@ -2,7 +2,7 @@ use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ BlockReader, ChainStateBlockReader, DBProvider, PruneCheckpointReader, PruneCheckpointWriter, - StaticFileProviderFactory, + StaticFileProviderFactory, StorageSettingsCache, }; use reth_prune::{ PruneMode, PruneModes, PruneSegment, PrunerBuilder, SegmentOutput, SegmentOutputCheckpoint, @@ -45,7 +45,7 @@ where + ChainStateBlockReader + StaticFileProviderFactory< Primitives: NodePrimitives, - >, + > + StorageSettingsCache, { fn id(&self) -> StageId { StageId::Prune @@ -146,7 +146,7 @@ where + ChainStateBlockReader + StaticFileProviderFactory< Primitives: NodePrimitives, - >, + > + StorageSettingsCache, { fn id(&self) -> StageId { StageId::PruneSenderRecovery diff --git a/crates/stages/stages/src/stages/sender_recovery.rs b/crates/stages/stages/src/stages/sender_recovery.rs index 947f062095..a7cd8128f3 100644 --- a/crates/stages/stages/src/stages/sender_recovery.rs +++ b/crates/stages/stages/src/stages/sender_recovery.rs @@ -80,30 +80,29 @@ where return Ok(ExecOutput::done(input.checkpoint())) } - let (tx_range, block_range, is_final_range) = - input.next_block_range_with_transaction_threshold(provider, self.commit_threshold)?; - let end_block = *block_range.end(); - - // No transactions to walk over - if tx_range.is_empty() { - info!(target: "sync::stages::sender_recovery", ?tx_range, "Target transaction already reached"); + let Some(range_output) = + input.next_block_range_with_transaction_threshold(provider, self.commit_threshold)? + else { + info!(target: "sync::stages::sender_recovery", "No transaction senders to recover"); return Ok(ExecOutput { - checkpoint: StageCheckpoint::new(end_block) + checkpoint: StageCheckpoint::new(input.target()) .with_entities_stage_checkpoint(stage_checkpoint(provider)?), - done: is_final_range, + done: true, }) - } + }; + let end_block = *range_output.block_range.end(); // Acquire the cursor for inserting elements let mut senders_cursor = provider.tx_ref().cursor_write::()?; - info!(target: "sync::stages::sender_recovery", ?tx_range, "Recovering senders"); + info!(target: "sync::stages::sender_recovery", tx_range = ?range_output.tx_range, "Recovering senders"); // Iterate over transactions in batches, recover the senders and append them - let batch = tx_range + let batch = range_output + .tx_range .clone() .step_by(BATCH_SIZE) - .map(|start| start..std::cmp::min(start + BATCH_SIZE as u64, tx_range.end)) + .map(|start| start..std::cmp::min(start + BATCH_SIZE as u64, range_output.tx_range.end)) .collect::>>(); let tx_batch_sender = setup_range_recovery(provider); @@ -115,7 +114,7 @@ where Ok(ExecOutput { checkpoint: StageCheckpoint::new(end_block) .with_entities_stage_checkpoint(stage_checkpoint(provider)?), - done: is_final_range, + done: range_output.is_final_range, }) } diff --git a/crates/stages/stages/src/stages/tx_lookup.rs b/crates/stages/stages/src/stages/tx_lookup.rs index 8b1c531736..daf63828b0 100644 --- a/crates/stages/stages/src/stages/tx_lookup.rs +++ b/crates/stages/stages/src/stages/tx_lookup.rs @@ -126,14 +126,21 @@ where ); loop { - let (tx_range, block_range, is_final_range) = - input.next_block_range_with_transaction_threshold(provider, self.chunk_size)?; + let Some(range_output) = + input.next_block_range_with_transaction_threshold(provider, self.chunk_size)? + else { + input.checkpoint = Some( + StageCheckpoint::new(input.target()) + .with_entities_stage_checkpoint(stage_checkpoint(provider)?), + ); + break; + }; - let end_block = *block_range.end(); + let end_block = *range_output.block_range.end(); - info!(target: "sync::stages::transaction_lookup", ?tx_range, "Calculating transaction hashes"); + info!(target: "sync::stages::transaction_lookup", tx_range = ?range_output.tx_range, "Calculating transaction hashes"); - for (key, value) in provider.transaction_hashes_by_range(tx_range)? { + for (key, value) in provider.transaction_hashes_by_range(range_output.tx_range)? { hash_collector.insert(key, value)?; } @@ -142,7 +149,7 @@ where .with_entities_stage_checkpoint(stage_checkpoint(provider)?), ); - if is_final_range { + if range_output.is_final_range { let append_only = provider.count_entries::()?.is_zero(); let mut txhash_cursor = provider diff --git a/crates/stages/types/src/checkpoints.rs b/crates/stages/types/src/checkpoints.rs index 04f4123c9f..c21412ea43 100644 --- a/crates/stages/types/src/checkpoints.rs +++ b/crates/stages/types/src/checkpoints.rs @@ -328,7 +328,9 @@ impl EntitiesCheckpoint { // Truncate to 2 decimal places, rounding down so that 99.999% becomes 99.99% and not 100%. #[cfg(not(feature = "std"))] { - Some(format!("{:.2}%", (percentage * 100.0) / 100.0)) + // Manual floor implementation using integer arithmetic for no_std + let scaled = (percentage * 100.0) as u64; + Some(format!("{:.2}%", scaled as f64 / 100.0)) } #[cfg(feature = "std")] Some(format!("{:.2}%", (percentage * 100.0).floor() / 100.0)) diff --git a/crates/stages/types/src/execution.rs b/crates/stages/types/src/execution.rs index a334951abe..caf7c2448e 100644 --- a/crates/stages/types/src/execution.rs +++ b/crates/stages/types/src/execution.rs @@ -2,11 +2,8 @@ use core::time::Duration; /// The thresholds at which the execution stage writes state changes to the database. /// -/// If either of the thresholds (`max_blocks` and `max_changes`) are hit, then the execution stage -/// commits all pending changes to the database. -/// -/// A third threshold, `max_changesets`, can be set to periodically write changesets to the -/// current database transaction, which frees up memory. +/// If any of the thresholds (`max_blocks`, `max_changes`, `max_cumulative_gas`, or `max_duration`) +/// are hit, then the execution stage commits all pending changes to the database. #[derive(Debug, Clone)] pub struct ExecutionStageThresholds { /// The maximum number of blocks to execute before the execution stage commits. diff --git a/crates/stateless/src/lib.rs b/crates/stateless/src/lib.rs index 6813638485..2ce3615d57 100644 --- a/crates/stateless/src/lib.rs +++ b/crates/stateless/src/lib.rs @@ -44,6 +44,8 @@ pub use recover_block::UncompressedPublicKey; #[doc(inline)] pub use trie::StatelessTrie; #[doc(inline)] +pub use validation::stateless_validation; +#[doc(inline)] pub use validation::stateless_validation_with_trie; /// Implementation of stateless validation diff --git a/crates/stateless/src/validation.rs b/crates/stateless/src/validation.rs index ad8aff085b..b44f6f60a5 100644 --- a/crates/stateless/src/validation.rs +++ b/crates/stateless/src/validation.rs @@ -52,7 +52,7 @@ pub enum StatelessValidationError { }, /// Error during stateless block execution. - #[error("stateless block execution failed")] + #[error("stateless block execution failed: {0}")] StatelessExecutionFailed(String), /// Error during consensus validation of the block. diff --git a/crates/stateless/src/witness_db.rs b/crates/stateless/src/witness_db.rs index 40acf2289d..fc6f8ca37c 100644 --- a/crates/stateless/src/witness_db.rs +++ b/crates/stateless/src/witness_db.rs @@ -23,18 +23,12 @@ where { /// Map of block numbers to block hashes. /// This is used to service the `BLOCKHASH` opcode. - // TODO: use Vec instead -- ancestors should be contiguous - // TODO: so we can use the current_block_number and an offset to - // TODO: get the block number of a particular ancestor block_hashes_by_block_number: BTreeMap, /// Map of code hashes to bytecode. /// Used to fetch contract code needed during execution. bytecode: B256Map, /// The sparse Merkle Patricia Trie containing account and storage state. /// This is used to provide account/storage values during EVM execution. - /// TODO: Ideally we do not have this trie and instead a simple map. - /// TODO: Then as a corollary we can avoid unnecessary hashing in `Database::storage` - /// TODO: and `Database::basic` without needing to cache the hashed Addresses and Keys trie: &'a T, } diff --git a/crates/static-file/types/src/segment.rs b/crates/static-file/types/src/segment.rs index 3bca841042..a7e8e09ac8 100644 --- a/crates/static-file/types/src/segment.rs +++ b/crates/static-file/types/src/segment.rs @@ -1,8 +1,5 @@ use crate::{BlockNumber, Compression}; -use alloc::{ - format, - string::{String, ToString}, -}; +use alloc::{format, string::String}; use alloy_primitives::TxNumber; use core::{ops::RangeInclusive, str::FromStr}; use derive_more::Display; @@ -86,7 +83,7 @@ impl StaticFileSegment { ) -> String { let prefix = self.filename(block_range); - let filters_name = "none".to_string(); + let filters_name = "none"; // ATTENTION: if changing the name format, be sure to reflect those changes in // [`Self::parse_filename`.] @@ -127,12 +124,18 @@ impl StaticFileSegment { /// Returns `true` if a segment row is linked to a transaction. pub const fn is_tx_based(&self) -> bool { - matches!(self, Self::Receipts | Self::Transactions) + match self { + Self::Receipts | Self::Transactions => true, + Self::Headers => false, + } } /// Returns `true` if a segment row is linked to a block. pub const fn is_block_based(&self) -> bool { - matches!(self, Self::Headers) + match self { + Self::Headers => true, + Self::Receipts | Self::Transactions => false, + } } } diff --git a/crates/storage/codecs/src/alloy/mod.rs b/crates/storage/codecs/src/alloy/mod.rs index 485dfbaa58..0f747c141e 100644 --- a/crates/storage/codecs/src/alloy/mod.rs +++ b/crates/storage/codecs/src/alloy/mod.rs @@ -25,6 +25,9 @@ cond_mod!( block_access_list ); +#[cfg(all(feature = "op", feature = "std"))] +pub mod optimism; + pub mod transaction; #[cfg(test)] diff --git a/crates/storage/codecs/src/alloy/optimism.rs b/crates/storage/codecs/src/alloy/optimism.rs new file mode 100644 index 0000000000..7a851a5041 --- /dev/null +++ b/crates/storage/codecs/src/alloy/optimism.rs @@ -0,0 +1,97 @@ +//! Compact implementations for Optimism types. + +use crate::Compact; +use alloc::{borrow::Cow, vec::Vec}; +use alloy_consensus::{Receipt, TxReceipt}; +use alloy_primitives::Log; +use op_alloy_consensus::{OpDepositReceipt, OpReceipt, OpTxType}; +use reth_codecs_derive::CompactZstd; + +#[derive(CompactZstd)] +#[reth_codecs(crate = "crate")] +#[reth_zstd( + compressor = reth_zstd_compressors::RECEIPT_COMPRESSOR, + decompressor = reth_zstd_compressors::RECEIPT_DECOMPRESSOR +)] +struct CompactOpReceipt<'a> { + tx_type: OpTxType, + success: bool, + cumulative_gas_used: u64, + #[expect(clippy::owned_cow)] + logs: Cow<'a, Vec>, + deposit_nonce: Option, + deposit_receipt_version: Option, +} + +impl<'a> From<&'a OpReceipt> for CompactOpReceipt<'a> { + fn from(receipt: &'a OpReceipt) -> Self { + Self { + tx_type: receipt.tx_type(), + success: receipt.status(), + cumulative_gas_used: receipt.cumulative_gas_used(), + logs: Cow::Borrowed(&receipt.as_receipt().logs), + deposit_nonce: if let OpReceipt::Deposit(receipt) = receipt { + receipt.deposit_nonce + } else { + None + }, + deposit_receipt_version: if let OpReceipt::Deposit(receipt) = receipt { + receipt.deposit_receipt_version + } else { + None + }, + } + } +} + +impl From> for OpReceipt { + fn from(receipt: CompactOpReceipt<'_>) -> Self { + let CompactOpReceipt { + tx_type, + success, + cumulative_gas_used, + logs, + deposit_nonce, + deposit_receipt_version, + } = receipt; + + let inner = + Receipt { status: success.into(), cumulative_gas_used, logs: logs.into_owned() }; + + match tx_type { + OpTxType::Legacy => Self::Legacy(inner), + OpTxType::Eip2930 => Self::Eip2930(inner), + OpTxType::Eip1559 => Self::Eip1559(inner), + OpTxType::Eip7702 => Self::Eip7702(inner), + OpTxType::Deposit => { + Self::Deposit(OpDepositReceipt { inner, deposit_nonce, deposit_receipt_version }) + } + } + } +} + +impl Compact for OpReceipt { + fn to_compact(&self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + CompactOpReceipt::from(self).to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (receipt, buf) = CompactOpReceipt::from_compact(buf, len); + (receipt.into(), buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::UnusedBits, validate_bitflag_backwards_compat}; + + #[test] + fn test_ensure_backwards_compatibility() { + assert_eq!(CompactOpReceipt::bitflag_encoded_bytes(), 2); + validate_bitflag_backwards_compat!(CompactOpReceipt<'_>, UnusedBits::NotZero); + } +} diff --git a/crates/storage/db-api/Cargo.toml b/crates/storage/db-api/Cargo.toml index bd77b9d63d..e91c6c12e7 100644 --- a/crates/storage/db-api/Cargo.toml +++ b/crates/storage/db-api/Cargo.toml @@ -50,7 +50,9 @@ proptest = { workspace = true, optional = true } [dev-dependencies] # reth libs with arbitrary reth-codecs = { workspace = true, features = ["test-utils"] } +reth-db-models = { workspace = true, features = ["arbitrary"] } +alloy-primitives = { workspace = true, features = ["rand"] } rand.workspace = true test-fuzz.workspace = true diff --git a/crates/storage/db-api/src/database.rs b/crates/storage/db-api/src/database.rs index df7c3a5678..1f8e3e125a 100644 --- a/crates/storage/db-api/src/database.rs +++ b/crates/storage/db-api/src/database.rs @@ -26,11 +26,11 @@ pub trait Database: Send + Sync + Debug { /// end of the execution. fn view(&self, f: F) -> Result where - F: FnOnce(&Self::TX) -> T, + F: FnOnce(&mut Self::TX) -> T, { - let tx = self.tx()?; + let mut tx = self.tx()?; - let res = f(&tx); + let res = f(&mut tx); tx.commit()?; Ok(res) diff --git a/crates/storage/db-common/src/init.rs b/crates/storage/db-common/src/init.rs index d05abb9ea9..f2790dfe23 100644 --- a/crates/storage/db-common/src/init.rs +++ b/crates/storage/db-common/src/init.rs @@ -85,6 +85,32 @@ impl From for InitStorageError { /// Write the genesis block if it has not already been written pub fn init_genesis(factory: &PF) -> Result +where + PF: DatabaseProviderFactory + + StaticFileProviderFactory> + + ChainSpecProvider + + StageCheckpointReader + + BlockHashReader + + StorageSettingsCache, + PF::ProviderRW: StaticFileProviderFactory + + StageCheckpointWriter + + HistoryWriter + + HeaderProvider + + HashingWriter + + StateWriter + + TrieWriter + + MetadataWriter + + AsRef, + PF::ChainSpec: EthChainSpec

::BlockHeader>, +{ + init_genesis_with_settings(factory, StorageSettings::legacy()) +} + +/// Write the genesis block if it has not already been written with [`StorageSettings`]. +pub fn init_genesis_with_settings( + factory: &PF, + storage_settings: StorageSettings, +) -> Result where PF: DatabaseProviderFactory + StaticFileProviderFactory> @@ -164,7 +190,6 @@ where static_file_provider.latest_writer(StaticFileSegment::Transactions)?.increment_block(0)?; // Behaviour reserved only for new nodes should be set here. - let storage_settings = StorageSettings::new(); provider_rw.write_storage_settings(storage_settings)?; // `commit_unwind`` will first commit the DB and then the static file provider, which is diff --git a/crates/storage/db/src/implementation/mdbx/mod.rs b/crates/storage/db/src/implementation/mdbx/mod.rs index b00bfd3c9a..05da99d682 100644 --- a/crates/storage/db/src/implementation/mdbx/mod.rs +++ b/crates/storage/db/src/implementation/mdbx/mod.rs @@ -154,6 +154,15 @@ impl DatabaseArguments { self } + /// Sets the database page size value. + pub const fn with_geometry_page_size(mut self, page_size: Option) -> Self { + if let Some(size) = page_size { + self.geometry.page_size = Some(reth_libmdbx::PageSize::Set(size)); + } + + self + } + /// Sets the database sync mode. pub const fn with_sync_mode(mut self, sync_mode: Option) -> Self { if let Some(sync_mode) = sync_mode { diff --git a/crates/storage/db/src/lib.rs b/crates/storage/db/src/lib.rs index a630672384..d3851d04ad 100644 --- a/crates/storage/db/src/lib.rs +++ b/crates/storage/db/src/lib.rs @@ -185,12 +185,13 @@ pub mod test_utils { #[track_caller] pub fn create_test_rw_db_with_path>(path: P) -> Arc> { let path = path.as_ref().to_path_buf(); + let emsg = format!("{ERROR_DB_CREATION}: {path:?}"); let db = init_db( path.as_path(), DatabaseArguments::new(ClientVersion::default()) .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Unbounded)), ) - .expect(ERROR_DB_CREATION); + .expect(&emsg); Arc::new(TempDatabase::new(db, path)) } @@ -201,8 +202,9 @@ pub mod test_utils { .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Unbounded)); let path = tempdir_path(); + let emsg = format!("{ERROR_DB_CREATION}: {path:?}"); { - init_db(path.as_path(), args.clone()).expect(ERROR_DB_CREATION); + init_db(path.as_path(), args.clone()).expect(&emsg); } let db = open_db_read_only(path.as_path(), args).expect(ERROR_DB_OPEN); Arc::new(TempDatabase::new(db, path)) diff --git a/crates/storage/errors/src/provider.rs b/crates/storage/errors/src/provider.rs index ed5230c18f..4aadd6a8b1 100644 --- a/crates/storage/errors/src/provider.rs +++ b/crates/storage/errors/src/provider.rs @@ -103,7 +103,11 @@ pub enum ProviderError { /// Static File is not found at specified path. #[cfg(feature = "std")] #[error("not able to find {_0} static file at {_1:?}")] - MissingStaticFilePath(StaticFileSegment, std::path::PathBuf), + MissingStaticFileSegmentPath(StaticFileSegment, std::path::PathBuf), + /// Static File is not found at specified path. + #[cfg(feature = "std")] + #[error("not able to find static file at {_0:?}")] + MissingStaticFilePath(std::path::PathBuf), /// Static File is not found for requested block. #[error("not able to find {_0} static file for block number {_1}")] MissingStaticFileBlock(StaticFileSegment, BlockNumber), diff --git a/crates/storage/nippy-jar/src/lib.rs b/crates/storage/nippy-jar/src/lib.rs index 02a34fca52..1eaa33a313 100644 --- a/crates/storage/nippy-jar/src/lib.rs +++ b/crates/storage/nippy-jar/src/lib.rs @@ -1041,10 +1041,10 @@ mod tests { assert_eq!(writer.rows(), 0); assert_eq!(writer.max_row_size(), 0); assert_eq!(File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize, 0); - // Only the byte that indicates how many bytes per offset should be left + // Offset size byte (1) + final offset (8) = 9 bytes assert_eq!( File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize, - 1 + 9 ); writer.commit().unwrap(); assert!(!writer.is_dirty()); diff --git a/crates/storage/nippy-jar/src/writer.rs b/crates/storage/nippy-jar/src/writer.rs index cf899791ee..2225b9d6f4 100644 --- a/crates/storage/nippy-jar/src/writer.rs +++ b/crates/storage/nippy-jar/src/writer.rs @@ -292,7 +292,12 @@ impl NippyJarWriter { // If all rows are to be pruned if new_num_offsets <= 1 { // <= 1 because the one offset would actually be the expected file data size - self.offsets_file.get_mut().set_len(1)?; + // + // When no rows remain, keep the offset size byte and the final offset (data + // file size = 0). This maintains the same structure as when + // a file is initially created. + // See `NippyJarWriter::create_or_open_files` for the initial file format. + self.offsets_file.get_mut().set_len(1 + OFFSET_SIZE_BYTES as u64)?; self.data_file.get_mut().set_len(0)?; } else { // Calculate the new length for the on-disk offset list diff --git a/crates/storage/provider/src/either_writer.rs b/crates/storage/provider/src/either_writer.rs index 2797a642d6..a981371d2a 100644 --- a/crates/storage/provider/src/either_writer.rs +++ b/crates/storage/provider/src/either_writer.rs @@ -12,6 +12,7 @@ use reth_primitives_traits::ReceiptTy; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{DBProvider, NodePrimitivesProvider, StorageSettingsCache}; use reth_storage_errors::provider::ProviderResult; +use strum::EnumIs; /// Type alias for [`EitherWriter`] constructors. type EitherWriterTy<'a, P, T> = EitherWriter< @@ -81,3 +82,36 @@ where } } } + +impl EitherWriter<'_, (), ()> { + /// Returns the destination for writing receipts. + /// + /// The rules are as follows: + /// - If the node should not always write receipts to static files, and any receipt pruning is + /// enabled, write to the database. + /// - If the node should always write receipts to static files, but receipt log filter pruning + /// is enabled, write to the database. + /// - Otherwise, write to static files. + pub fn receipts_destination( + provider: &P, + ) -> EitherWriterDestination { + let receipts_in_static_files = provider.cached_storage_settings().receipts_in_static_files; + let prune_modes = provider.prune_modes_ref(); + + if !receipts_in_static_files && prune_modes.has_receipts_pruning() || + // TODO: support writing receipts to static files with log filter pruning enabled + receipts_in_static_files && !prune_modes.receipts_log_filter.is_empty() + { + EitherWriterDestination::Database + } else { + EitherWriterDestination::StaticFile + } + } +} + +#[derive(Debug, EnumIs)] +#[allow(missing_docs)] +pub enum EitherWriterDestination { + Database, + StaticFile, +} diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 62bde757ee..793b15db8e 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -69,8 +69,7 @@ use reth_trie::{ TrieCursorIter, }, updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted}, - BranchNodeCompact, HashedPostStateSorted, Nibbles, StoredNibbles, StoredNibblesSubKey, - TrieChangeSetsEntry, + HashedPostStateSorted, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry, }; use reth_trie_db::{ DatabaseAccountTrieCursor, DatabaseStorageTrieCursor, DatabaseTrieCursorFactory, @@ -131,6 +130,13 @@ impl DatabaseProviderRW { pub fn into_tx(self) -> ::TXMut { self.0.into_tx() } + + /// Override the minimum pruning distance for testing purposes. + #[cfg(any(test, feature = "test-utils"))] + pub const fn with_minimum_pruning_distance(mut self, distance: u64) -> Self { + self.0.minimum_pruning_distance = distance; + self + } } impl From> @@ -157,6 +163,8 @@ pub struct DatabaseProvider { storage: Arc, /// Storage configuration settings for this node storage_settings: Arc>, + /// Minimum distance from tip required for pruning + minimum_pruning_distance: u64, } impl DatabaseProvider { @@ -262,7 +270,15 @@ impl DatabaseProvider { storage: Arc, storage_settings: Arc>, ) -> Self { - Self { tx, chain_spec, static_file_provider, prune_modes, storage, storage_settings } + Self { + tx, + chain_spec, + static_file_provider, + prune_modes, + storage, + storage_settings, + minimum_pruning_distance: MINIMUM_PRUNING_DISTANCE, + } } } @@ -309,12 +325,10 @@ impl DatabaseProvider DatabaseProvider>>(from_tx..)?; - if !self.prune_modes.has_receipts_pruning() { + if EitherWriter::receipts_destination(self).is_static_file() { let static_file_receipt_num = self.static_file_provider.get_highest_static_file_tx(StaticFileSegment::Receipts); @@ -509,7 +523,15 @@ impl DatabaseProvider { storage: Arc, storage_settings: Arc>, ) -> Self { - Self { tx, chain_spec, static_file_provider, prune_modes, storage, storage_settings } + Self { + tx, + chain_spec, + static_file_provider, + prune_modes, + storage, + storage_settings, + minimum_pruning_distance: MINIMUM_PRUNING_DISTANCE, + } } /// Consume `DbTx` or `DbTxMut`. @@ -1621,8 +1643,16 @@ impl StateWriter // All receipts from the last 128 blocks are required for blockchain tree, even with // [`PruneSegment::ContractLogs`]. - let prunable_receipts = - PruneMode::Distance(MINIMUM_PRUNING_DISTANCE).should_prune(first_block, tip); + // + // Receipts can only be skipped if we're dealing with legacy nodes that write them to + // Database, OR if receipts_in_static_files is enabled but no receipts exist in static + // files yet. Once receipts exist in static files, we must continue writing to maintain + // continuity and have no gaps. + let prunable_receipts = (EitherWriter::receipts_destination(self).is_database() || + self.static_file_provider() + .get_highest_static_file_tx(StaticFileSegment::Receipts) + .is_none()) && + PruneMode::Distance(self.minimum_pruning_distance).should_prune(first_block, tip); // Prepare set of addresses which logs should not be pruned. let mut allowed_addresses: HashSet = HashSet::new(); @@ -2135,17 +2165,13 @@ impl TrieWriter for DatabaseProvider // Wrap the cursor in DatabaseAccountTrieCursor let mut db_account_cursor = DatabaseAccountTrieCursor::new(curr_values_cursor); - // Static empty array for when updates_overlay is None - static EMPTY_ACCOUNT_UPDATES: Vec<(Nibbles, Option)> = Vec::new(); - - // Get the overlay updates for account trie, or use an empty array - let account_overlay_updates = updates_overlay - .map(|overlay| overlay.account_nodes_ref()) - .unwrap_or(&EMPTY_ACCOUNT_UPDATES); + // Create empty TrieUpdatesSorted for when updates_overlay is None + let empty_updates = TrieUpdatesSorted::default(); + let overlay = updates_overlay.unwrap_or(&empty_updates); // Wrap the cursor in InMemoryTrieCursor with the overlay let mut in_memory_account_cursor = - InMemoryTrieCursor::new(Some(&mut db_account_cursor), account_overlay_updates); + InMemoryTrieCursor::new_account(&mut db_account_cursor, overlay); for (path, _) in trie_updates.account_nodes_ref() { num_entries += 1; @@ -2383,8 +2409,8 @@ impl StorageTrieWriter for DatabaseP B256::default(), // Will be set per iteration ); - // Static empty array for when updates_overlay is None - static EMPTY_UPDATES: Vec<(Nibbles, Option)> = Vec::new(); + // Create empty TrieUpdatesSorted for when updates_overlay is None + let empty_updates = TrieUpdatesSorted::default(); for (hashed_address, storage_trie_updates) in storage_tries { let changeset_key = BlockNumberHashedAddress((block_number, *hashed_address)); @@ -2393,15 +2419,15 @@ impl StorageTrieWriter for DatabaseP changed_curr_values_cursor = DatabaseStorageTrieCursor::new(changed_curr_values_cursor.cursor, *hashed_address); - // Get the overlay updates for this storage trie, or use an empty array - let overlay_updates = updates_overlay - .and_then(|overlay| overlay.storage_tries_ref().get(hashed_address)) - .map(|updates| updates.storage_nodes_ref()) - .unwrap_or(&EMPTY_UPDATES); + // Get the overlay updates, or use empty updates + let overlay = updates_overlay.unwrap_or(&empty_updates); // Wrap the cursor in InMemoryTrieCursor with the overlay - let mut in_memory_changed_cursor = - InMemoryTrieCursor::new(Some(&mut changed_curr_values_cursor), overlay_updates); + let mut in_memory_changed_cursor = InMemoryTrieCursor::new_storage( + &mut changed_curr_values_cursor, + overlay, + *hashed_address, + ); // Create an iterator which produces the current values of all updated paths, or None if // they are currently unset. @@ -2417,8 +2443,11 @@ impl StorageTrieWriter for DatabaseP DatabaseStorageTrieCursor::new(wiped_nodes_cursor.cursor, *hashed_address); // Wrap the wiped nodes cursor in InMemoryTrieCursor with the overlay - let mut in_memory_wiped_cursor = - InMemoryTrieCursor::new(Some(&mut wiped_nodes_cursor), overlay_updates); + let mut in_memory_wiped_cursor = InMemoryTrieCursor::new_storage( + &mut wiped_nodes_cursor, + overlay, + *hashed_address, + ); let all_nodes = TrieCursorIter::new(&mut in_memory_wiped_cursor); @@ -3174,7 +3203,9 @@ mod tests { test_utils::{blocks::BlockchainTestData, create_test_provider_factory}, BlockWriter, }; + use reth_ethereum_primitives::Receipt; use reth_testing_utils::generators::{self, random_block, BlockParams}; + use reth_trie::Nibbles; #[test] fn test_receipts_by_block_range_empty_range() { @@ -4650,4 +4681,142 @@ mod tests { "storage_nibbles2 should have the value that was created and will be deleted" ); } + + #[test] + fn test_prunable_receipts_logic() { + let insert_blocks = + |provider_rw: &DatabaseProviderRW<_, _>, tip_block: u64, tx_count: u8| { + let mut rng = generators::rng(); + for block_num in 0..=tip_block { + let block = random_block( + &mut rng, + block_num, + BlockParams { tx_count: Some(tx_count), ..Default::default() }, + ); + provider_rw.insert_block(block.try_recover().unwrap()).unwrap(); + } + }; + + let write_receipts = |provider_rw: DatabaseProviderRW<_, _>, block: u64| { + let outcome = ExecutionOutcome { + first_block: block, + receipts: vec![vec![Receipt { + tx_type: Default::default(), + success: true, + cumulative_gas_used: block, // identifier to assert against + logs: vec![], + }]], + ..Default::default() + }; + provider_rw.write_state(&outcome, crate::OriginalValuesKnown::No).unwrap(); + provider_rw.commit().unwrap(); + }; + + // Legacy mode (receipts in DB) - should be prunable + { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::legacy(); + factory.set_storage_settings_cache(storage_settings); + let factory = factory.with_prune_modes(PruneModes { + receipts: Some(PruneMode::Before(100)), + ..Default::default() + }); + + let tip_block = 200u64; + let first_block = 1u64; + + // create chain + let provider_rw = factory.provider_rw().unwrap(); + insert_blocks(&provider_rw, tip_block, 1); + provider_rw.commit().unwrap(); + + write_receipts( + factory.provider_rw().unwrap().with_minimum_pruning_distance(100), + first_block, + ); + write_receipts( + factory.provider_rw().unwrap().with_minimum_pruning_distance(100), + tip_block - 1, + ); + + let provider = factory.provider().unwrap(); + + for (block, num_receipts) in [(0, 0), (tip_block - 1, 1)] { + assert!(provider + .receipts_by_block(block.into()) + .unwrap() + .is_some_and(|r| r.len() == num_receipts)); + } + } + + // Static files mode + { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::new().with_receipts_in_static_files(); + factory.set_storage_settings_cache(storage_settings); + let factory = factory.with_prune_modes(PruneModes { + receipts: Some(PruneMode::Before(2)), + ..Default::default() + }); + + let tip_block = 200u64; + + // create chain + let provider_rw = factory.provider_rw().unwrap(); + insert_blocks(&provider_rw, tip_block, 1); + provider_rw.commit().unwrap(); + + // Attempt to write receipts for block 0 and 1 (should be skipped) + write_receipts(factory.provider_rw().unwrap().with_minimum_pruning_distance(100), 0); + write_receipts(factory.provider_rw().unwrap().with_minimum_pruning_distance(100), 1); + + assert!(factory + .static_file_provider() + .get_highest_static_file_tx(StaticFileSegment::Receipts) + .is_none(),); + assert!(factory + .static_file_provider() + .get_highest_static_file_block(StaticFileSegment::Receipts) + .is_some_and(|b| b == 1),); + + // Since we have prune mode Before(2), the next receipt (block 2) should be written to + // static files. + write_receipts(factory.provider_rw().unwrap().with_minimum_pruning_distance(100), 2); + assert!(factory + .static_file_provider() + .get_highest_static_file_tx(StaticFileSegment::Receipts) + .is_some_and(|num| num == 2),); + + // After having a receipt already in static files, attempt to skip the next receipt by + // changing the prune mode. It should NOT skip it and should still write the receipt, + // since static files do not support gaps. + let factory = factory.with_prune_modes(PruneModes { + receipts: Some(PruneMode::Before(100)), + ..Default::default() + }); + let provider_rw = factory.provider_rw().unwrap().with_minimum_pruning_distance(1); + assert!(PruneMode::Distance(1).should_prune(3, tip_block)); + write_receipts(provider_rw, 3); + + // Ensure we can only fetch the 2 last receipts. + // + // Test setup only has 1 tx per block and each receipt has its cumulative_gas_used set + // to the block number it belongs to easily identify and assert. + let provider = factory.provider().unwrap(); + assert!(EitherWriter::receipts_destination(&provider).is_static_file()); + for (num, num_receipts) in [(0, 0), (1, 0), (2, 1), (3, 1)] { + assert!(provider + .receipts_by_block(num.into()) + .unwrap() + .is_some_and(|r| r.len() == num_receipts)); + + let receipt = provider.receipt(num).unwrap(); + if num_receipts > 0 { + assert!(receipt.is_some_and(|r| r.cumulative_gas_used == num)); + } else { + assert!(receipt.is_none()); + } + } + } + } } diff --git a/crates/storage/provider/src/providers/state/overlay.rs b/crates/storage/provider/src/providers/state/overlay.rs index d3ef87e6c4..ac0368b401 100644 --- a/crates/storage/provider/src/providers/state/overlay.rs +++ b/crates/storage/provider/src/providers/state/overlay.rs @@ -1,6 +1,9 @@ use alloy_primitives::{BlockNumber, B256}; +use metrics::{Counter, Histogram}; +use parking_lot::RwLock; use reth_db_api::DatabaseError; use reth_errors::{ProviderError, ProviderResult}; +use reth_metrics::Metrics; use reth_prune_types::PruneSegment; use reth_stages_types::StageId; use reth_storage_api::{ @@ -16,8 +19,39 @@ use reth_trie::{ use reth_trie_db::{ DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseTrieCursorFactory, }; -use std::sync::Arc; -use tracing::debug; +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Arc, + time::{Duration, Instant}, +}; +use tracing::{debug, debug_span, instrument}; + +/// Metrics for overlay state provider operations. +#[derive(Clone, Metrics)] +#[metrics(scope = "storage.providers.overlay")] +pub(crate) struct OverlayStateProviderMetrics { + /// Duration of creating the database provider transaction + create_provider_duration: Histogram, + /// Duration of retrieving trie updates from the database + retrieve_trie_reverts_duration: Histogram, + /// Duration of retrieving hashed state from the database + retrieve_hashed_state_reverts_duration: Histogram, + /// Size of trie updates (number of entries) + trie_updates_size: Histogram, + /// Size of hashed state (number of entries) + hashed_state_size: Histogram, + /// Overall duration of the [`OverlayStateProviderFactory::database_provider_ro`] call + database_provider_ro_duration: Histogram, + /// Number of cache misses when fetching [`Overlay`]s from the overlay cache. + overlay_cache_misses: Counter, +} + +/// Contains all fields required to initialize an [`OverlayStateProvider`]. +#[derive(Debug, Clone)] +struct Overlay { + trie_updates: Arc, + hashed_post_state: Arc, +} /// Factory for creating overlay state providers with optional reverts and overlays. /// @@ -33,12 +67,24 @@ pub struct OverlayStateProviderFactory { trie_overlay: Option>, /// Optional hashed state overlay hashed_state_overlay: Option>, + /// Metrics for tracking provider operations + metrics: OverlayStateProviderMetrics, + /// A cache which maps `db_tip -> Overlay`. If the db tip changes during usage of the factory + /// then a new entry will get added to this, but in most cases only one entry is present. + overlay_cache: Arc>>, } impl OverlayStateProviderFactory { /// Create a new overlay state provider factory - pub const fn new(factory: F) -> Self { - Self { factory, block_hash: None, trie_overlay: None, hashed_state_overlay: None } + pub fn new(factory: F) -> Self { + Self { + factory, + block_hash: None, + trie_overlay: None, + hashed_state_overlay: None, + metrics: OverlayStateProviderMetrics::default(), + overlay_cache: Default::default(), + } } /// Set the block hash for collecting reverts. All state will be reverted to the point @@ -74,7 +120,10 @@ where F::Provider: TrieReader + StageCheckpointReader + PruneCheckpointReader + BlockNumReader, { /// Returns the block number for [`Self`]'s `block_hash` field, if any. - fn get_block_number(&self, provider: &F::Provider) -> ProviderResult> { + fn get_requested_block_number( + &self, + provider: &F::Provider, + ) -> ProviderResult> { if let Some(block_hash) = self.block_hash { Ok(Some( provider @@ -86,6 +135,16 @@ where } } + /// Returns the block which is at the tip of the DB, i.e. the block which the state tables of + /// the DB are currently synced to. + fn get_db_tip_block_number(&self, provider: &F::Provider) -> ProviderResult { + provider + .get_stage_checkpoint(StageId::MerkleChangeSets)? + .as_ref() + .map(|chk| chk.block_number) + .ok_or_else(|| ProviderError::InsufficientChangesets { requested: 0, available: 0..=0 }) + } + /// Returns whether or not it is required to collect reverts, and validates that there are /// sufficient changesets to revert to the requested block number if so. /// @@ -95,27 +154,19 @@ where fn reverts_required( &self, provider: &F::Provider, + db_tip_block: BlockNumber, requested_block: BlockNumber, ) -> ProviderResult { - // Get the MerkleChangeSets stage and prune checkpoints. - let stage_checkpoint = provider.get_stage_checkpoint(StageId::MerkleChangeSets)?; - let prune_checkpoint = provider.get_prune_checkpoint(PruneSegment::MerkleChangeSets)?; - - // Get the upper bound from stage checkpoint - let upper_bound = - stage_checkpoint.as_ref().map(|chk| chk.block_number).ok_or_else(|| { - ProviderError::InsufficientChangesets { - requested: requested_block, - available: 0..=0, - } - })?; - - // If the requested block is the DB tip (determined by the MerkleChangeSets stage - // checkpoint) then there won't be any reverts necessary, and we can simply return Ok. - if upper_bound == requested_block { + // If the requested block is the DB tip then there won't be any reverts necessary, and we + // can simply return Ok. + if db_tip_block == requested_block { return Ok(false) } + // Get the MerkleChangeSets prune checkpoints, which will be used to determine the lower + // bound. + let prune_checkpoint = provider.get_prune_checkpoint(PruneSegment::MerkleChangeSets)?; + // Extract the lower bound from prune checkpoint if available. // // If not available we assume pruning has never ran and so there is no lower bound. This @@ -129,7 +180,7 @@ where .map(|block_number| block_number + 1) .unwrap_or_default(); - let available_range = lower_bound..=upper_bound; + let available_range = lower_bound..=db_tip_block; // Check if the requested block is within the available range if !available_range.contains(&requested_block) { @@ -141,37 +192,58 @@ where Ok(true) } -} -impl DatabaseProviderROFactory for OverlayStateProviderFactory -where - F: DatabaseProviderFactory, - F::Provider: TrieReader + StageCheckpointReader + PruneCheckpointReader + BlockNumReader, -{ - type Provider = OverlayStateProvider; - - /// Create a read-only [`OverlayStateProvider`]. - fn database_provider_ro(&self) -> ProviderResult> { - // Get a read-only provider - let provider = self.factory.database_provider_ro()?; + /// Calculates a new [`Overlay`] given a transaction and the current db tip. + #[instrument( + level = "debug", + target = "providers::state::overlay", + skip_all, + fields(db_tip_block) + )] + fn calculate_overlay( + &self, + provider: &F::Provider, + db_tip_block: BlockNumber, + ) -> ProviderResult { + // Set up variables we'll use for recording metrics. There's two different code-paths here, + // and we want to make sure both record metrics, so we do metrics recording after. + let retrieve_trie_reverts_duration; + let retrieve_hashed_state_reverts_duration; + let trie_updates_total_len; + let hashed_state_updates_total_len; // If block_hash is provided, collect reverts - let (trie_updates, hashed_state) = if let Some(from_block) = - self.get_block_number(&provider)? && - self.reverts_required(&provider, from_block)? + let (trie_updates, hashed_post_state) = if let Some(from_block) = + self.get_requested_block_number(provider)? && + self.reverts_required(provider, db_tip_block, from_block)? { // Collect trie reverts - let mut trie_reverts = provider.trie_reverts(from_block + 1)?; + let mut trie_reverts = { + let _guard = + debug_span!(target: "providers::state::overlay", "Retrieving trie reverts") + .entered(); + + let start = Instant::now(); + let res = provider.trie_reverts(from_block + 1)?; + retrieve_trie_reverts_duration = start.elapsed(); + res + }; // Collect state reverts - // - // TODO(mediocregopher) make from_reverts return sorted - // https://github.com/paradigmxyz/reth/issues/19382 - let mut hashed_state_reverts = HashedPostState::from_reverts::( - provider.tx_ref(), - from_block + 1.., - )? - .into_sorted(); + let mut hashed_state_reverts = { + let _guard = debug_span!(target: "providers::state::overlay", "Retrieving hashed state reverts").entered(); + + let start = Instant::now(); + // TODO(mediocregopher) make from_reverts return sorted + // https://github.com/paradigmxyz/reth/issues/19382 + let res = HashedPostState::from_reverts::( + provider.tx_ref(), + from_block + 1.., + )? + .into_sorted(); + retrieve_hashed_state_reverts_duration = start.elapsed(); + res + }; // Extend with overlays if provided. If the reverts are empty we should just use the // overlays directly, because `extend_ref` will actually clone the overlay. @@ -195,12 +267,15 @@ where None => Arc::new(hashed_state_reverts), }; + trie_updates_total_len = trie_updates.total_len(); + hashed_state_updates_total_len = hashed_state_updates.total_len(); + debug!( target: "providers::state::overlay", block_hash = ?self.block_hash, ?from_block, - num_trie_updates = ?trie_updates.total_len(), - num_state_updates = ?hashed_state_updates.total_len(), + num_trie_updates = ?trie_updates_total_len, + num_state_updates = ?hashed_state_updates_total_len, "Reverted to target block", ); @@ -214,10 +289,99 @@ where .clone() .unwrap_or_else(|| Arc::new(HashedPostStateSorted::default())); + retrieve_trie_reverts_duration = Duration::ZERO; + retrieve_hashed_state_reverts_duration = Duration::ZERO; + trie_updates_total_len = trie_updates.total_len(); + hashed_state_updates_total_len = hashed_state.total_len(); + (trie_updates, hashed_state) }; - Ok(OverlayStateProvider::new(provider, trie_updates, hashed_state)) + // Record metrics + self.metrics + .retrieve_trie_reverts_duration + .record(retrieve_trie_reverts_duration.as_secs_f64()); + self.metrics + .retrieve_hashed_state_reverts_duration + .record(retrieve_hashed_state_reverts_duration.as_secs_f64()); + self.metrics.trie_updates_size.record(trie_updates_total_len as f64); + self.metrics.hashed_state_size.record(hashed_state_updates_total_len as f64); + + Ok(Overlay { trie_updates, hashed_post_state }) + } + + /// Fetches an [`Overlay`] from the cache based on the current db tip block. If there is no + /// cached value then this calculates the [`Overlay`] and populates the cache. + #[instrument(level = "debug", target = "providers::state::overlay", skip_all)] + fn get_overlay(&self, provider: &F::Provider) -> ProviderResult { + // If we have no anchor block configured then we will never need to get trie reverts, just + // return the in-memory overlay. + if self.block_hash.is_none() { + let trie_updates = + self.trie_overlay.clone().unwrap_or_else(|| Arc::new(TrieUpdatesSorted::default())); + let hashed_post_state = self + .hashed_state_overlay + .clone() + .unwrap_or_else(|| Arc::new(HashedPostStateSorted::default())); + return Ok(Overlay { trie_updates, hashed_post_state }) + } + + let db_tip_block = self.get_db_tip_block_number(provider)?; + + // If the overlay is present in the cache then return it directly. + if let Some(overlay) = self.overlay_cache.as_ref().read().get(&db_tip_block) { + return Ok(overlay.clone()); + } + + // If the overlay is not present then we need to calculate a new one. We grab a write lock, + // and then check the cache again in case some other thread populated the cache since we + // checked with the read-lock. If still not present we calculate and populate. + let mut cache_miss = false; + let overlay = match self.overlay_cache.as_ref().write().entry(db_tip_block) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + cache_miss = true; + let overlay = self.calculate_overlay(provider, db_tip_block)?; + entry.insert(overlay.clone()); + overlay + } + }; + + if cache_miss { + self.metrics.overlay_cache_misses.increment(1); + } + + Ok(overlay) + } +} + +impl DatabaseProviderROFactory for OverlayStateProviderFactory +where + F: DatabaseProviderFactory, + F::Provider: TrieReader + StageCheckpointReader + PruneCheckpointReader + BlockNumReader, +{ + type Provider = OverlayStateProvider; + + /// Create a read-only [`OverlayStateProvider`]. + #[instrument(level = "debug", target = "providers::state::overlay", skip_all)] + fn database_provider_ro(&self) -> ProviderResult> { + let overall_start = Instant::now(); + + // Get a read-only provider + let provider = { + let _guard = + debug_span!(target: "providers::state::overlay", "Creating db provider").entered(); + + let start = Instant::now(); + let res = self.factory.database_provider_ro()?; + self.metrics.create_provider_duration.record(start.elapsed()); + res + }; + + let Overlay { trie_updates, hashed_post_state } = self.get_overlay(&provider)?; + + self.metrics.database_provider_ro_duration.record(overall_start.elapsed()); + Ok(OverlayStateProvider::new(provider, trie_updates, hashed_post_state)) } } diff --git a/crates/storage/provider/src/providers/static_file/manager.rs b/crates/storage/provider/src/providers/static_file/manager.rs index 2e1d34109a..772e6dd13e 100644 --- a/crates/storage/provider/src/providers/static_file/manager.rs +++ b/crates/storage/provider/src/providers/static_file/manager.rs @@ -3,9 +3,9 @@ use super::{ StaticFileJarProvider, StaticFileProviderRW, StaticFileProviderRWRefMut, }; use crate::{ - to_range, BlockHashReader, BlockNumReader, BlockReader, BlockSource, HeaderProvider, - ReceiptProvider, StageCheckpointReader, StatsReader, TransactionVariant, TransactionsProvider, - TransactionsProviderExt, + to_range, BlockHashReader, BlockNumReader, BlockReader, BlockSource, EitherWriter, + HeaderProvider, ReceiptProvider, StageCheckpointReader, StatsReader, TransactionVariant, + TransactionsProvider, TransactionsProviderExt, }; use alloy_consensus::{ transaction::{SignerRecoverable, TransactionMeta}, @@ -40,7 +40,7 @@ use reth_static_file_types::{ find_fixed_range, HighestStaticFiles, SegmentHeader, SegmentRangeInclusive, StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE, }; -use reth_storage_api::{BlockBodyIndicesProvider, DBProvider}; +use reth_storage_api::{BlockBodyIndicesProvider, DBProvider, StorageSettingsCache}; use reth_storage_errors::provider::{ProviderError, ProviderResult}; use std::{ collections::{BTreeMap, HashMap}, @@ -53,7 +53,7 @@ use tracing::{debug, info, trace, warn}; /// Alias type for a map that can be queried for block or transaction ranges. It uses `u64` to /// represent either a block or a transaction number end of a static file range. -type SegmentRanges = HashMap>; +type SegmentRanges = BTreeMap; /// Access mode on a static file provider. RO/RW. #[derive(Debug, Default, PartialEq, Eq)] @@ -111,6 +111,25 @@ impl StaticFileProviderBuilder { StaticFileProviderInner::new(path, StaticFileAccess::RO).map(|inner| Self { inner }) } + /// Set custom blocks per file for specific segments. + /// + /// Each static file segment is stored across multiple files, and each of these files contains + /// up to the specified number of blocks of data. When the file gets full, a new file is + /// created with the new block range. + /// + /// This setting affects the size of each static file, and can be set per segment. + /// + /// If it is changed for an existing node, existing static files will not be affected and will + /// be finished with the old blocks per file setting, but new static files will use the new + /// setting. + pub fn with_blocks_per_file_for_segments( + mut self, + segments: HashMap, + ) -> Self { + self.inner.blocks_per_file.extend(segments); + self + } + /// Set a custom number of blocks per file for all segments. pub fn with_blocks_per_file(mut self, blocks_per_file: u64) -> Self { for segment in StaticFileSegment::iter() { @@ -267,16 +286,8 @@ pub struct StaticFileProviderInner { /// Maintains a map which allows for concurrent access to different `NippyJars`, over different /// segments and ranges. map: DashMap<(BlockNumber, StaticFileSegment), LoadedJar>, - /// Min static file range for each segment. - /// This index is initialized on launch to keep track of the lowest, non-expired static file - /// per segment and gets updated on `Self::update_index()`. - /// - /// This tracks the lowest static file per segment together with the block range in that - /// file. E.g. static file is batched in 500k block intervals then the lowest static file - /// is [0..499K], and the block range is start = 0, end = 499K. - /// This index is mainly used to History expiry, which targets transactions, e.g. pre-merge - /// history expiry would lead to removing all static files below the merge height. - static_files_min_block: RwLock>, + /// Indexes per segment. + indexes: RwLock>, /// This is an additional index that tracks the expired height, this will track the highest /// block number that has been expired (missing). The first, non expired block is /// `expired_history_height + 1`. @@ -288,21 +299,6 @@ pub struct StaticFileProviderInner { /// This additional tracker exists for more efficient lookups because the node must be aware of /// the expired height. earliest_history_height: AtomicU64, - /// Max static file block for each segment - static_files_max_block: RwLock>, - /// Expected on disk static file block ranges indexed by max expected blocks. - /// - /// For example, a static file for expected block range `0..=499_000` may have only block range - /// `0..=1000` contained in it, as it wasn't fully filled yet. This index maps the max expected - /// block to the expected range, i.e. block `499_000` to block range `0..=499_000`. - static_files_expected_block_index: RwLock, - /// Available on disk static file block ranges indexed by max transactions. - /// - /// For example, a static file for block range `0..=499_000` may only have block range - /// `0..=1000` and transaction range `0..=2000` contained in it. This index maps the max - /// available transaction to the available block range, i.e. transaction `2000` to block range - /// `0..=1000`. - static_files_tx_index: RwLock, /// Directory where `static_files` are located path: PathBuf, /// Maintains a writer set of [`StaticFileSegment`]. @@ -333,12 +329,9 @@ impl StaticFileProviderInner { let provider = Self { map: Default::default(), + indexes: Default::default(), writers: Default::default(), - static_files_min_block: Default::default(), earliest_history_height: Default::default(), - static_files_max_block: Default::default(), - static_files_expected_block_index: Default::default(), - static_files_tx_index: Default::default(), path: path.as_ref().to_path_buf(), metrics: None, access, @@ -364,7 +357,7 @@ impl StaticFileProviderInner { pub fn find_fixed_range_with_block_index( &self, segment: StaticFileSegment, - block_index: Option<&BTreeMap>, + block_index: Option<&SegmentRanges>, block: BlockNumber, ) -> SegmentRangeInclusive { let blocks_per_file = @@ -403,7 +396,7 @@ impl StaticFileProviderInner { /// existing file, if any. /// /// This function will block indefinitely if a write lock for - /// [`Self::static_files_expected_block_index`] is acquired. In that case, use + /// [`Self::indexes`] is already acquired. In that case, use /// [`Self::find_fixed_range_with_block_index`]. pub fn find_fixed_range( &self, @@ -412,7 +405,10 @@ impl StaticFileProviderInner { ) -> SegmentRangeInclusive { self.find_fixed_range_with_block_index( segment, - self.static_files_expected_block_index.read().get(&segment), + self.indexes + .read() + .get(&segment) + .map(|index| &index.expected_block_ranges_by_max_block), block, ) } @@ -520,7 +516,7 @@ impl StaticFileProvider { &path .file_name() .ok_or_else(|| { - ProviderError::MissingStaticFilePath(segment, path.to_path_buf()) + ProviderError::MissingStaticFileSegmentPath(segment, path.to_path_buf()) })? .to_string_lossy(), ) @@ -541,6 +537,21 @@ impl StaticFileProvider { Ok(None) } + /// Gets the [`StaticFileJarProvider`] of the requested path. + pub fn get_segment_provider_for_path( + &self, + path: &Path, + ) -> ProviderResult>> { + StaticFileSegment::parse_filename( + &path + .file_name() + .ok_or_else(|| ProviderError::MissingStaticFilePath(path.to_path_buf()))? + .to_string_lossy(), + ) + .map(|(segment, block_range)| self.get_or_create_jar_provider(segment, &block_range)) + .transpose() + } + /// Given a segment and block range it removes the cached provider from the map. /// /// CAUTION: cached provider should be dropped before calling this or IT WILL deadlock. @@ -582,7 +593,7 @@ impl StaticFileProvider { let mut deleted_headers = Vec::new(); loop { - let Some(block_height) = self.get_lowest_static_file_block(segment) else { + let Some(block_height) = self.get_lowest_range_end(segment) else { return Ok(deleted_headers) }; @@ -681,11 +692,16 @@ impl StaticFileProvider { segment: StaticFileSegment, block: u64, ) -> Option { - self.static_files_max_block - .read() - .get(&segment) - .filter(|max| **max >= block) - .map(|_| self.find_fixed_range(segment, block)) + let indexes = self.indexes.read(); + let index = indexes.get(&segment)?; + + (index.max_block >= block).then(|| { + self.find_fixed_range_with_block_index( + segment, + Some(&index.expected_block_ranges_by_max_block), + block, + ) + }) } /// Gets a static file segment's fixed block range from the provider inner @@ -695,12 +711,13 @@ impl StaticFileProvider { segment: StaticFileSegment, tx: u64, ) -> Option { - let static_files = self.static_files_tx_index.read(); - let segment_static_files = static_files.get(&segment)?; + let indexes = self.indexes.read(); + let index = indexes.get(&segment)?; + let available_block_ranges_by_max_tx = index.available_block_ranges_by_max_tx.as_ref()?; // It's more probable that the request comes from a newer tx height, so we iterate // the static_files in reverse. - let mut static_files_rev_iter = segment_static_files.iter().rev().peekable(); + let mut static_files_rev_iter = available_block_ranges_by_max_tx.iter().rev().peekable(); while let Some((tx_end, block_range)) = static_files_rev_iter.next() { if tx > *tx_end { @@ -709,7 +726,11 @@ impl StaticFileProvider { } let tx_start = static_files_rev_iter.peek().map(|(tx_end, _)| *tx_end + 1).unwrap_or(0); if tx_start <= tx { - return Some(self.find_fixed_range(segment, block_range.end())) + return Some(self.find_fixed_range_with_block_index( + segment, + Some(&index.expected_block_ranges_by_max_block), + block_range.end(), + )) } } None @@ -726,18 +747,19 @@ impl StaticFileProvider { segment: StaticFileSegment, segment_max_block: Option, ) -> ProviderResult<()> { - let mut min_block = self.static_files_min_block.write(); - let mut max_block = self.static_files_max_block.write(); - let mut expected_block_index = self.static_files_expected_block_index.write(); - let mut tx_index = self.static_files_tx_index.write(); + debug!( + target: "provider::static_file", + ?segment, + ?segment_max_block, + "Updating provider index" + ); + let mut indexes = self.indexes.write(); match segment_max_block { Some(segment_max_block) => { - // Update the max block for the segment - max_block.insert(segment, segment_max_block); let fixed_range = self.find_fixed_range_with_block_index( segment, - expected_block_index.get(&segment), + indexes.get(&segment).map(|index| &index.expected_block_ranges_by_max_block), segment_max_block, ); @@ -746,6 +768,33 @@ impl StaticFileProvider { ) .map_err(ProviderError::other)?; + let index = indexes + .entry(segment) + .and_modify(|index| { + // Update max block + index.max_block = segment_max_block; + + // Update expected block range index + + // Remove all expected block ranges that are less than the new max block + index + .expected_block_ranges_by_max_block + .retain(|_, block_range| block_range.start() < fixed_range.start()); + // Insert new expected block range + index + .expected_block_ranges_by_max_block + .insert(fixed_range.end(), fixed_range); + }) + .or_insert_with(|| StaticFileSegmentIndex { + min_block_range: None, + max_block: segment_max_block, + expected_block_ranges_by_max_block: BTreeMap::from([( + fixed_range.end(), + fixed_range, + )]), + available_block_ranges_by_max_tx: None, + }); + // Update min_block to track the lowest block range of the segment. // This is initially set by initialize_index() on node startup, but must be updated // as the file grows to prevent stale values. @@ -762,28 +811,17 @@ impl StaticFileProvider { // 3. Pruner calls get_lowest_static_file_block() -> returns 100 (correct). Without // this update, it would incorrectly return 0 (stale) if let Some(current_block_range) = jar.user_header().block_range() { - min_block - .entry(segment) - .and_modify(|current_min| { - // delete_jar WILL ALWAYS re-initialize all indexes, so we are always - // sure that current_min is always the lowest. - if current_block_range.start() == current_min.start() { - *current_min = current_block_range; - } - }) - .or_insert(current_block_range); + if let Some(min_block_range) = index.min_block_range.as_mut() { + // delete_jar WILL ALWAYS re-initialize all indexes, so we are always + // sure that current_min is always the lowest. + if current_block_range.start() == min_block_range.start() { + *min_block_range = current_block_range; + } + } else { + index.min_block_range = Some(current_block_range); + } } - // Update the expected block index - expected_block_index - .entry(segment) - .and_modify(|index| { - index.retain(|_, block_range| block_range.start() < fixed_range.start()); - - index.insert(fixed_range.end(), fixed_range); - }) - .or_insert_with(|| BTreeMap::from([(fixed_range.end(), fixed_range)])); - // Updates the tx index by first removing all entries which have a higher // block_start than our current static file. if let Some(tx_range) = jar.user_header().tx_range() { @@ -800,100 +838,94 @@ impl StaticFileProvider { // equal than our current one. This is important in the case // that we prune a lot of rows resulting in a file (and thus // a higher block range) deletion. - tx_index - .entry(segment) - .and_modify(|index| { - index.retain(|_, block_range| { - block_range.start() < fixed_range.start() - }); - index.insert(tx_end, current_block_range); - }) - .or_insert_with(|| BTreeMap::from([(tx_end, current_block_range)])); + if let Some(index) = index.available_block_ranges_by_max_tx.as_mut() { + index + .retain(|_, block_range| block_range.start() < fixed_range.start()); + index.insert(tx_end, current_block_range); + } else { + index.available_block_ranges_by_max_tx = + Some(BTreeMap::from([(tx_end, current_block_range)])); + } } } else if segment.is_tx_based() { // The unwinded file has no more transactions/receipts. However, the highest // block is within this files' block range. We only retain // entries with block ranges before the current one. - tx_index.entry(segment).and_modify(|index| { + if let Some(index) = index.available_block_ranges_by_max_tx.as_mut() { index.retain(|_, block_range| block_range.start() < fixed_range.start()); - }); + } // If the index is empty, just remove it. - if tx_index.get(&segment).is_some_and(|index| index.is_empty()) { - tx_index.remove(&segment); - } + index.available_block_ranges_by_max_tx.take_if(|index| index.is_empty()); } // Update the cached provider. + debug!(target: "provider::static_file", ?segment, "Inserting updated jar into cache"); self.map.insert((fixed_range.end(), segment), LoadedJar::new(jar)?); // Delete any cached provider that no longer has an associated jar. + debug!(target: "provider::static_file", ?segment, "Cleaning up jar map"); self.map.retain(|(end, seg), _| !(*seg == segment && *end > fixed_range.end())); } None => { - max_block.remove(&segment); - min_block.remove(&segment); - expected_block_index.remove(&segment); - tx_index.remove(&segment); + debug!(target: "provider::static_file", ?segment, "Removing segment from index"); + indexes.remove(&segment); } }; + debug!(target: "provider::static_file", ?segment, "Updated provider index"); Ok(()) } /// Initializes the inner transaction and block index pub fn initialize_index(&self) -> ProviderResult<()> { - let mut min_block = self.static_files_min_block.write(); - let mut max_block = self.static_files_max_block.write(); - let mut expected_block_index = self.static_files_expected_block_index.write(); - let mut tx_index = self.static_files_tx_index.write(); - - min_block.clear(); - max_block.clear(); - tx_index.clear(); + let mut indexes = self.indexes.write(); + indexes.clear(); for (segment, headers) in iter_static_files(&self.path).map_err(ProviderError::other)? { // Update first and last block for each segment - if let Some((block_range, _)) = headers.first() { - min_block.insert(segment, *block_range); - } - if let Some((block_range, _)) = headers.last() { - max_block.insert(segment, block_range.end()); - } + // + // It's safe to call `expect` here, because every segment has at least one header + // associated with it. + let min_block_range = Some(headers.first().expect("headers are not empty").0); + let max_block = headers.last().expect("headers are not empty").0.end(); + + let mut expected_block_ranges_by_max_block = BTreeMap::default(); + let mut available_block_ranges_by_max_tx = None; for (block_range, header) in headers { // Update max expected block -> expected_block_range index - expected_block_index - .entry(segment) - .and_modify(|index| { - index.insert(header.expected_block_end(), header.expected_block_range()); - }) - .or_insert_with(|| { - BTreeMap::from([( - header.expected_block_end(), - header.expected_block_range(), - )]) - }); + expected_block_ranges_by_max_block + .insert(header.expected_block_end(), header.expected_block_range()); // Update max tx -> block_range index if let Some(tx_range) = header.tx_range() { let tx_end = tx_range.end(); - tx_index - .entry(segment) - .and_modify(|index| { - index.insert(tx_end, block_range); - }) - .or_insert_with(|| BTreeMap::from([(tx_end, block_range)])); + available_block_ranges_by_max_tx + .get_or_insert_with(BTreeMap::default) + .insert(tx_end, block_range); } } + + indexes.insert( + segment, + StaticFileSegmentIndex { + min_block_range, + max_block, + expected_block_ranges_by_max_block, + available_block_ranges_by_max_tx, + }, + ); } // If this is a re-initialization, we need to clear this as well self.map.clear(); // initialize the expired history height to the lowest static file block - if let Some(lowest_range) = min_block.get(&StaticFileSegment::Transactions) { + if let Some(lowest_range) = + indexes.get(&StaticFileSegment::Transactions).and_then(|index| index.min_block_range) + { // the earliest height is the lowest available block number self.earliest_history_height .store(lowest_range.start(), std::sync::atomic::Ordering::Relaxed); @@ -928,10 +960,13 @@ impl StaticFileProvider { pub fn check_consistency( &self, provider: &Provider, - has_receipt_pruning: bool, ) -> ProviderResult> where - Provider: DBProvider + BlockReader + StageCheckpointReader + ChainSpecProvider, + Provider: DBProvider + + BlockReader + + StageCheckpointReader + + ChainSpecProvider + + StorageSettingsCache, N: NodePrimitives, { // OVM historical import is broken and does not work with this check. It's importing @@ -966,11 +1001,14 @@ impl StaticFileProvider { }; for segment in StaticFileSegment::iter() { + debug!(target: "reth::providers::static_file", ?segment, "Checking consistency for segment"); match segment { StaticFileSegment::Headers | StaticFileSegment::Transactions => {} StaticFileSegment::Receipts => { - if has_receipt_pruning { - // Pruned nodes (including full node) do not store receipts as static files. + if EitherWriter::receipts_destination(provider).is_database() { + // Old pruned nodes (including full node) do not store receipts as static + // files. + debug!(target: "reth::providers::static_file", ?segment, "Skipping receipts consistency check: receipts stored in database"); continue } @@ -980,12 +1018,14 @@ impl StaticFileProvider { // Gnosis and Chiado's historical import is broken and does not work with // this check. They are importing receipts along // with importing headers/bodies. + debug!(target: "reth::providers::static_file", ?segment, "Skipping receipts consistency check: broken historical import for gnosis/chiado"); continue; } } } let initial_highest_block = self.get_highest_static_file_block(segment); + debug!(target: "reth::providers::static_file", ?segment, ?initial_highest_block, "Initial highest block for segment"); // File consistency is broken if: // @@ -996,8 +1036,10 @@ impl StaticFileProvider { // we are expected to still have. We need to check the Database and unwind everything // accordingly. if self.access.is_read_only() { + debug!(target: "reth::providers::static_file", ?segment, "Checking segment consistency (read-only)"); self.check_segment_consistency(segment)?; } else { + debug!(target: "reth::providers::static_file", ?segment, "Fetching latest writer which might heal any potential inconsistency"); // Fetching the writer will attempt to heal any file level inconsistency. self.latest_writer(segment)?; } @@ -1024,20 +1066,25 @@ impl StaticFileProvider { // from a pruning interruption might have decreased the number of transactions without // being able to update the last block of the static file segment. let highest_tx = self.get_highest_static_file_tx(segment); + debug!(target: "reth::providers::static_file", ?segment, ?highest_tx, ?highest_block, "Highest transaction for segment"); if let Some(highest_tx) = highest_tx { let mut last_block = highest_block.unwrap_or_default(); + debug!(target: "reth::providers::static_file", ?segment, last_block, highest_tx, "Verifying last transaction matches last block indices"); loop { if let Some(indices) = provider.block_body_indices(last_block)? { + debug!(target: "reth::providers::static_file", ?segment, last_block, last_tx_num = indices.last_tx_num(), highest_tx, "Found block body indices"); if indices.last_tx_num() <= highest_tx { break } } else { + debug!(target: "reth::providers::static_file", ?segment, last_block, "Block body indices not found, static files ahead of database"); // If the block body indices can not be found, then it means that static // files is ahead of database, and the `ensure_invariants` check will fix // it by comparing with stage checkpoints. break } if last_block == 0 { + debug!(target: "reth::providers::static_file", ?segment, "Reached block 0 in verification loop"); break } last_block -= 1; @@ -1054,6 +1101,7 @@ impl StaticFileProvider { } } + debug!(target: "reth::providers::static_file", ?segment, "Ensuring invariants for segment"); if let Some(unwind) = match segment { StaticFileSegment::Headers => self .ensure_invariants::<_, tables::Headers>( @@ -1077,7 +1125,10 @@ impl StaticFileProvider { highest_block, )?, } { + debug!(target: "reth::providers::static_file", ?segment, unwind_target=unwind, "Invariants check returned unwind target"); update_unwind_target(unwind); + } else { + debug!(target: "reth::providers::static_file", ?segment, "Invariants check completed, no unwind needed"); } } @@ -1087,14 +1138,20 @@ impl StaticFileProvider { /// Checks consistency of the latest static file segment and throws an error if at fault. /// Read-only. pub fn check_segment_consistency(&self, segment: StaticFileSegment) -> ProviderResult<()> { + debug!(target: "reth::providers::static_file", ?segment, "Checking segment consistency"); if let Some(latest_block) = self.get_highest_static_file_block(segment) { let file_path = self .directory() .join(segment.filename(&self.find_fixed_range(segment, latest_block))); + debug!(target: "reth::providers::static_file", ?segment, ?file_path, latest_block, "Loading NippyJar for consistency check"); let jar = NippyJar::::load(&file_path).map_err(ProviderError::other)?; + debug!(target: "reth::providers::static_file", ?segment, "NippyJar loaded, checking consistency"); NippyJarChecker::new(jar).check_consistency().map_err(ProviderError::other)?; + debug!(target: "reth::providers::static_file", ?segment, "NippyJar consistency check passed"); + } else { + debug!(target: "reth::providers::static_file", ?segment, "No static file block found, skipping consistency check"); } Ok(()) } @@ -1123,9 +1180,11 @@ impl StaticFileProvider { where Provider: DBProvider + BlockReader + StageCheckpointReader, { + debug!(target: "reth::providers::static_file", ?segment, ?highest_static_file_entry, ?highest_static_file_block, "Ensuring invariants"); let mut db_cursor = provider.tx_ref().cursor_read::()?; if let Some((db_first_entry, _)) = db_cursor.first()? { + debug!(target: "reth::providers::static_file", ?segment, db_first_entry, "Found first database entry"); if let (Some(highest_entry), Some(highest_block)) = (highest_static_file_entry, highest_static_file_block) { @@ -1149,8 +1208,11 @@ impl StaticFileProvider { highest_static_file_entry .is_none_or(|highest_entry| db_last_entry > highest_entry) { + debug!(target: "reth::providers::static_file", ?segment, db_last_entry, ?highest_static_file_entry, "Database has entries beyond static files, no unwind needed"); return Ok(None) } + } else { + debug!(target: "reth::providers::static_file", ?segment, "No database entries found"); } let highest_static_file_entry = highest_static_file_entry.unwrap_or_default(); @@ -1158,14 +1220,14 @@ impl StaticFileProvider { // If static file entry is ahead of the database entries, then ensure the checkpoint block // number matches. - let checkpoint_block_number = provider - .get_stage_checkpoint(match segment { - StaticFileSegment::Headers => StageId::Headers, - StaticFileSegment::Transactions => StageId::Bodies, - StaticFileSegment::Receipts => StageId::Execution, - })? - .unwrap_or_default() - .block_number; + let stage_id = match segment { + StaticFileSegment::Headers => StageId::Headers, + StaticFileSegment::Transactions => StageId::Bodies, + StaticFileSegment::Receipts => StageId::Execution, + }; + let checkpoint_block_number = + provider.get_stage_checkpoint(stage_id)?.unwrap_or_default().block_number; + debug!(target: "reth::providers::static_file", ?segment, ?stage_id, checkpoint_block_number, highest_static_file_block, "Retrieved stage checkpoint"); // If the checkpoint is ahead, then we lost static file data. May be data corruption. if checkpoint_block_number > highest_static_file_block { @@ -1191,22 +1253,38 @@ impl StaticFileProvider { "Unwinding static file segment." ); let mut writer = self.latest_writer(segment)?; - if segment.is_headers() { - // TODO(joshie): is_block_meta - writer.prune_headers(highest_static_file_block - checkpoint_block_number)?; - } else if let Some(block) = provider.block_body_indices(checkpoint_block_number)? { - // todo joshie: is querying block_body_indices a potential issue once bbi is moved - // to sf as well - let number = highest_static_file_entry - block.last_tx_num(); - if segment.is_receipts() { - writer.prune_receipts(number, checkpoint_block_number)?; - } else { - writer.prune_transactions(number, checkpoint_block_number)?; + match segment { + StaticFileSegment::Headers => { + let prune_count = highest_static_file_block - checkpoint_block_number; + debug!(target: "reth::providers::static_file", ?segment, prune_count, "Pruning headers"); + // TODO(joshie): is_block_meta + writer.prune_headers(prune_count)?; + } + StaticFileSegment::Transactions | StaticFileSegment::Receipts => { + if let Some(block) = provider.block_body_indices(checkpoint_block_number)? { + let number = highest_static_file_entry - block.last_tx_num(); + debug!(target: "reth::providers::static_file", ?segment, prune_count = number, checkpoint_block_number, "Pruning transaction based segment"); + + match segment { + StaticFileSegment::Transactions => { + writer.prune_transactions(number, checkpoint_block_number)? + } + StaticFileSegment::Receipts => { + writer.prune_receipts(number, checkpoint_block_number)? + } + StaticFileSegment::Headers => unreachable!(), + } + } else { + debug!(target: "reth::providers::static_file", ?segment, checkpoint_block_number, "No block body indices found for checkpoint block"); + } } } + debug!(target: "reth::providers::static_file", ?segment, "Committing writer after pruning"); writer.commit()?; + debug!(target: "reth::providers::static_file", ?segment, "Writer committed successfully"); } + debug!(target: "reth::providers::static_file", ?segment, "Invariants ensured, returning None"); Ok(None) } @@ -1221,45 +1299,46 @@ impl StaticFileProvider { self.earliest_history_height.load(std::sync::atomic::Ordering::Relaxed) } - /// Gets the lowest transaction static file block if it exists. - /// - /// For example if the transactions static file has blocks 0-499, this will return 499.. - /// - /// If there is nothing on disk for the given segment, this will return [`None`]. - pub fn get_lowest_transaction_static_file_block(&self) -> Option { - self.get_lowest_static_file_block(StaticFileSegment::Transactions) - } - - /// Gets the lowest static file's block height if it exists for a static file segment. - /// - /// For example if the static file has blocks 0-499, this will return 499.. - /// - /// If there is nothing on disk for the given segment, this will return [`None`]. - pub fn get_lowest_static_file_block(&self, segment: StaticFileSegment) -> Option { - self.static_files_min_block.read().get(&segment).map(|range| range.end()) - } - /// Gets the lowest static file's block range if it exists for a static file segment. /// /// If there is nothing on disk for the given segment, this will return [`None`]. pub fn get_lowest_range(&self, segment: StaticFileSegment) -> Option { - self.static_files_min_block.read().get(&segment).copied() + self.indexes.read().get(&segment).and_then(|index| index.min_block_range) + } + + /// Gets the lowest static file's block range start if it exists for a static file segment. + /// + /// For example if the lowest static file has blocks 0-499, this will return 0. + /// + /// If there is nothing on disk for the given segment, this will return [`None`]. + pub fn get_lowest_range_start(&self, segment: StaticFileSegment) -> Option { + self.get_lowest_range(segment).map(|range| range.start()) + } + + /// Gets the lowest static file's block range end if it exists for a static file segment. + /// + /// For example if the static file has blocks 0-499, this will return 499. + /// + /// If there is nothing on disk for the given segment, this will return [`None`]. + pub fn get_lowest_range_end(&self, segment: StaticFileSegment) -> Option { + self.get_lowest_range(segment).map(|range| range.end()) } /// Gets the highest static file's block height if it exists for a static file segment. /// /// If there is nothing on disk for the given segment, this will return [`None`]. pub fn get_highest_static_file_block(&self, segment: StaticFileSegment) -> Option { - self.static_files_max_block.read().get(&segment).copied() + self.indexes.read().get(&segment).map(|index| index.max_block) } /// Gets the highest static file transaction. /// /// If there is nothing on disk for the given segment, this will return [`None`]. pub fn get_highest_static_file_tx(&self, segment: StaticFileSegment) -> Option { - self.static_files_tx_index + self.indexes .read() .get(&segment) + .and_then(|index| index.available_block_ranges_by_max_tx.as_ref()) .and_then(|index| index.last_key_value().map(|(last_tx, _)| *last_tx)) } @@ -1277,7 +1356,9 @@ impl StaticFileProvider { segment: StaticFileSegment, func: impl Fn(StaticFileJarProvider<'_, N>) -> ProviderResult>, ) -> ProviderResult> { - if let Some(ranges) = self.static_files_expected_block_index.read().get(&segment) { + if let Some(ranges) = + self.indexes.read().get(&segment).map(|index| &index.expected_block_ranges_by_max_block) + { // Iterate through all ranges in reverse order (highest to lowest) for range in ranges.values().rev() { if let Some(res) = func(self.get_or_create_jar_provider(segment, range)?)? { @@ -1439,7 +1520,7 @@ impl StaticFileProvider { /// /// # Arguments /// * `segment` - The segment of the static file to query. - /// * `block_range` - The range of data to fetch. + /// * `block_or_tx_range` - The range of data to fetch. /// * `fetch_from_static_file` - A function to fetch data from the `static_file`. /// * `fetch_from_database` - A function to fetch data from the database. /// * `predicate` - A function used to evaluate each item in the fetched data. Fetching is @@ -1483,25 +1564,64 @@ impl StaticFileProvider { Ok(data) } - /// Returns `static_files` directory + /// Returns static files directory #[cfg(any(test, feature = "test-utils"))] pub fn path(&self) -> &Path { &self.path } - /// Returns `static_files` transaction index + /// Returns transaction index #[cfg(any(test, feature = "test-utils"))] - pub fn tx_index(&self) -> &RwLock { - &self.static_files_tx_index + pub fn tx_index(&self, segment: StaticFileSegment) -> Option { + self.indexes + .read() + .get(&segment) + .and_then(|index| index.available_block_ranges_by_max_tx.as_ref()) + .cloned() } - /// Returns `static_files` expected block index + /// Returns expected block index #[cfg(any(test, feature = "test-utils"))] - pub fn expected_block_index(&self) -> &RwLock { - &self.static_files_expected_block_index + pub fn expected_block_index(&self, segment: StaticFileSegment) -> Option { + self.indexes + .read() + .get(&segment) + .map(|index| &index.expected_block_ranges_by_max_block) + .cloned() } } +#[derive(Debug)] +struct StaticFileSegmentIndex { + /// Min static file block range. + /// + /// This index is initialized on launch to keep track of the lowest, non-expired static file + /// per segment and gets updated on [`StaticFileProvider::update_index`]. + /// + /// This tracks the lowest static file per segment together with the block range in that + /// file. E.g. static file is batched in 500k block intervals then the lowest static file + /// is [0..499K], and the block range is start = 0, end = 499K. + /// + /// This index is mainly used for history expiry, which targets transactions, e.g. pre-merge + /// history expiry would lead to removing all static files below the merge height. + min_block_range: Option, + /// Max static file block. + max_block: u64, + /// Expected static file block ranges indexed by max expected blocks. + /// + /// For example, a static file for expected block range `0..=499_000` may have only block range + /// `0..=1000` contained in it, as it's not fully filled yet. This index maps the max expected + /// block to the expected range, i.e. block `499_000` to block range `0..=499_000`. + expected_block_ranges_by_max_block: SegmentRanges, + /// Available on disk static file block ranges indexed by max transactions. + /// + /// For example, a static file for block range `0..=499_000` may only have block range + /// `0..=1000` and transaction range `0..=2000` contained in it. This index maps the max + /// available transaction to the available block range, i.e. transaction `2000` to block range + /// `0..=1000`. + available_block_ranges_by_max_tx: Option, +} + /// Helper trait to manage different [`StaticFileProviderRW`] of an `Arc 0 { - if segment.is_receipts() { - // Used as ID for validation - receipt.cumulative_gas_used = *next_tx_num; - writer.append_receipt(*next_tx_num, &receipt).unwrap(); - } else { - // Used as ID for validation - tx.nonce = *next_tx_num; - let tx: TransactionSigned = - tx.clone().into_signed(Signature::test_signature()).into(); - writer.append_transaction(*next_tx_num, &tx).unwrap(); + match segment { + StaticFileSegment::Headers => panic!("non tx based segment"), + StaticFileSegment::Transactions => { + // Used as ID for validation + tx.nonce = *next_tx_num; + let tx: TransactionSigned = + tx.clone().into_signed(Signature::test_signature()).into(); + writer.append_transaction(*next_tx_num, &tx).unwrap(); + } + StaticFileSegment::Receipts => { + // Used as ID for validation + receipt.cumulative_gas_used = *next_tx_num; + writer.append_receipt(*next_tx_num, &receipt).unwrap(); + } } *next_tx_num += 1; tx_count -= 1; @@ -387,11 +391,12 @@ mod tests { }); // Ensure transaction index - let tx_index = sf_rw.tx_index().read(); - let expected_tx_index = - vec![(8, SegmentRangeInclusive::new(0, 9)), (9, SegmentRangeInclusive::new(20, 29))]; + let expected_tx_index = BTreeMap::from([ + (8, SegmentRangeInclusive::new(0, 9)), + (9, SegmentRangeInclusive::new(20, 29)), + ]); assert_eq!( - tx_index.get(&segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()), + sf_rw.tx_index(segment), (!expected_tx_index.is_empty()).then_some(expected_tx_index), "tx index mismatch", ); @@ -414,15 +419,17 @@ mod tests { last_block: u64, expected_tx_tip: Option, expected_file_count: i32, - expected_tx_index: Vec<(TxNumber, SegmentRangeInclusive)>, + expected_tx_index: BTreeMap, ) -> eyre::Result<()> { let mut writer = sf_rw.latest_writer(segment)?; // Prune transactions or receipts based on the segment type - if segment.is_receipts() { - writer.prune_receipts(prune_count, last_block)?; - } else { - writer.prune_transactions(prune_count, last_block)?; + match segment { + StaticFileSegment::Headers => panic!("non tx based segment"), + StaticFileSegment::Transactions => { + writer.prune_transactions(prune_count, last_block)? + } + StaticFileSegment::Receipts => writer.prune_receipts(prune_count, last_block)?, } writer.commit()?; @@ -437,18 +444,18 @@ mod tests { // Verify that transactions and receipts are returned correctly. Uses // cumulative_gas_used & nonce as ids. if let Some(id) = expected_tx_tip { - if segment.is_receipts() { - assert_eyre( - expected_tx_tip, - sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used), - "tx mismatch", - )?; - } else { - assert_eyre( + match segment { + StaticFileSegment::Headers => panic!("non tx based segment"), + StaticFileSegment::Transactions => assert_eyre( expected_tx_tip, sf_rw.transaction_by_id(id)?.map(|t| t.nonce()), "tx mismatch", - )?; + )?, + StaticFileSegment::Receipts => assert_eyre( + expected_tx_tip, + sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used), + "receipt mismatch", + )?, } } @@ -460,9 +467,8 @@ mod tests { )?; // Ensure that the inner tx index (max_tx -> block range) is as expected - let tx_index = sf_rw.tx_index().read(); assert_eyre( - tx_index.get(&segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()), + sf_rw.tx_index(segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()), (!expected_tx_index.is_empty()).then_some(expected_tx_index), "tx index mismatch", )?; @@ -500,7 +506,7 @@ mod tests { blocks_per_file * 2, Some(highest_tx - 1), initial_file_count, - vec![(highest_tx - 1, SegmentRangeInclusive::new(0, 9))], + BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]), ), // Case 1: 10..=19 has no txs. There are no txes in the whole block range, but want // to unwind to block 9. Ensures that the 20..=29 and 10..=19 files @@ -510,7 +516,7 @@ mod tests { blocks_per_file - 1, Some(highest_tx - 1), files_per_range, - vec![(highest_tx - 1, SegmentRangeInclusive::new(0, 9))], + BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]), ), // Case 2: Prune most txs up to block 1. ( @@ -518,10 +524,10 @@ mod tests { 1, Some(0), files_per_range, - vec![(0, SegmentRangeInclusive::new(0, 1))], + BTreeMap::from([(0, SegmentRangeInclusive::new(0, 1))]), ), // Case 3: Prune remaining tx and ensure that file is not deleted. - (1, 0, None, files_per_range, vec![]), + (1, 0, None, files_per_range, BTreeMap::from([])), ]; // Loop through test cases @@ -578,14 +584,11 @@ mod tests { assert_eq!(sf_rw.headers_range(0..=15)?.len(), 16); assert_eq!( - sf_rw.expected_block_index().read().deref(), - &HashMap::from([( - StaticFileSegment::Headers, - BTreeMap::from([ - (9, SegmentRangeInclusive::new(0, 9)), - (19, SegmentRangeInclusive::new(10, 19)) - ]) - )]) + sf_rw.expected_block_index(StaticFileSegment::Headers), + Some(BTreeMap::from([ + (9, SegmentRangeInclusive::new(0, 9)), + (19, SegmentRangeInclusive::new(10, 19)) + ])), ) } @@ -604,15 +607,12 @@ mod tests { assert_eq!(sf_rw.headers_range(0..=22)?.len(), 23); assert_eq!( - sf_rw.expected_block_index().read().deref(), - &HashMap::from([( - StaticFileSegment::Headers, - BTreeMap::from([ - (9, SegmentRangeInclusive::new(0, 9)), - (19, SegmentRangeInclusive::new(10, 19)), - (24, SegmentRangeInclusive::new(20, 24)) - ]) - )]) + sf_rw.expected_block_index(StaticFileSegment::Headers), + Some(BTreeMap::from([ + (9, SegmentRangeInclusive::new(0, 9)), + (19, SegmentRangeInclusive::new(10, 19)), + (24, SegmentRangeInclusive::new(20, 24)) + ])) ) } @@ -631,17 +631,14 @@ mod tests { assert_eq!(sf_rw.headers_range(0..=40)?.len(), 41); assert_eq!( - sf_rw.expected_block_index().read().deref(), - &HashMap::from([( - StaticFileSegment::Headers, - BTreeMap::from([ - (9, SegmentRangeInclusive::new(0, 9)), - (19, SegmentRangeInclusive::new(10, 19)), - (24, SegmentRangeInclusive::new(20, 24)), - (39, SegmentRangeInclusive::new(25, 39)), - (54, SegmentRangeInclusive::new(40, 54)) - ]) - )]) + sf_rw.expected_block_index(StaticFileSegment::Headers), + Some(BTreeMap::from([ + (9, SegmentRangeInclusive::new(0, 9)), + (19, SegmentRangeInclusive::new(10, 19)), + (24, SegmentRangeInclusive::new(20, 24)), + (39, SegmentRangeInclusive::new(25, 39)), + (54, SegmentRangeInclusive::new(40, 54)) + ])) ) } diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index 2fc4ba61fc..6fa6aa294d 100644 --- a/crates/storage/provider/src/providers/static_file/writer.rs +++ b/crates/storage/provider/src/providers/static_file/writer.rs @@ -61,12 +61,16 @@ impl StaticFileWriters { } pub(crate) fn commit(&self) -> ProviderResult<()> { + debug!(target: "provider::static_file", "Committing all static file segments"); + for writer_lock in [&self.headers, &self.transactions, &self.receipts] { let mut writer = writer_lock.write(); if let Some(writer) = writer.as_mut() { writer.commit()?; } } + + debug!(target: "provider::static_file", "Committed all static file segments"); Ok(()) } @@ -219,6 +223,14 @@ impl StaticFileProviderRW { self.user_header_mut().prune(pruned_rows); } + debug!( + target: "provider::static_file", + segment = ?self.writer.user_header().segment(), + path = ?self.data_path, + pruned_rows, + "Ensuring end range consistency" + ); + self.writer.commit().map_err(ProviderError::other)?; // Updates the [SnapshotProvider] manager @@ -237,6 +249,12 @@ impl StaticFileProviderRW { // Truncates the data file if instructed to. if let Some((to_delete, last_block_number)) = self.prune_on_commit.take() { + debug!( + target: "provider::static_file", + segment = ?self.writer.user_header().segment(), + to_delete, + "Pruning data on commit" + ); match self.writer.user_header().segment() { StaticFileSegment::Headers => self.prune_header_data(to_delete)?, StaticFileSegment::Transactions => self @@ -248,6 +266,12 @@ impl StaticFileProviderRW { } if self.writer.is_dirty() { + debug!( + target: "provider::static_file", + segment = ?self.writer.user_header().segment(), + "Committing writer to disk" + ); + // Commits offsets and new user_header to disk self.writer.commit().map_err(ProviderError::other)?; @@ -264,7 +288,7 @@ impl StaticFileProviderRW { segment = ?self.writer.user_header().segment(), path = ?self.data_path, duration = ?start.elapsed(), - "Commit" + "Committed writer to disk" ); self.update_index()?; @@ -280,6 +304,12 @@ impl StaticFileProviderRW { pub fn commit_without_sync_all(&mut self) -> ProviderResult<()> { let start = Instant::now(); + debug!( + target: "provider::static_file", + segment = ?self.writer.user_header().segment(), + "Committing writer to disk (without sync)" + ); + // Commits offsets and new user_header to disk self.writer.commit_without_sync_all().map_err(ProviderError::other)?; @@ -296,7 +326,7 @@ impl StaticFileProviderRW { segment = ?self.writer.user_header().segment(), path = ?self.data_path, duration = ?start.elapsed(), - "Commit" + "Committed writer to disk (without sync)" ); self.update_index()?; diff --git a/crates/storage/provider/src/test_utils/blocks.rs b/crates/storage/provider/src/test_utils/blocks.rs index 0b27c5dc99..4a3e293992 100644 --- a/crates/storage/provider/src/test_utils/blocks.rs +++ b/crates/storage/provider/src/test_utils/blocks.rs @@ -45,8 +45,8 @@ pub fn assert_genesis_block( assert_eq!(tx.table::().unwrap(), vec![]); assert_eq!(tx.table::().unwrap(), vec![]); assert_eq!(tx.table::().unwrap(), vec![]); - // TODO check after this gets done: https://github.com/paradigmxyz/reth/issues/1588 - // Bytecodes are not reverted assert_eq!(tx.table::().unwrap(), vec![]); + // Reorged bytecodes are not reverted per https://github.com/paradigmxyz/reth/issues/1588 + // assert_eq!(tx.table::().unwrap(), vec![]); assert_eq!(tx.table::().unwrap(), vec![]); assert_eq!(tx.table::().unwrap(), vec![]); assert_eq!(tx.table::().unwrap(), vec![]); diff --git a/crates/tasks/src/lib.rs b/crates/tasks/src/lib.rs index de45c41e24..905c91e11b 100644 --- a/crates/tasks/src/lib.rs +++ b/crates/tasks/src/lib.rs @@ -635,7 +635,7 @@ impl TaskExecutor { impl TaskSpawner for TaskExecutor { fn spawn(&self, fut: BoxFuture<'static, ()>) -> JoinHandle<()> { self.metrics.inc_regular_tasks(); - self.spawn(fut) + Self::spawn(self, fut) } fn spawn_critical(&self, name: &'static str, fut: BoxFuture<'static, ()>) -> JoinHandle<()> { @@ -645,7 +645,7 @@ impl TaskSpawner for TaskExecutor { fn spawn_blocking(&self, fut: BoxFuture<'static, ()>) -> JoinHandle<()> { self.metrics.inc_regular_blocking_tasks(); - self.spawn_blocking(fut) + Self::spawn_blocking(self, fut) } fn spawn_critical_blocking( diff --git a/crates/transaction-pool/src/pool/events.rs b/crates/transaction-pool/src/pool/events.rs index f6bdd4a4d0..034c761612 100644 --- a/crates/transaction-pool/src/pool/events.rs +++ b/crates/transaction-pool/src/pool/events.rs @@ -83,7 +83,7 @@ impl TransactionEvent { /// Returns `true` if the event is final and no more events are expected for this transaction /// hash. pub const fn is_final(&self) -> bool { - matches!(self, Self::Replaced(_) | Self::Mined(_) | Self::Discarded) + matches!(self, Self::Replaced(_) | Self::Mined(_) | Self::Discarded | Self::Invalid) } } diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index 49247dc8b8..1b75188cd4 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -116,8 +116,6 @@ pub struct TxPool { all_transactions: AllTransactions, /// Transaction pool metrics metrics: TxPoolMetrics, - /// The last update kind that was applied to the pool. - latest_update_kind: Option, } // === impl TxPool === @@ -137,7 +135,6 @@ impl TxPool { all_transactions: AllTransactions::new(&config), config, metrics: Default::default(), - latest_update_kind: None, } } @@ -646,7 +643,7 @@ impl TxPool { block_info: BlockInfo, mined_transactions: Vec, changed_senders: FxHashMap, - update_kind: PoolUpdateKind, + _update_kind: PoolUpdateKind, ) -> OnNewCanonicalStateOutcome { // update block info let block_hash = block_info.last_seen_block_hash; @@ -682,9 +679,6 @@ impl TxPool { self.update_transaction_type_metrics(); self.metrics.performed_state_updates.increment(1); - // Update the latest update kind - self.latest_update_kind = Some(update_kind); - OnNewCanonicalStateOutcome { block_hash, mined: mined_transactions, diff --git a/crates/trie/common/src/hashed_state.rs b/crates/trie/common/src/hashed_state.rs index f3d8999e21..e0ce4da731 100644 --- a/crates/trie/common/src/hashed_state.rs +++ b/crates/trie/common/src/hashed_state.rs @@ -331,25 +331,44 @@ impl HashedPostState { } } - /// Converts hashed post state into [`HashedPostStateSorted`]. - pub fn into_sorted(mut self) -> HashedPostStateSorted { - self.drain_into_sorted() + /// Extend this hashed post state with sorted data, converting directly into the unsorted + /// `HashMap` representation. This is more efficient than first converting to `HashedPostState` + /// and then extending, as it avoids creating intermediate `HashMap` allocations. + pub fn extend_from_sorted(&mut self, sorted: &HashedPostStateSorted) { + // Reserve capacity for accounts + self.accounts.reserve(sorted.accounts.len()); + + // Insert accounts (Some = updated, None = destroyed) + for (address, account) in &sorted.accounts { + self.accounts.insert(*address, *account); + } + + // Reserve capacity for storages + self.storages.reserve(sorted.storages.len()); + + // Extend storages + for (hashed_address, sorted_storage) in &sorted.storages { + match self.storages.entry(*hashed_address) { + hash_map::Entry::Vacant(entry) => { + let mut new_storage = HashedStorage::new(false); + new_storage.extend_from_sorted(sorted_storage); + entry.insert(new_storage); + } + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().extend_from_sorted(sorted_storage); + } + } + } } - /// Converts hashed post state into [`HashedPostStateSorted`], but keeping the maps allocated by - /// draining. - /// - /// This effectively clears all the fields in the [`HashedPostStateSorted`]. - /// - /// This allows us to reuse the allocated space. This allocates new space for the sorted hashed - /// post state, like `into_sorted`. - pub fn drain_into_sorted(&mut self) -> HashedPostStateSorted { - let mut accounts: Vec<_> = self.accounts.drain().collect(); + /// Converts hashed post state into [`HashedPostStateSorted`]. + pub fn into_sorted(self) -> HashedPostStateSorted { + let mut accounts: Vec<_> = self.accounts.into_iter().collect(); accounts.sort_unstable_by_key(|(address, _)| *address); let storages = self .storages - .drain() + .into_iter() .map(|(hashed_address, storage)| (hashed_address, storage.into_sorted())) .collect(); @@ -423,6 +442,24 @@ impl HashedStorage { self.storage.extend(other.storage.iter().map(|(&k, &v)| (k, v))); } + /// Extend hashed storage with sorted data, converting directly into the unsorted `HashMap` + /// representation. This is more efficient than first converting to `HashedStorage` and + /// then extending, as it avoids creating intermediate `HashMap` allocations. + pub fn extend_from_sorted(&mut self, sorted: &HashedStorageSorted) { + if sorted.wiped { + self.wiped = true; + self.storage.clear(); + } + + // Reserve capacity for all slots + self.storage.reserve(sorted.storage_slots.len()); + + // Insert all storage slots + for (slot, value) in &sorted.storage_slots { + self.storage.insert(*slot, *value); + } + } + /// Converts hashed storage into [`HashedStorageSorted`]. pub fn into_sorted(self) -> HashedStorageSorted { let mut storage_slots: Vec<_> = self.storage.into_iter().collect(); @@ -484,6 +521,12 @@ impl HashedPostStateSorted { .or_insert_with(|| other_storage.clone()); } } + + /// Clears all accounts and storage data. + pub fn clear(&mut self) { + self.accounts.clear(); + self.storages.clear(); + } } impl AsRef for HashedPostStateSorted { @@ -1213,6 +1256,83 @@ mod tests { assert_eq!(storage3.storage_slots[1].1, U256::ZERO); } + /// Test extending with sorted accounts merges correctly into `HashMap` + #[test] + fn test_hashed_post_state_extend_from_sorted_with_accounts() { + let addr1 = B256::random(); + let addr2 = B256::random(); + + let mut state = HashedPostState::default(); + state.accounts.insert(addr1, Some(Default::default())); + + let mut sorted_state = HashedPostStateSorted::default(); + sorted_state.accounts.push((addr2, Some(Default::default()))); + + state.extend_from_sorted(&sorted_state); + + assert_eq!(state.accounts.len(), 2); + assert!(state.accounts.contains_key(&addr1)); + assert!(state.accounts.contains_key(&addr2)); + } + + /// Test destroyed accounts (None values) are inserted correctly + #[test] + fn test_hashed_post_state_extend_from_sorted_with_destroyed_accounts() { + let addr1 = B256::random(); + + let mut state = HashedPostState::default(); + + let mut sorted_state = HashedPostStateSorted::default(); + sorted_state.accounts.push((addr1, None)); + + state.extend_from_sorted(&sorted_state); + + assert!(state.accounts.contains_key(&addr1)); + assert_eq!(state.accounts.get(&addr1), Some(&None)); + } + + /// Test non-wiped storage merges both zero and non-zero valued slots + #[test] + fn test_hashed_storage_extend_from_sorted_non_wiped() { + let slot1 = B256::random(); + let slot2 = B256::random(); + let slot3 = B256::random(); + + let mut storage = HashedStorage::from_iter(false, [(slot1, U256::from(100))]); + + let sorted = HashedStorageSorted { + storage_slots: vec![(slot2, U256::from(200)), (slot3, U256::ZERO)], + wiped: false, + }; + + storage.extend_from_sorted(&sorted); + + assert!(!storage.wiped); + assert_eq!(storage.storage.len(), 3); + assert_eq!(storage.storage.get(&slot1), Some(&U256::from(100))); + assert_eq!(storage.storage.get(&slot2), Some(&U256::from(200))); + assert_eq!(storage.storage.get(&slot3), Some(&U256::ZERO)); + } + + /// Test wiped=true clears existing storage and only keeps new slots (critical edge case) + #[test] + fn test_hashed_storage_extend_from_sorted_wiped() { + let slot1 = B256::random(); + let slot2 = B256::random(); + + let mut storage = HashedStorage::from_iter(false, [(slot1, U256::from(100))]); + + let sorted = + HashedStorageSorted { storage_slots: vec![(slot2, U256::from(200))], wiped: true }; + + storage.extend_from_sorted(&sorted); + + assert!(storage.wiped); + // After wipe, old storage should be cleared and only new storage remains + assert_eq!(storage.storage.len(), 1); + assert_eq!(storage.storage.get(&slot2), Some(&U256::from(200))); + } + #[test] fn test_hashed_post_state_chunking_length() { let addr1 = B256::from([1; 32]); diff --git a/crates/trie/common/src/input.rs b/crates/trie/common/src/input.rs index 522cfa9ed4..89f7811a3a 100644 --- a/crates/trie/common/src/input.rs +++ b/crates/trie/common/src/input.rs @@ -1,4 +1,9 @@ -use crate::{prefix_set::TriePrefixSetsMut, updates::TrieUpdates, HashedPostState}; +use crate::{ + prefix_set::TriePrefixSetsMut, + updates::{TrieUpdates, TrieUpdatesSorted}, + HashedPostState, HashedPostStateSorted, +}; +use alloc::sync::Arc; /// Inputs for trie-related computations. #[derive(Default, Debug, Clone)] @@ -41,6 +46,20 @@ impl TrieInput { input } + /// Create new trie input from the provided sorted blocks, from oldest to newest. + /// Converts sorted types to unsorted for aggregation. + pub fn from_blocks_sorted<'a>( + blocks: impl IntoIterator, + ) -> Self { + let mut input = Self::default(); + for (hashed_state, trie_updates) in blocks { + // Extend directly from sorted types, avoiding intermediate HashMap allocations + input.nodes.extend_from_sorted(trie_updates); + input.state.extend_from_sorted(hashed_state); + } + input + } + /// Extend the trie input with the provided blocks, from oldest to newest. /// /// For blocks with missing trie updates, the trie input will be extended with prefix sets @@ -119,3 +138,40 @@ impl TrieInput { self } } + +/// Sorted variant of [`TrieInput`] for efficient proof generation. +/// +/// This type holds sorted versions of trie data structures, which eliminates the need +/// for expensive sorting operations during multiproof generation. +#[derive(Default, Debug, Clone)] +pub struct TrieInputSorted { + /// Sorted cached in-memory intermediate trie nodes. + pub nodes: Arc, + /// Sorted in-memory overlay hashed state. + pub state: Arc, + /// Prefix sets for computation. + pub prefix_sets: TriePrefixSetsMut, +} + +impl TrieInputSorted { + /// Create new sorted trie input. + pub const fn new( + nodes: Arc, + state: Arc, + prefix_sets: TriePrefixSetsMut, + ) -> Self { + Self { nodes, state, prefix_sets } + } + + /// Create from unsorted [`TrieInput`] by sorting. + pub fn from_unsorted(input: TrieInput) -> Self { + Self { + nodes: Arc::new(input.nodes.into_sorted()), + state: Arc::new(input.state.into_sorted()), + prefix_sets: input.prefix_sets, + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/crates/trie/common/src/lib.rs b/crates/trie/common/src/lib.rs index e4292a5201..f212dd2910 100644 --- a/crates/trie/common/src/lib.rs +++ b/crates/trie/common/src/lib.rs @@ -17,7 +17,7 @@ pub use hashed_state::*; /// Input for trie computation. mod input; -pub use input::TrieInput; +pub use input::{TrieInput, TrieInputSorted}; /// The implementation of hash builder. pub mod hash_builder; @@ -41,6 +41,9 @@ pub use storage::{StorageTrieEntry, TrieChangeSetsEntry}; mod subnode; pub use subnode::StoredSubNode; +mod trie; +pub use trie::{ProofTrieNode, TrieMasks}; + /// The implementation of a container for storing intermediate changes to a trie. /// The container indicates when the trie has been modified. pub mod prefix_set; diff --git a/crates/trie/common/src/trie.rs b/crates/trie/common/src/trie.rs new file mode 100644 index 0000000000..8794839301 --- /dev/null +++ b/crates/trie/common/src/trie.rs @@ -0,0 +1,45 @@ +//! Types related to sparse trie nodes and masks. + +use crate::Nibbles; +use alloy_trie::{nodes::TrieNode, TrieMask}; + +/// 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 } + } +} + +/// Carries all information needed by a sparse trie to reveal a particular node. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProofTrieNode { + /// 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/trie/common/src/updates.rs b/crates/trie/common/src/updates.rs index b0d178cd1d..f1db882781 100644 --- a/crates/trie/common/src/updates.rs +++ b/crates/trie/common/src/updates.rs @@ -73,6 +73,44 @@ impl TrieUpdates { self.account_nodes.retain(|nibbles, _| !other.removed_nodes.contains(nibbles)); } + /// Extend trie updates with sorted data, converting directly into the unsorted `HashMap` + /// representation. This is more efficient than first converting to `TrieUpdates` and + /// then extending, as it avoids creating intermediate `HashMap` allocations. + /// + /// This top-level helper merges account nodes and delegates each account's storage trie to + /// [`StorageTrieUpdates::extend_from_sorted`]. + pub fn extend_from_sorted(&mut self, sorted: &TrieUpdatesSorted) { + // Reserve capacity for account nodes + let new_nodes_count = sorted.account_nodes.len(); + self.account_nodes.reserve(new_nodes_count); + + // Insert account nodes from sorted (only non-None entries) + for (nibbles, maybe_node) in &sorted.account_nodes { + if nibbles.is_empty() { + continue; + } + match maybe_node { + Some(node) => { + self.removed_nodes.remove(nibbles); + self.account_nodes.insert(*nibbles, node.clone()); + } + None => { + self.account_nodes.remove(nibbles); + self.removed_nodes.insert(*nibbles); + } + } + } + + // Extend storage tries + self.storage_tries.reserve(sorted.storage_tries.len()); + for (hashed_address, sorted_storage) in &sorted.storage_tries { + self.storage_tries + .entry(*hashed_address) + .or_default() + .extend_from_sorted(sorted_storage); + } + } + /// Insert storage updates for a given hashed address. pub fn insert_storage_updates( &mut self, @@ -108,17 +146,6 @@ impl TrieUpdates { /// Converts trie updates into [`TrieUpdatesSorted`]. pub fn into_sorted(mut self) -> TrieUpdatesSorted { - self.drain_into_sorted() - } - - /// Converts trie updates into [`TrieUpdatesSorted`], but keeping the maps allocated by - /// draining. - /// - /// This effectively clears all the fields in the [`TrieUpdatesSorted`]. - /// - /// This allows us to reuse the allocated space. This allocates new space for the sorted - /// updates, like `into_sorted`. - pub fn drain_into_sorted(&mut self) -> TrieUpdatesSorted { let mut account_nodes = self .account_nodes .drain() @@ -253,6 +280,38 @@ impl StorageTrieUpdates { self.storage_nodes.retain(|nibbles, _| !other.removed_nodes.contains(nibbles)); } + /// Extend storage trie updates with sorted data, converting directly into the unsorted + /// `HashMap` representation. This is more efficient than first converting to + /// `StorageTrieUpdates` and then extending, as it avoids creating intermediate `HashMap` + /// allocations. + /// + /// This is invoked from [`TrieUpdates::extend_from_sorted`] for each account. + pub fn extend_from_sorted(&mut self, sorted: &StorageTrieUpdatesSorted) { + if sorted.is_deleted { + self.storage_nodes.clear(); + self.removed_nodes.clear(); + } + self.is_deleted |= sorted.is_deleted; + + // Reserve capacity for storage nodes + let new_nodes_count = sorted.storage_nodes.len(); + self.storage_nodes.reserve(new_nodes_count); + + // Remove nodes marked as removed and insert new nodes + for (nibbles, maybe_node) in &sorted.storage_nodes { + if nibbles.is_empty() { + continue; + } + if let Some(node) = maybe_node { + self.removed_nodes.remove(nibbles); + self.storage_nodes.insert(*nibbles, node.clone()); + } else { + self.storage_nodes.remove(nibbles); + self.removed_nodes.insert(*nibbles); + } + } + } + /// Finalize storage trie updates for by taking updates from walker and hash builder. pub fn finalize(&mut self, hash_builder: HashBuilder, removed_keys: HashSet) { // Retrieve updated nodes from hash builder. @@ -499,6 +558,12 @@ impl TrieUpdatesSorted { .or_insert_with(|| storage_trie.clone()); } } + + /// Clears all account nodes and storage tries. + pub fn clear(&mut self) { + self.account_nodes.clear(); + self.storage_tries.clear(); + } } impl AsRef for TrieUpdatesSorted { @@ -749,6 +814,151 @@ mod tests { assert_eq!(storage3.storage_nodes[0].0, Nibbles::from_nibbles_unchecked([0x06])); assert_eq!(storage3.storage_nodes[1].0, Nibbles::from_nibbles_unchecked([0x07])); } + + /// Test extending with storage tries adds both nodes and removed nodes correctly + #[test] + fn test_trie_updates_extend_from_sorted_with_storage_tries() { + let hashed_address = B256::from([1; 32]); + + let mut updates = TrieUpdates::default(); + + let storage_trie = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x0a]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x0b]), None), + ], + }; + + let sorted = TrieUpdatesSorted { + account_nodes: vec![], + storage_tries: B256Map::from_iter([(hashed_address, storage_trie)]), + }; + + updates.extend_from_sorted(&sorted); + + assert_eq!(updates.storage_tries.len(), 1); + let storage = updates.storage_tries.get(&hashed_address).unwrap(); + assert!(!storage.is_deleted); + assert_eq!(storage.storage_nodes.len(), 1); + assert!(storage.removed_nodes.contains(&Nibbles::from_nibbles_unchecked([0x0b]))); + } + + /// Test deleted=true clears old storage nodes before adding new ones (critical edge case) + #[test] + fn test_trie_updates_extend_from_sorted_with_deleted_storage() { + let hashed_address = B256::from([1; 32]); + + let mut updates = TrieUpdates::default(); + updates.storage_tries.insert( + hashed_address, + StorageTrieUpdates { + is_deleted: false, + storage_nodes: HashMap::from_iter([( + Nibbles::from_nibbles_unchecked([0x01]), + BranchNodeCompact::default(), + )]), + removed_nodes: Default::default(), + }, + ); + + let storage_trie = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![( + Nibbles::from_nibbles_unchecked([0x0a]), + Some(BranchNodeCompact::default()), + )], + }; + + let sorted = TrieUpdatesSorted { + account_nodes: vec![], + storage_tries: B256Map::from_iter([(hashed_address, storage_trie)]), + }; + + updates.extend_from_sorted(&sorted); + + let storage = updates.storage_tries.get(&hashed_address).unwrap(); + assert!(storage.is_deleted); + // After deletion, old nodes should be cleared + assert_eq!(storage.storage_nodes.len(), 1); + assert!(storage.storage_nodes.contains_key(&Nibbles::from_nibbles_unchecked([0x0a]))); + } + + /// Test non-deleted storage merges nodes and tracks removed nodes + #[test] + fn test_storage_trie_updates_extend_from_sorted_non_deleted() { + let mut storage = StorageTrieUpdates { + is_deleted: false, + storage_nodes: HashMap::from_iter([( + Nibbles::from_nibbles_unchecked([0x01]), + BranchNodeCompact::default(), + )]), + removed_nodes: Default::default(), + }; + + let sorted = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x02]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x03]), None), + ], + }; + + storage.extend_from_sorted(&sorted); + + assert!(!storage.is_deleted); + assert_eq!(storage.storage_nodes.len(), 2); + assert!(storage.removed_nodes.contains(&Nibbles::from_nibbles_unchecked([0x03]))); + } + + /// Test deleted=true clears old nodes before extending (edge case) + #[test] + fn test_storage_trie_updates_extend_from_sorted_deleted() { + let mut storage = StorageTrieUpdates { + is_deleted: false, + storage_nodes: HashMap::from_iter([( + Nibbles::from_nibbles_unchecked([0x01]), + BranchNodeCompact::default(), + )]), + removed_nodes: Default::default(), + }; + + let sorted = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![( + Nibbles::from_nibbles_unchecked([0x0a]), + Some(BranchNodeCompact::default()), + )], + }; + + storage.extend_from_sorted(&sorted); + + assert!(storage.is_deleted); + // Old nodes should be cleared when deleted + assert_eq!(storage.storage_nodes.len(), 1); + assert!(storage.storage_nodes.contains_key(&Nibbles::from_nibbles_unchecked([0x0a]))); + } + + /// Test empty nibbles are filtered out during conversion (edge case bug) + #[test] + fn test_trie_updates_extend_from_sorted_filters_empty_nibbles() { + let mut updates = TrieUpdates::default(); + + let sorted = TrieUpdatesSorted { + account_nodes: vec![ + (Nibbles::default(), Some(BranchNodeCompact::default())), // Empty nibbles + (Nibbles::from_nibbles_unchecked([0x01]), Some(BranchNodeCompact::default())), + ], + storage_tries: B256Map::default(), + }; + + updates.extend_from_sorted(&sorted); + + // Empty nibbles should be filtered out + assert_eq!(updates.account_nodes.len(), 1); + assert!(updates.account_nodes.contains_key(&Nibbles::from_nibbles_unchecked([0x01]))); + assert!(!updates.account_nodes.contains_key(&Nibbles::default())); + } } /// Bincode-compatible trie updates type serde implementations. diff --git a/crates/trie/db/src/hashed_cursor.rs b/crates/trie/db/src/hashed_cursor.rs index 4fe3d57429..10a1fd8363 100644 --- a/crates/trie/db/src/hashed_cursor.rs +++ b/crates/trie/db/src/hashed_cursor.rs @@ -69,6 +69,10 @@ where fn next(&mut self) -> Result, DatabaseError> { self.0.next() } + + fn reset(&mut self) { + // Database cursors are stateless, no reset needed + } } /// The structure wrapping a database cursor for hashed storage and @@ -102,6 +106,10 @@ where fn next(&mut self) -> Result, DatabaseError> { Ok(self.cursor.next_dup_val()?.map(|e| (e.key, e.value))) } + + fn reset(&mut self) { + // Database cursors are stateless, no reset needed + } } impl HashedStorageCursor for DatabaseHashedStorageCursor @@ -111,4 +119,8 @@ where fn is_storage_empty(&mut self) -> Result { Ok(self.cursor.seek_exact(self.hashed_address)?.is_none()) } + + fn set_hashed_address(&mut self, hashed_address: B256) { + self.hashed_address = hashed_address; + } } diff --git a/crates/trie/db/src/proof.rs b/crates/trie/db/src/proof.rs index 8f79c21c15..feb5d358ca 100644 --- a/crates/trie/db/src/proof.rs +++ b/crates/trie/db/src/proof.rs @@ -97,7 +97,11 @@ pub trait DatabaseStorageProof<'a, TX> { } impl<'a, TX: DbTx> DatabaseStorageProof<'a, TX> - for StorageProof, DatabaseHashedCursorFactory<&'a TX>> + for StorageProof< + 'static, + DatabaseTrieCursorFactory<&'a TX>, + DatabaseHashedCursorFactory<&'a TX>, + > { fn from_tx(tx: &'a TX, address: Address) -> Self { Self::new(DatabaseTrieCursorFactory::new(tx), DatabaseHashedCursorFactory::new(tx), address) diff --git a/crates/trie/db/src/trie_cursor.rs b/crates/trie/db/src/trie_cursor.rs index d05c3fd92d..7b9c402545 100644 --- a/crates/trie/db/src/trie_cursor.rs +++ b/crates/trie/db/src/trie_cursor.rs @@ -6,7 +6,7 @@ use reth_db_api::{ DatabaseError, }; use reth_trie::{ - trie_cursor::{TrieCursor, TrieCursorFactory}, + trie_cursor::{TrieCursor, TrieCursorFactory, TrieStorageCursor}, updates::StorageTrieUpdatesSorted, BranchNodeCompact, Nibbles, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, }; @@ -91,6 +91,10 @@ where fn current(&mut self) -> Result, DatabaseError> { Ok(self.0.current()?.map(|(k, _)| k.0)) } + + fn reset(&mut self) { + // No-op for database cursors + } } /// A cursor over the storage tries stored in the database. @@ -190,6 +194,19 @@ where fn current(&mut self) -> Result, DatabaseError> { Ok(self.cursor.current()?.map(|(_, v)| v.nibbles.0)) } + + fn reset(&mut self) { + // No-op for database cursors + } +} + +impl TrieStorageCursor for DatabaseStorageTrieCursor +where + C: DbCursorRO + DbDupCursorRO + Send + Sync, +{ + fn set_hashed_address(&mut self, hashed_address: B256) { + self.hashed_address = hashed_address; + } } #[cfg(test)] diff --git a/crates/trie/parallel/src/proof_task.rs b/crates/trie/parallel/src/proof_task.rs index 07822d8251..58dc99fc37 100644 --- a/crates/trie/parallel/src/proof_task.rs +++ b/crates/trie/parallel/src/proof_task.rs @@ -45,11 +45,11 @@ use reth_execution_errors::{SparseTrieError, SparseTrieErrorKind}; use reth_provider::{DatabaseProviderROFactory, ProviderError, ProviderResult}; use reth_storage_errors::db::DatabaseError; use reth_trie::{ - hashed_cursor::HashedCursorFactory, + hashed_cursor::{HashedCursorFactory, HashedCursorMetricsCache, InstrumentedHashedCursor}, node_iter::{TrieElement, TrieNodeIter}, prefix_set::TriePrefixSets, proof::{ProofBlindedAccountProvider, ProofBlindedStorageProvider, StorageProof}, - trie_cursor::TrieCursorFactory, + trie_cursor::{InstrumentedTrieCursor, TrieCursorFactory, TrieCursorMetricsCache}, walker::TrieWalker, DecodedMultiProof, DecodedStorageMultiProof, HashBuilder, HashedPostState, MultiProofTargets, Nibbles, TRIE_ACCOUNT_RLP_MAX_SIZE, @@ -72,7 +72,9 @@ use tokio::runtime::Handle; use tracing::{debug, debug_span, error, trace}; #[cfg(feature = "metrics")] -use crate::proof_task_metrics::ProofTaskTrieMetrics; +use crate::proof_task_metrics::{ + ProofTaskCursorMetrics, ProofTaskCursorMetricsCache, ProofTaskTrieMetrics, +}; type StorageProofResult = Result; type TrieNodeProviderResult = Result, SparseTrieError>; @@ -151,6 +153,8 @@ impl ProofWorkerHandle { executor.spawn_blocking(move || { #[cfg(feature = "metrics")] let metrics = ProofTaskTrieMetrics::default(); + #[cfg(feature = "metrics")] + let cursor_metrics = ProofTaskCursorMetrics::new(); let _guard = span.enter(); let worker = StorageProofWorker::new( @@ -160,6 +164,8 @@ impl ProofWorkerHandle { storage_available_workers_clone, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + cursor_metrics, ); if let Err(error) = worker.run() { error!( @@ -187,6 +193,8 @@ impl ProofWorkerHandle { executor.spawn_blocking(move || { #[cfg(feature = "metrics")] let metrics = ProofTaskTrieMetrics::default(); + #[cfg(feature = "metrics")] + let cursor_metrics = ProofTaskCursorMetrics::new(); let _guard = span.enter(); let worker = AccountProofWorker::new( @@ -197,6 +205,8 @@ impl ProofWorkerHandle { account_available_workers_clone, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + cursor_metrics, ); if let Err(error) = worker.run() { error!( @@ -406,7 +416,12 @@ where /// /// Used by storage workers in the worker pool to compute storage proofs. #[inline] - fn compute_storage_proof(&self, input: StorageProofInput) -> StorageProofResult { + fn compute_storage_proof( + &self, + input: StorageProofInput, + trie_cursor_metrics: &mut TrieCursorMetricsCache, + hashed_cursor_metrics: &mut HashedCursorMetricsCache, + ) -> StorageProofResult { // Consume the input so we can move large collections (e.g. target slots) without cloning. let StorageProofInput { hashed_address, @@ -438,8 +453,12 @@ where .with_prefix_set_mut(PrefixSetMut::from(prefix_set.iter().copied())) .with_branch_node_masks(with_branch_node_masks) .with_added_removed_keys(added_removed_keys) + .with_trie_cursor_metrics(trie_cursor_metrics) + .with_hashed_cursor_metrics(hashed_cursor_metrics) .storage_multiproof(target_slots) .map_err(|e| ParallelStateRootError::Other(e.to_string())); + trie_cursor_metrics.record_span("trie_cursor"); + hashed_cursor_metrics.record_span("hashed_cursor"); // Decode proof into DecodedStorageMultiProof let decoded_result = raw_proof_result.and_then(|raw_proof| { @@ -651,6 +670,9 @@ struct StorageProofWorker { /// Metrics collector for this worker #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics, + /// Cursor metrics for this worker + #[cfg(feature = "metrics")] + cursor_metrics: ProofTaskCursorMetrics, } impl StorageProofWorker @@ -664,6 +686,7 @@ where worker_id: usize, available_workers: Arc, #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics, + #[cfg(feature = "metrics")] cursor_metrics: ProofTaskCursorMetrics, ) -> Self { Self { task_ctx, @@ -672,6 +695,8 @@ where available_workers, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + cursor_metrics, } } @@ -692,7 +717,7 @@ where /// /// If this function panics, the worker thread terminates but other workers /// continue operating and the system degrades gracefully. - fn run(self) -> ProviderResult<()> { + fn run(mut self) -> ProviderResult<()> { let Self { task_ctx, work_rx, @@ -700,6 +725,8 @@ where available_workers, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + ref mut cursor_metrics, } = self; // Create provider from factory @@ -714,6 +741,7 @@ where let mut storage_proofs_processed = 0u64; let mut storage_nodes_processed = 0u64; + let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default(); // Initially mark this worker as available. available_workers.fetch_add(1, Ordering::Relaxed); @@ -730,6 +758,7 @@ where input, proof_result_sender, &mut storage_proofs_processed, + &mut cursor_metrics_cache, ); } @@ -758,7 +787,10 @@ where ); #[cfg(feature = "metrics")] - metrics.record_storage_nodes(storage_nodes_processed as usize); + { + metrics.record_storage_nodes(storage_nodes_processed as usize); + cursor_metrics.record(&mut cursor_metrics_cache); + } Ok(()) } @@ -770,6 +802,7 @@ where input: StorageProofInput, proof_result_sender: ProofResultContext, storage_proofs_processed: &mut u64, + cursor_metrics_cache: &mut ProofTaskCursorMetricsCache, ) where Provider: TrieCursorFactory + HashedCursorFactory, { @@ -777,6 +810,9 @@ where let ProofResultContext { sender, sequence_number: seq, state, start_time } = proof_result_sender; + let mut trie_cursor_metrics = TrieCursorMetricsCache::default(); + let mut hashed_cursor_metrics = HashedCursorMetricsCache::default(); + trace!( target: "trie::proof_task", worker_id, @@ -787,7 +823,11 @@ where ); let proof_start = Instant::now(); - let result = proof_tx.compute_storage_proof(input); + let result = proof_tx.compute_storage_proof( + input, + &mut trie_cursor_metrics, + &mut hashed_cursor_metrics, + ); let proof_elapsed = proof_start.elapsed(); *storage_proofs_processed += 1; @@ -821,8 +861,24 @@ where hashed_address = ?hashed_address, proof_time_us = proof_elapsed.as_micros(), total_processed = storage_proofs_processed, + trie_cursor_duration_us = trie_cursor_metrics.total_duration.as_micros(), + hashed_cursor_duration_us = hashed_cursor_metrics.total_duration.as_micros(), + ?trie_cursor_metrics, + ?hashed_cursor_metrics, "Storage proof completed" ); + + #[cfg(feature = "metrics")] + { + // Accumulate per-proof metrics into the worker's cache + let per_proof_cache = ProofTaskCursorMetricsCache { + account_trie_cursor: TrieCursorMetricsCache::default(), + account_hashed_cursor: HashedCursorMetricsCache::default(), + storage_trie_cursor: trie_cursor_metrics, + storage_hashed_cursor: hashed_cursor_metrics, + }; + cursor_metrics_cache.extend(&per_proof_cache); + } } /// Processes a blinded storage node lookup request. @@ -891,6 +947,9 @@ struct AccountProofWorker { /// Metrics collector for this worker #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics, + /// Cursor metrics for this worker + #[cfg(feature = "metrics")] + cursor_metrics: ProofTaskCursorMetrics, } impl AccountProofWorker @@ -905,6 +964,7 @@ where storage_work_tx: CrossbeamSender, available_workers: Arc, #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics, + #[cfg(feature = "metrics")] cursor_metrics: ProofTaskCursorMetrics, ) -> Self { Self { task_ctx, @@ -914,6 +974,8 @@ where available_workers, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + cursor_metrics, } } @@ -934,7 +996,7 @@ where /// /// If this function panics, the worker thread terminates but other workers /// continue operating and the system degrades gracefully. - fn run(self) -> ProviderResult<()> { + fn run(mut self) -> ProviderResult<()> { let Self { task_ctx, work_rx, @@ -943,6 +1005,8 @@ where available_workers, #[cfg(feature = "metrics")] metrics, + #[cfg(feature = "metrics")] + ref mut cursor_metrics, } = self; // Create provider from factory @@ -957,6 +1021,7 @@ where let mut account_proofs_processed = 0u64; let mut account_nodes_processed = 0u64; + let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default(); // Count this worker as available only after successful initialization. available_workers.fetch_add(1, Ordering::Relaxed); @@ -973,6 +1038,7 @@ where storage_work_tx.clone(), *input, &mut account_proofs_processed, + &mut cursor_metrics_cache, ); } @@ -1000,7 +1066,10 @@ where ); #[cfg(feature = "metrics")] - metrics.record_account_nodes(account_nodes_processed as usize); + { + metrics.record_account_nodes(account_nodes_processed as usize); + cursor_metrics.record(&mut cursor_metrics_cache); + } Ok(()) } @@ -1012,6 +1081,7 @@ where storage_work_tx: CrossbeamSender, input: AccountMultiproofInput, account_proofs_processed: &mut u64, + cursor_metrics_cache: &mut ProofTaskCursorMetricsCache, ) where Provider: TrieCursorFactory + HashedCursorFactory, { @@ -1087,6 +1157,9 @@ where let proof_elapsed = proof_start.elapsed(); let total_elapsed = start.elapsed(); + let proof_cursor_metrics = tracker.cursor_metrics; + proof_cursor_metrics.record_spans(); + let stats = tracker.finish(); let result = result.map(|proof| ProofResult::AccountMultiproof { proof, stats }); *account_proofs_processed += 1; @@ -1114,8 +1187,20 @@ where proof_time_us = proof_elapsed.as_micros(), total_elapsed_us = total_elapsed.as_micros(), total_processed = account_proofs_processed, + account_trie_cursor_duration_us = proof_cursor_metrics.account_trie_cursor.total_duration.as_micros(), + account_hashed_cursor_duration_us = proof_cursor_metrics.account_hashed_cursor.total_duration.as_micros(), + storage_trie_cursor_duration_us = proof_cursor_metrics.storage_trie_cursor.total_duration.as_micros(), + storage_hashed_cursor_duration_us = proof_cursor_metrics.storage_hashed_cursor.total_duration.as_micros(), + account_trie_cursor_metrics = ?proof_cursor_metrics.account_trie_cursor, + account_hashed_cursor_metrics = ?proof_cursor_metrics.account_hashed_cursor, + storage_trie_cursor_metrics = ?proof_cursor_metrics.storage_trie_cursor, + storage_hashed_cursor_metrics = ?proof_cursor_metrics.storage_hashed_cursor, "Account multiproof completed" ); + + #[cfg(feature = "metrics")] + // Accumulate per-proof metrics into the worker's cache + cursor_metrics_cache.extend(&proof_cursor_metrics); } /// Processes a blinded account node lookup request. @@ -1184,13 +1269,20 @@ where let accounts_added_removed_keys = ctx.multi_added_removed_keys.as_ref().map(|keys| keys.get_accounts()); + // Create local metrics caches for account cursors. We can't directly use the metrics caches in + // the tracker due to the call to `inc_missed_leaves` which occurs on it. + let mut account_trie_cursor_metrics = TrieCursorMetricsCache::default(); + let mut account_hashed_cursor_metrics = HashedCursorMetricsCache::default(); + + // Wrap account trie cursor with instrumented cursor + let account_trie_cursor = provider.account_trie_cursor().map_err(ProviderError::Database)?; + let account_trie_cursor = + InstrumentedTrieCursor::new(account_trie_cursor, &mut account_trie_cursor_metrics); + // Create the walker. - let walker = TrieWalker::<_>::state_trie( - provider.account_trie_cursor().map_err(ProviderError::Database)?, - ctx.prefix_set, - ) - .with_added_removed_keys(accounts_added_removed_keys) - .with_deletions_retained(true); + let walker = TrieWalker::<_>::state_trie(account_trie_cursor, ctx.prefix_set) + .with_added_removed_keys(accounts_added_removed_keys) + .with_deletions_retained(true); // Create a hash builder to rebuild the root node since it is not available in the database. let retainer = ctx @@ -1208,10 +1300,14 @@ where let mut collected_decoded_storages: B256Map = B256Map::with_capacity_and_hasher(ctx.targets.len(), Default::default()); let mut account_rlp = Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE); - let mut account_node_iter = TrieNodeIter::state_trie( - walker, - provider.hashed_account_cursor().map_err(ProviderError::Database)?, - ); + + // Wrap account hashed cursor with instrumented cursor + let account_hashed_cursor = + provider.hashed_account_cursor().map_err(ProviderError::Database)?; + let account_hashed_cursor = + InstrumentedHashedCursor::new(account_hashed_cursor, &mut account_hashed_cursor_metrics); + + let mut account_node_iter = TrieNodeIter::state_trie(walker, account_hashed_cursor); let mut storage_proof_receivers = ctx.storage_proof_receivers; @@ -1223,6 +1319,11 @@ where TrieElement::Leaf(hashed_address, account) => { let root = match storage_proof_receivers.remove(&hashed_address) { Some(receiver) => { + let _guard = debug_span!( + target: "trie::proof_task", + "Waiting for storage proof", + ?hashed_address, + ); // Block on this specific storage proof receiver - enables interleaved // parallelism let proof_msg = receiver.recv().map_err(|_| { @@ -1235,6 +1336,8 @@ where ) })?; + drop(_guard); + // Extract storage proof from the result let proof = match proof_msg.result? { ProofResult::StorageProof { hashed_address: addr, proof } => { @@ -1265,6 +1368,12 @@ where let root = StorageProof::new_hashed(provider, provider, hashed_address) .with_prefix_set_mut(Default::default()) + .with_trie_cursor_metrics( + &mut tracker.cursor_metrics.storage_trie_cursor, + ) + .with_hashed_cursor_metrics( + &mut tracker.cursor_metrics.storage_hashed_cursor, + ) .storage_multiproof( ctx.targets .get(&hashed_address) @@ -1322,6 +1431,10 @@ where (Default::default(), Default::default()) }; + // Extend tracker with accumulated metrics from account cursors + tracker.cursor_metrics.account_trie_cursor.extend(&account_trie_cursor_metrics); + tracker.cursor_metrics.account_hashed_cursor.extend(&account_hashed_cursor_metrics); + Ok(DecodedMultiProof { account_subtree: decoded_account_subtree, branch_node_hash_masks, diff --git a/crates/trie/parallel/src/proof_task_metrics.rs b/crates/trie/parallel/src/proof_task_metrics.rs index 6492e28d12..f9b8d70c16 100644 --- a/crates/trie/parallel/src/proof_task_metrics.rs +++ b/crates/trie/parallel/src/proof_task_metrics.rs @@ -1,4 +1,9 @@ use reth_metrics::{metrics::Histogram, Metrics}; +use reth_trie::{ + hashed_cursor::{HashedCursorMetrics, HashedCursorMetricsCache}, + trie_cursor::{TrieCursorMetrics, TrieCursorMetricsCache}, + TrieType, +}; /// Metrics for the proof task. #[derive(Clone, Metrics)] @@ -21,3 +26,87 @@ impl ProofTaskTrieMetrics { self.blinded_storage_nodes.record(count as f64); } } + +/// Cursor metrics for proof task operations. +#[derive(Clone, Debug)] +pub struct ProofTaskCursorMetrics { + /// Metrics for account trie cursor operations. + pub account_trie_cursor: TrieCursorMetrics, + /// Metrics for account hashed cursor operations. + pub account_hashed_cursor: HashedCursorMetrics, + /// Metrics for storage trie cursor operations. + pub storage_trie_cursor: TrieCursorMetrics, + /// Metrics for storage hashed cursor operations. + pub storage_hashed_cursor: HashedCursorMetrics, +} + +impl ProofTaskCursorMetrics { + /// Create a new instance with properly initialized cursor metrics. + pub fn new() -> Self { + Self { + account_trie_cursor: TrieCursorMetrics::new(TrieType::State), + account_hashed_cursor: HashedCursorMetrics::new(TrieType::State), + storage_trie_cursor: TrieCursorMetrics::new(TrieType::Storage), + storage_hashed_cursor: HashedCursorMetrics::new(TrieType::Storage), + } + } + + /// Record the cached metrics from the provided cache and reset the cache counters. + /// + /// This method adds the current counter values from the cache to the Prometheus metrics + /// and then resets all cache counters to zero. + pub fn record(&mut self, cache: &mut ProofTaskCursorMetricsCache) { + self.account_trie_cursor.record(&mut cache.account_trie_cursor); + self.account_hashed_cursor.record(&mut cache.account_hashed_cursor); + self.storage_trie_cursor.record(&mut cache.storage_trie_cursor); + self.storage_hashed_cursor.record(&mut cache.storage_hashed_cursor); + cache.reset(); + } +} + +impl Default for ProofTaskCursorMetrics { + fn default() -> Self { + Self::new() + } +} + +/// Cached cursor metrics for proof task operations. +#[derive(Clone, Debug, Default, Copy)] +pub struct ProofTaskCursorMetricsCache { + /// Cached metrics for account trie cursor operations. + pub account_trie_cursor: TrieCursorMetricsCache, + /// Cached metrics for account hashed cursor operations. + pub account_hashed_cursor: HashedCursorMetricsCache, + /// Cached metrics for storage trie cursor operations. + pub storage_trie_cursor: TrieCursorMetricsCache, + /// Cached metrics for storage hashed cursor operations. + pub storage_hashed_cursor: HashedCursorMetricsCache, +} + +impl ProofTaskCursorMetricsCache { + /// Extend this cache by adding the counts from another cache. + /// + /// This accumulates the counter values from `other` into this cache. + pub fn extend(&mut self, other: &Self) { + self.account_trie_cursor.extend(&other.account_trie_cursor); + self.account_hashed_cursor.extend(&other.account_hashed_cursor); + self.storage_trie_cursor.extend(&other.storage_trie_cursor); + self.storage_hashed_cursor.extend(&other.storage_hashed_cursor); + } + + /// Reset all counters to zero. + pub const fn reset(&mut self) { + self.account_trie_cursor.reset(); + self.account_hashed_cursor.reset(); + self.storage_trie_cursor.reset(); + self.storage_hashed_cursor.reset(); + } + + /// Record the spans for metrics. + pub fn record_spans(&self) { + self.account_trie_cursor.record_span("account_trie_cursor"); + self.account_hashed_cursor.record_span("account_hashed_cursor"); + self.storage_trie_cursor.record_span("storage_trie_cursor"); + self.storage_hashed_cursor.record_span("storage_hashed_cursor"); + } +} diff --git a/crates/trie/parallel/src/stats.rs b/crates/trie/parallel/src/stats.rs index de5b0a628e..088b95c970 100644 --- a/crates/trie/parallel/src/stats.rs +++ b/crates/trie/parallel/src/stats.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "metrics")] +use crate::proof_task_metrics::ProofTaskCursorMetricsCache; use derive_more::Deref; use reth_trie::stats::{TrieStats, TrieTracker}; @@ -34,6 +36,9 @@ pub struct ParallelTrieTracker { trie: TrieTracker, precomputed_storage_roots: u64, missed_leaves: u64, + #[cfg(feature = "metrics")] + /// Local tracking of cursor-related metrics + pub cursor_metrics: ProofTaskCursorMetricsCache, } impl ParallelTrieTracker { diff --git a/crates/trie/sparse-parallel/src/trie.rs b/crates/trie/sparse-parallel/src/trie.rs index 4244f20ab1..5911a4563b 100644 --- a/crates/trie/sparse-parallel/src/trie.rs +++ b/crates/trie/sparse-parallel/src/trie.rs @@ -9,12 +9,13 @@ use alloy_trie::{BranchNodeCompact, TrieMask, EMPTY_ROOT_HASH}; use reth_execution_errors::{SparseTrieErrorKind, SparseTrieResult}; use reth_trie_common::{ prefix_set::{PrefixSet, PrefixSetMut}, - BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, RlpNode, TrieNode, CHILD_INDEX_RANGE, + BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, ProofTrieNode, RlpNode, TrieMasks, + TrieNode, CHILD_INDEX_RANGE, }; use reth_trie_sparse::{ provider::{RevealedNode, TrieNodeProvider}, - LeafLookup, LeafLookupError, RevealedSparseNode, RlpNodeStackItem, SparseNode, SparseNodeType, - SparseTrieInterface, SparseTrieUpdates, TrieMasks, + LeafLookup, LeafLookupError, RlpNodeStackItem, SparseNode, SparseNodeType, SparseTrieInterface, + SparseTrieUpdates, }; use smallvec::SmallVec; use std::{ @@ -172,7 +173,7 @@ impl SparseTrieInterface for ParallelSparseTrie { self } - fn reveal_nodes(&mut self, mut nodes: Vec) -> SparseTrieResult<()> { + fn reveal_nodes(&mut self, mut nodes: Vec) -> SparseTrieResult<()> { if nodes.is_empty() { return Ok(()) } @@ -180,7 +181,7 @@ impl SparseTrieInterface for ParallelSparseTrie { // Sort nodes first by their subtrie, and secondarily by their path. This allows for // grouping nodes by their subtrie using `chunk_by`. nodes.sort_unstable_by( - |RevealedSparseNode { path: path_a, .. }, RevealedSparseNode { path: path_b, .. }| { + |ProofTrieNode { path: path_a, .. }, ProofTrieNode { path: path_b, .. }| { let subtrie_type_a = SparseSubtrieType::from_path(path_a); let subtrie_type_b = SparseSubtrieType::from_path(path_b); subtrie_type_a.cmp(&subtrie_type_b).then(path_a.cmp(path_b)) @@ -188,7 +189,7 @@ impl SparseTrieInterface for ParallelSparseTrie { ); // Update the top-level branch node masks. This is simple and can't be done in parallel. - for RevealedSparseNode { path, masks, .. } in &nodes { + for ProofTrieNode { path, masks, .. } in &nodes { if let Some(tree_mask) = masks.tree_mask { self.branch_node_tree_masks.insert(*path, tree_mask); } @@ -743,24 +744,14 @@ impl SparseTrieInterface for ParallelSparseTrie { // Update subtrie hashes in parallel { use rayon::iter::{IntoParallelIterator, ParallelIterator}; - use tracing::debug_span; let (tx, rx) = mpsc::channel(); let branch_node_tree_masks = &self.branch_node_tree_masks; let branch_node_hash_masks = &self.branch_node_hash_masks; - let span = tracing::Span::current(); changed_subtries .into_par_iter() .map(|mut changed_subtrie| { - let _enter = debug_span!( - target: "trie::parallel_sparse", - parent: span.clone(), - "subtrie", - index = changed_subtrie.index - ) - .entered(); - #[cfg(feature = "metrics")] let start = std::time::Instant::now(); changed_subtrie.subtrie.update_hashes( @@ -2688,14 +2679,14 @@ mod tests { prefix_set::PrefixSetMut, proof::{ProofNodes, ProofRetainer}, updates::TrieUpdates, - BranchNode, ExtensionNode, HashBuilder, LeafNode, RlpNode, TrieMask, TrieNode, - EMPTY_ROOT_HASH, + BranchNode, ExtensionNode, HashBuilder, LeafNode, ProofTrieNode, RlpNode, TrieMask, + TrieMasks, TrieNode, EMPTY_ROOT_HASH, }; use reth_trie_db::DatabaseTrieCursorFactory; use reth_trie_sparse::{ provider::{DefaultTrieNodeProvider, RevealedNode, TrieNodeProvider}, - LeafLookup, LeafLookupError, RevealedSparseNode, SerialSparseTrie, SparseNode, - SparseTrieInterface, SparseTrieUpdates, TrieMasks, + LeafLookup, LeafLookupError, SerialSparseTrie, SparseNode, SparseTrieInterface, + SparseTrieUpdates, }; use std::collections::{BTreeMap, BTreeSet}; @@ -2990,9 +2981,9 @@ mod tests { .into_sorted(); let mut node_iter = TrieNodeIter::state_trie( walker, - HashedPostStateCursor::new( - Option::>::None, - hashed_post_state.accounts(), + HashedPostStateCursor::new_account( + NoopHashedCursor::::default(), + &hashed_post_state, ), ); @@ -3277,7 +3268,7 @@ mod tests { let node = create_leaf_node([0x2, 0x3], 42); let masks = TrieMasks::none(); - trie.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]).unwrap(); + trie.reveal_nodes(vec![ProofTrieNode { path, node, masks }]).unwrap(); assert_matches!( trie.upper_subtrie.nodes.get(&path), @@ -3298,7 +3289,7 @@ mod tests { let node = create_leaf_node([0x3, 0x4], 42); let masks = TrieMasks::none(); - trie.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]).unwrap(); + trie.reveal_nodes(vec![ProofTrieNode { path, node, masks }]).unwrap(); // Check that the lower subtrie was created let idx = path_subtrie_index_unchecked(&path); @@ -3322,7 +3313,7 @@ mod tests { let node = create_leaf_node([0x4, 0x5], 42); let masks = TrieMasks::none(); - trie.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]).unwrap(); + trie.reveal_nodes(vec![ProofTrieNode { path, node, masks }]).unwrap(); // Check that the lower subtrie's path hasn't changed let idx = path_subtrie_index_unchecked(&path); @@ -3383,7 +3374,7 @@ mod tests { let node = create_extension_node([0x2], child_hash); let masks = TrieMasks::none(); - trie.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]).unwrap(); + trie.reveal_nodes(vec![ProofTrieNode { path, node, masks }]).unwrap(); // Extension node should be in upper trie assert_matches!( @@ -3445,7 +3436,7 @@ mod tests { let node = create_branch_node_with_children(&[0x0, 0x7, 0xf], child_hashes.clone()); let masks = TrieMasks::none(); - trie.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]).unwrap(); + trie.reveal_nodes(vec![ProofTrieNode { path, node, masks }]).unwrap(); // Branch node should be in upper trie assert_matches!( @@ -3502,10 +3493,10 @@ mod tests { // Reveal nodes using reveal_nodes trie.reveal_nodes(vec![ - RevealedSparseNode { path: branch_path, node: branch_node, masks: TrieMasks::none() }, - RevealedSparseNode { path: leaf_1_path, node: leaf_1, masks: TrieMasks::none() }, - RevealedSparseNode { path: leaf_2_path, node: leaf_2, masks: TrieMasks::none() }, - RevealedSparseNode { path: leaf_3_path, node: leaf_3, masks: TrieMasks::none() }, + ProofTrieNode { path: branch_path, node: branch_node, masks: TrieMasks::none() }, + ProofTrieNode { path: leaf_1_path, node: leaf_1, masks: TrieMasks::none() }, + ProofTrieNode { path: leaf_2_path, node: leaf_2, masks: TrieMasks::none() }, + ProofTrieNode { path: leaf_3_path, node: leaf_3, masks: TrieMasks::none() }, ]) .unwrap(); @@ -4207,7 +4198,7 @@ mod tests { // Convert the logs into reveal_nodes call on a fresh ParallelSparseTrie let nodes = vec![ // Branch at 0x4f8807 - RevealedSparseNode { + ProofTrieNode { path: branch_path, node: { TrieNode::Branch(BranchNode::new( @@ -4270,7 +4261,7 @@ mod tests { }, }, // Branch at 0x4f88072 - RevealedSparseNode { + ProofTrieNode { path: removed_branch_path, node: { let stack = vec![ @@ -4290,7 +4281,7 @@ mod tests { }, }, // Extension at 0x4f880722 - RevealedSparseNode { + ProofTrieNode { path: Nibbles::from_nibbles([0x4, 0xf, 0x8, 0x8, 0x0, 0x7, 0x2, 0x2]), node: { let extension_node = ExtensionNode::new( @@ -4304,7 +4295,7 @@ mod tests { masks: TrieMasks { hash_mask: None, tree_mask: None }, }, // Leaf at 0x4f88072c - RevealedSparseNode { + ProofTrieNode { path: Nibbles::from_nibbles([0x4, 0xf, 0x8, 0x8, 0x0, 0x7, 0x2, 0xc]), node: { let leaf_node = LeafNode::new( @@ -4423,9 +4414,9 @@ mod tests { // Step 2: Reveal nodes in the trie let mut trie = ParallelSparseTrie::from_root(extension, TrieMasks::none(), true).unwrap(); trie.reveal_nodes(vec![ - RevealedSparseNode { path: branch_path, node: branch, masks: TrieMasks::none() }, - RevealedSparseNode { path: leaf_1_path, node: leaf_1, masks: TrieMasks::none() }, - RevealedSparseNode { path: leaf_2_path, node: leaf_2, masks: TrieMasks::none() }, + ProofTrieNode { path: branch_path, node: branch, masks: TrieMasks::none() }, + ProofTrieNode { path: leaf_1_path, node: leaf_1, masks: TrieMasks::none() }, + ProofTrieNode { path: leaf_2_path, node: leaf_2, masks: TrieMasks::none() }, ]) .unwrap(); @@ -4960,12 +4951,12 @@ mod tests { // └── 1 -> Leaf (Path = 1) sparse .reveal_nodes(vec![ - RevealedSparseNode { + ProofTrieNode { path: Nibbles::default(), node: branch, masks: TrieMasks { hash_mask: None, tree_mask: Some(TrieMask::new(0b01)) }, }, - RevealedSparseNode { + ProofTrieNode { path: Nibbles::from_nibbles([0x1]), node: TrieNode::Leaf(leaf), masks: TrieMasks::none(), @@ -5009,12 +5000,12 @@ mod tests { // └── 1 -> Leaf (Path = 1) sparse .reveal_nodes(vec![ - RevealedSparseNode { + ProofTrieNode { path: Nibbles::default(), node: branch, masks: TrieMasks { hash_mask: None, tree_mask: Some(TrieMask::new(0b01)) }, }, - RevealedSparseNode { + ProofTrieNode { path: Nibbles::from_nibbles([0x1]), node: TrieNode::Leaf(leaf), masks: TrieMasks::none(), @@ -5361,13 +5352,13 @@ mod tests { Default::default(), [key1()], ); - let revealed_nodes: Vec = hash_builder_proof_nodes + let revealed_nodes: Vec = hash_builder_proof_nodes .nodes_sorted() .into_iter() .map(|(path, node)| { let hash_mask = branch_node_hash_masks.get(&path).copied(); let tree_mask = branch_node_tree_masks.get(&path).copied(); - RevealedSparseNode { + ProofTrieNode { path, node: TrieNode::decode(&mut &node[..]).unwrap(), masks: TrieMasks { hash_mask, tree_mask }, @@ -5399,13 +5390,13 @@ mod tests { Default::default(), [key3()], ); - let revealed_nodes: Vec = hash_builder_proof_nodes + let revealed_nodes: Vec = hash_builder_proof_nodes .nodes_sorted() .into_iter() .map(|(path, node)| { let hash_mask = branch_node_hash_masks.get(&path).copied(); let tree_mask = branch_node_tree_masks.get(&path).copied(); - RevealedSparseNode { + ProofTrieNode { path, node: TrieNode::decode(&mut &node[..]).unwrap(), masks: TrieMasks { hash_mask, tree_mask }, @@ -5478,13 +5469,13 @@ mod tests { Default::default(), [key1(), Nibbles::from_nibbles_unchecked([0x01])], ); - let revealed_nodes: Vec = hash_builder_proof_nodes + let revealed_nodes: Vec = hash_builder_proof_nodes .nodes_sorted() .into_iter() .map(|(path, node)| { let hash_mask = branch_node_hash_masks.get(&path).copied(); let tree_mask = branch_node_tree_masks.get(&path).copied(); - RevealedSparseNode { + ProofTrieNode { path, node: TrieNode::decode(&mut &node[..]).unwrap(), masks: TrieMasks { hash_mask, tree_mask }, @@ -5516,13 +5507,13 @@ mod tests { Default::default(), [key2()], ); - let revealed_nodes: Vec = hash_builder_proof_nodes + let revealed_nodes: Vec = hash_builder_proof_nodes .nodes_sorted() .into_iter() .map(|(path, node)| { let hash_mask = branch_node_hash_masks.get(&path).copied(); let tree_mask = branch_node_tree_masks.get(&path).copied(); - RevealedSparseNode { + ProofTrieNode { path, node: TrieNode::decode(&mut &node[..]).unwrap(), masks: TrieMasks { hash_mask, tree_mask }, @@ -5601,13 +5592,13 @@ mod tests { Default::default(), [key1()], ); - let revealed_nodes: Vec = hash_builder_proof_nodes + let revealed_nodes: Vec = hash_builder_proof_nodes .nodes_sorted() .into_iter() .map(|(path, node)| { let hash_mask = branch_node_hash_masks.get(&path).copied(); let tree_mask = branch_node_tree_masks.get(&path).copied(); - RevealedSparseNode { + ProofTrieNode { path, node: TrieNode::decode(&mut &node[..]).unwrap(), masks: TrieMasks { hash_mask, tree_mask }, @@ -6595,16 +6586,12 @@ mod tests { let leaf_masks = TrieMasks::none(); trie.reveal_nodes(vec![ - RevealedSparseNode { + ProofTrieNode { path: Nibbles::from_nibbles([0x3]), node: TrieNode::Branch(branch_0x3_node), masks: branch_0x3_masks, }, - RevealedSparseNode { - path: leaf_path, - node: TrieNode::Leaf(leaf_node), - masks: leaf_masks, - }, + ProofTrieNode { path: leaf_path, node: TrieNode::Leaf(leaf_node), masks: leaf_masks }, ]) .unwrap(); diff --git a/crates/trie/sparse/Cargo.toml b/crates/trie/sparse/Cargo.toml index cb97420c93..b2c7ee0f56 100644 --- a/crates/trie/sparse/Cargo.toml +++ b/crates/trie/sparse/Cargo.toml @@ -90,7 +90,3 @@ harness = false [[bench]] name = "rlp_node" harness = false - -[[bench]] -name = "update" -harness = false diff --git a/crates/trie/sparse/benches/root.rs b/crates/trie/sparse/benches/root.rs index 76eaac91a7..ece0aa5313 100644 --- a/crates/trie/sparse/benches/root.rs +++ b/crates/trie/sparse/benches/root.rs @@ -12,7 +12,7 @@ use reth_trie::{ walker::TrieWalker, HashedStorage, }; -use reth_trie_common::{HashBuilder, Nibbles}; +use reth_trie_common::{updates::TrieUpdatesSorted, HashBuilder, Nibbles}; use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; fn calculate_root_from_leaves(c: &mut Criterion) { @@ -133,18 +133,36 @@ fn calculate_root_from_leaves_repeated(c: &mut Criterion) { ) }; + // Create a TrieUpdatesSorted with just this storage trie + let mut storage_tries = Default::default(); + alloy_primitives::map::B256Map::insert( + &mut storage_tries, + B256::ZERO, + trie_updates_sorted.clone(), + ); + let full_trie_updates = + TrieUpdatesSorted::new(Vec::new(), storage_tries); + let walker = TrieWalker::<_>::storage_trie( - InMemoryTrieCursor::new( - Some(NoopStorageTrieCursor::default()), - &trie_updates_sorted.storage_nodes, + InMemoryTrieCursor::new_storage( + NoopStorageTrieCursor::default(), + &full_trie_updates, + B256::ZERO, ), prefix_set, ); + let hashed_address = B256::ZERO; + let mut storages = alloy_primitives::map::B256Map::default(); + storages.insert(hashed_address, storage_sorted.clone()); + let hashed_post_state = + reth_trie::HashedPostStateSorted::new(Vec::new(), storages); + let mut node_iter = TrieNodeIter::storage_trie( walker, - HashedPostStateCursor::new( - Option::>::None, - &storage_sorted.storage_slots, + HashedPostStateCursor::new_storage( + NoopHashedCursor::::default(), + &hashed_post_state, + hashed_address, ), ); diff --git a/crates/trie/sparse/benches/update.rs b/crates/trie/sparse/benches/update.rs deleted file mode 100644 index dff0260a9a..0000000000 --- a/crates/trie/sparse/benches/update.rs +++ /dev/null @@ -1,104 +0,0 @@ -#![allow(missing_docs)] - -use alloy_primitives::{B256, U256}; -use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; -use proptest::{prelude::*, strategy::ValueTree}; -use rand::seq::IteratorRandom; -use reth_trie_common::Nibbles; -use reth_trie_sparse::{provider::DefaultTrieNodeProvider, SerialSparseTrie, SparseTrie}; - -const LEAF_COUNTS: [usize; 2] = [1_000, 5_000]; - -fn update_leaf(c: &mut Criterion) { - let mut group = c.benchmark_group("update_leaf"); - - for leaf_count in LEAF_COUNTS { - group.bench_function(BenchmarkId::from_parameter(leaf_count), |b| { - let leaves = generate_leaves(leaf_count); - // Start with an empty trie - let provider = DefaultTrieNodeProvider; - - b.iter_batched( - || { - let mut trie = SparseTrie::::revealed_empty(); - // Pre-populate with data - for (path, value) in leaves.iter().cloned() { - trie.update_leaf(path, value, &provider).unwrap(); - } - - let new_leaves = leaves - .iter() - // Update 10% of existing leaves with new values - .choose_multiple(&mut rand::rng(), leaf_count / 10) - .into_iter() - .map(|(path, _)| { - ( - path, - alloy_rlp::encode_fixed_size(&U256::from(path.len() * 2)).to_vec(), - ) - }) - .collect::>(); - - (trie, new_leaves) - }, - |(mut trie, new_leaves)| { - for (path, new_value) in new_leaves { - trie.update_leaf(*path, new_value, &provider).unwrap(); - } - trie - }, - BatchSize::LargeInput, - ); - }); - } -} - -fn remove_leaf(c: &mut Criterion) { - let mut group = c.benchmark_group("remove_leaf"); - - for leaf_count in LEAF_COUNTS { - group.bench_function(BenchmarkId::from_parameter(leaf_count), |b| { - let leaves = generate_leaves(leaf_count); - // Start with an empty trie - let provider = DefaultTrieNodeProvider; - - b.iter_batched( - || { - let mut trie = SparseTrie::::revealed_empty(); - // Pre-populate with data - for (path, value) in leaves.iter().cloned() { - trie.update_leaf(path, value, &provider).unwrap(); - } - - let delete_leaves = leaves - .iter() - .map(|(path, _)| path) - // Remove 10% leaves - .choose_multiple(&mut rand::rng(), leaf_count / 10); - - (trie, delete_leaves) - }, - |(mut trie, delete_leaves)| { - for path in delete_leaves { - trie.remove_leaf(path, &provider).unwrap(); - } - trie - }, - BatchSize::LargeInput, - ); - }); - } -} - -fn generate_leaves(size: usize) -> Vec<(Nibbles, Vec)> { - proptest::collection::hash_map(any::(), any::(), size) - .new_tree(&mut Default::default()) - .unwrap() - .current() - .iter() - .map(|(key, value)| (Nibbles::unpack(key), alloy_rlp::encode_fixed_size(value).to_vec())) - .collect() -} - -criterion_group!(benches, update_leaf, remove_leaf); -criterion_main!(benches); diff --git a/crates/trie/sparse/src/state.rs b/crates/trie/sparse/src/state.rs index f142385c3c..c276bc6b5b 100644 --- a/crates/trie/sparse/src/state.rs +++ b/crates/trie/sparse/src/state.rs @@ -1,7 +1,7 @@ use crate::{ provider::{TrieNodeProvider, TrieNodeProviderFactory}, traits::SparseTrieInterface, - RevealedSparseNode, SerialSparseTrie, SparseTrie, TrieMasks, + SerialSparseTrie, SparseTrie, }; use alloc::{collections::VecDeque, vec::Vec}; use alloy_primitives::{ @@ -15,8 +15,9 @@ use reth_primitives_traits::Account; use reth_trie_common::{ proof::ProofNodes, updates::{StorageTrieUpdates, TrieUpdates}, - DecodedMultiProof, DecodedStorageMultiProof, MultiProof, Nibbles, RlpNode, StorageMultiProof, - TrieAccount, TrieMask, TrieNode, EMPTY_ROOT_HASH, TRIE_ACCOUNT_RLP_MAX_SIZE, + DecodedMultiProof, DecodedStorageMultiProof, MultiProof, Nibbles, ProofTrieNode, RlpNode, + StorageMultiProof, TrieAccount, TrieMask, TrieMasks, TrieNode, EMPTY_ROOT_HASH, + TRIE_ACCOUNT_RLP_MAX_SIZE, }; use tracing::{instrument, trace}; @@ -945,9 +946,9 @@ struct ProofNodesMetricValues { #[derive(Debug, PartialEq, Eq)] struct FilterMappedProofNodes { /// Root node which was pulled out of the original node set to be handled specially. - root_node: Option, + root_node: Option, /// Filtered, decoded and unsorted proof nodes. Root node is removed. - nodes: Vec, + nodes: Vec, /// Number of new nodes that will be revealed. This includes all children of branch nodes, even /// if they are not in the proof. new_nodes: usize, @@ -955,7 +956,7 @@ struct FilterMappedProofNodes { metric_values: ProofNodesMetricValues, } -/// Filters the decoded nodes that are already revealed, maps them to `RevealedSparseNodes`, +/// Filters the decoded nodes that are already revealed, maps them to `SparseTrieNode`s, /// separates the root node if present, and returns additional information about the number of /// total, skipped, and new nodes. fn filter_map_revealed_nodes( @@ -1006,7 +1007,7 @@ fn filter_map_revealed_nodes( _ => TrieMasks::none(), }; - let node = RevealedSparseNode { path, node: proof_node, masks }; + let node = ProofTrieNode { path, node: proof_node, masks }; if is_root { // Perform sanity check. @@ -1382,12 +1383,12 @@ mod tests { assert_eq!( decoded, FilterMappedProofNodes { - root_node: Some(RevealedSparseNode { + root_node: Some(ProofTrieNode { path: Nibbles::default(), node: branch, masks: TrieMasks::none(), }), - nodes: vec![RevealedSparseNode { + nodes: vec![ProofTrieNode { path: Nibbles::from_nibbles([0x1]), node: leaf, masks: TrieMasks::none(), diff --git a/crates/trie/sparse/src/traits.rs b/crates/trie/sparse/src/traits.rs index 308695ec0f..55a17a9a7f 100644 --- a/crates/trie/sparse/src/traits.rs +++ b/crates/trie/sparse/src/traits.rs @@ -7,9 +7,9 @@ use alloy_primitives::{ map::{HashMap, HashSet}, B256, }; -use alloy_trie::{BranchNodeCompact, TrieMask}; +use alloy_trie::BranchNodeCompact; use reth_execution_errors::SparseTrieResult; -use reth_trie_common::{Nibbles, TrieNode}; +use reth_trie_common::{Nibbles, ProofTrieNode, TrieMasks, TrieNode}; use crate::provider::TrieNodeProvider; @@ -74,7 +74,7 @@ pub trait SparseTrieInterface: Sized + Debug + Send + Sync { node: TrieNode, masks: TrieMasks, ) -> SparseTrieResult<()> { - self.reveal_nodes(vec![RevealedSparseNode { path, node, masks }]) + self.reveal_nodes(vec![ProofTrieNode { path, node, masks }]) } /// Reveals one or more trie nodes if they have not been revealed before. @@ -91,7 +91,7 @@ pub trait SparseTrieInterface: Sized + Debug + Send + Sync { /// # Returns /// /// `Ok(())` if successful, or an error if any of the nodes was not revealed. - fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()>; + fn reveal_nodes(&mut self, nodes: Vec) -> SparseTrieResult<()>; /// Updates the value of a leaf node at the specified path. /// @@ -232,36 +232,6 @@ pub trait SparseTrieInterface: Sized + Debug + Send + Sync { fn shrink_values_to(&mut self, size: usize); } -/// 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 @@ -307,14 +277,3 @@ pub enum LeafLookup { /// 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/trie/sparse/src/trie.rs b/crates/trie/sparse/src/trie.rs index cf92942b82..acad15bc15 100644 --- a/crates/trie/sparse/src/trie.rs +++ b/crates/trie/sparse/src/trie.rs @@ -1,7 +1,6 @@ use crate::{ provider::{RevealedNode, TrieNodeProvider}, - LeafLookup, LeafLookupError, RevealedSparseNode, SparseTrieInterface, SparseTrieUpdates, - TrieMasks, + LeafLookup, LeafLookupError, SparseTrieInterface, SparseTrieUpdates, }; use alloc::{ borrow::Cow, @@ -20,8 +19,8 @@ use alloy_rlp::Decodable; use reth_execution_errors::{SparseTrieErrorKind, SparseTrieResult}; use reth_trie_common::{ prefix_set::{PrefixSet, PrefixSetMut}, - BranchNodeCompact, BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, RlpNode, TrieMask, - TrieNode, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, + BranchNodeCompact, BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, ProofTrieNode, + RlpNode, TrieMask, TrieMasks, TrieNode, CHILD_INDEX_RANGE, EMPTY_ROOT_HASH, }; use smallvec::SmallVec; use tracing::{debug, instrument, trace}; @@ -589,7 +588,7 @@ impl SparseTrieInterface for SerialSparseTrie { Ok(()) } - fn reveal_nodes(&mut self, mut nodes: Vec) -> SparseTrieResult<()> { + 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)?; @@ -2416,9 +2415,9 @@ mod tests { .into_sorted(); let mut node_iter = TrieNodeIter::state_trie( walker, - HashedPostStateCursor::new( - Option::>::None, - hashed_post_state.accounts(), + HashedPostStateCursor::new_account( + NoopHashedCursor::::default(), + &hashed_post_state, ), ); diff --git a/crates/trie/trie/src/forward_cursor.rs b/crates/trie/trie/src/forward_cursor.rs index 53d07d5247..5abb5e2431 100644 --- a/crates/trie/trie/src/forward_cursor.rs +++ b/crates/trie/trie/src/forward_cursor.rs @@ -39,6 +39,12 @@ impl<'a, K, V> ForwardInMemoryCursor<'a, K, V> { self.entries.get(self.idx) } + /// Resets the cursor to the beginning of the collection. + #[inline] + pub const fn reset(&mut self) { + self.idx = 0; + } + #[inline] fn next(&mut self) -> Option<&(K, V)> { let entry = self.entries.get(self.idx)?; diff --git a/crates/trie/trie/src/hashed_cursor/metrics.rs b/crates/trie/trie/src/hashed_cursor/metrics.rs new file mode 100644 index 0000000000..71de95eab1 --- /dev/null +++ b/crates/trie/trie/src/hashed_cursor/metrics.rs @@ -0,0 +1,190 @@ +use super::{HashedCursor, HashedStorageCursor}; +use alloy_primitives::B256; +use reth_storage_errors::db::DatabaseError; +use std::time::{Duration, Instant}; +use tracing::debug_span; + +#[cfg(feature = "metrics")] +use crate::TrieType; +#[cfg(feature = "metrics")] +use reth_metrics::metrics::{self, Histogram}; + +/// Prometheus metrics for hashed cursor operations. +/// +/// Tracks the number of cursor operations for monitoring and performance analysis. +#[cfg(feature = "metrics")] +#[derive(Clone, Debug)] +pub struct HashedCursorMetrics { + /// Histogram tracking overall time spent in database operations + overall_duration: Histogram, + /// Histogram for `next()` operations + next_histogram: Histogram, + /// Histogram for `seek()` operations + seek_histogram: Histogram, + /// Histogram for `is_storage_empty()` operations + is_storage_empty_histogram: Histogram, +} + +#[cfg(feature = "metrics")] +impl HashedCursorMetrics { + /// Create a new metrics instance with the specified trie type label. + pub fn new(trie_type: TrieType) -> Self { + let trie_type_str = trie_type.as_str(); + + Self { + overall_duration: metrics::histogram!( + "trie.hashed_cursor.overall_duration", + "type" => trie_type_str + ), + next_histogram: metrics::histogram!( + "trie.hashed_cursor.operations", + "type" => trie_type_str, + "operation" => "next" + ), + seek_histogram: metrics::histogram!( + "trie.hashed_cursor.operations", + "type" => trie_type_str, + "operation" => "seek" + ), + is_storage_empty_histogram: metrics::histogram!( + "trie.hashed_cursor.operations", + "type" => trie_type_str, + "operation" => "is_storage_empty" + ), + } + } + + /// Record the cached metrics from the provided cache and reset the cache counters. + /// + /// This method adds the current counter values from the cache to the Prometheus metrics + /// and then resets all cache counters to zero. + pub fn record(&mut self, cache: &mut HashedCursorMetricsCache) { + self.next_histogram.record(cache.next_count as f64); + self.seek_histogram.record(cache.seek_count as f64); + self.is_storage_empty_histogram.record(cache.is_storage_empty_count as f64); + self.overall_duration.record(cache.total_duration.as_secs_f64()); + cache.reset(); + } +} + +/// Cached metrics counters for hashed cursor operations. +#[derive(Debug, Copy, Clone)] +pub struct HashedCursorMetricsCache { + /// Counter for `next()` calls + pub next_count: usize, + /// Counter for `seek()` calls + pub seek_count: usize, + /// Counter for `is_storage_empty()` calls (if applicable) + pub is_storage_empty_count: usize, + /// Total duration spent in database operations + pub total_duration: Duration, +} + +impl Default for HashedCursorMetricsCache { + fn default() -> Self { + Self { + next_count: 0, + seek_count: 0, + is_storage_empty_count: 0, + total_duration: Duration::ZERO, + } + } +} + +impl HashedCursorMetricsCache { + /// Reset all counters to zero. + pub const fn reset(&mut self) { + self.next_count = 0; + self.seek_count = 0; + self.is_storage_empty_count = 0; + self.total_duration = Duration::ZERO; + } + + /// Extend this cache by adding the counts from another cache. + /// + /// This accumulates the counter values from `other` into this cache. + pub fn extend(&mut self, other: &Self) { + self.next_count += other.next_count; + self.seek_count += other.seek_count; + self.is_storage_empty_count += other.is_storage_empty_count; + self.total_duration += other.total_duration; + } + + /// Record the span for metrics. + pub fn record_span(&self, name: &'static str) { + let _span = debug_span!( + target: "trie::trie_cursor", + "Hashed cursor metrics", + name, + next_count = self.next_count, + seek_count = self.seek_count, + is_storage_empty_count = self.is_storage_empty_count, + total_duration = self.total_duration.as_secs_f64(), + ) + .entered(); + } +} + +/// A wrapper around a [`HashedCursor`] that tracks metrics for cursor operations. +/// +/// This implementation counts the number of times each cursor operation is called: +/// - `next()` - Move to the next entry +/// - `seek()` - Seek to a key or the next greater key +#[derive(Debug)] +pub struct InstrumentedHashedCursor<'metrics, C> { + /// The underlying cursor being wrapped + cursor: C, + /// Cached metrics counters + metrics: &'metrics mut HashedCursorMetricsCache, +} + +impl<'metrics, C> InstrumentedHashedCursor<'metrics, C> { + /// Create a new metrics cursor wrapping the given cursor. + pub const fn new(cursor: C, metrics: &'metrics mut HashedCursorMetricsCache) -> Self { + Self { cursor, metrics } + } +} + +impl<'metrics, C> HashedCursor for InstrumentedHashedCursor<'metrics, C> +where + C: HashedCursor, +{ + type Value = C::Value; + + fn seek(&mut self, key: B256) -> Result, DatabaseError> { + let start = Instant::now(); + self.metrics.seek_count += 1; + let result = self.cursor.seek(key); + self.metrics.total_duration += start.elapsed(); + result + } + + fn next(&mut self) -> Result, DatabaseError> { + let start = Instant::now(); + self.metrics.next_count += 1; + let result = self.cursor.next(); + self.metrics.total_duration += start.elapsed(); + result + } + + fn reset(&mut self) { + self.cursor.reset() + } +} + +impl<'metrics, C> HashedStorageCursor for InstrumentedHashedCursor<'metrics, C> +where + C: HashedStorageCursor, +{ + fn is_storage_empty(&mut self) -> Result { + let start = Instant::now(); + self.metrics.is_storage_empty_count += 1; + let result = self.cursor.is_storage_empty(); + self.metrics.total_duration += start.elapsed(); + result + } + + fn set_hashed_address(&mut self, hashed_address: B256) { + self.cursor.set_hashed_address(hashed_address) + } +} diff --git a/crates/trie/trie/src/hashed_cursor/mock.rs b/crates/trie/trie/src/hashed_cursor/mock.rs index 334b5ba77c..fd3e8d2f25 100644 --- a/crates/trie/trie/src/hashed_cursor/mock.rs +++ b/crates/trie/trie/src/hashed_cursor/mock.rs @@ -13,12 +13,12 @@ use tracing::instrument; #[derive(Clone, Default, Debug)] pub struct MockHashedCursorFactory { hashed_accounts: Arc>, - hashed_storage_tries: B256Map>>, + hashed_storage_tries: Arc>>, /// List of keys that the hashed accounts cursor has visited. visited_account_keys: Arc>>>, /// List of keys that the hashed storages cursor has visited, per storage trie. - visited_storage_keys: B256Map>>>>, + visited_storage_keys: Arc>>>>, } impl MockHashedCursorFactory { @@ -31,12 +31,9 @@ impl MockHashedCursorFactory { hashed_storage_tries.keys().map(|k| (*k, Default::default())).collect(); Self { hashed_accounts: Arc::new(hashed_accounts), - hashed_storage_tries: hashed_storage_tries - .into_iter() - .map(|(k, v)| (k, Arc::new(v))) - .collect(), + hashed_storage_tries: Arc::new(hashed_storage_tries), visited_account_keys: Default::default(), - visited_storage_keys, + visited_storage_keys: Arc::new(visited_storage_keys), } } @@ -72,39 +69,93 @@ impl HashedCursorFactory for MockHashedCursorFactory { &self, hashed_address: B256, ) -> Result, DatabaseError> { - Ok(MockHashedCursor::new( - self.hashed_storage_tries - .get(&hashed_address) - .ok_or_else(|| { - DatabaseError::Other(format!("storage trie for {hashed_address:?} not found")) - })? - .clone(), - self.visited_storage_keys - .get(&hashed_address) - .ok_or_else(|| { - DatabaseError::Other(format!("storage trie for {hashed_address:?} not found")) - })? - .clone(), - )) + MockHashedCursor::new_storage( + self.hashed_storage_tries.clone(), + self.visited_storage_keys.clone(), + hashed_address, + ) } } +/// Mock hashed cursor type - determines whether this is an account or storage cursor. +#[derive(Debug)] +enum MockHashedCursorType { + Account { + values: Arc>, + visited_keys: Arc>>>, + }, + Storage { + all_storage_values: Arc>>, + all_visited_storage_keys: Arc>>>>, + current_hashed_address: B256, + }, +} + /// Mock hashed cursor. -#[derive(Default, Debug)] +#[derive(Debug)] pub struct MockHashedCursor { /// The current key. If set, it is guaranteed to exist in `values`. current_key: Option, - values: Arc>, - visited_keys: Arc>>>, + cursor_type: MockHashedCursorType, } impl MockHashedCursor { - /// Creates a new mock hashed cursor with the given values and key tracking. + /// Creates a new mock hashed cursor for accounts with the given values and key tracking. pub fn new( values: Arc>, visited_keys: Arc>>>, ) -> Self { - Self { current_key: None, values, visited_keys } + Self { + current_key: None, + cursor_type: MockHashedCursorType::Account { values, visited_keys }, + } + } + + /// Creates a new mock hashed cursor for storage with access to all storage tries. + pub fn new_storage( + all_storage_values: Arc>>, + all_visited_storage_keys: Arc>>>>, + hashed_address: B256, + ) -> Result { + if !all_storage_values.contains_key(&hashed_address) { + return Err(DatabaseError::Other(format!( + "storage trie for {hashed_address:?} not found" + ))); + } + Ok(Self { + current_key: None, + cursor_type: MockHashedCursorType::Storage { + all_storage_values, + all_visited_storage_keys, + current_hashed_address: hashed_address, + }, + }) + } + + /// Returns the values map for the current cursor type. + fn values(&self) -> &BTreeMap { + match &self.cursor_type { + MockHashedCursorType::Account { values, .. } => values.as_ref(), + MockHashedCursorType::Storage { + all_storage_values, current_hashed_address, .. + } => all_storage_values + .get(current_hashed_address) + .expect("current_hashed_address should exist in all_storage_values"), + } + } + + /// Returns the visited keys mutex for the current cursor type. + fn visited_keys(&self) -> &Mutex>> { + match &self.cursor_type { + MockHashedCursorType::Account { visited_keys, .. } => visited_keys.as_ref(), + MockHashedCursorType::Storage { + all_visited_storage_keys, + current_hashed_address, + .. + } => all_visited_storage_keys + .get(current_hashed_address) + .expect("current_hashed_address should exist in all_visited_storage_keys"), + } } } @@ -114,11 +165,11 @@ impl HashedCursor for MockHashedCursor { #[instrument(skip(self), ret(level = "trace"))] fn seek(&mut self, key: B256) -> Result, DatabaseError> { // Find the first key that is greater than or equal to the given key. - let entry = self.values.iter().find_map(|(k, v)| (k >= &key).then(|| (*k, v.clone()))); + let entry = self.values().iter().find_map(|(k, v)| (k >= &key).then(|| (*k, v.clone()))); if let Some((key, _)) = &entry { self.current_key = Some(*key); } - self.visited_keys.lock().push(KeyVisit { + self.visited_keys().lock().push(KeyVisit { visit_type: KeyVisitType::SeekNonExact(key), visited_key: entry.as_ref().map(|(k, _)| *k), }); @@ -127,7 +178,7 @@ impl HashedCursor for MockHashedCursor { #[instrument(skip(self), ret(level = "trace"))] fn next(&mut self) -> Result, DatabaseError> { - let mut iter = self.values.iter(); + let mut iter = self.values().iter(); // Jump to the first key that has a prefix of the current key if it's set, or to the first // key otherwise. iter.find(|(k, _)| { @@ -139,17 +190,33 @@ impl HashedCursor for MockHashedCursor { if let Some((key, _)) = &entry { self.current_key = Some(*key); } - self.visited_keys.lock().push(KeyVisit { + self.visited_keys().lock().push(KeyVisit { visit_type: KeyVisitType::Next, visited_key: entry.as_ref().map(|(k, _)| *k), }); Ok(entry) } + + fn reset(&mut self) { + self.current_key = None; + } } impl HashedStorageCursor for MockHashedCursor { #[instrument(level = "trace", skip(self), ret)] fn is_storage_empty(&mut self) -> Result { - Ok(self.values.is_empty()) + Ok(self.values().is_empty()) + } + + fn set_hashed_address(&mut self, hashed_address: B256) { + self.reset(); + match &mut self.cursor_type { + MockHashedCursorType::Storage { current_hashed_address, .. } => { + *current_hashed_address = hashed_address; + } + MockHashedCursorType::Account { .. } => { + panic!("set_hashed_address called on account cursor") + } + } } } diff --git a/crates/trie/trie/src/hashed_cursor/mod.rs b/crates/trie/trie/src/hashed_cursor/mod.rs index 6c4788a336..90d70c1c68 100644 --- a/crates/trie/trie/src/hashed_cursor/mod.rs +++ b/crates/trie/trie/src/hashed_cursor/mod.rs @@ -13,6 +13,12 @@ pub mod noop; #[cfg(test)] pub mod mock; +/// Metrics tracking hashed cursor implementations. +pub mod metrics; +#[cfg(feature = "metrics")] +pub use metrics::HashedCursorMetrics; +pub use metrics::{HashedCursorMetricsCache, InstrumentedHashedCursor}; + /// The factory trait for creating cursors over the hashed state. #[auto_impl::auto_impl(&)] pub trait HashedCursorFactory { @@ -47,6 +53,13 @@ pub trait HashedCursor { /// Move the cursor to the next entry and return it. fn next(&mut self) -> Result, DatabaseError>; + + /// Reset the cursor to its initial state. + /// + /// # Important + /// + /// After calling this method, the subsequent operation MUST be a [`HashedCursor::seek`] call. + fn reset(&mut self); } /// The cursor for iterating over hashed storage entries. @@ -54,4 +67,11 @@ pub trait HashedCursor { pub trait HashedStorageCursor: HashedCursor { /// Returns `true` if there are no entries for a given key. fn is_storage_empty(&mut self) -> Result; + + /// Set the hashed address for the storage cursor. + /// + /// # Important + /// + /// After calling this method, the subsequent operation MUST be a [`HashedCursor::seek`] call. + fn set_hashed_address(&mut self, hashed_address: B256); } diff --git a/crates/trie/trie/src/hashed_cursor/noop.rs b/crates/trie/trie/src/hashed_cursor/noop.rs index 4f59af2ed3..88726d3b67 100644 --- a/crates/trie/trie/src/hashed_cursor/noop.rs +++ b/crates/trie/trie/src/hashed_cursor/noop.rs @@ -56,10 +56,18 @@ where fn next(&mut self) -> Result, DatabaseError> { Ok(None) } + + fn reset(&mut self) { + // Noop + } } impl HashedStorageCursor for NoopHashedCursor { fn is_storage_empty(&mut self) -> Result { Ok(true) } + + fn set_hashed_address(&mut self, _hashed_address: B256) { + // Noop + } } diff --git a/crates/trie/trie/src/hashed_cursor/post_state.rs b/crates/trie/trie/src/hashed_cursor/post_state.rs index bf80ee63a1..0b78119baf 100644 --- a/crates/trie/trie/src/hashed_cursor/post_state.rs +++ b/crates/trie/trie/src/hashed_cursor/post_state.rs @@ -35,27 +35,16 @@ where fn hashed_account_cursor(&self) -> Result, DatabaseError> { let cursor = self.cursor_factory.hashed_account_cursor()?; - Ok(HashedPostStateCursor::new(Some(cursor), &self.post_state.as_ref().accounts)) + Ok(HashedPostStateCursor::new_account(cursor, self.post_state.as_ref())) } fn hashed_storage_cursor( &self, hashed_address: B256, ) -> Result, DatabaseError> { - static EMPTY_UPDATES: Vec<(B256, U256)> = Vec::new(); - - let post_state_storage = self.post_state.as_ref().storages.get(&hashed_address); - let (storage_slots, wiped) = post_state_storage - .map(|u| (u.storage_slots_ref(), u.is_wiped())) - .unwrap_or((&EMPTY_UPDATES, false)); - - let cursor = if wiped { - None - } else { - Some(self.cursor_factory.hashed_storage_cursor(hashed_address)?) - }; - - Ok(HashedPostStateCursor::new(cursor, storage_slots)) + let post_state = self.post_state.as_ref(); + let cursor = self.cursor_factory.hashed_storage_cursor(hashed_address)?; + Ok(HashedPostStateCursor::new_storage(cursor, post_state, hashed_address)) } } @@ -101,8 +90,10 @@ pub struct HashedPostStateCursor<'a, C, V> where V: HashedPostStateCursorValue, { - /// The underlying `database_cursor`. If None then it is assumed there is no DB data. - cursor: Option, + /// The underlying cursor. + cursor: C, + /// Whether the underlying cursor should be ignored (when storage was wiped). + cursor_wiped: bool, /// Entry that `database_cursor` is currently pointing to. cursor_entry: Option<(B256, V::NonZero)>, /// Forward-only in-memory cursor over underlying V. @@ -113,6 +104,64 @@ where /// Tracks whether `seek` has been called. Used to prevent re-seeking the DB cursor /// when it has been exhausted by iteration. seeked: bool, + /// Reference to the full post state. + post_state: &'a HashedPostStateSorted, +} + +impl<'a, C> HashedPostStateCursor<'a, C, Option> +where + C: HashedCursor, +{ + /// Create new account cursor which combines a DB cursor and the post state. + pub fn new_account(cursor: C, post_state: &'a HashedPostStateSorted) -> Self { + let post_state_cursor = ForwardInMemoryCursor::new(&post_state.accounts); + Self { + cursor, + cursor_wiped: false, + cursor_entry: None, + post_state_cursor, + last_key: None, + seeked: false, + post_state, + } + } +} + +impl<'a, C> HashedPostStateCursor<'a, C, U256> +where + C: HashedStorageCursor, +{ + /// Create new storage cursor with full post state reference. + /// This allows the cursor to switch between storage tries when `set_hashed_address` is called. + pub fn new_storage( + cursor: C, + post_state: &'a HashedPostStateSorted, + hashed_address: B256, + ) -> Self { + let (post_state_cursor, cursor_wiped) = + Self::get_storage_overlay(post_state, hashed_address); + Self { + cursor, + cursor_wiped, + cursor_entry: None, + post_state_cursor, + last_key: None, + seeked: false, + post_state, + } + } + + /// Returns the storage overlay for `hashed_address` and whether it was wiped. + fn get_storage_overlay( + post_state: &'a HashedPostStateSorted, + hashed_address: B256, + ) -> (ForwardInMemoryCursor<'a, B256, U256>, bool) { + let post_state_storage = post_state.storages.get(&hashed_address); + let cursor_wiped = post_state_storage.is_some_and(|u| u.is_wiped()); + let storage_slots = post_state_storage.map(|u| u.storage_slots_ref()).unwrap_or(&[]); + + (ForwardInMemoryCursor::new(storage_slots), cursor_wiped) + } } impl<'a, C, V> HashedPostStateCursor<'a, C, V> @@ -120,22 +169,9 @@ where C: HashedCursor, V: HashedPostStateCursorValue, { - /// Creates a new post state cursor which combines a DB cursor and in-memory post state updates. - /// - /// # Parameters - /// - `cursor`: The database cursor. Pass `None` to indicate: - /// - For accounts: Empty database (no persisted accounts) - /// - For storage: Wiped storage (e.g., via `SELFDESTRUCT` - all previous storage destroyed) - /// - `updates`: Pre-sorted post state updates. - pub fn new(cursor: Option, updates: &'a [(B256, V)]) -> Self { - debug_assert!(updates.is_sorted_by_key(|(k, _)| k), "Overlay values must be sorted by key"); - Self { - cursor, - cursor_entry: None, - post_state_cursor: ForwardInMemoryCursor::new(updates), - last_key: None, - seeked: false, - } + /// Returns a mutable reference to the underlying cursor if it's not wiped, None otherwise. + fn get_cursor_mut(&mut self) -> Option<&mut C> { + (!self.cursor_wiped).then_some(&mut self.cursor) } /// Asserts that the next entry to be returned from the cursor is not previous to the last entry @@ -162,7 +198,7 @@ where }; if should_seek { - self.cursor_entry = self.cursor.as_mut().map(|c| c.seek(key)).transpose()?.flatten(); + self.cursor_entry = self.get_cursor_mut().map(|c| c.seek(key)).transpose()?.flatten(); } Ok(()) @@ -175,7 +211,7 @@ where // If the previous entry is `None`, and we've done a seek previously, then the cursor is // exhausted, and we shouldn't call `next` again. if self.cursor_entry.is_some() { - self.cursor_entry = self.cursor.as_mut().map(|c| c.next()).transpose()?.flatten(); + self.cursor_entry = self.get_cursor_mut().map(|c| c.next()).transpose()?.flatten(); } Ok(()) @@ -281,14 +317,33 @@ where self.set_last_key(&entry); Ok(entry) } + + fn reset(&mut self) { + let Self { + cursor, + cursor_wiped, + cursor_entry, + post_state_cursor, + last_key, + seeked, + post_state: _, + } = self; + + cursor.reset(); + post_state_cursor.reset(); + + *cursor_wiped = false; + *cursor_entry = None; + *last_key = None; + *seeked = false; + } } /// The cursor to iterate over post state hashed values and corresponding database entries. /// It will always give precedence to the data from the post state. -impl HashedStorageCursor for HashedPostStateCursor<'_, C, V> +impl HashedStorageCursor for HashedPostStateCursor<'_, C, U256> where - C: HashedStorageCursor, - V: HashedPostStateCursorValue, + C: HashedStorageCursor, { /// Returns `true` if the account has no storage entries. /// @@ -301,8 +356,15 @@ where } // If no non-zero slots in post state, check the database. - // Returns true if cursor is None (wiped storage or empty DB). - self.cursor.as_mut().map_or(Ok(true), |c| c.is_storage_empty()) + // Returns true if cursor is wiped. + self.get_cursor_mut().map_or(Ok(true), |c| c.is_storage_empty()) + } + + fn set_hashed_address(&mut self, hashed_address: B256) { + self.reset(); + self.cursor.set_hashed_address(hashed_address); + (self.post_state_cursor, self.cursor_wiped) = + HashedPostStateCursor::::get_storage_overlay(self.post_state, hashed_address); } } @@ -417,7 +479,18 @@ mod tests { let db_nodes_arc = Arc::new(db_nodes_map); let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockHashedCursor::new(db_nodes_arc, visited_keys); - let mut test_cursor = HashedPostStateCursor::new(Some(mock_cursor), &post_state_nodes); + + // Create a HashedPostStateSorted with the storage data + let hashed_address = B256::ZERO; + let storage_sorted = reth_trie_common::HashedStorageSorted { + storage_slots: post_state_nodes, + wiped: false, + }; + let mut storages = alloy_primitives::map::B256Map::default(); + storages.insert(hashed_address, storage_sorted); + let post_state = HashedPostStateSorted::new(Vec::new(), storages); + + let mut test_cursor = HashedPostStateCursor::new_storage(mock_cursor, &post_state, hashed_address); // Test: seek to the beginning first let control_first = control_cursor.seek(B256::ZERO).unwrap(); diff --git a/crates/trie/trie/src/lib.rs b/crates/trie/trie/src/lib.rs index e53049b587..aebcbab7af 100644 --- a/crates/trie/trie/src/lib.rs +++ b/crates/trie/trie/src/lib.rs @@ -32,6 +32,9 @@ pub mod node_iter; /// Merkle proof generation. pub mod proof; +/// Merkle proof generation v2 (leaf-only implementation). +pub mod proof_v2; + /// Trie witness generation. pub mod witness; diff --git a/crates/trie/trie/src/node_iter.rs b/crates/trie/trie/src/node_iter.rs index aef668e8ff..b57fc2da70 100644 --- a/crates/trie/trie/src/node_iter.rs +++ b/crates/trie/trie/src/node_iter.rs @@ -352,9 +352,9 @@ mod tests { let mut node_iter = TrieNodeIter::state_trie( walker, - HashedPostStateCursor::new( - Option::>::None, - hashed_post_state.accounts(), + HashedPostStateCursor::new_account( + NoopHashedCursor::::default(), + &hashed_post_state, ), ); diff --git a/crates/trie/trie/src/proof/mod.rs b/crates/trie/trie/src/proof/mod.rs index efd958e574..034c488064 100644 --- a/crates/trie/trie/src/proof/mod.rs +++ b/crates/trie/trie/src/proof/mod.rs @@ -1,8 +1,11 @@ use crate::{ - hashed_cursor::{HashedCursorFactory, HashedStorageCursor}, + hashed_cursor::{ + HashedCursorFactory, HashedCursorMetricsCache, HashedStorageCursor, + InstrumentedHashedCursor, + }, node_iter::{TrieElement, TrieNodeIter}, prefix_set::{PrefixSetMut, TriePrefixSetsMut}, - trie_cursor::TrieCursorFactory, + trie_cursor::{InstrumentedTrieCursor, TrieCursorFactory, TrieCursorMetricsCache}, walker::TrieWalker, HashBuilder, Nibbles, TRIE_ACCOUNT_RLP_MAX_SIZE, }; @@ -194,7 +197,7 @@ where /// Generates storage merkle proofs. #[derive(Debug)] -pub struct StorageProof { +pub struct StorageProof<'a, T, H, K = AddedRemovedKeys> { /// The factory for traversing trie nodes. trie_cursor_factory: T, /// The factory for hashed cursors. @@ -207,9 +210,13 @@ pub struct StorageProof { collect_branch_node_masks: bool, /// Provided by the user to give the necessary context to retain extra proofs. added_removed_keys: Option, + /// Optional reference to accumulate trie cursor metrics. + trie_cursor_metrics: Option<&'a mut TrieCursorMetricsCache>, + /// Optional reference to accumulate hashed cursor metrics. + hashed_cursor_metrics: Option<&'a mut HashedCursorMetricsCache>, } -impl StorageProof { +impl StorageProof<'static, T, H> { /// Create a new [`StorageProof`] instance. pub fn new(t: T, h: H, address: Address) -> Self { Self::new_hashed(t, h, keccak256(address)) @@ -224,13 +231,18 @@ impl StorageProof { prefix_set: PrefixSetMut::default(), collect_branch_node_masks: false, added_removed_keys: None, + trie_cursor_metrics: None, + hashed_cursor_metrics: None, } } } -impl StorageProof { +impl<'a, T, H, K> StorageProof<'a, T, H, K> { /// Set the trie cursor factory. - pub fn with_trie_cursor_factory(self, trie_cursor_factory: TF) -> StorageProof { + pub fn with_trie_cursor_factory( + self, + trie_cursor_factory: TF, + ) -> StorageProof<'a, TF, H, K> { StorageProof { trie_cursor_factory, hashed_cursor_factory: self.hashed_cursor_factory, @@ -238,6 +250,8 @@ impl StorageProof { prefix_set: self.prefix_set, collect_branch_node_masks: self.collect_branch_node_masks, added_removed_keys: self.added_removed_keys, + trie_cursor_metrics: self.trie_cursor_metrics, + hashed_cursor_metrics: self.hashed_cursor_metrics, } } @@ -245,7 +259,7 @@ impl StorageProof { pub fn with_hashed_cursor_factory( self, hashed_cursor_factory: HF, - ) -> StorageProof { + ) -> StorageProof<'a, T, HF, K> { StorageProof { trie_cursor_factory: self.trie_cursor_factory, hashed_cursor_factory, @@ -253,6 +267,8 @@ impl StorageProof { prefix_set: self.prefix_set, collect_branch_node_masks: self.collect_branch_node_masks, added_removed_keys: self.added_removed_keys, + trie_cursor_metrics: self.trie_cursor_metrics, + hashed_cursor_metrics: self.hashed_cursor_metrics, } } @@ -268,6 +284,24 @@ impl StorageProof { self } + /// Set the trie cursor metrics cache to accumulate metrics into. + pub const fn with_trie_cursor_metrics( + mut self, + metrics: &'a mut TrieCursorMetricsCache, + ) -> Self { + self.trie_cursor_metrics = Some(metrics); + self + } + + /// Set the hashed cursor metrics cache to accumulate metrics into. + pub const fn with_hashed_cursor_metrics( + mut self, + metrics: &'a mut HashedCursorMetricsCache, + ) -> Self { + self.hashed_cursor_metrics = Some(metrics); + self + } + /// Configures the retainer to retain proofs for certain nodes which would otherwise fall /// outside the target set, when those nodes might be required to calculate the state root when /// keys have been added or removed to the trie. @@ -276,7 +310,7 @@ impl StorageProof { pub fn with_added_removed_keys( self, added_removed_keys: Option, - ) -> StorageProof { + ) -> StorageProof<'a, T, H, K2> { StorageProof { trie_cursor_factory: self.trie_cursor_factory, hashed_cursor_factory: self.hashed_cursor_factory, @@ -284,11 +318,13 @@ impl StorageProof { prefix_set: self.prefix_set, collect_branch_node_masks: self.collect_branch_node_masks, added_removed_keys, + trie_cursor_metrics: self.trie_cursor_metrics, + hashed_cursor_metrics: self.hashed_cursor_metrics, } } } -impl StorageProof +impl<'a, T, H, K> StorageProof<'a, T, H, K> where T: TrieCursorFactory, H: HashedCursorFactory, @@ -305,22 +341,37 @@ where /// Generate storage proof. pub fn storage_multiproof( - mut self, + self, targets: B256Set, ) -> Result { - let mut hashed_storage_cursor = + let mut discard_hashed_cursor_metrics = HashedCursorMetricsCache::default(); + let hashed_cursor_metrics = + self.hashed_cursor_metrics.unwrap_or(&mut discard_hashed_cursor_metrics); + + let hashed_storage_cursor = self.hashed_cursor_factory.hashed_storage_cursor(self.hashed_address)?; + let mut hashed_storage_cursor = + InstrumentedHashedCursor::new(hashed_storage_cursor, hashed_cursor_metrics); + // short circuit on empty storage if hashed_storage_cursor.is_storage_empty()? { return Ok(StorageMultiProof::empty()) } + let mut discard_trie_cursor_metrics = TrieCursorMetricsCache::default(); + let trie_cursor_metrics = + self.trie_cursor_metrics.unwrap_or(&mut discard_trie_cursor_metrics); + let target_nibbles = targets.into_iter().map(Nibbles::unpack).collect::>(); - self.prefix_set.extend_keys(target_nibbles.clone()); + let mut prefix_set = self.prefix_set; + prefix_set.extend_keys(target_nibbles.clone()); let trie_cursor = self.trie_cursor_factory.storage_trie_cursor(self.hashed_address)?; - let walker = TrieWalker::<_>::storage_trie(trie_cursor, self.prefix_set.freeze()) + + let trie_cursor = InstrumentedTrieCursor::new(trie_cursor, trie_cursor_metrics); + + let walker = TrieWalker::<_>::storage_trie(trie_cursor, prefix_set.freeze()) .with_added_removed_keys(self.added_removed_keys.as_ref()); let retainer = ProofRetainer::from_iter(target_nibbles) diff --git a/crates/trie/trie/src/proof_v2/mod.rs b/crates/trie/trie/src/proof_v2/mod.rs new file mode 100644 index 0000000000..dc3fb24e6f --- /dev/null +++ b/crates/trie/trie/src/proof_v2/mod.rs @@ -0,0 +1,712 @@ +//! Proof calculation version 2: Leaf-only implementation. +//! +//! This module provides a rewritten proof calculator that: +//! - Uses only leaf data (HashedAccounts/Storages) to generate proofs +//! - Returns proof nodes sorted lexicographically by path +//! - Automatically resets after each calculation +//! - Re-uses cursors across calculations +//! - Supports generic value types with lazy evaluation + +use crate::{ + hashed_cursor::{HashedCursor, HashedStorageCursor}, + trie_cursor::{TrieCursor, TrieStorageCursor}, +}; +use alloy_primitives::{B256, U256}; +use alloy_trie::TrieMask; +use reth_execution_errors::trie::StateProofError; +use reth_trie_common::{BranchNode, Nibbles, ProofTrieNode, RlpNode, TrieMasks, TrieNode}; +use tracing::{instrument, trace}; + +mod value; +pub use value::*; + +mod node; +use node::*; + +/// Target to use with the `tracing` crate. +static TRACE_TARGET: &str = "trie::proof_v2"; + +/// A proof calculator that generates merkle proofs using only leaf data. +/// +/// The calculator: +/// - Accepts one or more B256 proof targets sorted lexicographically +/// - Returns proof nodes sorted lexicographically by path +/// - Automatically resets after each calculation +/// - Re-uses cursors from one calculation to the next +#[derive(Debug)] +pub struct ProofCalculator { + /// Trie cursor for traversing stored branch nodes. + trie_cursor: TC, + /// Hashed cursor for iterating over leaf data. + hashed_cursor: HC, + /// Branches which are currently in the process of being constructed, each being a child of + /// the previous one. + branch_stack: Vec, + /// The path of the last branch in `branch_stack`. + branch_path: Nibbles, + /// Children of branches in the `branch_stack`. + /// + /// Each branch in `branch_stack` tracks which children are in this stack using its + /// `state_mask`; the number of children the branch has in this stack is equal to the number of + /// bits set in its `state_mask`. + /// + /// The children for the bottom branch in `branch_stack` are found at the bottom of this stack, + /// and so on. When a branch is removed from `branch_stack` its children are removed from this + /// one, and the branch is pushed onto this stack in their place (see [`Self::pop_branch`]. + child_stack: Vec>, + /// Free-list of re-usable buffers of [`RlpNode`]s, used for encoding branch nodes to RLP. + /// + /// We are generally able to re-use these buffers across different branch nodes for the + /// duration of a proof calculation, but occasionally we will lose one when when a branch + /// node is returned as a `ProofTrieNode`. + rlp_nodes_bufs: Vec>, + /// Re-usable byte buffer, used for RLP encoding. + rlp_encode_buf: Vec, +} + +impl ProofCalculator { + /// Create a new [`ProofCalculator`] instance for calculating account proofs. + pub const fn new(trie_cursor: TC, hashed_cursor: HC) -> Self { + Self { + trie_cursor, + hashed_cursor, + branch_stack: Vec::<_>::new(), + branch_path: Nibbles::new(), + child_stack: Vec::<_>::new(), + rlp_nodes_bufs: Vec::<_>::new(), + rlp_encode_buf: Vec::<_>::new(), + } + } +} + +impl ProofCalculator +where + TC: TrieCursor, + HC: HashedCursor, + VE: LeafValueEncoder, +{ + /// Takes a re-usable `RlpNode` buffer from the internal free-list, or allocates a new one if + /// the free-list is empty. + /// + /// The returned Vec will have a length of zero. + fn take_rlp_nodes_buf(&mut self) -> Vec { + self.rlp_nodes_bufs + .pop() + .map(|mut buf| { + buf.clear(); + buf + }) + .unwrap_or_else(|| Vec::with_capacity(16)) + } + + /// Pushes a new branch onto the `branch_stack`, while also pushing the given leaf onto the + /// `child_stack`. + /// + /// This method expects that there already exists a child on the `child_stack`, and that that + /// child has a non-zero short key. The new branch is constructed based on the top child from + /// the `child_stack` and the given leaf. + fn push_new_branch(&mut self, leaf_key: Nibbles, leaf_val: VE::DeferredEncoder) { + // First determine the new leaf's shortkey relative to the current branch. If there is no + // current branch then the short key is the full key. + let leaf_short_key = if self.branch_stack.is_empty() { + leaf_key + } else { + // When there is a current branch then trim off its path as well as the nibble that it + // has set for this leaf. + trim_nibbles_prefix(&leaf_key, self.branch_path.len() + 1) + }; + + trace!( + target: TRACE_TARGET, + ?leaf_short_key, + branch_path = ?self.branch_path, + "push_new_branch: called", + ); + + // Get the new branch's first child, which is the child on the top of the stack with which + // the new leaf shares the same nibble on the current branch. + let first_child = self + .child_stack + .last_mut() + .expect("push_branch can't be called with empty child_stack"); + + let first_child_short_key = first_child.short_key(); + debug_assert!( + !first_child_short_key.is_empty(), + "push_branch called when top child on stack is not a leaf or extension with a short key", + ); + + // Determine how many nibbles are shared between the new branch's first child and the new + // leaf. This common prefix will be the extension of the new branch + let common_prefix_len = first_child_short_key.common_prefix_length(&leaf_short_key); + + // Trim off the common prefix from the first child's short key, plus one nibble which will + // stored by the new branch itself in its state mask. + let first_child_nibble = first_child_short_key.get_unchecked(common_prefix_len); + first_child.trim_short_key_prefix(common_prefix_len + 1); + + // Similarly, trim off the common prefix, plus one nibble for the new branch, from the new + // leaf's short key. + let leaf_nibble = leaf_short_key.get_unchecked(common_prefix_len); + let leaf_short_key = trim_nibbles_prefix(&leaf_short_key, common_prefix_len + 1); + + // Push the new leaf onto the child stack; it will be the second child of the new branch. + // The new branch's first child is the child already on the top of the stack, for which + // we've already adjusted its short key. + self.child_stack + .push(ProofTrieBranchChild::Leaf { short_key: leaf_short_key, value: leaf_val }); + + // Construct the state mask of the new branch, and push the new branch onto the branch + // stack. + self.branch_stack.push(ProofTrieBranch { + ext_len: common_prefix_len as u8, + state_mask: { + let mut m = TrieMask::default(); + m.set_bit(first_child_nibble); + m.set_bit(leaf_nibble); + m + }, + tree_mask: TrieMask::default(), + hash_mask: TrieMask::default(), + }); + + // Update the branch path to reflect the new branch which was just pushed. Its path will be + // the path of the previous branch, plus the nibble shared by each child, plus the parent + // extension (denoted by a non-zero `ext_len`). Since the new branch's path is a prefix of + // the original leaf_key we can just slice that. + // + // If the branch is the first branch then we do not add the extra 1, as there is no nibble + // in a parent branch to account for. + let branch_path_len = self.branch_path.len() + + common_prefix_len + + if self.branch_stack.len() == 1 { 0 } else { 1 }; + self.branch_path = leaf_key.slice_unchecked(0, branch_path_len); + + trace!( + target: TRACE_TARGET, + ?leaf_short_key, + ?common_prefix_len, + new_branch = ?self.branch_stack.last().expect("branch_stack was just pushed to"), + ?branch_path_len, + branch_path = ?self.branch_path, + "push_new_branch: returning", + ); + } + + /// Pops the top branch off of the `branch_stack`, hashes its children on the `child_stack`, and + /// replaces those children on the `child_stack`. The `branch_path` field will be updated + /// accordingly. + /// + /// # Panics + /// + /// This method panics if `branch_stack` is empty. + fn pop_branch(&mut self) -> Result<(), StateProofError> { + let mut rlp_nodes_buf = self.take_rlp_nodes_buf(); + let branch = self.branch_stack.pop().expect("branch_stack cannot be empty"); + + trace!( + target: TRACE_TARGET, + ?branch, + branch_path = ?self.branch_path, + "pop_branch: called", + ); + + // Take the branch's children off the stack, using the state mask to determine how many + // there are. + let num_children = branch.state_mask.count_ones() as usize; + debug_assert!(num_children > 1, "A branch must have at least two children"); + debug_assert!( + self.child_stack.len() >= num_children, + "Stack is missing necessary children" + ); + let children = self.child_stack.drain(self.child_stack.len() - num_children..); + + // We will be pushing the branch onto the child stack, which will require its parent + // extension's short key (if it has a parent extension). Calculate this short key from the + // `branch_path` prior to modifying the `branch_path`. + let short_key = trim_nibbles_prefix( + &self.branch_path, + self.branch_path.len() - branch.ext_len as usize, + ); + + // Update the branch_path. If this branch is the only branch then only its extension needs + // to be trimmed, otherwise we also need to remove its nibble from its parent. + let new_path_len = self.branch_path.len() - + branch.ext_len as usize - + if self.branch_stack.is_empty() { 0 } else { 1 }; + + debug_assert!(self.branch_path.len() >= new_path_len); + self.branch_path = self.branch_path.slice_unchecked(0, new_path_len); + + // From here we will be encoding the branch node and pushing it onto the child stack, + // replacing its children. + + // Collect children into an `RlpNode` Vec by calling into_rlp on each. + for child in children { + self.rlp_encode_buf.clear(); + let (child_rlp_node, freed_rlp_nodes_buf) = child.into_rlp(&mut self.rlp_encode_buf)?; + rlp_nodes_buf.push(child_rlp_node); + + // If there is an `RlpNode` buffer which can be re-used then push it onto the free-list. + if let Some(buf) = freed_rlp_nodes_buf { + self.rlp_nodes_bufs.push(buf); + } + } + + debug_assert_eq!( + rlp_nodes_buf.len(), + branch.state_mask.count_ones() as usize, + "children length must match number of bits set in state_mask" + ); + + // Construct the `BranchNode`. + let branch_node = BranchNode::new(rlp_nodes_buf, branch.state_mask); + + // Wrap the `BranchNode` so it can be pushed onto the child stack. + let branch_as_child = if short_key.is_empty() { + // If there is no extension then push a branch node + ProofTrieBranchChild::Branch(branch_node) + } else { + // Otherwise push an extension node + ProofTrieBranchChild::Extension { short_key, child: branch_node } + }; + + self.child_stack.push(branch_as_child); + Ok(()) + } + + /// Adds a single leaf for a key to the stack, possibly collapsing an existing branch and/or + /// creating a new one depending on the path of the key. + fn add_leaf(&mut self, key: Nibbles, val: VE::DeferredEncoder) -> Result<(), StateProofError> { + loop { + // Get the branch currently being built. If there are no branches on the stack then it + // means either the trie is empty or only a single leaf has been added previously. + let curr_branch = match self.branch_stack.last_mut() { + Some(curr_branch) => curr_branch, + None if self.child_stack.is_empty() => { + // If the child stack is empty then this is the first leaf, push it and be done + self.child_stack + .push(ProofTrieBranchChild::Leaf { short_key: key, value: val }); + return Ok(()) + } + None => { + // If the child stack is not empty then it must only have a single other child + // which is either a leaf or extension with a non-zero short key. + debug_assert_eq!(self.child_stack.len(), 1); + debug_assert!(!self + .child_stack + .last() + .expect("already checked for emptiness") + .short_key() + .is_empty()); + self.push_new_branch(key, val); + return Ok(()) + } + }; + + // Find the common prefix length, which is the number of nibbles shared between the + // current branch and the key. + let common_prefix_len = self.branch_path.common_prefix_length(&key); + + // If the current branch does not share all of its nibbles with the new key then it is + // not the parent of the new key. In this case the current branch will have no more + // children. We can pop it and loop back to the top to try again with its parent branch. + if common_prefix_len < self.branch_path.len() { + self.pop_branch()?; + continue + } + + // If the current branch is a prefix of the new key then the leaf is a child of the + // branch. If the branch doesn't have the leaf's nibble set then the leaf can be added + // directly, otherwise a new branch must be created in-between this branch and that + // existing child. + let nibble = key.get_unchecked(common_prefix_len); + if curr_branch.state_mask.is_bit_set(nibble) { + // This method will also push the new leaf onto the `child_stack`. + self.push_new_branch(key, val); + } else { + curr_branch.state_mask.set_bit(nibble); + + // Add this leaf as a new child of the current branch (no intermediate branch + // needed). + self.child_stack.push(ProofTrieBranchChild::Leaf { + short_key: key.slice_unchecked(common_prefix_len + 1, key.len()), + value: val, + }); + } + + return Ok(()) + } + } + + /// Internal implementation of proof calculation. Assumes both cursors have already been reset. + /// See docs on [`Self::proof`] for expected behavior. + fn proof_inner( + &mut self, + value_encoder: &VE, + targets: impl IntoIterator, + ) -> Result, StateProofError> { + trace!(target: TRACE_TARGET, "proof_inner called"); + + // In debug builds, verify that targets are sorted + #[cfg(debug_assertions)] + let targets = { + let mut prev: Option = None; + targets.into_iter().inspect(move |target| { + if let Some(prev) = prev { + debug_assert!( + prev <= *target, + "targets must be sorted lexicographically: {:?} > {:?}", + prev, + target + ); + } + prev = Some(*target); + }) + }; + + #[cfg(not(debug_assertions))] + let targets = targets.into_iter(); + + // Ensure initial state is cleared. By the end of the method call these should be empty once + // again. + debug_assert!(self.branch_stack.is_empty()); + debug_assert!(self.branch_path.is_empty()); + debug_assert!(self.child_stack.is_empty()); + + // Silence unused variable warning for now + let _ = targets; + + let mut proof_nodes = Vec::new(); + let mut hashed_cursor_current = self.hashed_cursor.seek(B256::ZERO)?; + loop { + trace!(target: TRACE_TARGET, ?hashed_cursor_current, "proof_inner loop"); + + // Fetch the next leaf from the hashed cursor, converting the key to Nibbles and + // immediately creating the DeferredValueEncoder so that encoding of the leaf value can + // begin ASAP. + let Some((key, val)) = hashed_cursor_current.map(|(key_b256, val)| { + debug_assert_eq!(key_b256.len(), 32); + // SAFETY: key is a B256 and so is exactly 32-bytes. + let key = unsafe { Nibbles::unpack_unchecked(key_b256.as_slice()) }; + let val = value_encoder.deferred_encoder(key_b256, val); + (key, val) + }) else { + break + }; + + self.add_leaf(key, val)?; + hashed_cursor_current = self.hashed_cursor.next()?; + } + + // Once there's no more leaves we can pop the remaining branches, if any. + while !self.branch_stack.is_empty() { + self.pop_branch()?; + } + + // At this point the branch stack should be empty. If the child stack is empty it means no + // keys were ever iterated from the hashed cursor in the first place. Otherwise there should + // only be a single node left: the root node. + debug_assert!(self.branch_stack.is_empty()); + debug_assert!(self.branch_path.is_empty()); + debug_assert!(self.child_stack.len() < 2); + + // Determine the root node based on the child stack, and push the proof of the root node + // onto the result stack. + let root_node = if let Some(node) = self.child_stack.pop() { + self.rlp_encode_buf.clear(); + node.into_trie_node(&mut self.rlp_encode_buf)? + } else { + TrieNode::EmptyRoot + }; + + proof_nodes.push(ProofTrieNode { + path: Nibbles::new(), // root path + node: root_node, + masks: TrieMasks::none(), + }); + + Ok(proof_nodes) + } +} + +impl ProofCalculator +where + TC: TrieCursor, + HC: HashedCursor, + VE: LeafValueEncoder, +{ + /// Generate a proof for the given targets. + /// + /// Given lexicographically sorted targets, returns nodes whose paths are a prefix of any + /// target. The returned nodes will be sorted lexicographically by path. + /// + /// # Panics + /// + /// In debug builds, panics if the targets are not sorted lexicographically. + #[instrument(target = TRACE_TARGET, level = "trace", skip_all)] + pub fn proof( + &mut self, + value_encoder: &VE, + targets: impl IntoIterator, + ) -> Result, StateProofError> { + self.trie_cursor.reset(); + self.hashed_cursor.reset(); + self.proof_inner(value_encoder, targets) + } +} + +/// A proof calculator for storage tries. +pub type StorageProofCalculator = ProofCalculator; + +impl StorageProofCalculator +where + TC: TrieStorageCursor, + HC: HashedStorageCursor, +{ + /// Create a new [`StorageProofCalculator`] instance. + pub const fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self { + Self::new(trie_cursor, hashed_cursor) + } + + /// Generate a proof for a storage trie at the given hashed address. + /// + /// Given lexicographically sorted targets, returns nodes whose paths are a prefix of any + /// target. The returned nodes will be sorted lexicographically by path. + /// + /// # Panics + /// + /// In debug builds, panics if the targets are not sorted lexicographically. + #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))] + pub fn storage_proof( + &mut self, + hashed_address: B256, + targets: impl IntoIterator, + ) -> Result, StateProofError> { + /// Static storage value encoder instance used by all storage proofs. + static STORAGE_VALUE_ENCODER: StorageValueEncoder = StorageValueEncoder; + + self.hashed_cursor.set_hashed_address(hashed_address); + + // Shortcut: check if storage is empty + if self.hashed_cursor.is_storage_empty()? { + // Return a single EmptyRoot node at the root path + return Ok(vec![ProofTrieNode { + path: Nibbles::default(), + node: TrieNode::EmptyRoot, + masks: TrieMasks::none(), + }]) + } + + // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has + // been checked. + self.trie_cursor.set_hashed_address(hashed_address); + + // Use the static StorageValueEncoder and pass it to proof_inner + self.proof_inner(&STORAGE_VALUE_ENCODER, targets) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + hashed_cursor::{mock::MockHashedCursorFactory, HashedCursorFactory}, + proof::Proof, + trie_cursor::{mock::MockTrieCursorFactory, TrieCursorFactory}, + }; + use alloy_primitives::map::B256Map; + use alloy_rlp::Decodable; + use itertools::Itertools; + use reth_trie_common::{HashedPostState, MultiProofTargets}; + use std::collections::BTreeMap; + + /// Target to use with the `tracing` crate. + static TRACE_TARGET: &str = "trie::proof_v2::tests"; + + /// A test harness for comparing `ProofCalculator` and legacy `Proof` implementations. + /// + /// This harness creates mock cursor factories from a `HashedPostState` and provides + /// a method to test that both proof implementations produce equivalent results. + struct ProofTestHarness { + /// Mock factory for trie cursors (empty by default for leaf-only tests) + trie_cursor_factory: MockTrieCursorFactory, + /// Mock factory for hashed cursors, populated from `HashedPostState` + hashed_cursor_factory: MockHashedCursorFactory, + } + + impl ProofTestHarness { + /// Creates a new test harness from a `HashedPostState`. + /// + /// The `HashedPostState` is used to populate the mock hashed cursor factory directly. + /// The trie cursor factory is empty by default, suitable for testing the leaf-only + /// proof calculator. + fn new(post_state: HashedPostState) -> Self { + trace!(target: TRACE_TARGET, ?post_state, "Creating ProofTestHarness"); + + // Extract accounts from post state, filtering out None (deleted accounts) + let hashed_accounts: BTreeMap = post_state + .accounts + .into_iter() + .filter_map(|(addr, account)| account.map(|acc| (addr, acc))) + .collect(); + + // Extract storage tries from post state + let hashed_storage_tries: B256Map> = post_state + .storages + .into_iter() + .map(|(addr, hashed_storage)| { + // Convert HashedStorage to BTreeMap, filtering out zero values (deletions) + let storage_map: BTreeMap = hashed_storage + .storage + .into_iter() + .filter_map(|(slot, value)| (value != U256::ZERO).then_some((slot, value))) + .collect(); + (addr, storage_map) + }) + .collect(); + + // Ensure that there's a storage trie dataset for every storage trie, even if empty. + let storage_trie_nodes: B256Map> = hashed_storage_tries + .keys() + .copied() + .map(|addr| (addr, Default::default())) + .collect(); + + // Create mock hashed cursor factory populated with the post state data + let hashed_cursor_factory = + MockHashedCursorFactory::new(hashed_accounts, hashed_storage_tries); + + // Create empty trie cursor factory (leaf-only calculator doesn't need trie nodes) + let trie_cursor_factory = + MockTrieCursorFactory::new(BTreeMap::new(), storage_trie_nodes); + + Self { trie_cursor_factory, hashed_cursor_factory } + } + + /// Asserts that `ProofCalculator` and legacy `Proof` produce equivalent results for account + /// proofs. + /// + /// This method calls both implementations with the given account targets and compares + /// the results. For now, it performs a basic comparison by checking that both succeed + /// and produce non-empty results. More detailed comparison logic can be added as needed. + fn assert_proof( + &self, + // For now ProofCalculator doesn't support real targets, we just compare calculated + // roots. + _targets: impl IntoIterator + Clone, + ) -> Result<(), StateProofError> { + // Create ProofCalculator (proof_v2) with account cursors + let trie_cursor = self.trie_cursor_factory.account_trie_cursor()?; + let hashed_cursor = self.hashed_cursor_factory.hashed_account_cursor()?; + + // Call ProofCalculator::proof with account targets + let value_encoder = SyncAccountValueEncoder::new( + self.trie_cursor_factory.clone(), + self.hashed_cursor_factory.clone(), + ); + let mut proof_calculator = ProofCalculator::new(trie_cursor, hashed_cursor); + let proof_v2_result = proof_calculator.proof(&value_encoder, [Nibbles::new()])?; + + // Call Proof::multiproof (legacy implementation) + let proof_legacy_result = + Proof::new(self.trie_cursor_factory.clone(), self.hashed_cursor_factory.clone()) + .multiproof(MultiProofTargets::default())?; + + // Decode and sort legacy proof nodes + let proof_legacy_nodes = proof_legacy_result + .account_subtree + .iter() + .map(|(path, node_enc)| { + let mut buf = node_enc.as_ref(); + let node = TrieNode::decode(&mut buf) + .expect("legacy implementation should not produce malformed proof nodes"); + + ProofTrieNode { + path: *path, + node, + masks: TrieMasks { + hash_mask: proof_legacy_result + .branch_node_hash_masks + .get(path) + .copied(), + tree_mask: proof_legacy_result + .branch_node_tree_masks + .get(path) + .copied(), + }, + } + }) + .sorted_by_key(|n| n.path) + .collect::>(); + + // Basic comparison: both should succeed and produce identical results + assert_eq!(proof_legacy_nodes, proof_v2_result); + + Ok(()) + } + } + + mod proptest_tests { + use super::*; + use alloy_primitives::{map::B256Map, U256}; + use proptest::prelude::*; + use reth_primitives_traits::Account; + use reth_trie_common::HashedPostState; + + /// Generate a strategy for Account values + fn account_strategy() -> impl Strategy { + (any::(), any::(), any::<[u8; 32]>()).prop_map( + |(nonce, balance, code_hash)| Account { + nonce, + balance: U256::from(balance), + bytecode_hash: Some(B256::from(code_hash)), + }, + ) + } + + /// Generate a strategy for `HashedPostState` with random accounts + fn hashed_post_state_strategy() -> impl Strategy { + prop::collection::vec((any::<[u8; 32]>(), account_strategy()), 0..20).prop_map( + |accounts| { + let account_map = accounts + .into_iter() + .map(|(addr_bytes, account)| (B256::from(addr_bytes), Some(account))) + .collect::>(); + + // All accounts have empty storages. + let storages = account_map + .keys() + .copied() + .map(|addr| (addr, Default::default())) + .collect::>(); + + HashedPostState { accounts: account_map, storages } + }, + ) + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(5000))] + + /// Tests that ProofCalculator produces valid proofs for randomly generated + /// HashedPostState with empty target sets. + /// + /// This test: + /// - Generates random accounts in a HashedPostState + /// - Creates a test harness with the generated state + /// - Calls assert_proof with an empty target set + /// - Verifies both ProofCalculator and legacy Proof succeed + #[test] + fn proptest_proof_with_empty_targets( + post_state in hashed_post_state_strategy(), + ) { + reth_tracing::init_test_tracing(); + let harness = ProofTestHarness::new(post_state); + + // Pass empty target set + harness.assert_proof(std::iter::empty()).expect("Proof generation failed"); + } + } + } +} diff --git a/crates/trie/trie/src/proof_v2/node.rs b/crates/trie/trie/src/proof_v2/node.rs new file mode 100644 index 0000000000..dfe9d15053 --- /dev/null +++ b/crates/trie/trie/src/proof_v2/node.rs @@ -0,0 +1,206 @@ +use crate::proof_v2::DeferredValueEncoder; +use alloy_rlp::Encodable; +use alloy_trie::nodes::ExtensionNodeRef; +use reth_execution_errors::trie::StateProofError; +use reth_trie_common::{ + BranchNode, ExtensionNode, LeafNode, LeafNodeRef, Nibbles, RlpNode, TrieMask, TrieNode, +}; + +/// A trie node which is the child of a branch in the trie. +#[derive(Debug)] +pub(crate) enum ProofTrieBranchChild { + /// A leaf node whose value has yet to be calculated and encoded. + Leaf { + /// The short key of the leaf. + short_key: Nibbles, + /// The [`DeferredValueEncoder`] which will encode the leaf's value. + value: RF, + }, + /// An extension node whose child branch has not yet been converted to an [`RlpNode`] + Extension { + /// The short key of the leaf. + short_key: Nibbles, + /// The node of the child branch. + child: BranchNode, + }, + /// A branch node whose children have already been flattened into [`RlpNode`]s. + Branch(BranchNode), +} + +impl ProofTrieBranchChild { + /// Converts this child into its RLP node representation. This potentially also returns an + /// `RlpNode` buffer which can be re-used for other [`ProofTrieBranchChild`]s. + pub(crate) fn into_rlp( + self, + buf: &mut Vec, + ) -> Result<(RlpNode, Option>), StateProofError> { + match self { + Self::Leaf { short_key, value } => { + // RLP encode the value itself + value.encode(buf)?; + let value_enc_len = buf.len(); + + // Determine the required buffer size for the encoded leaf + let leaf_enc_len = LeafNodeRef::new(&short_key, buf).length(); + + // We want to re-use buf for the encoding of the leaf node as well. To do this we + // will keep appending to it, leaving the already encoded value in-place. First we + // must ensure the buffer is big enough, then we'll split. + buf.resize(value_enc_len + leaf_enc_len, 0); + + // SAFETY we have just resized the above to be greater than `value_enc_len`, so it + // must be in-bounds. + let (value_buf, mut leaf_buf) = + unsafe { buf.split_at_mut_unchecked(value_enc_len) }; + + // Encode the leaf into the right side of the split buffer, and return the RlpNode. + LeafNodeRef::new(&short_key, value_buf).encode(&mut leaf_buf); + Ok((RlpNode::from_rlp(&buf[value_enc_len..]), None)) + } + Self::Extension { short_key, child } => { + let (branch_rlp, rlp_buf) = Self::Branch(child).into_rlp(buf)?; + buf.clear(); + + ExtensionNodeRef::new(&short_key, branch_rlp.as_slice()).encode(buf); + Ok((RlpNode::from_rlp(buf), rlp_buf)) + } + Self::Branch(branch_node) => { + branch_node.encode(buf); + Ok((RlpNode::from_rlp(buf), Some(branch_node.stack))) + } + } + } + + /// Converts this child into a [`TrieNode`]. + pub(crate) fn into_trie_node(self, buf: &mut Vec) -> Result { + match self { + Self::Leaf { short_key, value } => { + value.encode(buf)?; + Ok(TrieNode::Leaf(LeafNode::new(short_key, core::mem::take(buf)))) + } + Self::Extension { short_key, child } => { + child.encode(buf); + let child_rlp_node = RlpNode::from_rlp(buf); + Ok(TrieNode::Extension(ExtensionNode { key: short_key, child: child_rlp_node })) + } + Self::Branch(branch_node) => Ok(TrieNode::Branch(branch_node)), + } + } + + /// Returns the short key of the child, if it is a leaf or extension, or empty if its a + /// [`Self::Branch`]. + pub(crate) fn short_key(&self) -> &Nibbles { + match self { + Self::Leaf { short_key, .. } | Self::Extension { short_key, .. } => short_key, + Self::Branch(_) => { + static EMPTY_NIBBLES: Nibbles = Nibbles::new(); + &EMPTY_NIBBLES + } + } + } + + /// Trims the given number of nibbles off the head of the short key. + /// + /// If the node is an extension and the given length is the same as its short key length, then + /// the node is replaced with its child. + /// + /// # Panics + /// + /// - If the given len is longer than the short key + /// - If the given len is the same as the length of a leaf's short key + /// - If the node is a [`Self::Branch`] + pub(crate) fn trim_short_key_prefix(&mut self, len: usize) { + match self { + Self::Extension { short_key, child } if short_key.len() == len => { + *self = Self::Branch(core::mem::take(child)); + } + Self::Leaf { short_key, .. } | Self::Extension { short_key, .. } => { + *short_key = trim_nibbles_prefix(short_key, len); + } + Self::Branch(_) => { + panic!("Cannot call `trim_short_key_prefix` on Branch") + } + } + } +} + +/// A single branch in the trie which is under construction. The actual child nodes of the branch +/// will be tracked as [`ProofTrieBranchChild`]s on a stack. +#[derive(Debug)] +pub(crate) struct ProofTrieBranch { + /// The length of the parent extension node's short key. If zero then the branch's parent is + /// not an extension but instead another branch. + pub(crate) ext_len: u8, + /// A mask tracking which child nibbles are set on the branch so far. There will be a single + /// child on the stack for each set bit. + pub(crate) state_mask: TrieMask, + /// A subset of `state_mask`. Each bit is set if the `state_mask` bit is set and: + /// - The child is a branch which is stored in the DB. + /// - The child is an extension whose child branch is stored in the DB. + #[expect(unused)] + pub(crate) tree_mask: TrieMask, + /// A subset of `state_mask`. Each bit is set if the hash for the child is cached in the DB. + #[expect(unused)] + pub(crate) hash_mask: TrieMask, +} + +/// Trims the first `len` nibbles from the head of the given `Nibbles`. +/// +/// # Panics +/// +/// Panics if the given `len` is greater than the length of the `Nibbles`. +pub(crate) fn trim_nibbles_prefix(n: &Nibbles, len: usize) -> Nibbles { + debug_assert!(n.len() >= len); + n.slice_unchecked(len, n.len()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trim_nibbles_prefix_basic() { + // Create nibbles [1, 2, 3, 4, 5, 6] + let nibbles = Nibbles::from_nibbles([1, 2, 3, 4, 5, 6]); + + // Trim first 2 nibbles + let trimmed = trim_nibbles_prefix(&nibbles, 2); + assert_eq!(trimmed.len(), 4); + + // Verify the remaining nibbles are [3, 4, 5, 6] + assert_eq!(trimmed.get(0), Some(3)); + assert_eq!(trimmed.get(1), Some(4)); + assert_eq!(trimmed.get(2), Some(5)); + assert_eq!(trimmed.get(3), Some(6)); + } + + #[test] + fn test_trim_nibbles_prefix_zero() { + // Create nibbles [10, 11, 12, 13] + let nibbles = Nibbles::from_nibbles([10, 11, 12, 13]); + + // Trim zero nibbles - should return identical nibbles + let trimmed = trim_nibbles_prefix(&nibbles, 0); + assert_eq!(trimmed, nibbles); + } + + #[test] + fn test_trim_nibbles_prefix_all() { + // Create nibbles [1, 2, 3, 4] + let nibbles = Nibbles::from_nibbles([1, 2, 3, 4]); + + // Trim all nibbles - should return empty + let trimmed = trim_nibbles_prefix(&nibbles, 4); + assert!(trimmed.is_empty()); + } + + #[test] + fn test_trim_nibbles_prefix_empty() { + // Create empty nibbles + let nibbles = Nibbles::new(); + + // Trim zero from empty - should return empty + let trimmed = trim_nibbles_prefix(&nibbles, 0); + assert!(trimmed.is_empty()); + } +} diff --git a/crates/trie/trie/src/proof_v2/value.rs b/crates/trie/trie/src/proof_v2/value.rs new file mode 100644 index 0000000000..9f5f97a271 --- /dev/null +++ b/crates/trie/trie/src/proof_v2/value.rs @@ -0,0 +1,174 @@ +//! Generic value encoder types for proof calculation with lazy evaluation. + +use crate::{ + hashed_cursor::HashedCursorFactory, proof_v2::ProofCalculator, trie_cursor::TrieCursorFactory, +}; +use alloy_primitives::{B256, U256}; +use alloy_rlp::Encodable; +use reth_execution_errors::trie::StateProofError; +use reth_primitives_traits::Account; +use reth_trie_common::Nibbles; +use std::rc::Rc; + +/// A trait for deferred RLP-encoding of leaf values. +pub trait DeferredValueEncoder { + /// RLP encodes the value into the provided buffer. + /// + /// # Arguments + /// + /// * `buf` - A mutable buffer to encode the data into + fn encode(self, buf: &mut Vec) -> Result<(), StateProofError>; +} + +/// A trait for RLP-encoding values for proof calculation. This trait is designed to allow the lazy +/// computation of leaf values in a generic way. +/// +/// When calculating a leaf value in a storage trie the [`DeferredValueEncoder`] simply holds onto +/// the slot value, and the `encode` method synchronously RLP-encodes it. +/// +/// When calculating a leaf value in the accounts trie we create a [`DeferredValueEncoder`] to +/// initiate any asynchronous computation of the account's storage root we want to do. Later we call +/// [`DeferredValueEncoder::encode`] to obtain the result of that computation and RLP-encode it. +pub trait LeafValueEncoder { + /// The type of value being encoded (e.g., U256 for storage, Account for accounts). + type Value; + + /// The type that will compute and encode the value when needed. + type DeferredEncoder: DeferredValueEncoder; + + /// Returns an encoder that will RLP-encode the value when its `encode` method is called. + /// + /// # Arguments + /// + /// * `key` - The key the value was stored at in the DB + /// * `value` - The value to encode + /// + /// The returned deferred encoder will be called as late as possible in the algorithm to + /// maximize the time available for parallel computation (e.g., storage root calculation). + fn deferred_encoder(&self, key: B256, value: Self::Value) -> Self::DeferredEncoder; +} + +/// An encoder for storage slot values. +/// +/// This encoder simply RLP-encodes U256 storage values directly. +#[derive(Debug, Clone, Copy, Default)] +pub struct StorageValueEncoder; + +/// The deferred encoder for a storage slot value. +#[derive(Debug, Clone, Copy)] +pub struct StorageDeferredValueEncoder(U256); + +impl DeferredValueEncoder for StorageDeferredValueEncoder { + fn encode(self, buf: &mut Vec) -> Result<(), StateProofError> { + self.0.encode(buf); + Ok(()) + } +} + +impl LeafValueEncoder for StorageValueEncoder { + type Value = U256; + type DeferredEncoder = StorageDeferredValueEncoder; + + fn deferred_encoder(&self, _key: B256, value: Self::Value) -> Self::DeferredEncoder { + StorageDeferredValueEncoder(value) + } +} + +/// An account value encoder that synchronously computes storage roots. +/// +/// This encoder contains factories for creating trie and hashed cursors. Storage roots are +/// computed synchronously within the deferred encoder using a `StorageProofCalculator`. +#[derive(Debug, Clone)] +pub struct SyncAccountValueEncoder { + /// Factory for creating trie cursors. + trie_cursor_factory: Rc, + /// Factory for creating hashed cursors. + hashed_cursor_factory: Rc, +} + +impl SyncAccountValueEncoder { + /// Create a new account value encoder with the given factories. + pub fn new(trie_cursor_factory: T, hashed_cursor_factory: H) -> Self { + Self { + trie_cursor_factory: Rc::new(trie_cursor_factory), + hashed_cursor_factory: Rc::new(hashed_cursor_factory), + } + } +} + +/// The deferred encoder for an account value with synchronous storage root calculation. +#[derive(Debug, Clone)] +pub struct SyncAccountDeferredValueEncoder { + trie_cursor_factory: Rc, + hashed_cursor_factory: Rc, + hashed_address: B256, + account: Account, +} + +impl DeferredValueEncoder for SyncAccountDeferredValueEncoder +where + T: TrieCursorFactory, + H: HashedCursorFactory, +{ + // Synchronously computes the storage root for this account and RLP-encodes the resulting + // `TrieAccount` into `buf` + fn encode(self, buf: &mut Vec) -> Result<(), StateProofError> { + // Create cursors for storage proof calculation + let trie_cursor = self.trie_cursor_factory.storage_trie_cursor(self.hashed_address)?; + let hashed_cursor = + self.hashed_cursor_factory.hashed_storage_cursor(self.hashed_address)?; + + // Create storage proof calculator with StorageValueEncoder + let mut storage_proof_calculator = ProofCalculator::new_storage(trie_cursor, hashed_cursor); + + // Compute storage root by calling storage_proof with the root path as a target. + // This returns just the root node of the storage trie. + let storage_root = storage_proof_calculator + .storage_proof(self.hashed_address, [Nibbles::new()]) + .map(|nodes| { + // Encode the root node to RLP and hash it + let root_node = + nodes.first().expect("storage_proof always returns at least the root"); + root_node.node.encode(buf); + + let storage_root = alloy_primitives::keccak256(buf.as_slice()); + + // Clear the buffer so we can re-use it to encode the TrieAccount + buf.clear(); + + storage_root + })?; + + // Combine account with storage root to create TrieAccount + let trie_account = self.account.into_trie_account(storage_root); + + // Encode the trie account + trie_account.encode(buf); + + Ok(()) + } +} + +impl LeafValueEncoder for SyncAccountValueEncoder +where + T: TrieCursorFactory, + H: HashedCursorFactory, +{ + type Value = Account; + type DeferredEncoder = SyncAccountDeferredValueEncoder; + + fn deferred_encoder( + &self, + hashed_address: B256, + account: Self::Value, + ) -> Self::DeferredEncoder { + // Return a deferred encoder that will synchronously compute the storage root when encode() + // is called. + SyncAccountDeferredValueEncoder { + trie_cursor_factory: self.trie_cursor_factory.clone(), + hashed_cursor_factory: self.hashed_cursor_factory.clone(), + hashed_address, + account, + } + } +} diff --git a/crates/trie/trie/src/trie_cursor/in_memory.rs b/crates/trie/trie/src/trie_cursor/in_memory.rs index 308a8edbd0..941fbf9633 100644 --- a/crates/trie/trie/src/trie_cursor/in_memory.rs +++ b/crates/trie/trie/src/trie_cursor/in_memory.rs @@ -1,4 +1,4 @@ -use super::{TrieCursor, TrieCursorFactory}; +use super::{TrieCursor, TrieCursorFactory, TrieStorageCursor}; use crate::{forward_cursor::ForwardInMemoryCursor, updates::TrieUpdatesSorted}; use alloy_primitives::B256; use reth_storage_errors::db::DatabaseError; @@ -37,29 +37,16 @@ where fn account_trie_cursor(&self) -> Result, DatabaseError> { let cursor = self.cursor_factory.account_trie_cursor()?; - Ok(InMemoryTrieCursor::new(Some(cursor), self.trie_updates.as_ref().account_nodes_ref())) + Ok(InMemoryTrieCursor::new_account(cursor, self.trie_updates.as_ref())) } fn storage_trie_cursor( &self, hashed_address: B256, ) -> Result, DatabaseError> { - // if the storage trie has no updates then we use this as the in-memory overlay. - const EMPTY_UPDATES: &[(Nibbles, Option)] = &[]; - - let storage_trie_updates = - self.trie_updates.as_ref().storage_tries_ref().get(&hashed_address); - let (storage_nodes, cleared) = storage_trie_updates - .map(|u| (u.storage_nodes_ref(), u.is_deleted())) - .unwrap_or((EMPTY_UPDATES, false)); - - let cursor = if cleared { - None - } else { - Some(self.cursor_factory.storage_trie_cursor(hashed_address)?) - }; - - Ok(InMemoryTrieCursor::new(cursor, storage_nodes)) + let trie_updates = self.trie_updates.as_ref(); + let cursor = self.cursor_factory.storage_trie_cursor(hashed_address)?; + Ok(InMemoryTrieCursor::new_storage(cursor, trie_updates, hashed_address)) } } @@ -67,8 +54,10 @@ where /// It will always give precedence to the data from the trie updates. #[derive(Debug)] pub struct InMemoryTrieCursor<'a, C> { - /// The underlying cursor. If None then it is assumed there is no DB data. - cursor: Option, + /// The underlying cursor. + cursor: C, + /// Whether the underlying cursor should be ignored (when storage trie was wiped). + cursor_wiped: bool, /// Entry that `cursor` is currently pointing to. cursor_entry: Option<(Nibbles, BranchNodeCompact)>, /// Forward-only in-memory cursor over storage trie nodes. @@ -77,21 +66,60 @@ pub struct InMemoryTrieCursor<'a, C> { last_key: Option, /// Whether an initial seek was called. seeked: bool, + /// Reference to the full trie updates. + trie_updates: &'a TrieUpdatesSorted, } impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> { - /// Create new trie cursor which combines a DB cursor (None to assume empty DB) and a set of - /// in-memory trie nodes. - pub fn new( - cursor: Option, - trie_updates: &'a [(Nibbles, Option)], + /// Create new account trie cursor which combines a DB cursor and the trie updates. + pub fn new_account(cursor: C, trie_updates: &'a TrieUpdatesSorted) -> Self { + let in_memory_cursor = ForwardInMemoryCursor::new(trie_updates.account_nodes_ref()); + Self { + cursor, + cursor_wiped: false, + cursor_entry: None, + in_memory_cursor, + last_key: None, + seeked: false, + trie_updates, + } + } + + /// Create new storage trie cursor with full trie updates reference. + /// This allows the cursor to switch between storage tries when `set_hashed_address` is called. + pub fn new_storage( + cursor: C, + trie_updates: &'a TrieUpdatesSorted, + hashed_address: B256, ) -> Self { - debug_assert!( - trie_updates.is_sorted_by_key(|(k, _)| k), - "Overlay values must be sorted by path" - ); - let in_memory_cursor = ForwardInMemoryCursor::new(trie_updates); - Self { cursor, cursor_entry: None, in_memory_cursor, last_key: None, seeked: false } + let (in_memory_cursor, cursor_wiped) = + Self::get_storage_overlay(trie_updates, hashed_address); + Self { + cursor, + cursor_wiped, + cursor_entry: None, + in_memory_cursor, + last_key: None, + seeked: false, + trie_updates, + } + } + + /// Returns the storage overlay for `hashed_address` and whether it was deleted. + fn get_storage_overlay( + trie_updates: &'a TrieUpdatesSorted, + hashed_address: B256, + ) -> (ForwardInMemoryCursor<'a, Nibbles, Option>, bool) { + let storage_trie_updates = trie_updates.storage_tries_ref().get(&hashed_address); + let cursor_wiped = storage_trie_updates.is_some_and(|u| u.is_deleted()); + let storage_nodes = storage_trie_updates.map(|u| u.storage_nodes_ref()).unwrap_or(&[]); + + (ForwardInMemoryCursor::new(storage_nodes), cursor_wiped) + } + + /// Returns a mutable reference to the underlying cursor if it's not wiped, None otherwise. + fn get_cursor_mut(&mut self) -> Option<&mut C> { + (!self.cursor_wiped).then_some(&mut self.cursor) } /// Asserts that the next entry to be returned from the cursor is not previous to the last entry @@ -118,7 +146,7 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> { }; if should_seek { - self.cursor_entry = self.cursor.as_mut().map(|c| c.seek(key)).transpose()?.flatten(); + self.cursor_entry = self.get_cursor_mut().map(|c| c.seek(key)).transpose()?.flatten(); } Ok(()) @@ -131,7 +159,7 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> { // If the previous entry is `None`, and we've done a seek previously, then the cursor is // exhausted and we shouldn't call `next` again. if self.cursor_entry.is_some() { - self.cursor_entry = self.cursor.as_mut().map(|c| c.next()).transpose()?.flatten(); + self.cursor_entry = self.get_cursor_mut().map(|c| c.next()).transpose()?.flatten(); } Ok(()) @@ -242,9 +270,38 @@ impl TrieCursor for InMemoryTrieCursor<'_, C> { fn current(&mut self) -> Result, DatabaseError> { match &self.last_key { Some(key) => Ok(Some(*key)), - None => Ok(self.cursor.as_mut().map(|c| c.current()).transpose()?.flatten()), + None => Ok(self.get_cursor_mut().map(|c| c.current()).transpose()?.flatten()), } } + + fn reset(&mut self) { + let Self { + cursor, + cursor_wiped, + cursor_entry, + in_memory_cursor, + last_key, + seeked, + trie_updates: _, + } = self; + + cursor.reset(); + in_memory_cursor.reset(); + + *cursor_wiped = false; + *cursor_entry = None; + *last_key = None; + *seeked = false; + } +} + +impl TrieStorageCursor for InMemoryTrieCursor<'_, C> { + fn set_hashed_address(&mut self, hashed_address: B256) { + self.reset(); + self.cursor.set_hashed_address(hashed_address); + (self.in_memory_cursor, self.cursor_wiped) = + Self::get_storage_overlay(self.trie_updates, hashed_address); + } } #[cfg(test)] @@ -268,7 +325,8 @@ mod tests { let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys); - let mut cursor = InMemoryTrieCursor::new(Some(mock_cursor), &test_case.in_memory_nodes); + let trie_updates = TrieUpdatesSorted::new(test_case.in_memory_nodes, Default::default()); + let mut cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates); let mut results = Vec::new(); @@ -451,7 +509,8 @@ mod tests { let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys); - let mut cursor = InMemoryTrieCursor::new(Some(mock_cursor), &in_memory_nodes); + let trie_updates = TrieUpdatesSorted::new(in_memory_nodes, Default::default()); + let mut cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates); let result = cursor.seek_exact(Nibbles::from_nibbles([0x2])).unwrap(); assert_eq!( @@ -550,7 +609,8 @@ mod tests { let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys); - let mut cursor = InMemoryTrieCursor::new(Some(mock_cursor), &in_memory_nodes); + let trie_updates = TrieUpdatesSorted::new(in_memory_nodes, Default::default()); + let mut cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates); assert_eq!(cursor.current().unwrap(), None); @@ -600,7 +660,8 @@ mod tests { let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys); - let mut cursor = InMemoryTrieCursor::new(Some(mock_cursor), &in_memory_nodes); + let trie_updates = TrieUpdatesSorted::new(in_memory_nodes, Default::default()); + let mut cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates); // Seek to beginning should return None (all nodes are deleted) tracing::debug!("seeking to 0x"); @@ -764,7 +825,8 @@ mod tests { let db_nodes_arc = Arc::new(db_nodes_map); let visited_keys = Arc::new(Mutex::new(Vec::new())); let mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys); - let mut test_cursor = InMemoryTrieCursor::new(Some(mock_cursor), &in_memory_nodes); + let trie_updates = TrieUpdatesSorted::new(in_memory_nodes, Default::default()); + let mut test_cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates); // Test: seek to the beginning first let control_first = control_cursor.seek(Nibbles::default()).unwrap(); diff --git a/crates/trie/trie/src/trie_cursor/metrics.rs b/crates/trie/trie/src/trie_cursor/metrics.rs new file mode 100644 index 0000000000..ebbe75002c --- /dev/null +++ b/crates/trie/trie/src/trie_cursor/metrics.rs @@ -0,0 +1,189 @@ +use super::{TrieCursor, TrieStorageCursor}; +use crate::{BranchNodeCompact, Nibbles}; +use alloy_primitives::B256; +use reth_storage_errors::db::DatabaseError; +use std::time::{Duration, Instant}; +use tracing::debug_span; + +#[cfg(feature = "metrics")] +use crate::TrieType; +#[cfg(feature = "metrics")] +use reth_metrics::metrics::{self, Histogram}; + +/// Prometheus metrics for trie cursor operations. +/// +/// Tracks the number of cursor operations for monitoring and performance analysis. +#[cfg(feature = "metrics")] +#[derive(Clone, Debug)] +pub struct TrieCursorMetrics { + /// Histogram tracking overall time spent in database operations + overall_duration: Histogram, + /// Histogram for `next()` operations + next_histogram: Histogram, + /// Histogram for `seek()` operations + seek_histogram: Histogram, + /// Histogram for `seek_exact()` operations + seek_exact_histogram: Histogram, +} + +#[cfg(feature = "metrics")] +impl TrieCursorMetrics { + /// Create a new metrics instance with the specified trie type label. + pub fn new(trie_type: TrieType) -> Self { + let trie_type_str = trie_type.as_str(); + + Self { + overall_duration: metrics::histogram!( + "trie.cursor.overall_duration", + "type" => trie_type_str + ), + next_histogram: metrics::histogram!( + "trie.cursor.operations", + "type" => trie_type_str, + "operation" => "next" + ), + seek_histogram: metrics::histogram!( + "trie.cursor.operations", + "type" => trie_type_str, + "operation" => "seek" + ), + seek_exact_histogram: metrics::histogram!( + "trie.cursor.operations", + "type" => trie_type_str, + "operation" => "seek_exact" + ), + } + } + + /// Record the cached metrics from the provided cache and reset the cache counters. + /// + /// This method adds the current counter values from the cache to the Prometheus metrics + /// and then resets all cache counters to zero. + pub fn record(&mut self, cache: &mut TrieCursorMetricsCache) { + self.next_histogram.record(cache.next_count as f64); + self.seek_histogram.record(cache.seek_count as f64); + self.seek_exact_histogram.record(cache.seek_exact_count as f64); + self.overall_duration.record(cache.total_duration.as_secs_f64()); + cache.reset(); + } +} + +/// Cached metrics counters for trie cursor operations. +#[derive(Debug, Copy, Clone)] +pub struct TrieCursorMetricsCache { + /// Counter for `next()` calls + pub next_count: usize, + /// Counter for `seek()` calls + pub seek_count: usize, + /// Counter for `seek_exact()` calls + pub seek_exact_count: usize, + /// Total duration spent in database operations + pub total_duration: Duration, +} + +impl Default for TrieCursorMetricsCache { + fn default() -> Self { + Self { next_count: 0, seek_count: 0, seek_exact_count: 0, total_duration: Duration::ZERO } + } +} + +impl TrieCursorMetricsCache { + /// Reset all counters to zero. + pub const fn reset(&mut self) { + self.next_count = 0; + self.seek_count = 0; + self.seek_exact_count = 0; + self.total_duration = Duration::ZERO; + } + + /// Extend this cache by adding the counts from another cache. + /// + /// This accumulates the counter values from `other` into this cache. + pub fn extend(&mut self, other: &Self) { + self.next_count += other.next_count; + self.seek_count += other.seek_count; + self.seek_exact_count += other.seek_exact_count; + self.total_duration += other.total_duration; + } + + /// Record the span for metrics. + pub fn record_span(&self, name: &'static str) { + let _span = debug_span!( + target: "trie::trie_cursor", + "Trie cursor metrics", + name, + next_count = self.next_count, + seek_count = self.seek_count, + seek_exact_count = self.seek_exact_count, + total_duration = self.total_duration.as_secs_f64(), + ) + .entered(); + } +} + +/// A wrapper around a [`TrieCursor`] that tracks metrics for cursor operations. +/// +/// This implementation counts the number of times each cursor operation is called: +/// - `next()` - Move to the next entry +/// - `seek()` - Seek to a key or the next greater key +/// - `seek_exact()` - Seek to an exact key match +#[derive(Debug)] +pub struct InstrumentedTrieCursor<'metrics, C> { + /// The underlying cursor being wrapped + cursor: C, + /// Cached metrics counters + metrics: &'metrics mut TrieCursorMetricsCache, +} + +impl<'metrics, C> InstrumentedTrieCursor<'metrics, C> { + /// Create a new metrics cursor wrapping the given cursor. + pub const fn new(cursor: C, metrics: &'metrics mut TrieCursorMetricsCache) -> Self { + Self { cursor, metrics } + } +} + +impl<'metrics, C: TrieCursor> TrieCursor for InstrumentedTrieCursor<'metrics, C> { + fn seek_exact( + &mut self, + key: Nibbles, + ) -> Result, DatabaseError> { + let start = Instant::now(); + self.metrics.seek_exact_count += 1; + let result = self.cursor.seek_exact(key); + self.metrics.total_duration += start.elapsed(); + result + } + + fn seek( + &mut self, + key: Nibbles, + ) -> Result, DatabaseError> { + let start = Instant::now(); + self.metrics.seek_count += 1; + let result = self.cursor.seek(key); + self.metrics.total_duration += start.elapsed(); + result + } + + fn next(&mut self) -> Result, DatabaseError> { + let start = Instant::now(); + self.metrics.next_count += 1; + let result = self.cursor.next(); + self.metrics.total_duration += start.elapsed(); + result + } + + fn current(&mut self) -> Result, DatabaseError> { + self.cursor.current() + } + + fn reset(&mut self) { + self.cursor.reset() + } +} + +impl<'metrics, C: TrieStorageCursor> TrieStorageCursor for InstrumentedTrieCursor<'metrics, C> { + fn set_hashed_address(&mut self, hashed_address: B256) { + self.cursor.set_hashed_address(hashed_address) + } +} diff --git a/crates/trie/trie/src/trie_cursor/mock.rs b/crates/trie/trie/src/trie_cursor/mock.rs index 313df0443e..cbb2b0ffb1 100644 --- a/crates/trie/trie/src/trie_cursor/mock.rs +++ b/crates/trie/trie/src/trie_cursor/mock.rs @@ -2,7 +2,7 @@ use parking_lot::{Mutex, MutexGuard}; use std::{collections::BTreeMap, sync::Arc}; use tracing::instrument; -use super::{TrieCursor, TrieCursorFactory}; +use super::{TrieCursor, TrieCursorFactory, TrieStorageCursor}; use crate::{ mock::{KeyVisit, KeyVisitType}, BranchNodeCompact, Nibbles, @@ -14,12 +14,12 @@ use reth_storage_errors::db::DatabaseError; #[derive(Clone, Default, Debug)] pub struct MockTrieCursorFactory { account_trie_nodes: Arc>, - storage_tries: B256Map>>, + storage_tries: Arc>>, /// List of keys that the account trie cursor has visited. visited_account_keys: Arc>>>, /// List of keys that the storage trie cursor has visited, per storage trie. - visited_storage_keys: B256Map>>>>, + visited_storage_keys: Arc>>>>, } impl MockTrieCursorFactory { @@ -31,9 +31,9 @@ impl MockTrieCursorFactory { let visited_storage_keys = storage_tries.keys().map(|k| (*k, Default::default())).collect(); Self { account_trie_nodes: Arc::new(account_trie_nodes), - storage_tries: storage_tries.into_iter().map(|(k, v)| (k, Arc::new(v))).collect(), + storage_tries: Arc::new(storage_tries), visited_account_keys: Default::default(), - visited_storage_keys, + visited_storage_keys: Arc::new(visited_storage_keys), } } @@ -71,40 +71,94 @@ impl TrieCursorFactory for MockTrieCursorFactory { &self, hashed_address: B256, ) -> Result, DatabaseError> { - Ok(MockTrieCursor::new( - self.storage_tries - .get(&hashed_address) - .ok_or_else(|| { - DatabaseError::Other(format!("storage trie for {hashed_address:?} not found")) - })? - .clone(), - self.visited_storage_keys - .get(&hashed_address) - .ok_or_else(|| { - DatabaseError::Other(format!("storage trie for {hashed_address:?} not found")) - })? - .clone(), - )) + MockTrieCursor::new_storage( + self.storage_tries.clone(), + self.visited_storage_keys.clone(), + hashed_address, + ) } } +/// Mock trie cursor type - determines whether this is an account or storage cursor. +#[derive(Debug)] +enum MockTrieCursorType { + Account { + trie_nodes: Arc>, + visited_keys: Arc>>>, + }, + Storage { + all_storage_tries: Arc>>, + all_visited_storage_keys: Arc>>>>, + current_hashed_address: B256, + }, +} + /// Mock trie cursor. -#[derive(Default, Debug)] +#[derive(Debug)] #[non_exhaustive] pub struct MockTrieCursor { /// The current key. If set, it is guaranteed to exist in `trie_nodes`. current_key: Option, - trie_nodes: Arc>, - visited_keys: Arc>>>, + cursor_type: MockTrieCursorType, } impl MockTrieCursor { - /// Creates a new mock trie cursor with the given trie nodes and key tracking. + /// Creates a new mock trie cursor for accounts with the given trie nodes and key tracking. pub fn new( trie_nodes: Arc>, visited_keys: Arc>>>, ) -> Self { - Self { current_key: None, trie_nodes, visited_keys } + Self { + current_key: None, + cursor_type: MockTrieCursorType::Account { trie_nodes, visited_keys }, + } + } + + /// Creates a new mock trie cursor for storage with access to all storage tries. + pub fn new_storage( + all_storage_tries: Arc>>, + all_visited_storage_keys: Arc>>>>, + hashed_address: B256, + ) -> Result { + if !all_storage_tries.contains_key(&hashed_address) { + return Err(DatabaseError::Other(format!( + "storage trie for {hashed_address:?} not found" + ))); + } + Ok(Self { + current_key: None, + cursor_type: MockTrieCursorType::Storage { + all_storage_tries, + all_visited_storage_keys, + current_hashed_address: hashed_address, + }, + }) + } + + /// Returns the trie nodes map for the current cursor type. + fn trie_nodes(&self) -> &BTreeMap { + match &self.cursor_type { + MockTrieCursorType::Account { trie_nodes, .. } => trie_nodes.as_ref(), + MockTrieCursorType::Storage { all_storage_tries, current_hashed_address, .. } => { + all_storage_tries + .get(current_hashed_address) + .expect("current_hashed_address should exist in all_storage_tries") + } + } + } + + /// Returns the visited keys mutex for the current cursor type. + fn visited_keys(&self) -> &Mutex>> { + match &self.cursor_type { + MockTrieCursorType::Account { visited_keys, .. } => visited_keys.as_ref(), + MockTrieCursorType::Storage { + all_visited_storage_keys, + current_hashed_address, + .. + } => all_visited_storage_keys + .get(current_hashed_address) + .expect("current_hashed_address should exist in all_visited_storage_keys"), + } } } @@ -114,11 +168,11 @@ impl TrieCursor for MockTrieCursor { &mut self, key: Nibbles, ) -> Result, DatabaseError> { - let entry = self.trie_nodes.get(&key).cloned().map(|value| (key, value)); + let entry = self.trie_nodes().get(&key).cloned().map(|value| (key, value)); if let Some((key, _)) = &entry { self.current_key = Some(*key); } - self.visited_keys.lock().push(KeyVisit { + self.visited_keys().lock().push(KeyVisit { visit_type: KeyVisitType::SeekExact(key), visited_key: entry.as_ref().map(|(k, _)| *k), }); @@ -131,11 +185,12 @@ impl TrieCursor for MockTrieCursor { key: Nibbles, ) -> Result, DatabaseError> { // Find the first key that is greater than or equal to the given key. - let entry = self.trie_nodes.iter().find_map(|(k, v)| (k >= &key).then(|| (*k, v.clone()))); + let entry = + self.trie_nodes().iter().find_map(|(k, v)| (k >= &key).then(|| (*k, v.clone()))); if let Some((key, _)) = &entry { self.current_key = Some(*key); } - self.visited_keys.lock().push(KeyVisit { + self.visited_keys().lock().push(KeyVisit { visit_type: KeyVisitType::SeekNonExact(key), visited_key: entry.as_ref().map(|(k, _)| *k), }); @@ -144,7 +199,7 @@ impl TrieCursor for MockTrieCursor { #[instrument(skip(self), ret(level = "trace"))] fn next(&mut self) -> Result, DatabaseError> { - let mut iter = self.trie_nodes.iter(); + let mut iter = self.trie_nodes().iter(); // Jump to the first key that has a prefix of the current key if it's set, or to the first // key otherwise. iter.find(|(k, _)| self.current_key.as_ref().is_none_or(|current| k.starts_with(current))) @@ -154,7 +209,7 @@ impl TrieCursor for MockTrieCursor { if let Some((key, _)) = &entry { self.current_key = Some(*key); } - self.visited_keys.lock().push(KeyVisit { + self.visited_keys().lock().push(KeyVisit { visit_type: KeyVisitType::Next, visited_key: entry.as_ref().map(|(k, _)| *k), }); @@ -165,4 +220,22 @@ impl TrieCursor for MockTrieCursor { fn current(&mut self) -> Result, DatabaseError> { Ok(self.current_key) } + + fn reset(&mut self) { + self.current_key = None; + } +} + +impl TrieStorageCursor for MockTrieCursor { + fn set_hashed_address(&mut self, hashed_address: B256) { + self.reset(); + match &mut self.cursor_type { + MockTrieCursorType::Storage { current_hashed_address, .. } => { + *current_hashed_address = hashed_address; + } + MockTrieCursorType::Account { .. } => { + panic!("set_hashed_address called on account cursor") + } + } + } } diff --git a/crates/trie/trie/src/trie_cursor/mod.rs b/crates/trie/trie/src/trie_cursor/mod.rs index 05a6c09e94..32fd17c996 100644 --- a/crates/trie/trie/src/trie_cursor/mod.rs +++ b/crates/trie/trie/src/trie_cursor/mod.rs @@ -18,6 +18,12 @@ pub mod depth_first; #[cfg(test)] pub mod mock; +/// Metrics tracking trie cursor implementations. +pub mod metrics; +#[cfg(feature = "metrics")] +pub use metrics::TrieCursorMetrics; +pub use metrics::{InstrumentedTrieCursor, TrieCursorMetricsCache}; + pub use self::{depth_first::DepthFirstTrieIterator, in_memory::*, subnode::CursorSubNode}; /// Factory for creating trie cursors. @@ -29,7 +35,7 @@ pub trait TrieCursorFactory { Self: 'a; /// The storage trie cursor type. - type StorageTrieCursor<'a>: TrieCursor + type StorageTrieCursor<'a>: TrieStorageCursor where Self: 'a; @@ -62,6 +68,26 @@ pub trait TrieCursor { /// Get the current entry. fn current(&mut self) -> Result, DatabaseError>; + + /// Reset the cursor to the beginning. + /// + /// # Important + /// + /// After calling this method, the subsequent operation MUST be a [`TrieCursor::seek`] or + /// [`TrieCursor::seek_exact`] call. + fn reset(&mut self); +} + +/// A cursor for traversing storage trie nodes. +#[auto_impl::auto_impl(&mut)] +pub trait TrieStorageCursor: TrieCursor { + /// Set the hashed address for the storage trie cursor. + /// + /// # Important + /// + /// After calling this method, the subsequent operation MUST be a [`TrieCursor::seek`] or + /// [`TrieCursor::seek_exact`] call. + fn set_hashed_address(&mut self, hashed_address: B256); } /// Iterator wrapper for `TrieCursor` types diff --git a/crates/trie/trie/src/trie_cursor/noop.rs b/crates/trie/trie/src/trie_cursor/noop.rs index a00a18e4f0..0a51fd806d 100644 --- a/crates/trie/trie/src/trie_cursor/noop.rs +++ b/crates/trie/trie/src/trie_cursor/noop.rs @@ -1,4 +1,4 @@ -use super::{TrieCursor, TrieCursorFactory}; +use super::{TrieCursor, TrieCursorFactory, TrieStorageCursor}; use crate::{BranchNodeCompact, Nibbles}; use alloy_primitives::B256; use reth_storage_errors::db::DatabaseError; @@ -60,6 +60,10 @@ impl TrieCursor for NoopAccountTrieCursor { fn current(&mut self) -> Result, DatabaseError> { Ok(None) } + + fn reset(&mut self) { + // Noop + } } /// Noop storage trie cursor. @@ -89,4 +93,14 @@ impl TrieCursor for NoopStorageTrieCursor { fn current(&mut self) -> Result, DatabaseError> { Ok(None) } + + fn reset(&mut self) { + // Noop + } +} + +impl TrieStorageCursor for NoopStorageTrieCursor { + fn set_hashed_address(&mut self, _hashed_address: B256) { + // Noop + } } diff --git a/docs/crates/db.md b/docs/crates/db.md index abaa1c83bb..4e368cad77 100644 --- a/docs/crates/db.md +++ b/docs/crates/db.md @@ -8,12 +8,14 @@ The database is a central component to Reth, enabling persistent storage for dat Within Reth, the database is organized via "tables". A table is any struct that implements the `Table` trait. -[File: crates/storage/db-api/src/table.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/table.rs#L64-L93) +[File: crates/storage/db-api/src/table.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/table.rs#L87-L101) ```rust ignore pub trait Table: Send + Sync + Debug + 'static { /// Return table name as it is present inside the MDBX. const NAME: &'static str; + /// Whether the table is also a `DUPSORT` table. + const DUPSORT: bool; /// Key element of `Table`. /// /// Sorting should be taken into account when encoding this. @@ -32,10 +34,10 @@ pub trait Value: Compress + Decompress + Serialize {} The `Table` trait has two generic values, `Key` and `Value`, which need to implement the `Key` and `Value` traits, respectively. The `Encode` trait is responsible for transforming data into bytes so it can be stored in the database, while the `Decode` trait transforms the bytes back into their original form. Similarly, the `Compress` and `Decompress` traits transform the data to and from a compressed format when storing or reading data from the database. -There are many tables within the node, all used to store different types of data from `Headers` to `Transactions` and more. Below is a list of all of the tables. You can follow [this link](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db/src/tables/mod.rs#L274-L414) if you would like to see the table definitions for any of the tables below. +There are many tables within the node, all used to store different types of data from `Headers` to `Transactions` and more. Below is a list of all of the tables. You can follow [this link](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/tables/mod.rs) if you would like to see the table definitions for any of the tables below. - CanonicalHeaders -- HeaderTerminalDifficulties +- HeaderTerminalDifficulties (deprecated) - HeaderNumbers - Headers - BlockBodyIndices @@ -56,26 +58,29 @@ There are many tables within the node, all used to store different types of data - HashedStorages - AccountsTrie - StoragesTrie +- AccountsTrieChangeSets +- StoragesTrieChangeSets - TransactionSenders - StageCheckpoints - StageCheckpointProgresses - PruneCheckpoints - VersionHistory - ChainState +- Metadata
## Database -Reth's database design revolves around its main [Database trait](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/database.rs#L8-L52), which implements the database's functionality across many types. Let's take a quick look at the `Database` trait and how it works. +Reth's database design revolves around its main [Database trait](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/database.rs#L8-L52), which implements the database's functionality across many types. Let's take a quick look at the `Database` trait and how it works. -[File: crates/storage/db-api/src/database.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/database.rs#L8-L52) +[File: crates/storage/db-api/src/database.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/database.rs#L8-L52) ```rust ignore /// Main Database trait that can open read-only and read-write transactions. /// /// Sealed trait which cannot be implemented by 3rd parties, exposed only for consumption. -pub trait Database: Send + Sync { +pub trait Database: Send + Sync + Debug { /// Read-Only database transaction type TX: DbTx + Send + Sync + Debug + 'static; /// Read-Write database transaction @@ -93,11 +98,11 @@ pub trait Database: Send + Sync { /// end of the execution. fn view(&self, f: F) -> Result where - F: FnOnce(&Self::TX) -> T, + F: FnOnce(&mut Self::TX) -> T, { - let tx = self.tx()?; + let mut tx = self.tx()?; - let res = f(&tx); + let res = f(&mut tx); tx.commit()?; Ok(res) @@ -119,50 +124,39 @@ pub trait Database: Send + Sync { } ``` -Any type that implements the `Database` trait can create a database transaction, as well as view or update existing transactions. As an example, let's revisit the `Transaction` struct from the `stages` crate. This struct contains a field named `db` which is a reference to a generic type `DB` that implements the `Database` trait. The `Transaction` struct can use the `db` field to store new headers, bodies and senders in the database. In the code snippet below, you can see the `Transaction::open()` method, which uses the `Database::tx_mut()` function to create a mutable transaction. - -[File: crates/stages/src/db.rs](https://github.com/paradigmxyz/reth/blob/00a49f5ee78b0a88fea409283e6bb9c96d4bb31e/crates/stages/src/db.rs#L28) +Any type that implements the `Database` trait can create a database transaction, as well as view or update existing transactions. For example, you can open a read-write transaction directly via `tx_mut()`, write to tables, and commit: ```rust ignore -pub struct Transaction<'this, DB: Database> { - /// A handle to the DB. - pub(crate) db: &'this DB, - tx: Option<::TXMut>, -} - -//--snip-- -impl<'this, DB> Transaction<'this, DB> -where - DB: Database, -{ - //--snip-- - - /// Open a new inner transaction. - pub fn open(&mut self) -> Result<(), Error> { - self.tx = Some(self.db.tx_mut()?); - Ok(()) - } -} +let tx = db.tx_mut()?; +tx.put::(block_number, block.hash())?; +tx.put::(block_number, header.clone())?; +tx.put::(block.hash(), block_number)?; +tx.commit()?; ``` The `Database` defines two associated types `TX` and `TXMut`. -[File: crates/storage/db-api/src/database.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/database.rs#L54-L78) +[File: crates/storage/db-api/src/database.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/database.rs) The `TX` type can be any type that implements the `DbTx` trait, which provides a set of functions to interact with read only transactions. -[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/transaction.rs#L7-L29) +[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/transaction.rs#L11-L40) ```rust ignore /// Read only transaction -pub trait DbTx: Send + Sync { +pub trait DbTx: Debug + Send + Sync { /// Cursor type for this read-only transaction type Cursor: DbCursorRO + Send + Sync; /// `DupCursor` type for this read-only transaction type DupCursor: DbDupCursorRO + DbCursorRO + Send + Sync; - /// Get value + /// Get value by an owned key fn get(&self, key: T::Key) -> Result, DatabaseError>; + /// Get value by a reference to the encoded key (avoids cloning for raw keys) + fn get_by_encoded_key( + &self, + key: &::Encoded, + ) -> Result, DatabaseError>; /// Commit for read only transaction will consume and free transaction and allows /// freeing of memory pages fn commit(self) -> Result; @@ -181,7 +175,7 @@ pub trait DbTx: Send + Sync { The `TXMut` type can be any type that implements the `DbTxMut` trait, which provides a set of functions to interact with read/write transactions and the associated cursor types. -[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/transaction.rs#L31-L54) +[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/transaction.rs) ```rust ignore /// Read write transaction that allows writing to database @@ -196,8 +190,12 @@ pub trait DbTxMut: Send + Sync { + Send + Sync; - /// Put value in database + /// Put value to database fn put(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError>; + /// Append value with the largest key to database (fast path) + fn append(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> { + self.put::(key, value) + } /// Delete value from database fn delete(&self, key: T::Key, value: Option) -> Result; @@ -212,21 +210,16 @@ pub trait DbTxMut: Send + Sync { Let's take a look at the `DbTx` and `DbTxMut` traits in action. -Revisiting the `DatabaseProvider` struct as an example, the `DatabaseProvider::header_by_number()` function uses the `DbTx::get()` function to get a header from the `Headers` table. +Revisiting the `DatabaseProvider` struct as an example, the `DatabaseProvider::header_by_number()` function currently delegates to the static-file provider: -[File: crates/storage/provider/src/providers/database/provider.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/provider/src/providers/database/provider.rs#L1319-L1336) +[File: crates/storage/provider/src/providers/database/mod.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/provider/src/providers/database/mod.rs#L280-L282) ```rust ignore impl HeaderProvider for DatabaseProvider { //--snip-- - fn header_by_number(&self, num: BlockNumber) -> ProviderResult> { - self.static_file_provider.get_with_static_file_or_database( - StaticFileSegment::Headers, - num, - |static_file| static_file.header_by_number(num), - || Ok(self.tx.get::(num)?), - ) + fn header_by_number(&self, num: BlockNumber) -> ProviderResult> { + self.static_file_provider.header_by_number(num) } //--snip-- @@ -235,7 +228,7 @@ impl HeaderProvider for DatabaseProvider { Notice that the function uses a [turbofish](https://techblog.tonsser.com/posts/what-is-rusts-turbofish) to define which table to use when passing in the `key` to the `DbTx::get()` function. Taking a quick look at the function definition, a generic `T` is defined that implements the `Table` trait mentioned at the beginning of this chapter. -[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/db-api/src/transaction.rs#L15) +[File: crates/storage/db-api/src/transaction.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/db-api/src/transaction.rs) ```rust ignore fn get(&self, key: T::Key) -> Result, DatabaseError>; @@ -245,31 +238,29 @@ This design pattern is very powerful and allows Reth to use the methods availabl Let's take a look at a couple of examples before moving on. In the snippet below, the `DbTxMut::put()` method is used to insert values into the `CanonicalHeaders`, `Headers` and `HeaderNumbers` tables. -[File: crates/storage/provider/src/providers/database/provider.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/storage/provider/src/providers/database/provider.rs#L2606-L2745) +[File: crates/storage/provider/src/providers/database/provider.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/provider/src/providers/database/provider.rs) ```rust ignore self.tx.put::(block_number, block.hash())?; -self.tx.put::(block_number, block.header.as_ref().clone())?; +self.tx.put::(block_number, block.header.clone())?; self.tx.put::(block.hash(), block_number)?; ``` Let's take a look at the `DatabaseProviderRW` struct, which is used to create a mutable transaction to interact with the database. The `DatabaseProviderRW` struct implements the `Deref` and `DerefMut` traits, which return a reference to its first field, which is a `TxMut`. Recall that `TxMut` is a generic type on the `Database` trait, which is defined as `type TXMut: DbTxMut + DbTx + Send + Sync;`, giving it access to all of the functions available to `DbTx`, including the `DbTx::get()` function. -This next example uses the `DbTx::cursor_read()` method to get a `Cursor`. The `Cursor` type provides a way to traverse through rows in a database table, one row at a time. A cursor enables the program to perform an operation (updating, deleting, etc) on each row in the table individually. The following code snippet gets a cursor for a few different tables in the database. +This next example shows reading headers from static files using the static-file provider. -[File: crates/static-file/static-file/src/segments/headers.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/static-file/static-file/src/segments/headers.rs#L22-L58) +[File: crates/storage/provider/src/providers/static_file/manager.rs](https://github.com/paradigmxyz/reth/blob/main/crates/storage/provider/src/providers/static_file/manager.rs#L1680-L1690) ```rust ignore -# Get a cursor for the Headers table -let mut headers_cursor = provider.tx_ref().cursor_read::()?; -# Then we can walk the cursor to get the headers for a specific block range -let headers_walker = headers_cursor.walk_range(block_range.clone())?; +// Read headers for a specific block range from static files +let headers = provider.static_file_provider().headers_range(block_range.clone())?; ``` Let's look at an example of how cursors are used. The code snippet below contains the `unwind` method from the `BodyStage` defined in the `stages` crate. This function is responsible for unwinding any changes to the database if there is an error when executing the body stage within the Reth pipeline. -[File: crates/stages/stages/src/stages/bodies.rs](https://github.com/paradigmxyz/reth/blob/bf9cac7571f018fec581fe3647862dab527aeafb/crates/stages/stages/src/stages/bodies.rs#L267-L345) +[File: crates/stages/stages/src/stages/bodies.rs](https://github.com/paradigmxyz/reth/blob/main/crates/stages/stages/src/stages/bodies.rs) ```rust ignore /// Unwind the stage. diff --git a/docs/crates/stages.md b/docs/crates/stages.md index a6f107c2c0..f6fdbf9603 100644 --- a/docs/crates/stages.md +++ b/docs/crates/stages.md @@ -1,9 +1,25 @@ # Stages -The `stages` lib plays a central role in syncing the node, maintaining state, updating the database and more. The stages involved in the Reth pipeline are the `HeaderStage`, `BodyStage`, `SenderRecoveryStage`, and `ExecutionStage` (note that this list is non-exhaustive, and more pipeline stages will be added in the near future). Each of these stages is queued up and stored within the Reth pipeline. +The `stages` lib plays a central role in syncing the node, maintaining state, updating the database and more. The stages involved in the Reth pipeline are queued up and stored within the Reth pipeline. In the default configuration, the pipeline runs the following stages in order: + +- HeaderStage +- BodyStage +- SenderRecoveryStage +- ExecutionStage +- PruneSenderRecoveryStage (if pruning for sender recovery is enabled) +- MerkleUnwindStage +- AccountHashingStage +- StorageHashingStage +- MerkleExecuteStage +- MerkleChangeSets +- TransactionLookupStage +- IndexStorageHistoryStage +- IndexAccountHistoryStage +- PruneStage (execute) +- FinishStage -When the node is first started, a new `Pipeline` is initialized and all of the stages are added into `Pipeline.stages`. Then, the `Pipeline::run` function is called, which starts the pipeline, executing all of the stages continuously in an infinite loop. This process syncs the chain, keeping everything up to date with the chain tip. +When the node is first started, a new `Pipeline` is initialized and all of the stages are added into `Pipeline.stages`. Then, the `Pipeline::run` function is called, which starts the pipeline, executing all of the stages continuously in an infinite loop. This process syncs the chain, keeping everything up to date with the chain tip. Each stage within the pipeline implements the `Stage` trait which provides function interfaces to get the stage id, execute the stage and unwind the changes to the database if there was an issue during the stage execution. @@ -14,15 +30,13 @@ To get a better idea of what is happening at each part of the pipeline, let's wa ## HeaderStage - -The `HeaderStage` is responsible for syncing the block headers, validating the header integrity and writing the headers to the database. When the `execute()` function is called, the local head of the chain is updated to the most recent block height previously executed by the stage. At this point, the node status is also updated with that block's height, hash and total difficulty. These values are used during any new eth/65 handshakes. After updating the head, a stream is established with other peers in the network to sync the missing chain headers between the most recent state stored in the database and the chain tip. The `HeaderStage` contains a `downloader` attribute, which is a type that implements the `HeaderDownloader` trait. A `HeaderDownloader` is a `Stream` that returns batches of headers. +The `HeaderStage` is responsible for syncing the block headers, validating the header integrity and writing the headers to storage. When the stage runs, it determines the sync gap between the local head and the tip, then downloads headers in reverse (from tip down to the local head) using a `HeaderDownloader` stream. Headers are buffered in ETL collectors and then written to static files and to `HeaderNumbers` in the database in a single step. -The `HeaderStage` relies on the downloader stream to return the headers in descending order starting from the chain tip down to the latest block in the database. While other stages in the `Pipeline` start from the most recent block in the database up to the chain tip, the `HeaderStage` works in reverse to avoid [long-range attacks](https://messari.io/report/long-range-attack). When a node downloads headers in ascending order, it will not know if it is being subjected to a long-range attack until it reaches the most recent blocks. To combat this, the `HeaderStage` starts by getting the chain tip from the Consensus Layer, verifies the tip, and then walks backwards by the parent hash. +The `HeaderStage` relies on the downloader stream to return the headers in descending order starting from the chain tip down to the latest block in the database. While other stages in the `Pipeline` start from the most recent block in the database up to the chain tip, the `HeaderStage` works in reverse to avoid [long-range attacks](https://messari.io/report/long-range-attack). When a node downloads headers in ascending order, it will not know if it is being subjected to a long-range attack until it reaches the most recent blocks. To combat this, the `HeaderStage` starts by getting the chain tip, verifies the tip, and then walks backwards by the parent hash. - -Each header is then validated to ensure that it has the proper parent. Note that this is only a basic response validation, and the `HeaderDownloader` uses the `validate` method during the `stream`, so that each header is validated according to the consensus specification before the header is yielded from the stream. After this, each header is then written to the database. If a header is not valid or the stream encounters any other error, the error is propagated up through the stage execution, the changes to the database are unwound and the stage is resumed from the most recent valid state. +Each header is validated to ensure it correctly attaches to its parent and conforms to consensus expectations by the downloader before it is yielded. After download, headers are written to storage. If a header is not valid or the stream encounters any other error, the error is propagated up through the stage execution, the changes to the database are unwound and the stage is resumed from the most recent valid state. -This process continues until all of the headers have been downloaded and written to the database. Finally, the total difficulty of the chain's head is updated and the function returns `Ok(ExecOutput { stage_progress, done: true })`, signaling that the header sync has been completed successfully. +This process continues until all of the headers have been downloaded and written to storage. Finally, the function returns, for example: `Ok(ExecOutput { checkpoint: StageCheckpoint::new(last_header_number).with_headers_stage_checkpoint(...), done: true })`, signaling that the header sync has been completed successfully.
@@ -36,9 +50,9 @@ The transactions root is a value that is calculated based on the transactions in When the `BodyStage` is looking at the headers to determine which block to download, it will skip the blocks where the `header.ommers_hash` and the `header.transaction_root` are empty, denoting that the block is empty as well. -Once the `BodyStage` determines which block bodies to fetch, a new `bodies_stream` is created which downloads all of the bodies from the `starting_block`, up until the `target_block` is specified. Each time the `bodies_stream` yields a value, a `SealedBlock` is created using the block header, the ommers hash and the newly downloaded block body. +Once the `BodyStage` determines which block bodies to fetch, a new `bodies_stream` is created which downloads all of the bodies from the `starting_block`, up until the `target_block` is specified. Each time the `bodies_stream` yields a value, a response is received indicating either an empty block or a full block body to be written. -The new block is then pre-validated, checking that the ommers hash and transactions root in the block header are the same in the block body. Following a successful pre-validation, the `BodyStage` loops through each transaction in the `block.body`, adding the transaction to the database. This process is repeated for every downloaded block body, with the `BodyStage` returning `Ok(ExecOutput { stage_progress, done: true })` signaling it successfully completed. +The `BodyStage` writes the received block bodies to storage. Validation of block body correctness relative to headers is enforced by the downloader and later by execution/consensus. This process is repeated for every downloaded block body, with the `BodyStage` returning `Ok(ExecOutput { checkpoint: StageCheckpoint::new(highest_block).with_entities_stage_checkpoint(...), done: ... })` signaling progress/completion.
@@ -50,7 +64,7 @@ In an [ECDSA (Elliptic Curve Digital Signature Algorithm) signature](https://wik The "r" is the x-coordinate of a point on the elliptic curve that is calculated as part of the signature process. The "s" is the s-value that is calculated during the signature process. It is derived from the private key and the message being signed. Lastly, the "v" is the "recovery value" that is used to recover the public key from the signature, which is derived from the signature and the message that was signed. Together, the "r", "s", and "v" values make up an ECDSA signature, and they are used to verify the authenticity of the signed transaction. -Once the transaction signer has been recovered, the signer is then added to the database. This process is repeated for every transaction that was retrieved, and similarly to previous stages, `Ok(ExecOutput { stage_progress, done: true })` is returned to signal a successful completion of the stage. +Once the transaction signer has been recovered, the signer is then added to the database. This process is repeated for every transaction that was retrieved, and similarly to previous stages, `Ok(ExecOutput { checkpoint: StageCheckpoint::new(end_block).with_entities_stage_checkpoint(...), done: ... })` is returned to signal a successful completion of the stage.
@@ -58,15 +72,19 @@ Once the transaction signer has been recovered, the signer is then added to the Finally, after all headers, bodies and senders are added to the database, the `ExecutionStage` starts to execute. This stage is responsible for executing all of the transactions and updating the state stored in the database. -After all headers and their corresponding transactions have been executed, all of the resulting state changes are applied to the database, updating account balances, account bytecode and other state changes. After applying all of the execution state changes, if there was a block reward, it is applied to the validator's account. +After all headers and their corresponding transactions have been executed, all of the resulting state changes are applied to the database, updating account balances, account bytecode and other state changes. In post-Merge Ethereum, there is no inflationary block reward on the Execution Layer; fees/priority tips are handled within transaction execution. -At the end of the `execute()` function, a familiar value is returned, `Ok(ExecOutput { stage_progress, done: true })` signaling a successful completion of the `ExecutionStage`. +At the end of the `execute()` function, a familiar value is returned, `Ok(ExecOutput { checkpoint: StageCheckpoint::new(stage_progress).with_execution_stage_checkpoint(...), done: ... })` signaling a successful completion of the `ExecutionStage`.
## MerkleUnwindStage -The `MerkleUnwindStage` is responsible for unwinding the Merkle Patricia trie state when a reorg occurs or when there's a need to rollback state changes. This stage ensures that the state trie remains consistent with the chain's canonical history by reverting any state changes that need to be undone. It works closely with the `MerkleExecuteStage` to maintain state integrity. +The `MerkleUnwindStage` is responsible for unwinding the Merkle Patricia trie when reorgs occur or when there's a need to roll back state changes. This ensures the trie remains consistent with the chain's canonical history by reverting changes beyond the unwind point. It typically runs before the hashing stages to unwind trie state during reorgs or rollbacks. + +## MerkleExecuteStage + +The `MerkleExecuteStage` runs after `AccountHashingStage` and `StorageHashingStage` and is responsible for constructing or updating the state root based on the latest hashed account and storage data. It processes state changes from executed transactions and maintains the state root included in block headers.
@@ -82,9 +100,9 @@ The `StorageHashingStage` is responsible for computing hashes of contract storag
-## MerkleExecuteStage +## MerkleChangeSets -The `MerkleExecuteStage` handles the construction and updates of the Merkle Patricia trie, which is Ethereum's core data structure for storing state. This stage processes state changes from executed transactions and builds the corresponding branches in the state trie. It's responsible for maintaining the state root that's included in block headers. +The `MerkleChangeSets` stage consolidates and finalizes Merkle-related change sets after the `MerkleStage` execution mode has run, ensuring consistent trie updates and checkpoints.
@@ -106,6 +124,18 @@ The `IndexAccountHistoryStage` builds indices for account history, tracking how
+## PruneSenderRecoveryStage + +The `PruneSenderRecoveryStage` removes entries from `TransactionSenders` according to configured prune modes. It typically runs after `ExecutionStage` when pruning for sender recovery is enabled. + +
+ +## PruneStage + +The `PruneStage` performs pruning for the configured segments (such as history tables) based on `PruneModes`. It runs after hashing/merkle and history indexing stages. + +
+ ## FinishStage The `FinishStage` is the final stage in the pipeline that performs cleanup and verification tasks. It ensures that all previous stages have been completed successfully and that the node's state is consistent. This stage may also update various metrics and status indicators to reflect the completion of a sync cycle. @@ -116,4 +146,4 @@ The `FinishStage` is the final stage in the pipeline that performs cleanup and v Now that we have covered all of the stages that are currently included in the `Pipeline`, you know how the Reth client stays synced with the chain tip and updates the database with all of the new headers, bodies, senders and state changes. While this chapter provides an overview on how the pipeline stages work, the following chapters will dive deeper into the database, the networking stack and other exciting corners of the Reth codebase. Feel free to check out any parts of the codebase mentioned in this chapter, and when you are ready, the next chapter will dive into the `database`. -[Next Chapter]() +[Next Chapter](db.md) diff --git a/docs/design/review.md b/docs/design/review.md index 304d3582f5..1e639ad98c 100644 --- a/docs/design/review.md +++ b/docs/design/review.md @@ -4,7 +4,7 @@ This document contains some of our research on how other codebases designed vari ## P2P -* [`Sentry`](https://erigon.gitbook.io/docs/summary/fundamentals/modules/sentry), a pluggable p2p node following the [Erigon gRPC architecture](https://erigon.substack.com/p/current-status-of-silkworm-and-silkrpc): +* [`Sentry`](https://docs.erigon.tech/fundamentals/modules/sentry), a pluggable p2p node following the [Erigon gRPC architecture](https://erigon.substack.com/p/current-status-of-silkworm-and-silkrpc): * [`vorot93`](https://github.com/vorot93/) first started by implementing a rust devp2p stack in [`devp2p`](https://github.com/vorot93/devp2p) * vorot93 then started work on sentry, using devp2p, to satisfy the erigon architecture of modular components connected with gRPC. * The code from rust-ethereum/devp2p was merged into sentry, and rust-ethereum/devp2p was archived diff --git a/docs/vocs/docs/pages/cli/SUMMARY.mdx b/docs/vocs/docs/pages/cli/SUMMARY.mdx index 7f7012f4c1..6a7eb22884 100644 --- a/docs/vocs/docs/pages/cli/SUMMARY.mdx +++ b/docs/vocs/docs/pages/cli/SUMMARY.mdx @@ -19,8 +19,15 @@ - [`reth db clear mdbx`](/cli/reth/db/clear/mdbx) - [`reth db clear static-file`](/cli/reth/db/clear/static-file) - [`reth db repair-trie`](/cli/reth/db/repair-trie) + - [`reth db static-file-header`](/cli/reth/db/static-file-header) + - [`reth db static-file-header block`](/cli/reth/db/static-file-header/block) + - [`reth db static-file-header path`](/cli/reth/db/static-file-header/path) - [`reth db version`](/cli/reth/db/version) - [`reth db path`](/cli/reth/db/path) + - [`reth db settings`](/cli/reth/db/settings) + - [`reth db settings get`](/cli/reth/db/settings/get) + - [`reth db settings set`](/cli/reth/db/settings/set) + - [`reth db settings set receipts_in_static_files`](/cli/reth/db/settings/set/receipts_in_static_files) - [`reth download`](/cli/reth/download) - [`reth stage`](/cli/reth/stage) - [`reth stage run`](/cli/reth/stage/run) diff --git a/docs/vocs/docs/pages/cli/reth/db.mdx b/docs/vocs/docs/pages/cli/reth/db.mdx index 6b98c08112..fc4b44a66b 100644 --- a/docs/vocs/docs/pages/cli/reth/db.mdx +++ b/docs/vocs/docs/pages/cli/reth/db.mdx @@ -9,17 +9,19 @@ $ reth db --help Usage: reth db [OPTIONS] Commands: - stats Lists all the tables, their entry count and their size - list Lists the contents of a table - checksum Calculates the content checksum of a table - diff Create a diff between two database tables or two entire databases - get Gets the content of a table for the given key - drop Deletes all database entries - clear Deletes all table entries - repair-trie Verifies trie consistency and outputs any inconsistencies - version Lists current and local database versions - path Returns the full database path - help Print this message or the help of the given subcommand(s) + stats Lists all the tables, their entry count and their size + list Lists the contents of a table + checksum Calculates the content checksum of a table + diff Create a diff between two database tables or two entire databases + get Gets the content of a table for the given key + drop Deletes all database entries + clear Deletes all table entries + repair-trie Verifies trie consistency and outputs any inconsistencies + static-file-header Reads and displays the static file segment header + version Lists current and local database versions + path Returns the full database path + settings Manage storage settings + help Print this message or the help of the given subcommand(s) Options: -h, --help @@ -78,6 +80,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -90,6 +101,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/db/diff.mdx b/docs/vocs/docs/pages/cli/reth/db/diff.mdx index 5625853118..6032a93b20 100644 --- a/docs/vocs/docs/pages/cli/reth/db/diff.mdx +++ b/docs/vocs/docs/pages/cli/reth/db/diff.mdx @@ -41,6 +41,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) diff --git a/docs/vocs/docs/pages/cli/reth/db/settings.mdx b/docs/vocs/docs/pages/cli/reth/db/settings.mdx new file mode 100644 index 0000000000..90ab3a3c8b --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/settings.mdx @@ -0,0 +1,144 @@ +# reth db settings + +Manage storage settings + +```bash +$ reth db settings --help +``` +```txt +Usage: reth db settings [OPTIONS] + +Commands: + get Get current storage settings from database + set Set storage settings in database + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/settings/get.mdx b/docs/vocs/docs/pages/cli/reth/db/settings/get.mdx new file mode 100644 index 0000000000..b96290cf0a --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/settings/get.mdx @@ -0,0 +1,139 @@ +# reth db settings get + +Get current storage settings from database + +```bash +$ reth db settings get --help +``` +```txt +Usage: reth db settings get [OPTIONS] + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/settings/set.mdx b/docs/vocs/docs/pages/cli/reth/db/settings/set.mdx new file mode 100644 index 0000000000..2f4b8df7a9 --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/settings/set.mdx @@ -0,0 +1,143 @@ +# reth db settings set + +Set storage settings in database + +```bash +$ reth db settings set --help +``` +```txt +Usage: reth db settings set [OPTIONS] + +Commands: + receipts_in_static_files Store receipts in static files instead of the database + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/settings/set/receipts_in_static_files.mdx b/docs/vocs/docs/pages/cli/reth/db/settings/set/receipts_in_static_files.mdx new file mode 100644 index 0000000000..22cc15b1b2 --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/settings/set/receipts_in_static_files.mdx @@ -0,0 +1,143 @@ +# reth db settings set receipts_in_static_files + +Store receipts in static files instead of the database + +```bash +$ reth db settings set receipts_in_static_files --help +``` +```txt +Usage: reth db settings set receipts_in_static_files [OPTIONS] + +Arguments: + + [possible values: true, false] + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/static-file-header.mdx b/docs/vocs/docs/pages/cli/reth/db/static-file-header.mdx new file mode 100644 index 0000000000..5d4764a2e6 --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/static-file-header.mdx @@ -0,0 +1,144 @@ +# reth db static-file-header + +Reads and displays the static file segment header + +```bash +$ reth db static-file-header --help +``` +```txt +Usage: reth db static-file-header [OPTIONS] + +Commands: + block Query by segment and block number + path Query by path to static file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/static-file-header/block.mdx b/docs/vocs/docs/pages/cli/reth/db/static-file-header/block.mdx new file mode 100644 index 0000000000..4d7fb55544 --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/static-file-header/block.mdx @@ -0,0 +1,151 @@ +# reth db static-file-header block + +Query by segment and block number + +```bash +$ reth db static-file-header block --help +``` +```txt +Usage: reth db static-file-header block [OPTIONS] + +Arguments: + + Static file segment + + Possible values: + - headers: Static File segment responsible for the `CanonicalHeaders`, `Headers`, `HeaderTerminalDifficulties` tables + - transactions: Static File segment responsible for the `Transactions` table + - receipts: Static File segment responsible for the `Receipts` table + + + Block number to query + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/static-file-header/path.mdx b/docs/vocs/docs/pages/cli/reth/db/static-file-header/path.mdx new file mode 100644 index 0000000000..2c8dc766cb --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/static-file-header/path.mdx @@ -0,0 +1,143 @@ +# reth db static-file-header path + +Query by path to static file + +```bash +$ reth db static-file-header path --help +``` +```txt +Usage: reth db static-file-header path [OPTIONS] + +Arguments: + + Path to the static file + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + mainnet, sepolia, holesky, hoodi, dev + + [default: mainnet] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces. + + - `http`: expects endpoint path to end with `/v1/traces` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] +``` \ No newline at end of file diff --git a/docs/vocs/docs/pages/cli/reth/db/stats.mdx b/docs/vocs/docs/pages/cli/reth/db/stats.mdx index 5bd316847c..5d985385e6 100644 --- a/docs/vocs/docs/pages/cli/reth/db/stats.mdx +++ b/docs/vocs/docs/pages/cli/reth/db/stats.mdx @@ -9,6 +9,9 @@ $ reth db stats --help Usage: reth db stats [OPTIONS] Options: + --skip-consistency-checks + Skip consistency checks for static files + --detailed-sizes Show only the total size for static files diff --git a/docs/vocs/docs/pages/cli/reth/download.mdx b/docs/vocs/docs/pages/cli/reth/download.mdx index 0ba6c7407e..63c8e507c1 100644 --- a/docs/vocs/docs/pages/cli/reth/download.mdx +++ b/docs/vocs/docs/pages/cli/reth/download.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + -u, --url Specify a snapshot URL or let the command propose a default one. diff --git a/docs/vocs/docs/pages/cli/reth/export-era.mdx b/docs/vocs/docs/pages/cli/reth/export-era.mdx index 051c81fcce..7efbe06426 100644 --- a/docs/vocs/docs/pages/cli/reth/export-era.mdx +++ b/docs/vocs/docs/pages/cli/reth/export-era.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --first-block-number Optional first block number to export from the db. It is by default 0. diff --git a/docs/vocs/docs/pages/cli/reth/import-era.mdx b/docs/vocs/docs/pages/cli/reth/import-era.mdx index 14aa47e0ef..a04bf89dcf 100644 --- a/docs/vocs/docs/pages/cli/reth/import-era.mdx +++ b/docs/vocs/docs/pages/cli/reth/import-era.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --path The path to a directory for import. diff --git a/docs/vocs/docs/pages/cli/reth/import.mdx b/docs/vocs/docs/pages/cli/reth/import.mdx index b8051d9d2f..5699512754 100644 --- a/docs/vocs/docs/pages/cli/reth/import.mdx +++ b/docs/vocs/docs/pages/cli/reth/import.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --no-state Disables stages that require state. diff --git a/docs/vocs/docs/pages/cli/reth/init-state.mdx b/docs/vocs/docs/pages/cli/reth/init-state.mdx index e43c87f806..e37d06c63a 100644 --- a/docs/vocs/docs/pages/cli/reth/init-state.mdx +++ b/docs/vocs/docs/pages/cli/reth/init-state.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --without-evm Specifies whether to initialize the state without relying on EVM historical data. diff --git a/docs/vocs/docs/pages/cli/reth/init.mdx b/docs/vocs/docs/pages/cli/reth/init.mdx index 6ad439c6a0..f0eff64c6a 100644 --- a/docs/vocs/docs/pages/cli/reth/init.mdx +++ b/docs/vocs/docs/pages/cli/reth/init.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index ea8c85f404..bed2a926da 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -160,6 +160,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. @@ -245,6 +250,13 @@ Networking: [default: All] + --tx-ingress-policy + Transaction ingress policy + + Determines which peers' transactions are accepted over P2P. + + [default: All] + --disable-tx-gossip Disable transaction pool gossip @@ -258,11 +270,18 @@ Networking: [default: sqrt] --required-block-hashes - Comma separated list of required block hashes. Peers that don't have these blocks will be filtered out + Comma separated list of required block hashes or block number=hash pairs. Peers that don't have these blocks will be filtered out. Format: hash or `block_number=hash` (e.g., 23115201=0x1234...) --network-id Optional network ID to override the chain specification's network ID for P2P connections + --netrestrict + Restrict network communication to the given IP networks (CIDR masks). + + Comma separated list of CIDR network specifications. Only peers with IP addresses within these ranges will be allowed to connect. + + Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + RPC: --http Enable the HTTP-RPC server @@ -738,6 +757,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -881,7 +909,7 @@ Engine: --engine.multiproof-chunk-size Multiproof task chunk size for proof targets - [default: 10] + [default: 60] --engine.reserved-cpu-cores Configure the number of reserved CPU cores for non-reth processes @@ -923,6 +951,23 @@ ERA: The ERA1 files are read from the remote host using HTTP GET requests parsing headers and bodies. +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Ress: --ress.enable Enable support for `ress` subprotocol diff --git a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx index a7670bacce..8eca4be487 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx @@ -106,6 +106,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. @@ -191,6 +196,13 @@ Networking: [default: All] + --tx-ingress-policy + Transaction ingress policy + + Determines which peers' transactions are accepted over P2P. + + [default: All] + --disable-tx-gossip Disable transaction pool gossip @@ -204,11 +216,18 @@ Networking: [default: sqrt] --required-block-hashes - Comma separated list of required block hashes. Peers that don't have these blocks will be filtered out + Comma separated list of required block hashes or block number=hash pairs. Peers that don't have these blocks will be filtered out. Format: hash or `block_number=hash` (e.g., 23115201=0x1234...) --network-id Optional network ID to override the chain specification's network ID for P2P connections + --netrestrict + Restrict network communication to the given IP networks (CIDR masks). + + Comma separated list of CIDR network specifications. Only peers with IP addresses within these ranges will be allowed to connect. + + Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + Datadir: --datadir The path to the data dir for all reth files and subdirectories. diff --git a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx index 76afd9a4cf..ada244eca4 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx @@ -106,6 +106,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. @@ -191,6 +196,13 @@ Networking: [default: All] + --tx-ingress-policy + Transaction ingress policy + + Determines which peers' transactions are accepted over P2P. + + [default: All] + --disable-tx-gossip Disable transaction pool gossip @@ -204,11 +216,18 @@ Networking: [default: sqrt] --required-block-hashes - Comma separated list of required block hashes. Peers that don't have these blocks will be filtered out + Comma separated list of required block hashes or block number=hash pairs. Peers that don't have these blocks will be filtered out. Format: hash or `block_number=hash` (e.g., 23115201=0x1234...) --network-id Optional network ID to override the chain specification's network ID for P2P connections + --netrestrict + Restrict network communication to the given IP networks (CIDR masks). + + Comma separated list of CIDR network specifications. Only peers with IP addresses within these ranges will be allowed to connect. + + Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + Datadir: --datadir The path to the data dir for all reth files and subdirectories. diff --git a/docs/vocs/docs/pages/cli/reth/prune.mdx b/docs/vocs/docs/pages/cli/reth/prune.mdx index 1febf6cdd5..d93c0d2302 100644 --- a/docs/vocs/docs/pages/cli/reth/prune.mdx +++ b/docs/vocs/docs/pages/cli/reth/prune.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/re-execute.mdx b/docs/vocs/docs/pages/cli/reth/re-execute.mdx index 742cbe5482..b51a66c7ac 100644 --- a/docs/vocs/docs/pages/cli/reth/re-execute.mdx +++ b/docs/vocs/docs/pages/cli/reth/re-execute.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --from The height to start at @@ -90,6 +116,9 @@ Database: [default: 10] + --skip-invalid-blocks + Continues with execution when an invalid block is encountered and collects these blocks + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/stage/drop.mdx b/docs/vocs/docs/pages/cli/reth/stage/drop.mdx index 05153f3fc2..0a055a6104 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/drop.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/drop.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Possible values: - headers: The headers stage within the pipeline diff --git a/docs/vocs/docs/pages/cli/reth/stage/dump.mdx b/docs/vocs/docs/pages/cli/reth/stage/dump.mdx index b74ee2280b..4ac4cd5d9b 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/dump.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/dump.mdx @@ -72,6 +72,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -84,6 +93,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/stage/run.mdx b/docs/vocs/docs/pages/cli/reth/stage/run.mdx index b7a5a41aaf..b77e278676 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/run.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/run.mdx @@ -65,6 +65,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -77,6 +86,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --metrics Enable Prometheus metrics. @@ -101,6 +127,8 @@ Database: Useful when you want to run diagnostics on the database. + NOTE: This flag is currently required for the headers, bodies, and execution stages because they use static files and must commit to properly unwind and run. + --checkpoints Save stage checkpoints @@ -210,6 +238,11 @@ Networking: This will also deterministically set the peer ID. If not specified, it will be set in the data dir for the chain being used. + --p2p-secret-key-hex + Hex encoded secret key to use for this node. + + This will also deterministically set the peer ID. Cannot be used together with `--p2p-secret-key`. + --no-persist-peers Do not persist peers. @@ -295,6 +328,13 @@ Networking: [default: All] + --tx-ingress-policy + Transaction ingress policy + + Determines which peers' transactions are accepted over P2P. + + [default: All] + --disable-tx-gossip Disable transaction pool gossip @@ -308,11 +348,18 @@ Networking: [default: sqrt] --required-block-hashes - Comma separated list of required block hashes. Peers that don't have these blocks will be filtered out + Comma separated list of required block hashes or block number=hash pairs. Peers that don't have these blocks will be filtered out. Format: hash or `block_number=hash` (e.g., 23115201=0x1234...) --network-id Optional network ID to override the chain specification's network ID for P2P connections + --netrestrict + Restrict network communication to the given IP networks (CIDR masks). + + Comma separated list of CIDR network specifications. Only peers with IP addresses within these ranges will be allowed to connect. + + Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/docs/vocs/docs/pages/cli/reth/stage/unwind.mdx b/docs/vocs/docs/pages/cli/reth/stage/unwind.mdx index 5c3a7d54f4..fcba15254d 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/unwind.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/unwind.mdx @@ -70,6 +70,15 @@ Database: The default value is 8TB. + --db.page-size + Database page size (e.g., 4KB, 8KB, 16KB). + + Specifies the page size used by the MDBX database. + + The page size determines the maximum database size. MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum database size is 8TB. To allow larger databases, increase this value to 8KB or higher. + + WARNING: This setting is only configurable at database creation; changing it later requires re-syncing. + --db.growth-step Database growth step (e.g., 4GB, 4KB) @@ -82,6 +91,23 @@ Database: --db.sync-mode Controls how aggressively the database synchronizes data to disk +Static Files: + --static-files.blocks-per-file.headers + Number of blocks per file for the headers segment + + --static-files.blocks-per-file.transactions + Number of blocks per file for the transactions segment + + --static-files.blocks-per-file.receipts + Number of blocks per file for the receipts segment + + --static-files.receipts + Store receipts in static files instead of the database. + + When enabled, receipts will be written to static files on disk instead of the database. + + Note: This setting can only be configured at genesis initialization. Once the node has been initialized, changing this flag requires re-syncing from scratch. + --offline If this is enabled, then all stages except headers, bodies, and sender recovery will be unwound diff --git a/docs/vocs/docs/pages/installation/source.mdx b/docs/vocs/docs/pages/installation/source.mdx index a7e1a2c33c..0666b818de 100644 --- a/docs/vocs/docs/pages/installation/source.mdx +++ b/docs/vocs/docs/pages/installation/source.mdx @@ -37,7 +37,7 @@ operating system: These are needed to build bindings for Reth's database. -The Minimum Supported Rust Version (MSRV) of this project is 1.80.0. If you already have a version of Rust installed, +The Minimum Supported Rust Version (MSRV) of this project is 1.88.0. If you already have a version of Rust installed, you can check your version by running `rustc --version`. To update your version of Rust, run `rustup update`. ## Build Reth diff --git a/docs/vocs/docs/pages/run/monitoring.mdx b/docs/vocs/docs/pages/run/monitoring.mdx index 1b463efdb7..ad9d23ee23 100644 --- a/docs/vocs/docs/pages/run/monitoring.mdx +++ b/docs/vocs/docs/pages/run/monitoring.mdx @@ -22,7 +22,7 @@ Now, as the node is running, you can `curl` the endpoint you provided to the `-- curl 127.0.0.1:9001 ``` -The response from this is quite descriptive, but it can be a bit verbose. Plus, it's just a static_file of the metrics at the time that you `curl`ed the endpoint. +The response from this is quite descriptive, but it can be a bit verbose. Plus, it's just a static file of the metrics at the time that you `curl`ed the endpoint. You can run the following command in a separate terminal to periodically poll the endpoint, and just print the values (without the header text) to the terminal: @@ -138,6 +138,34 @@ To configure the dashboard in Grafana, click on the squares icon in the upper le And voilà, you should see your dashboard! If you're not yet connected to any peers, the dashboard will look like it's in an empty state, but once you are, you should see it start populating with data. +## Observability with OTLP + +Reth supports OTLP via the `tracing` crate, meaning logs and traces can be exported to OpenTelemetry backends. For example, [Grafana Tempo](https://grafana.com/oss/tempo/) and [Jaeger Tracing](https://www.jaegertracing.io/) can be used to query and explore traces and logs from reth. + +If you already have a backend set up on your infrastructure, you can point reth to export its traces by providing the `--tracing-otlp` argument. + +To run Jaeger locally, you can read the [Jaeger setup docs](https://www.jaegertracing.io/docs/2.11/getting-started/). +This should run Jaeger with the otlp port `4318` and the dashboard port `16686`: +```bash +docker run --rm --name jaeger \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 5778:5778 \ + -p 9411:9411 \ + cr.jaegertracing.io/jaegertracing/jaeger:2.11.0 +``` + +Now we can provide the `--tracing-otlp` argument with reth: + +```bash +reth node --tracing-otlp=http://localhost:4317 --tracing-otlp-protocol grpc +``` + +The traces reth exported should now be searchable and viewable on http://localhost:16686. + +For environments where reth is processing a high number of transactions or blocks, it may be a good idea to bump `OTEL_BLRP_MAX_QUEUE_SIZE`, which has a default of `2048`. This controls how many log records can be recorded before batching and exporting. If this is set to too low of a value, spans and events may be dropped by the exporter. + ## Conclusion In this runbook, we took you through starting the node, exposing different log levels, exporting metrics, and finally viewing those metrics in a Grafana dashboard. diff --git a/docs/vocs/vocs.config.ts b/docs/vocs/vocs.config.ts index 5a4ea636fb..8664eedd3b 100644 --- a/docs/vocs/vocs.config.ts +++ b/docs/vocs/vocs.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ }, { text: 'GitHub', link: 'https://github.com/paradigmxyz/reth' }, { - text: 'v1.9.2', + text: 'v1.9.3', items: [ { text: 'Releases', diff --git a/etc/docker-compose.yml b/etc/docker-compose.yml index 73311616fd..7fab1ce75a 100644 --- a/etc/docker-compose.yml +++ b/etc/docker-compose.yml @@ -61,11 +61,13 @@ services: - ./grafana/datasources:/etc/grafana/provisioning/datasources - ./grafana/dashboards:/etc/grafana/provisioning_temp/dashboards # 1. Copy dashboards from temp directory to prevent modifying original host files - # 2. Replace Prometheus datasource placeholder with the actual name + # 2. Replace Prometheus datasource placeholders with the actual name # 3. Run Grafana entrypoint: > sh -c "cp -r /etc/grafana/provisioning_temp/dashboards/. /etc/grafana/provisioning/dashboards && find /etc/grafana/provisioning/dashboards/ -name '*.json' -exec sed -i 's/$${DS_PROMETHEUS}/Prometheus/g' {} \+ && + find /etc/grafana/provisioning/dashboards/ -name '*.json' -exec sed -i 's/$${datasource}/Prometheus/g' {} \+ && + find /etc/grafana/provisioning/dashboards/ -name '*.json' -exec sed -i 's/$${VAR_INSTANCE_LABEL}/instance/g' {} \+ && /run.sh" volumes: diff --git a/examples/custom-node/src/engine.rs b/examples/custom-node/src/engine.rs index 47054d2cae..b4873cfcce 100644 --- a/examples/custom-node/src/engine.rs +++ b/examples/custom-node/src/engine.rs @@ -72,6 +72,15 @@ impl ExecutionPayload for CustomExecutionData { } } +impl From<&reth_optimism_flashblocks::FlashBlockCompleteSequence> for CustomExecutionData { + fn from(sequence: &reth_optimism_flashblocks::FlashBlockCompleteSequence) -> Self { + let inner = OpExecutionData::from(sequence); + // Derive extension from sequence data - using gas_used from last flashblock as an example + let extension = sequence.last().diff.gas_used; + Self { inner, extension } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomPayloadAttributes { #[serde(flatten)] diff --git a/examples/custom-node/src/evm/config.rs b/examples/custom-node/src/evm/config.rs index a7dee31a83..f2bd332689 100644 --- a/examples/custom-node/src/evm/config.rs +++ b/examples/custom-node/src/evm/config.rs @@ -9,6 +9,7 @@ use alloy_eips::{eip2718::WithEncoded, Decodable2718}; use alloy_evm::EvmEnv; use alloy_op_evm::OpBlockExecutionCtx; use alloy_rpc_types_engine::PayloadError; +use op_alloy_rpc_types_engine::flashblock::OpFlashblockPayloadBase; use op_revm::OpSpecId; use reth_engine_primitives::ExecutableTxIterator; use reth_ethereum::{ @@ -23,7 +24,6 @@ use reth_op::{ node::{OpEvmConfig, OpNextBlockEnvAttributes, OpRethReceiptBuilder}, primitives::SignedTransaction, }; -use reth_optimism_flashblocks::ExecutionPayloadBaseV1; use reth_rpc_api::eth::helpers::pending_block::BuildPendingEnv; use std::sync::Arc; @@ -143,8 +143,8 @@ pub struct CustomNextBlockEnvAttributes { extension: u64, } -impl From for CustomNextBlockEnvAttributes { - fn from(value: ExecutionPayloadBaseV1) -> Self { +impl From for CustomNextBlockEnvAttributes { + fn from(value: OpFlashblockPayloadBase) -> Self { Self { inner: value.into(), extension: 0 } } } diff --git a/examples/custom-node/src/rpc.rs b/examples/custom-node/src/rpc.rs index 8259297367..b6dc7742d9 100644 --- a/examples/custom-node/src/rpc.rs +++ b/examples/custom-node/src/rpc.rs @@ -3,14 +3,15 @@ use crate::{ primitives::{CustomHeader, CustomTransaction}, }; use alloy_consensus::error::ValueError; +use alloy_evm::EvmEnv; use alloy_network::TxSigner; use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::{OpTransactionReceipt, OpTransactionRequest}; use reth_op::rpc::RpcTypes; use reth_rpc_api::eth::{ - transaction::TryIntoTxEnv, EthTxEnvError, SignTxRequestError, SignableTxRequest, TryIntoSimTx, + EthTxEnvError, SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv, }; -use revm::context::{BlockEnv, CfgEnv}; +use revm::context::BlockEnv; #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] @@ -34,10 +35,9 @@ impl TryIntoTxEnv for OpTransactionRequest { fn try_into_tx_env( self, - cfg_env: &CfgEnv, - block_env: &BlockEnv, + evm_env: &EvmEnv, ) -> Result { - Ok(CustomTxEnv::Op(self.try_into_tx_env(cfg_env, block_env)?)) + Ok(CustomTxEnv::Op(self.try_into_tx_env(evm_env)?)) } } diff --git a/examples/db-access/src/main.rs b/examples/db-access/src/main.rs index 5c77d94066..1042ac55be 100644 --- a/examples/db-access/src/main.rs +++ b/examples/db-access/src/main.rs @@ -1,13 +1,13 @@ #![warn(unused_crate_dependencies)] -use alloy_primitives::{Address, B256}; +use alloy_primitives::{keccak256, Address, B256}; use reth_ethereum::{ chainspec::ChainSpecBuilder, node::EthereumNode, primitives::{AlloyBlockHeader, SealedBlock, SealedHeader}, provider::{ - providers::ReadOnlyConfig, AccountReader, BlockReader, BlockSource, HeaderProvider, - ReceiptProvider, StateProvider, TransactionVariant, TransactionsProvider, + providers::ReadOnlyConfig, AccountReader, BlockNumReader, BlockReader, BlockSource, + HeaderProvider, ReceiptProvider, StateProvider, TransactionVariant, TransactionsProvider, }, rpc::eth::primitives::Filter, TransactionSigned, @@ -39,16 +39,13 @@ fn main() -> eyre::Result<()> { txs_provider_example(&provider)?; receipts_provider_example(&provider)?; + state_provider_example(factory.latest()?, &provider, provider.best_block_number()?)?; + state_provider_example(factory.history_by_block_number(block_num)?, &provider, block_num)?; + // Closes the RO transaction opened in the `factory.provider()` call. This is optional and // would happen anyway at the end of the function scope. drop(provider); - // Run the example against latest state - state_provider_example(factory.latest()?)?; - - // Run it with historical state - state_provider_example(factory.history_by_block_number(block_num)?)?; - Ok(()) } @@ -178,15 +175,21 @@ fn receipts_provider_example< let header = provider.header_by_number(header_num)?.unwrap(); let bloom = header.logs_bloom(); - // 2. Construct the address/topics filters - // For a hypothetical address, we'll want to filter down for a specific indexed topic (e.g. - // `from`). - let addr = Address::random(); - let topic = B256::random(); + // 2. Construct the address/topics filters. topic0 always refers to the event signature, so + // filter it with event_signature() (or use the .event() helper). The remaining helpers map to + // the indexed parameters in declaration order (topic1 -> first indexed param, etc). + let contract_addr = Address::random(); + let indexed_from = Address::random(); + let indexed_to = Address::random(); + let transfer_signature = keccak256("Transfer(address,address,uint256)"); - // TODO: Make it clearer how to choose between event_signature(topic0) (event name) and the - // other 3 indexed topics. This API is a bit clunky and not obvious to use at the moment. - let filter = Filter::new().address(addr).event_signature(topic); + // This matches ERC-20 Transfer events emitted by contract_addr where both indexed addresses are + // fixed. If your event declares a third indexed parameter, continue with topic3(...). + let filter = Filter::new() + .address(contract_addr) + .event_signature(transfer_signature) + .topic1(indexed_from) + .topic2(indexed_to); // 3. If the address & topics filters match do something. We use the outer check against the // bloom filter stored in the header to avoid having to query the receipts table when there @@ -204,16 +207,39 @@ fn receipts_provider_example< Ok(()) } -fn state_provider_example(provider: T) -> eyre::Result<()> { +/// The `StateProvider` allows querying the state tables. +fn state_provider_example( + provider: T, + headers: &H, + number: u64, +) -> eyre::Result<()> { let address = Address::random(); let storage_key = B256::random(); + let slots = [storage_key]; + + let header = headers.header_by_number(number)?.ok_or(eyre::eyre!("header not found"))?; + let state_root = header.state_root(); // Can get account / storage state with simple point queries - let _account = provider.basic_account(&address)?; - let _code = provider.account_code(&address)?; - let _storage = provider.storage(address, storage_key)?; - // TODO: unimplemented. - // let _proof = provider.proof(address, &[])?; + let account = provider.basic_account(&address)?; + let code = provider.account_code(&address)?; + let storage_value = provider.storage(address, storage_key)?; + + println!( + "state at block #{number}: addr={address:?}, nonce={}, balance={}, storage[{:?}]={:?}, has_code={}", + account.as_ref().map(|acc| acc.nonce).unwrap_or_default(), + account.as_ref().map(|acc| acc.balance).unwrap_or_default(), + storage_key, + storage_value, + code.is_some() + ); + + // Returns a bundled proof with the account's info + let proof = provider.proof(Default::default(), address, &slots)?; + + // Can verify the returned proof against the state root + proof.verify(state_root)?; + println!("account proof verified against state root {state_root:?}"); Ok(()) } diff --git a/examples/exex-subscription/src/main.rs b/examples/exex-subscription/src/main.rs index e39408a3dc..a2b46686f7 100644 --- a/examples/exex-subscription/src/main.rs +++ b/examples/exex-subscription/src/main.rs @@ -1,8 +1,5 @@ -#![allow(dead_code)] - //! An ExEx example that installs a new RPC subscription endpoint that emits storage changes for a //! requested address. -#[allow(dead_code)] use alloy_primitives::{Address, U256}; use futures::TryStreamExt; use jsonrpsee::{ @@ -167,8 +164,7 @@ async fn my_exex( fn main() -> eyre::Result<()> { reth_ethereum::cli::Cli::parse_args().run(|builder, _| async move { let (subscriptions_tx, subscriptions_rx) = mpsc::unbounded_channel::(); - - let rpc = StorageWatcherRpc::new(subscriptions_tx.clone()); + let rpc = StorageWatcherRpc::new(subscriptions_tx); let handle: NodeHandleFor = builder .node(EthereumNode::default()) diff --git a/testing/prestate/tx-selfdestruct-prestate.json b/testing/prestate/tx-selfdestruct-prestate.json new file mode 100644 index 0000000000..5ab17b6c2c --- /dev/null +++ b/testing/prestate/tx-selfdestruct-prestate.json @@ -0,0 +1,43 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "0x5cf6756af36644fc13b2d5d8637a50ae7e6c5fef": { + "balance": "0x1b1989a1deedbd" + }, + "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5": { + "balance": "0xc8714456682467dc", + "nonce": 797368 + }, + "0xa7fb5ca286fc3fd67525629048a4de3ba24cba2e": { + "balance": "0x23af8fab959cdfb42", + "nonce": 236280 + }, + "0xc77ad0a71008d7094a62cfbd250a2eb2afdf2776": { + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106100935760003560e01c80638da5cb5b116100665780638da5cb5b146100f55780639f9fb96814610106578063f2fde38b14610127578063f3fef3a31461013a578063fc0c546a1461014d57600080fd5b806331d4fd77146100985780634e71e0c8146100ad5780635b51bec0146100b557806366d003ac146100d0575b600080fd5b6100ab6100a63660046104a1565b610160565b005b6100ab6101e5565b604051640302e362e360dc1b81526020015b60405180910390f35b6002546001600160a01b03165b6040516001600160a01b0390911681526020016100c7565b6000546001600160a01b03166100dd565b610119610114366004610505565b610283565b6040516100c792919061051d565b6100ab610135366004610480565b61030f565b6100ab6101483660046104dc565b610357565b6003546100dd906001600160a01b031681565b336101736000546001600160a01b031690565b6001600160a01b03161461018657600080fd5b6001600160a01b03831661019957600080fd5b600280546001600160a01b0319166001600160a01b0385161790556101be828261038b565b5050600054600280546001600160a01b0319166001600160a01b0390921691909117905550565b6001546001600160a01b0316331415610281576001546001600160a01b03166102166000546001600160a01b031690565b6001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a360018054600080546001600160a01b0383166001600160a01b031991821681179092559182169092556002805490911690911790555b565b60606000806040518060200161029890610457565b601f1982820381018352601f90910116604081815282516020808501919091206001600160f81b0319828501526bffffffffffffffffffffffff193060601b166021850152603584019890985260558084019890985281518084039098018852607590920190528551950194909420939492505050565b336103226000546001600160a01b031690565b6001600160a01b03161461033557600080fd5b600180546001600160a01b0319166001600160a01b0392909216919091179055565b3361036a6000546001600160a01b031690565b6001600160a01b03161461037d57600080fd5b610387828261038b565b5050565b60008061039783610283565b600380546001600160a01b0319166001600160a01b038881169190911790915582519294509092508216319060009085906020860183f590506001600160a01b0381166103e357600080fd5b600380546001600160a01b03191690557fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb6104266002546001600160a01b031690565b604080516001600160a01b0392831681529186166020830152810184905260600160405180910390a1505050505050565b6103118061058283390190565b80356001600160a01b038116811461047b57600080fd5b919050565b600060208284031215610491578081fd5b61049a82610464565b9392505050565b6000806000606084860312156104b5578182fd5b6104be84610464565b92506104cc60208501610464565b9150604084013590509250925092565b600080604083850312156104ee578182fd5b6104f783610464565b946020939093013593505050565b600060208284031215610516578081fd5b5035919050565b6040815260008351806040840152815b8181101561054a576020818701810151606086840101520161052d565b8181111561055b5782606083860101525b506001600160a01b0393909316602083015250601f91909101601f19160160600191905056fe608060408190526319b400eb60e21b8152339060009082906366d003ac9060849060209060048186803b15801561003557600080fd5b505afa158015610049573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061006d9190610271565b90506000826001600160a01b031663fc0c546a6040518163ffffffff1660e01b815260040160206040518083038186803b1580156100aa57600080fd5b505afa1580156100be573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e29190610271565b90506001600160a01b0381161561018d576040516370a0823160e01b815230600482015261018d9083906001600160a01b038416906370a082319060240160206040518083038186803b15801561013857600080fd5b505afa15801561014c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061017091906102bf565b836001600160a01b031661019960201b610009179092919060201c565b816001600160a01b0316ff5b604080516001600160a01b038481166024830152604480830185905283518084039091018152606490920183526020820180516001600160e01b031663a9059cbb60e01b17905291516000928392908716916101f591906102d7565b6000604051808303816000865af19150503d8060008114610232576040519150601f19603f3d011682016040523d82523d6000602084013e610237565b606091505b5091509150818015610261575080511580610261575080806020019051810190610261919061029f565b61026a57600080fd5b5050505050565b600060208284031215610282578081fd5b81516001600160a01b0381168114610298578182fd5b9392505050565b6000602082840312156102b0578081fd5b81518015158114610298578182fd5b6000602082840312156102d0578081fd5b5051919050565b60008251815b818110156102f757602081860181015185830152016102dd565b818111156103055782828501525b50919091019291505056fea2646970667358221220820d083773baf3f84f3af74133087e936c58f2a05fdf46b525ba37dba6ae0e2d64736f6c63430008040033", + "codeHash": "0x1b064b625546024df8b0e61d74d84bba7e1f22e31ed3b3c1b37fbe533e33bd72", + "nonce": 118087, + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000a7fb5ca286fc3fd67525629048a4de3ba24cba2e", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0x000000000000000000000000a7fb5ca286fc3fd67525629048a4de3ba24cba2e", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "0xdac17f958d2ee523a2206206994597c13d831ec7": { + "balance": "0x1", + "code": "0x606060405260043610610196576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde031461019b5780630753c30c14610229578063095ea7b3146102625780630e136b19146102a45780630ecb93c0146102d157806318160ddd1461030a57806323b872dd1461033357806326976e3f1461039457806327e235e3146103e9578063313ce56714610436578063353907141461045f5780633eaaf86b146104885780633f4ba83a146104b157806359bf1abe146104c65780635c658165146105175780635c975abb1461058357806370a08231146105b05780638456cb59146105fd578063893d20e8146106125780638da5cb5b1461066757806395d89b41146106bc578063a9059cbb1461074a578063c0324c771461078c578063cc872b66146107b8578063db006a75146107db578063dd62ed3e146107fe578063dd644f721461086a578063e47d606014610893578063e4997dc5146108e4578063e5b5019a1461091d578063f2fde38b14610946578063f3bdc2281461097f575b600080fd5b34156101a657600080fd5b6101ae6109b8565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101ee5780820151818401526020810190506101d3565b50505050905090810190601f16801561021b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561023457600080fd5b610260600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610a56565b005b341561026d57600080fd5b6102a2600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610b73565b005b34156102af57600080fd5b6102b7610cc1565b604051808215151515815260200191505060405180910390f35b34156102dc57600080fd5b610308600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610cd4565b005b341561031557600080fd5b61031d610ded565b6040518082815260200191505060405180910390f35b341561033e57600080fd5b610392600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610ebd565b005b341561039f57600080fd5b6103a761109d565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156103f457600080fd5b610420600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506110c3565b6040518082815260200191505060405180910390f35b341561044157600080fd5b6104496110db565b6040518082815260200191505060405180910390f35b341561046a57600080fd5b6104726110e1565b6040518082815260200191505060405180910390f35b341561049357600080fd5b61049b6110e7565b6040518082815260200191505060405180910390f35b34156104bc57600080fd5b6104c46110ed565b005b34156104d157600080fd5b6104fd600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506111ab565b604051808215151515815260200191505060405180910390f35b341561052257600080fd5b61056d600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611201565b6040518082815260200191505060405180910390f35b341561058e57600080fd5b610596611226565b604051808215151515815260200191505060405180910390f35b34156105bb57600080fd5b6105e7600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611239565b6040518082815260200191505060405180910390f35b341561060857600080fd5b610610611348565b005b341561061d57600080fd5b610625611408565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561067257600080fd5b61067a611431565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156106c757600080fd5b6106cf611456565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561070f5780820151818401526020810190506106f4565b50505050905090810190601f16801561073c5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561075557600080fd5b61078a600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506114f4565b005b341561079757600080fd5b6107b6600480803590602001909190803590602001909190505061169e565b005b34156107c357600080fd5b6107d96004808035906020019091905050611783565b005b34156107e657600080fd5b6107fc600480803590602001909190505061197a565b005b341561080957600080fd5b610854600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611b0d565b6040518082815260200191505060405180910390f35b341561087557600080fd5b61087d611c52565b6040518082815260200191505060405180910390f35b341561089e57600080fd5b6108ca600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611c58565b604051808215151515815260200191505060405180910390f35b34156108ef57600080fd5b61091b600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611c78565b005b341561092857600080fd5b610930611d91565b6040518082815260200191505060405180910390f35b341561095157600080fd5b61097d600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611db5565b005b341561098a57600080fd5b6109b6600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611e8a565b005b60078054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610a4e5780601f10610a2357610100808354040283529160200191610a4e565b820191906000526020600020905b815481529060010190602001808311610a3157829003601f168201915b505050505081565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515610ab157600080fd5b6001600a60146101000a81548160ff02191690831515021790555080600a60006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507fcc358699805e9a8b7f77b522628c7cb9abd07d9efb86b6fb616af1609036a99e81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a150565b604060048101600036905010151515610b8b57600080fd5b600a60149054906101000a900460ff1615610cb157600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663aee92d333385856040518463ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050600060405180830381600087803b1515610c9857600080fd5b6102c65a03f11515610ca957600080fd5b505050610cbc565b610cbb838361200e565b5b505050565b600a60149054906101000a900460ff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515610d2f57600080fd5b6001600660008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055507f42e160154868087d6bfdc0ca23d96a1c1cfa32f1b72ba9ba27b69b98a0d819dc81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a150565b6000600a60149054906101000a900460ff1615610eb457600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166318160ddd6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b1515610e9257600080fd5b6102c65a03f11515610ea357600080fd5b505050604051805190509050610eba565b60015490505b90565b600060149054906101000a900460ff16151515610ed957600080fd5b600660008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16151515610f3257600080fd5b600a60149054906101000a900460ff161561108c57600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b477adb338585856040518563ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001945050505050600060405180830381600087803b151561107357600080fd5b6102c65a03f1151561108457600080fd5b505050611098565b6110978383836121ab565b5b505050565b600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60026020528060005260406000206000915090505481565b60095481565b60045481565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561114857600080fd5b600060149054906101000a900460ff16151561116357600080fd5b60008060146101000a81548160ff0219169083151502179055507f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3360405160405180910390a1565b6000600660008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff169050919050565b6005602052816000526040600020602052806000526040600020600091509150505481565b600060149054906101000a900460ff1681565b6000600a60149054906101000a900460ff161561133757600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166370a08231836000604051602001526040518263ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001915050602060405180830381600087803b151561131557600080fd5b6102c65a03f1151561132657600080fd5b505050604051805190509050611343565b61134082612652565b90505b919050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156113a357600080fd5b600060149054906101000a900460ff161515156113bf57600080fd5b6001600060146101000a81548160ff0219169083151502179055507f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62560405160405180910390a1565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60088054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156114ec5780601f106114c1576101008083540402835291602001916114ec565b820191906000526020600020905b8154815290600101906020018083116114cf57829003601f168201915b505050505081565b600060149054906101000a900460ff1615151561151057600080fd5b600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1615151561156957600080fd5b600a60149054906101000a900460ff161561168f57600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16636e18980a3384846040518463ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050600060405180830381600087803b151561167657600080fd5b6102c65a03f1151561168757600080fd5b50505061169a565b611699828261269b565b5b5050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156116f957600080fd5b60148210151561170857600080fd5b60328110151561171757600080fd5b81600381905550611736600954600a0a82612a0390919063ffffffff16565b6004819055507fb044a1e409eac5c48e5af22d4af52670dd1a99059537a78b31b48c6500a6354e600354600454604051808381526020018281526020019250505060405180910390a15050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156117de57600080fd5b60015481600154011115156117f257600080fd5b600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205481600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054011115156118c257600080fd5b80600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550806001600082825401925050819055507fcb8241adb0c3fdb35b70c24ce35c5eb0c17af7431c99f827d44a445ca624176a816040518082815260200191505060405180910390a150565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156119d557600080fd5b80600154101515156119e657600080fd5b80600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515611a5557600080fd5b8060016000828254039250508190555080600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055507f702d5967f45f6513a38ffc42d6ba9bf230bd40e8f53b16363c7eb4fd2deb9a44816040518082815260200191505060405180910390a150565b6000600a60149054906101000a900460ff1615611c3f57600a60009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663dd62ed3e84846000604051602001526040518363ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200192505050602060405180830381600087803b1515611c1d57600080fd5b6102c65a03f11515611c2e57600080fd5b505050604051805190509050611c4c565b611c498383612a3e565b90505b92915050565b60035481565b60066020528060005260406000206000915054906101000a900460ff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515611cd357600080fd5b6000600660008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055507fd7e9ec6e6ecd65492dce6bf513cd6867560d49544421d0783ddf06e76c24470c81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a150565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515611e1057600080fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141515611e8757806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505b50565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515611ee757600080fd5b600660008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff161515611f3f57600080fd5b611f4882611239565b90506000600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550806001600082825403925050819055507f61e6e66b0d6339b2980aecc6ccc0039736791f0ccde9ed512e789a7fbdd698c68282604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060405180910390a15050565b60406004810160003690501015151561202657600080fd5b600082141580156120b457506000600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b1515156120c057600080fd5b81600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a3505050565b60008060006060600481016000369050101515156121c857600080fd5b600560008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054935061227061271061226260035488612a0390919063ffffffff16565b612ac590919063ffffffff16565b92506004548311156122825760045492505b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84101561233e576122bd8585612ae090919063ffffffff16565b600560008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b6123518386612ae090919063ffffffff16565b91506123a585600260008a73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612ae090919063ffffffff16565b600260008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061243a82600260008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612af990919063ffffffff16565b600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555060008311156125e4576124f983600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612af990919063ffffffff16565b600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a35b8573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050505050565b6000600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6000806040600481016000369050101515156126b657600080fd5b6126df6127106126d160035487612a0390919063ffffffff16565b612ac590919063ffffffff16565b92506004548311156126f15760045492505b6127048385612ae090919063ffffffff16565b915061275884600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612ae090919063ffffffff16565b600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506127ed82600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612af990919063ffffffff16565b600260008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506000831115612997576128ac83600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054612af990919063ffffffff16565b600260008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a35b8473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a35050505050565b6000806000841415612a185760009150612a37565b8284029050828482811515612a2957fe5b04141515612a3357fe5b8091505b5092915050565b6000600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6000808284811515612ad357fe5b0490508091505092915050565b6000828211151515612aee57fe5b818303905092915050565b6000808284019050838110151515612b0d57fe5b80915050929150505600a165627a7a72305820645ee12d73db47fd78ba77fa1f824c3c8f9184061b3b10386beb4dc9236abb280029", + "codeHash": "0xb44fb4e949d0f78f87f79ee46428f23a2a5713ce6fc6e0beb3dda78c2ac1ea55", + "nonce": 1, + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000c6cde7c39eb2f0f0095f41570af89efc2c1ea828", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000004": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x000000000000000000000000000000000000000000000000000000000000000a": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x04af465794ace36b644b505e744c3e5bb4a1136e86a6794cce220609f98e2c59": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x2ffe96b501e60292eb0316c8fc38442c42e408e4f13dbcfa5251330006d737ed": "0x000000000000000000000000000000000000000000000000000001cc47344a6e", + "0x7778dedb5c9fd9ac49539a0a3828992d817bdb68f3215a7557c2f9ce3d7e7dd8": "0x0000000000000000000000000000000000000000000000000000000003197500" + } + } + } +} diff --git a/testing/testing-utils/Cargo.toml b/testing/testing-utils/Cargo.toml index 06e73631ef..35b4042427 100644 --- a/testing/testing-utils/Cargo.toml +++ b/testing/testing-utils/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] reth-ethereum-primitives = { workspace = true, features = ["arbitrary", "std"] } reth-primitives-traits = { workspace = true, features = ["secp256k1", "arbitrary"] } - alloy-genesis.workspace = true alloy-primitives = { workspace = true, features = ["rand"] } alloy-consensus.workspace = true @@ -23,3 +22,6 @@ alloy-eips.workspace = true rand.workspace = true secp256k1 = { workspace = true, features = ["rand"] } rand_08.workspace = true + +[dev-dependencies] +alloy-rlp.workspace = true diff --git a/testing/testing-utils/src/generators.rs b/testing/testing-utils/src/generators.rs index 09116129bc..c7fdad6eb0 100644 --- a/testing/testing-utils/src/generators.rs +++ b/testing/testing-utils/src/generators.rs @@ -534,7 +534,7 @@ mod tests { #[test] fn test_sign_eip_155() { // reference: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#example - let transaction = Transaction::Legacy(TxLegacy { + let tx = TxLegacy { chain_id: Some(1), nonce: 9, gas_price: 20 * 10_u128.pow(9), @@ -542,12 +542,11 @@ mod tests { to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(10_u128.pow(18)), input: Bytes::default(), - }); + }; + let transaction = Transaction::Legacy(tx.clone()); - // TODO resolve dependency issue - // let expected = - // hex!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080"); - // assert_eq!(expected, &alloy_rlp::encode(transaction)); + let expected = hex!("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080"); + assert_eq!(expected.as_slice(), &alloy_rlp::encode(tx)); let hash = transaction.signature_hash(); let expected = diff --git a/testing/testing-utils/src/lib.rs b/testing/testing-utils/src/lib.rs index 8baf40d1b6..e493ce78f3 100644 --- a/testing/testing-utils/src/lib.rs +++ b/testing/testing-utils/src/lib.rs @@ -8,8 +8,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] +pub mod generators; pub mod genesis_allocator; pub use genesis_allocator::GenesisAllocator; - -pub mod generators;