mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
48 Commits
devnet4
...
mediocrego
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07e228412 | ||
|
|
4b4a1b80d8 | ||
|
|
5ab335d04e | ||
|
|
baf6ef9778 | ||
|
|
e71cf3040b | ||
|
|
adf2930e84 | ||
|
|
d9d3f69557 | ||
|
|
8248aa29d1 | ||
|
|
0d19b17bf3 | ||
|
|
344037d04e | ||
|
|
aca6261107 | ||
|
|
d4ca2e2687 | ||
|
|
e5e0abb47e | ||
|
|
5d4019049a | ||
|
|
743d42ff6d | ||
|
|
843b5a826a | ||
|
|
225e3ae238 | ||
|
|
345fbbbfdb | ||
|
|
db17c899c3 | ||
|
|
b6eec2e684 | ||
|
|
31d0c7852d | ||
|
|
b60758ef73 | ||
|
|
6e8dbe34a4 | ||
|
|
c4d0949a23 | ||
|
|
d5169eda88 | ||
|
|
87b5240ec1 | ||
|
|
ffb0587b19 | ||
|
|
45db5e0b5d | ||
|
|
7db14d095d | ||
|
|
134a7f364b | ||
|
|
d92ad5aa34 | ||
|
|
b5ad0018a3 | ||
|
|
5036eb59fb | ||
|
|
812e479b69 | ||
|
|
5041d55bc3 | ||
|
|
ebfaa6f4c5 | ||
|
|
cacb69aca9 | ||
|
|
be4e8cd017 | ||
|
|
c757a310e1 | ||
|
|
ca20cc13ef | ||
|
|
9e38dde3e0 | ||
|
|
ca352832b8 | ||
|
|
84b520560b | ||
|
|
e2bd518097 | ||
|
|
761acad803 | ||
|
|
b97544a05e | ||
|
|
037828f6aa | ||
|
|
9883b7140e |
25
.github/scripts/hive/build_simulators.sh
vendored
25
.github/scripts/hive/build_simulators.sh
vendored
@@ -1,6 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
fixture_variant="${1:-osaka}"
|
||||
|
||||
case "${fixture_variant}" in
|
||||
amsterdam)
|
||||
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.7.0/fixtures_bal.tar.gz"
|
||||
eels_branch="devnets/bal/4"
|
||||
;;
|
||||
osaka)
|
||||
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz"
|
||||
eels_branch="forks/osaka"
|
||||
;;
|
||||
*)
|
||||
echo "unknown hive fixture variant: ${fixture_variant}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create the hive_assets directory
|
||||
mkdir hive_assets/
|
||||
|
||||
@@ -12,12 +29,12 @@ go build .
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
echo "Building images"
|
||||
./hive -client reth --sim "ethereum/eels/consume-engine" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.buildarg fixtures="${eels_fixtures}" \
|
||||
--sim.buildarg branch="${eels_branch}" \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/eels/consume-rlp" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.buildarg fixtures="${eels_fixtures}" \
|
||||
--sim.buildarg branch="${eels_branch}" \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
|
||||
|
||||
6
.github/scripts/hive/run_simulator.sh
vendored
6
.github/scripts/hive/run_simulator.sh
vendored
@@ -5,6 +5,12 @@ cd hivetests/
|
||||
|
||||
sim="${1}"
|
||||
limit="${2}"
|
||||
fixture_variant="${3:-}"
|
||||
|
||||
if [[ "${fixture_variant}" == "osaka" && "${sim}" == *"eels"* && "${limit}" == *"tests/amsterdam"* ]]; then
|
||||
echo "osaka fixtures do not support amsterdam tests"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use lower parallelism for eels tests to avoid OOM-killing the runner
|
||||
parallelism=16
|
||||
|
||||
204
.github/workflows/hive.yml
vendored
204
.github/workflows/hive.yml
vendored
@@ -25,7 +25,14 @@ jobs:
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- amsterdam
|
||||
- osaka
|
||||
name: Prepare Hive - ${{ matrix.variant == 'amsterdam' && 'Amsterdam' || 'Osaka' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
@@ -48,11 +55,11 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ./hive_assets
|
||||
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
|
||||
key: hive-assets-${{ matrix.variant }}-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
|
||||
|
||||
- name: Build hive assets
|
||||
if: steps.cache-hive.outputs.cache-hit != 'true'
|
||||
run: .github/scripts/hive/build_simulators.sh
|
||||
run: .github/scripts/hive/build_simulators.sh ${{ matrix.variant }}
|
||||
|
||||
- name: Load cached Docker images
|
||||
if: steps.cache-hive.outputs.cache-hit == 'true'
|
||||
@@ -70,9 +77,186 @@ jobs:
|
||||
- name: Upload hive assets
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hive_assets
|
||||
name: hive_assets_${{ matrix.variant }}
|
||||
path: ./hive_assets
|
||||
test:
|
||||
test-amsterdam:
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# ethereum/rpc to be deprecated:
|
||||
# https://github.com/ethereum/hive/pull/1117
|
||||
scenario:
|
||||
- sim: smoke/genesis
|
||||
- sim: smoke/network
|
||||
- sim: ethereum/sync
|
||||
- sim: devp2p
|
||||
limit: discv4
|
||||
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
|
||||
# action on our side, remove from here when we get unexpected passes on these tests
|
||||
# - sim: devp2p
|
||||
# limit: eth
|
||||
# include:
|
||||
# - MaliciousHandshake
|
||||
# # failures tracked in https://github.com/paradigmxyz/reth/issues/14825
|
||||
# - Status
|
||||
# - GetBlockHeaders
|
||||
# - ZeroRequestID
|
||||
# - GetBlockBodies
|
||||
# - Transaction
|
||||
# - NewPooledTxs
|
||||
- sim: devp2p
|
||||
limit: discv5
|
||||
include:
|
||||
# failures tracked at https://github.com/paradigmxyz/reth/issues/14825
|
||||
- PingLargeRequestID
|
||||
- sim: ethereum/engine
|
||||
limit: engine-exchange-capabilities
|
||||
- sim: ethereum/engine
|
||||
limit: engine-withdrawals
|
||||
- sim: ethereum/engine
|
||||
limit: engine-auth
|
||||
- sim: ethereum/engine
|
||||
limit: engine-api
|
||||
- sim: ethereum/engine
|
||||
limit: cancun
|
||||
# eth_ rpc methods
|
||||
- sim: ethereum/rpc-compat
|
||||
include:
|
||||
- eth_blockNumber
|
||||
- eth_call
|
||||
- eth_chainId
|
||||
- eth_createAccessList
|
||||
- eth_estimateGas
|
||||
- eth_feeHistory
|
||||
- eth_getBalance
|
||||
- eth_getBlockBy
|
||||
- eth_getBlockTransactionCountBy
|
||||
- eth_getCode
|
||||
- eth_getProof
|
||||
- eth_getStorage
|
||||
- eth_getTransactionBy
|
||||
- eth_getTransactionCount
|
||||
- eth_getTransactionReceipt
|
||||
- eth_sendRawTransaction
|
||||
- eth_syncing
|
||||
# debug_ rpc methods
|
||||
- debug_
|
||||
|
||||
# consume-engine
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/prague.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/cancun.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/shanghai.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/berlin.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/istanbul.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/homestead.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/frontier.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/paris.*
|
||||
|
||||
# consume-rlp
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/prague.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/cancun.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/shanghai.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/berlin.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/istanbul.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/homestead.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/frontier.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/paris.*
|
||||
needs:
|
||||
- build-reth
|
||||
- prepare-hive
|
||||
name: Hive-Amsterdam / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hive_assets_amsterdam
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: reth
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker images
|
||||
run: .github/scripts/hive/load_images.sh
|
||||
|
||||
- name: Move hive binary
|
||||
run: |
|
||||
mv /tmp/hive /usr/local/bin
|
||||
chmod +x /usr/local/bin/hive
|
||||
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ethereum/hive
|
||||
ref: master
|
||||
path: hivetests
|
||||
|
||||
- name: Run simulator
|
||||
run: |
|
||||
LIMIT="${{ matrix.scenario.limit }}"
|
||||
TESTS="${{ join(matrix.scenario.include, '|') }}"
|
||||
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
|
||||
FILTER="$LIMIT/$TESTS"
|
||||
elif [ -n "$LIMIT" ]; then
|
||||
FILTER="$LIMIT"
|
||||
elif [ -n "$TESTS" ]; then
|
||||
FILTER="/$TESTS"
|
||||
else
|
||||
FILTER="/"
|
||||
fi
|
||||
echo "filter: $FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "amsterdam"
|
||||
|
||||
- name: Parse hive output
|
||||
run: |
|
||||
find hivetests/workspace/logs -type f -name "*.json" ! -name "hive.json" | xargs -I {} python .github/scripts/hive/parse.py {} --exclusion .github/scripts/hive/expected_failures.yaml --ignored .github/scripts/hive/ignored_tests.yaml
|
||||
|
||||
- name: Print simulator output
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
cat hivetests/workspace/logs/*simulator*.log
|
||||
|
||||
- name: Print reth client logs
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
cat hivetests/workspace/logs/reth/client-*.log
|
||||
|
||||
test-osaka:
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -178,7 +362,7 @@ jobs:
|
||||
needs:
|
||||
- build-reth
|
||||
- prepare-hive
|
||||
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
name: Hive-Osaka / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
@@ -191,7 +375,7 @@ jobs:
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hive_assets
|
||||
name: hive_assets_osaka
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
@@ -229,7 +413,7 @@ jobs:
|
||||
FILTER="/"
|
||||
fi
|
||||
echo "filter: $FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "osaka"
|
||||
|
||||
- name: Parse hive output
|
||||
run: |
|
||||
@@ -245,7 +429,9 @@ jobs:
|
||||
run: |
|
||||
cat hivetests/workspace/logs/reth/client-*.log
|
||||
notify-on-error:
|
||||
needs: test
|
||||
needs:
|
||||
- test-amsterdam
|
||||
- test-osaka
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -10442,6 +10442,8 @@ dependencies = [
|
||||
"proptest-arbitrary-interop",
|
||||
"rand 0.9.4",
|
||||
"rayon",
|
||||
"reth-chainspec",
|
||||
"reth-ethereum-primitives",
|
||||
"reth-execution-errors",
|
||||
"reth-metrics",
|
||||
"reth-primitives-traits",
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -33,8 +33,17 @@ ENV FEATURES=$FEATURES
|
||||
RUN cargo chef cook --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path recipe.json
|
||||
|
||||
# Build application
|
||||
# Platform-specific RUSTFLAGS: amd64 uses x86-64-v3 (Haswell+) with pclmulqdq for rocksdb
|
||||
#
|
||||
# TARGETPLATFORM is set by BuildKit: https://docs.docker.com/reference/dockerfile#automatic-platform-args-in-the-global-scope
|
||||
ARG TARGETPLATFORM
|
||||
COPY --exclude=dist . .
|
||||
RUN cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
|
||||
RUN if [ -n "$RUSTFLAGS" ]; then \
|
||||
export RUSTFLAGS="$RUSTFLAGS"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
|
||||
fi && \
|
||||
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
|
||||
|
||||
# ARG is not resolved in COPY so we have to hack around it by copying the
|
||||
# binary to a temporary location
|
||||
|
||||
@@ -39,7 +39,7 @@ Both `new-payload-fcu` and `new-payload-only` support `--rpc-block-fetch-retries
|
||||
to control how many times block fetches are retried after an RPC failure. The default is `10`.
|
||||
Use `--rpc-block-fetch-retries forever` to keep retrying indefinitely.
|
||||
|
||||
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
|
||||
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold. This can be customized with `--persistence-threshold <N>`.
|
||||
|
||||
By default, the WebSocket URL for persistence subscriptions is derived from `--engine-rpc-url` (converting to ws:// on port 8546). Use `--ws-rpc-url` to override this.
|
||||
|
||||
|
||||
@@ -67,9 +67,8 @@ pub struct Command {
|
||||
|
||||
/// Engine persistence threshold used for deciding when to wait for persistence.
|
||||
///
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
|
||||
/// matches the engine's `DEFAULT_PERSISTENCE_THRESHOLD` (2), so waits occur
|
||||
/// at blocks 3, 6, 9, etc.
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks.
|
||||
/// By default this matches the engine's `DEFAULT_PERSISTENCE_THRESHOLD`.
|
||||
#[arg(
|
||||
long = "persistence-threshold",
|
||||
value_name = "PERSISTENCE_THRESHOLD",
|
||||
|
||||
@@ -18,7 +18,7 @@ reth-errors.workspace = true
|
||||
reth-execution-types.workspace = true
|
||||
reth-metrics.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-primitives-traits = { workspace = true, features = ["dashmap"] }
|
||||
reth-storage-api.workspace = true
|
||||
reth-trie.workspace = true
|
||||
|
||||
|
||||
@@ -320,6 +320,19 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
/// This will update the links between blocks and remove all blocks that are [..
|
||||
/// `persisted_height`].
|
||||
pub fn remove_persisted_blocks(&self, persisted_num_hash: BlockNumHash) {
|
||||
self.remove_persisted_blocks_until(persisted_num_hash, persisted_num_hash.number);
|
||||
}
|
||||
|
||||
/// Removes blocks from the in-memory state through `remove_until` while still reporting the
|
||||
/// provided block as the persisted tip.
|
||||
///
|
||||
/// This is used when block bodies/plain state have been persisted further than trie data, so a
|
||||
/// suffix still needs to remain in memory for trie-backed operations.
|
||||
pub fn remove_persisted_blocks_until(
|
||||
&self,
|
||||
persisted_num_hash: BlockNumHash,
|
||||
remove_until: BlockNumber,
|
||||
) {
|
||||
self.set_persisted(persisted_num_hash);
|
||||
// if the persisted hash is not in the canonical in memory state, do nothing, because it
|
||||
// means canonical blocks were not actually persisted.
|
||||
@@ -337,16 +350,15 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
let mut numbers = self.inner.in_memory_state.numbers.write();
|
||||
let mut blocks = self.inner.in_memory_state.blocks.write();
|
||||
|
||||
let BlockNumHash { number: persisted_height, hash: _ } = persisted_num_hash;
|
||||
let remove_until = remove_until.min(persisted_num_hash.number);
|
||||
|
||||
// clear all numbers
|
||||
numbers.clear();
|
||||
|
||||
// drain all blocks and only keep the ones that are not persisted (below the persisted
|
||||
// height)
|
||||
// Drain all blocks and keep only the suffix that still has to stay in memory.
|
||||
let mut old_blocks = blocks
|
||||
.drain()
|
||||
.filter(|(_, b)| b.block_ref().recovered_block().number() > persisted_height)
|
||||
.filter(|(_, b)| b.block_ref().recovered_block().number() > remove_until)
|
||||
.map(|(_, b)| b.block.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -4,26 +4,32 @@
|
||||
//! lazily on first access. This allows execution to start before the trie overlay
|
||||
//! is fully computed.
|
||||
|
||||
use crate::DeferredTrieData;
|
||||
use crate::{EthPrimitives, ExecutedBlock};
|
||||
use alloy_primitives::B256;
|
||||
use reth_primitives_traits::{
|
||||
dashmap::{self, DashMap},
|
||||
AlloyBlockHeader, NodePrimitives,
|
||||
};
|
||||
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// Inputs captured for lazy overlay computation.
|
||||
#[derive(Clone)]
|
||||
struct LazyOverlayInputs {
|
||||
/// The persisted ancestor hash (anchor) this overlay should be built on.
|
||||
anchor_hash: B256,
|
||||
/// Deferred trie data handles for all in-memory blocks (newest to oldest).
|
||||
blocks: Vec<DeferredTrieData>,
|
||||
struct LazyOverlayInputs<N: NodePrimitives = EthPrimitives> {
|
||||
/// In-memory blocks from tip to anchor child.
|
||||
///
|
||||
/// Blocks must be provided in reverse chain order (newest to oldest).
|
||||
blocks: Vec<ExecutedBlock<N>>,
|
||||
}
|
||||
|
||||
/// Lazily computed trie overlay.
|
||||
///
|
||||
/// Captures the inputs needed to compute a [`TrieInputSorted`] and defers the actual
|
||||
/// computation until first access. This is conceptually similar to [`DeferredTrieData`]
|
||||
/// but for overlay computation.
|
||||
/// computation until first access.
|
||||
///
|
||||
/// Blocks must be provided in reverse chain order (newest to oldest), so the first block is the
|
||||
/// chain tip and the last block is the oldest in-memory block in the chain segment.
|
||||
///
|
||||
/// # Fast Path vs Slow Path
|
||||
///
|
||||
@@ -31,37 +37,41 @@ struct LazyOverlayInputs {
|
||||
/// matches our expected anchor, we can reuse it directly (O(1)).
|
||||
/// - **Slow path**: Otherwise, we merge all ancestor blocks' trie data into a new overlay.
|
||||
#[derive(Clone)]
|
||||
pub struct LazyOverlay {
|
||||
/// Computed result, cached after first access.
|
||||
inner: Arc<OnceLock<TrieInputSorted>>,
|
||||
pub struct LazyOverlay<N: NodePrimitives = EthPrimitives> {
|
||||
/// Computed results, cached by requested anchor hash.
|
||||
inner: Arc<DashMap<B256, Arc<TrieInputSorted>>>,
|
||||
/// Inputs for lazy computation.
|
||||
inputs: LazyOverlayInputs,
|
||||
inputs: LazyOverlayInputs<N>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LazyOverlay {
|
||||
impl<N: NodePrimitives> std::fmt::Debug for LazyOverlay<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LazyOverlay")
|
||||
.field("anchor_hash", &self.inputs.anchor_hash)
|
||||
.field(
|
||||
"oldest_block_parent_hash",
|
||||
&self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash()),
|
||||
)
|
||||
.field("num_blocks", &self.inputs.blocks.len())
|
||||
.field("computed", &self.inner.get().is_some())
|
||||
.field("cached_anchors", &self.inner.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl LazyOverlay {
|
||||
/// Create a new lazy overlay with the given anchor hash and block handles.
|
||||
impl<N: NodePrimitives> LazyOverlay<N> {
|
||||
/// Create a new lazy overlay from in-memory blocks.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `anchor_hash` - The persisted ancestor hash this overlay is built on top of
|
||||
/// * `blocks` - Deferred trie data handles for in-memory blocks (newest to oldest)
|
||||
pub fn new(anchor_hash: B256, blocks: Vec<DeferredTrieData>) -> Self {
|
||||
Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { anchor_hash, blocks } }
|
||||
}
|
||||
/// * `blocks` - Executed blocks in reverse chain order (newest to oldest)
|
||||
pub fn new(blocks: Vec<ExecutedBlock<N>>) -> Self {
|
||||
debug_assert!(
|
||||
blocks.windows(2).all(|window| {
|
||||
window[0].recovered_block().parent_hash() == window[1].recovered_block().hash()
|
||||
}),
|
||||
"LazyOverlay blocks must be ordered newest to oldest along a single chain"
|
||||
);
|
||||
|
||||
/// Returns the anchor hash this overlay is built on.
|
||||
pub const fn anchor_hash(&self) -> B256 {
|
||||
self.inputs.anchor_hash
|
||||
Self { inner: Default::default(), inputs: LazyOverlayInputs { blocks } }
|
||||
}
|
||||
|
||||
/// Returns the number of in-memory blocks this overlay covers.
|
||||
@@ -69,43 +79,75 @@ impl LazyOverlay {
|
||||
self.inputs.blocks.len()
|
||||
}
|
||||
|
||||
/// Returns true if the overlay has already been computed.
|
||||
pub fn is_computed(&self) -> bool {
|
||||
self.inner.get().is_some()
|
||||
/// Returns the oldest anchor hash this overlay can serve.
|
||||
///
|
||||
/// This is the parent hash of the oldest block in the stored newest-to-oldest chain segment.
|
||||
pub fn anchor_hash(&self) -> Option<B256> {
|
||||
self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash())
|
||||
}
|
||||
|
||||
/// Returns the computed trie input, computing it if necessary.
|
||||
/// Returns true if there are no blocks in the overlay, or if one of the blocks has the given
|
||||
/// hash as a parent hash.
|
||||
pub fn has_anchor_hash(&self, hash: B256) -> bool {
|
||||
self.inputs.blocks.is_empty() ||
|
||||
self.inputs.blocks.iter().any(|b| b.recovered_block().parent_hash() == hash)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Returns true if the overlay has already been computed for the requested anchor.
|
||||
pub fn is_computed(&self, anchor_hash: B256) -> bool {
|
||||
self.inner.contains_key(&anchor_hash)
|
||||
}
|
||||
|
||||
/// Returns the computed trie input for the requested anchor, computing it if necessary.
|
||||
///
|
||||
/// The first call triggers computation (which may block waiting for deferred data).
|
||||
/// Subsequent calls return the cached result immediately.
|
||||
pub fn get(&self) -> &TrieInputSorted {
|
||||
self.inner.get_or_init(|| self.compute())
|
||||
/// Subsequent calls for the same anchor return the cached result immediately.
|
||||
pub fn get(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
|
||||
match self.inner.entry(anchor_hash) {
|
||||
dashmap::Entry::Occupied(entry) => Arc::clone(entry.get()),
|
||||
dashmap::Entry::Vacant(entry) => {
|
||||
let input = self.compute(anchor_hash);
|
||||
entry.insert(Arc::clone(&input));
|
||||
input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the overlay as (nodes, state) tuple for use with `OverlayStateProviderFactory`.
|
||||
pub fn as_overlay(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
|
||||
let input = self.get();
|
||||
pub fn as_overlay(
|
||||
&self,
|
||||
anchor_hash: B256,
|
||||
) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
|
||||
let input = self.get(anchor_hash);
|
||||
(Arc::clone(&input.nodes), Arc::clone(&input.state))
|
||||
}
|
||||
|
||||
/// Compute the trie input overlay.
|
||||
fn compute(&self) -> TrieInputSorted {
|
||||
let anchor_hash = self.inputs.anchor_hash;
|
||||
fn compute(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
|
||||
let blocks = &self.inputs.blocks;
|
||||
|
||||
if blocks.is_empty() {
|
||||
debug!(target: "chain_state::lazy_overlay", "No in-memory blocks, returning empty overlay");
|
||||
return TrieInputSorted::default();
|
||||
return Default::default()
|
||||
}
|
||||
|
||||
let Some(last_index) =
|
||||
blocks.iter().position(|block| block.recovered_block().parent_hash() == anchor_hash)
|
||||
else {
|
||||
panic!(
|
||||
"LazyOverlay does not contain a block whose parent hash matches requested anchor {anchor_hash}"
|
||||
);
|
||||
};
|
||||
let blocks = &blocks[..=last_index];
|
||||
|
||||
// Fast path: Check if tip block's overlay is ready and anchor matches.
|
||||
// The tip block (first in list) has the cumulative overlay from all ancestors.
|
||||
// The tip block (first in list) has the cumulative overlay from all ancestors up to the
|
||||
// requested anchor.
|
||||
if let Some(tip) = blocks.first() {
|
||||
let data = tip.wait_cloned();
|
||||
let data = tip.trie_data();
|
||||
if let Some(anchored) = &data.anchored_trie_input {
|
||||
if anchored.anchor_hash == anchor_hash {
|
||||
trace!(target: "chain_state::lazy_overlay", %anchor_hash, "Reusing tip block's cached overlay (fast path)");
|
||||
return (*anchored.trie_input).clone();
|
||||
return Arc::clone(&anchored.trie_input);
|
||||
}
|
||||
debug!(
|
||||
target: "chain_state::lazy_overlay",
|
||||
@@ -116,23 +158,30 @@ impl LazyOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: Merge all blocks' trie data into a new overlay.
|
||||
debug!(target: "chain_state::lazy_overlay", num_blocks = blocks.len(), "Merging blocks (slow path)");
|
||||
Self::merge_blocks(blocks)
|
||||
// Slow path: Merge the prefix of blocks from the tip back to the requested anchor.
|
||||
debug!(
|
||||
target: "chain_state::lazy_overlay",
|
||||
%anchor_hash,
|
||||
num_blocks = blocks.len(),
|
||||
"Merging blocks (slow path)"
|
||||
);
|
||||
Arc::new(Self::merge_blocks(blocks))
|
||||
}
|
||||
|
||||
/// Merge all blocks' trie data into a single [`TrieInputSorted`].
|
||||
///
|
||||
/// Blocks are ordered newest to oldest.
|
||||
fn merge_blocks(blocks: &[DeferredTrieData]) -> TrieInputSorted {
|
||||
fn merge_blocks(blocks: &[ExecutedBlock<N>]) -> TrieInputSorted {
|
||||
if blocks.is_empty() {
|
||||
return TrieInputSorted::default();
|
||||
}
|
||||
|
||||
let state =
|
||||
HashedPostStateSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().hashed_state));
|
||||
let nodes =
|
||||
TrieUpdatesSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().trie_updates));
|
||||
let state = HashedPostStateSorted::merge_batch(
|
||||
blocks.iter().map(|block| block.trie_data().hashed_state),
|
||||
);
|
||||
let nodes = TrieUpdatesSorted::merge_batch(
|
||||
blocks.iter().map(|block| block.trie_data().trie_updates),
|
||||
);
|
||||
|
||||
TrieInputSorted { state, nodes, prefix_sets: Default::default() }
|
||||
}
|
||||
@@ -141,46 +190,138 @@ impl LazyOverlay {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_trie::{updates::TrieUpdates, HashedPostState};
|
||||
use crate::{test_utils::TestBlockBuilder, ComputedTrieData, EthPrimitives, ExecutedBlock};
|
||||
use alloy_primitives::U256;
|
||||
use reth_primitives_traits::Account;
|
||||
use reth_trie::{updates::TrieUpdatesSorted, HashedPostState, HashedStorage};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn empty_deferred(anchor: B256) -> DeferredTrieData {
|
||||
DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
Vec::new(),
|
||||
fn with_unique_state(
|
||||
block: &ExecutedBlock<EthPrimitives>,
|
||||
id: u8,
|
||||
) -> ExecutedBlock<EthPrimitives> {
|
||||
let hashed_address = B256::with_last_byte(id);
|
||||
let hashed_slot = B256::with_last_byte(id.saturating_add(32));
|
||||
let hashed_state = HashedPostState::default()
|
||||
.with_accounts([(hashed_address, Some(Account::default()))])
|
||||
.with_storages([(
|
||||
hashed_address,
|
||||
HashedStorage::from_iter(false, [(hashed_slot, U256::from(id))]),
|
||||
)])
|
||||
.into_sorted();
|
||||
|
||||
ExecutedBlock::new(
|
||||
Arc::clone(&block.recovered_block),
|
||||
Arc::clone(&block.execution_output),
|
||||
ComputedTrieData::without_trie_input(
|
||||
Arc::new(hashed_state),
|
||||
Arc::new(TrieUpdatesSorted::default()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_blocks_returns_default() {
|
||||
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
|
||||
let result = overlay.get();
|
||||
assert!(result.state.is_empty());
|
||||
assert!(result.nodes.is_empty());
|
||||
fn test_blocks() -> Vec<ExecutedBlock<EthPrimitives>> {
|
||||
TestBlockBuilder::eth()
|
||||
.get_executed_blocks(1..4)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(|(index, block)| with_unique_state(&block, index as u8 + 1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_block_uses_data_directly() {
|
||||
let anchor = B256::random();
|
||||
let deferred = empty_deferred(anchor);
|
||||
let overlay = LazyOverlay::new(anchor, vec![deferred]);
|
||||
let block = TestBlockBuilder::eth().get_executed_block_with_number(1, B256::random());
|
||||
let anchor_hash = block.recovered_block().parent_hash();
|
||||
let overlay = LazyOverlay::new(vec![block]);
|
||||
|
||||
assert!(!overlay.is_computed());
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
assert!(!overlay.is_computed(anchor_hash));
|
||||
let _ = overlay.get(anchor_hash);
|
||||
assert!(overlay.is_computed(anchor_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_after_first_access() {
|
||||
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
|
||||
fn caches_results_per_anchor() {
|
||||
let blocks = test_blocks();
|
||||
let prefix_anchor = blocks[2].recovered_block().hash();
|
||||
let full_anchor = blocks[2].recovered_block().parent_hash();
|
||||
let overlay = LazyOverlay::new(blocks);
|
||||
|
||||
// First access computes
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
let prefix = overlay.get(prefix_anchor);
|
||||
let full = overlay.get(full_anchor);
|
||||
|
||||
// Second access uses cache
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
assert!(overlay.is_computed(prefix_anchor));
|
||||
assert!(overlay.is_computed(full_anchor));
|
||||
assert!(!Arc::ptr_eq(&prefix, &full));
|
||||
assert!(Arc::ptr_eq(&prefix, &overlay.get(prefix_anchor)));
|
||||
assert!(Arc::ptr_eq(&full, &overlay.get(full_anchor)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requested_anchor_limits_the_merged_prefix() {
|
||||
let blocks = test_blocks();
|
||||
let prefix_anchor = blocks[2].recovered_block().hash();
|
||||
let expected = LazyOverlay::merge_blocks(&blocks[..2]);
|
||||
let overlay = LazyOverlay::new(blocks);
|
||||
let actual = overlay.get(prefix_anchor);
|
||||
|
||||
assert_eq!(actual.nodes.as_ref(), expected.nodes.as_ref());
|
||||
assert_eq!(actual.state.as_ref(), expected.state.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_hash_returns_oldest_served_anchor() {
|
||||
let blocks = test_blocks();
|
||||
let expected_anchor = blocks.last().unwrap().recovered_block().parent_hash();
|
||||
let overlay = LazyOverlay::new(blocks);
|
||||
|
||||
assert_eq!(overlay.anchor_hash(), Some(expected_anchor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reuses_tip_overlay_when_anchor_matches() {
|
||||
let mut blocks = test_blocks();
|
||||
let prefix_anchor = blocks[2].recovered_block().hash();
|
||||
let tip_overlay = Arc::new(LazyOverlay::merge_blocks(&blocks[..2]));
|
||||
let tip_data = blocks[0].trie_data();
|
||||
|
||||
blocks[0] = ExecutedBlock::new(
|
||||
Arc::clone(&blocks[0].recovered_block),
|
||||
Arc::clone(&blocks[0].execution_output),
|
||||
ComputedTrieData::with_trie_input(
|
||||
tip_data.hashed_state,
|
||||
tip_data.trie_updates,
|
||||
prefix_anchor,
|
||||
Arc::clone(&tip_overlay),
|
||||
),
|
||||
);
|
||||
|
||||
let overlay = LazyOverlay::new(blocks);
|
||||
let actual = overlay.get(prefix_anchor);
|
||||
|
||||
assert!(Arc::ptr_eq(&actual, &tip_overlay));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "LazyOverlay does not contain a block whose parent hash matches requested anchor"
|
||||
)]
|
||||
fn missing_anchor_panics() {
|
||||
let blocks = test_blocks();
|
||||
let missing_anchor = blocks[0].recovered_block().hash();
|
||||
let overlay = LazyOverlay::new(blocks);
|
||||
|
||||
let _ = overlay.get(missing_anchor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "LazyOverlay blocks must be ordered newest to oldest along a single chain"
|
||||
)]
|
||||
fn misordered_blocks_panic() {
|
||||
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
|
||||
let _ = LazyOverlay::new(blocks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! state), compacts MDBX, then runs the pipeline to rebuild them.
|
||||
|
||||
use crate::common::CliNodeTypes;
|
||||
use alloy_primitives::Address;
|
||||
use clap::Parser;
|
||||
use reth_db::{
|
||||
mdbx::{self, ffi},
|
||||
@@ -132,8 +133,12 @@ impl Command {
|
||||
.and_then(|cp| cp.block_number)
|
||||
.map_or(0, |b| b + 1);
|
||||
|
||||
let mut writer =
|
||||
sf_provider.get_writer(first_block, StaticFileSegment::AccountChangeSets)?;
|
||||
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
|
||||
// earlier than first_block (e.g. 2603897 from prune checkpoint).
|
||||
let mut writer = sf_provider.latest_writer(StaticFileSegment::AccountChangeSets)?;
|
||||
if first_block > 0 {
|
||||
writer.ensure_at_block(first_block - 1)?;
|
||||
}
|
||||
|
||||
let mut count = 0u64;
|
||||
let mut walker = cursor.walk(Some(first_block))?.peekable();
|
||||
@@ -174,11 +179,15 @@ impl Command {
|
||||
.and_then(|cp| cp.block_number)
|
||||
.map_or(0, |b| b + 1);
|
||||
|
||||
let mut writer =
|
||||
sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?;
|
||||
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
|
||||
// earlier than first_block (e.g. 2603897 from prune checkpoint).
|
||||
let mut writer = sf_provider.latest_writer(StaticFileSegment::StorageChangeSets)?;
|
||||
if first_block > 0 {
|
||||
writer.ensure_at_block(first_block - 1)?;
|
||||
}
|
||||
|
||||
let mut count = 0u64;
|
||||
let mut walker = cursor.walk(Some(Default::default()))?.peekable();
|
||||
let mut walker = cursor.walk(Some((first_block, Address::ZERO).into()))?.peekable();
|
||||
|
||||
for block in first_block..=tip {
|
||||
let mut entries = Vec::new();
|
||||
@@ -238,6 +247,18 @@ impl Command {
|
||||
.map_or(0, |b| b + 1);
|
||||
let first_block = prune_start.max(existing.map_or(0, |b| b + 1));
|
||||
|
||||
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
|
||||
// earlier than first_block (e.g. 2603897 from prune checkpoint).
|
||||
if first_block > 0 {
|
||||
let mut writer = sf_provider.latest_writer(StaticFileSegment::Receipts)?;
|
||||
writer.ensure_at_block(first_block - 1)?;
|
||||
writer.commit()?;
|
||||
}
|
||||
|
||||
let before = sf_provider
|
||||
.get_highest_static_file_tx(StaticFileSegment::Receipts)
|
||||
.map_or(0, |tx| tx + 1);
|
||||
|
||||
let block_range = first_block..=tip;
|
||||
|
||||
let segment = reth_static_file::segments::Receipts;
|
||||
@@ -245,7 +266,11 @@ impl Command {
|
||||
|
||||
sf_provider.commit()?;
|
||||
|
||||
info!(target: "reth::cli", "Receipts migrated");
|
||||
let after = sf_provider
|
||||
.get_highest_static_file_tx(StaticFileSegment::Receipts)
|
||||
.map_or(0, |tx| tx + 1);
|
||||
let count = after - before;
|
||||
info!(target: "reth::cli", count, "Receipts migrated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
|
||||
PayloadAttributes::default()
|
||||
})
|
||||
.with_tree_config_modifier(|config| {
|
||||
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)
|
||||
config.with_persistence_threshold(6).with_memory_block_buffer_target(5)
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -189,7 +189,7 @@ async fn test_rocksdb_transaction_queries() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -200,7 +200,7 @@ async fn test_rocksdb_transaction_queries() -> Result<()> {
|
||||
let signer = wallets[0].clone();
|
||||
let client = nodes[0].rpc_client().expect("RPC client should be available");
|
||||
|
||||
let raw_tx = TransactionTestContext::transfer_tx_bytes(chain_id, signer).await;
|
||||
let raw_tx = TransactionTestContext::transfer_tx_bytes(chain_id, signer.clone()).await;
|
||||
let tx_hash = nodes[0].rpc.inject_tx(raw_tx).await?;
|
||||
|
||||
// Wait for tx to enter pending pool before mining
|
||||
@@ -209,6 +209,14 @@ async fn test_rocksdb_transaction_queries() -> Result<()> {
|
||||
let payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload.block().number(), 1);
|
||||
|
||||
let flush_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 1).await;
|
||||
let flush_tx_hash = nodes[0].rpc.inject_tx(flush_tx).await?;
|
||||
wait_for_pending_tx(&client, flush_tx_hash).await;
|
||||
|
||||
let flush_payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(flush_payload.block().number(), 2);
|
||||
|
||||
// Query each transaction by hash
|
||||
let tx: Option<Transaction> = client.request("eth_getTransactionByHash", [tx_hash]).await?;
|
||||
let tx = tx.expect("Transaction should be found");
|
||||
@@ -256,7 +264,7 @@ async fn test_rocksdb_multi_tx_same_block() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -283,6 +291,14 @@ async fn test_rocksdb_multi_tx_same_block() -> Result<()> {
|
||||
let payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload.block().number(), 1);
|
||||
|
||||
let flush_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 3).await;
|
||||
let flush_tx_hash = nodes[0].rpc.inject_tx(flush_tx).await?;
|
||||
wait_for_pending_tx(&client, flush_tx_hash).await;
|
||||
|
||||
let flush_payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(flush_payload.block().number(), 2);
|
||||
|
||||
// Verify block contains all 3 txs
|
||||
let block: Option<alloy_rpc_types_eth::Block> =
|
||||
client.request("eth_getBlockByNumber", ("0x1", true)).await?;
|
||||
@@ -324,7 +340,7 @@ async fn test_rocksdb_txs_across_blocks() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -409,7 +425,7 @@ async fn test_rocksdb_pending_tx_not_in_storage() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -417,7 +433,7 @@ async fn test_rocksdb_pending_tx_not_in_storage() -> Result<()> {
|
||||
let signer = wallets[0].clone();
|
||||
|
||||
// Inject tx but do NOT mine
|
||||
let raw_tx = TransactionTestContext::transfer_tx_bytes(chain_id, signer).await;
|
||||
let raw_tx = TransactionTestContext::transfer_tx_bytes(chain_id, signer.clone()).await;
|
||||
let tx_hash = nodes[0].rpc.inject_tx(raw_tx).await?;
|
||||
|
||||
// Verify tx is in pending pool via RPC
|
||||
@@ -442,6 +458,14 @@ async fn test_rocksdb_pending_tx_not_in_storage() -> Result<()> {
|
||||
let payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload.block().number(), 1);
|
||||
|
||||
let flush_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 1).await;
|
||||
let flush_tx_hash = nodes[0].rpc.inject_tx(flush_tx).await?;
|
||||
wait_for_pending_tx(&client, flush_tx_hash).await;
|
||||
|
||||
let flush_payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(flush_payload.block().number(), 2);
|
||||
|
||||
// Poll until tx appears in RocksDB
|
||||
let tx_number = poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash).await;
|
||||
assert_eq!(tx_number, 0, "First tx should have tx_number 0");
|
||||
@@ -473,7 +497,7 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -495,10 +519,6 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
let block1_hash = payload1.block().hash();
|
||||
assert_eq!(payload1.block().number(), 1);
|
||||
|
||||
// Poll until tx1 appears in RocksDB (ensures persistence happened)
|
||||
let tx_number1 = poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash1).await;
|
||||
assert_eq!(tx_number1, 0, "First tx should have tx_number 0");
|
||||
|
||||
// Mine block 2 with transaction from signer1 (nonce 1)
|
||||
let raw_tx2 =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer1.clone(), 1).await;
|
||||
@@ -508,6 +528,10 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
let payload2 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload2.block().number(), 2);
|
||||
|
||||
// The second block triggers the first persistence cycle, which flushes both block 1 and 2.
|
||||
let tx_number1 = poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash1).await;
|
||||
assert_eq!(tx_number1, 0, "First tx should have tx_number 0");
|
||||
|
||||
// Poll until tx2 appears in RocksDB
|
||||
let tx_number2 = poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash2).await;
|
||||
assert_eq!(tx_number2, 1, "Second tx should have tx_number 1");
|
||||
@@ -521,6 +545,14 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
let payload3 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload3.block().number(), 3);
|
||||
|
||||
let flush_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer1.clone(), 3).await;
|
||||
let flush_tx_hash = nodes[0].rpc.inject_tx(flush_tx).await?;
|
||||
wait_for_pending_tx(&client, flush_tx_hash).await;
|
||||
|
||||
let flush_payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(flush_payload.block().number(), 4);
|
||||
|
||||
// Poll until tx3 appears in RocksDB
|
||||
let tx_number3 = poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash3).await;
|
||||
assert_eq!(tx_number3, 2, "Third tx should have tx_number 2");
|
||||
@@ -532,7 +564,7 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
let alt_tx_hash = nodes[0].rpc.inject_tx(raw_alt_tx).await?;
|
||||
wait_for_pending_tx(&client, alt_tx_hash).await;
|
||||
|
||||
// Build an alternate payload (this builds on top of the current head, i.e., block 3)
|
||||
// Build an alternate payload on top of the current flushed head.
|
||||
// But we want to reorg back to block 1, so we'll use the payload and then FCU to it
|
||||
let alt_payload = nodes[0].new_payload().await?;
|
||||
let alt_block_hash = nodes[0].submit_payload(alt_payload.clone()).await?;
|
||||
@@ -550,8 +582,8 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
let latest: Option<alloy_rpc_types_eth::Block> =
|
||||
client.request("eth_getBlockByNumber", ("latest", false)).await?;
|
||||
let latest = latest.expect("Latest block should exist");
|
||||
// The alt block is at height 4 (on top of block 3)
|
||||
assert!(latest.header.number >= 3, "Should be at height >= 3 after operation");
|
||||
// The alt block is built on top of the flushed canonical head.
|
||||
assert!(latest.header.number >= 4, "Should be at height >= 4 after operation");
|
||||
|
||||
// tx1 from block 1 should still be there
|
||||
let tx1: Option<Transaction> = client.request("eth_getTransactionByHash", [tx_hash1]).await?;
|
||||
@@ -596,7 +628,7 @@ async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -621,8 +653,6 @@ async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
|
||||
let payload1 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload1.block().number(), 1);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash1).await;
|
||||
|
||||
// Record state after block 1
|
||||
let balance_at_1: U256 = client.request("eth_getBalance", (sender, "0x1")).await?;
|
||||
let nonce_at_1: U256 = client.request("eth_getTransactionCount", (sender, "0x1")).await?;
|
||||
@@ -637,8 +667,6 @@ async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
|
||||
let payload2 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload2.block().number(), 2);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash2).await;
|
||||
|
||||
let balance_at_2: U256 = client.request("eth_getBalance", (sender, "0x2")).await?;
|
||||
let nonce_at_2: U256 = client.request("eth_getTransactionCount", (sender, "0x2")).await?;
|
||||
assert!(balance_at_2 < balance_at_1, "Balance should decrease further after second tx");
|
||||
@@ -652,18 +680,14 @@ async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
|
||||
let payload3 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload3.block().number(), 3);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash3).await;
|
||||
|
||||
let balance_at_3: U256 = client.request("eth_getBalance", (sender, "0x3")).await?;
|
||||
let nonce_at_3: U256 = client.request("eth_getTransactionCount", (sender, "0x3")).await?;
|
||||
assert!(balance_at_3 < balance_at_2, "Balance should decrease further after third tx");
|
||||
assert_eq!(nonce_at_3, U256::from(3), "Nonce should be 3 after third tx");
|
||||
|
||||
// Mine additional blocks to push blocks 1-3 out of the in-memory overlay.
|
||||
// With persistence_threshold=0 and memory_block_buffer_target=0, each new block
|
||||
// triggers persistence up to `head` followed by in-memory eviction. Mining several
|
||||
// more blocks ensures the engine loop has completed at least one full
|
||||
// persist-then-evict cycle covering blocks 1-3.
|
||||
// With a persistence threshold of 1, every second block triggers a flush, so a few extra
|
||||
// blocks are enough to durably persist and evict the earlier history we want to query.
|
||||
// Each block needs a transaction because the payload builder requires non-empty payloads.
|
||||
for nonce in 3..8u64 {
|
||||
let raw_tx =
|
||||
@@ -673,6 +697,7 @@ async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
wait_for_pending_tx(&client, tx_hash).await;
|
||||
nodes[0].advance_block().await?;
|
||||
}
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash3).await;
|
||||
// Allow the engine loop to process the persistence completions
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
@@ -743,7 +768,7 @@ async fn test_rocksdb_account_history_pruning() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.with_node_config_modifier(|mut config| {
|
||||
config.pruning.account_history_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.minimum_distance = Some(PRUNE_DISTANCE);
|
||||
@@ -840,7 +865,7 @@ async fn test_rocksdb_storage_history_pruning() -> Result<()> {
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(1))
|
||||
.with_node_config_modifier(|mut config| {
|
||||
config.pruning.storage_history_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.minimum_distance = Some(PRUNE_DISTANCE);
|
||||
@@ -912,10 +937,6 @@ async fn test_rocksdb_storage_history_pruning() -> Result<()> {
|
||||
|
||||
let payload1 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload1.block().number(), 1);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, deploy_hash).await;
|
||||
|
||||
// Let the persistence cycle complete before the next block (same cadence as the loop below)
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Get the deployed contract address from the receipt
|
||||
let receipt: Option<TransactionReceipt> =
|
||||
@@ -965,6 +986,10 @@ async fn test_rocksdb_storage_history_pruning() -> Result<()> {
|
||||
assert_eq!(payload.block().number(), block_num);
|
||||
last_tx_hash = tx_hash;
|
||||
|
||||
if nonce == 1 {
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, deploy_hash).await;
|
||||
}
|
||||
|
||||
// Let the persistence cycle complete before the next block
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ auto_impl.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-primitives = { workspace = true, features = ["getrandom"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
trie-debug = []
|
||||
|
||||
@@ -6,12 +6,33 @@ use core::time::Duration;
|
||||
/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
|
||||
pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
|
||||
|
||||
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
|
||||
pub const DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD: u64 = 16;
|
||||
/// Maximum number of consecutive canonical blocks whose non-trie outputs may be persisted ahead
|
||||
/// of trie persistence.
|
||||
pub const DEFAULT_DEFERRED_TRIE_BLOCKS: u64 = 0;
|
||||
|
||||
/// How close to the canonical head we persist blocks.
|
||||
pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
|
||||
|
||||
/// Derives the default canonical-minus-persisted gap that triggers backpressure.
|
||||
pub const fn default_persistence_backpressure_threshold(
|
||||
persistence_threshold: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
) -> u64 {
|
||||
let threshold = 2 * (persistence_threshold + memory_block_buffer_target);
|
||||
if threshold < 16 {
|
||||
16
|
||||
} else {
|
||||
threshold
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
|
||||
pub const DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD: u64 =
|
||||
default_persistence_backpressure_threshold(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
);
|
||||
|
||||
/// The size of proof targets chunk to spawn in one multiproof calculation.
|
||||
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 5;
|
||||
|
||||
@@ -60,6 +81,17 @@ const fn assert_backpressure_threshold_invariant(
|
||||
);
|
||||
}
|
||||
|
||||
const fn assert_state_masking_invariant(
|
||||
persistence_threshold: u64,
|
||||
num_state_masking_blocks: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
) {
|
||||
debug_assert!(
|
||||
num_state_masking_blocks + memory_block_buffer_target < persistence_threshold,
|
||||
"num_state_masking_blocks + memory_block_buffer_target must be less than persistence_threshold",
|
||||
);
|
||||
}
|
||||
|
||||
const fn default_cross_block_cache_size() -> usize {
|
||||
if cfg!(test) {
|
||||
1024 * 1024 // 1 MB in tests
|
||||
@@ -93,6 +125,9 @@ pub struct TreeConfig {
|
||||
/// Maximum number of blocks to be kept only in memory without triggering
|
||||
/// persistence.
|
||||
persistence_threshold: u64,
|
||||
/// Number of persisted blocks whose state/trie writes are masked instead of being durably
|
||||
/// written in the current cycle.
|
||||
num_state_masking_blocks: u64,
|
||||
/// How close to the canonical head we persist blocks. Represents the ideal
|
||||
/// number of most recent blocks to keep in memory for quick access and reorgs.
|
||||
///
|
||||
@@ -204,14 +239,24 @@ pub struct TreeConfig {
|
||||
|
||||
impl Default for TreeConfig {
|
||||
fn default() -> Self {
|
||||
let persistence_backpressure_threshold = default_persistence_backpressure_threshold(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
);
|
||||
assert_backpressure_threshold_invariant(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
persistence_backpressure_threshold,
|
||||
);
|
||||
assert_state_masking_invariant(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_DEFERRED_TRIE_BLOCKS,
|
||||
DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
);
|
||||
Self {
|
||||
persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
num_state_masking_blocks: DEFAULT_DEFERRED_TRIE_BLOCKS,
|
||||
memory_block_buffer_target: DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
persistence_backpressure_threshold: DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
persistence_backpressure_threshold,
|
||||
block_buffer_limit: DEFAULT_BLOCK_BUFFER_LIMIT,
|
||||
max_invalid_header_cache_length: DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH,
|
||||
invalid_header_hit_eviction_threshold: DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD,
|
||||
@@ -253,6 +298,7 @@ impl TreeConfig {
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub const fn new(
|
||||
persistence_threshold: u64,
|
||||
num_state_masking_blocks: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
persistence_backpressure_threshold: u64,
|
||||
block_buffer_limit: u32,
|
||||
@@ -285,8 +331,14 @@ impl TreeConfig {
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
);
|
||||
assert_state_masking_invariant(
|
||||
persistence_threshold,
|
||||
num_state_masking_blocks,
|
||||
memory_block_buffer_target,
|
||||
);
|
||||
Self {
|
||||
persistence_threshold,
|
||||
num_state_masking_blocks,
|
||||
memory_block_buffer_target,
|
||||
persistence_backpressure_threshold,
|
||||
block_buffer_limit,
|
||||
@@ -329,6 +381,11 @@ impl TreeConfig {
|
||||
self.persistence_threshold
|
||||
}
|
||||
|
||||
/// Return the number of persisted blocks whose state/trie writes are masked.
|
||||
pub const fn num_state_masking_blocks(&self) -> u64 {
|
||||
self.num_state_masking_blocks
|
||||
}
|
||||
|
||||
/// Return the memory block buffer target.
|
||||
pub const fn memory_block_buffer_target(&self) -> u64 {
|
||||
self.memory_block_buffer_target
|
||||
@@ -447,6 +504,22 @@ impl TreeConfig {
|
||||
self.persistence_threshold,
|
||||
self.persistence_backpressure_threshold,
|
||||
);
|
||||
assert_state_masking_invariant(
|
||||
self.persistence_threshold,
|
||||
self.num_state_masking_blocks,
|
||||
self.memory_block_buffer_target,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for the number of persisted blocks whose state/trie writes are masked.
|
||||
pub const fn with_num_state_masking_blocks(mut self, num_state_masking_blocks: u64) -> Self {
|
||||
self.num_state_masking_blocks = num_state_masking_blocks;
|
||||
assert_state_masking_invariant(
|
||||
self.persistence_threshold,
|
||||
self.num_state_masking_blocks,
|
||||
self.memory_block_buffer_target,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -456,6 +529,11 @@ impl TreeConfig {
|
||||
memory_block_buffer_target: u64,
|
||||
) -> Self {
|
||||
self.memory_block_buffer_target = memory_block_buffer_target;
|
||||
assert_state_masking_invariant(
|
||||
self.persistence_threshold,
|
||||
self.num_state_masking_blocks,
|
||||
self.memory_block_buffer_target,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -765,7 +843,26 @@ impl TreeConfig {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TreeConfig;
|
||||
use super::{
|
||||
default_persistence_backpressure_threshold, TreeConfig, DEFAULT_DEFERRED_TRIE_BLOCKS,
|
||||
DEFAULT_MEMORY_BLOCK_BUFFER_TARGET, DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn default_thresholds_use_derived_backpressure_threshold() {
|
||||
let config = TreeConfig::default();
|
||||
|
||||
assert_eq!(config.persistence_threshold(), DEFAULT_PERSISTENCE_THRESHOLD);
|
||||
assert_eq!(config.num_state_masking_blocks(), DEFAULT_DEFERRED_TRIE_BLOCKS);
|
||||
assert_eq!(config.memory_block_buffer_target(), DEFAULT_MEMORY_BLOCK_BUFFER_TARGET);
|
||||
assert_eq!(
|
||||
config.persistence_backpressure_threshold(),
|
||||
default_persistence_backpressure_threshold(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
@@ -776,4 +873,15 @@ mod tests {
|
||||
.with_persistence_threshold(4)
|
||||
.with_persistence_backpressure_threshold(4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "num_state_masking_blocks + memory_block_buffer_target must be less than persistence_threshold"
|
||||
)]
|
||||
fn rejects_state_masking_window_at_or_above_persistence_threshold() {
|
||||
let _ = TreeConfig::default()
|
||||
.with_persistence_threshold(4)
|
||||
.with_num_state_masking_blocks(2)
|
||||
.with_memory_block_buffer_target(2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::metrics::PersistenceMetrics;
|
||||
use alloy_eips::BlockNumHash;
|
||||
use crossbeam_channel::Sender as CrossbeamSender;
|
||||
use reth_chain_state::ExecutedBlock;
|
||||
use reth_errors::ProviderError;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
|
||||
use reth_provider::{
|
||||
providers::ProviderNodeTypes, BlockExecutionWriter, BlockHashReader, ChainStateBlockWriter,
|
||||
DBProvider, DatabaseProviderFactory, ProviderFactory, SaveBlocksMode,
|
||||
DBProvider, DatabaseProviderFactory, ProviderFactory, SaveBlocksMode, SaveBlocksPlan,
|
||||
StageCheckpointReader,
|
||||
};
|
||||
use reth_prune::{PrunerError, PrunerWithFactory};
|
||||
use reth_stages_api::{MetricEvent, MetricEventsSender};
|
||||
use reth_stages_api::{MetricEvent, MetricEventsSender, StageId};
|
||||
use reth_tasks::spawn_os_thread;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -26,8 +26,13 @@ use tracing::{debug, error, instrument};
|
||||
/// Unified result of any persistence operation.
|
||||
#[derive(Debug)]
|
||||
pub struct PersistenceResult {
|
||||
/// The last block that was persisted, if any.
|
||||
/// The highest block whose non-state/trie outputs are persisted, if any.
|
||||
pub last_block: Option<BlockNumHash>,
|
||||
/// The highest block whose state/trie data is fully persisted, if known.
|
||||
///
|
||||
/// When this lags behind [`Self::last_block`], callers must retain the suffix
|
||||
/// above it in memory so trie-backed operations can still unwind from that point.
|
||||
pub last_state_trie_block: Option<u64>,
|
||||
/// The commit duration, only available for save-blocks operations.
|
||||
pub commit_duration: Option<Duration>,
|
||||
}
|
||||
@@ -96,14 +101,14 @@ where
|
||||
while let Ok(action) = self.incoming.recv() {
|
||||
match action {
|
||||
PersistenceAction::RemoveBlocksAbove(new_tip_num, sender) => {
|
||||
let last_block = self.on_remove_blocks_above(new_tip_num)?;
|
||||
let result = self.on_remove_blocks_above(new_tip_num)?;
|
||||
// send new sync metrics based on removed blocks
|
||||
let _ =
|
||||
self.sync_metrics_tx.send(MetricEvent::SyncHeight { height: new_tip_num });
|
||||
let _ = sender.send(PersistenceResult { last_block, commit_duration: None });
|
||||
let _ = sender.send(result);
|
||||
}
|
||||
PersistenceAction::SaveBlocks(blocks, sender) => {
|
||||
let result = self.on_save_blocks(blocks)?;
|
||||
PersistenceAction::SaveBlocks(plan, sender) => {
|
||||
let result = self.on_save_blocks(plan)?;
|
||||
let result_number = result.last_block.map(|b| b.number);
|
||||
|
||||
let _ = sender.send(result);
|
||||
@@ -130,28 +135,40 @@ where
|
||||
fn on_remove_blocks_above(
|
||||
&self,
|
||||
new_tip_num: u64,
|
||||
) -> Result<Option<BlockNumHash>, PersistenceError> {
|
||||
) -> Result<PersistenceResult, PersistenceError> {
|
||||
debug!(target: "engine::persistence", ?new_tip_num, "Removing blocks");
|
||||
let start_time = Instant::now();
|
||||
let provider_rw = self.provider.database_provider_rw()?;
|
||||
|
||||
let new_tip_hash = provider_rw.block_hash(new_tip_num)?;
|
||||
provider_rw.remove_block_and_execution_above(new_tip_num)?;
|
||||
let last_state_trie_block =
|
||||
provider_rw.get_stage_checkpoint(StageId::Finish)?.map(|checkpoint| {
|
||||
checkpoint
|
||||
.finish_stage_checkpoint()
|
||||
.and_then(|finish| finish.partial_state_trie)
|
||||
.unwrap_or(checkpoint.block_number)
|
||||
});
|
||||
provider_rw.commit()?;
|
||||
|
||||
debug!(target: "engine::persistence", ?new_tip_num, ?new_tip_hash, "Removed blocks from disk");
|
||||
self.metrics.remove_blocks_above_duration_seconds.record(start_time.elapsed());
|
||||
Ok(new_tip_hash.map(|hash| BlockNumHash { hash, number: new_tip_num }))
|
||||
Ok(PersistenceResult {
|
||||
last_block: new_tip_hash.map(|hash| BlockNumHash { hash, number: new_tip_num }),
|
||||
last_state_trie_block,
|
||||
commit_duration: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(block_count = blocks.len()))]
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(block_count = plan.blocks.len()))]
|
||||
fn on_save_blocks(
|
||||
&mut self,
|
||||
blocks: Vec<ExecutedBlock<N::Primitives>>,
|
||||
plan: SaveBlocksPlan<N::Primitives>,
|
||||
) -> Result<PersistenceResult, PersistenceError> {
|
||||
let first_block = blocks.first().map(|b| b.recovered_block.num_hash());
|
||||
let last_block = blocks.last().map(|b| b.recovered_block.num_hash());
|
||||
let block_count = blocks.len();
|
||||
let first_block = plan.blocks.first().map(|block| block.recovered_block().num_hash());
|
||||
let last_block = plan.last_block();
|
||||
let block_count = plan.blocks.len();
|
||||
let mut last_state_trie_block = None;
|
||||
|
||||
let pending_finalized = self.pending_finalized_block.take();
|
||||
let pending_safe = self.pending_safe_block.take();
|
||||
@@ -160,19 +177,27 @@ where
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
if let Some(last) = last_block {
|
||||
if let Some(last_block) = last_block {
|
||||
let provider_rw = self.provider.database_provider_rw()?;
|
||||
provider_rw.save_blocks(blocks, SaveBlocksMode::Full)?;
|
||||
provider_rw.save_blocks(&plan, SaveBlocksMode::Full)?;
|
||||
last_state_trie_block = provider_rw
|
||||
.get_stage_checkpoint(StageId::Finish)?
|
||||
.and_then(|checkpoint| {
|
||||
checkpoint
|
||||
.finish_stage_checkpoint()
|
||||
.and_then(|finish| finish.partial_state_trie)
|
||||
})
|
||||
.or(Some(last_block.number));
|
||||
|
||||
if let Some(finalized) = pending_finalized {
|
||||
provider_rw.save_finalized_block_number(finalized.min(last.number))?;
|
||||
if finalized > last.number {
|
||||
provider_rw.save_finalized_block_number(finalized.min(last_block.number))?;
|
||||
if finalized > last_block.number {
|
||||
self.pending_finalized_block = Some(finalized);
|
||||
}
|
||||
}
|
||||
if let Some(safe) = pending_safe {
|
||||
provider_rw.save_safe_block_number(safe.min(last.number))?;
|
||||
if safe > last.number {
|
||||
provider_rw.save_safe_block_number(safe.min(last_block.number))?;
|
||||
if safe > last_block.number {
|
||||
self.pending_safe_block = Some(safe);
|
||||
}
|
||||
}
|
||||
@@ -185,13 +210,13 @@ where
|
||||
//
|
||||
// The pruner reads the indices from rocksdb, filters it, and writes to indices, so it
|
||||
// must be able to read anything written by save_blocks.
|
||||
if self.pruner.is_pruning_needed(last.number) {
|
||||
debug!(target: "engine::persistence", block_num=?last.number, "Running pruner");
|
||||
if self.pruner.is_pruning_needed(last_block.number) {
|
||||
debug!(target: "engine::persistence", block_num=?last_block.number, "Running pruner");
|
||||
let prune_start = Instant::now();
|
||||
let provider_rw = self.provider.database_provider_rw()?;
|
||||
let _ = self.pruner.run_with_provider(&provider_rw, last.number)?;
|
||||
let _ = self.pruner.run_with_provider(&provider_rw, last_block.number)?;
|
||||
provider_rw.commit()?;
|
||||
debug!(target: "engine::persistence", tip=?last.number, "Finished pruning after saving blocks");
|
||||
debug!(target: "engine::persistence", tip=?last_block.number, "Finished pruning after saving blocks");
|
||||
self.metrics.prune_before_duration_seconds.record(prune_start.elapsed());
|
||||
}
|
||||
}
|
||||
@@ -200,7 +225,7 @@ where
|
||||
self.metrics.save_blocks_batch_size.record(block_count as f64);
|
||||
self.metrics.save_blocks_duration_seconds.record(elapsed);
|
||||
|
||||
Ok(PersistenceResult { last_block, commit_duration: Some(elapsed) })
|
||||
Ok(PersistenceResult { last_block, last_state_trie_block, commit_duration: Some(elapsed) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,9 +247,10 @@ pub enum PersistenceAction<N: NodePrimitives = EthPrimitives> {
|
||||
/// The section of tree state that should be persisted. These blocks are expected in order of
|
||||
/// increasing block number.
|
||||
///
|
||||
/// First, header, transaction, and receipt-related data should be written to static files.
|
||||
/// Then the execution history-related data will be written to the database.
|
||||
SaveBlocks(Vec<ExecutedBlock<N>>, CrossbeamSender<PersistenceResult>),
|
||||
/// First, header, transaction, and receipt-related data should be written to static files for
|
||||
/// the deferred trie region. Then the execution history-related data will be written to the
|
||||
/// database, while trie catchup is persisted for the prefix.
|
||||
SaveBlocks(SaveBlocksPlan<N>, CrossbeamSender<PersistenceResult>),
|
||||
|
||||
/// Removes block data above the given block number from the database.
|
||||
///
|
||||
@@ -308,10 +334,10 @@ impl<T: NodePrimitives> PersistenceHandle<T> {
|
||||
/// If there are no blocks to persist, then `None` is sent in the sender.
|
||||
pub fn save_blocks(
|
||||
&self,
|
||||
blocks: Vec<ExecutedBlock<T>>,
|
||||
plan: SaveBlocksPlan<T>,
|
||||
tx: CrossbeamSender<PersistenceResult>,
|
||||
) -> Result<(), SendError<PersistenceAction<T>>> {
|
||||
self.send_action(PersistenceAction::SaveBlocks(blocks, tx))
|
||||
self.send_action(PersistenceAction::SaveBlocks(plan, tx))
|
||||
}
|
||||
|
||||
/// Queues the finalized block number to be persisted on disk.
|
||||
@@ -375,12 +401,12 @@ impl Drop for ServiceGuard {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{B256, U256};
|
||||
use reth_chain_state::test_utils::TestBlockBuilder;
|
||||
use reth_chain_state::{test_utils::TestBlockBuilder, ExecutedBlock};
|
||||
use reth_exex_types::FinishedExExHeight;
|
||||
use reth_provider::{
|
||||
providers::{ProviderFactoryBuilder, ReadOnlyConfig},
|
||||
test_utils::{create_test_provider_factory, MockNodeTypes},
|
||||
AccountReader, ChainSpecProvider, HeaderProvider, StorageSettingsCache,
|
||||
AccountReader, ChainSpecProvider, HeaderProvider, SaveBlocksPlanStep, StorageSettingsCache,
|
||||
TryIntoHistoricalStateProvider,
|
||||
};
|
||||
use reth_prune::Pruner;
|
||||
@@ -389,6 +415,13 @@ mod tests {
|
||||
fn default_persistence_handle() -> PersistenceHandle<EthPrimitives> {
|
||||
let provider = create_test_provider_factory();
|
||||
|
||||
persistence_handle(provider)
|
||||
}
|
||||
|
||||
fn persistence_handle<N>(provider: ProviderFactory<N>) -> PersistenceHandle<EthPrimitives>
|
||||
where
|
||||
N: ProviderNodeTypes<Primitives = EthPrimitives>,
|
||||
{
|
||||
let (_finished_exex_height_tx, finished_exex_height_rx) =
|
||||
tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
|
||||
|
||||
@@ -399,18 +432,31 @@ mod tests {
|
||||
PersistenceHandle::<EthPrimitives>::spawn_service(provider, pruner, sync_metrics_tx)
|
||||
}
|
||||
|
||||
fn full_save_plan(blocks: Vec<ExecutedBlock<EthPrimitives>>) -> SaveBlocksPlan<EthPrimitives> {
|
||||
let full_range = 0..blocks.len();
|
||||
SaveBlocksPlan::new(
|
||||
blocks,
|
||||
vec![SaveBlocksPlanStep::new(
|
||||
full_range.clone(),
|
||||
Some(full_range.end..full_range.end),
|
||||
true,
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_blocks_empty() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let handle = default_persistence_handle();
|
||||
|
||||
let blocks = vec![];
|
||||
let blocks = full_save_plan(vec![]);
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
|
||||
let result = rx.recv().unwrap();
|
||||
assert!(result.last_block.is_none());
|
||||
assert!(result.last_state_trie_block.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -423,14 +469,16 @@ mod tests {
|
||||
test_block_builder.get_executed_block_with_number(block_number, B256::random());
|
||||
let block_hash = executed.recovered_block().hash();
|
||||
|
||||
let blocks = vec![executed];
|
||||
let blocks = full_save_plan(vec![executed]);
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
|
||||
let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("test timed out");
|
||||
|
||||
assert_eq!(block_hash, result.last_block.unwrap().hash);
|
||||
let last_block = result.last_block.unwrap();
|
||||
assert_eq!(block_hash, last_block.hash);
|
||||
assert_eq!(result.last_state_trie_block, Some(last_block.number));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -443,9 +491,11 @@ mod tests {
|
||||
let last_hash = blocks.last().unwrap().recovered_block().hash();
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
handle.save_blocks(full_save_plan(blocks), tx).unwrap();
|
||||
let result = rx.recv().unwrap();
|
||||
assert_eq!(last_hash, result.last_block.unwrap().hash);
|
||||
let last_block = result.last_block.unwrap();
|
||||
assert_eq!(last_hash, last_block.hash);
|
||||
assert_eq!(result.last_state_trie_block, Some(last_block.number));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -460,13 +510,57 @@ mod tests {
|
||||
let last_hash = blocks.last().unwrap().recovered_block().hash();
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
handle.save_blocks(full_save_plan(blocks), tx).unwrap();
|
||||
|
||||
let result = rx.recv().unwrap();
|
||||
assert_eq!(last_hash, result.last_block.unwrap().hash);
|
||||
let last_block = result.last_block.unwrap();
|
||||
assert_eq!(last_hash, last_block.hash);
|
||||
assert_eq!(result.last_state_trie_block, Some(last_block.number));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_blocks_above_preserves_partial_state_trie() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let provider = create_test_provider_factory();
|
||||
let mut test_block_builder = TestBlockBuilder::eth().with_state();
|
||||
let blocks = test_block_builder.get_executed_blocks(0..4).collect::<Vec<_>>();
|
||||
|
||||
let provider_rw = provider.database_provider_rw().unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&SaveBlocksPlan::new(
|
||||
blocks,
|
||||
vec![
|
||||
SaveBlocksPlanStep::new(0..2, Some(2..4), true),
|
||||
SaveBlocksPlanStep::new(2..4, None, true),
|
||||
],
|
||||
),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let handle = persistence_handle(provider.clone());
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.remove_blocks_above(2, tx).unwrap();
|
||||
|
||||
let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("test timed out");
|
||||
let last_block = result.last_block.unwrap();
|
||||
assert_eq!(last_block.number, 2);
|
||||
assert_eq!(result.last_state_trie_block, Some(1));
|
||||
|
||||
let finish_checkpoint =
|
||||
provider.provider().unwrap().get_stage_checkpoint(StageId::Finish).unwrap().unwrap();
|
||||
assert_eq!(finish_checkpoint.block_number, 2);
|
||||
assert_eq!(
|
||||
finish_checkpoint.finish_stage_checkpoint().unwrap().partial_state_trie,
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
/// Verifies that committing `save_blocks` history before running the pruner
|
||||
/// prevents the pruner from overwriting new entries.
|
||||
///
|
||||
@@ -555,7 +649,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let provider_rw = provider_factory.database_provider_rw().unwrap();
|
||||
provider_rw.save_blocks(blocks_a, SaveBlocksMode::Full).unwrap();
|
||||
provider_rw.save_blocks(&full_save_plan(blocks_a), SaveBlocksMode::Full).unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
}
|
||||
|
||||
@@ -612,7 +706,12 @@ mod tests {
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider_rw = pf.database_provider_rw().unwrap();
|
||||
provider_rw.save_blocks(vec![block_b2], SaveBlocksMode::Full).unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&full_save_plan(std::slice::from_ref(&block_b2).to_vec()),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
});
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ use reth_primitives_traits::{
|
||||
};
|
||||
use reth_provider::{
|
||||
BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader,
|
||||
StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader,
|
||||
StorageSettingsCache, TransactionVariant,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, SaveBlocksPlan,
|
||||
SaveBlocksPlanStep, StageCheckpointReader, StateProviderBox, StateProviderFactory, StateReader,
|
||||
StorageChangeSetReader, StorageSettingsCache, TransactionVariant,
|
||||
};
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_stages_api::ControlFlow;
|
||||
@@ -433,6 +433,7 @@ where
|
||||
|
||||
let persistence_state = PersistenceState {
|
||||
last_persisted_block: BlockNumHash::new(best_block_number, header.hash()),
|
||||
last_state_trie_persisted_block: BlockNumHash::new(best_block_number, header.hash()),
|
||||
rx: None,
|
||||
};
|
||||
|
||||
@@ -1350,7 +1351,7 @@ where
|
||||
/// Helper method to remove blocks and set the persistence state. This ensures we keep track of
|
||||
/// the current persistence action while we're removing blocks.
|
||||
fn remove_blocks(&mut self, new_tip_num: u64) {
|
||||
debug!(target: "engine::tree", ?new_tip_num, last_persisted_block_number=?self.persistence_state.last_persisted_block.number, "Removing blocks using persistence task");
|
||||
debug!(target: "engine::tree", ?new_tip_num, last_persisted_block=?self.persistence_state.last_persisted_block.number, "Removing blocks using persistence task");
|
||||
if new_tip_num < self.persistence_state.last_persisted_block.number {
|
||||
debug!(target: "engine::tree", ?new_tip_num, "Starting remove blocks job");
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
@@ -1361,24 +1362,25 @@ where
|
||||
|
||||
/// Helper method to save blocks and set the persistence state. This ensures we keep track of
|
||||
/// the current persistence action while we're saving blocks.
|
||||
fn persist_blocks(&mut self, blocks_to_persist: Vec<ExecutedBlock<N>>) {
|
||||
if blocks_to_persist.is_empty() {
|
||||
fn persist_blocks(&mut self, plan: SaveBlocksPlan<N>) {
|
||||
if plan.is_empty() {
|
||||
debug!(target: "engine::tree", "Returned empty set of blocks to persist");
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: checked non-empty above
|
||||
let highest_num_hash = blocks_to_persist
|
||||
.iter()
|
||||
.max_by_key(|block| block.recovered_block().number())
|
||||
.map(|b| b.recovered_block().num_hash())
|
||||
.expect("Checked non-empty persisting blocks");
|
||||
let last_block = plan.last_block().expect("checked non-empty persisting blocks");
|
||||
|
||||
debug!(target: "engine::tree", count=blocks_to_persist.len(), blocks = ?blocks_to_persist.iter().map(|block| block.recovered_block().num_hash()).collect::<Vec<_>>(), "Persisting blocks");
|
||||
debug!(
|
||||
target: "engine::tree",
|
||||
count = plan.blocks.len(),
|
||||
steps = ?plan.steps,
|
||||
blocks = ?plan.blocks.iter().map(|block| block.recovered_block().num_hash()).collect::<Vec<_>>(),
|
||||
"Persisting blocks"
|
||||
);
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
let _ = self.persistence.save_blocks(blocks_to_persist, tx);
|
||||
let _ = self.persistence.save_blocks(plan, tx);
|
||||
|
||||
self.persistence_state.start_save(highest_num_hash, rx);
|
||||
self.persistence_state.start_save(last_block, rx);
|
||||
}
|
||||
|
||||
/// Triggers new persistence actions if no persistence task is currently in progress.
|
||||
@@ -1390,9 +1392,8 @@ where
|
||||
if let Some(new_tip_num) = self.find_disk_reorg()? {
|
||||
self.remove_blocks(new_tip_num)
|
||||
} else if self.should_persist() {
|
||||
let blocks_to_persist =
|
||||
self.get_canonical_blocks_to_persist(PersistTarget::Threshold)?;
|
||||
self.persist_blocks(blocks_to_persist);
|
||||
let plan = self.get_save_blocks_plan(PersistTarget::Threshold)?;
|
||||
self.persist_blocks(plan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,15 +1424,15 @@ where
|
||||
self.on_persistence_complete(result, start_time)?;
|
||||
}
|
||||
|
||||
let blocks_to_persist = self.get_canonical_blocks_to_persist(PersistTarget::Head)?;
|
||||
let plan = self.get_save_blocks_plan(PersistTarget::Head)?;
|
||||
|
||||
if blocks_to_persist.is_empty() {
|
||||
if plan.is_empty() {
|
||||
debug!(target: "engine::tree", "persistence complete, signaling termination");
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
debug!(target: "engine::tree", count = blocks_to_persist.len(), "persisting remaining blocks before shutdown");
|
||||
self.persist_blocks(blocks_to_persist);
|
||||
debug!(target: "engine::tree", count = plan.blocks.len(), "persisting remaining blocks before shutdown");
|
||||
self.persist_blocks(plan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,25 +1468,25 @@ where
|
||||
) -> Result<(), AdvancePersistenceError> {
|
||||
self.metrics.engine.persistence_duration.record(start_time.elapsed());
|
||||
|
||||
let commit_duration = result.commit_duration;
|
||||
let Some(BlockNumHash {
|
||||
hash: last_persisted_block_hash,
|
||||
number: last_persisted_block_number,
|
||||
}) = result.last_block
|
||||
let PersistenceResult { last_block, last_state_trie_block, commit_duration } = result;
|
||||
let Some(BlockNumHash { hash: last_block_hash, number: last_block_number }) = last_block
|
||||
else {
|
||||
// if this happened, then we persisted no blocks because we sent an empty vec of blocks
|
||||
warn!(target: "engine::tree", "Persistence task completed but did not persist any blocks");
|
||||
return Ok(())
|
||||
};
|
||||
|
||||
debug!(target: "engine::tree", ?last_persisted_block_hash, ?last_persisted_block_number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
|
||||
self.persistence_state.finish(last_persisted_block_hash, last_persisted_block_number);
|
||||
let last_block = BlockNumHash::new(last_block_number, last_block_hash);
|
||||
let last_state_trie_persisted_block =
|
||||
self.last_state_trie_persisted_block(last_block, last_state_trie_block)?;
|
||||
|
||||
debug!(target: "engine::tree", ?last_block_hash, ?last_block_number, last_state_trie_persisted_block = last_state_trie_persisted_block.number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
|
||||
self.persistence_state.finish(last_block, last_state_trie_persisted_block);
|
||||
|
||||
// Evict trie changesets for blocks below the eviction threshold.
|
||||
// Keep at least CHANGESET_CACHE_RETENTION_BLOCKS from the persisted tip, and also respect
|
||||
// the finalized block if set.
|
||||
let min_threshold =
|
||||
last_persisted_block_number.saturating_sub(CHANGESET_CACHE_RETENTION_BLOCKS);
|
||||
let min_threshold = last_block_number.saturating_sub(CHANGESET_CACHE_RETENTION_BLOCKS);
|
||||
let eviction_threshold =
|
||||
if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() {
|
||||
// Use the minimum of finalized block and retention threshold to be conservative
|
||||
@@ -1496,7 +1497,7 @@ where
|
||||
};
|
||||
debug!(
|
||||
target: "engine::tree",
|
||||
last_persisted = last_persisted_block_number,
|
||||
last_persisted_block = last_block_number,
|
||||
finalized_number = ?self.canonical_in_memory_state.get_finalized_num_hash().map(|f| f.number),
|
||||
eviction_threshold,
|
||||
"Evicting changesets below threshold"
|
||||
@@ -1506,22 +1507,50 @@ where
|
||||
// Invalidate cached overlay since the anchor has changed
|
||||
self.state.tree_state.invalidate_cached_overlay();
|
||||
|
||||
self.on_new_persisted_block()?;
|
||||
self.on_new_persisted_block(last_state_trie_persisted_block)?;
|
||||
|
||||
// Re-prepare overlay for the current canonical head with the new anchor.
|
||||
// Spawn a background task to trigger computation so it's ready when the next payload
|
||||
// arrives.
|
||||
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
|
||||
if let Some(prepared) = self.state.tree_state.prepare_canonical_overlay() {
|
||||
self.runtime.spawn_blocking_named("prepare-overlay", move || {
|
||||
let _ = overlay.get();
|
||||
let _ = prepared.overlay.get(prepared.anchor_hash);
|
||||
});
|
||||
}
|
||||
|
||||
self.purge_timing_stats(last_persisted_block_number, commit_duration);
|
||||
self.purge_timing_stats(last_block_number, commit_duration);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the highest block that can be dropped from memory after persistence completes.
|
||||
fn last_state_trie_persisted_block(
|
||||
&self,
|
||||
last_block: BlockNumHash,
|
||||
last_state_trie_block: Option<u64>,
|
||||
) -> ProviderResult<BlockNumHash> {
|
||||
let Some(last_state_trie_block) = last_state_trie_block else { return Ok(last_block) };
|
||||
debug_assert!(
|
||||
last_state_trie_block <= last_block.number,
|
||||
"state/trie frontier cannot exceed the last persisted block"
|
||||
);
|
||||
if last_state_trie_block >= last_block.number {
|
||||
return Ok(last_block)
|
||||
}
|
||||
|
||||
let hash = self
|
||||
.canonical_in_memory_state
|
||||
.hash_by_number(last_state_trie_block)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| {
|
||||
self.provider
|
||||
.block_hash(last_state_trie_block)?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(last_state_trie_block.into()))
|
||||
})?;
|
||||
|
||||
Ok(BlockNumHash::new(last_state_trie_block, hash))
|
||||
}
|
||||
|
||||
/// Handles a message from the engine.
|
||||
///
|
||||
/// Returns `ControlFlow::Break(())` if the engine should terminate.
|
||||
@@ -1825,7 +1854,7 @@ where
|
||||
// update the tracked chain height, after backfill sync both the canonical height and
|
||||
// persisted height are the same
|
||||
self.state.tree_state.set_canonical_head(new_head.num_hash());
|
||||
self.persistence_state.finish(new_head.hash(), new_head.number());
|
||||
self.persistence_state.finish(new_head.num_hash(), new_head.num_hash());
|
||||
|
||||
// update the tracked canonical head
|
||||
self.canonical_in_memory_state.set_canonical_head(new_head);
|
||||
@@ -2033,62 +2062,96 @@ where
|
||||
self.config.persistence_threshold()
|
||||
}
|
||||
|
||||
/// Returns a batch of consecutive canonical blocks to persist in the range
|
||||
/// `(last_persisted_number .. target]`. The expected order is oldest -> newest.
|
||||
fn get_canonical_blocks_to_persist(
|
||||
/// Returns the save plan for the next persistence cycle.
|
||||
fn get_save_blocks_plan(
|
||||
&self,
|
||||
target: PersistTarget,
|
||||
) -> Result<Vec<ExecutedBlock<N>>, AdvancePersistenceError> {
|
||||
) -> Result<SaveBlocksPlan<N>, AdvancePersistenceError> {
|
||||
// We will calculate the state root using the database, so we need to be sure there are no
|
||||
// changes
|
||||
debug_assert!(!self.persistence_state.in_progress());
|
||||
|
||||
let mut blocks_to_persist = Vec::new();
|
||||
let mut blocks = Vec::new();
|
||||
let mut current_hash = self.state.tree_state.canonical_block_hash();
|
||||
let last_persisted_number = self.persistence_state.last_persisted_block.number;
|
||||
let last_state_trie_persisted_block_number =
|
||||
self.persistence_state.last_state_trie_persisted_block.number;
|
||||
let last_persisted_block_number = self.persistence_state.last_persisted_block.number;
|
||||
let canonical_head_number = self.state.tree_state.canonical_block_number();
|
||||
|
||||
let target_number = match target {
|
||||
PersistTarget::Head => canonical_head_number,
|
||||
let last_block_target_number = match target {
|
||||
PersistTarget::Threshold => {
|
||||
canonical_head_number.saturating_sub(self.config.memory_block_buffer_target())
|
||||
}
|
||||
PersistTarget::Head => canonical_head_number,
|
||||
};
|
||||
|
||||
debug!(
|
||||
target: "engine::tree",
|
||||
?current_hash,
|
||||
?last_persisted_number,
|
||||
?last_state_trie_persisted_block_number,
|
||||
?last_persisted_block_number,
|
||||
?canonical_head_number,
|
||||
?target_number,
|
||||
"Returning canonical blocks to persist"
|
||||
target = ?target,
|
||||
"Returning save plan"
|
||||
);
|
||||
while let Some(block) = self.state.tree_state.blocks_by_hash.get(¤t_hash) {
|
||||
if block.recovered_block().number() <= last_persisted_number {
|
||||
if block.recovered_block().number() <= last_state_trie_persisted_block_number {
|
||||
break;
|
||||
}
|
||||
|
||||
if block.recovered_block().number() <= target_number {
|
||||
blocks_to_persist.push(block.clone());
|
||||
if block.recovered_block().number() <= last_block_target_number {
|
||||
blocks.push(block.clone());
|
||||
}
|
||||
|
||||
current_hash = block.recovered_block().parent_hash();
|
||||
}
|
||||
|
||||
// Reverse the order so that the oldest block comes first
|
||||
blocks_to_persist.reverse();
|
||||
blocks.reverse();
|
||||
|
||||
Ok(blocks_to_persist)
|
||||
let trie_catchup_block_count = last_persisted_block_number
|
||||
.saturating_sub(last_state_trie_persisted_block_number)
|
||||
.min(blocks.len() as u64) as usize;
|
||||
let persist_rest_block_count = blocks.len().saturating_sub(trie_catchup_block_count);
|
||||
let state_masking_block_count =
|
||||
persist_rest_block_count.min(self.config.num_state_masking_blocks() as usize);
|
||||
let full_persist_block_count = persist_rest_block_count - state_masking_block_count;
|
||||
let full_persist_start = trie_catchup_block_count;
|
||||
let state_masking_start = full_persist_start + full_persist_block_count;
|
||||
let state_masking_range = state_masking_start..blocks.len();
|
||||
let mut steps = Vec::new();
|
||||
|
||||
if trie_catchup_block_count > 0 {
|
||||
steps.push(SaveBlocksPlanStep::new(
|
||||
0..trie_catchup_block_count,
|
||||
Some(state_masking_range.clone()),
|
||||
false,
|
||||
));
|
||||
}
|
||||
if full_persist_block_count > 0 {
|
||||
steps.push(SaveBlocksPlanStep::new(
|
||||
full_persist_start..state_masking_start,
|
||||
Some(state_masking_range.clone()),
|
||||
true,
|
||||
));
|
||||
}
|
||||
if state_masking_block_count > 0 {
|
||||
steps.push(SaveBlocksPlanStep::new(state_masking_range, None, true));
|
||||
}
|
||||
|
||||
Ok(SaveBlocksPlan::new(blocks, steps))
|
||||
}
|
||||
|
||||
/// This clears the blocks from the in-memory tree state that have been persisted to the
|
||||
/// database.
|
||||
/// This clears the blocks from the in-memory tree state that no longer need to stay resident
|
||||
/// after persistence completes.
|
||||
///
|
||||
/// This also updates the canonical in-memory state to reflect the newest persisted block
|
||||
/// height.
|
||||
/// This also updates the canonical in-memory state to reflect the newest persisted block tip,
|
||||
/// even if trie persistence only advanced through an earlier block.
|
||||
///
|
||||
/// Assumes that `finish` has been called on the `persistence_state` at least once
|
||||
fn on_new_persisted_block(&mut self) -> ProviderResult<()> {
|
||||
fn on_new_persisted_block(
|
||||
&mut self,
|
||||
in_memory_persisted_block: BlockNumHash,
|
||||
) -> ProviderResult<()> {
|
||||
// If we have an on-disk reorg, we need to handle it first before touching the in-memory
|
||||
// state.
|
||||
if let Some(remove_above) = self.find_disk_reorg()? {
|
||||
@@ -2097,11 +2160,11 @@ where
|
||||
}
|
||||
|
||||
let finalized = self.state.forkchoice_state_tracker.last_valid_finalized();
|
||||
self.remove_before(self.persistence_state.last_persisted_block, finalized)?;
|
||||
self.canonical_in_memory_state.remove_persisted_blocks(BlockNumHash {
|
||||
number: self.persistence_state.last_persisted_block.number,
|
||||
hash: self.persistence_state.last_persisted_block.hash,
|
||||
});
|
||||
self.remove_before(in_memory_persisted_block, finalized)?;
|
||||
self.canonical_in_memory_state.remove_persisted_blocks_until(
|
||||
self.persistence_state.last_persisted_block,
|
||||
in_memory_persisted_block.number,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -970,7 +970,7 @@ mod tests {
|
||||
use rand::Rng;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_db_common::init::init_genesis;
|
||||
use reth_ethereum_primitives::TransactionSigned;
|
||||
use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
|
||||
use reth_evm::OnStateHook;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_primitives_traits::{Account, Recovered, StorageEntry};
|
||||
@@ -1252,7 +1252,7 @@ mod tests {
|
||||
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
|
||||
OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
OverlayBuilder::new(ChangesetCache::new()),
|
||||
OverlayBuilder::<EthPrimitives>::new(genesis_hash, ChangesetCache::new()),
|
||||
),
|
||||
&TreeConfig::default(),
|
||||
);
|
||||
|
||||
@@ -896,6 +896,7 @@ mod tests {
|
||||
use reth_provider::{
|
||||
providers::{OverlayBuilder, OverlayStateProviderFactory},
|
||||
test_utils::create_test_provider_factory,
|
||||
ChainSpecProvider,
|
||||
};
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use reth_trie_parallel::proof_task::ProofTaskCtx;
|
||||
@@ -984,9 +985,13 @@ mod tests {
|
||||
fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let anchor_hash = provider_factory.chain_spec().genesis_hash();
|
||||
let overlay_factory = OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
OverlayBuilder::new(ChangesetCache::new()),
|
||||
OverlayBuilder::<reth_chain_state::EthPrimitives>::new(
|
||||
anchor_hash,
|
||||
ChangesetCache::new(),
|
||||
),
|
||||
);
|
||||
let proof_worker_handle =
|
||||
ProofWorkerHandle::new(&runtime, ProofTaskCtx::new(overlay_factory), false);
|
||||
|
||||
@@ -527,8 +527,7 @@ where
|
||||
// Create overlay factory for payload processor (StateRootTask path needs it for
|
||||
// multiproofs)
|
||||
let provider_factory = self.provider.clone();
|
||||
let overlay_builder = OverlayBuilder::new(self.changeset_cache.clone())
|
||||
.with_block_hash(Some(anchor_hash))
|
||||
let overlay_builder = OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
|
||||
.with_lazy_overlay(lazy_overlay);
|
||||
let overlay_factory =
|
||||
OverlayStateProviderFactory::new(provider_factory.clone(), overlay_builder.clone());
|
||||
@@ -1100,7 +1099,7 @@ where
|
||||
fn compute_state_root_parallel(
|
||||
&self,
|
||||
provider_factory: P,
|
||||
overlay_builder: OverlayBuilder,
|
||||
overlay_builder: OverlayBuilder<N>,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
|
||||
let hashed_state = hashed_state.get();
|
||||
@@ -1245,7 +1244,7 @@ where
|
||||
&self,
|
||||
state_provider_builder: StateProviderBuilder<N, P>,
|
||||
provider_factory: P,
|
||||
overlay_builder: OverlayBuilder,
|
||||
overlay_builder: OverlayBuilder<N>,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
task_trie_updates: TrieUpdates,
|
||||
) -> bool {
|
||||
@@ -1448,7 +1447,7 @@ where
|
||||
env: ExecutionEnv<Evm>,
|
||||
txs: T,
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
overlay_factory: OverlayStateProviderFactory<P, N>,
|
||||
strategy: StateRootStrategy,
|
||||
) -> Result<
|
||||
PayloadHandle<
|
||||
@@ -1568,7 +1567,7 @@ where
|
||||
fn get_parent_lazy_overlay(
|
||||
parent_hash: B256,
|
||||
state: &EngineApiTreeState<N>,
|
||||
) -> (Option<LazyOverlay>, B256) {
|
||||
) -> (Option<LazyOverlay<N>>, B256) {
|
||||
// Get blocks leading to the parent to determine the anchor
|
||||
let (anchor_hash, blocks) =
|
||||
state.tree_state.blocks_by_hash(parent_hash).unwrap_or_else(|| (parent_hash, vec![]));
|
||||
@@ -1596,10 +1595,7 @@ where
|
||||
"Creating lazy overlay for in-memory blocks"
|
||||
);
|
||||
|
||||
// Extract deferred trie data handles (non-blocking)
|
||||
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
|
||||
|
||||
(Some(LazyOverlay::new(anchor_hash, handles)), anchor_hash)
|
||||
(Some(LazyOverlay::new(blocks)), anchor_hash)
|
||||
}
|
||||
|
||||
/// Spawns a background task to compute and sort trie data for the executed block.
|
||||
@@ -2033,8 +2029,7 @@ where
|
||||
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
|
||||
let overlay_factory = OverlayStateProviderFactory::new(
|
||||
self.provider.clone(),
|
||||
OverlayBuilder::new(self.changeset_cache.clone())
|
||||
.with_block_hash(Some(anchor_hash))
|
||||
OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
|
||||
.with_lazy_overlay(lazy_overlay),
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
use crate::persistence::PersistenceResult;
|
||||
use alloy_eips::BlockNumHash;
|
||||
use alloy_primitives::B256;
|
||||
use crossbeam_channel::Receiver as CrossbeamReceiver;
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use tracing::trace;
|
||||
@@ -30,10 +29,12 @@ use tracing::trace;
|
||||
/// The state of the persistence task.
|
||||
#[derive(Debug)]
|
||||
pub struct PersistenceState {
|
||||
/// Hash and number of the last block persisted.
|
||||
/// Hash and number of the highest block whose non-state/trie outputs are persisted.
|
||||
///
|
||||
/// This tracks the chain height that is persisted on disk
|
||||
/// This tracks the highest canonical block with durable block/static-file/plain-state data.
|
||||
pub(crate) last_persisted_block: BlockNumHash,
|
||||
/// Hash and number of the highest block whose state/trie outputs are persisted.
|
||||
pub(crate) last_state_trie_persisted_block: BlockNumHash,
|
||||
/// Receiver end of channel where the result of the persistence task will be
|
||||
/// sent when done. A None value means there's no persistence task in progress.
|
||||
pub(crate) rx:
|
||||
@@ -76,13 +77,18 @@ impl PersistenceState {
|
||||
/// Sets state for a finished persistence task.
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
last_persisted_block_hash: B256,
|
||||
last_persisted_block_number: u64,
|
||||
last_persisted_block: BlockNumHash,
|
||||
last_state_trie_persisted_block: BlockNumHash,
|
||||
) {
|
||||
trace!(target: "engine::tree", block= %last_persisted_block_number, hash=%last_persisted_block_hash, "updating persistence state");
|
||||
trace!(
|
||||
target: "engine::tree",
|
||||
last_persisted_block = %last_persisted_block.number,
|
||||
last_state_trie_persisted_block = %last_state_trie_persisted_block.number,
|
||||
"updating persistence state"
|
||||
);
|
||||
self.rx = None;
|
||||
self.last_persisted_block =
|
||||
BlockNumHash::new(last_persisted_block_number, last_persisted_block_hash);
|
||||
self.last_persisted_block = last_persisted_block;
|
||||
self.last_state_trie_persisted_block = last_state_trie_persisted_block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use alloy_primitives::{
|
||||
map::{B256Map, B256Set},
|
||||
BlockNumber, B256,
|
||||
};
|
||||
use reth_chain_state::{DeferredTrieData, EthPrimitives, ExecutedBlock, LazyOverlay};
|
||||
use reth_chain_state::{EthPrimitives, ExecutedBlock, LazyOverlay};
|
||||
use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader};
|
||||
use std::{
|
||||
collections::{btree_map, hash_map, BTreeMap, VecDeque},
|
||||
@@ -43,7 +43,7 @@ pub struct TreeState<N: NodePrimitives = EthPrimitives> {
|
||||
/// This is optimistically prepared after the canonical head changes, so that
|
||||
/// the next payload building on the canonical head can use it immediately
|
||||
/// without recomputing.
|
||||
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay>,
|
||||
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay<N>>,
|
||||
}
|
||||
|
||||
impl<N: NodePrimitives> TreeState<N> {
|
||||
@@ -106,10 +106,10 @@ impl<N: NodePrimitives> TreeState<N> {
|
||||
/// This should be called after the canonical head changes to optimistically
|
||||
/// prepare the overlay for the next payload that will likely build on it.
|
||||
///
|
||||
/// Returns a clone of the [`LazyOverlay`] so the caller can spawn a background
|
||||
/// task to trigger computation via [`LazyOverlay::get`]. This ensures the overlay
|
||||
/// is actually computed before the next payload arrives.
|
||||
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<LazyOverlay> {
|
||||
/// Returns a clone of the prepared overlay so the caller can spawn a background
|
||||
/// task to trigger computation via [`LazyOverlay::get`] for the cached anchor.
|
||||
/// This ensures the overlay is actually computed before the next payload arrives.
|
||||
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<PreparedCanonicalOverlay<N>> {
|
||||
let canonical_hash = self.current_canonical_head.hash;
|
||||
|
||||
// Get blocks leading to the canonical head
|
||||
@@ -119,25 +119,23 @@ impl<N: NodePrimitives> TreeState<N> {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Extract deferred trie data handles from blocks (newest to oldest)
|
||||
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
|
||||
|
||||
let overlay = LazyOverlay::new(anchor_hash, handles);
|
||||
self.cached_canonical_overlay = Some(PreparedCanonicalOverlay {
|
||||
let num_blocks = blocks.len();
|
||||
let prepared = PreparedCanonicalOverlay {
|
||||
parent_hash: canonical_hash,
|
||||
overlay: overlay.clone(),
|
||||
overlay: LazyOverlay::new(blocks),
|
||||
anchor_hash,
|
||||
});
|
||||
};
|
||||
self.cached_canonical_overlay = Some(prepared.clone());
|
||||
|
||||
debug!(
|
||||
target: "engine::tree",
|
||||
%canonical_hash,
|
||||
%anchor_hash,
|
||||
num_blocks = blocks.len(),
|
||||
num_blocks,
|
||||
"Prepared cached canonical overlay"
|
||||
);
|
||||
|
||||
Some(overlay)
|
||||
Some(prepared)
|
||||
}
|
||||
|
||||
/// Returns the cached overlay if it matches the requested parent hash and anchor.
|
||||
@@ -148,7 +146,7 @@ impl<N: NodePrimitives> TreeState<N> {
|
||||
&self,
|
||||
parent_hash: B256,
|
||||
expected_anchor: B256,
|
||||
) -> Option<&PreparedCanonicalOverlay> {
|
||||
) -> Option<&PreparedCanonicalOverlay<N>> {
|
||||
self.cached_canonical_overlay.as_ref().filter(|cached| {
|
||||
cached.parent_hash == parent_hash && cached.anchor_hash == expected_anchor
|
||||
})
|
||||
@@ -429,10 +427,10 @@ impl<N: NodePrimitives> TreeState<N> {
|
||||
/// the next payload (which typically builds on the canonical head) to reuse
|
||||
/// the pre-computed overlay immediately without re-traversing in-memory blocks.
|
||||
///
|
||||
/// The overlay captures deferred trie data handles from all in-memory blocks
|
||||
/// The overlay captures executed blocks from all in-memory blocks
|
||||
/// between the canonical head and the persisted anchor. When a new payload
|
||||
/// arrives building on the canonical head, this cached overlay can be used
|
||||
/// directly instead of calling `blocks_by_hash` and collecting handles again.
|
||||
/// directly instead of calling `blocks_by_hash` again.
|
||||
///
|
||||
/// # Invalidation
|
||||
///
|
||||
@@ -440,16 +438,16 @@ impl<N: NodePrimitives> TreeState<N> {
|
||||
/// - Persistence completes (anchor changes)
|
||||
/// - The canonical head changes to a different block
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreparedCanonicalOverlay {
|
||||
pub struct PreparedCanonicalOverlay<N: NodePrimitives = EthPrimitives> {
|
||||
/// The block hash for which this overlay is prepared as a parent.
|
||||
///
|
||||
/// When a payload arrives with this parent hash, the overlay can be reused.
|
||||
pub parent_hash: B256,
|
||||
/// The pre-computed lazy overlay containing deferred trie data handles.
|
||||
/// The pre-computed lazy overlay containing executed blocks for the canonical segment.
|
||||
///
|
||||
/// This is computed optimistically after `set_canonical_head` so subsequent
|
||||
/// payloads don't need to re-collect the handles.
|
||||
pub overlay: LazyOverlay,
|
||||
/// This is computed optimistically after `set_canonical_head` so subsequent payloads don't
|
||||
/// need to walk the in-memory chain again.
|
||||
pub overlay: LazyOverlay<N>,
|
||||
/// The anchor hash (persisted ancestor) this overlay is based on.
|
||||
///
|
||||
/// Used to verify the overlay is still valid (anchor hasn't changed due to persistence).
|
||||
|
||||
@@ -222,7 +222,11 @@ impl TestHarness {
|
||||
engine_api_tree_state,
|
||||
canonical_in_memory_state,
|
||||
persistence_handle,
|
||||
PersistenceState { last_persisted_block: BlockNumHash::default(), rx: None },
|
||||
PersistenceState {
|
||||
last_persisted_block: BlockNumHash::default(),
|
||||
last_state_trie_persisted_block: BlockNumHash::default(),
|
||||
rx: None,
|
||||
},
|
||||
payload_builder,
|
||||
tree_config,
|
||||
EngineApiKind::Ethereum,
|
||||
@@ -360,6 +364,17 @@ impl TestHarness {
|
||||
}
|
||||
}
|
||||
|
||||
type ExpectedPlanStep = (std::ops::Range<usize>, Option<std::ops::Range<usize>>, bool);
|
||||
|
||||
fn assert_plan_steps(plan: &SaveBlocksPlan<EthPrimitives>, expected: &[ExpectedPlanStep]) {
|
||||
assert_eq!(plan.steps.len(), expected.len());
|
||||
for (step, (block_range, masking_range, persist_rest)) in plan.steps.iter().zip(expected) {
|
||||
assert_eq!(&step.block_range, block_range);
|
||||
assert_eq!(&step.state_trie_masking_range, masking_range);
|
||||
assert_eq!(step.persist_rest, *persist_rest);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplified test metrics for validation calls
|
||||
#[derive(Debug, Default)]
|
||||
struct TestMetrics {
|
||||
@@ -554,12 +569,16 @@ async fn test_tree_persist_blocks() {
|
||||
|
||||
let received_action =
|
||||
test_harness.action_rx.recv().expect("Failed to receive save blocks action");
|
||||
if let PersistenceAction::SaveBlocks(saved_blocks, _) = received_action {
|
||||
if let PersistenceAction::SaveBlocks(plan, _) = received_action {
|
||||
// only blocks.len() - tree_config.memory_block_buffer_target() will be
|
||||
// persisted
|
||||
let expected_persist_len = blocks.len() - tree_config.memory_block_buffer_target() as usize;
|
||||
assert_eq!(saved_blocks.len(), expected_persist_len);
|
||||
assert_eq!(saved_blocks, blocks[..expected_persist_len]);
|
||||
assert_eq!(plan.blocks.len(), expected_persist_len);
|
||||
assert_eq!(plan.blocks, blocks[..expected_persist_len]);
|
||||
assert_plan_steps(
|
||||
&plan,
|
||||
&[(0..expected_persist_len, Some(expected_persist_len..expected_persist_len), true)],
|
||||
);
|
||||
} else {
|
||||
panic!("unexpected action received {received_action:?}");
|
||||
}
|
||||
@@ -704,8 +723,8 @@ fn test_backpressure_waits_for_persistence_before_reading_incoming() {
|
||||
test_harness.tree.config = test_harness
|
||||
.tree
|
||||
.config
|
||||
.with_persistence_threshold(0)
|
||||
.with_persistence_backpressure_threshold(1);
|
||||
.with_persistence_threshold(1)
|
||||
.with_persistence_backpressure_threshold(2);
|
||||
|
||||
let (persist_tx, persist_rx) = crossbeam_channel::bounded(1);
|
||||
let persisted = blocks.last().unwrap().recovered_block().num_hash();
|
||||
@@ -736,6 +755,7 @@ fn test_backpressure_waits_for_persistence_before_reading_incoming() {
|
||||
persist_tx
|
||||
.send(PersistenceResult {
|
||||
last_block: Some(persisted),
|
||||
last_state_trie_block: Some(persisted.number),
|
||||
commit_duration: Some(Duration::ZERO),
|
||||
})
|
||||
.unwrap();
|
||||
@@ -770,10 +790,10 @@ async fn test_tree_state_on_new_head_reorg() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let chain_spec = MAINNET.clone();
|
||||
|
||||
// Set persistence_threshold to 1
|
||||
// Keep a single block in memory while still leaving room for the persistence threshold.
|
||||
let mut test_harness = TestHarness::new(chain_spec);
|
||||
test_harness.tree.config =
|
||||
test_harness.tree.config.with_persistence_threshold(1).with_memory_block_buffer_target(1);
|
||||
test_harness.tree.config.with_persistence_threshold(2).with_memory_block_buffer_target(1);
|
||||
let mut test_block_builder = TestBlockBuilder::eth();
|
||||
let blocks: Vec<_> = test_block_builder.get_executed_blocks(1..6).collect();
|
||||
|
||||
@@ -824,15 +844,16 @@ async fn test_tree_state_on_new_head_reorg() {
|
||||
|
||||
// get rid of the prev action
|
||||
let received_action = test_harness.action_rx.recv().unwrap();
|
||||
let PersistenceAction::SaveBlocks(saved_blocks, sender) = received_action else {
|
||||
let PersistenceAction::SaveBlocks(plan, sender) = received_action else {
|
||||
panic!("received wrong action");
|
||||
};
|
||||
assert_eq!(saved_blocks, vec![blocks[0].clone(), blocks[1].clone()]);
|
||||
assert_eq!(plan.blocks, vec![blocks[0].clone(), blocks[1].clone()]);
|
||||
|
||||
// send the response so we can advance again
|
||||
sender
|
||||
.send(PersistenceResult {
|
||||
last_block: Some(blocks[1].recovered_block().num_hash()),
|
||||
last_state_trie_block: Some(blocks[1].recovered_block().number()),
|
||||
commit_duration: Some(Duration::ZERO),
|
||||
})
|
||||
.unwrap();
|
||||
@@ -968,8 +989,10 @@ async fn test_get_canonical_blocks_to_persist() {
|
||||
test_harness = test_harness.with_blocks(blocks.clone());
|
||||
|
||||
let last_persisted_block_number = 3;
|
||||
test_harness.tree.persistence_state.last_persisted_block =
|
||||
let last_persisted_block =
|
||||
blocks[last_persisted_block_number as usize].recovered_block.num_hash();
|
||||
test_harness.tree.persistence_state.last_persisted_block = last_persisted_block;
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block = last_persisted_block;
|
||||
|
||||
let persistence_threshold = 4;
|
||||
let memory_block_buffer_target = 3;
|
||||
@@ -977,16 +1000,15 @@ async fn test_get_canonical_blocks_to_persist() {
|
||||
.with_persistence_threshold(persistence_threshold)
|
||||
.with_memory_block_buffer_target(memory_block_buffer_target);
|
||||
|
||||
let blocks_to_persist =
|
||||
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
|
||||
let plan = test_harness.tree.get_save_blocks_plan(PersistTarget::Threshold).unwrap();
|
||||
|
||||
let expected_blocks_to_persist_length: usize =
|
||||
(canonical_head_number - memory_block_buffer_target - last_persisted_block_number)
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blocks_to_persist.len(), expected_blocks_to_persist_length);
|
||||
for (i, item) in blocks_to_persist.iter().enumerate().take(expected_blocks_to_persist_length) {
|
||||
assert_eq!(plan.blocks.len(), expected_blocks_to_persist_length);
|
||||
for (i, item) in plan.blocks.iter().enumerate().take(expected_blocks_to_persist_length) {
|
||||
assert_eq!(item.recovered_block().number, last_persisted_block_number + i as u64 + 1);
|
||||
}
|
||||
|
||||
@@ -997,15 +1019,14 @@ async fn test_get_canonical_blocks_to_persist() {
|
||||
|
||||
assert!(test_harness.tree.state.tree_state.sealed_header_by_hash(&fork_block_hash).is_some());
|
||||
|
||||
let blocks_to_persist =
|
||||
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
|
||||
assert_eq!(blocks_to_persist.len(), expected_blocks_to_persist_length);
|
||||
let plan = test_harness.tree.get_save_blocks_plan(PersistTarget::Threshold).unwrap();
|
||||
assert_eq!(plan.blocks.len(), expected_blocks_to_persist_length);
|
||||
|
||||
// check that the fork block is not included in the blocks to persist
|
||||
assert!(!blocks_to_persist.iter().any(|b| b.recovered_block().hash() == fork_block_hash));
|
||||
assert!(!plan.blocks.iter().any(|b| b.recovered_block().hash() == fork_block_hash));
|
||||
|
||||
// check that the original block 4 is still included
|
||||
assert!(blocks_to_persist.iter().any(|b| b.recovered_block().number == 4 &&
|
||||
assert!(plan.blocks.iter().any(|b| b.recovered_block().number == 4 &&
|
||||
b.recovered_block().hash() == blocks[4].recovered_block().hash()));
|
||||
|
||||
// check that if we advance persistence, the persistence action is the correct value
|
||||
@@ -1013,11 +1034,193 @@ async fn test_get_canonical_blocks_to_persist() {
|
||||
assert_eq!(
|
||||
test_harness.tree.persistence_state.current_action().cloned(),
|
||||
Some(CurrentPersistenceAction::SavingBlocks {
|
||||
highest: blocks_to_persist.last().unwrap().recovered_block().num_hash()
|
||||
highest: plan.blocks.last().unwrap().recovered_block().num_hash()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_save_blocks_plan_with_deferred_trie_blocks() {
|
||||
let chain_spec = MAINNET.clone();
|
||||
let mut test_harness = TestHarness::new(chain_spec);
|
||||
let mut test_block_builder = TestBlockBuilder::eth();
|
||||
|
||||
let blocks: Vec<_> = test_block_builder.get_executed_blocks(0..7).collect();
|
||||
test_harness = test_harness.with_blocks(blocks.clone());
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block =
|
||||
blocks[1].recovered_block().num_hash();
|
||||
test_harness.tree.persistence_state.last_persisted_block =
|
||||
blocks[3].recovered_block().num_hash();
|
||||
test_harness.tree.config = TreeConfig::default()
|
||||
.with_persistence_threshold(4)
|
||||
.with_memory_block_buffer_target(1)
|
||||
.with_num_state_masking_blocks(2);
|
||||
|
||||
let plan = test_harness.tree.get_save_blocks_plan(PersistTarget::Threshold).unwrap();
|
||||
|
||||
assert_plan_steps(&plan, &[(0..2, Some(2..4), false), (2..4, None, true)]);
|
||||
assert_eq!(plan.blocks.len(), 4);
|
||||
assert_eq!(
|
||||
plan.blocks.iter().map(|block| block.recovered_block().number()).collect::<Vec<_>>(),
|
||||
vec![2, 3, 4, 5]
|
||||
);
|
||||
assert_eq!(plan.last_block(), Some(blocks[5].recovered_block().num_hash()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_save_blocks_plan_persists_full_region_before_deferred_tail() {
|
||||
let chain_spec = MAINNET.clone();
|
||||
let mut test_harness = TestHarness::new(chain_spec);
|
||||
let mut test_block_builder = TestBlockBuilder::eth();
|
||||
|
||||
let blocks: Vec<_> = test_block_builder.get_executed_blocks(0..31).collect();
|
||||
test_harness = test_harness.with_blocks(blocks.clone());
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block =
|
||||
blocks[12].recovered_block().num_hash();
|
||||
test_harness.tree.persistence_state.last_persisted_block =
|
||||
blocks[15].recovered_block().num_hash();
|
||||
test_harness.tree.config = TreeConfig::default()
|
||||
.with_persistence_threshold(5)
|
||||
.with_memory_block_buffer_target(2)
|
||||
.with_num_state_masking_blocks(2);
|
||||
|
||||
let plan = test_harness.tree.get_save_blocks_plan(PersistTarget::Threshold).unwrap();
|
||||
|
||||
assert_plan_steps(
|
||||
&plan,
|
||||
&[(0..3, Some(14..16), false), (3..14, Some(14..16), true), (14..16, None, true)],
|
||||
);
|
||||
assert_eq!(plan.blocks.len(), 16);
|
||||
assert_eq!(
|
||||
plan.blocks.iter().map(|block| block.recovered_block().number()).collect::<Vec<_>>(),
|
||||
(13..=28).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(plan.last_block(), Some(blocks[28].recovered_block().num_hash()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_persistence_complete_retains_blocks_above_partial_state_trie() {
|
||||
let chain_spec = MAINNET.clone();
|
||||
let mut test_harness = TestHarness::new(chain_spec);
|
||||
let mut test_block_builder = TestBlockBuilder::eth();
|
||||
|
||||
let blocks: Vec<_> = test_block_builder.get_executed_blocks(0..7).collect();
|
||||
test_harness = test_harness.with_blocks(blocks.clone());
|
||||
test_harness.tree.persistence_state.last_persisted_block =
|
||||
blocks[1].recovered_block().num_hash();
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block =
|
||||
blocks[1].recovered_block().num_hash();
|
||||
|
||||
let persisted_tip = blocks[5].recovered_block().num_hash();
|
||||
let last_state_trie_block = blocks[3].recovered_block().number();
|
||||
|
||||
test_harness
|
||||
.tree
|
||||
.on_persistence_complete(
|
||||
PersistenceResult {
|
||||
last_block: Some(persisted_tip),
|
||||
last_state_trie_block: Some(last_state_trie_block),
|
||||
commit_duration: Some(Duration::ZERO),
|
||||
},
|
||||
Instant::now(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(test_harness.tree.persistence_state.last_persisted_block, persisted_tip);
|
||||
assert_eq!(
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block,
|
||||
blocks[3].recovered_block().num_hash()
|
||||
);
|
||||
assert_eq!(
|
||||
test_harness.tree.canonical_in_memory_state.get_persisted_num_hash(),
|
||||
Some(persisted_tip)
|
||||
);
|
||||
|
||||
for block in &blocks[..=last_state_trie_block as usize] {
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.state
|
||||
.tree_state
|
||||
.executed_block_by_hash(block.recovered_block().hash())
|
||||
.is_none());
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.canonical_in_memory_state
|
||||
.state_by_number(block.recovered_block().number())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
for block in &blocks[last_state_trie_block as usize + 1..] {
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.state
|
||||
.tree_state
|
||||
.executed_block_by_hash(block.recovered_block().hash())
|
||||
.is_some());
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.canonical_in_memory_state
|
||||
.state_by_number(block.recovered_block().number())
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_persistence_complete_without_partial_state_trie_prunes_through_tip() {
|
||||
let chain_spec = MAINNET.clone();
|
||||
let mut test_harness = TestHarness::new(chain_spec);
|
||||
let mut test_block_builder = TestBlockBuilder::eth();
|
||||
|
||||
let blocks: Vec<_> = test_block_builder.get_executed_blocks(0..7).collect();
|
||||
test_harness = test_harness.with_blocks(blocks.clone());
|
||||
test_harness.tree.persistence_state.last_persisted_block =
|
||||
blocks[1].recovered_block().num_hash();
|
||||
test_harness.tree.persistence_state.last_state_trie_persisted_block =
|
||||
blocks[1].recovered_block().num_hash();
|
||||
|
||||
let persisted_tip = blocks[5].recovered_block().num_hash();
|
||||
|
||||
test_harness
|
||||
.tree
|
||||
.on_persistence_complete(
|
||||
PersistenceResult {
|
||||
last_block: Some(persisted_tip),
|
||||
last_state_trie_block: None,
|
||||
commit_duration: Some(Duration::ZERO),
|
||||
},
|
||||
Instant::now(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for block in &blocks[..=persisted_tip.number as usize] {
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.state
|
||||
.tree_state
|
||||
.executed_block_by_hash(block.recovered_block().hash())
|
||||
.is_none());
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.canonical_in_memory_state
|
||||
.state_by_number(block.recovered_block().number())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
for block in &blocks[persisted_tip.number as usize + 1..] {
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.state
|
||||
.tree_state
|
||||
.executed_block_by_hash(block.recovered_block().hash())
|
||||
.is_some());
|
||||
assert!(test_harness
|
||||
.tree
|
||||
.canonical_in_memory_state
|
||||
.state_by_number(block.recovered_block().number())
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_engine_tree_fcu_missing_head() {
|
||||
let chain_spec = MAINNET.clone();
|
||||
@@ -2112,15 +2315,18 @@ mod forkchoice_updated_tests {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(PersistenceAction::SaveBlocks(saved_blocks, sender)) =
|
||||
if let Ok(PersistenceAction::SaveBlocks(plan, sender)) =
|
||||
action_rx.recv_timeout(std::time::Duration::from_millis(100))
|
||||
{
|
||||
if let Some(last) = saved_blocks.last() {
|
||||
if let Some(last) = plan.last_block() {
|
||||
last_persisted_number = last.number;
|
||||
} else if let Some(last) = plan.blocks.last() {
|
||||
last_persisted_number = last.recovered_block().number;
|
||||
}
|
||||
sender
|
||||
.send(PersistenceResult {
|
||||
last_block: saved_blocks.last().map(|b| b.recovered_block().num_hash()),
|
||||
last_block: plan.last_block(),
|
||||
last_state_trie_block: plan.last_block().map(|tip| tip.number),
|
||||
commit_duration: Some(Duration::ZERO),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -287,7 +287,7 @@ where
|
||||
let tx_recovered =
|
||||
tx.try_into_recovered().map_err(|_| ProviderError::SenderRecoveryError)?;
|
||||
let gas_used = match builder.execute_transaction(tx_recovered) {
|
||||
Ok(gas_used) => gas_used,
|
||||
Ok(gas_used) => gas_used.tx_gas_used(),
|
||||
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
|
||||
hash,
|
||||
error,
|
||||
|
||||
@@ -342,7 +342,7 @@ where
|
||||
let tx_hash = *tx.tx_hash();
|
||||
|
||||
let gas_used = match builder.execute_transaction(tx) {
|
||||
Ok(gas_used) => gas_used,
|
||||
Ok(gas_used) => gas_used.tx_gas_used(),
|
||||
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
|
||||
error, ..
|
||||
})) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{ConfigureEvm, Database, OnStateHook, TxEnvFor};
|
||||
use alloc::{boxed::Box, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::{BlockHeader, Header};
|
||||
use alloy_eips::eip2718::WithEncoded;
|
||||
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory};
|
||||
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory, GasOutput};
|
||||
use alloy_evm::{
|
||||
block::{CommitChanges, ExecutableTxParts},
|
||||
Evm, EvmEnv, EvmFactory, RecoveredTx, ToTxEnv,
|
||||
@@ -327,7 +327,7 @@ pub trait BlockBuilder {
|
||||
&mut self,
|
||||
tx: impl ExecutorTx<Self::Executor>,
|
||||
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
|
||||
) -> Result<Option<u64>, BlockExecutionError>;
|
||||
) -> Result<Option<GasOutput>, BlockExecutionError>;
|
||||
|
||||
/// Invokes [`BlockExecutor::execute_transaction_with_result_closure`] and saves the
|
||||
/// transaction in internal state.
|
||||
@@ -335,7 +335,7 @@ pub trait BlockBuilder {
|
||||
&mut self,
|
||||
tx: impl ExecutorTx<Self::Executor>,
|
||||
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result),
|
||||
) -> Result<u64, BlockExecutionError> {
|
||||
) -> Result<GasOutput, BlockExecutionError> {
|
||||
self.execute_transaction_with_commit_condition(tx, |res| {
|
||||
f(res);
|
||||
CommitChanges::Yes
|
||||
@@ -348,7 +348,7 @@ pub trait BlockBuilder {
|
||||
fn execute_transaction(
|
||||
&mut self,
|
||||
tx: impl ExecutorTx<Self::Executor>,
|
||||
) -> Result<u64, BlockExecutionError> {
|
||||
) -> Result<GasOutput, BlockExecutionError> {
|
||||
self.execute_transaction_with_result_closure(tx, |_| ())
|
||||
}
|
||||
|
||||
@@ -460,13 +460,13 @@ where
|
||||
&mut self,
|
||||
tx: impl ExecutorTx<Self::Executor>,
|
||||
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
|
||||
) -> Result<Option<u64>, BlockExecutionError> {
|
||||
) -> Result<Option<GasOutput>, BlockExecutionError> {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
if let Some(gas_used) =
|
||||
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
|
||||
{
|
||||
self.transactions.push(tx);
|
||||
Ok(Some(gas_used.tx_gas_used()))
|
||||
Ok(Some(gas_used))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ use reth_node_metrics::{
|
||||
};
|
||||
use reth_provider::{
|
||||
providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
|
||||
BlockHashReader, BlockNumReader, ProviderError, ProviderFactory, ProviderResult,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
|
||||
BlockHashReader, BlockNumReader, DatabaseProviderFactory, ProviderError, ProviderFactory,
|
||||
ProviderResult, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
|
||||
StaticFileProviderFactory,
|
||||
};
|
||||
use reth_prune::{PruneModes, PrunerBuilder};
|
||||
@@ -75,7 +75,7 @@ use reth_rpc_builder::config::RethRpcServerConfig;
|
||||
use reth_rpc_layer::JwtSecret;
|
||||
use reth_stages::{
|
||||
sets::DefaultStages, stages::EraImportSource, MetricEvent, PipelineBuilder, PipelineTarget,
|
||||
StageId,
|
||||
StageCheckpoint, StageId,
|
||||
};
|
||||
use reth_static_file::StaticFileProducer;
|
||||
use reth_tasks::TaskExecutor;
|
||||
@@ -518,19 +518,26 @@ where
|
||||
// the unwind targets for each storage layer if inconsistencies are
|
||||
// found.
|
||||
let (rocksdb_unwind, static_file_unwind) = factory.check_consistency()?;
|
||||
let partial_trie_unwind = partial_trie_unwind_target(
|
||||
factory.database_provider_ro()?.get_stage_checkpoint(StageId::Finish)?,
|
||||
);
|
||||
|
||||
// Take the minimum block number to ensure all storage layers are consistent.
|
||||
let unwind_target = [rocksdb_unwind, static_file_unwind].into_iter().flatten().min();
|
||||
let unwind_target =
|
||||
[rocksdb_unwind, static_file_unwind, partial_trie_unwind].into_iter().flatten().min();
|
||||
|
||||
if let Some(unwind_block) = unwind_target {
|
||||
let inconsistency_source = [
|
||||
rocksdb_unwind.map(|_| "RocksDB"),
|
||||
static_file_unwind.map(|_| "static file"),
|
||||
partial_trie_unwind.map(|_| "partial state trie"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" and ");
|
||||
// Highly unlikely to happen, and given its destructive nature, it's better to panic
|
||||
// instead. Unwinding to 0 would leave MDBX with a huge free list size.
|
||||
let inconsistency_source = match (rocksdb_unwind, static_file_unwind) {
|
||||
(Some(_), Some(_)) => "RocksDB and static file",
|
||||
(Some(_), None) => "RocksDB",
|
||||
(None, Some(_)) => "static file",
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
assert_ne!(
|
||||
unwind_block, 0,
|
||||
"A {} inconsistency was found that would trigger an unwind to block 0",
|
||||
@@ -1269,11 +1276,19 @@ pub fn metrics_hooks<N: NodeTypesWithDB>(provider_factory: &ProviderFactory<N>)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn partial_trie_unwind_target(finish_checkpoint: Option<StageCheckpoint>) -> Option<BlockNumber> {
|
||||
let finish_checkpoint = finish_checkpoint?;
|
||||
let partial_state_trie = finish_checkpoint.finish_stage_checkpoint()?.partial_state_trie?;
|
||||
|
||||
(partial_state_trie != finish_checkpoint.block_number).then_some(partial_state_trie)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{LaunchContext, NodeConfig};
|
||||
use super::{partial_trie_unwind_target, LaunchContext, NodeConfig};
|
||||
use reth_config::Config;
|
||||
use reth_node_core::args::PruningArgs;
|
||||
use reth_stages::{FinishCheckpoint, StageCheckpoint};
|
||||
|
||||
const EXTENSION: &str = "toml";
|
||||
|
||||
@@ -1325,4 +1340,24 @@ mod tests {
|
||||
assert_eq!(reth_config, loaded_config);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_trie_unwind_target_uses_partial_finish_checkpoint() {
|
||||
let finish_checkpoint = StageCheckpoint::new(42)
|
||||
.with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(21) });
|
||||
|
||||
assert_eq!(partial_trie_unwind_target(Some(finish_checkpoint)), Some(21));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_trie_unwind_target_ignores_matching_or_missing_partial_checkpoint() {
|
||||
let matching_finish_checkpoint = StageCheckpoint::new(42)
|
||||
.with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(42) });
|
||||
let missing_partial_finish_checkpoint = StageCheckpoint::new(42)
|
||||
.with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: None });
|
||||
|
||||
assert_eq!(partial_trie_unwind_target(Some(matching_finish_checkpoint)), None);
|
||||
assert_eq!(partial_trie_unwind_target(Some(missing_partial_finish_checkpoint)), None);
|
||||
assert_eq!(partial_trie_unwind_target(None), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ use clap::{builder::Resettable, Args};
|
||||
use eyre::ensure;
|
||||
use reth_cli_util::{parse_duration_from_secs_or_ms, parsers::format_duration_as_secs_or_ms};
|
||||
use reth_engine_primitives::{
|
||||
TreeConfig, DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD, DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
|
||||
DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD, DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS,
|
||||
DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
|
||||
default_persistence_backpressure_threshold, TreeConfig, DEFAULT_DEFERRED_TRIE_BLOCKS,
|
||||
DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD, DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
|
||||
DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS, DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
|
||||
};
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
|
||||
@@ -24,7 +24,8 @@ static ENGINE_DEFAULTS: OnceLock<DefaultEngineValues> = OnceLock::new();
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DefaultEngineValues {
|
||||
persistence_threshold: u64,
|
||||
persistence_backpressure_threshold: u64,
|
||||
persistence_backpressure_threshold: Option<u64>,
|
||||
deferred_trie_blocks: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
legacy_state_root_task_enabled: bool,
|
||||
@@ -73,9 +74,26 @@ impl DefaultEngineValues {
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the default persistence backpressure threshold.
|
||||
pub const fn persistence_backpressure_threshold(&self) -> u64 {
|
||||
match self.persistence_backpressure_threshold {
|
||||
Some(v) => v,
|
||||
None => default_persistence_backpressure_threshold(
|
||||
self.persistence_threshold,
|
||||
self.memory_block_buffer_target,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the default persistence backpressure threshold
|
||||
pub const fn with_persistence_backpressure_threshold(mut self, v: u64) -> Self {
|
||||
self.persistence_backpressure_threshold = v;
|
||||
self.persistence_backpressure_threshold = Some(v);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default deferred trie block target
|
||||
pub const fn with_deferred_trie_blocks(mut self, v: u64) -> Self {
|
||||
self.deferred_trie_blocks = v;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -261,7 +279,8 @@ impl Default for DefaultEngineValues {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
persistence_backpressure_threshold: DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
persistence_backpressure_threshold: None,
|
||||
deferred_trie_blocks: DEFAULT_DEFERRED_TRIE_BLOCKS,
|
||||
memory_block_buffer_target: DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
invalid_header_hit_eviction_threshold: DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD,
|
||||
legacy_state_root_task_enabled: false,
|
||||
@@ -311,9 +330,14 @@ pub struct EngineArgs {
|
||||
/// Configure the maximum canonical-minus-persisted gap before engine API processing stalls.
|
||||
///
|
||||
/// This value must be greater than `--engine.persistence-threshold`.
|
||||
#[arg(long = "engine.persistence-backpressure-threshold", default_value_t = DefaultEngineValues::get_global().persistence_backpressure_threshold)]
|
||||
#[arg(long = "engine.persistence-backpressure-threshold", default_value_t = DefaultEngineValues::get_global().persistence_backpressure_threshold())]
|
||||
pub persistence_backpressure_threshold: u64,
|
||||
|
||||
/// Configure how many of the blocks being persisted should only mask state/trie writes instead
|
||||
/// of durably persisting their state/trie updates in the current cycle.
|
||||
#[arg(long = "engine.deferred-trie-blocks", default_value_t = DefaultEngineValues::get_global().deferred_trie_blocks)]
|
||||
pub deferred_trie_blocks: u64,
|
||||
|
||||
/// Configure the target number of blocks to keep in memory.
|
||||
#[arg(long = "engine.memory-block-buffer-target", default_value_t = DefaultEngineValues::get_global().memory_block_buffer_target)]
|
||||
pub memory_block_buffer_target: u64,
|
||||
@@ -546,6 +570,7 @@ impl Default for EngineArgs {
|
||||
let DefaultEngineValues {
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
deferred_trie_blocks,
|
||||
memory_block_buffer_target,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
legacy_state_root_task_enabled,
|
||||
@@ -578,7 +603,15 @@ impl Default for EngineArgs {
|
||||
} = DefaultEngineValues::get_global().clone();
|
||||
Self {
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
persistence_backpressure_threshold: persistence_backpressure_threshold.unwrap_or_else(
|
||||
|| {
|
||||
default_persistence_backpressure_threshold(
|
||||
persistence_threshold,
|
||||
memory_block_buffer_target,
|
||||
)
|
||||
},
|
||||
),
|
||||
deferred_trie_blocks,
|
||||
memory_block_buffer_target,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
legacy_state_root_task_enabled,
|
||||
@@ -630,6 +663,13 @@ impl EngineArgs {
|
||||
self.persistence_backpressure_threshold,
|
||||
self.persistence_threshold
|
||||
);
|
||||
ensure!(
|
||||
self.deferred_trie_blocks + self.memory_block_buffer_target < self.persistence_threshold,
|
||||
"--engine.deferred-trie-blocks ({}) + --engine.memory-block-buffer-target ({}) must be less than --engine.persistence-threshold ({})",
|
||||
self.deferred_trie_blocks,
|
||||
self.memory_block_buffer_target,
|
||||
self.persistence_threshold,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -638,6 +678,7 @@ impl EngineArgs {
|
||||
let config = TreeConfig::default()
|
||||
.with_persistence_threshold(self.persistence_threshold)
|
||||
.with_persistence_backpressure_threshold(self.persistence_backpressure_threshold)
|
||||
.with_num_state_masking_blocks(self.deferred_trie_blocks)
|
||||
.with_memory_block_buffer_target(self.memory_block_buffer_target)
|
||||
.with_invalid_header_hit_eviction_threshold(self.invalid_header_hit_eviction_threshold)
|
||||
.with_legacy_state_root(self.legacy_state_root_task_enabled)
|
||||
@@ -695,12 +736,48 @@ mod tests {
|
||||
assert_eq!(args, default_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_engine_values_derive_backpressure_threshold() {
|
||||
let defaults = DefaultEngineValues::default()
|
||||
.with_persistence_threshold(10)
|
||||
.with_memory_block_buffer_target(3);
|
||||
|
||||
assert_eq!(defaults.persistence_backpressure_threshold(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_backpressure_default_override_is_preserved() {
|
||||
let defaults = DefaultEngineValues::default()
|
||||
.with_persistence_backpressure_threshold(99)
|
||||
.with_persistence_threshold(10)
|
||||
.with_memory_block_buffer_target(3);
|
||||
|
||||
assert_eq!(defaults.persistence_backpressure_threshold(), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_args_default_thresholds_match_expected_defaults() {
|
||||
let args = EngineArgs::default();
|
||||
|
||||
assert_eq!(args.persistence_threshold, DEFAULT_PERSISTENCE_THRESHOLD);
|
||||
assert_eq!(args.deferred_trie_blocks, DEFAULT_DEFERRED_TRIE_BLOCKS);
|
||||
assert_eq!(args.memory_block_buffer_target, DEFAULT_MEMORY_BLOCK_BUFFER_TARGET);
|
||||
assert_eq!(
|
||||
args.persistence_backpressure_threshold,
|
||||
default_persistence_backpressure_threshold(
|
||||
args.persistence_threshold,
|
||||
args.memory_block_buffer_target,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn engine_args() {
|
||||
let args = EngineArgs {
|
||||
persistence_threshold: 100,
|
||||
persistence_backpressure_threshold: 101,
|
||||
deferred_trie_blocks: 25,
|
||||
memory_block_buffer_target: 50,
|
||||
invalid_header_hit_eviction_threshold: 7,
|
||||
legacy_state_root_task_enabled: true,
|
||||
@@ -745,6 +822,8 @@ mod tests {
|
||||
"100",
|
||||
"--engine.persistence-backpressure-threshold",
|
||||
"101",
|
||||
"--engine.deferred-trie-blocks",
|
||||
"25",
|
||||
"--engine.memory-block-buffer-target",
|
||||
"50",
|
||||
"--engine.invalid-header-cache-hit-eviction-threshold",
|
||||
@@ -788,6 +867,21 @@ mod tests {
|
||||
assert_eq!(parsed_args, args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_deferred_trie_blocks() {
|
||||
let args = CommandParser::<EngineArgs>::parse_from([
|
||||
"reth",
|
||||
"--engine.persistence-threshold",
|
||||
"8",
|
||||
"--engine.deferred-trie-blocks",
|
||||
"7",
|
||||
])
|
||||
.args;
|
||||
|
||||
assert_eq!(args.deferred_trie_blocks, 7);
|
||||
assert_eq!(args.tree_config().num_state_masking_blocks(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid_backpressure_threshold() {
|
||||
let args = EngineArgs {
|
||||
@@ -801,6 +895,21 @@ mod tests {
|
||||
assert!(err.contains("engine.persistence-threshold"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_state_masking_window_at_or_above_threshold() {
|
||||
let args = EngineArgs {
|
||||
persistence_threshold: 4,
|
||||
deferred_trie_blocks: 2,
|
||||
memory_block_buffer_target: 2,
|
||||
..EngineArgs::default()
|
||||
};
|
||||
|
||||
let err = args.validate().unwrap_err().to_string();
|
||||
assert!(err.contains("engine.deferred-trie-blocks"));
|
||||
assert!(err.contains("engine.memory-block-buffer-target"));
|
||||
assert!(err.contains("engine.persistence-threshold"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_slow_block_threshold() {
|
||||
// Test default value (None - disabled)
|
||||
|
||||
@@ -338,7 +338,7 @@ pub trait LoadPendingBlock:
|
||||
}
|
||||
|
||||
let gas_used = match builder.execute_transaction(tx) {
|
||||
Ok(gas_used) => gas_used,
|
||||
Ok(gas_used) => gas_used.tx_gas_used(),
|
||||
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
|
||||
error,
|
||||
..
|
||||
|
||||
@@ -179,7 +179,7 @@ where
|
||||
|
||||
let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
|
||||
let gas_used = match builder.execute_transaction(tx) {
|
||||
Ok(gas_used) => gas_used,
|
||||
Ok(gas_used) => gas_used.tx_gas_used(),
|
||||
Err(err) => {
|
||||
if skip_invalid_transactions {
|
||||
debug!(
|
||||
|
||||
@@ -295,7 +295,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed, // 1 seeded block body + batch size
|
||||
total // seeded headers
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: false }) if block_number < 200 &&
|
||||
processed == batch_size + 1 && total == previous_stage + 1
|
||||
);
|
||||
@@ -333,7 +334,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true
|
||||
}) if processed + 1 == total && total == previous_stage + 1
|
||||
@@ -370,7 +372,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: false }) if block_number >= 10 &&
|
||||
processed - 1 == batch_size && total == previous_stage + 1
|
||||
);
|
||||
@@ -391,7 +394,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: true }) if block_number > first_run_checkpoint.block_number &&
|
||||
processed + 1 == total && total == previous_stage + 1
|
||||
);
|
||||
@@ -432,7 +436,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: true }) if block_number == previous_stage &&
|
||||
processed + 1 == total && total == previous_stage + 1
|
||||
);
|
||||
@@ -460,7 +465,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed: 1,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}}) if total == previous_stage + 1
|
||||
);
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
output,
|
||||
Ok(ExecOutput {
|
||||
checkpoint: StageCheckpoint { block_number, stage_checkpoint: None },
|
||||
checkpoint: StageCheckpoint { block_number, stage_checkpoint: None, .. },
|
||||
done: false
|
||||
}) if block_number == era_cap
|
||||
);
|
||||
@@ -318,7 +318,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
output,
|
||||
Ok(ExecOutput {
|
||||
checkpoint: StageCheckpoint { block_number, stage_checkpoint: None },
|
||||
checkpoint: StageCheckpoint { block_number, stage_checkpoint: None, .. },
|
||||
done: true
|
||||
}) if block_number == target
|
||||
);
|
||||
|
||||
@@ -1015,7 +1015,8 @@ mod tests {
|
||||
processed,
|
||||
total
|
||||
}
|
||||
}))
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true
|
||||
} if processed == total && total == block.gas_used);
|
||||
@@ -1170,7 +1171,8 @@ mod tests {
|
||||
processed: 0,
|
||||
total
|
||||
}
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}
|
||||
} if total == block.gas_used);
|
||||
|
||||
|
||||
@@ -397,6 +397,7 @@ mod tests {
|
||||
},
|
||||
..
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true,
|
||||
}) if block_number == previous_stage &&
|
||||
|
||||
@@ -594,7 +594,8 @@ mod tests {
|
||||
processed,
|
||||
total,
|
||||
}
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: true }) if block_number == tip.number &&
|
||||
from == checkpoint && to == previous_stage &&
|
||||
// -1 because we don't need to download the local head
|
||||
@@ -666,7 +667,8 @@ mod tests {
|
||||
processed,
|
||||
total,
|
||||
}
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: true }) if block_number == tip.number &&
|
||||
from == checkpoint && to == previous_stage &&
|
||||
// -1 because we don't need to download the local head
|
||||
|
||||
@@ -502,7 +502,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true
|
||||
}) if block_number == previous_stage && processed == total &&
|
||||
@@ -542,7 +543,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true
|
||||
}) if block_number == previous_stage && processed == total &&
|
||||
@@ -584,7 +586,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
})),
|
||||
..
|
||||
},
|
||||
done: true
|
||||
}) if block_number == previous_stage && processed == total &&
|
||||
|
||||
@@ -527,7 +527,8 @@ mod tests {
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed: 1,
|
||||
total: 1
|
||||
}))
|
||||
})),
|
||||
..
|
||||
}, done: true }) if block_number == previous_stage
|
||||
);
|
||||
|
||||
|
||||
@@ -337,12 +337,12 @@ mod tests {
|
||||
result,
|
||||
Ok(ExecOutput {
|
||||
checkpoint: StageCheckpoint {
|
||||
block_number,
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
}, done: true }) if block_number == previous_stage && processed == total &&
|
||||
block_number,
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
}, done: true }) if block_number == previous_stage && processed == total &&
|
||||
total == runner.db.count_entries::<tables::Transactions>().unwrap() as u64
|
||||
);
|
||||
|
||||
@@ -383,12 +383,12 @@ mod tests {
|
||||
result,
|
||||
Ok(ExecOutput {
|
||||
checkpoint: StageCheckpoint {
|
||||
block_number,
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
}, done: true }) if block_number == previous_stage && processed == total &&
|
||||
block_number,
|
||||
stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
|
||||
processed,
|
||||
total
|
||||
}))
|
||||
}, done: true }) if block_number == previous_stage && processed == total &&
|
||||
total == runner.db.count_entries::<tables::Transactions>().unwrap() as u64
|
||||
);
|
||||
|
||||
|
||||
@@ -379,6 +379,9 @@ pub struct StageCheckpoint {
|
||||
pub stage_checkpoint: Option<StageUnitCheckpoint>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "reth-codec"))]
|
||||
reth_codecs::impl_compression_for_compact!(StageCheckpoint);
|
||||
|
||||
impl StageCheckpoint {
|
||||
/// Creates a new [`StageCheckpoint`] with only `block_number` set.
|
||||
pub fn new(block_number: BlockNumber) -> Self {
|
||||
@@ -431,13 +434,21 @@ impl StageCheckpoint {
|
||||
progress: entities,
|
||||
..
|
||||
}) => Some(entities),
|
||||
StageUnitCheckpoint::MerkleChangeSets(_) => None,
|
||||
StageUnitCheckpoint::MerkleChangeSets(_) | StageUnitCheckpoint::Finish(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "reth-codec"))]
|
||||
reth_codecs::impl_compression_for_compact!(StageCheckpoint);
|
||||
/// Saves the progress of the Finish stage.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(any(test, feature = "test-utils"), derive(arbitrary::Arbitrary))]
|
||||
#[cfg_attr(any(test, feature = "reth-codec"), derive(reth_codecs::Compact))]
|
||||
#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct FinishCheckpoint {
|
||||
/// The highest block with a partially persisted state and trie.
|
||||
pub partial_state_trie: Option<BlockNumber>,
|
||||
}
|
||||
|
||||
// TODO(alexey): add a merkle checkpoint. Currently it's hard because [`MerkleCheckpoint`]
|
||||
// is not a Copy type.
|
||||
@@ -465,6 +476,8 @@ pub enum StageUnitCheckpoint {
|
||||
/// Note: This variant is only kept for backward compatibility with the Compact codec.
|
||||
/// The `MerkleChangeSets` stage has been removed.
|
||||
MerkleChangeSets(MerkleChangeSetsCheckpoint),
|
||||
/// Saves the progress of the Finish stage.
|
||||
Finish(FinishCheckpoint),
|
||||
}
|
||||
|
||||
impl StageUnitCheckpoint {
|
||||
@@ -573,6 +586,15 @@ stage_unit_checkpoints!(
|
||||
index_history_stage_checkpoint,
|
||||
/// Sets the stage checkpoint to index history.
|
||||
with_index_history_stage_checkpoint
|
||||
),
|
||||
(
|
||||
6,
|
||||
Finish,
|
||||
FinishCheckpoint,
|
||||
/// Returns the finish stage checkpoint, if any.
|
||||
finish_stage_checkpoint,
|
||||
/// Sets the stage checkpoint to finish.
|
||||
with_finish_stage_checkpoint
|
||||
)
|
||||
);
|
||||
|
||||
@@ -664,4 +686,15 @@ mod tests {
|
||||
let (decoded, _) = MerkleCheckpoint::from_compact(&buf, encoded);
|
||||
assert_eq!(decoded, checkpoint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_checkpoint_roundtrip() {
|
||||
let checkpoint = StageCheckpoint::new(42)
|
||||
.with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(21) });
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let encoded = checkpoint.to_compact(&mut buf);
|
||||
let (decoded, _) = StageCheckpoint::from_compact(&buf, encoded);
|
||||
assert_eq!(decoded, checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub use id::StageId;
|
||||
mod checkpoints;
|
||||
pub use checkpoints::{
|
||||
AccountHashingCheckpoint, CheckpointBlockRange, EntitiesCheckpoint, ExecutionCheckpoint,
|
||||
HeadersCheckpoint, IndexHistoryCheckpoint, MerkleCheckpoint, StageCheckpoint,
|
||||
FinishCheckpoint, HeadersCheckpoint, IndexHistoryCheckpoint, MerkleCheckpoint, StageCheckpoint,
|
||||
StageUnitCheckpoint, StorageHashingCheckpoint, StorageRootMerkleCheckpoint,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ pub mod providers;
|
||||
pub use providers::{
|
||||
DatabaseProvider, DatabaseProviderRO, DatabaseProviderRW, HistoricalStateProvider,
|
||||
HistoricalStateProviderRef, LatestStateProvider, LatestStateProviderRef, ProviderFactory,
|
||||
PruneShardOutcome, PrunedIndices, SaveBlocksMode, StaticFileAccess, StaticFileProviderBuilder,
|
||||
StaticFileWriteCtx, StaticFileWriter,
|
||||
PruneShardOutcome, PrunedIndices, SaveBlocksMode, SaveBlocksPlan, SaveBlocksPlanStep,
|
||||
StaticFileAccess, StaticFileProviderBuilder, StaticFileWriteCtx, StaticFileWriter,
|
||||
};
|
||||
|
||||
pub mod changeset_walker;
|
||||
|
||||
@@ -790,7 +790,8 @@ mod tests {
|
||||
create_test_provider_factory, create_test_provider_factory_with_chain_spec,
|
||||
MockNodeTypesWithDB,
|
||||
},
|
||||
BlockWriter, CanonChainTracker, ProviderFactory, SaveBlocksMode,
|
||||
BlockWriter, CanonChainTracker, ProviderFactory, SaveBlocksMode, SaveBlocksPlan,
|
||||
SaveBlocksPlanStep,
|
||||
};
|
||||
use alloy_eips::{BlockHashOrNumber, BlockNumHash, BlockNumberOrTag};
|
||||
use alloy_primitives::{BlockNumber, TxNumber, B256};
|
||||
@@ -1007,7 +1008,15 @@ mod tests {
|
||||
|
||||
// Push to disk
|
||||
let provider_rw = hook_provider.database_provider_rw().unwrap();
|
||||
provider_rw.save_blocks(vec![lowest_memory_block], SaveBlocksMode::Full).unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&SaveBlocksPlan::new(
|
||||
vec![lowest_memory_block],
|
||||
vec![SaveBlocksPlanStep::new(0..1, Some(1..1), true)],
|
||||
),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
// Remove from memory
|
||||
|
||||
@@ -51,6 +51,9 @@ pub use provider::{
|
||||
CommitOrder, DatabaseProvider, DatabaseProviderRO, DatabaseProviderRW, SaveBlocksMode,
|
||||
};
|
||||
|
||||
mod save_blocks;
|
||||
pub use save_blocks::{SaveBlocksPlan, SaveBlocksPlanStep};
|
||||
|
||||
use super::ProviderNodeTypes;
|
||||
use reth_trie::KeccakKeyHasher;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
use alloy_eips::BlockNumHash;
|
||||
use reth_chain_state::ExecutedBlock;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use std::ops::Range;
|
||||
|
||||
/// A single persistence step over a contiguous region of [`SaveBlocksPlan::blocks`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SaveBlocksPlanStep {
|
||||
/// Range of [`SaveBlocksPlan::blocks`] covered by this step.
|
||||
pub block_range: Range<usize>,
|
||||
/// Optional range of blocks whose state/trie updates should be used to mask this step's
|
||||
/// durable state/trie writes.
|
||||
///
|
||||
/// `Some(empty_range)` means persist state/trie without any masking. `None` means skip
|
||||
/// durable state/trie persistence for this step.
|
||||
pub state_trie_masking_range: Option<Range<usize>>,
|
||||
/// Whether to persist non-state/trie data for this step.
|
||||
pub persist_rest: bool,
|
||||
}
|
||||
|
||||
impl SaveBlocksPlanStep {
|
||||
/// Creates a new persistence step.
|
||||
pub const fn new(
|
||||
block_range: Range<usize>,
|
||||
state_trie_masking_range: Option<Range<usize>>,
|
||||
persist_rest: bool,
|
||||
) -> Self {
|
||||
Self { block_range, state_trie_masking_range, persist_rest }
|
||||
}
|
||||
|
||||
/// Returns `true` if this step persists state/trie data.
|
||||
pub const fn persists_state_trie(&self) -> bool {
|
||||
self.state_trie_masking_range.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Plan for a single `save_blocks` persistence cycle.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SaveBlocksPlan<N: NodePrimitives = EthPrimitives> {
|
||||
/// Canonical blocks covered by this plan.
|
||||
pub blocks: Vec<ExecutedBlock<N>>,
|
||||
/// Ordered persistence steps over [`Self::blocks`].
|
||||
pub steps: Vec<SaveBlocksPlanStep>,
|
||||
}
|
||||
|
||||
impl<N: NodePrimitives> SaveBlocksPlan<N> {
|
||||
/// Creates a new save plan.
|
||||
pub const fn new(blocks: Vec<ExecutedBlock<N>>, steps: Vec<SaveBlocksPlanStep>) -> Self {
|
||||
Self { blocks, steps }
|
||||
}
|
||||
|
||||
/// Returns `true` if the plan contains no blocks to persist.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.last_block().is_none()
|
||||
}
|
||||
|
||||
/// Returns the highest block covered by this plan.
|
||||
pub fn last_block(&self) -> Option<BlockNumHash> {
|
||||
let last_index =
|
||||
self.steps.iter().rev().find_map(|step| step.block_range.end.checked_sub(1))?;
|
||||
self.blocks.get(last_index).map(|block| block.recovered_block().num_hash())
|
||||
}
|
||||
|
||||
/// Returns the highest block whose state/trie data is durably persisted by this plan.
|
||||
pub fn last_state_trie_block(&self) -> Option<BlockNumHash> {
|
||||
let last_index = self
|
||||
.steps
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|step| step.persists_state_trie())?
|
||||
.block_range
|
||||
.end
|
||||
.checked_sub(1)?;
|
||||
self.blocks.get(last_index).map(|block| block.recovered_block().num_hash())
|
||||
}
|
||||
|
||||
/// Returns the contiguous range of blocks whose non-state/trie outputs are persisted.
|
||||
pub fn persist_rest_range(&self) -> Option<Range<usize>> {
|
||||
let mut ranges =
|
||||
self.steps.iter().filter(|step| step.persist_rest).map(|step| &step.block_range);
|
||||
let first = ranges.next()?.clone();
|
||||
let merged = ranges.fold(first, |mut merged, range| {
|
||||
debug_assert_eq!(merged.end, range.start, "persist_rest steps must be contiguous");
|
||||
merged.end = range.end;
|
||||
merged
|
||||
});
|
||||
Some(merged)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use reth_db_api::{
|
||||
transaction::DbTx,
|
||||
BlockNumberList,
|
||||
};
|
||||
use reth_primitives_traits::{Account, Bytecode};
|
||||
use reth_primitives_traits::{Account, Bytecode, NodePrimitives};
|
||||
use reth_storage_api::{
|
||||
BlockNumReader, BytecodeReader, DBProvider, NodePrimitivesProvider, PruneCheckpointReader,
|
||||
StageCheckpointReader, StateProofProvider, StorageChangeSetReader, StorageRootProvider,
|
||||
@@ -33,7 +33,7 @@ use reth_trie_db::{
|
||||
ChangesetCache, DatabaseProof, DatabaseStateRoot, DatabaseStorageProof, DatabaseStorageRoot,
|
||||
};
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
|
||||
|
||||
type DbStateRoot<'a, TX, A> = StateRoot<
|
||||
reth_trie_db::DatabaseTrieCursorFactory<&'a TX, A>,
|
||||
@@ -121,7 +121,13 @@ impl HistoryInfo {
|
||||
/// - [`tables::AccountChangeSets`]
|
||||
/// - [`tables::StorageChangeSets`]
|
||||
#[derive(Debug)]
|
||||
pub struct HistoricalStateProviderRef<'b, Provider> {
|
||||
pub struct HistoricalStateProviderRef<
|
||||
'b,
|
||||
Provider,
|
||||
N: NodePrimitives = <Provider as NodePrimitivesProvider>::Primitives,
|
||||
> where
|
||||
Provider: NodePrimitivesProvider<Primitives = N>,
|
||||
{
|
||||
/// Database provider
|
||||
provider: &'b Provider,
|
||||
/// Changeset cache handle for retrieving trie changesets.
|
||||
@@ -130,10 +136,18 @@ pub struct HistoricalStateProviderRef<'b, Provider> {
|
||||
block_number: BlockNumber,
|
||||
/// Lowest blocks at which different parts of the state are available.
|
||||
lowest_available_blocks: LowestAvailableBlocks,
|
||||
/// Marker for the provider's node primitives.
|
||||
_primitives: PhantomData<N>,
|
||||
}
|
||||
|
||||
impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + BlockNumReader>
|
||||
HistoricalStateProviderRef<'b, Provider>
|
||||
impl<'b, Provider, N> HistoricalStateProviderRef<'b, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Create new `StateProvider` for historical block number
|
||||
pub fn new(
|
||||
@@ -146,6 +160,7 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
changeset_cache,
|
||||
block_number,
|
||||
lowest_available_blocks: Default::default(),
|
||||
_primitives: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +172,13 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
lowest_available_blocks: LowestAvailableBlocks,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self {
|
||||
Self { provider, changeset_cache, block_number, lowest_available_blocks }
|
||||
Self {
|
||||
provider,
|
||||
changeset_cache,
|
||||
block_number,
|
||||
lowest_available_blocks,
|
||||
_primitives: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup an account in the `AccountsHistory` table using `EitherReader`.
|
||||
@@ -282,14 +303,13 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
// Historical providers expose state at the start of `self.block_number`, so the overlay
|
||||
// builder needs the previous canonical block hash to preserve those semantics.
|
||||
let target_block = self.block_number.saturating_sub(1);
|
||||
let block_hash = self
|
||||
let anchor_hash = self
|
||||
.provider
|
||||
.block_hash(target_block)?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(target_block.into()))?;
|
||||
|
||||
let TrieInputSorted { nodes, state, prefix_sets } = input;
|
||||
let overlay_builder = OverlayBuilder::new(self.changeset_cache.clone())
|
||||
.with_block_hash(Some(block_hash))
|
||||
let overlay_builder = OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
|
||||
.with_overlay_source(Some(OverlaySource::Immediate { trie: nodes, state }));
|
||||
let Overlay { trie_updates, hashed_post_state } =
|
||||
overlay_builder.build_overlay(self.provider)?;
|
||||
@@ -316,21 +336,26 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
}
|
||||
}
|
||||
|
||||
impl<Provider: DBProvider + BlockNumReader> HistoricalStateProviderRef<'_, Provider> {
|
||||
impl<Provider, N> HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider + BlockNumReader + NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
fn tx(&self) -> &Provider::Tx {
|
||||
self.provider.tx_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ BlockNumReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider,
|
||||
> AccountReader for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> AccountReader for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ BlockNumReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Get basic account information.
|
||||
fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
|
||||
@@ -358,8 +383,11 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
impl<Provider: DBProvider + BlockNumReader + BlockHashReader> BlockHashReader
|
||||
for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> BlockHashReader for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider:
|
||||
DBProvider + BlockNumReader + BlockHashReader + NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Get block hash by number.
|
||||
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
|
||||
@@ -375,16 +403,18 @@ impl<Provider: DBProvider + BlockNumReader + BlockHashReader> BlockHashReader
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StateRootProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> StateRootProvider for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
@@ -425,16 +455,18 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StorageRootProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> StorageRootProvider for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
fn storage_root(
|
||||
&self,
|
||||
@@ -521,16 +553,18 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StateProofProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> StateProofProvider for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Get account and storage proofs.
|
||||
fn proof(
|
||||
@@ -604,24 +638,29 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
impl<Provider> HashedPostStateProvider for HistoricalStateProviderRef<'_, Provider> {
|
||||
impl<Provider, N> HashedPostStateProvider for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
fn hashed_post_state(&self, bundle_state: &revm_database::BundleState) -> HashedPostState {
|
||||
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider,
|
||||
> StateProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> StateProvider for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Expects a plain (unhashed) storage key slot.
|
||||
fn storage(
|
||||
@@ -633,8 +672,10 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
impl<Provider: DBProvider + BlockNumReader> BytecodeReader
|
||||
for HistoricalStateProviderRef<'_, Provider>
|
||||
impl<Provider, N> BytecodeReader for HistoricalStateProviderRef<'_, Provider, N>
|
||||
where
|
||||
Provider: DBProvider + BlockNumReader + NodePrimitivesProvider<Primitives = N>,
|
||||
N: NodePrimitives,
|
||||
{
|
||||
/// Get account code by its hash
|
||||
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
|
||||
@@ -690,7 +731,16 @@ impl<Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + BlockNumR
|
||||
self.lowest_available_blocks.storage_history_block_number = Some(block_number);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Provider: DBProvider
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ NodePrimitivesProvider,
|
||||
> HistoricalStateProvider<Provider>
|
||||
{
|
||||
/// Returns a new provider that takes the `TX` as reference
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> HistoricalStateProviderRef<'_, Provider> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use alloy_primitives::{BlockNumber, B256};
|
||||
use alloy_eips::BlockNumHash;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, B256};
|
||||
use metrics::{Counter, Histogram};
|
||||
use reth_chain_state::LazyOverlay;
|
||||
use reth_chain_state::{EthPrimitives, LazyOverlay};
|
||||
use reth_db_api::{tables, transaction::DbTx, DatabaseError};
|
||||
use reth_errors::{ProviderError, ProviderResult};
|
||||
use reth_metrics::Metrics;
|
||||
use reth_primitives_traits::dashmap::{self, DashMap};
|
||||
use reth_primitives_traits::{
|
||||
dashmap::{self, DashMap},
|
||||
NodePrimitives,
|
||||
};
|
||||
use reth_prune_types::PruneSegment;
|
||||
use reth_stages_types::StageId;
|
||||
use reth_storage_api::{
|
||||
@@ -24,6 +28,7 @@ use reth_trie_db::{
|
||||
PackedStoragesTrie,
|
||||
};
|
||||
use std::{
|
||||
ops::RangeInclusive,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -61,7 +66,7 @@ pub(super) struct Overlay {
|
||||
/// Either provides immediate pre-computed overlay data, or a lazy overlay that computes
|
||||
/// on first access.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OverlaySource {
|
||||
pub(super) enum OverlaySource<N: NodePrimitives = EthPrimitives> {
|
||||
/// Immediate overlay with already-computed data.
|
||||
Immediate {
|
||||
/// Trie updates overlay.
|
||||
@@ -70,19 +75,7 @@ pub enum OverlaySource {
|
||||
state: Arc<HashedPostStateSorted>,
|
||||
},
|
||||
/// Lazy overlay computed on first access.
|
||||
Lazy(LazyOverlay),
|
||||
}
|
||||
|
||||
impl OverlaySource {
|
||||
/// Resolve the overlay source into (trie, state) tuple.
|
||||
///
|
||||
/// For lazy overlays, this may block waiting for deferred data.
|
||||
fn resolve(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
|
||||
match self {
|
||||
Self::Immediate { trie, state } => (Arc::clone(trie), Arc::clone(state)),
|
||||
Self::Lazy(lazy) => lazy.as_overlay(),
|
||||
}
|
||||
}
|
||||
Lazy(LazyOverlay<N>),
|
||||
}
|
||||
|
||||
/// Builder for calculating trie and hashed-state overlays.
|
||||
@@ -90,54 +83,61 @@ impl OverlaySource {
|
||||
/// This stores the overlay configuration and the logic for resolving immediate/lazy overlays and
|
||||
/// collecting reverts. It is intentionally independent from any provider factory or overlay cache.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OverlayBuilder {
|
||||
/// Optional block hash for collecting reverts
|
||||
block_hash: Option<B256>,
|
||||
pub struct OverlayBuilder<N: NodePrimitives = EthPrimitives> {
|
||||
/// Anchor hash to revert the DB state to before applying overlays.
|
||||
anchor_hash: B256,
|
||||
/// Optional overlay source (lazy or immediate).
|
||||
overlay_source: Option<OverlaySource>,
|
||||
overlay_source: Option<OverlaySource<N>>,
|
||||
/// Changeset cache handle for retrieving trie changesets
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Metrics for tracking provider operations
|
||||
metrics: OverlayStateProviderMetrics,
|
||||
}
|
||||
|
||||
impl OverlayBuilder {
|
||||
impl<N: NodePrimitives> OverlayBuilder<N> {
|
||||
/// Create a new overlay builder.
|
||||
pub fn new(changeset_cache: ChangesetCache) -> Self {
|
||||
pub fn new(anchor_hash: B256, changeset_cache: ChangesetCache) -> Self {
|
||||
Self {
|
||||
block_hash: None,
|
||||
anchor_hash,
|
||||
overlay_source: None,
|
||||
changeset_cache,
|
||||
metrics: OverlayStateProviderMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the block hash for collecting reverts. All state will be reverted to the point
|
||||
/// _after_ this block has been processed.
|
||||
pub const fn with_block_hash(mut self, block_hash: Option<B256>) -> Self {
|
||||
self.block_hash = block_hash;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay source (lazy or immediate).
|
||||
///
|
||||
/// This overlay will be applied on top of any reverts applied via `with_block_hash`.
|
||||
pub fn with_overlay_source(mut self, source: Option<OverlaySource>) -> Self {
|
||||
/// This overlay will be applied on top of any reverts applied via `anchor_hash`.
|
||||
pub(super) fn with_overlay_source(mut self, source: Option<OverlaySource<N>>) -> Self {
|
||||
if let Some(OverlaySource::Lazy(lazy_overlay)) = source.as_ref() {
|
||||
self.assert_lazy_overlay_anchor(lazy_overlay);
|
||||
}
|
||||
self.overlay_source = source;
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_lazy_overlay_anchor(&self, lazy_overlay: &LazyOverlay<N>) {
|
||||
let Some(lazy_overlay_anchor) = lazy_overlay.anchor_hash() else { return };
|
||||
assert!(
|
||||
lazy_overlay_anchor == self.anchor_hash,
|
||||
"LazyOverlay's anchor ({}) != OverlayBuilder's anchor ({})",
|
||||
lazy_overlay_anchor,
|
||||
self.anchor_hash,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a lazy overlay that will be computed on first access.
|
||||
///
|
||||
/// Convenience method that wraps the lazy overlay in `OverlaySource::Lazy`.
|
||||
pub fn with_lazy_overlay(mut self, lazy_overlay: Option<LazyOverlay>) -> Self {
|
||||
/// Panics if the [`LazyOverlay`]'s anchor hash does not match [`Self`]'s `anchor_hash`.
|
||||
pub fn with_lazy_overlay(mut self, lazy_overlay: Option<LazyOverlay<N>>) -> Self {
|
||||
if let Some(lazy_overlay) = lazy_overlay.as_ref() {
|
||||
self.assert_lazy_overlay_anchor(lazy_overlay);
|
||||
}
|
||||
self.overlay_source = lazy_overlay.map(OverlaySource::Lazy);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the hashed state overlay.
|
||||
///
|
||||
/// This overlay will be applied on top of any reverts applied via `with_block_hash`.
|
||||
pub fn with_hashed_state_overlay(
|
||||
mut self,
|
||||
hashed_state_overlay: Option<Arc<HashedPostStateSorted>>,
|
||||
@@ -160,9 +160,9 @@ impl OverlayBuilder {
|
||||
Some(OverlaySource::Immediate { state, .. }) => {
|
||||
Arc::make_mut(state).extend_ref_and_sort(&other);
|
||||
}
|
||||
Some(OverlaySource::Lazy(lazy)) => {
|
||||
Some(OverlaySource::Lazy(overlay)) => {
|
||||
// Resolve lazy overlay and convert to immediate with extension
|
||||
let (trie, mut state) = lazy.as_overlay();
|
||||
let (trie, mut state) = overlay.as_overlay(self.anchor_hash);
|
||||
Arc::make_mut(&mut state).extend_ref_and_sort(&other);
|
||||
self.overlay_source = Some(OverlaySource::Immediate { trie, state });
|
||||
}
|
||||
@@ -180,45 +180,65 @@ impl OverlayBuilder {
|
||||
///
|
||||
/// If an overlay source is set, it is resolved (blocking if lazy).
|
||||
/// Otherwise, returns empty defaults.
|
||||
fn resolve_overlays(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
|
||||
fn resolve_overlays(
|
||||
&self,
|
||||
anchor_hash: BlockHash,
|
||||
) -> ProviderResult<(Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>)> {
|
||||
match &self.overlay_source {
|
||||
Some(source) => source.resolve(),
|
||||
None => {
|
||||
(Arc::new(TrieUpdatesSorted::default()), Arc::new(HashedPostStateSorted::default()))
|
||||
Some(OverlaySource::Lazy(lazy_overlay)) => Ok(lazy_overlay.as_overlay(anchor_hash)),
|
||||
Some(OverlaySource::Immediate { trie, state }) => {
|
||||
if anchor_hash != self.anchor_hash {
|
||||
return Err(ProviderError::other(std::io::Error::other(format!(
|
||||
"anchor_hash {anchor_hash} doesn't match OverlayBuilder's configured anchor ({})",
|
||||
self.anchor_hash
|
||||
))))
|
||||
}
|
||||
Ok((Arc::clone(trie), Arc::clone(state)))
|
||||
}
|
||||
None => Ok((
|
||||
Arc::new(TrieUpdatesSorted::default()),
|
||||
Arc::new(HashedPostStateSorted::default()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the block number for [`Self`]'s `block_hash` field, if any.
|
||||
fn get_requested_block_number<Provider>(
|
||||
&self,
|
||||
provider: &Provider,
|
||||
) -> ProviderResult<Option<BlockNumber>>
|
||||
/// Returns the block number for [`Self`]'s `anchor_hash` field.
|
||||
fn get_block_number<Provider>(&self, provider: &Provider) -> ProviderResult<BlockNumber>
|
||||
where
|
||||
Provider: BlockNumReader,
|
||||
{
|
||||
if let Some(block_hash) = self.block_hash {
|
||||
Ok(Some(
|
||||
provider
|
||||
.convert_hash_or_number(block_hash.into())?
|
||||
.ok_or_else(|| ProviderError::BlockHashNotFound(block_hash))?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
provider
|
||||
.convert_hash_or_number(self.anchor_hash.into())?
|
||||
.ok_or(ProviderError::BlockHashNotFound(self.anchor_hash))
|
||||
}
|
||||
|
||||
/// 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<Provider>(&self, provider: &Provider) -> ProviderResult<BlockNumber>
|
||||
/// Returns the highest blocks whose state/trie data and non-state/trie data are durably
|
||||
/// available in the database.
|
||||
fn get_db_tip_blocks<Provider>(
|
||||
&self,
|
||||
provider: &Provider,
|
||||
) -> ProviderResult<(BlockNumHash, BlockNumHash)>
|
||||
where
|
||||
Provider: StageCheckpointReader,
|
||||
Provider: StageCheckpointReader + BlockNumReader,
|
||||
{
|
||||
provider
|
||||
.get_stage_checkpoint(StageId::Finish)?
|
||||
.as_ref()
|
||||
.map(|chk| chk.block_number)
|
||||
.ok_or_else(|| ProviderError::InsufficientChangesets { requested: 0, available: 0..=0 })
|
||||
let checkpoint = provider.get_stage_checkpoint(StageId::Finish)?.ok_or_else(|| {
|
||||
ProviderError::InsufficientChangesets { requested: 0, available: 0..=0 }
|
||||
})?;
|
||||
let block_number = checkpoint
|
||||
.finish_stage_checkpoint()
|
||||
.and_then(|finish| finish.partial_state_trie)
|
||||
.unwrap_or(checkpoint.block_number);
|
||||
let state_trie_tip_hash = provider
|
||||
.convert_number(block_number.into())?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))?;
|
||||
let finish_tip_number = checkpoint.block_number;
|
||||
let finish_tip_hash = provider
|
||||
.convert_number(finish_tip_number.into())?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(finish_tip_number.into()))?;
|
||||
Ok((
|
||||
BlockNumHash::new(block_number, state_trie_tip_hash),
|
||||
BlockNumHash::new(finish_tip_number, finish_tip_hash),
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns whether or not it is required to collect reverts, and validates that there are
|
||||
@@ -229,18 +249,21 @@ impl OverlayBuilder {
|
||||
fn reverts_required<Provider>(
|
||||
&self,
|
||||
provider: &Provider,
|
||||
db_tip_block: BlockNumber,
|
||||
requested_block: BlockNumber,
|
||||
) -> ProviderResult<bool>
|
||||
state_trie_tip_block: BlockNumHash,
|
||||
finish_tip_block: BlockNumHash,
|
||||
) -> ProviderResult<Option<RangeInclusive<BlockNumber>>>
|
||||
where
|
||||
Provider: PruneCheckpointReader,
|
||||
Provider: BlockNumReader + PruneCheckpointReader,
|
||||
{
|
||||
// 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)
|
||||
// If the anchor is the current durable state/trie frontier then there won't be any
|
||||
// reverts
|
||||
// necessary.
|
||||
if state_trie_tip_block.hash == self.anchor_hash {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
let anchor_number = self.get_block_number(provider)?;
|
||||
|
||||
// Check account history prune checkpoint to determine the lower bound of available data.
|
||||
// The prune checkpoint's block_number is the highest pruned block, so data is available
|
||||
// starting from the next block.
|
||||
@@ -250,30 +273,39 @@ impl OverlayBuilder {
|
||||
.map(|block_number| block_number + 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
let available_range = lower_bound..=db_tip_block;
|
||||
let available_range = lower_bound..=finish_tip_block.number;
|
||||
|
||||
// Check if the requested block is within the available range
|
||||
if !available_range.contains(&requested_block) {
|
||||
if !available_range.contains(&anchor_number) {
|
||||
return Err(ProviderError::InsufficientChangesets {
|
||||
requested: requested_block,
|
||||
requested: anchor_number,
|
||||
available: available_range,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
if anchor_number > state_trie_tip_block.number {
|
||||
return Err(ProviderError::InsufficientChangesets {
|
||||
requested: anchor_number,
|
||||
available: lower_bound..=state_trie_tip_block.number,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(Some(anchor_number + 1..=finish_tip_block.number))
|
||||
}
|
||||
|
||||
/// Calculates a new [`Overlay`] given a transaction and the current db tip.
|
||||
/// Calculates a new [`Overlay`] given a transaction and the current durable state/trie
|
||||
/// frontier.
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
target = "providers::state::overlay",
|
||||
skip_all,
|
||||
fields(%db_tip_block)
|
||||
fields(?state_trie_tip_block, ?finish_tip_block, anchor_hash = ?self.anchor_hash)
|
||||
)]
|
||||
fn calculate_overlay<Provider>(
|
||||
&self,
|
||||
provider: &Provider,
|
||||
db_tip_block: BlockNumber,
|
||||
state_trie_tip_block: BlockNumHash,
|
||||
finish_tip_block: BlockNumHash,
|
||||
) -> ProviderResult<Overlay>
|
||||
where
|
||||
Provider: ChangeSetReader
|
||||
@@ -292,18 +324,13 @@ impl OverlayBuilder {
|
||||
let trie_updates_total_len;
|
||||
let hashed_state_updates_total_len;
|
||||
|
||||
// If block_hash is provided, collect reverts
|
||||
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 any reverts which are required to bring the DB view back to the anchor hash.
|
||||
let (trie_updates, hashed_post_state) = if let Some(revert_blocks) =
|
||||
self.reverts_required(provider, state_trie_tip_block, finish_tip_block)?
|
||||
{
|
||||
debug!(
|
||||
target: "providers::state::overlay",
|
||||
block_hash = ?self.block_hash,
|
||||
from_block,
|
||||
db_tip_block,
|
||||
range_start = from_block + 1,
|
||||
range_end = db_tip_block,
|
||||
?revert_blocks,
|
||||
"Collecting trie reverts for overlay state provider"
|
||||
);
|
||||
|
||||
@@ -317,9 +344,8 @@ impl OverlayBuilder {
|
||||
|
||||
// Use changeset cache to retrieve and accumulate reverts to restore state after
|
||||
// from_block
|
||||
let accumulated_reverts = self
|
||||
.changeset_cache
|
||||
.get_or_compute_range(provider, (from_block + 1)..=db_tip_block)?;
|
||||
let accumulated_reverts =
|
||||
self.changeset_cache.get_or_compute_range(provider, revert_blocks.clone())?;
|
||||
|
||||
retrieve_trie_reverts_duration = start.elapsed();
|
||||
accumulated_reverts
|
||||
@@ -330,14 +356,14 @@ impl OverlayBuilder {
|
||||
let _guard = debug_span!(target: "providers::state::overlay", "retrieving_hashed_state_reverts").entered();
|
||||
|
||||
let start = Instant::now();
|
||||
let res = reth_trie_db::from_reverts_auto(provider, from_block + 1..)?;
|
||||
let res = reth_trie_db::from_reverts_auto(provider, revert_blocks)?;
|
||||
retrieve_hashed_state_reverts_duration = start.elapsed();
|
||||
res
|
||||
};
|
||||
|
||||
// Resolve overlays (lazy or immediate) and extend reverts with them.
|
||||
// If reverts are empty, use overlays directly to avoid cloning.
|
||||
let (overlay_trie, overlay_state) = self.resolve_overlays();
|
||||
let (overlay_trie, overlay_state) = self.resolve_overlays(self.anchor_hash)?;
|
||||
|
||||
let trie_updates = if trie_reverts.is_empty() {
|
||||
overlay_trie
|
||||
@@ -362,8 +388,6 @@ impl OverlayBuilder {
|
||||
|
||||
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,
|
||||
"Reverted to target block",
|
||||
@@ -371,8 +395,9 @@ impl OverlayBuilder {
|
||||
|
||||
(trie_updates, hashed_state_updates)
|
||||
} else {
|
||||
// If no block_hash, use overlays directly (resolving lazy if set)
|
||||
let (trie_updates, hashed_state) = self.resolve_overlays();
|
||||
// If no reverts are needed then the requested anchor is exactly the durable
|
||||
// state/trie frontier. Use overlays directly from that frontier.
|
||||
let (trie_updates, hashed_state) = self.resolve_overlays(state_trie_tip_block.hash)?;
|
||||
|
||||
retrieve_trie_reverts_duration = Duration::ZERO;
|
||||
retrieve_hashed_state_reverts_duration = Duration::ZERO;
|
||||
@@ -407,13 +432,8 @@ impl OverlayBuilder {
|
||||
+ BlockNumReader
|
||||
+ StorageSettingsCache,
|
||||
{
|
||||
if self.block_hash.is_none() {
|
||||
let (trie_updates, hashed_post_state) = self.resolve_overlays();
|
||||
return Ok(Overlay { trie_updates, hashed_post_state })
|
||||
}
|
||||
|
||||
let db_tip_block = self.get_db_tip_block_number(provider)?;
|
||||
self.calculate_overlay(provider, db_tip_block)
|
||||
let (state_trie_tip_block, finish_tip_block) = self.get_db_tip_blocks(provider)?;
|
||||
self.calculate_overlay(provider, state_trie_tip_block, finish_tip_block)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,24 +442,50 @@ impl OverlayBuilder {
|
||||
/// This factory allows building an `OverlayStateProvider` whose DB state has been reverted to a
|
||||
/// particular block, and/or with additional overlay information added on top.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OverlayStateProviderFactory<F> {
|
||||
pub struct OverlayStateProviderFactory<F, N: NodePrimitives = EthPrimitives> {
|
||||
/// The underlying database provider factory
|
||||
factory: F,
|
||||
/// Overlay builder containing the configuration and overlay calculation logic.
|
||||
overlay_builder: OverlayBuilder,
|
||||
/// 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<DashMap<BlockNumber, Overlay>>,
|
||||
overlay_builder: OverlayBuilder<N>,
|
||||
/// A cache which maps `(state_trie_tip_hash, finish_tip_hash) -> Overlay`.
|
||||
///
|
||||
/// Under partial persistence the overlay depends on both the durable trie frontier and the
|
||||
/// fully durable Finish frontier, so both hashes are part of the cache key.
|
||||
overlay_cache: Arc<DashMap<(BlockHash, BlockHash), Overlay>>,
|
||||
}
|
||||
|
||||
impl<F> OverlayStateProviderFactory<F> {
|
||||
impl<F, N: NodePrimitives> OverlayStateProviderFactory<F, N> {
|
||||
/// Create a new overlay state provider factory
|
||||
pub fn new(factory: F, overlay_builder: OverlayBuilder) -> Self {
|
||||
pub fn new(factory: F, overlay_builder: OverlayBuilder<N>) -> Self {
|
||||
Self { factory, overlay_builder, overlay_cache: Default::default() }
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Set a lazy overlay that will be computed on first access.
|
||||
pub fn with_lazy_overlay(mut self, lazy_overlay: Option<LazyOverlay<N>>) -> Self {
|
||||
self.overlay_builder = self.overlay_builder.with_lazy_overlay(lazy_overlay);
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the hashed state overlay.
|
||||
pub fn with_hashed_state_overlay(
|
||||
mut self,
|
||||
hashed_state_overlay: Option<Arc<HashedPostStateSorted>>,
|
||||
) -> Self {
|
||||
self.overlay_builder = self.overlay_builder.with_hashed_state_overlay(hashed_state_overlay);
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
|
||||
/// Extends the existing hashed state overlay with the given [`HashedPostStateSorted`].
|
||||
pub fn with_extended_hashed_state_overlay(mut self, other: HashedPostStateSorted) -> Self {
|
||||
self.overlay_builder = self.overlay_builder.with_extended_hashed_state_overlay(other);
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
|
||||
/// Fetches an [`Overlay`] from the cache based on the current durable frontiers. 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<Provider>(&self, provider: &Provider) -> ProviderResult<Overlay>
|
||||
where
|
||||
@@ -451,32 +497,31 @@ impl<F> OverlayStateProviderFactory<F> {
|
||||
+ BlockNumReader
|
||||
+ StorageSettingsCache,
|
||||
{
|
||||
// No anchor block — just resolve the in-memory overlay directly.
|
||||
if self.overlay_builder.block_hash.is_none() {
|
||||
return self.overlay_builder.build_overlay(provider)
|
||||
}
|
||||
let (state_trie_tip_block, finish_tip_block) =
|
||||
self.overlay_builder.get_db_tip_blocks(provider)?;
|
||||
|
||||
let db_tip_block = self.overlay_builder.get_db_tip_block_number(provider)?;
|
||||
|
||||
let overlay = match self.overlay_cache.entry(db_tip_block) {
|
||||
dashmap::Entry::Occupied(entry) => entry.get().clone(),
|
||||
dashmap::Entry::Vacant(entry) => {
|
||||
self.overlay_builder.metrics.overlay_cache_misses.increment(1);
|
||||
let overlay = self.overlay_builder.build_overlay(provider)?;
|
||||
entry.insert(overlay.clone());
|
||||
overlay
|
||||
}
|
||||
};
|
||||
let overlay =
|
||||
match self.overlay_cache.entry((state_trie_tip_block.hash, finish_tip_block.hash)) {
|
||||
dashmap::Entry::Occupied(entry) => entry.get().clone(),
|
||||
dashmap::Entry::Vacant(entry) => {
|
||||
self.overlay_builder.metrics.overlay_cache_misses.increment(1);
|
||||
let overlay = self.overlay_builder.build_overlay(provider)?;
|
||||
entry.insert(overlay.clone());
|
||||
overlay
|
||||
}
|
||||
};
|
||||
|
||||
Ok(overlay)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> DatabaseProviderROFactory for OverlayStateProviderFactory<F>
|
||||
impl<F, N> DatabaseProviderROFactory for OverlayStateProviderFactory<F, N>
|
||||
where
|
||||
N: NodePrimitives,
|
||||
F: DatabaseProviderFactory,
|
||||
F::Provider: StageCheckpointReader
|
||||
+ PruneCheckpointReader
|
||||
+ DBProvider
|
||||
+ BlockNumReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
@@ -622,3 +667,191 @@ where
|
||||
hashed_cursor_factory.hashed_storage_cursor(hashed_address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
test_utils::create_test_provider_factory, BlockWriter, SaveBlocksMode, SaveBlocksPlan,
|
||||
SaveBlocksPlanStep,
|
||||
};
|
||||
use alloy_primitives::{B256, U256};
|
||||
use reth_chain_state::{test_utils::TestBlockBuilder, ComputedTrieData, ExecutedBlock};
|
||||
use reth_primitives_traits::Account;
|
||||
use reth_stages_types::{FinishCheckpoint, StageCheckpoint};
|
||||
use reth_storage_api::StageCheckpointWriter;
|
||||
use reth_trie::{updates::TrieUpdatesSorted, HashedPostState, HashedStorage};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn full_save_plan(
|
||||
blocks: impl IntoIterator<Item = ExecutedBlock<EthPrimitives>>,
|
||||
) -> SaveBlocksPlan<EthPrimitives> {
|
||||
let blocks = blocks.into_iter().collect::<Vec<_>>();
|
||||
let full_range = 0..blocks.len();
|
||||
SaveBlocksPlan::new(
|
||||
blocks,
|
||||
vec![SaveBlocksPlanStep::new(
|
||||
full_range.clone(),
|
||||
Some(full_range.end..full_range.end),
|
||||
true,
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn partial_save_plan(
|
||||
blocks: impl IntoIterator<Item = ExecutedBlock<EthPrimitives>>,
|
||||
steps: Vec<SaveBlocksPlanStep>,
|
||||
) -> SaveBlocksPlan<EthPrimitives> {
|
||||
SaveBlocksPlan::new(blocks.into_iter().collect(), steps)
|
||||
}
|
||||
|
||||
fn with_unique_state(
|
||||
block: &ExecutedBlock<EthPrimitives>,
|
||||
id: u8,
|
||||
) -> ExecutedBlock<EthPrimitives> {
|
||||
let hashed_address = B256::with_last_byte(id);
|
||||
let hashed_slot = B256::with_last_byte(id.saturating_add(32));
|
||||
let hashed_state = HashedPostState::default()
|
||||
.with_accounts([(hashed_address, Some(Account::default()))])
|
||||
.with_storages([(
|
||||
hashed_address,
|
||||
HashedStorage::from_iter(false, [(hashed_slot, U256::from(id))]),
|
||||
)])
|
||||
.into_sorted();
|
||||
|
||||
ExecutedBlock::new(
|
||||
Arc::clone(&block.recovered_block),
|
||||
Arc::clone(&block.execution_output),
|
||||
ComputedTrieData::without_trie_input(
|
||||
Arc::new(hashed_state),
|
||||
Arc::new(TrieUpdatesSorted::default()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_overlay_uses_partial_trie_frontier_as_lazy_overlay_base() {
|
||||
let factory = create_test_provider_factory();
|
||||
let mut block_builder = TestBlockBuilder::eth();
|
||||
let blocks = block_builder
|
||||
.get_executed_blocks(0..5)
|
||||
.enumerate()
|
||||
.map(|(index, block)| with_unique_state(&block, index as u8 + 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let state_trie_tip = &blocks[1];
|
||||
let finish_tip = &blocks[3];
|
||||
let lazy_overlay_blocks = vec![blocks[4].clone(), blocks[3].clone(), blocks[2].clone()];
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw.insert_block(blocks[0].recovered_block()).unwrap();
|
||||
provider_rw.insert_block(state_trie_tip.recovered_block()).unwrap();
|
||||
provider_rw.insert_block(blocks[2].recovered_block()).unwrap();
|
||||
provider_rw.insert_block(finish_tip.recovered_block()).unwrap();
|
||||
provider_rw
|
||||
.save_stage_checkpoint(
|
||||
StageId::Finish,
|
||||
StageCheckpoint::new(finish_tip.block_number()).with_finish_stage_checkpoint(
|
||||
FinishCheckpoint { partial_state_trie: Some(state_trie_tip.block_number()) },
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider = factory.provider().unwrap();
|
||||
let overlay = OverlayBuilder::<EthPrimitives>::new(
|
||||
state_trie_tip.recovered_block().hash(),
|
||||
ChangesetCache::new(),
|
||||
)
|
||||
.with_lazy_overlay(Some(LazyOverlay::new(lazy_overlay_blocks)))
|
||||
.build_overlay(&provider)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(overlay.hashed_post_state.accounts.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_overlay_rejects_anchor_between_state_trie_frontier_and_finish() {
|
||||
let factory = create_test_provider_factory();
|
||||
let mut block_builder = TestBlockBuilder::eth().with_state();
|
||||
|
||||
let genesis = block_builder.get_executed_blocks(0..1).next().unwrap();
|
||||
let blocks = block_builder.get_executed_blocks(1..4).collect::<Vec<_>>();
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&full_save_plan(std::slice::from_ref(&genesis).to_vec()),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&partial_save_plan(
|
||||
blocks.clone(),
|
||||
vec![
|
||||
SaveBlocksPlanStep::new(0..1, Some(1..3), true),
|
||||
SaveBlocksPlanStep::new(1..3, None, true),
|
||||
],
|
||||
),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider = factory.provider().unwrap();
|
||||
let anchor = blocks[1].recovered_block().hash();
|
||||
let err = OverlayBuilder::<EthPrimitives>::new(anchor, ChangesetCache::new())
|
||||
.with_lazy_overlay(Some(LazyOverlay::new(vec![blocks[2].clone()])))
|
||||
.build_overlay(&provider)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, ProviderError::InsufficientChangesets { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_overlay_rejects_finish_anchor_without_trie_bridge() {
|
||||
let factory = create_test_provider_factory();
|
||||
let mut block_builder = TestBlockBuilder::eth().with_state();
|
||||
|
||||
let genesis = block_builder.get_executed_blocks(0..1).next().unwrap();
|
||||
let blocks = block_builder.get_executed_blocks(1..4).collect::<Vec<_>>();
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&full_save_plan(std::slice::from_ref(&genesis).to_vec()),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw
|
||||
.save_blocks(
|
||||
&partial_save_plan(
|
||||
blocks.clone(),
|
||||
vec![
|
||||
SaveBlocksPlanStep::new(0..1, Some(1..3), true),
|
||||
SaveBlocksPlanStep::new(1..3, None, true),
|
||||
],
|
||||
),
|
||||
SaveBlocksMode::Full,
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider = factory.provider().unwrap();
|
||||
let finish_anchor = blocks[2].recovered_block().hash();
|
||||
|
||||
let err = OverlayBuilder::<EthPrimitives>::new(finish_anchor, ChangesetCache::new())
|
||||
.with_lazy_overlay(None)
|
||||
.build_overlay(&provider)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, ProviderError::InsufficientChangesets { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,14 +696,15 @@ impl<N: NodePrimitives> StaticFileProviderRW<N> {
|
||||
|
||||
/// Updates the `self.reader` internal index.
|
||||
fn update_index(&self) -> ProviderResult<()> {
|
||||
let segment = self.writer.user_header().segment();
|
||||
|
||||
// We find the maximum block of the segment by checking this writer's last block.
|
||||
//
|
||||
// However if there's no block range (because there's no data), we try to calculate it by
|
||||
// subtracting 1 from the expected block start, resulting on the last block of the
|
||||
// previous file.
|
||||
//
|
||||
// If that expected block start is 0, then it means that there's no actual block data, and
|
||||
// there's no block data in static files.
|
||||
// previous file — but only if that file actually exists. If the previous file doesn't
|
||||
// exist (e.g. first-ever file for a segment starting past range boundary), there's
|
||||
// nothing to index.
|
||||
let segment_max_block = self
|
||||
.writer
|
||||
.user_header()
|
||||
@@ -711,12 +712,18 @@ impl<N: NodePrimitives> StaticFileProviderRW<N> {
|
||||
.as_ref()
|
||||
.map(|block_range| block_range.end())
|
||||
.or_else(|| {
|
||||
(self.writer.user_header().expected_block_start() >
|
||||
self.reader().genesis_block_number())
|
||||
.then(|| self.writer.user_header().expected_block_start() - 1)
|
||||
let expected_start = self.writer.user_header().expected_block_start();
|
||||
if expected_start <= self.reader().genesis_block_number() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev_block = expected_start - 1;
|
||||
let prev_range = self.reader().find_fixed_range(segment, prev_block);
|
||||
let prev_path = self.reader().directory().join(segment.filename(&prev_range));
|
||||
prev_path.exists().then_some(prev_block)
|
||||
});
|
||||
|
||||
self.reader().update_index(self.writer.user_header().segment(), segment_max_block)
|
||||
self.reader().update_index(segment, segment_max_block)
|
||||
}
|
||||
|
||||
/// Ensures that the writer is positioned at the specified block number.
|
||||
|
||||
@@ -809,4 +809,78 @@ mod tests {
|
||||
"Should have 7 blocks * 5 changes = 35 rows"
|
||||
);
|
||||
}
|
||||
|
||||
/// Opening a writer for a block past the first range boundary should succeed
|
||||
/// even when no previous static file exists for the segment.
|
||||
#[test]
|
||||
fn test_get_writer_no_previous_file() {
|
||||
let (static_dir, _) = create_test_static_files_dir();
|
||||
let provider = setup_test_provider(&static_dir, 100);
|
||||
|
||||
// Request a writer starting at block 250, which falls into range 200..=299.
|
||||
// No file exists for range 100..=199 (the "previous" range).
|
||||
// This must not panic or error.
|
||||
let mut writer = provider
|
||||
.get_writer(250, StaticFileSegment::AccountChangeSets)
|
||||
.expect("get_writer should succeed without previous file");
|
||||
|
||||
// The index should have no entry for AccountChangeSets yet (empty jar).
|
||||
assert!(
|
||||
provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).is_none(),
|
||||
"Empty jar should not create an index entry"
|
||||
);
|
||||
|
||||
// Writing data requires padding from the range start (200) to block 250,
|
||||
// same as the migration code does.
|
||||
let writer_start = writer.next_block_number();
|
||||
for block in writer_start..250 {
|
||||
writer.append_account_changeset(vec![], block).unwrap();
|
||||
}
|
||||
let changeset = generate_test_changeset(250, 2);
|
||||
writer.append_account_changeset(changeset, 250).unwrap();
|
||||
writer.commit().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(),
|
||||
250,
|
||||
"After writing block 250, highest block should be 250"
|
||||
);
|
||||
}
|
||||
|
||||
/// When a previous file DOES exist, opening a new empty writer for the next
|
||||
/// range should still update the index to point at the previous file.
|
||||
#[test]
|
||||
fn test_get_writer_with_previous_file() {
|
||||
let (static_dir, _) = create_test_static_files_dir();
|
||||
let provider = setup_test_provider(&static_dir, 100);
|
||||
|
||||
// Write blocks 0..=99 to fill the first file completely.
|
||||
{
|
||||
let mut writer = provider.get_writer(0, StaticFileSegment::AccountChangeSets).unwrap();
|
||||
for block in 0..100 {
|
||||
writer.append_account_changeset(generate_test_changeset(block, 1), block).unwrap();
|
||||
}
|
||||
writer.commit().unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(),
|
||||
99
|
||||
);
|
||||
|
||||
// Now get a writer for block 100 (next range 100..=199).
|
||||
// The previous file (0..=99) exists, so this should succeed.
|
||||
let writer = provider
|
||||
.get_writer(100, StaticFileSegment::AccountChangeSets)
|
||||
.expect("get_writer should succeed with previous file");
|
||||
|
||||
// The index should still reflect the previous file's max block.
|
||||
assert_eq!(
|
||||
provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(),
|
||||
99,
|
||||
"Index should still point at previous file's max block"
|
||||
);
|
||||
|
||||
drop(writer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use core::ops::Not;
|
||||
use crate::{
|
||||
added_removed_keys::MultiAddedRemovedKeys,
|
||||
prefix_set::{PrefixSetMut, TriePrefixSetsMut},
|
||||
utils::{extend_sorted_vec, kway_merge_sorted},
|
||||
utils::{extend_sorted_vec, kway_merge_disjoint_sorted, kway_merge_sorted},
|
||||
KeyHasher, MultiProofTargets, Nibbles,
|
||||
};
|
||||
use alloc::{borrow::Cow, vec::Vec};
|
||||
@@ -691,6 +691,100 @@ impl HashedPostStateSorted {
|
||||
Self { accounts, storages }
|
||||
}
|
||||
|
||||
/// Merges the batch and removes any overlapping keys present in the mask.
|
||||
///
|
||||
/// Account keys are masked at the top level, while storage entries are only masked at the slot
|
||||
/// level unless the mask wipes the entire storage. For duplicate keys in the batch, later
|
||||
/// items take precedence over earlier ones. The order of the mask does not matter.
|
||||
pub fn disjointed_merge_batch<'a>(batch: Vec<&'a Self>, mask: Vec<&'a Self>) -> Self {
|
||||
let accounts = kway_merge_disjoint_sorted(
|
||||
batch.iter().map(|item| item.accounts.len()).sum(),
|
||||
batch.iter().rev().map(|item| item.accounts.as_slice()),
|
||||
mask.iter().map(|item| item.accounts.as_slice()),
|
||||
);
|
||||
|
||||
struct StorageAcc<'a> {
|
||||
wiped: bool,
|
||||
sealed: bool,
|
||||
slot_count: usize,
|
||||
slices: Vec<&'a [(B256, U256)]>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StorageMaskAcc<'a> {
|
||||
wiped: bool,
|
||||
slices: Vec<&'a [(B256, U256)]>,
|
||||
}
|
||||
|
||||
let mut storages = B256Map::with_capacity_and_hasher(
|
||||
batch.iter().map(|item| item.storages.len()).sum(),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
for item in batch.iter().rev() {
|
||||
for (hashed_address, storage) in &item.storages {
|
||||
let entry = storages.entry(*hashed_address).or_insert_with(|| StorageAcc {
|
||||
wiped: false,
|
||||
sealed: false,
|
||||
slot_count: 0,
|
||||
slices: Vec::new(),
|
||||
});
|
||||
|
||||
if entry.sealed {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.slices.push(storage.storage_slots.as_slice());
|
||||
entry.slot_count += storage.storage_slots.len();
|
||||
if storage.wiped {
|
||||
entry.wiped = true;
|
||||
entry.sealed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut storage_masks: B256Map<StorageMaskAcc<'a>> = B256Map::with_capacity_and_hasher(
|
||||
mask.iter().map(|item| item.storages.len()).sum(),
|
||||
Default::default(),
|
||||
);
|
||||
for item in mask {
|
||||
for (hashed_address, storage) in &item.storages {
|
||||
let entry = storage_masks.entry(*hashed_address).or_default();
|
||||
if entry.wiped {
|
||||
continue;
|
||||
}
|
||||
if storage.wiped {
|
||||
entry.wiped = true;
|
||||
entry.slices.clear();
|
||||
} else {
|
||||
entry.slices.push(storage.storage_slots.as_slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let storages = storages
|
||||
.into_iter()
|
||||
.filter_map(|(hashed_address, entry)| {
|
||||
let storage_slots = match storage_masks.get(&hashed_address) {
|
||||
Some(mask_entry) if mask_entry.wiped => return None,
|
||||
Some(mask_entry) => kway_merge_disjoint_sorted(
|
||||
entry.slot_count,
|
||||
entry.slices,
|
||||
mask_entry.slices.iter().copied(),
|
||||
),
|
||||
None => kway_merge_sorted(entry.slices),
|
||||
};
|
||||
|
||||
(!storage_slots.is_empty() || entry.wiped).then_some((
|
||||
hashed_address,
|
||||
HashedStorageSorted { wiped: entry.wiped, storage_slots },
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self { accounts, storages }
|
||||
}
|
||||
|
||||
/// Clears all accounts and storage data.
|
||||
pub fn clear(&mut self) {
|
||||
self.accounts.clear();
|
||||
@@ -1534,6 +1628,152 @@ mod tests {
|
||||
assert_eq!(state.accounts.get(&addr1), Some(&None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashed_post_state_sorted_disjointed_merge_batch() {
|
||||
fn account(nonce: u64) -> Account {
|
||||
Account { nonce, balance: U256::ZERO, bytecode_hash: None }
|
||||
}
|
||||
|
||||
let kept_account = B256::with_last_byte(1);
|
||||
let removed_account = B256::with_last_byte(2);
|
||||
let kept_storage = B256::with_last_byte(3);
|
||||
let removed_storage = B256::with_last_byte(4);
|
||||
let slot1 = B256::with_last_byte(11);
|
||||
let slot2 = B256::with_last_byte(12);
|
||||
|
||||
let older = HashedPostStateSorted::new(
|
||||
vec![(kept_account, Some(account(1))), (removed_account, Some(account(10)))],
|
||||
B256Map::from_iter([
|
||||
(
|
||||
kept_storage,
|
||||
HashedStorageSorted {
|
||||
wiped: false,
|
||||
storage_slots: vec![(slot1, U256::from(1))],
|
||||
},
|
||||
),
|
||||
(
|
||||
removed_storage,
|
||||
HashedStorageSorted {
|
||||
wiped: false,
|
||||
storage_slots: vec![(slot1, U256::from(2))],
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
let newer = HashedPostStateSorted::new(
|
||||
vec![(kept_account, Some(account(2)))],
|
||||
B256Map::from_iter([(
|
||||
kept_storage,
|
||||
HashedStorageSorted {
|
||||
wiped: false,
|
||||
storage_slots: vec![(slot1, U256::from(3)), (slot2, U256::from(4))],
|
||||
},
|
||||
)]),
|
||||
);
|
||||
|
||||
let remove_a = HashedPostStateSorted::new(
|
||||
vec![(removed_account, None)],
|
||||
B256Map::from_iter([
|
||||
(
|
||||
kept_storage,
|
||||
HashedStorageSorted { wiped: false, storage_slots: vec![(slot2, U256::ZERO)] },
|
||||
),
|
||||
(removed_storage, HashedStorageSorted { wiped: true, storage_slots: vec![] }),
|
||||
]),
|
||||
);
|
||||
|
||||
let remove_b = HashedPostStateSorted::new(
|
||||
vec![(B256::with_last_byte(255), Some(account(99)))],
|
||||
B256Map::default(),
|
||||
);
|
||||
|
||||
let result = HashedPostStateSorted::disjointed_merge_batch(
|
||||
vec![&older, &newer],
|
||||
vec![&remove_b, &remove_a],
|
||||
);
|
||||
|
||||
assert_eq!(result.accounts, vec![(kept_account, Some(account(2)))]);
|
||||
assert_eq!(result.storages.len(), 1);
|
||||
assert_eq!(
|
||||
result.storages.get(&kept_storage),
|
||||
Some(&HashedStorageSorted {
|
||||
wiped: false,
|
||||
storage_slots: vec![(slot1, U256::from(3))],
|
||||
})
|
||||
);
|
||||
assert!(!result.storages.contains_key(&removed_storage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashed_post_state_sorted_disjointed_merge_batch_removes_overlapping_batch_key() {
|
||||
fn account(nonce: u64) -> Account {
|
||||
Account { nonce, balance: U256::ZERO, bytecode_hash: None }
|
||||
}
|
||||
|
||||
let overlapping_account = B256::with_last_byte(21);
|
||||
let overlapping_storage = B256::with_last_byte(22);
|
||||
let slot = B256::with_last_byte(23);
|
||||
|
||||
let older = HashedPostStateSorted::new(
|
||||
vec![(overlapping_account, Some(account(1)))],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
HashedStorageSorted { wiped: false, storage_slots: vec![(slot, U256::from(1))] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let newer = HashedPostStateSorted::new(
|
||||
vec![(overlapping_account, Some(account(2)))],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
HashedStorageSorted { wiped: false, storage_slots: vec![(slot, U256::from(2))] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let remove = HashedPostStateSorted::new(
|
||||
vec![(overlapping_account, None)],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
HashedStorageSorted { wiped: true, storage_slots: vec![] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let result =
|
||||
HashedPostStateSorted::disjointed_merge_batch(vec![&older, &newer], vec![&remove]);
|
||||
|
||||
assert!(result.accounts.is_empty());
|
||||
assert!(result.storages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashed_post_state_sorted_disjointed_merge_batch_ignores_empty_storage_mask() {
|
||||
let storage = B256::with_last_byte(31);
|
||||
let slot = B256::with_last_byte(32);
|
||||
|
||||
let batch = HashedPostStateSorted::new(
|
||||
vec![],
|
||||
B256Map::from_iter([(
|
||||
storage,
|
||||
HashedStorageSorted { wiped: false, storage_slots: vec![(slot, U256::from(1))] },
|
||||
)]),
|
||||
);
|
||||
let mask = HashedPostStateSorted::new(
|
||||
vec![],
|
||||
B256Map::from_iter([(
|
||||
storage,
|
||||
HashedStorageSorted { wiped: false, storage_slots: vec![] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let result = HashedPostStateSorted::disjointed_merge_batch(vec![&batch], vec![&mask]);
|
||||
|
||||
assert_eq!(
|
||||
result.storages.get(&storage),
|
||||
Some(&HashedStorageSorted { wiped: false, storage_slots: vec![(slot, U256::from(1))] })
|
||||
);
|
||||
}
|
||||
|
||||
/// Test non-wiped storage merges both zero and non-zero valued slots
|
||||
#[test]
|
||||
fn test_hashed_storage_extend_from_sorted_non_wiped() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
utils::{extend_sorted_vec, kway_merge_sorted},
|
||||
utils::{extend_sorted_vec, kway_merge_disjoint_sorted, kway_merge_sorted},
|
||||
BranchNodeCompact, HashBuilder, Nibbles,
|
||||
};
|
||||
use alloc::{
|
||||
@@ -710,6 +710,101 @@ impl TrieUpdatesSorted {
|
||||
|
||||
Self { account_nodes, storage_tries }
|
||||
}
|
||||
|
||||
/// Merges the batch and removes any overlapping keys present in the mask.
|
||||
///
|
||||
/// Account trie nodes are masked at the top level, while storage trie entries are only masked
|
||||
/// at the node level unless the mask deletes the entire storage trie. For duplicate keys in
|
||||
/// the batch, later items take precedence over earlier ones. The order of the mask does not
|
||||
/// matter.
|
||||
pub fn disjointed_merge_batch<'a>(batch: Vec<&'a Self>, mask: Vec<&'a Self>) -> Self {
|
||||
let account_nodes = kway_merge_disjoint_sorted(
|
||||
batch.iter().map(|item| item.account_nodes.len()).sum(),
|
||||
batch.iter().rev().map(|item| item.account_nodes.as_slice()),
|
||||
mask.iter().map(|item| item.account_nodes.as_slice()),
|
||||
);
|
||||
|
||||
struct StorageAcc<'a> {
|
||||
is_deleted: bool,
|
||||
sealed: bool,
|
||||
node_count: usize,
|
||||
slices: Vec<&'a [(Nibbles, Option<BranchNodeCompact>)]>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StorageMaskAcc<'a> {
|
||||
is_deleted: bool,
|
||||
slices: Vec<&'a [(Nibbles, Option<BranchNodeCompact>)]>,
|
||||
}
|
||||
|
||||
let mut storage_tries = B256Map::with_capacity_and_hasher(
|
||||
batch.iter().map(|item| item.storage_tries.len()).sum(),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
for item in batch.iter().rev() {
|
||||
for (hashed_address, storage_trie) in &item.storage_tries {
|
||||
let entry = storage_tries.entry(*hashed_address).or_insert_with(|| StorageAcc {
|
||||
is_deleted: false,
|
||||
sealed: false,
|
||||
node_count: 0,
|
||||
slices: Vec::new(),
|
||||
});
|
||||
|
||||
if entry.sealed {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.slices.push(storage_trie.storage_nodes.as_slice());
|
||||
entry.node_count += storage_trie.storage_nodes.len();
|
||||
if storage_trie.is_deleted {
|
||||
entry.is_deleted = true;
|
||||
entry.sealed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut storage_masks: B256Map<StorageMaskAcc<'a>> = B256Map::with_capacity_and_hasher(
|
||||
mask.iter().map(|item| item.storage_tries.len()).sum(),
|
||||
Default::default(),
|
||||
);
|
||||
for item in mask {
|
||||
for (hashed_address, storage_trie) in &item.storage_tries {
|
||||
let entry = storage_masks.entry(*hashed_address).or_default();
|
||||
if entry.is_deleted {
|
||||
continue;
|
||||
}
|
||||
if storage_trie.is_deleted {
|
||||
entry.is_deleted = true;
|
||||
entry.slices.clear();
|
||||
} else {
|
||||
entry.slices.push(storage_trie.storage_nodes.as_slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let storage_tries = storage_tries
|
||||
.into_iter()
|
||||
.filter_map(|(hashed_address, entry)| {
|
||||
let storage_nodes = match storage_masks.get(&hashed_address) {
|
||||
Some(mask_entry) if mask_entry.is_deleted => return None,
|
||||
Some(mask_entry) => kway_merge_disjoint_sorted(
|
||||
entry.node_count,
|
||||
entry.slices,
|
||||
mask_entry.slices.iter().copied(),
|
||||
),
|
||||
None => kway_merge_sorted(entry.slices),
|
||||
};
|
||||
|
||||
(!storage_nodes.is_empty() || entry.is_deleted).then_some((
|
||||
hashed_address,
|
||||
StorageTrieUpdatesSorted { is_deleted: entry.is_deleted, storage_nodes },
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::new(account_nodes, storage_tries)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for TrieUpdatesSorted {
|
||||
@@ -977,6 +1072,158 @@ mod tests {
|
||||
assert_eq!(storage3.storage_nodes[1].0, Nibbles::from_nibbles_unchecked([0x07]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trie_updates_sorted_disjointed_merge_batch() {
|
||||
let kept_node = Nibbles::from_nibbles_unchecked([0x01]);
|
||||
let removed_node = Nibbles::from_nibbles_unchecked([0x02]);
|
||||
let kept_storage = B256::from([3; 32]);
|
||||
let removed_storage = B256::from([4; 32]);
|
||||
let slot1 = Nibbles::from_nibbles_unchecked([0x0a]);
|
||||
let slot2 = Nibbles::from_nibbles_unchecked([0x0b]);
|
||||
|
||||
let older = TrieUpdatesSorted::new(
|
||||
vec![(kept_node, Some(BranchNodeCompact::default())), (removed_node, None)],
|
||||
B256Map::from_iter([
|
||||
(
|
||||
kept_storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot1, None)],
|
||||
},
|
||||
),
|
||||
(
|
||||
removed_storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot1, Some(BranchNodeCompact::default()))],
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
let newer = TrieUpdatesSorted::new(
|
||||
vec![(kept_node, None)],
|
||||
B256Map::from_iter([(
|
||||
kept_storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot1, Some(BranchNodeCompact::default())), (slot2, None)],
|
||||
},
|
||||
)]),
|
||||
);
|
||||
|
||||
let remove_a = TrieUpdatesSorted::new(
|
||||
vec![(removed_node, Some(BranchNodeCompact::default()))],
|
||||
B256Map::from_iter([
|
||||
(
|
||||
kept_storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot2, Some(BranchNodeCompact::default()))],
|
||||
},
|
||||
),
|
||||
(
|
||||
removed_storage,
|
||||
StorageTrieUpdatesSorted { is_deleted: true, storage_nodes: vec![] },
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
let remove_b = TrieUpdatesSorted::new(
|
||||
vec![(Nibbles::from_nibbles_unchecked([0x0f]), Some(BranchNodeCompact::default()))],
|
||||
B256Map::default(),
|
||||
);
|
||||
|
||||
let result = TrieUpdatesSorted::disjointed_merge_batch(
|
||||
vec![&older, &newer],
|
||||
vec![&remove_b, &remove_a],
|
||||
);
|
||||
|
||||
assert_eq!(result.account_nodes, vec![(kept_node, None)]);
|
||||
assert_eq!(result.storage_tries.len(), 1);
|
||||
assert_eq!(
|
||||
result.storage_tries.get(&kept_storage),
|
||||
Some(&StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot1, Some(BranchNodeCompact::default()))],
|
||||
})
|
||||
);
|
||||
assert!(!result.storage_tries.contains_key(&removed_storage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trie_updates_sorted_disjointed_merge_batch_removes_overlapping_batch_key() {
|
||||
let overlapping_node = Nibbles::from_nibbles_unchecked([0x03]);
|
||||
let overlapping_storage = B256::from([5; 32]);
|
||||
let slot = Nibbles::from_nibbles_unchecked([0x0c]);
|
||||
|
||||
let older = TrieUpdatesSorted::new(
|
||||
vec![(overlapping_node, Some(BranchNodeCompact::default()))],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot, Some(BranchNodeCompact::default()))],
|
||||
},
|
||||
)]),
|
||||
);
|
||||
|
||||
let newer = TrieUpdatesSorted::new(
|
||||
vec![(overlapping_node, None)],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
StorageTrieUpdatesSorted { is_deleted: false, storage_nodes: vec![(slot, None)] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let remove = TrieUpdatesSorted::new(
|
||||
vec![(overlapping_node, Some(BranchNodeCompact::default()))],
|
||||
B256Map::from_iter([(
|
||||
overlapping_storage,
|
||||
StorageTrieUpdatesSorted { is_deleted: true, storage_nodes: vec![] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let result = TrieUpdatesSorted::disjointed_merge_batch(vec![&older, &newer], vec![&remove]);
|
||||
|
||||
assert!(result.account_nodes.is_empty());
|
||||
assert!(result.storage_tries.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trie_updates_sorted_disjointed_merge_batch_ignores_empty_storage_mask() {
|
||||
let storage = B256::from([6; 32]);
|
||||
let slot = Nibbles::from_nibbles_unchecked([0x0d]);
|
||||
|
||||
let batch = TrieUpdatesSorted::new(
|
||||
vec![],
|
||||
B256Map::from_iter([(
|
||||
storage,
|
||||
StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot, Some(BranchNodeCompact::default()))],
|
||||
},
|
||||
)]),
|
||||
);
|
||||
let mask = TrieUpdatesSorted::new(
|
||||
vec![],
|
||||
B256Map::from_iter([(
|
||||
storage,
|
||||
StorageTrieUpdatesSorted { is_deleted: false, storage_nodes: vec![] },
|
||||
)]),
|
||||
);
|
||||
|
||||
let result = TrieUpdatesSorted::disjointed_merge_batch(vec![&batch], vec![&mask]);
|
||||
|
||||
assert_eq!(
|
||||
result.storage_tries.get(&storage),
|
||||
Some(&StorageTrieUpdatesSorted {
|
||||
is_deleted: false,
|
||||
storage_nodes: vec![(slot, Some(BranchNodeCompact::default()))],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Test extending with storage tries adds both nodes and removed nodes correctly
|
||||
#[test]
|
||||
fn test_trie_updates_extend_from_sorted_with_storage_tries() {
|
||||
|
||||
@@ -26,6 +26,51 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Merge sorted left slices into a sorted `Vec`, excluding keys present in any right slice.
|
||||
///
|
||||
/// Callers pass left slices in priority order (index 0 = highest priority), so the first
|
||||
/// left slice's value for a key takes precedence over later slices. Right slice order is ignored;
|
||||
/// the right-hand side only contributes keys to exclude.
|
||||
pub(crate) fn kway_merge_disjoint_sorted<'a, K, V>(
|
||||
capacity: usize,
|
||||
left_slices: impl IntoIterator<Item = &'a [(K, V)]>,
|
||||
right_slices: impl IntoIterator<Item = &'a [(K, V)]>,
|
||||
) -> Vec<(K, V)>
|
||||
where
|
||||
K: Ord + Clone + 'a,
|
||||
V: Clone + 'a,
|
||||
{
|
||||
let mut right_keys = right_slices
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.iter().map(|(k, _)| k))
|
||||
.kmerge()
|
||||
.dedup()
|
||||
.peekable();
|
||||
|
||||
let mut out = Vec::with_capacity(capacity);
|
||||
for (_, key, value) in left_slices
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.enumerate()
|
||||
.map(|(i, s)| s.iter().map(move |(k, v)| (i, k, v)))
|
||||
.kmerge_by(|(i1, k1, _), (i2, k2, _)| (k1, i1) < (k2, i2))
|
||||
.dedup_by(|(_, k1, _), (_, k2, _)| *k1 == *k2)
|
||||
{
|
||||
while right_keys.peek().is_some_and(|right_key| *right_key < key) {
|
||||
right_keys.next();
|
||||
}
|
||||
|
||||
if right_keys.peek().is_some_and(|right_key| *right_key == key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push((key.clone(), value.clone()));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Extend a sorted vector with another sorted vector using 2 pointer merge.
|
||||
/// Values from `other` take precedence for duplicate keys.
|
||||
pub(crate) fn extend_sorted_vec<K, V>(target: &mut Vec<(K, V)>, other: &[(K, V)])
|
||||
@@ -183,4 +228,20 @@ mod tests {
|
||||
let result: Vec<(i32, &str)> = kway_merge_sorted(Vec::<&[(i32, &str)]>::new());
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kway_merge_disjoint_sorted() {
|
||||
let left_old = vec![(1, "old"), (2, "drop"), (4, "keep")];
|
||||
let left_new = vec![(1, "new"), (3, "new_only")];
|
||||
let right_a = vec![(2, "ignored"), (5, "ignored")];
|
||||
let right_b = vec![(3, "ignored")];
|
||||
|
||||
let result = kway_merge_disjoint_sorted(
|
||||
left_old.len() + left_new.len(),
|
||||
[left_new.as_slice(), left_old.as_slice()],
|
||||
[right_a.as_slice(), right_b.as_slice()],
|
||||
);
|
||||
|
||||
assert_eq!(result, vec![(1, "new"), (4, "keep")]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ rand = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# reth
|
||||
reth-chainspec.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-provider = { workspace = true, features = ["test-utils"] }
|
||||
reth-trie-db.workspace = true
|
||||
@@ -75,5 +77,7 @@ test-utils = [
|
||||
"reth-trie-db/test-utils",
|
||||
"reth-trie/test-utils",
|
||||
"reth-tasks/test-utils",
|
||||
"reth-chainspec/test-utils",
|
||||
"reth-trie-sparse?/test-utils",
|
||||
"reth-ethereum-primitives/test-utils",
|
||||
]
|
||||
|
||||
@@ -1151,7 +1151,9 @@ enum AccountWorkerJob {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_provider::test_utils::create_test_provider_factory;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_provider::test_utils::create_test_provider_factory_with_chain_spec;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_ctx<Factory>(factory: Factory) -> ProofTaskCtx<Factory> {
|
||||
ProofTaskCtx::new(factory)
|
||||
@@ -1160,11 +1162,16 @@ mod tests {
|
||||
/// Ensures `ProofWorkerHandle::new` spawns workers correctly.
|
||||
#[test]
|
||||
fn spawn_proof_workers_creates_handle() {
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let chain_spec = Arc::new(ChainSpec::default());
|
||||
let anchor_hash = chain_spec.genesis_hash();
|
||||
let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec);
|
||||
let changeset_cache = reth_trie_db::ChangesetCache::new();
|
||||
let factory = reth_provider::providers::OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
reth_provider::providers::OverlayBuilder::new(changeset_cache),
|
||||
reth_provider::providers::OverlayBuilder::<reth_ethereum_primitives::EthPrimitives>::new(
|
||||
anchor_hash,
|
||||
changeset_cache,
|
||||
),
|
||||
);
|
||||
let ctx = test_ctx(factory);
|
||||
|
||||
|
||||
@@ -274,16 +274,25 @@ mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{keccak256, Address, U256};
|
||||
use rand::Rng;
|
||||
use reth_primitives_traits::{Account, StorageEntry};
|
||||
use reth_provider::{test_utils::create_test_provider_factory, HashingWriter};
|
||||
use reth_chainspec::{ChainSpec, EthChainSpec};
|
||||
use reth_ethereum_primitives::{Block, BlockBody};
|
||||
use reth_primitives_traits::{Account, RecoveredBlock, SealedBlock, StorageEntry};
|
||||
use reth_provider::{
|
||||
test_utils::create_test_provider_factory_with_chain_spec, BlockWriter, ExecutionOutcome,
|
||||
HashingWriter,
|
||||
};
|
||||
use reth_trie::{test_utils, HashedPostState, HashedStorage};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn random_parallel_root() {
|
||||
let factory = create_test_provider_factory();
|
||||
let chain_spec = Arc::new(ChainSpec::default());
|
||||
let anchor_hash = chain_spec.genesis_hash();
|
||||
let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
|
||||
let changeset_cache = reth_trie_db::ChangesetCache::new();
|
||||
let overlay_builder = reth_provider::providers::OverlayBuilder::new(changeset_cache);
|
||||
let overlay_builder = reth_provider::providers::OverlayBuilder::<
|
||||
reth_ethereum_primitives::EthPrimitives,
|
||||
>::new(anchor_hash, changeset_cache);
|
||||
let mut overlay_factory = reth_provider::providers::OverlayStateProviderFactory::new(
|
||||
factory.clone(),
|
||||
overlay_builder.clone(),
|
||||
@@ -311,6 +320,20 @@ mod tests {
|
||||
|
||||
{
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
let genesis_block = RecoveredBlock::new_sealed(
|
||||
SealedBlock::<Block>::seal_parts(
|
||||
chain_spec.genesis_header().clone(),
|
||||
BlockBody::default(),
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
provider_rw
|
||||
.append_blocks_with_state(
|
||||
vec![genesis_block],
|
||||
&ExecutionOutcome::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
provider_rw
|
||||
.insert_account_for_hashing(
|
||||
state.iter().map(|(address, (account, _))| (*address, Some(*account))),
|
||||
|
||||
@@ -437,7 +437,10 @@ impl SparseNode {
|
||||
/// Returns the memory size of this node in bytes.
|
||||
pub const fn memory_size(&self) -> usize {
|
||||
match self {
|
||||
Self::Empty | Self::Branch { .. } => core::mem::size_of::<Self>(),
|
||||
Self::Empty => core::mem::size_of::<Self>(),
|
||||
Self::Branch { .. } => {
|
||||
core::mem::size_of::<Self>() + core::mem::size_of::<[B256; 16]>()
|
||||
}
|
||||
Self::Leaf { key, .. } | Self::Extension { key, .. } => {
|
||||
core::mem::size_of::<Self>() + key.len()
|
||||
}
|
||||
|
||||
@@ -56,30 +56,61 @@ where
|
||||
pub struct InMemoryTrieCursor<'a, C> {
|
||||
/// 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)>,
|
||||
/// Tracks whether the DB cursor is available, positioned, or exhausted.
|
||||
db_cursor_state: DbCursorState,
|
||||
/// Forward-only in-memory cursor over storage trie nodes.
|
||||
in_memory_cursor: ForwardInMemoryCursor<'a, Nibbles, Option<BranchNodeCompact>>,
|
||||
/// The key most recently returned from the Cursor.
|
||||
last_key: Option<Nibbles>,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Whether an initial seek was called.
|
||||
seeked: bool,
|
||||
/// Reference to the full trie updates.
|
||||
trie_updates: &'a TrieUpdatesSorted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DbCursorState {
|
||||
NeedsPosition,
|
||||
Positioned((Nibbles, BranchNodeCompact)),
|
||||
Exhausted,
|
||||
Wiped,
|
||||
}
|
||||
|
||||
impl DbCursorState {
|
||||
const fn new(cursor_wiped: bool) -> Self {
|
||||
if cursor_wiped {
|
||||
Self::Wiped
|
||||
} else {
|
||||
Self::NeedsPosition
|
||||
}
|
||||
}
|
||||
|
||||
const fn entry(&self) -> Option<&(Nibbles, BranchNodeCompact)> {
|
||||
match self {
|
||||
Self::Positioned(entry) => Some(entry),
|
||||
Self::NeedsPosition | Self::Exhausted | Self::Wiped => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_entry(&mut self, entry: Option<(Nibbles, BranchNodeCompact)>) {
|
||||
*self = match entry {
|
||||
Some(entry) => Self::Positioned(entry),
|
||||
None => Self::Exhausted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
/// 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,
|
||||
db_cursor_state: DbCursorState::NeedsPosition,
|
||||
in_memory_cursor,
|
||||
last_key: None,
|
||||
#[cfg(debug_assertions)]
|
||||
seeked: false,
|
||||
trie_updates,
|
||||
}
|
||||
@@ -96,10 +127,10 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
Self::get_storage_overlay(trie_updates, hashed_address);
|
||||
Self {
|
||||
cursor,
|
||||
cursor_wiped,
|
||||
cursor_entry: None,
|
||||
db_cursor_state: DbCursorState::new(cursor_wiped),
|
||||
in_memory_cursor,
|
||||
last_key: None,
|
||||
#[cfg(debug_assertions)]
|
||||
seeked: false,
|
||||
trie_updates,
|
||||
}
|
||||
@@ -119,7 +150,7 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
|
||||
/// 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)
|
||||
(!matches!(self.db_cursor_state, DbCursorState::Wiped)).then_some(&mut self.cursor)
|
||||
}
|
||||
|
||||
/// Asserts that the next entry to be returned from the cursor is not previous to the last entry
|
||||
@@ -135,31 +166,38 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
self.last_key = next_key;
|
||||
}
|
||||
|
||||
/// Seeks the `cursor_entry` field of the struct using the cursor.
|
||||
/// Positions the DB cursor state using the underlying cursor when needed.
|
||||
fn cursor_seek(&mut self, key: Nibbles) -> Result<(), DatabaseError> {
|
||||
// Only seek if:
|
||||
// 1. We have a cursor entry and need to seek forward (entry.0 < key), OR
|
||||
// 2. We have no cursor entry and haven't seeked yet (!self.seeked)
|
||||
let should_seek = match self.cursor_entry.as_ref() {
|
||||
Some(entry) => entry.0 < key,
|
||||
None => !self.seeked,
|
||||
// 2. The DB cursor needs to be positioned.
|
||||
let should_seek = match &self.db_cursor_state {
|
||||
DbCursorState::NeedsPosition => true,
|
||||
DbCursorState::Positioned((entry_key, _)) => entry_key < &key,
|
||||
DbCursorState::Exhausted | DbCursorState::Wiped => false,
|
||||
};
|
||||
|
||||
if should_seek {
|
||||
self.cursor_entry = self.get_cursor_mut().map(|c| c.seek(key)).transpose()?.flatten();
|
||||
let entry = self.get_cursor_mut().map(|c| c.seek(key)).transpose()?.flatten();
|
||||
self.db_cursor_state.set_entry(entry);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Seeks the `cursor_entry` field of the struct to the subsequent entry using the cursor.
|
||||
/// Advances the DB cursor state to the subsequent entry using the underlying cursor.
|
||||
fn cursor_next(&mut self) -> Result<(), DatabaseError> {
|
||||
debug_assert!(self.seeked);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_assert!(self.seeked);
|
||||
debug_assert!(!matches!(self.db_cursor_state, DbCursorState::NeedsPosition));
|
||||
}
|
||||
|
||||
// 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.get_cursor_mut().map(|c| c.next()).transpose()?.flatten();
|
||||
// Exhausted and wiped states are stable; only advance if the DB cursor currently points to
|
||||
// an entry.
|
||||
if matches!(self.db_cursor_state, DbCursorState::Positioned(_)) {
|
||||
let entry = self.get_cursor_mut().map(|c| c.next()).transpose()?.flatten();
|
||||
self.db_cursor_state.set_entry(entry);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -172,9 +210,12 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
/// node.
|
||||
fn choose_next_entry(&mut self) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
loop {
|
||||
match (self.in_memory_cursor.current().cloned(), &self.cursor_entry) {
|
||||
let mem_entry = self.in_memory_cursor.current().cloned();
|
||||
let db_entry = self.db_cursor_state.entry();
|
||||
|
||||
match (mem_entry, db_entry) {
|
||||
(Some((mem_key, None)), _)
|
||||
if self.cursor_entry.as_ref().is_none_or(|(db_key, _)| &mem_key < db_key) =>
|
||||
if db_entry.is_none_or(|(db_key, _)| &mem_key < db_key) =>
|
||||
{
|
||||
// If overlay has a removed node but DB cursor is exhausted or ahead of the
|
||||
// in-memory cursor then move ahead in-memory, as there might be further
|
||||
@@ -188,7 +229,7 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
self.cursor_next()?;
|
||||
}
|
||||
(Some((mem_key, Some(node))), _)
|
||||
if self.cursor_entry.as_ref().is_none_or(|(db_key, _)| &mem_key <= db_key) =>
|
||||
if db_entry.is_none_or(|(db_key, _)| &mem_key <= db_key) =>
|
||||
{
|
||||
// If overlay returns a node prior to the DB's node, or the DB is exhausted,
|
||||
// then we return the overlay's node.
|
||||
@@ -198,7 +239,7 @@ impl<'a, C: TrieCursor> InMemoryTrieCursor<'a, C> {
|
||||
// - mem_key > db_key
|
||||
// - overlay is exhausted
|
||||
// Return the db_entry. If DB is also exhausted then this returns None.
|
||||
_ => return Ok(self.cursor_entry.clone()),
|
||||
_ => return Ok(db_entry.cloned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,16 +250,38 @@ impl<C: TrieCursor> TrieCursor for InMemoryTrieCursor<'_, C> {
|
||||
&mut self,
|
||||
key: Nibbles,
|
||||
) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
self.cursor_seek(key)?;
|
||||
let mem_entry = self.in_memory_cursor.seek(&key);
|
||||
|
||||
self.seeked = true;
|
||||
|
||||
let entry = match (mem_entry, &self.cursor_entry) {
|
||||
(Some((mem_key, entry_inner)), _) if *mem_key == key => {
|
||||
entry_inner.clone().map(|node| (key, node))
|
||||
if let Some((mem_key, entry_inner)) = mem_entry &&
|
||||
*mem_key == key
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.seeked = true;
|
||||
}
|
||||
(_, Some((db_key, node))) if db_key == &key => Some((key, node.clone())),
|
||||
|
||||
// An exact overlay hit can move the logical cursor ahead without touching the DB. If
|
||||
// the DB cursor was still behind this key, force a re-seek before the next DB-backed
|
||||
// operation so `next()` cannot return a stale earlier entry.
|
||||
if matches!(&self.db_cursor_state, DbCursorState::Positioned((db_key, _)) if db_key < &key)
|
||||
{
|
||||
self.db_cursor_state = DbCursorState::NeedsPosition;
|
||||
}
|
||||
|
||||
let entry = entry_inner.clone().map(|node| (key, node));
|
||||
self.set_last_key(&entry);
|
||||
return Ok(entry)
|
||||
}
|
||||
|
||||
self.cursor_seek(key)?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.seeked = true;
|
||||
}
|
||||
|
||||
let entry = match self.db_cursor_state.entry() {
|
||||
Some((db_key, node)) if db_key == &key => Some((key, node.clone())),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -233,7 +296,10 @@ impl<C: TrieCursor> TrieCursor for InMemoryTrieCursor<'_, C> {
|
||||
self.cursor_seek(key)?;
|
||||
self.in_memory_cursor.seek(&key);
|
||||
|
||||
self.seeked = true;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.seeked = true;
|
||||
}
|
||||
|
||||
let entry = self.choose_next_entry()?;
|
||||
self.set_last_key(&entry);
|
||||
@@ -241,7 +307,10 @@ impl<C: TrieCursor> TrieCursor for InMemoryTrieCursor<'_, C> {
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
debug_assert!(self.seeked, "Cursor must be seek'd before next is called");
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_assert!(self.seeked, "Cursor must be seek'd before next is called");
|
||||
}
|
||||
|
||||
// A `last_key` of `None` indicates that the cursor is exhausted.
|
||||
let Some(last_key) = self.last_key else {
|
||||
@@ -256,7 +325,11 @@ impl<C: TrieCursor> TrieCursor for InMemoryTrieCursor<'_, C> {
|
||||
self.in_memory_cursor.first_after(&last_key);
|
||||
}
|
||||
|
||||
if let Some((key, _)) = &self.cursor_entry &&
|
||||
if matches!(self.db_cursor_state, DbCursorState::NeedsPosition) {
|
||||
self.cursor_seek(last_key)?;
|
||||
}
|
||||
|
||||
if let Some((key, _)) = self.db_cursor_state.entry() &&
|
||||
key == &last_key
|
||||
{
|
||||
self.cursor_next()?;
|
||||
@@ -275,23 +348,15 @@ impl<C: TrieCursor> TrieCursor for InMemoryTrieCursor<'_, C> {
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
let Self {
|
||||
cursor,
|
||||
cursor_wiped,
|
||||
cursor_entry,
|
||||
in_memory_cursor,
|
||||
last_key,
|
||||
seeked,
|
||||
trie_updates: _,
|
||||
} = self;
|
||||
self.cursor.reset();
|
||||
self.in_memory_cursor.reset();
|
||||
|
||||
cursor.reset();
|
||||
in_memory_cursor.reset();
|
||||
|
||||
*cursor_wiped = false;
|
||||
*cursor_entry = None;
|
||||
*last_key = None;
|
||||
*seeked = false;
|
||||
self.db_cursor_state = DbCursorState::NeedsPosition;
|
||||
self.last_key = None;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.seeked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,8 +364,10 @@ impl<C: TrieStorageCursor> 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) =
|
||||
let (in_memory_cursor, cursor_wiped) =
|
||||
Self::get_storage_overlay(self.trie_updates, hashed_address);
|
||||
self.in_memory_cursor = in_memory_cursor;
|
||||
self.db_cursor_state = DbCursorState::new(cursor_wiped);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +574,7 @@ mod tests {
|
||||
let db_nodes_map: BTreeMap<Nibbles, BranchNodeCompact> = db_nodes.into_iter().collect();
|
||||
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 mock_cursor = MockTrieCursor::new(db_nodes_arc, visited_keys.clone());
|
||||
|
||||
let trie_updates = TrieUpdatesSorted::new(in_memory_nodes, Default::default());
|
||||
let mut cursor = InMemoryTrieCursor::new_account(mock_cursor, &trie_updates);
|
||||
@@ -520,6 +587,7 @@ mod tests {
|
||||
BranchNodeCompact::new(0b0010, 0b0010, 0, vec![], None)
|
||||
))
|
||||
);
|
||||
assert!(visited_keys.lock().is_empty(), "exact overlay hit should not touch the DB cursor");
|
||||
|
||||
let result = cursor.seek_exact(Nibbles::from_nibbles([0x3])).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -950,6 +950,11 @@ Engine:
|
||||
|
||||
[default: 16]
|
||||
|
||||
--engine.deferred-trie-blocks <DEFERRED_TRIE_BLOCKS>
|
||||
Configure how many of the blocks being persisted should only mask state/trie writes instead of durably persisting their state/trie updates in the current cycle
|
||||
|
||||
[default: 0]
|
||||
|
||||
--engine.memory-block-buffer-target <MEMORY_BLOCK_BUFFER_TARGET>
|
||||
Configure the target number of blocks to keep in memory
|
||||
|
||||
|
||||
Reference in New Issue
Block a user