mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b2c458302 | ||
|
|
0722202930 | ||
|
|
8ec6e614f9 | ||
|
|
2c86c0b876 | ||
|
|
bd4cd28a8d | ||
|
|
6fa48a497a | ||
|
|
6886cd7742 | ||
|
|
eeb223f0b8 | ||
|
|
f344f5abfb | ||
|
|
68845d1114 | ||
|
|
ecfb6cc089 | ||
|
|
b271694301 | ||
|
|
41c68729ab | ||
|
|
79578e35b8 | ||
|
|
e4f14b2ae1 | ||
|
|
05e6da66e1 | ||
|
|
6be5520e34 | ||
|
|
d29db3b765 | ||
|
|
40c30dbc73 | ||
|
|
5c383818a6 | ||
|
|
cf6ffb1599 | ||
|
|
ba3cd2872a | ||
|
|
4f9af7c16a | ||
|
|
13c5504aa2 | ||
|
|
fa6b44b038 | ||
|
|
6377a957c1 | ||
|
|
378d4052ee | ||
|
|
62d99888d2 | ||
|
|
73f5d77b51 | ||
|
|
b62f71977a | ||
|
|
ad27be67be | ||
|
|
63f80907cc | ||
|
|
a57930481c | ||
|
|
bbcfe354a1 | ||
|
|
7839f3d876 | ||
|
|
e89b4611e4 | ||
|
|
2b7d4b54d4 | ||
|
|
fe7a4c80b6 | ||
|
|
122c5b322b | ||
|
|
f1ed5f0ade | ||
|
|
6364fb87d0 | ||
|
|
d55458479d | ||
|
|
42f49132b7 | ||
|
|
f39c47bd11 | ||
|
|
b1ac264107 | ||
|
|
0195da5b84 | ||
|
|
b964195ef8 | ||
|
|
252fe42c54 | ||
|
|
3edb271183 | ||
|
|
165a80441b | ||
|
|
981e32d4d9 | ||
|
|
d7522904a0 | ||
|
|
e92af360ae | ||
|
|
408ef4657d | ||
|
|
3574ecaaa0 |
116
.github/scripts/bench-reth-build.sh
vendored
116
.github/scripts/bench-reth-build.sh
vendored
@@ -1,24 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds (or fetches from cache) reth binaries for benchmarking.
|
||||
# Builds reth binaries for benchmarking from local source only.
|
||||
#
|
||||
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit> [branch-sha]
|
||||
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit>
|
||||
#
|
||||
# baseline — build/fetch the baseline binary at <commit> (merge-base)
|
||||
# baseline — build the baseline binary at <commit> (merge-base)
|
||||
# source-dir must be checked out at <commit>
|
||||
# feature — build/fetch the candidate binary + reth-bench at <commit>
|
||||
# feature — build the candidate binary + reth-bench at <commit>
|
||||
# source-dir must be checked out at <commit>
|
||||
# optional branch-sha is the PR head commit for cache key
|
||||
#
|
||||
# Outputs:
|
||||
# baseline: <source-dir>/target/profiling/reth (or reth-bb if BENCH_BIG_BLOCKS=true)
|
||||
# feature: <source-dir>/target/profiling/reth (or reth-bb), reth-bench installed to cargo bin
|
||||
#
|
||||
# Required: mc (MinIO client) with a configured alias
|
||||
# Optional env: BENCH_BIG_BLOCKS (true/false) — build reth-bb instead of reth
|
||||
set -euxo pipefail
|
||||
|
||||
MC="mc"
|
||||
MODE="$1"
|
||||
SOURCE_DIR="$2"
|
||||
COMMIT="$3"
|
||||
@@ -42,103 +39,38 @@ if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
EXTRA_RUSTFLAGS=" -C force-frame-pointers=yes"
|
||||
fi
|
||||
|
||||
# Cache suffix: hash of features+rustflags so different build configs get separate cache entries
|
||||
if [ -n "$EXTRA_FEATURES" ] || [ -n "$EXTRA_RUSTFLAGS" ]; then
|
||||
BUILD_SUFFIX="-$(echo "${EXTRA_FEATURES}${EXTRA_RUSTFLAGS}" | sha256sum | cut -c1-12)"
|
||||
else
|
||||
BUILD_SUFFIX=""
|
||||
fi
|
||||
# Build the requested node binary with the benchmark profile.
|
||||
build_node_binary() {
|
||||
local features_arg=""
|
||||
local workspace_arg=""
|
||||
|
||||
# Verify a cached reth binary was built from the expected commit.
|
||||
# `reth --version` outputs "Commit SHA: <full-sha>" on its own line.
|
||||
verify_binary() {
|
||||
local binary="$1" expected_commit="$2"
|
||||
local version binary_sha
|
||||
version=$("$binary" --version 2>/dev/null) || return 1
|
||||
binary_sha=$(echo "$version" | sed -n 's/^Commit SHA: *//p')
|
||||
if [ -z "$binary_sha" ]; then
|
||||
echo "Warning: could not extract commit SHA from version output"
|
||||
return 1
|
||||
cd "$SOURCE_DIR"
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
features_arg="--features ${EXTRA_FEATURES}"
|
||||
workspace_arg="--workspace"
|
||||
fi
|
||||
if [ "$binary_sha" = "$expected_commit" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "Cache mismatch: binary built from ${binary_sha} but expected ${expected_commit}"
|
||||
return 1
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --locked --profile profiling $NODE_PKG $workspace_arg $features_arg
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
baseline|main)
|
||||
BUCKET="minio/reth-binaries/${COMMIT}${BUILD_SUFFIX}"
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat --no-list "${BUCKET}/${NODE_BIN}" &>/dev/null; then
|
||||
echo "Cache hit for baseline (${COMMIT}), downloading ${NODE_BIN}..."
|
||||
if $MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}" && \
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/${NODE_BIN}" && \
|
||||
verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached baseline binary is stale or download failed, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
FEATURES_ARG=""
|
||||
WORKSPACE_ARG=""
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
FEATURES_ARG="--features ${EXTRA_FEATURES}"
|
||||
WORKSPACE_ARG="--workspace"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling $NODE_PKG $WORKSPACE_ARG $FEATURES_ARG
|
||||
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
|
||||
fi
|
||||
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
|
||||
build_node_binary
|
||||
;;
|
||||
|
||||
feature|branch)
|
||||
BRANCH_SHA="${4:-$COMMIT}"
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat --no-list "${BUCKET}/${NODE_BIN}" &>/dev/null && $MC stat --no-list "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
if $MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}" && \
|
||||
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench && \
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/${NODE_BIN}" /home/ubuntu/.cargo/bin/reth-bench && \
|
||||
verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached feature binary is stale or download failed, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
rustup show active-toolchain || rustup default stable
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# Can't use `make profiling` when adding features; build explicitly
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling --workspace $NODE_PKG --features "${EXTRA_FEATURES}"
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling $NODE_PKG
|
||||
fi
|
||||
make install-reth-bench
|
||||
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
|
||||
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
|
||||
fi
|
||||
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
|
||||
rustup show active-toolchain || rustup default stable
|
||||
build_node_binary
|
||||
make -C "$SOURCE_DIR" install-reth-bench
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 <baseline|feature> <source-dir> <commit> [branch-sha]"
|
||||
echo "Usage: $0 <baseline|feature> <source-dir> <commit>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
37
.github/scripts/bench-reth-local.sh
vendored
37
.github/scripts/bench-reth-local.sh
vendored
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# local-reth-bench.sh — Run the reth Engine API benchmark locally.
|
||||
#
|
||||
# Replicates the CI bench.yml workflow (build, snapshot, system tuning,
|
||||
# Replicates the CI bench.yml workflow (build, local snapshot validation, system tuning,
|
||||
# interleaved B-F-F-B execution, summary, charts) without any GitHub
|
||||
# Actions glue (no PR comments, no artifact upload, no Slack).
|
||||
#
|
||||
@@ -21,15 +21,17 @@
|
||||
# Requires: the reth repo at RETH_REPO (default: ~/reth)
|
||||
#
|
||||
# Dependencies (install before first run):
|
||||
# mc (MinIO client), schelk, cpupower, taskset, stdbuf, python3, curl,
|
||||
# make, uv, pzstd, jq, Rust toolchain (cargo/rustup)
|
||||
# schelk, cpupower, taskset, stdbuf, python3, curl,
|
||||
# make, uv, jq, Rust toolchain (cargo/rustup)
|
||||
# Optional:
|
||||
# mc for Tracy profile upload
|
||||
#
|
||||
# The script delegates to the existing bench-reth-*.sh scripts in the reth
|
||||
# repo for the actual build, snapshot, and run steps.
|
||||
set -euxo pipefail
|
||||
|
||||
# ── PATH ──────────────────────────────────────────────────────────────
|
||||
# Ensure cargo and user-local bins (mc, uv) are visible
|
||||
# Ensure cargo and user-local bins (uv) are visible
|
||||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
@@ -106,7 +108,7 @@ fi
|
||||
|
||||
# ── Check dependencies ───────────────────────────────────────────────
|
||||
missing=()
|
||||
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq cargo; do
|
||||
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq cargo; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
@@ -238,19 +240,14 @@ echo " Baseline src : $BASELINE_SRC"
|
||||
echo " Feature src : $FEATURE_SRC"
|
||||
echo
|
||||
|
||||
# ── Step 3: Check / download snapshot ────────────────────────────────
|
||||
echo "▸ Checking snapshot..."
|
||||
# ── Step 3: Validate local snapshot ──────────────────────────────────
|
||||
echo "▸ Validating local snapshot..."
|
||||
cd "$RETH_REPO"
|
||||
SNAPSHOT_NEEDED=false
|
||||
if ! "${SCRIPTS_DIR}/bench-reth-snapshot.sh" --check; then
|
||||
SNAPSHOT_NEEDED=true
|
||||
echo " Snapshot needs update."
|
||||
else
|
||||
echo " Snapshot is up-to-date."
|
||||
fi
|
||||
"${SCRIPTS_DIR}/bench-reth-snapshot.sh"
|
||||
echo " Snapshot is ready."
|
||||
echo
|
||||
|
||||
# ── Step 4: Build binaries (+ snapshot download) in parallel ─────────
|
||||
# ── Step 4: Build binaries in parallel ───────────────────────────────
|
||||
echo "▸ Building binaries (parallel)..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
@@ -262,19 +259,11 @@ PID_BASELINE=$!
|
||||
"${SCRIPTS_DIR}/bench-reth-build.sh" feature "$FEATURE_SRC" "$FEATURE_SHA" &
|
||||
PID_FEATURE=$!
|
||||
|
||||
PID_SNAPSHOT=
|
||||
if [ "$SNAPSHOT_NEEDED" = "true" ]; then
|
||||
echo " Also downloading snapshot in parallel..."
|
||||
"${SCRIPTS_DIR}/bench-reth-snapshot.sh" &
|
||||
PID_SNAPSHOT=$!
|
||||
fi
|
||||
|
||||
wait $PID_BASELINE || FAIL=1
|
||||
wait $PID_FEATURE || FAIL=1
|
||||
[ -n "$PID_SNAPSHOT" ] && { wait $PID_SNAPSHOT || FAIL=1; }
|
||||
|
||||
if [ $FAIL -ne 0 ]; then
|
||||
echo "Error: one or more parallel tasks failed (builds / snapshot)"
|
||||
echo "Error: one or more build tasks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo " Binaries built successfully."
|
||||
|
||||
10
.github/scripts/bench-reth-run.sh
vendored
10
.github/scripts/bench-reth-run.sh
vendored
@@ -88,10 +88,16 @@ trap cleanup EXIT
|
||||
# Stop any leftover reth process in the scope, then recover schelk state.
|
||||
sudo systemctl stop "$RETH_SCOPE" 2>/dev/null || true
|
||||
sudo systemctl reset-failed "$RETH_SCOPE" 2>/dev/null || true
|
||||
sudo schelk recover -y --kill || true
|
||||
sudo schelk recover -y --kill || sudo schelk full-recover -y || true
|
||||
|
||||
# Mount
|
||||
sudo schelk mount -y
|
||||
sudo schelk mount -y || true
|
||||
if [ ! -d "$DATADIR/db" ] || [ ! -d "$DATADIR/static_files" ]; then
|
||||
echo "::error::Failed to mount benchmark datadir at ${DATADIR}"
|
||||
ls -la "$SCHELK_MOUNT" || true
|
||||
ls -la "$DATADIR" || true
|
||||
exit 1
|
||||
fi
|
||||
sync
|
||||
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
|
||||
echo "=== Cache state after drop ==="
|
||||
|
||||
137
.github/scripts/bench-reth-snapshot.sh
vendored
137
.github/scripts/bench-reth-snapshot.sh
vendored
@@ -1,129 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Downloads the latest snapshot into the schelk volume using
|
||||
# `reth download` with progress reporting to the GitHub PR comment.
|
||||
#
|
||||
# Skips the download if the manifest content hasn't changed since
|
||||
# the last successful download (checked via SHA-256 of the manifest).
|
||||
# Validates that the benchmark snapshot has already been populated into the
|
||||
# local schelk volume.
|
||||
#
|
||||
# Usage: bench-reth-snapshot.sh [--check]
|
||||
# --check Only check if a download is needed; exits 0 if up-to-date, 10 if not.
|
||||
# --check Exit 0 if the local snapshot is ready, 10 if it is missing.
|
||||
#
|
||||
# Required env:
|
||||
# SCHELK_MOUNT – schelk mount point (e.g. /reth-bench)
|
||||
# BENCH_RETH_BINARY – path to the reth binary
|
||||
# GITHUB_TOKEN – token for GitHub API calls (only for download)
|
||||
# BENCH_COMMENT_ID – PR comment ID to update (optional)
|
||||
# BENCH_REPO – owner/repo (e.g. paradigmxyz/reth)
|
||||
# BENCH_JOB_URL – link to the Actions job
|
||||
# BENCH_ACTOR – user who triggered the benchmark
|
||||
# BENCH_CONFIG – config summary line
|
||||
# SCHELK_MOUNT – schelk mount point (e.g. /reth-bench)
|
||||
# Optional env:
|
||||
# BENCH_BIG_BLOCKS – true when validating the big-blocks snapshot datadir
|
||||
# BENCH_SNAPSHOT_NAME – expected snapshot label for log/error output
|
||||
set -euxo pipefail
|
||||
|
||||
MC="mc"
|
||||
BUCKET="minio/reth-snapshots"
|
||||
# Allow overriding the snapshot name (e.g. for big-blocks mode where the
|
||||
# big-blocks manifest specifies which base snapshot to use).
|
||||
SNAPSHOT_NAME="${BENCH_SNAPSHOT_NAME:-reth-1-minimal-stable-previous}"
|
||||
MANIFEST_PATH="${SNAPSHOT_NAME}/manifest.json"
|
||||
: "${SCHELK_MOUNT:?SCHELK_MOUNT must be set}"
|
||||
|
||||
DATADIR_NAME="datadir"
|
||||
HASH_MODE_SUFFIX=""
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
HASH_MODE_SUFFIX="-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
HASH_FILE="$HOME/.reth-bench-snapshot-hash${HASH_MODE_SUFFIX}"
|
||||
|
||||
# Fetch manifest and compute content hash for reliable freshness check
|
||||
MANIFEST_CONTENT=$($MC cat "${BUCKET}/${MANIFEST_PATH}" 2>/dev/null) || {
|
||||
echo "::error::Failed to fetch snapshot manifest from ${BUCKET}/${MANIFEST_PATH}"
|
||||
exit 2
|
||||
describe_snapshot() {
|
||||
if [ -n "${BENCH_SNAPSHOT_NAME:-}" ]; then
|
||||
printf '%s' "${BENCH_SNAPSHOT_NAME}"
|
||||
elif [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
printf '%s' 'big-block weekly snapshot'
|
||||
else
|
||||
printf '%s' 'benchmark snapshot'
|
||||
fi
|
||||
}
|
||||
REMOTE_HASH=$(echo "$MANIFEST_CONTENT" | sha256sum | awk '{print $1}')
|
||||
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
|
||||
snapshot_ready() {
|
||||
[ -d "$DATADIR/db" ] && [ -d "$DATADIR/static_files" ]
|
||||
}
|
||||
|
||||
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo "Snapshot is up-to-date (manifest hash: ${REMOTE_HASH:0:16}…)"
|
||||
EXPECTED_SNAPSHOT="$(describe_snapshot)"
|
||||
|
||||
sudo schelk recover -y --kill || sudo schelk full-recover -y || true
|
||||
sudo schelk mount -y || true
|
||||
|
||||
if snapshot_ready; then
|
||||
echo "Found local ${EXPECTED_SNAPSHOT} at ${DATADIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Snapshot needs update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}…}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
|
||||
echo "::error::Missing local ${EXPECTED_SNAPSHOT} at ${DATADIR}. Benchmarks no longer download snapshots; pre-populate the local schelk data first."
|
||||
ls -la "$SCHELK_MOUNT" || true
|
||||
ls -la "$DATADIR" || true
|
||||
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 10
|
||||
fi
|
||||
|
||||
RETH="${BENCH_RETH_BINARY:?BENCH_RETH_BINARY must be set}"
|
||||
if [ ! -x "$RETH" ]; then
|
||||
echo "::error::reth binary not found or not executable at $RETH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve the MinIO HTTP endpoint from the mc alias so reth can
|
||||
# fetch archives over HTTP (the manifest's embedded base_url points
|
||||
# to the cluster-internal address which is unreachable from runners).
|
||||
MINIO_ENDPOINT=$($MC alias list minio --json 2>/dev/null | jq -r '.URL // empty') || true
|
||||
if [ -z "$MINIO_ENDPOINT" ]; then
|
||||
echo "::error::Failed to resolve MinIO endpoint from mc alias 'minio'"
|
||||
exit 1
|
||||
fi
|
||||
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/${SNAPSHOT_NAME}"
|
||||
|
||||
# Rewrite manifest's base_url with the runner-reachable endpoint
|
||||
MANIFEST_TMP=$(mktemp --suffix=.json)
|
||||
trap 'rm -f -- "$MANIFEST_TMP"' EXIT
|
||||
echo "$MANIFEST_CONTENT" \
|
||||
| jq --arg base "$BASE_URL" '.base_url = $base' > "$MANIFEST_TMP"
|
||||
|
||||
# Prepare mount. If a previous run left the volume mounted, recover first.
|
||||
sudo schelk recover -y --kill || true
|
||||
sudo schelk mount -y
|
||||
sudo rm -rf "$DATADIR"
|
||||
sudo mkdir -p "$DATADIR"
|
||||
# reth download runs as current user (not root), needs write access
|
||||
sudo chown -R "$(id -u):$(id -g)" "$DATADIR"
|
||||
|
||||
update_comment() {
|
||||
local status="$1"
|
||||
[ -z "${BENCH_COMMENT_ID:-}" ] && return 0
|
||||
local body
|
||||
body="$(printf 'cc @%s\n\n🚀 Benchmark started! [View job](%s)\n\n⏳ **Status:** %s\n\n%s' \
|
||||
"$BENCH_ACTOR" "$BENCH_JOB_URL" "$status" "$BENCH_CONFIG")"
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${BENCH_REPO}/issues/comments/${BENCH_COMMENT_ID}" \
|
||||
-d "$(jq -nc --arg body "$body" '{body: $body}')" \
|
||||
> /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
update_comment "Downloading snapshot…"
|
||||
|
||||
# Download using reth download (manifest-path with rewritten base_url)
|
||||
"$RETH" download \
|
||||
--manifest-path "$MANIFEST_TMP" \
|
||||
-y \
|
||||
--minimal \
|
||||
--datadir "$DATADIR"
|
||||
|
||||
update_comment "Downloading snapshot… done"
|
||||
echo "Snapshot download complete"
|
||||
|
||||
# Sanity check: verify expected directories exist
|
||||
if [ ! -d "$DATADIR/db" ] || [ ! -d "$DATADIR/static_files" ]; then
|
||||
echo "::error::Snapshot download did not produce expected directory layout (missing db/ or static_files/)"
|
||||
ls -la "$DATADIR" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Promote the new snapshot to become the schelk baseline (virgin volume).
|
||||
# This copies changed blocks from scratch → virgin so that future
|
||||
# `schelk recover` calls restore to this new state.
|
||||
sync
|
||||
sudo schelk promote -y
|
||||
|
||||
# Save manifest hash
|
||||
echo "$REMOTE_HASH" > "$HASH_FILE"
|
||||
echo "Snapshot promoted to schelk baseline (manifest hash: ${REMOTE_HASH:0:16}…)"
|
||||
exit 1
|
||||
|
||||
67
.github/workflows/bench-scheduled.yml
vendored
67
.github/workflows/bench-scheduled.yml
vendored
@@ -307,12 +307,6 @@ jobs:
|
||||
linux-tools-"$(uname -r)" || \
|
||||
sudo apt-get install -y --no-install-recommends linux-tools-generic
|
||||
|
||||
# mc (MinIO client)
|
||||
if ! command -v mc &>/dev/null; then
|
||||
curl -sSfL https://dl.min.io/client/mc/release/linux-amd64/mc -o "$HOME/.local/bin/mc"
|
||||
chmod +x "$HOME/.local/bin/mc"
|
||||
fi
|
||||
|
||||
# uv (Python package manager)
|
||||
if ! command -v uv &>/dev/null; then
|
||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$HOME/.local/bin" sh
|
||||
@@ -340,7 +334,7 @@ jobs:
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
missing=()
|
||||
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq; do
|
||||
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
@@ -366,35 +360,30 @@ jobs:
|
||||
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if snapshot needs update
|
||||
- name: Validate local snapshot
|
||||
id: snapshot-check
|
||||
run: |
|
||||
set +e
|
||||
.github/scripts/bench-reth-snapshot.sh --check
|
||||
rc=$?
|
||||
set -e
|
||||
case "$rc" in
|
||||
0) echo "needed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
10) echo "needed=true" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Snapshot check failed (exit $rc)"
|
||||
exit "$rc" ;;
|
||||
esac
|
||||
run: .github/scripts/bench-reth-snapshot.sh
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
if [ -d ../reth-baseline ]; then
|
||||
git -C ../reth-baseline fetch origin "$BASELINE_REF"
|
||||
else
|
||||
git clone . ../reth-baseline
|
||||
fi
|
||||
git -C ../reth-baseline checkout "$BASELINE_REF"
|
||||
prepare_source_dir() {
|
||||
local dir="$1"
|
||||
local ref="$2"
|
||||
|
||||
if [ -d ../reth-feature ]; then
|
||||
git -C ../reth-feature fetch origin "$FEATURE_REF"
|
||||
else
|
||||
git clone . ../reth-feature
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
if [ -d "$dir" ]; then
|
||||
git -C "$dir" reset --hard HEAD
|
||||
git -C "$dir" clean -fdx
|
||||
git -C "$dir" fetch origin "$ref"
|
||||
else
|
||||
git clone . "$dir"
|
||||
fi
|
||||
|
||||
git -C "$dir" checkout --force "$ref"
|
||||
}
|
||||
|
||||
prepare_source_dir ../reth-baseline "$BASELINE_REF"
|
||||
|
||||
prepare_source_dir ../reth-feature "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
id: build
|
||||
@@ -418,15 +407,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download snapshot
|
||||
id: snapshot-download
|
||||
if: steps.snapshot-check.outputs.needed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BENCH_REPO: ${{ github.repository }}
|
||||
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/reth
|
||||
run: .github/scripts/bench-reth-snapshot.sh
|
||||
|
||||
# System tuning for reproducible benchmarks
|
||||
- name: System setup
|
||||
run: |
|
||||
@@ -925,8 +905,8 @@ jobs:
|
||||
if (!token || !channel) return;
|
||||
|
||||
const steps_status = [
|
||||
['validating local snapshot', '${{ steps.snapshot-check.outcome }}'],
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}'],
|
||||
@@ -975,6 +955,11 @@ jobs:
|
||||
}),
|
||||
});
|
||||
|
||||
- name: Clean build outputs
|
||||
if: always()
|
||||
run: |
|
||||
sudo rm -rf ../reth-baseline/target ../reth-feature/target "$BENCH_WORK_DIR" 2>/dev/null || true
|
||||
|
||||
- name: Restore system settings
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
166
.github/workflows/bench.yml
vendored
166
.github/workflows/bench.yml
vendored
@@ -650,12 +650,6 @@ jobs:
|
||||
linux-tools-"$(uname -r)" || \
|
||||
sudo apt-get install -y --no-install-recommends linux-tools-generic
|
||||
|
||||
# mc (MinIO client)
|
||||
if ! command -v mc &>/dev/null; then
|
||||
curl -sSfL https://dl.min.io/client/mc/release/linux-amd64/mc -o "$HOME/.local/bin/mc"
|
||||
chmod +x "$HOME/.local/bin/mc"
|
||||
fi
|
||||
|
||||
# uv (Python package manager)
|
||||
if ! command -v uv &>/dev/null; then
|
||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$HOME/.local/bin" sh
|
||||
@@ -690,7 +684,7 @@ jobs:
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
missing=()
|
||||
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq; do
|
||||
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
@@ -766,79 +760,68 @@ jobs:
|
||||
core.setOutput('feature-ref', featureRef);
|
||||
core.setOutput('feature-name', featureName);
|
||||
|
||||
- name: Check big-blocks freshness
|
||||
- name: Validate local big blocks
|
||||
if: env.BENCH_BIG_BLOCKS == 'true'
|
||||
id: big-blocks-check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MC="mc --config-dir /home/ubuntu/.mc"
|
||||
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
|
||||
HASH_FILE="$HOME/.reth-bench-big-blocks-hash"
|
||||
echo "Fetching big-blocks manifest from $MANIFEST..."
|
||||
BB_MANIFEST=$($MC cat "$MANIFEST" 2>/dev/null) || {
|
||||
echo "::error::Failed to fetch big-blocks manifest from $MANIFEST"
|
||||
BIG_BLOCKS_DIR="$HOME/.reth-bench-big-blocks"
|
||||
PAYLOAD_DIR="$BIG_BLOCKS_DIR/payloads"
|
||||
MANIFEST="$BIG_BLOCKS_DIR/manifest.json"
|
||||
echo "BENCH_BIG_BLOCKS_DIR=${BIG_BLOCKS_DIR}" >> "$GITHUB_ENV"
|
||||
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "::error::Missing local big-blocks manifest at $MANIFEST"
|
||||
exit 1
|
||||
}
|
||||
BASE_SNAPSHOT=$(echo "$BB_MANIFEST" | jq -r '.base_snapshot // empty')
|
||||
fi
|
||||
|
||||
BASE_SNAPSHOT=$(jq -r '.base_snapshot // empty' "$MANIFEST")
|
||||
if [ -z "$BASE_SNAPSHOT" ]; then
|
||||
echo "::error::Big-blocks manifest missing base_snapshot field"
|
||||
exit 1
|
||||
fi
|
||||
echo "Big-blocks base snapshot: $BASE_SNAPSHOT"
|
||||
echo "BENCH_SNAPSHOT_NAME=${BASE_SNAPSHOT}" >> "$GITHUB_ENV"
|
||||
|
||||
REMOTE_HASH=$(echo "$BB_MANIFEST" | sha256sum | awk '{print $1}')
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
|
||||
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo "Big blocks up-to-date (hash: ${REMOTE_HASH:0:16}…)"
|
||||
echo "needed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Big blocks need update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}…}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
|
||||
echo "needed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "remote-hash=${REMOTE_HASH}" >> "$GITHUB_OUTPUT"
|
||||
if [ ! -d "$PAYLOAD_DIR" ]; then
|
||||
echo "::error::Missing local big-block payload directory at $PAYLOAD_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check if snapshot needs update
|
||||
id: snapshot-check
|
||||
run: |
|
||||
set +e
|
||||
.github/scripts/bench-reth-snapshot.sh --check
|
||||
rc=$?
|
||||
set -e
|
||||
case "$rc" in
|
||||
0) echo "needed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
10) echo "needed=true" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Snapshot check failed (exit $rc)"
|
||||
exit "$rc" ;;
|
||||
esac
|
||||
PAYLOAD_COUNT=$(find "$PAYLOAD_DIR" -name '*.json' | wc -l)
|
||||
if [ "$PAYLOAD_COUNT" -eq 0 ]; then
|
||||
echo "::error::No payload files found in $PAYLOAD_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Update status (snapshot needed)
|
||||
if: env.BENCH_COMMENT_ID && steps.snapshot-check.outputs.needed == 'true'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
const s = require('./.github/scripts/bench-update-status.js');
|
||||
await s({github, context, status: 'Building binaries (snapshot update pending)...'});
|
||||
echo "Big-blocks base snapshot: $BASE_SNAPSHOT"
|
||||
echo "Payload files: $PAYLOAD_COUNT"
|
||||
echo "BENCH_SNAPSHOT_NAME=${BASE_SNAPSHOT}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate local snapshot
|
||||
id: snapshot-check
|
||||
run: .github/scripts/bench-reth-snapshot.sh
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
prepare_source_dir() {
|
||||
local dir="$1"
|
||||
local ref="$2"
|
||||
|
||||
if [ -d "$dir" ]; then
|
||||
git -C "$dir" reset --hard HEAD
|
||||
git -C "$dir" clean -fdx
|
||||
git -C "$dir" fetch origin "$ref"
|
||||
else
|
||||
git clone . "$dir"
|
||||
fi
|
||||
|
||||
git -C "$dir" checkout --force "$ref"
|
||||
}
|
||||
|
||||
BASELINE_REF="${{ steps.refs.outputs.baseline-ref }}"
|
||||
if [ -d ../reth-baseline ]; then
|
||||
git -C ../reth-baseline fetch origin "$BASELINE_REF"
|
||||
else
|
||||
git clone . ../reth-baseline
|
||||
fi
|
||||
git -C ../reth-baseline checkout "$BASELINE_REF"
|
||||
prepare_source_dir ../reth-baseline "$BASELINE_REF"
|
||||
|
||||
FEATURE_REF="${{ steps.refs.outputs.feature-ref }}"
|
||||
if [ -d ../reth-feature ]; then
|
||||
git -C ../reth-feature fetch origin "$FEATURE_REF"
|
||||
else
|
||||
git clone . ../reth-feature
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
prepare_source_dir ../reth-feature "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
id: build
|
||||
@@ -862,15 +845,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download snapshot
|
||||
id: snapshot-download
|
||||
if: steps.snapshot-check.outputs.needed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BENCH_REPO: ${{ github.repository }}
|
||||
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/${{ needs.reth-bench-ack.outputs.big-blocks == 'true' && 'reth-bb' || 'reth' }}
|
||||
run: .github/scripts/bench-reth-snapshot.sh
|
||||
|
||||
# System tuning for reproducible benchmarks
|
||||
- name: System setup
|
||||
run: |
|
||||
@@ -941,45 +915,6 @@ jobs:
|
||||
rm -rf "$BENCH_WORK_DIR"
|
||||
mkdir -p "$BENCH_WORK_DIR"
|
||||
|
||||
- name: Download big blocks
|
||||
if: env.BENCH_BIG_BLOCKS == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BIG_BLOCKS_DIR="$HOME/.reth-bench-big-blocks"
|
||||
echo "BENCH_BIG_BLOCKS_DIR=${BIG_BLOCKS_DIR}" >> "$GITHUB_ENV"
|
||||
if [ "${{ steps.big-blocks-check.outputs.needed }}" = "false" ]; then
|
||||
echo "Big blocks cached at $BIG_BLOCKS_DIR, skipping download"
|
||||
echo "Payload files: $(find "$BIG_BLOCKS_DIR/payloads" -name '*.json' | wc -l)"
|
||||
exit 0
|
||||
fi
|
||||
MC="mc --config-dir /home/ubuntu/.mc"
|
||||
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
|
||||
rm -rf "$BIG_BLOCKS_DIR"; mkdir -p "$BIG_BLOCKS_DIR"
|
||||
|
||||
# Download and parse manifest
|
||||
echo "Downloading manifest from $MANIFEST..."
|
||||
$MC cat "$MANIFEST" > "$BIG_BLOCKS_DIR/manifest.json"
|
||||
UPLOAD_PATH=$(jq -r '.upload_path' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
COUNT=$(jq -r '.count' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
TARGET_GAS=$(jq -r '.target_gas' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
echo "Manifest: count=$COUNT, target_gas=$TARGET_GAS, archive=$UPLOAD_PATH"
|
||||
|
||||
# Download and extract archive
|
||||
ARCHIVE="minio/$UPLOAD_PATH"
|
||||
echo "Downloading big blocks from $ARCHIVE..."
|
||||
$MC cat "$ARCHIVE" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
|
||||
echo "Big blocks downloaded to $BIG_BLOCKS_DIR"
|
||||
|
||||
# Verify expected directory structure
|
||||
if [ ! -d "$BIG_BLOCKS_DIR/payloads" ]; then
|
||||
echo "::error::Big blocks archive missing expected payloads/ directory"
|
||||
ls -laR "$BIG_BLOCKS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
echo "Payload files: $(find "$BIG_BLOCKS_DIR/payloads" -name '*.json' | wc -l)"
|
||||
# Save manifest hash for freshness check on next run
|
||||
echo "${{ steps.big-blocks-check.outputs.remote-hash }}" > "$HOME/.reth-bench-big-blocks-hash"
|
||||
|
||||
- name: Start metrics proxy
|
||||
run: |
|
||||
BENCH_ID="ci-${{ github.run_id }}"
|
||||
@@ -1374,9 +1309,11 @@ jobs:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const bigBlocks = process.env.BENCH_BIG_BLOCKS === 'true';
|
||||
const steps_status = [
|
||||
...(bigBlocks ? [['validating local big-block data', '${{ steps.big-blocks-check.outcome }}']] : []),
|
||||
['validating local snapshot', '${{ steps.snapshot-check.outcome }}'],
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
@@ -1410,9 +1347,11 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const bigBlocks = process.env.BENCH_BIG_BLOCKS === 'true';
|
||||
const steps_status = [
|
||||
...(bigBlocks ? [['validating local big-block data', '${{ steps.big-blocks-check.outcome }}']] : []),
|
||||
['validating local snapshot', '${{ steps.snapshot-check.outcome }}'],
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
@@ -1436,6 +1375,11 @@ jobs:
|
||||
body: `cc @${process.env.BENCH_ACTOR}\n\n⚠️ Benchmark cancelled. [View logs](${jobUrl})`,
|
||||
});
|
||||
|
||||
- name: Clean build outputs
|
||||
if: always()
|
||||
run: |
|
||||
sudo rm -rf ../reth-baseline/target ../reth-feature/target "$BENCH_WORK_DIR" 2>/dev/null || true
|
||||
|
||||
- name: Restore system settings
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
554
Cargo.lock
generated
554
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
103
Cargo.toml
103
Cargo.toml
@@ -326,8 +326,8 @@ reth-cli = { path = "crates/cli/cli" }
|
||||
reth-cli-commands = { path = "crates/cli/commands" }
|
||||
reth-cli-runner = { path = "crates/cli/runner" }
|
||||
reth-cli-util = { path = "crates/cli/util" }
|
||||
reth-codecs = { version = "0.3.0", default-features = false }
|
||||
reth-codecs-derive = "0.3.0"
|
||||
reth-codecs = { version = "0.3.1", default-features = false }
|
||||
reth-codecs-derive = "0.3.1"
|
||||
reth-config = { path = "crates/config", default-features = false }
|
||||
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
|
||||
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
|
||||
@@ -395,7 +395,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
|
||||
reth-payload-primitives = { path = "crates/payload/primitives" }
|
||||
reth-payload-validator = { path = "crates/payload/validator" }
|
||||
reth-payload-util = { path = "crates/payload/util" }
|
||||
reth-primitives-traits = { version = "0.3.0", default-features = false }
|
||||
reth-primitives-traits = { version = "0.3.1", default-features = false }
|
||||
reth-provider = { path = "crates/storage/provider" }
|
||||
reth-prune = { path = "crates/prune/prune" }
|
||||
reth-prune-types = { path = "crates/prune/types", default-features = false }
|
||||
@@ -411,7 +411,7 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal
|
||||
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
|
||||
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
|
||||
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
|
||||
reth-rpc-traits = { version = "0.3.0", default-features = false }
|
||||
reth-rpc-traits = { version = "0.3.1", default-features = false }
|
||||
reth-stages = { path = "crates/stages/stages" }
|
||||
reth-stages-api = { path = "crates/stages/api" }
|
||||
reth-stages-types = { path = "crates/stages/types", default-features = false }
|
||||
@@ -430,17 +430,17 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
|
||||
reth-trie-db = { path = "crates/trie/db" }
|
||||
reth-trie-parallel = { path = "crates/trie/parallel" }
|
||||
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.0", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.1", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "38.0.0", default-features = false }
|
||||
revm-bytecode = { version = "10.0.0", default-features = false }
|
||||
revm-database = { version = "13.0.0", default-features = false }
|
||||
revm-state = { version = "11.0.0", default-features = false }
|
||||
revm-primitives = { version = "23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "11.0.0", default-features = false }
|
||||
revm-inspectors = "0.39.0"
|
||||
revm = { version = "=37.0.0", default-features = false }
|
||||
revm-bytecode = { version = "=10.0.0", default-features = false }
|
||||
revm-database = { version = "=13.0.0", default-features = false }
|
||||
revm-state = { version = "=11.0.0", default-features = false }
|
||||
revm-primitives = { version = "=23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "=35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "=11.0.0", default-features = false }
|
||||
revm-inspectors = "=0.39.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -449,40 +449,40 @@ alloy-sol-types = { version = "1.5.6", default-features = false }
|
||||
|
||||
alloy-chains = { version = "0.2.33", default-features = false }
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.4", default-features = false }
|
||||
alloy-evm = { version = "0.33.0", default-features = false }
|
||||
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
|
||||
alloy-trie = { version = "0.9.4", default-features = false }
|
||||
|
||||
alloy-hardforks = "0.4.7"
|
||||
|
||||
alloy-consensus = { version = "2.0.0", default-features = false }
|
||||
alloy-contract = { version = "2.0.0", default-features = false }
|
||||
alloy-eips = { version = "2.0.0", default-features = false }
|
||||
alloy-genesis = { version = "2.0.0", default-features = false }
|
||||
alloy-json-rpc = { version = "2.0.0", default-features = false }
|
||||
alloy-network = { version = "2.0.0", default-features = false }
|
||||
alloy-network-primitives = { version = "2.0.0", default-features = false }
|
||||
alloy-provider = { version = "2.0.0", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-client = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types = { version = "2.0.0", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "2.0.0", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "2.0.0", default-features = false }
|
||||
alloy-serde = { version = "2.0.0", default-features = false }
|
||||
alloy-signer = { version = "2.0.0", default-features = false }
|
||||
alloy-signer-local = { version = "2.0.0", default-features = false }
|
||||
alloy-transport = { version = "2.0.0" }
|
||||
alloy-transport-http = { version = "2.0.0", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "2.0.0", default-features = false }
|
||||
alloy-transport-ws = { version = "2.0.0", default-features = false }
|
||||
alloy-consensus = { version = "2.0.1", default-features = false }
|
||||
alloy-contract = { version = "2.0.1", default-features = false }
|
||||
alloy-eips = { version = "2.0.1", default-features = false }
|
||||
alloy-genesis = { version = "2.0.1", default-features = false }
|
||||
alloy-json-rpc = { version = "2.0.1", default-features = false }
|
||||
alloy-network = { version = "2.0.1", default-features = false }
|
||||
alloy-network-primitives = { version = "2.0.1", default-features = false }
|
||||
alloy-provider = { version = "2.0.1", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-client = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types = { version = "2.0.1", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "2.0.1", default-features = false }
|
||||
alloy-serde = { version = "2.0.1", default-features = false }
|
||||
alloy-signer = { version = "2.0.1", default-features = false }
|
||||
alloy-signer-local = { version = "2.0.1", default-features = false }
|
||||
alloy-transport = { version = "2.0.1" }
|
||||
alloy-transport-http = { version = "2.0.1", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "2.0.1", default-features = false }
|
||||
alloy-transport-ws = { version = "2.0.1", default-features = false }
|
||||
|
||||
# misc
|
||||
either = { version = "1.15.0", default-features = false }
|
||||
@@ -700,3 +700,24 @@ vergen-git2 = "9.1.0"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
[patch.crates-io]
|
||||
revm = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "a2c7a41977b468d016a339f560acb76e002766f3" }
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6" }
|
||||
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
|
||||
@@ -69,6 +69,7 @@ default = [
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc",
|
||||
"asm-keccak",
|
||||
"keccak-cache-global",
|
||||
"min-debug-logs",
|
||||
]
|
||||
|
||||
@@ -89,6 +90,12 @@ asm-keccak = [
|
||||
"revm-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
keccak-cache-global = [
|
||||
"reth-node-core/keccak-cache-global",
|
||||
"reth-node-ethereum/keccak-cache-global",
|
||||
"alloy-primitives/keccak-cache-global",
|
||||
]
|
||||
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-ethereum-cli/min-debug-logs",
|
||||
|
||||
@@ -21,6 +21,8 @@ pub(crate) struct BenchContext {
|
||||
pub(crate) auth_provider: RootProvider<AnyNetwork>,
|
||||
/// The block provider is used for block queries.
|
||||
pub(crate) block_provider: RootProvider<AnyNetwork>,
|
||||
/// The local regular RPC provider is used for non-authenticated node RPCs like `testing_*`.
|
||||
pub(crate) local_rpc_provider: RootProvider<AnyNetwork>,
|
||||
/// The benchmark mode, which defines whether the benchmark should run for a closed or open
|
||||
/// range of blocks.
|
||||
pub(crate) benchmark_mode: BenchMode,
|
||||
@@ -83,6 +85,11 @@ impl BenchContext {
|
||||
let client = ClientBuilder::default().connect_with(auth_transport).await?;
|
||||
let auth_provider = RootProvider::<AnyNetwork>::new(client);
|
||||
|
||||
let local_rpc_url = Url::parse(&bench_args.local_rpc_url)?;
|
||||
info!(target: "reth-bench", "Connecting to local regular RPC at {} for testing namespace calls", local_rpc_url);
|
||||
let local_rpc_provider =
|
||||
RootProvider::<AnyNetwork>::new(ClientBuilder::default().http(local_rpc_url));
|
||||
|
||||
// Computes the block range for the benchmark.
|
||||
//
|
||||
// - If `--advance` is provided, fetches the latest block from the engine and sets:
|
||||
@@ -159,6 +166,7 @@ impl BenchContext {
|
||||
Ok(Self {
|
||||
auth_provider,
|
||||
block_provider,
|
||||
local_rpc_provider,
|
||||
benchmark_mode,
|
||||
next_block,
|
||||
use_reth_namespace,
|
||||
|
||||
@@ -14,14 +14,24 @@ use crate::{
|
||||
block_to_new_payload, call_forkchoice_updated_with_reth, call_new_payload_with_reth,
|
||||
},
|
||||
};
|
||||
use alloy_provider::{ext::DebugApi, Provider};
|
||||
use alloy_rpc_types_engine::ForkchoiceState;
|
||||
use alloy_consensus::TxEnvelope;
|
||||
use alloy_eips::Encodable2718;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_provider::{
|
||||
ext::DebugApi,
|
||||
network::{AnyNetwork, AnyRpcBlock},
|
||||
Provider, RootProvider,
|
||||
};
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionData, ExecutionPayloadEnvelopeV5, ForkchoiceState, PayloadAttributes,
|
||||
};
|
||||
use clap::Parser;
|
||||
use eyre::{Context, OptionExt};
|
||||
use eyre::{bail, ensure, Context, OptionExt};
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
|
||||
use reth_node_core::args::BenchmarkArgs;
|
||||
use reth_rpc_api::{RethNewPayloadInput, TestingBuildBlockRequestV1};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -32,6 +42,22 @@ pub struct Command {
|
||||
#[arg(long, value_name = "RPC_URL", verbatim_doc_comment)]
|
||||
rpc_url: String,
|
||||
|
||||
/// Build a separate fork with `testing_buildBlockV1` and alternate forkchoice updates between
|
||||
/// the canonical chain and that fork on every block while the fork grows up to the configured
|
||||
/// depth.
|
||||
///
|
||||
/// This requires enabling the hidden `testing` RPC module on the target node,
|
||||
/// for example with `reth node --http --http.api eth,testing`.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DEPTH",
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "8",
|
||||
value_parser = parse_reorg_depth,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
reorg: Option<usize>,
|
||||
|
||||
/// How long to wait after a forkchoice update before sending the next payload.
|
||||
///
|
||||
/// Accepts a duration string (e.g. `100ms`, `2s`) or a bare integer treated as
|
||||
@@ -75,22 +101,87 @@ pub struct Command {
|
||||
)]
|
||||
rpc_block_buffer_size: usize,
|
||||
|
||||
/// Weather to enable bal by default or not.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
enable_bal: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
benchmark: BenchmarkArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PreparedBuiltBlock {
|
||||
block_hash: B256,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueuedForkBlock {
|
||||
block_number: u64,
|
||||
prepared: PreparedBuiltBlock,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReorgState {
|
||||
depth: usize,
|
||||
fork_length: usize,
|
||||
branch_point_hash: Option<B256>,
|
||||
fork_parent_hash: Option<B256>,
|
||||
}
|
||||
|
||||
impl ReorgState {
|
||||
const fn new(depth: usize) -> Self {
|
||||
Self { depth, fork_length: 0, branch_point_hash: None, fork_parent_hash: None }
|
||||
}
|
||||
|
||||
const fn push_fork_head(&mut self, canonical_parent_hash: B256, fork_head_hash: B256) {
|
||||
if self.fork_length == 0 {
|
||||
self.branch_point_hash = Some(canonical_parent_hash);
|
||||
}
|
||||
self.fork_length += 1;
|
||||
self.fork_parent_hash = Some(fork_head_hash);
|
||||
}
|
||||
|
||||
fn forkchoice_state(&self, fork_head_hash: B256) -> eyre::Result<ForkchoiceState> {
|
||||
let branch_point_hash = self.branch_point_hash.ok_or_eyre("missing reorg branch point")?;
|
||||
|
||||
Ok(ForkchoiceState {
|
||||
head_block_hash: fork_head_hash,
|
||||
safe_block_hash: branch_point_hash,
|
||||
finalized_block_hash: branch_point_hash,
|
||||
})
|
||||
}
|
||||
|
||||
const fn reset(&mut self) {
|
||||
self.fork_length = 0;
|
||||
self.branch_point_hash = None;
|
||||
self.fork_parent_hash = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute `benchmark new-payload-fcu` command
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
if self.reorg.is_some() && self.benchmark.rlp_blocks {
|
||||
bail!("--reorg cannot be combined with --rlp-blocks")
|
||||
}
|
||||
if self.reorg.is_some() && self.enable_bal {
|
||||
bail!("--reorg cannot be combined with --enable-bal")
|
||||
}
|
||||
|
||||
// Log mode configuration
|
||||
if let Some(duration) = self.wait_time {
|
||||
info!(target: "reth-bench", "Using wait-time mode with {}ms minimum interval between blocks", duration.as_millis());
|
||||
}
|
||||
if let Some(depth) = self.reorg {
|
||||
info!(target: "reth-bench", depth, "Using testing_buildBlockV1 reorg mode");
|
||||
}
|
||||
|
||||
let BenchContext {
|
||||
benchmark_mode,
|
||||
block_provider,
|
||||
auth_provider,
|
||||
local_rpc_provider,
|
||||
next_block,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
@@ -178,7 +269,8 @@ impl Command {
|
||||
let mut blocks_processed = 0u64;
|
||||
let total_benchmark_duration = Instant::now();
|
||||
let mut total_wait_time = Duration::ZERO;
|
||||
|
||||
let mut reorg_state = self.reorg.map(ReorgState::new);
|
||||
let mut queued_fork_block = None;
|
||||
while let Some((block, head, safe, finalized, rlp)) = {
|
||||
let wait_start = Instant::now();
|
||||
let result = blocks.try_next().await?;
|
||||
@@ -188,17 +280,21 @@ impl Command {
|
||||
let gas_used = block.header.gas_used;
|
||||
let gas_limit = block.header.gas_limit;
|
||||
let block_number = block.header.number;
|
||||
let canonical_parent_hash = block.header.parent_hash;
|
||||
let transaction_count = block.transactions.len() as u64;
|
||||
|
||||
debug!(target: "reth-bench", ?block_number, "Sending payload");
|
||||
|
||||
let forkchoice_state = ForkchoiceState {
|
||||
let deferred_branch_start_block = reorg_state
|
||||
.as_ref()
|
||||
.filter(|state| state.fork_length == 0 && queued_fork_block.is_none())
|
||||
.map(|_| block.clone());
|
||||
let canonical_forkchoice_state = ForkchoiceState {
|
||||
head_block_hash: head,
|
||||
safe_block_hash: safe,
|
||||
finalized_block_hash: finalized,
|
||||
};
|
||||
|
||||
let bal = if rlp.is_none() && block.header.block_access_list_hash.is_some() {
|
||||
let bal = if rlp.is_none() &&
|
||||
(block.header.block_access_list_hash.is_some() || self.enable_bal)
|
||||
{
|
||||
Some(fetch_block_access_list(&block_provider, block.header.number).await?)
|
||||
} else {
|
||||
None
|
||||
@@ -212,10 +308,11 @@ impl Command {
|
||||
no_wait_for_caches,
|
||||
bal,
|
||||
)?;
|
||||
|
||||
debug!(target: "reth-bench", ?block_number, "Sending payload");
|
||||
let start = Instant::now();
|
||||
let server_timings =
|
||||
call_new_payload_with_reth(&auth_provider, version, params).await?;
|
||||
|
||||
let np_latency =
|
||||
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
|
||||
let new_payload_result = NewPayloadResult {
|
||||
@@ -236,17 +333,12 @@ impl Command {
|
||||
};
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
call_forkchoice_updated_with_reth(&auth_provider, version, forkchoice_state).await?;
|
||||
call_forkchoice_updated_with_reth(&auth_provider, version, canonical_forkchoice_state)
|
||||
.await?;
|
||||
let fcu_latency = fcu_start.elapsed();
|
||||
|
||||
let total_latency = if server_timings.is_some() {
|
||||
// When using server-side latency for newPayload, derive total from the
|
||||
// independently measured components to avoid mixing server-side and
|
||||
// client-side (network-inclusive) timings.
|
||||
np_latency + fcu_latency
|
||||
} else {
|
||||
start.elapsed()
|
||||
};
|
||||
let total_latency =
|
||||
if server_timings.is_some() { np_latency + fcu_latency } else { start.elapsed() };
|
||||
let combined_result = CombinedResult {
|
||||
block_number,
|
||||
gas_limit,
|
||||
@@ -256,6 +348,88 @@ impl Command {
|
||||
total_latency,
|
||||
};
|
||||
|
||||
if let Some(reorg_state) = reorg_state.as_mut() {
|
||||
if queued_fork_block.is_none() && reorg_state.fork_length == 0 {
|
||||
// A branch start uses a canonical parent, so it can be built lazily here
|
||||
// instead of being queued ahead of time.
|
||||
let block = deferred_branch_start_block
|
||||
.as_ref()
|
||||
.ok_or_eyre("missing deferred fork block for reorg branch start")?;
|
||||
queued_fork_block = Some(QueuedForkBlock {
|
||||
block_number,
|
||||
prepared: prepare_built_block(
|
||||
&local_rpc_provider,
|
||||
block,
|
||||
canonical_parent_hash,
|
||||
no_wait_for_caches,
|
||||
)
|
||||
.await?,
|
||||
});
|
||||
}
|
||||
|
||||
let queued = queued_fork_block
|
||||
.take()
|
||||
.ok_or_eyre("missing queued fork block for reorg replay")?;
|
||||
ensure!(
|
||||
queued.block_number == block_number,
|
||||
"queued fork block {} does not match source block {}",
|
||||
queued.block_number,
|
||||
block_number
|
||||
);
|
||||
let prepared = queued.prepared;
|
||||
|
||||
call_new_payload_with_reth(&auth_provider, None, prepared.params).await?;
|
||||
|
||||
reorg_state.push_fork_head(canonical_parent_hash, prepared.block_hash);
|
||||
let forkchoice_state = reorg_state.forkchoice_state(prepared.block_hash)?;
|
||||
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
block_number,
|
||||
branch_point = %forkchoice_state.safe_block_hash,
|
||||
fork_head = %prepared.block_hash,
|
||||
fork_depth = reorg_state.fork_length,
|
||||
max_reorg_depth = reorg_state.depth,
|
||||
"Switching forkchoice to reorg branch"
|
||||
);
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
call_forkchoice_updated_with_reth(&auth_provider, None, forkchoice_state).await?;
|
||||
let _fork_fcu_latency = fcu_start.elapsed();
|
||||
|
||||
let next_fork_block_number = block_number + 1;
|
||||
if reorg_state.fork_length < reorg_state.depth {
|
||||
queued_fork_block = queue_fork_block(
|
||||
&block_provider,
|
||||
&local_rpc_provider,
|
||||
&benchmark_mode,
|
||||
next_fork_block_number,
|
||||
Some(prepared.block_hash),
|
||||
no_wait_for_caches,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
block_number,
|
||||
reorg_depth = reorg_state.depth,
|
||||
"Resetting reorg branch after reaching max depth"
|
||||
);
|
||||
|
||||
// `testing_buildBlockV1` resolves the parent from canonical state, so switch
|
||||
// back to the source chain before reseeding the next queued fork block.
|
||||
call_forkchoice_updated_with_reth(
|
||||
&auth_provider,
|
||||
version,
|
||||
canonical_forkchoice_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
reorg_state.reset();
|
||||
queued_fork_block = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude time spent waiting on the block prefetch channel from the benchmark duration.
|
||||
// We want to measure engine throughput, not RPC fetch latency.
|
||||
blocks_processed += 1;
|
||||
@@ -312,3 +486,155 @@ impl Command {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_built_block(
|
||||
block_provider: &RootProvider<AnyNetwork>,
|
||||
block: &AnyRpcBlock,
|
||||
parent_block_hash: B256,
|
||||
no_wait_for_caches: bool,
|
||||
) -> eyre::Result<PreparedBuiltBlock> {
|
||||
const MAX_BUILD_ATTEMPTS: usize = 10;
|
||||
const BUILD_RETRY_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
let request = build_block_request(block, parent_block_hash)?;
|
||||
let built_payload: ExecutionPayloadEnvelopeV5 = {
|
||||
let mut attempts_remaining = MAX_BUILD_ATTEMPTS;
|
||||
|
||||
loop {
|
||||
match block_provider.client().request("testing_buildBlockV1", [request.clone()]).await {
|
||||
Ok(payload) => break payload,
|
||||
Err(err) if attempts_remaining > 1 && is_retryable_build_block_error(&err) => {
|
||||
warn!(
|
||||
target: "reth-bench",
|
||||
block_number = block.header.number,
|
||||
%parent_block_hash,
|
||||
attempts_remaining,
|
||||
error = %err,
|
||||
"Retrying testing_buildBlockV1 after transient fork build failure"
|
||||
);
|
||||
attempts_remaining -= 1;
|
||||
tokio::time::sleep(BUILD_RETRY_INTERVAL).await;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err).wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to build block {} via testing_buildBlockV1",
|
||||
block.header.number
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let payload = &built_payload.execution_payload.payload_inner.payload_inner;
|
||||
let block_hash = payload.block_hash;
|
||||
let (payload, sidecar) = built_payload
|
||||
.into_payload_and_sidecar(block.header.parent_beacon_block_root.unwrap_or_default());
|
||||
// Fork payloads are built immediately before the next `testing_buildBlockV1` call. Leaving
|
||||
// reth's default persistence wait enabled here gives the regular RPC side a consistent base
|
||||
// state for the next synthetic fork block build.
|
||||
let params = serde_json::to_value((
|
||||
RethNewPayloadInput::ExecutionData(ExecutionData { payload, sidecar }),
|
||||
None::<bool>,
|
||||
no_wait_for_caches.then_some(false),
|
||||
))?;
|
||||
|
||||
Ok(PreparedBuiltBlock { block_hash, params })
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn queue_fork_block(
|
||||
block_provider: &RootProvider<AnyNetwork>,
|
||||
local_rpc_provider: &RootProvider<AnyNetwork>,
|
||||
benchmark_mode: &crate::bench_mode::BenchMode,
|
||||
block_number: u64,
|
||||
parent_block_hash: Option<B256>,
|
||||
no_wait_for_caches: bool,
|
||||
) -> eyre::Result<Option<QueuedForkBlock>> {
|
||||
if !benchmark_mode.contains(block_number) {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
let future_block = block_provider
|
||||
.get_block_by_number(alloy_eips::BlockNumberOrTag::Number(block_number))
|
||||
.full()
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to fetch block by number {block_number}"))?
|
||||
.ok_or_eyre("Block not found")?;
|
||||
let parent_block_hash = parent_block_hash.unwrap_or(future_block.header.parent_hash);
|
||||
|
||||
Ok(Some(QueuedForkBlock {
|
||||
block_number,
|
||||
prepared: prepare_built_block(
|
||||
local_rpc_provider,
|
||||
&future_block,
|
||||
parent_block_hash,
|
||||
no_wait_for_caches,
|
||||
)
|
||||
.await?,
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_retryable_build_block_error(err: &alloy_transport::TransportError) -> bool {
|
||||
let message = err.to_string();
|
||||
message.contains("block not found: hash") ||
|
||||
message.contains("block hash not found for block number")
|
||||
}
|
||||
|
||||
fn build_block_request(
|
||||
block: &AnyRpcBlock,
|
||||
parent_block_hash: B256,
|
||||
) -> eyre::Result<TestingBuildBlockRequestV1> {
|
||||
let mut transactions = block
|
||||
.clone()
|
||||
.try_into_transactions()
|
||||
.map_err(|_| eyre::eyre!("Block transactions must be fetched in full for --reorg"))?
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let tx: TxEnvelope =
|
||||
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type in RPC block"))?;
|
||||
if tx.is_eip4844() {
|
||||
return Ok(None)
|
||||
}
|
||||
Ok(Some(tx.encoded_2718().into()))
|
||||
})
|
||||
.filter_map(|tx| tx.transpose())
|
||||
.collect::<eyre::Result<Vec<_>>>()?;
|
||||
|
||||
// `testing_buildBlockV1` only takes raw transaction bytes, so we exclude blob transactions
|
||||
// from the synthetic fork blocks rather than trying to reconstruct their sidecars.
|
||||
// Keep only 90% of the remaining transactions so the alternate branch produces a materially
|
||||
// different post-state instead of only differing by header data.
|
||||
let keep = transactions.len().saturating_mul(9) / 10;
|
||||
transactions.truncate(keep);
|
||||
|
||||
let rpc_block = block.clone().into_inner();
|
||||
|
||||
Ok(TestingBuildBlockRequestV1 {
|
||||
parent_block_hash,
|
||||
payload_attributes: PayloadAttributes {
|
||||
timestamp: block.header.timestamp,
|
||||
prev_randao: block.header.mix_hash.unwrap_or_default(),
|
||||
suggested_fee_recipient: block.header.beneficiary,
|
||||
withdrawals: rpc_block.withdrawals.map(|withdrawals| withdrawals.into_inner()),
|
||||
parent_beacon_block_root: block.header.parent_beacon_block_root,
|
||||
slot_number: block.header.slot_number,
|
||||
},
|
||||
transactions,
|
||||
extra_data: Some(block.header.extra_data.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_reorg_depth(value: &str) -> Result<usize, String> {
|
||||
let depth = value
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("invalid reorg depth {value:?}, expected a positive integer"))?;
|
||||
|
||||
if depth == 0 {
|
||||
return Err("reorg depth must be greater than 0".to_string())
|
||||
}
|
||||
|
||||
Ok(depth)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ impl Command {
|
||||
rlp_blocks,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
..
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let total_blocks = benchmark_mode.total_blocks();
|
||||
|
||||
@@ -150,16 +150,22 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
// commands can proceed.
|
||||
debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
|
||||
reth_fs_util::create_dir_all(&rocksdb_path)?;
|
||||
RocksDBProvider::builder(data_dir.rocksdb())
|
||||
let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.build()?
|
||||
.with_database_log_level(self.db.log_level);
|
||||
if let Some(cache_size) = self.db.rocksdb_block_cache_size {
|
||||
builder = builder.with_block_cache_size(cache_size);
|
||||
}
|
||||
builder.build()?
|
||||
} else {
|
||||
RocksDBProvider::builder(data_dir.rocksdb())
|
||||
let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.with_read_only(!access.is_read_write())
|
||||
.build()?
|
||||
.with_read_only(!access.is_read_write());
|
||||
if let Some(cache_size) = self.db.rocksdb_block_cache_size {
|
||||
builder = builder.with_block_cache_size(cache_size);
|
||||
}
|
||||
builder.build()?
|
||||
};
|
||||
|
||||
let provider_factory =
|
||||
|
||||
@@ -14,7 +14,7 @@ use reth_db_api::{
|
||||
table::{Compress, Decompress, DupSort, Table},
|
||||
tables,
|
||||
transaction::DbTx,
|
||||
RawKey, RawTable, Receipts, TableViewer, Transactions,
|
||||
RawKey, RawTable, TableViewer,
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
|
||||
@@ -264,15 +264,12 @@ impl Command {
|
||||
);
|
||||
}
|
||||
StaticFileSegment::Transactions => {
|
||||
let transaction = <<Transactions as Table>::Value>::decompress(
|
||||
content[0].as_slice(),
|
||||
)?;
|
||||
let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
|
||||
println!("{}", serde_json::to_string_pretty(&transaction)?);
|
||||
}
|
||||
StaticFileSegment::Receipts => {
|
||||
let receipt = <<Receipts as Table>::Value>::decompress(
|
||||
content[0].as_slice(),
|
||||
)?;
|
||||
let receipt =
|
||||
ReceiptTy::<N>::decompress(content[0].as_slice())?;
|
||||
println!("{}", serde_json::to_string_pretty(&receipt)?);
|
||||
}
|
||||
StaticFileSegment::TransactionSenders => {
|
||||
|
||||
@@ -50,8 +50,13 @@ where
|
||||
info!(target: "reth::cli", new_tip = ?header.num_hash(), "Setting up dummy EVM chain before importing state.");
|
||||
|
||||
let static_file_provider = provider_rw.static_file_provider();
|
||||
// Write EVM dummy data up to `header - 1` block
|
||||
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
|
||||
// Write EVM dummy data up to `header - 1` block. Skip when the supplied
|
||||
// header is at block 0: `header.number() - 1` would underflow in u64 to
|
||||
// `u64::MAX`, sending `append_dummy_chain` into a 1..=u64::MAX loop that
|
||||
// exhausts memory before failing.
|
||||
if header.number() > 0 {
|
||||
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "Appending first valid block.");
|
||||
|
||||
@@ -191,7 +196,13 @@ mod tests {
|
||||
use alloy_primitives::{address, b256};
|
||||
use reth_db_common::init::init_genesis;
|
||||
use reth_provider::{test_utils::create_test_provider_factory, DatabaseProviderFactory};
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
io::Write,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
@@ -264,4 +275,45 @@ mod tests {
|
||||
|
||||
assert_eq!(actual_next_height, expected_next_height);
|
||||
}
|
||||
|
||||
/// Regression: a header at block 0 used to send `append_dummy_chain` into
|
||||
/// a `1..=u64::MAX` loop because `header.number() - 1` underflowed in
|
||||
/// u64. The guard `if header.number() > 0` skips the dummy-chain step
|
||||
/// when there is no pre-genesis range to backfill, so `header_factory`
|
||||
/// is never invoked.
|
||||
#[test]
|
||||
fn test_setup_without_evm_skips_dummy_chain_for_genesis_header() {
|
||||
let header = Header { number: 0, ..Default::default() };
|
||||
let header_hash = header.hash_slow();
|
||||
|
||||
let provider_factory = create_test_provider_factory();
|
||||
init_genesis(&provider_factory).unwrap();
|
||||
let provider_rw = provider_factory.database_provider_rw().unwrap();
|
||||
|
||||
let factory_calls = Arc::new(AtomicU64::new(0));
|
||||
let factory_calls_inner = Arc::clone(&factory_calls);
|
||||
|
||||
// The Result of `setup_without_evm` itself is not asserted: with
|
||||
// `number == 0` plus a genesis already written by `init_genesis`,
|
||||
// the subsequent `append_first_block` may legitimately fail. The
|
||||
// bug under test is the OOM in the dummy-chain loop, observable
|
||||
// through the factory-call counter below.
|
||||
let _ = setup_without_evm(
|
||||
&provider_rw,
|
||||
SealedHeader::new(header, header_hash),
|
||||
move |number| {
|
||||
// Bound calls so a regression cannot exhaust the test
|
||||
// runner's memory; the only correct value here is 0.
|
||||
let n = factory_calls_inner.fetch_add(1, Ordering::Relaxed);
|
||||
assert!(n < 8, "header_factory must not be invoked for a genesis-block header");
|
||||
Header { number, ..Default::default() }
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
factory_calls.load(Ordering::Relaxed),
|
||||
0,
|
||||
"append_dummy_chain must be skipped when header.number() == 0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ impl<C: ChainSpecParser> DownloadArgs<C> {
|
||||
)
|
||||
}
|
||||
|
||||
config.peers.trusted_nodes_only = self.network.trusted_only;
|
||||
config.peers.trusted_nodes_only |= self.network.trusted_only;
|
||||
|
||||
let default_secret_key_path = data_dir.p2p_secret();
|
||||
let p2p_secret_key = self.network.secret_key(default_secret_key_path)?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::common::{
|
||||
EnvironmentArgs,
|
||||
};
|
||||
use alloy_consensus::{transaction::TxHashRef, BlockHeader, TxReceipt};
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use clap::Parser;
|
||||
use eyre::WrapErr;
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
|
||||
@@ -12,14 +13,19 @@ use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_evm::{execute::Executor, ConfigureEvm};
|
||||
use reth_primitives_traits::{format_gas_throughput, BlockBody, GotExpected};
|
||||
use reth_primitives_traits::{format_gas_throughput, Account, BlockBody, GotExpected};
|
||||
use reth_provider::{
|
||||
BlockNumReader, BlockReader, ChainSpecProvider, DatabaseProviderFactory, ReceiptProvider,
|
||||
StaticFileProviderFactory, TransactionVariant,
|
||||
};
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_revm::{
|
||||
database::StateProviderDatabase,
|
||||
db::{states::reverts::AccountInfoRevert, BundleState},
|
||||
};
|
||||
use reth_stages::stages::calculate_gas_used_from_headers;
|
||||
use reth_storage_api::{ChangeSetReader, DBProvider, StorageChangeSetReader};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
@@ -68,13 +74,18 @@ impl<C: ChainSpecParser> Command<C> {
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
|
||||
/// Execute `re-execute` command
|
||||
pub async fn execute<N>(
|
||||
self,
|
||||
mut self,
|
||||
components: impl CliComponentsBuilder<N>,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
{
|
||||
// Default to 4GB RocksDB block cache for re-execute unless explicitly set.
|
||||
if self.env.db.rocksdb_block_cache_size.is_none() {
|
||||
self.env.db.rocksdb_block_cache_size = Some(4 << 30);
|
||||
}
|
||||
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
|
||||
|
||||
let components = components(provider_factory.chain_spec());
|
||||
@@ -108,15 +119,6 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
min_block..=max_block,
|
||||
)?;
|
||||
|
||||
let db_at = {
|
||||
let provider_factory = provider_factory.clone();
|
||||
move |block_number: u64| {
|
||||
StateProviderDatabase(
|
||||
provider_factory.history_by_block_number(block_number).unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let skip_invalid_blocks = self.skip_invalid_blocks;
|
||||
let blocks_per_chunk = self.blocks_per_chunk;
|
||||
let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
|
||||
@@ -132,13 +134,23 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let provider_factory = provider_factory.clone();
|
||||
let evm_config = components.evm_config().clone();
|
||||
let consensus = components.consensus().clone();
|
||||
let db_at = db_at.clone();
|
||||
let stats_tx = stats_tx.clone();
|
||||
let info_tx = info_tx.clone();
|
||||
let cancellation = cancellation.clone();
|
||||
let next_block = Arc::clone(&next_block);
|
||||
tasks.spawn_blocking(move || {
|
||||
let executor_lifetime = Duration::from_secs(600);
|
||||
let provider = provider_factory.database_provider_ro()?.disable_long_read_transaction_safety();
|
||||
|
||||
let db_at = {
|
||||
|block_number: u64| {
|
||||
StateProviderDatabase(
|
||||
provider
|
||||
.history_by_block_number(block_number)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
if cancellation.is_cancelled() {
|
||||
@@ -248,11 +260,28 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
if executor.size_hint() > 5_000_000 ||
|
||||
executor_created.elapsed() > executor_lifetime
|
||||
{
|
||||
executor =
|
||||
evm_config.batch_executor(db_at(block.number()));
|
||||
let last_block = block.number();
|
||||
let old_executor = std::mem::replace(
|
||||
&mut executor,
|
||||
evm_config.batch_executor(db_at(last_block)),
|
||||
);
|
||||
let bundle = old_executor.into_state().take_bundle();
|
||||
verify_bundle_against_changesets(
|
||||
&provider,
|
||||
&bundle,
|
||||
last_block,
|
||||
)?;
|
||||
executor_created = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Full verification at chunk end for remaining unverified blocks
|
||||
let bundle = executor.into_state().take_bundle();
|
||||
verify_bundle_against_changesets(
|
||||
&provider,
|
||||
&bundle,
|
||||
chunk_end - 1,
|
||||
)?;
|
||||
}
|
||||
|
||||
eyre::Ok(())
|
||||
@@ -333,3 +362,98 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies reverts against database changesets.
|
||||
///
|
||||
/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
|
||||
/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
|
||||
/// (from DB storage wipe) absent from reverts.
|
||||
fn verify_bundle_against_changesets<P>(
|
||||
provider: &P,
|
||||
bundle: &BundleState,
|
||||
last_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
P: ChangeSetReader + StorageChangeSetReader,
|
||||
{
|
||||
// Verify reverts against changesets per block
|
||||
for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
|
||||
let block_number = last_block - i as u64;
|
||||
|
||||
let mut cs_accounts: HashMap<Address, Option<Account>> = provider
|
||||
.account_block_changeset(block_number)?
|
||||
.into_iter()
|
||||
.map(|cs| (cs.address, cs.info))
|
||||
.collect();
|
||||
|
||||
let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
|
||||
for (bna, entry) in provider.storage_changeset(block_number)? {
|
||||
cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
|
||||
}
|
||||
|
||||
for (addr, revert) in block_reverts {
|
||||
// Verify account info
|
||||
match &revert.account {
|
||||
AccountInfoRevert::DoNothing => {
|
||||
eyre::ensure!(
|
||||
!cs_accounts.contains_key(addr),
|
||||
"Block {block_number}: account {addr} in changeset but revert is DoNothing",
|
||||
);
|
||||
}
|
||||
AccountInfoRevert::DeleteIt => {
|
||||
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
|
||||
eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
|
||||
})?;
|
||||
eyre::ensure!(
|
||||
cs_info.is_none(),
|
||||
"Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
|
||||
);
|
||||
}
|
||||
AccountInfoRevert::RevertTo(info) => {
|
||||
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
|
||||
eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
|
||||
})?;
|
||||
let revert_acct = Some(Account::from(info));
|
||||
eyre::ensure!(
|
||||
revert_acct == cs_info,
|
||||
"Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify storage slots — remove matched changeset entries as we go
|
||||
let mut cs_slots = cs_storage.get_mut(addr);
|
||||
for (slot_key, revert_slot) in &revert.storage {
|
||||
let b256_key = B256::from(*slot_key);
|
||||
match cs_slots.as_mut().and_then(|s| s.remove(&b256_key)) {
|
||||
Some(cs_value) => eyre::ensure!(
|
||||
revert_slot.to_previous_value() == cs_value,
|
||||
"Block {block_number}: {addr} slot {b256_key} mismatch: \
|
||||
revert={} cs={cs_value}",
|
||||
revert_slot.to_previous_value(),
|
||||
),
|
||||
None => eyre::ensure!(
|
||||
revert.wipe_storage,
|
||||
"Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining cs_storage slots for this address must be from a destroyed account
|
||||
if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
|
||||
eyre::ensure!(
|
||||
revert.wipe_storage,
|
||||
"Block {block_number}: {addr} has {} unmatched storage slots in changeset",
|
||||
remaining.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining cs_accounts entries had no corresponding revert
|
||||
if let Some(addr) = cs_accounts.keys().next() {
|
||||
eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use reth_db_api::{
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_node_api::HeaderTy;
|
||||
use reth_node_api::{HeaderTy, TxTy};
|
||||
use reth_node_core::dirs::{ChainPath, DataDirPath};
|
||||
use reth_provider::{
|
||||
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
|
||||
@@ -88,7 +88,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
|
||||
)
|
||||
})??;
|
||||
output_db.update(|tx| {
|
||||
tx.import_table_with_range::<tables::BlockOmmers, _>(
|
||||
tx.import_table_with_range::<tables::BlockOmmers<HeaderTy<N>>, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from),
|
||||
to,
|
||||
@@ -110,7 +110,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
|
||||
})??;
|
||||
|
||||
output_db.update(|tx| {
|
||||
tx.import_table_with_range::<tables::Transactions, _>(
|
||||
tx.import_table_with_range::<tables::Transactions<TxTy<N>>, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from_tx),
|
||||
to_tx,
|
||||
|
||||
@@ -210,7 +210,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let consensus = Arc::new(components.consensus().clone());
|
||||
|
||||
let mut config = config;
|
||||
config.peers.trusted_nodes_only = self.network.trusted_only;
|
||||
config.peers.trusted_nodes_only |= self.network.trusted_only;
|
||||
config.peers.trusted_nodes.extend(self.network.trusted_peers.clone());
|
||||
|
||||
let network_secret_path = self
|
||||
|
||||
@@ -30,10 +30,16 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
fmt::Debug,
|
||||
string::{String, ToString},
|
||||
sync::Arc,
|
||||
vec::Vec,
|
||||
};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
|
||||
use core::error::Error;
|
||||
use core::{error::Error, fmt::Display};
|
||||
|
||||
/// Pre-computed receipt root and logs bloom.
|
||||
///
|
||||
@@ -104,6 +110,18 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
|
||||
/// Note: validating blocks does not include other validations of the Consensus
|
||||
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
|
||||
|
||||
/// Returns `true` if the given consensus error is transient and may resolve on its own.
|
||||
///
|
||||
/// On fast chains, clock skew between nodes can cause a valid block's timestamp to
|
||||
/// appear briefly in the future. Caching such blocks as permanently invalid would
|
||||
/// prevent them from being re-validated once the local clock catches up.
|
||||
///
|
||||
/// Transient errors will not cause the block hash to be cached as permanently invalid,
|
||||
/// allowing the block to be re-validated later.
|
||||
fn is_transient_error(&self, _error: &ConsensusError) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Validate a block disregarding world state using an optional pre-computed transaction root.
|
||||
///
|
||||
/// If `transaction_root` is provided, the implementation should use the pre-computed
|
||||
@@ -456,19 +474,49 @@ pub enum ConsensusError {
|
||||
/// EIP-7825: Transaction gas limit exceeds maximum allowed
|
||||
#[error(transparent)]
|
||||
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
|
||||
/// Other, likely an injected L2 error.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
/// Other unspecified error.
|
||||
/// Any additional consensus error, for example L2-specific errors.
|
||||
#[error(transparent)]
|
||||
Custom(#[from] Arc<dyn Error + Send + Sync>),
|
||||
Other(#[from] Arc<dyn Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl ConsensusError {
|
||||
/// Returns a new [`ConsensusError::Other`] instance with the given error.
|
||||
pub fn other<E>(error: E) -> Self
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Other(Arc::new(error))
|
||||
}
|
||||
|
||||
/// Returns a new [`ConsensusError::Other`] instance with the given message.
|
||||
pub fn msg(msg: impl Display) -> Self {
|
||||
Self::other(MessageError(msg.to_string()))
|
||||
}
|
||||
|
||||
/// Returns `true` if the error is a state root error.
|
||||
pub const fn is_state_root_error(&self) -> bool {
|
||||
matches!(self, Self::BodyStateRootDiff(_))
|
||||
}
|
||||
|
||||
/// Returns the arbitrary error if it is [`ConsensusError::Other`].
|
||||
pub fn as_other(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
|
||||
match self {
|
||||
Self::Other(err) => Some(err.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`ConsensusError::Other`] value if it is of that type.
|
||||
/// Returns `None` otherwise.
|
||||
pub fn downcast_other_ref<T: Error + 'static>(&self) -> Option<&T> {
|
||||
let other = self.as_other()?;
|
||||
other.downcast_ref()
|
||||
}
|
||||
|
||||
/// Returns `true` if this type is a [`ConsensusError::Other`] of that error type.
|
||||
pub fn is_other<T: Error + 'static>(&self) -> bool {
|
||||
self.as_other().map(|err| err.is::<T>()).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidTransactionError> for ConsensusError {
|
||||
@@ -500,6 +548,10 @@ pub struct TxGasLimitTooHighErr {
|
||||
pub max_allowed: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{0}")]
|
||||
struct MessageError(String);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -509,24 +561,31 @@ mod tests {
|
||||
struct CustomL2Error;
|
||||
|
||||
#[test]
|
||||
fn test_custom_error_conversion() {
|
||||
// Test conversion from custom error to ConsensusError
|
||||
let custom_err = CustomL2Error;
|
||||
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
|
||||
let consensus_err: ConsensusError = arc_err.into();
|
||||
|
||||
// Verify it's the Custom variant
|
||||
assert!(matches!(consensus_err, ConsensusError::Custom(_)));
|
||||
fn test_other_error_conversion() {
|
||||
let consensus_err = ConsensusError::other(CustomL2Error);
|
||||
assert!(matches!(consensus_err, ConsensusError::Other(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_error_display() {
|
||||
let custom_err = CustomL2Error;
|
||||
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
|
||||
let consensus_err: ConsensusError = arc_err.into();
|
||||
|
||||
// Verify the error message is preserved through transparent attribute
|
||||
fn test_other_error_display() {
|
||||
let consensus_err = ConsensusError::other(CustomL2Error);
|
||||
let error_message = format!("{}", consensus_err);
|
||||
assert_eq!(error_message, "Custom L2 consensus error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_other_error_downcast() {
|
||||
let consensus_err = ConsensusError::other(CustomL2Error);
|
||||
|
||||
assert!(consensus_err.is_other::<CustomL2Error>());
|
||||
assert!(consensus_err.downcast_other_ref::<CustomL2Error>().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_other_msg() {
|
||||
let consensus_err = ConsensusError::msg("consensus message");
|
||||
|
||||
assert_eq!(consensus_err.to_string(), "consensus message");
|
||||
assert!(consensus_err.downcast_other_ref::<MessageError>().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
|
||||
/// The size of proof targets chunk to spawn in one multiproof calculation.
|
||||
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 5;
|
||||
|
||||
/// Default number of cache hits before an invalid header entry is evicted and reprocessed.
|
||||
pub const DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD: u8 = 128;
|
||||
|
||||
/// Gas threshold below which the small block chunk size is used.
|
||||
pub const SMALL_BLOCK_GAS_THRESHOLD: u64 = 20_000_000;
|
||||
|
||||
@@ -102,6 +105,11 @@ pub struct TreeConfig {
|
||||
block_buffer_limit: u32,
|
||||
/// Number of invalid headers to keep in cache.
|
||||
max_invalid_header_cache_length: u32,
|
||||
/// Number of cache hits before an invalid header entry is evicted and reprocessed.
|
||||
///
|
||||
/// Setting this to `0` effectively disables the cache because entries are evicted on the
|
||||
/// first lookup.
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
/// Maximum number of blocks to execute sequentially in a batch.
|
||||
///
|
||||
/// This is used as a cutoff to prevent long-running sequential block execution when we receive
|
||||
@@ -170,6 +178,23 @@ pub struct TreeConfig {
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
/// Whether to share sparse trie with the payload builder.
|
||||
share_sparse_trie_with_payload_builder: bool,
|
||||
/// Whether to suppress persistence cycles while building a payload.
|
||||
///
|
||||
/// When enabled, persistence is deferred from the moment an FCU with payload attributes
|
||||
/// arrives until the next FCU without attributes. This avoids persistence I/O competing
|
||||
/// with block building on latency-sensitive chains.
|
||||
suppress_persistence_during_build: bool,
|
||||
/// Whether to disable BAL (Block Access List, EIP-7928) based parallel execution.
|
||||
/// When disabled, falls back to transaction-based prewarming even when a BAL is available.
|
||||
disable_bal_parallel_execution: bool,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
/// When disabled, the BAL hashed post state is not sent to the multiproof task for
|
||||
/// early parallel state root computation.
|
||||
disable_bal_parallel_state_root: bool,
|
||||
/// Whether to disable BAL (Block Access List) batched IO during prewarming.
|
||||
/// When disabled, falls back to individual per-slot storage reads instead of
|
||||
/// batched cursor reads via `storage_range`.
|
||||
disable_bal_batch_io: bool,
|
||||
/// Maximum random jitter applied before each proof computation (trie-debug only).
|
||||
/// When set, each proof worker sleeps for a random duration up to this value
|
||||
/// before starting a proof calculation.
|
||||
@@ -189,6 +214,7 @@ impl Default for TreeConfig {
|
||||
persistence_backpressure_threshold: DEFAULT_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,
|
||||
max_execute_block_batch_size: DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE,
|
||||
legacy_state_root: false,
|
||||
always_compare_trie_updates: false,
|
||||
@@ -212,6 +238,10 @@ impl Default for TreeConfig {
|
||||
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
|
||||
share_execution_cache_with_payload_builder: false,
|
||||
share_sparse_trie_with_payload_builder: false,
|
||||
suppress_persistence_during_build: false,
|
||||
disable_bal_parallel_execution: false,
|
||||
disable_bal_parallel_state_root: false,
|
||||
disable_bal_batch_io: false,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -227,6 +257,7 @@ impl TreeConfig {
|
||||
persistence_backpressure_threshold: u64,
|
||||
block_buffer_limit: u32,
|
||||
max_invalid_header_cache_length: u32,
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
max_execute_block_batch_size: usize,
|
||||
legacy_state_root: bool,
|
||||
always_compare_trie_updates: bool,
|
||||
@@ -260,6 +291,7 @@ impl TreeConfig {
|
||||
persistence_backpressure_threshold,
|
||||
block_buffer_limit,
|
||||
max_invalid_header_cache_length,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
max_execute_block_batch_size,
|
||||
legacy_state_root,
|
||||
always_compare_trie_updates,
|
||||
@@ -283,6 +315,10 @@ impl TreeConfig {
|
||||
state_root_task_timeout,
|
||||
share_execution_cache_with_payload_builder,
|
||||
share_sparse_trie_with_payload_builder,
|
||||
suppress_persistence_during_build: false,
|
||||
disable_bal_parallel_execution: false,
|
||||
disable_bal_parallel_state_root: false,
|
||||
disable_bal_batch_io: false,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -313,6 +349,14 @@ impl TreeConfig {
|
||||
self.max_invalid_header_cache_length
|
||||
}
|
||||
|
||||
/// Return the invalid header cache hit eviction threshold.
|
||||
///
|
||||
/// Setting this to `0` effectively disables the cache because entries are evicted on the
|
||||
/// first lookup.
|
||||
pub const fn invalid_header_hit_eviction_threshold(&self) -> u8 {
|
||||
self.invalid_header_hit_eviction_threshold
|
||||
}
|
||||
|
||||
/// Return the maximum execute block batch size.
|
||||
pub const fn max_execute_block_batch_size(&self) -> usize {
|
||||
self.max_execute_block_batch_size
|
||||
@@ -443,6 +487,15 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for the invalid header cache hit eviction threshold.
|
||||
pub const fn with_invalid_header_hit_eviction_threshold(
|
||||
mut self,
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
) -> Self {
|
||||
self.invalid_header_hit_eviction_threshold = invalid_header_hit_eviction_threshold;
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for maximum execute block batch size.
|
||||
pub const fn with_max_execute_block_batch_size(
|
||||
mut self,
|
||||
@@ -646,6 +699,56 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether persistence is suppressed during payload building.
|
||||
pub const fn suppress_persistence_during_build(&self) -> bool {
|
||||
self.suppress_persistence_during_build
|
||||
}
|
||||
|
||||
/// Setter for whether to suppress persistence during payload building.
|
||||
pub const fn with_suppress_persistence_during_build(mut self, value: bool) -> Self {
|
||||
self.suppress_persistence_during_build = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether BAL-based parallel execution is disabled.
|
||||
pub const fn disable_bal_parallel_execution(&self) -> bool {
|
||||
self.disable_bal_parallel_execution
|
||||
}
|
||||
|
||||
/// Setter for whether to disable BAL-based parallel execution.
|
||||
pub const fn without_bal_parallel_execution(
|
||||
mut self,
|
||||
disable_bal_parallel_execution: bool,
|
||||
) -> Self {
|
||||
self.disable_bal_parallel_execution = disable_bal_parallel_execution;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether BAL-driven parallel state root computation is disabled.
|
||||
pub const fn disable_bal_parallel_state_root(&self) -> bool {
|
||||
self.disable_bal_parallel_state_root
|
||||
}
|
||||
|
||||
/// Setter for whether to disable BAL-driven parallel state root computation.
|
||||
pub const fn without_bal_parallel_state_root(
|
||||
mut self,
|
||||
disable_bal_parallel_state_root: bool,
|
||||
) -> Self {
|
||||
self.disable_bal_parallel_state_root = disable_bal_parallel_state_root;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether BAL batched IO is disabled.
|
||||
pub const fn disable_bal_batch_io(&self) -> bool {
|
||||
self.disable_bal_batch_io
|
||||
}
|
||||
|
||||
/// Setter for whether to disable BAL batched IO.
|
||||
pub const fn without_bal_batch_io(mut self, disable_bal_batch_io: bool) -> Self {
|
||||
self.disable_bal_batch_io = disable_bal_batch_io;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the proof jitter duration, if configured (trie-debug only).
|
||||
#[cfg(feature = "trie-debug")]
|
||||
pub const fn proof_jitter(&self) -> Option<Duration> {
|
||||
|
||||
@@ -8,25 +8,28 @@ use schnellru::{ByLength, LruMap};
|
||||
use std::fmt::Debug;
|
||||
use tracing::warn;
|
||||
|
||||
/// The max hit counter for invalid headers in the cache before it is forcefully evicted.
|
||||
///
|
||||
/// In other words, if a header is referenced more than this number of times, it will be evicted to
|
||||
/// allow for reprocessing.
|
||||
const INVALID_HEADER_HIT_EVICTION_THRESHOLD: u8 = 128;
|
||||
|
||||
/// Keeps track of invalid headers.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidHeaderCache {
|
||||
/// This maps a header hash to a reference to its invalid ancestor.
|
||||
headers: LruMap<B256, HeaderEntry>,
|
||||
/// Number of cache hits before an invalid header entry is evicted and reprocessed.
|
||||
hit_eviction_threshold: u8,
|
||||
/// Metrics for the cache.
|
||||
metrics: InvalidHeaderCacheMetrics,
|
||||
}
|
||||
|
||||
impl InvalidHeaderCache {
|
||||
/// Invalid header cache constructor.
|
||||
pub fn new(max_length: u32) -> Self {
|
||||
Self { headers: LruMap::new(ByLength::new(max_length)), metrics: Default::default() }
|
||||
///
|
||||
/// Setting `hit_eviction_threshold` to `0` effectively disables the cache because entries are
|
||||
/// evicted on the first lookup.
|
||||
pub fn new(max_length: u32, hit_eviction_threshold: u8) -> Self {
|
||||
Self {
|
||||
headers: LruMap::new(ByLength::new(max_length)),
|
||||
hit_eviction_threshold,
|
||||
metrics: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_entry(&mut self, hash: B256, header: BlockWithParent) {
|
||||
@@ -41,7 +44,7 @@ impl InvalidHeaderCache {
|
||||
{
|
||||
let entry = self.headers.get(hash)?;
|
||||
entry.hit_count += 1;
|
||||
if entry.hit_count < INVALID_HEADER_HIT_EVICTION_THRESHOLD {
|
||||
if entry.hit_count < self.hit_eviction_threshold {
|
||||
return Some(entry.header)
|
||||
}
|
||||
}
|
||||
@@ -110,17 +113,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hit_eviction() {
|
||||
let mut cache = InvalidHeaderCache::new(10);
|
||||
let hit_eviction_threshold = 3;
|
||||
let mut cache = InvalidHeaderCache::new(10, hit_eviction_threshold);
|
||||
let header = Header::default();
|
||||
let header = SealedHeader::seal_slow(header);
|
||||
cache.insert(header.block_with_parent());
|
||||
assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, 0);
|
||||
|
||||
for hit in 1..INVALID_HEADER_HIT_EVICTION_THRESHOLD {
|
||||
for hit in 1..hit_eviction_threshold {
|
||||
assert!(cache.get(&header.hash()).is_some());
|
||||
assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, hit);
|
||||
}
|
||||
|
||||
assert!(cache.get(&header.hash()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_hit_eviction_threshold_effectively_disables_cache() {
|
||||
let mut cache = InvalidHeaderCache::new(10, 0);
|
||||
let header = SealedHeader::seal_slow(Header::default());
|
||||
cache.insert(header.block_with_parent());
|
||||
|
||||
assert!(cache.get(&header.hash()).is_none());
|
||||
assert_eq!(cache.headers.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use alloy_primitives::B256;
|
||||
use alloy_rpc_types_engine::{
|
||||
ForkchoiceState, PayloadStatus, PayloadStatusEnum, PayloadValidationError,
|
||||
};
|
||||
use error::{InsertBlockError, InsertBlockFatalError};
|
||||
use error::{InsertBlockError, InsertBlockFatalError, InsertBlockValidationError};
|
||||
use reth_chain_state::{
|
||||
CanonicalInMemoryState, ComputedTrieData, ExecutedBlock, ExecutionTimingStats,
|
||||
MemoryOverlayStateProvider, NewCanonicalChain,
|
||||
@@ -151,11 +151,15 @@ impl<N: NodePrimitives> EngineApiTreeState<N> {
|
||||
fn new(
|
||||
block_buffer_limit: u32,
|
||||
max_invalid_header_cache_length: u32,
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
canonical_block: BlockNumHash,
|
||||
engine_kind: EngineApiKind,
|
||||
) -> Self {
|
||||
Self {
|
||||
invalid_headers: InvalidHeaderCache::new(max_invalid_header_cache_length),
|
||||
invalid_headers: InvalidHeaderCache::new(
|
||||
max_invalid_header_cache_length,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
),
|
||||
buffer: BlockBuffer::new(block_buffer_limit),
|
||||
tree_state: TreeState::new(canonical_block, engine_kind),
|
||||
forkchoice_state_tracker: ForkchoiceStateTracker::default(),
|
||||
@@ -305,6 +309,9 @@ where
|
||||
/// Stored here (not in `ExecutedBlock`) to avoid leaking observability concerns into the block
|
||||
/// type. Entries are removed when blocks are persisted or invalidated.
|
||||
execution_timing_stats: HashMap<B256, Box<ExecutionTimingStats>>,
|
||||
/// Set when an FCU with payload attributes is received, cleared on the next FCU without.
|
||||
/// Suppresses persistence cycles during payload building.
|
||||
building_payload: bool,
|
||||
/// Task runtime for spawning blocking work on named, reusable threads.
|
||||
runtime: reth_tasks::Runtime,
|
||||
}
|
||||
@@ -396,6 +403,7 @@ where
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
execution_timing_stats: HashMap::new(),
|
||||
building_payload: false,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
@@ -432,6 +440,7 @@ where
|
||||
let state = EngineApiTreeState::new(
|
||||
config.block_buffer_limit(),
|
||||
config.max_invalid_header_cache_length(),
|
||||
config.invalid_header_hit_eviction_threshold(),
|
||||
header.num_hash(),
|
||||
kind,
|
||||
);
|
||||
@@ -1112,6 +1121,8 @@ where
|
||||
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
|
||||
trace!(target: "engine::tree", ?attrs, "invoked forkchoice update");
|
||||
|
||||
self.building_payload = attrs.is_some() && self.config.suppress_persistence_during_build();
|
||||
|
||||
// Record metrics
|
||||
self.record_forkchoice_metrics();
|
||||
|
||||
@@ -2005,9 +2016,13 @@ where
|
||||
}
|
||||
|
||||
/// Returns true if the canonical chain length minus the last persisted
|
||||
/// block is greater than or equal to the persistence threshold and
|
||||
/// backfill is not running.
|
||||
/// block is greater than or equal to the persistence threshold,
|
||||
/// backfill is not running, and no payload is currently being built.
|
||||
pub const fn should_persist(&self) -> bool {
|
||||
if self.building_payload {
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.backfill_sync_state.is_idle() {
|
||||
// can't persist if backfill is running
|
||||
return false
|
||||
@@ -3009,8 +3024,22 @@ where
|
||||
);
|
||||
let latest_valid_hash = self.latest_valid_hash_for_invalid_payload(block.parent_hash())?;
|
||||
|
||||
// keep track of the invalid header
|
||||
self.state.invalid_headers.insert(block.block_with_parent());
|
||||
// keep track of the invalid header unless the consensus impl considers it transient
|
||||
let is_transient = match &validation_err {
|
||||
InsertBlockValidationError::Consensus(err) => self.consensus.is_transient_error(err),
|
||||
_ => false,
|
||||
};
|
||||
if is_transient {
|
||||
warn!(
|
||||
target: "engine::tree",
|
||||
invalid_hash=%block.hash(),
|
||||
invalid_number=block.number(),
|
||||
%validation_err,
|
||||
"Skipping invalid header cache insert for transient validation error",
|
||||
);
|
||||
} else {
|
||||
self.state.invalid_headers.insert(block.block_with_parent());
|
||||
}
|
||||
self.emit_event(EngineApiEvent::BeaconConsensus(ConsensusEngineEvent::InvalidBlock(
|
||||
Box::new(block),
|
||||
)));
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::tree::{
|
||||
CacheWaitDurations, CachedStateMetrics, CachedStateMetricsSource, ExecutionCache,
|
||||
PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig, WaitForCaches,
|
||||
};
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_eip7928::bal::DecodedBal;
|
||||
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal};
|
||||
use alloy_primitives::B256;
|
||||
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
|
||||
@@ -122,6 +122,12 @@ where
|
||||
sparse_trie_max_hot_accounts: usize,
|
||||
/// Whether sparse trie cache pruning is fully disabled.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to disable BAL-based parallel execution (falls back to tx-based prewarming).
|
||||
disable_bal_parallel_execution: bool,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
disable_bal_parallel_state_root: bool,
|
||||
/// Whether BAL batched IO is disabled.
|
||||
disable_bal_batch_io: bool,
|
||||
}
|
||||
|
||||
impl<N, Evm> PayloadProcessor<Evm>
|
||||
@@ -157,6 +163,9 @@ where
|
||||
disable_sparse_trie_cache_pruning: config.disable_sparse_trie_cache_pruning(),
|
||||
cache_metrics: (!config.disable_cache_metrics())
|
||||
.then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
|
||||
disable_bal_parallel_execution: config.disable_bal_parallel_execution(),
|
||||
disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
|
||||
disable_bal_batch_io: config.disable_bal_batch_io(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +250,6 @@ where
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
multiproof_provider_factory: F,
|
||||
config: &TreeConfig,
|
||||
bal: Option<Arc<BlockAccessList>>,
|
||||
) -> IteratorPayloadHandle<Evm, I, N>
|
||||
where
|
||||
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
|
||||
@@ -264,13 +272,12 @@ where
|
||||
halve_workers,
|
||||
config,
|
||||
);
|
||||
let install_state_hook = bal.is_none();
|
||||
let install_state_hook = env.decoded_bal.is_none();
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder,
|
||||
Some(state_root_handle.updates_tx().clone()),
|
||||
bal,
|
||||
);
|
||||
|
||||
PayloadHandle {
|
||||
@@ -291,14 +298,13 @@ where
|
||||
env: ExecutionEnv<Evm>,
|
||||
transactions: I,
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
bal: Option<Arc<BlockAccessList>>,
|
||||
) -> IteratorPayloadHandle<Evm, I, N>
|
||||
where
|
||||
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
|
||||
{
|
||||
let (prewarm_rx, execution_rx) =
|
||||
self.spawn_tx_iterator(transactions, env.transaction_count);
|
||||
let prewarm_handle = self.spawn_caching_with(env, prewarm_rx, provider_builder, None, bal);
|
||||
let prewarm_handle = self.spawn_caching_with(env, prewarm_rx, provider_builder, None);
|
||||
PayloadHandle {
|
||||
state_root_handle: None,
|
||||
install_state_hook: false,
|
||||
@@ -456,7 +462,7 @@ where
|
||||
level = "debug",
|
||||
target = "engine::tree::payload_processor",
|
||||
skip_all,
|
||||
fields(bal=%bal.is_some())
|
||||
fields(bal=%env.decoded_bal.is_some())
|
||||
)]
|
||||
fn spawn_caching_with<P>(
|
||||
&self,
|
||||
@@ -464,7 +470,6 @@ where
|
||||
transactions: mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
to_sparse_trie_task: Option<CrossbeamSender<StateRootMessage>>,
|
||||
bal: Option<Arc<BlockAccessList>>,
|
||||
) -> CacheTaskHandle<N::Receipt>
|
||||
where
|
||||
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
|
||||
@@ -475,7 +480,7 @@ where
|
||||
let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
|
||||
|
||||
let executed_tx_index = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let maybe_decoded_bal = env.decoded_bal.clone();
|
||||
// configure prewarming
|
||||
let prewarm_ctx = PrewarmContext {
|
||||
env,
|
||||
@@ -488,6 +493,8 @@ where
|
||||
executed_tx_index: Arc::clone(&executed_tx_index),
|
||||
precompile_cache_disabled: self.precompile_cache_disabled,
|
||||
precompile_cache_map: self.precompile_cache_map.clone(),
|
||||
disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
|
||||
disable_bal_batch_io: self.disable_bal_batch_io,
|
||||
};
|
||||
|
||||
let (prewarm_task, to_prewarm_task) = PrewarmCacheTask::new(
|
||||
@@ -496,14 +503,16 @@ where
|
||||
prewarm_ctx,
|
||||
to_sparse_trie_task,
|
||||
);
|
||||
|
||||
{
|
||||
let to_prewarm_task = to_prewarm_task.clone();
|
||||
let disable_bal_parallel_execution = self.disable_bal_parallel_execution;
|
||||
self.executor.spawn_blocking_named("prewarm", move || {
|
||||
let mode = if skip_prewarm {
|
||||
PrewarmMode::Skipped
|
||||
} else if let Some(bal) = bal {
|
||||
PrewarmMode::BlockAccessList(bal)
|
||||
} else if let Some(decoded_bal) =
|
||||
maybe_decoded_bal.filter(|_| !disable_bal_parallel_execution)
|
||||
{
|
||||
PrewarmMode::BlockAccessList(decoded_bal)
|
||||
} else {
|
||||
PrewarmMode::Transactions(transactions)
|
||||
};
|
||||
@@ -597,6 +606,7 @@ where
|
||||
proof_worker_handle,
|
||||
trie_metrics.clone(),
|
||||
sparse_state_trie,
|
||||
parent_state_root,
|
||||
chunk_size,
|
||||
);
|
||||
|
||||
@@ -923,6 +933,9 @@ pub struct ExecutionEnv<Evm: ConfigureEvm> {
|
||||
/// Withdrawals included in the block.
|
||||
/// Used to generate prefetch targets for withdrawal addresses.
|
||||
pub withdrawals: Option<Vec<Withdrawal>>,
|
||||
/// Optional decoded BAL for the block.
|
||||
/// Used to validate and optimize execution.
|
||||
pub decoded_bal: Option<Arc<DecodedBal>>,
|
||||
}
|
||||
|
||||
impl<Evm: ConfigureEvm> ExecutionEnv<Evm>
|
||||
@@ -940,6 +953,7 @@ where
|
||||
transaction_count: 0,
|
||||
gas_used: 0,
|
||||
withdrawals: None,
|
||||
decoded_bal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -961,7 +975,7 @@ mod tests {
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_primitives_traits::{Account, Recovered, StorageEntry};
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, OverlayStateProviderFactory},
|
||||
providers::{BlockchainProvider, OverlayBuilder, OverlayStateProviderFactory},
|
||||
test_utils::create_test_provider_factory_with_chain_spec,
|
||||
ChainSpecProvider, HashingWriter,
|
||||
};
|
||||
@@ -1145,19 +1159,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
let account = revm_state::Account {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
},
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
storage,
|
||||
status: AccountStatus::Touched,
|
||||
transaction_id: 0,
|
||||
let mut account = revm_state::Account::default();
|
||||
account.info = AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
};
|
||||
account.storage = storage;
|
||||
account.status = AccountStatus::Touched;
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
@@ -1236,9 +1247,11 @@ mod tests {
|
||||
std::convert::identity,
|
||||
),
|
||||
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
|
||||
OverlayStateProviderFactory::new(provider_factory, ChangesetCache::new()),
|
||||
OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
OverlayBuilder::new(ChangesetCache::new()),
|
||||
),
|
||||
&TreeConfig::default(),
|
||||
None, // No BAL for test
|
||||
);
|
||||
|
||||
let mut state_hook = handle.state_hook().expect("state hook is None");
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::tree::{
|
||||
StateProviderBuilder,
|
||||
};
|
||||
use alloy_consensus::transaction::TxHashRef;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_eip7928::bal::DecodedBal;
|
||||
use alloy_eips::eip4895::Withdrawal;
|
||||
use alloy_primitives::{keccak256, StorageKey, B256};
|
||||
use crossbeam_channel::Sender as CrossbeamSender;
|
||||
@@ -48,7 +48,7 @@ pub enum PrewarmMode<Tx> {
|
||||
/// Prewarm by executing transactions from a stream, each paired with its block index.
|
||||
Transactions(Receiver<(usize, Tx)>),
|
||||
/// Prewarm by prefetching slots from a Block Access List.
|
||||
BlockAccessList(Arc<BlockAccessList>),
|
||||
BlockAccessList(Arc<DecodedBal>),
|
||||
/// Transaction prewarming is skipped (e.g. small blocks where the overhead exceeds the
|
||||
/// benefit). No workers are spawned.
|
||||
Skipped,
|
||||
@@ -331,9 +331,10 @@ where
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
|
||||
fn run_bal_prewarm(
|
||||
&self,
|
||||
bal: Arc<BlockAccessList>,
|
||||
decoded_bal: Arc<DecodedBal>,
|
||||
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
|
||||
) {
|
||||
let bal = decoded_bal.as_bal();
|
||||
if bal.is_empty() {
|
||||
if let Some(to_sparse_trie_task) = self.to_sparse_trie_task.as_ref() {
|
||||
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
|
||||
@@ -355,8 +356,8 @@ where
|
||||
let parent_span = Span::current();
|
||||
let prefetch_parent_span = parent_span.clone();
|
||||
let stream_parent_span = parent_span;
|
||||
let prefetch_bal = Arc::clone(&bal);
|
||||
let stream_bal = Arc::clone(&bal);
|
||||
let prefetch_bal = Arc::clone(&decoded_bal);
|
||||
let stream_bal = Arc::clone(&decoded_bal);
|
||||
let (prefetch_tx, prefetch_rx) = oneshot::channel();
|
||||
let (stream_tx, stream_rx) = oneshot::channel();
|
||||
|
||||
@@ -367,12 +368,12 @@ where
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
parent: &prefetch_parent_span,
|
||||
"bal_prefetch_storage",
|
||||
bal_accounts = prefetch_bal.len(),
|
||||
bal_accounts = prefetch_bal.as_bal().len(),
|
||||
);
|
||||
let provider_parent_span = branch_span.clone();
|
||||
let _span = branch_span.entered();
|
||||
|
||||
prefetch_bal.par_iter().for_each_init(
|
||||
prefetch_bal.as_bal().par_iter().for_each_init(
|
||||
|| {
|
||||
(
|
||||
prefetch_ctx.clone(),
|
||||
@@ -400,12 +401,12 @@ where
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
parent: &stream_parent_span,
|
||||
"bal_hashed_state_stream",
|
||||
bal_accounts = stream_bal.len(),
|
||||
bal_accounts = stream_bal.as_bal().len(),
|
||||
);
|
||||
let provider_parent_span = branch_span.clone();
|
||||
let _span = branch_span.entered();
|
||||
|
||||
stream_bal.par_iter().for_each_init(
|
||||
stream_bal.as_bal().par_iter().for_each_init(
|
||||
|| (ctx.clone(), None::<Box<dyn AccountReader>>, provider_parent_span.clone()),
|
||||
|(ctx, provider, parent_span), account_changes| {
|
||||
ctx.send_bal_hashed_state(
|
||||
@@ -536,6 +537,10 @@ where
|
||||
pub precompile_cache_disabled: bool,
|
||||
/// The precompile cache map.
|
||||
pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
pub disable_bal_parallel_state_root: bool,
|
||||
/// Whether BAL batched IO is disabled.
|
||||
pub disable_bal_batch_io: bool,
|
||||
}
|
||||
|
||||
/// Per-thread EVM state initialised by [`PrewarmContext::evm_for_ctx`] and stored in
|
||||
@@ -631,6 +636,9 @@ where
|
||||
account_changes: &alloy_eip7928::AccountChanges,
|
||||
to_sparse_trie_task: &CrossbeamSender<StateRootMessage>,
|
||||
) {
|
||||
if self.disable_bal_parallel_state_root {
|
||||
return;
|
||||
}
|
||||
let address = account_changes.address;
|
||||
let mut hashed_address = None;
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ use reth_trie_parallel::{
|
||||
root::ParallelStateRootError,
|
||||
};
|
||||
use reth_trie_sparse::{
|
||||
errors::SparseTrieResult, ConfigurableSparseTrie, DeferredDrops, LeafUpdate,
|
||||
RevealableSparseTrie, SparseStateTrie, SparseTrie,
|
||||
errors::{SparseStateTrieErrorKind, SparseTrieErrorKind, SparseTrieResult},
|
||||
ConfigurableSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
|
||||
SparseTrie,
|
||||
};
|
||||
use revm_primitives::{hash_map::Entry, B256Map};
|
||||
use tracing::{debug, debug_span, error, instrument, trace_span};
|
||||
@@ -46,6 +47,8 @@ pub(super) struct SparseTrieCacheTask<A = ConfigurableSparseTrie, S = Configurab
|
||||
updates: CrossbeamReceiver<SparseTrieTaskMessage>,
|
||||
/// `SparseStateTrie` used for computing the state root.
|
||||
trie: SparseStateTrie<A, S>,
|
||||
/// The parent block's state root.
|
||||
parent_state_root: B256,
|
||||
/// Handle to the proof worker pools (storage and account).
|
||||
proof_worker_handle: ProofWorkerHandle,
|
||||
|
||||
@@ -120,6 +123,7 @@ where
|
||||
proof_worker_handle: ProofWorkerHandle,
|
||||
metrics: MultiProofTaskMetrics,
|
||||
trie: SparseStateTrie<A, S>,
|
||||
parent_state_root: B256,
|
||||
chunk_size: usize,
|
||||
) -> Self {
|
||||
let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
|
||||
@@ -138,6 +142,7 @@ where
|
||||
updates: hashed_state_rx,
|
||||
proof_worker_handle,
|
||||
trie,
|
||||
parent_state_root,
|
||||
chunk_size,
|
||||
max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
|
||||
account_updates: Default::default(),
|
||||
@@ -359,10 +364,25 @@ where
|
||||
debug!(target: "engine::root", "All proofs processed, ending calculation");
|
||||
|
||||
let start = Instant::now();
|
||||
let (state_root, trie_updates) =
|
||||
self.trie.root_with_updates(&self.proof_worker_handle).map_err(|e| {
|
||||
ParallelStateRootError::Other(format!("could not calculate state root: {e:?}"))
|
||||
})?;
|
||||
let (state_root, trie_updates) = match self.trie.root_with_updates() {
|
||||
Ok(result) => result,
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
|
||||
) =>
|
||||
{
|
||||
// A still-blind account trie means this block never changed state, so preserve
|
||||
// the cached parent root instead of fetching and revealing
|
||||
// the unchanged root node.
|
||||
(self.parent_state_root, TrieUpdates::default())
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(ParallelStateRootError::Other(format!(
|
||||
"could not calculate state root: {err:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "trie-debug")]
|
||||
let debug_recorders = self.trie.take_debug_recorders();
|
||||
@@ -873,6 +893,12 @@ enum SparseTrieTaskMessage {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{keccak256, Address, B256, U256};
|
||||
use reth_provider::{
|
||||
providers::{OverlayBuilder, OverlayStateProviderFactory},
|
||||
test_utils::create_test_provider_factory,
|
||||
};
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use reth_trie_parallel::proof_task::ProofTaskCtx;
|
||||
use reth_trie_sparse::ArenaParallelSparseTrie;
|
||||
|
||||
#[test]
|
||||
@@ -953,4 +979,45 @@ mod tests {
|
||||
assert_eq!(decoded.storage_root, storage_root);
|
||||
assert_eq!(account_rlp_buf, encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 overlay_factory = OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
OverlayBuilder::new(ChangesetCache::new()),
|
||||
);
|
||||
let proof_worker_handle =
|
||||
ProofWorkerHandle::new(&runtime, ProofTaskCtx::new(overlay_factory), false);
|
||||
|
||||
let default_trie = RevealableSparseTrie::blind_from(ConfigurableSparseTrie::Arena(
|
||||
ArenaParallelSparseTrie::default(),
|
||||
));
|
||||
let trie = SparseStateTrie::default()
|
||||
.with_accounts_trie(default_trie.clone())
|
||||
.with_default_storage_trie(default_trie)
|
||||
.with_updates(true);
|
||||
|
||||
let parent_state_root = B256::from([0x55; 32]);
|
||||
let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
|
||||
let mut task = SparseTrieCacheTask::new_with_trie(
|
||||
&runtime,
|
||||
updates_rx,
|
||||
proof_worker_handle,
|
||||
MultiProofTaskMetrics::default(),
|
||||
trie,
|
||||
parent_state_root,
|
||||
1,
|
||||
);
|
||||
|
||||
updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
|
||||
drop(updates_tx);
|
||||
|
||||
let outcome = task.run().expect("state root computation should succeed");
|
||||
|
||||
assert_eq!(outcome.state_root, parent_state_root);
|
||||
assert!(outcome.trie_updates.is_empty());
|
||||
assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ use crate::tree::{
|
||||
PayloadHandle, StateProviderBuilder, StateProviderDatabase, TreeConfig, WaitForCaches,
|
||||
};
|
||||
use alloy_consensus::transaction::{Either, TxHashRef};
|
||||
use alloy_eip7928::{bal::Bal, BlockAccessList};
|
||||
use alloy_eip7928::{
|
||||
bal::{Bal, DecodedBal},
|
||||
BlockAccessList,
|
||||
};
|
||||
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
|
||||
use alloy_evm::Evm;
|
||||
use alloy_primitives::{map::B256Set, B256};
|
||||
@@ -77,13 +80,14 @@ use reth_primitives_traits::{
|
||||
RecoveredBlock, SealedBlock, SealedHeader, SignerRecoverable,
|
||||
};
|
||||
use reth_provider::{
|
||||
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockNumReader, BlockReader,
|
||||
ChangeSetReader, DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider,
|
||||
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
|
||||
StateProviderFactory, StateReader, StorageChangeSetReader, StorageSettingsCache,
|
||||
providers::{OverlayBuilder, OverlayStateProviderFactory},
|
||||
BlockExecutionOutput, BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory,
|
||||
DatabaseProviderROFactory, HashedPostStateProvider, ProviderError, PruneCheckpointReader,
|
||||
StageCheckpointReader, StateProvider, StateProviderBox, StateProviderFactory, StateReader,
|
||||
StorageChangeSetReader, StorageSettingsCache,
|
||||
};
|
||||
use reth_revm::db::{states::bundle_state::BundleRetention, BundleAccount, State};
|
||||
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState, StateRoot};
|
||||
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState};
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
|
||||
use revm_primitives::{Address, KECCAK_EMPTY};
|
||||
@@ -487,6 +491,12 @@ where
|
||||
.in_scope(|| self.evm_env_for(&input))
|
||||
.map_err(NewPayloadError::other)?;
|
||||
|
||||
// Extract the decoded BAL, if valid and available.
|
||||
let decoded_bal = ensure_ok!(input
|
||||
.try_decoded_access_list()
|
||||
.map_err(|err| { Box::<dyn std::error::Error + Send + Sync>::from(err) }))
|
||||
.map(Arc::new);
|
||||
|
||||
let env = ExecutionEnv {
|
||||
evm_env,
|
||||
hash: input.hash(),
|
||||
@@ -495,6 +505,7 @@ where
|
||||
transaction_count: input.transaction_count(),
|
||||
gas_used: input.gas_used(),
|
||||
withdrawals: input.withdrawals().map(|w| w.to_vec()),
|
||||
decoded_bal,
|
||||
};
|
||||
|
||||
// Plan the strategy used for state root computation.
|
||||
@@ -509,33 +520,26 @@ where
|
||||
// Get an iterator over the transactions in the payload
|
||||
let txs = self.tx_iterator_for(&input)?;
|
||||
|
||||
// Extract the BAL, if valid and available
|
||||
let block_access_list = ensure_ok!(input
|
||||
.block_access_list()
|
||||
.transpose()
|
||||
// Eventually gets converted to a `InsertBlockErrorKind::Other`
|
||||
.map_err(Box::<dyn std::error::Error + Send + Sync>::from))
|
||||
.map(Arc::new);
|
||||
|
||||
// Create lazy overlay from ancestors - this doesn't block, allowing execution to start
|
||||
// before the trie data is ready. The overlay will be computed on first access.
|
||||
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, ctx.state());
|
||||
|
||||
// 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))
|
||||
.with_lazy_overlay(lazy_overlay);
|
||||
let overlay_factory =
|
||||
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
|
||||
.with_block_hash(Some(anchor_hash))
|
||||
.with_lazy_overlay(lazy_overlay);
|
||||
OverlayStateProviderFactory::new(provider_factory.clone(), overlay_builder.clone());
|
||||
|
||||
// Spawn the appropriate processor based on strategy
|
||||
let mut handle = ensure_ok!(self.spawn_payload_processor(
|
||||
env.clone(),
|
||||
txs,
|
||||
provider_builder,
|
||||
provider_builder.clone(),
|
||||
overlay_factory.clone(),
|
||||
strategy,
|
||||
block_access_list,
|
||||
));
|
||||
|
||||
// Create optional cache stats for detailed block logging
|
||||
@@ -664,7 +668,7 @@ where
|
||||
let task_result = ensure_ok_post_block!(
|
||||
self.await_state_root_with_timeout(
|
||||
&mut handle,
|
||||
overlay_factory.clone(),
|
||||
provider_builder.clone(),
|
||||
&hashed_state,
|
||||
),
|
||||
block
|
||||
@@ -688,7 +692,9 @@ where
|
||||
// Compare trie updates with serial computation if configured
|
||||
if self.config.always_compare_trie_updates() {
|
||||
let _has_diff = self.compare_trie_updates_with_serial(
|
||||
overlay_factory.clone(),
|
||||
provider_builder.clone(),
|
||||
provider_factory,
|
||||
overlay_builder,
|
||||
&hashed_state,
|
||||
trie_updates.as_ref().clone(),
|
||||
);
|
||||
@@ -727,7 +733,11 @@ where
|
||||
}
|
||||
StateRootStrategy::Parallel => {
|
||||
debug!(target: "engine::tree::payload_validator", "Using parallel state root algorithm");
|
||||
match self.compute_state_root_parallel(overlay_factory.clone(), &hashed_state) {
|
||||
match self.compute_state_root_parallel(
|
||||
provider_factory,
|
||||
overlay_builder,
|
||||
&hashed_state,
|
||||
) {
|
||||
Ok(result) => {
|
||||
let elapsed = root_time.elapsed();
|
||||
info!(
|
||||
@@ -763,7 +773,9 @@ where
|
||||
}
|
||||
|
||||
let (root, updates) = ensure_ok_post_block!(
|
||||
Self::compute_state_root_serial(overlay_factory.clone(), &hashed_state),
|
||||
provider_builder
|
||||
.build()
|
||||
.and_then(|provider| Self::compute_state_root_serial(provider, &hashed_state)),
|
||||
block
|
||||
);
|
||||
|
||||
@@ -1087,7 +1099,8 @@ where
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
|
||||
fn compute_state_root_parallel(
|
||||
&self,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
provider_factory: P,
|
||||
overlay_builder: OverlayBuilder,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
|
||||
let hashed_state = hashed_state.get();
|
||||
@@ -1095,34 +1108,24 @@ where
|
||||
// need to use the prefix sets which were generated from it to indicate to the
|
||||
// ParallelStateRoot which parts of the trie need to be recomputed.
|
||||
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
|
||||
let overlay_factory =
|
||||
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());
|
||||
let overlay_factory = OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
overlay_builder.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted()),
|
||||
);
|
||||
ParallelStateRoot::new(overlay_factory, prefix_sets, self.runtime.clone())
|
||||
.incremental_root_with_updates()
|
||||
}
|
||||
|
||||
/// Compute state root for the given hashed post state in serial.
|
||||
///
|
||||
/// Uses an overlay factory which provides the state of the parent block, along with the
|
||||
/// [`HashedPostState`] containing the changes of this block, to compute the state root and
|
||||
/// trie updates for this block.
|
||||
/// Uses the same provider construction path as main execution and computes the state root and
|
||||
/// trie updates for this block directly via
|
||||
/// [`reth_provider::StateRootProvider::state_root_with_updates`].
|
||||
fn compute_state_root_serial(
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
state_provider: StateProviderBox,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
) -> ProviderResult<(B256, TrieUpdates)> {
|
||||
let hashed_state = hashed_state.get();
|
||||
// The `hashed_state` argument will be taken into account as part of the overlay, but we
|
||||
// need to use the prefix sets which were generated from it to indicate to the
|
||||
// StateRoot which parts of the trie need to be recomputed.
|
||||
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
|
||||
let overlay_factory =
|
||||
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());
|
||||
|
||||
let provider = overlay_factory.database_provider_ro()?;
|
||||
|
||||
Ok(StateRoot::new(&provider, &provider)
|
||||
.with_prefix_sets(prefix_sets)
|
||||
.root_with_updates()?)
|
||||
state_provider.state_root_with_updates(hashed_state.get().clone())
|
||||
}
|
||||
|
||||
/// Awaits the state root from the background task, with an optional timeout fallback.
|
||||
@@ -1147,7 +1150,7 @@ where
|
||||
fn await_state_root_with_timeout<Tx, Err, R: Send + Sync + 'static>(
|
||||
&self,
|
||||
handle: &mut PayloadHandle<Tx, Err, R>,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
state_provider_builder: StateProviderBuilder<N, P>,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
|
||||
let Some(timeout) = self.config.state_root_task_timeout() else {
|
||||
@@ -1172,10 +1175,11 @@ where
|
||||
let (seq_tx, seq_rx) =
|
||||
std::sync::mpsc::channel::<ProviderResult<(B256, TrieUpdates)>>();
|
||||
|
||||
let seq_overlay = overlay_factory;
|
||||
let seq_hashed_state = hashed_state.clone();
|
||||
self.payload_processor.executor().spawn_blocking_named("serial-root", move || {
|
||||
let result = Self::compute_state_root_serial(seq_overlay, &seq_hashed_state);
|
||||
let result = state_provider_builder.build().and_then(|provider| {
|
||||
Self::compute_state_root_serial(provider, &seq_hashed_state)
|
||||
});
|
||||
let _ = seq_tx.send(result);
|
||||
});
|
||||
|
||||
@@ -1239,13 +1243,18 @@ where
|
||||
/// updates.
|
||||
fn compare_trie_updates_with_serial(
|
||||
&self,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
state_provider_builder: StateProviderBuilder<N, P>,
|
||||
provider_factory: P,
|
||||
overlay_builder: OverlayBuilder,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
task_trie_updates: TrieUpdates,
|
||||
) -> bool {
|
||||
debug!(target: "engine::tree::payload_validator", "Comparing trie updates with serial computation");
|
||||
|
||||
match Self::compute_state_root_serial(overlay_factory.clone(), hashed_state) {
|
||||
match state_provider_builder
|
||||
.build()
|
||||
.and_then(|provider| Self::compute_state_root_serial(provider, hashed_state))
|
||||
{
|
||||
Ok((serial_root, serial_trie_updates)) => {
|
||||
debug!(
|
||||
target: "engine::tree::payload_validator",
|
||||
@@ -1254,6 +1263,8 @@ where
|
||||
);
|
||||
|
||||
// Get a database provider to use as trie cursor factory
|
||||
let overlay_factory =
|
||||
OverlayStateProviderFactory::new(provider_factory, overlay_builder);
|
||||
match overlay_factory.database_provider_ro() {
|
||||
Ok(provider) => {
|
||||
match super::trie_updates::compare_trie_updates(
|
||||
@@ -1439,7 +1450,6 @@ where
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
strategy: StateRootStrategy,
|
||||
block_access_list: Option<Arc<BlockAccessList>>,
|
||||
) -> Result<
|
||||
PayloadHandle<
|
||||
impl ExecutableTxFor<Evm> + use<N, P, Evm, V, T>,
|
||||
@@ -1459,7 +1469,6 @@ where
|
||||
provider_builder,
|
||||
overlay_factory,
|
||||
&self.config,
|
||||
block_access_list,
|
||||
);
|
||||
|
||||
// record prewarming initialization duration
|
||||
@@ -1472,12 +1481,8 @@ where
|
||||
}
|
||||
StateRootStrategy::Parallel | StateRootStrategy::Synchronous => {
|
||||
let start = Instant::now();
|
||||
let handle = self.payload_processor.spawn_cache_exclusive(
|
||||
env,
|
||||
txs,
|
||||
provider_builder,
|
||||
block_access_list,
|
||||
);
|
||||
let handle =
|
||||
self.payload_processor.spawn_cache_exclusive(env, txs, provider_builder);
|
||||
|
||||
// Record prewarming initialization duration
|
||||
self.metrics
|
||||
@@ -2026,10 +2031,12 @@ where
|
||||
state: &EngineApiTreeState<N>,
|
||||
) -> Option<StateRootHandle> {
|
||||
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
|
||||
let overlay_factory =
|
||||
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
|
||||
let overlay_factory = OverlayStateProviderFactory::new(
|
||||
self.provider.clone(),
|
||||
OverlayBuilder::new(self.changeset_cache.clone())
|
||||
.with_block_hash(Some(anchor_hash))
|
||||
.with_lazy_overlay(lazy_overlay);
|
||||
.with_lazy_overlay(lazy_overlay),
|
||||
);
|
||||
|
||||
Some(self.payload_processor.spawn_state_root(
|
||||
overlay_factory,
|
||||
@@ -2110,6 +2117,17 @@ impl<T: PayloadTypes> BlockOrPayload<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the decoded block access list, if present and successfully decoded.
|
||||
pub fn try_decoded_access_list(&self) -> Result<Option<DecodedBal>, alloy_rlp::Error> {
|
||||
match self {
|
||||
Self::Payload(payload) => payload
|
||||
.block_access_list()
|
||||
.map(|block_access_list| DecodedBal::from_rlp_bytes(block_access_list.clone()))
|
||||
.transpose(),
|
||||
Self::Block(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of transactions in the payload or block.
|
||||
pub fn transaction_count(&self) -> usize
|
||||
where
|
||||
|
||||
@@ -184,11 +184,18 @@ impl TestHarness {
|
||||
let payload_validator = MockEngineValidator;
|
||||
|
||||
let (from_tree_tx, from_tree_rx) = unbounded_channel();
|
||||
let tree_config =
|
||||
TreeConfig::default().with_legacy_state_root(false).with_has_enough_parallelism(true);
|
||||
|
||||
let header = chain_spec.genesis_header().clone();
|
||||
let header = SealedHeader::seal_slow(header);
|
||||
let engine_api_tree_state =
|
||||
EngineApiTreeState::new(10, 10, header.num_hash(), EngineApiKind::Ethereum);
|
||||
let engine_api_tree_state = EngineApiTreeState::new(
|
||||
10,
|
||||
10,
|
||||
tree_config.invalid_header_hit_eviction_threshold(),
|
||||
header.num_hash(),
|
||||
EngineApiKind::Ethereum,
|
||||
);
|
||||
let canonical_in_memory_state = CanonicalInMemoryState::with_head(header, None, None);
|
||||
|
||||
let (to_payload_service, _payload_command_rx) = unbounded_channel();
|
||||
@@ -217,8 +224,7 @@ impl TestHarness {
|
||||
persistence_handle,
|
||||
PersistenceState { last_persisted_block: BlockNumHash::default(), rx: None },
|
||||
payload_builder,
|
||||
// always assume enough parallelism for tests
|
||||
TreeConfig::default().with_legacy_state_root(false).with_has_enough_parallelism(true),
|
||||
tree_config,
|
||||
EngineApiKind::Ethereum,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! and injecting them into era1 files with `Era1Writer`.
|
||||
|
||||
use crate::calculate_td_by_number;
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_primitives::{BlockNumber, B256, U256};
|
||||
use alloy_consensus::{BlockHeader, Sealable, TxReceipt};
|
||||
use alloy_primitives::{BlockNumber, U256};
|
||||
use eyre::{eyre, Result};
|
||||
use reth_era::{
|
||||
common::file_ops::{EraFileId, StreamWriter},
|
||||
@@ -13,7 +13,7 @@ use reth_era::{
|
||||
types::{
|
||||
execution::{
|
||||
Accumulator, BlockTuple, CompressedBody, CompressedHeader, CompressedReceipts,
|
||||
TotalDifficulty, MAX_BLOCKS_PER_ERA1,
|
||||
HeaderRecord, TotalDifficulty, MAX_BLOCKS_PER_ERA1,
|
||||
},
|
||||
group::{BlockIndex, Era1Id},
|
||||
},
|
||||
@@ -139,17 +139,21 @@ where
|
||||
|
||||
let headers = provider.headers_range(start_block..=end_block)?;
|
||||
|
||||
// Extract first 4 bytes of last block's state root as historical identifier
|
||||
let historical_root = headers
|
||||
.last()
|
||||
.map(|header| {
|
||||
let state_root = header.state_root();
|
||||
[state_root[0], state_root[1], state_root[2], state_root[3]]
|
||||
// Pre-compute accumulator from headers to determine filename
|
||||
let mut precompute_td = total_difficulty;
|
||||
let header_records: Vec<HeaderRecord> = headers
|
||||
.iter()
|
||||
.map(|h| {
|
||||
precompute_td += h.difficulty();
|
||||
HeaderRecord { block_hash: h.hash_slow(), total_difficulty: precompute_td }
|
||||
})
|
||||
.unwrap_or([0u8; 4]);
|
||||
.collect();
|
||||
let accumulator = Accumulator::from_header_records(&header_records)
|
||||
.map_err(|e| eyre!("Failed to compute accumulator: {e}"))?;
|
||||
let file_hash: [u8; 4] = accumulator.root[..4].try_into().unwrap();
|
||||
|
||||
let era1_id = Era1Id::new(&config.network, start_block, block_count as u32)
|
||||
.with_hash(historical_root);
|
||||
let era1_id =
|
||||
Era1Id::new(&config.network, start_block, block_count as u32).with_hash(file_hash);
|
||||
|
||||
let era1_id = if config.max_blocks_per_file == MAX_BLOCKS_PER_ERA1 as u64 {
|
||||
era1_id
|
||||
@@ -166,7 +170,6 @@ where
|
||||
let mut offsets = Vec::<i64>::with_capacity(block_count);
|
||||
let mut position = VERSION_ENTRY_SIZE as i64;
|
||||
let mut blocks_written = 0;
|
||||
let mut final_header_data = Vec::new();
|
||||
|
||||
for (i, header) in headers.into_iter().enumerate() {
|
||||
let expected_block_number = start_block + i as u64;
|
||||
@@ -178,11 +181,6 @@ where
|
||||
&mut total_difficulty,
|
||||
)?;
|
||||
|
||||
// Save last block's header data for accumulator
|
||||
if expected_block_number == end_block {
|
||||
final_header_data = compressed_header.data.clone();
|
||||
}
|
||||
|
||||
let difficulty = TotalDifficulty::new(total_difficulty);
|
||||
|
||||
let header_size = compressed_header.data.len() + ENTRY_HEADER_SIZE;
|
||||
@@ -218,10 +216,12 @@ where
|
||||
}
|
||||
}
|
||||
if blocks_written > 0 {
|
||||
let accumulator_hash =
|
||||
B256::from_slice(&final_header_data[0..32.min(final_header_data.len())]);
|
||||
let accumulator = Accumulator::new(accumulator_hash);
|
||||
let block_index = BlockIndex::new(start_block, offsets);
|
||||
// Convert absolute offsets to relative (measured from block-index entry start)
|
||||
let accumulator_entry_size = (ENTRY_HEADER_SIZE + 32) as i64;
|
||||
let block_index_position = position + accumulator_entry_size;
|
||||
let relative_offsets: Vec<i64> =
|
||||
offsets.iter().map(|&abs| abs - block_index_position).collect();
|
||||
let block_index = BlockIndex::new(start_block, relative_offsets);
|
||||
|
||||
writer.write_accumulator(&accumulator)?;
|
||||
writer.write_block_index(&block_index)?;
|
||||
@@ -310,7 +310,9 @@ where
|
||||
|
||||
let compressed_header = CompressedHeader::from_header(&header)?;
|
||||
let compressed_body = CompressedBody::from_body(&body)?;
|
||||
let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts)
|
||||
let receipts_with_bloom: Vec<_> =
|
||||
receipts.iter().map(|r| TxReceipt::with_bloom_ref(r)).collect();
|
||||
let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts_with_bloom)
|
||||
.map_err(|e| eyre!("Failed to compress receipts: {}", e))?;
|
||||
|
||||
Ok((compressed_header, compressed_body, compressed_receipts))
|
||||
|
||||
@@ -24,6 +24,7 @@ snap.workspace = true
|
||||
# ssz encoding and decoding
|
||||
ethereum_ssz.workspace = true
|
||||
ethereum_ssz_derive.workspace = true
|
||||
sha2.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
eyre.workspace = true
|
||||
|
||||
@@ -76,6 +76,7 @@ use crate::{
|
||||
use alloy_consensus::{Block, BlockBody, Header};
|
||||
use alloy_primitives::{B256, U256};
|
||||
use alloy_rlp::{Decodable, Encodable};
|
||||
use sha2::{Digest, Sha256};
|
||||
use snap::{read::FrameDecoder, write::FrameEncoder};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
@@ -493,6 +494,73 @@ impl Accumulator {
|
||||
|
||||
Ok(Self { root: B256::from(root) })
|
||||
}
|
||||
|
||||
/// Compute the accumulator from a list of header records.
|
||||
///
|
||||
/// Implements `hash_tree_root(List[HeaderRecord, 8192])` per the ERA1 spec:
|
||||
/// - Each leaf is `sha256(block_hash || total_difficulty_le_bytes32)`
|
||||
/// - Leaves are padded to `MAX_BLOCKS_PER_ERA1` (8192) with zero hashes
|
||||
/// - Binary Merkle tree is computed bottom-up
|
||||
/// - Final root is `sha256(merkle_root || le_bytes32(actual_count))`
|
||||
///
|
||||
/// Returns `Err` if `records` exceeds [`MAX_BLOCKS_PER_ERA1`].
|
||||
pub fn from_header_records(records: &[HeaderRecord]) -> Result<Self, E2sError> {
|
||||
let capacity = MAX_BLOCKS_PER_ERA1;
|
||||
|
||||
if records.len() > capacity {
|
||||
return Err(E2sError::Ssz(format!(
|
||||
"Too many header records: got {}, max {}",
|
||||
records.len(),
|
||||
capacity
|
||||
)));
|
||||
}
|
||||
|
||||
// Compute leaf hash for each header record
|
||||
let mut leaves = Vec::with_capacity(capacity);
|
||||
for record in records {
|
||||
let mut data = [0u8; 64];
|
||||
data[..32].copy_from_slice(record.block_hash.as_slice());
|
||||
data[32..].copy_from_slice(&record.total_difficulty.to_le_bytes::<32>());
|
||||
leaves.push(<[u8; 32]>::from(Sha256::digest(data)));
|
||||
}
|
||||
|
||||
// Pad to capacity with zero hashes
|
||||
leaves.resize(capacity, [0u8; 32]);
|
||||
|
||||
// Binary Merkle tree bottom-up (capacity is always a power of two)
|
||||
while leaves.len() > 1 {
|
||||
let mut next_level = Vec::with_capacity(leaves.len() / 2);
|
||||
for pair in leaves.chunks_exact(2) {
|
||||
let mut data = [0u8; 64];
|
||||
data[..32].copy_from_slice(&pair[0]);
|
||||
data[32..].copy_from_slice(&pair[1]);
|
||||
next_level.push(<[u8; 32]>::from(Sha256::digest(data)));
|
||||
}
|
||||
leaves = next_level;
|
||||
}
|
||||
|
||||
let merkle_root = leaves[0];
|
||||
|
||||
// mix_in_length: sha256(merkle_root || le_bytes32(actual_length))
|
||||
let mut mix = [0u8; 64];
|
||||
mix[..32].copy_from_slice(&merkle_root);
|
||||
let length = records.len() as u64;
|
||||
mix[32..40].copy_from_slice(&length.to_le_bytes());
|
||||
// remaining bytes stay zero (uint256 LE padding)
|
||||
|
||||
Ok(Self { root: B256::from(<[u8; 32]>::from(Sha256::digest(mix))) })
|
||||
}
|
||||
}
|
||||
|
||||
/// A header record used to compute the ERA1 accumulator.
|
||||
///
|
||||
/// Per the ERA1 spec: `header-record := { block-hash: Bytes32, total-difficulty: Uint256 }`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderRecord {
|
||||
/// The block hash (keccak256 of RLP-encoded header)
|
||||
pub block_hash: B256,
|
||||
/// The cumulative total difficulty at this block
|
||||
pub total_difficulty: U256,
|
||||
}
|
||||
|
||||
/// A block tuple in an Era1 file, containing all components for a single block
|
||||
@@ -691,6 +759,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accumulator_from_header_records_known_vectors() {
|
||||
// Known-answer vectors computed from the SSZ spec:
|
||||
// hash_tree_root(List[HeaderRecord, 8192])
|
||||
let expected_empty: B256 =
|
||||
"4a8c3a07c8d23adc5bac61157555c3c784d53d9bc110c1370809bd23cd93777d".parse().unwrap();
|
||||
let expected_single_zero: B256 =
|
||||
"81fd641249670887a731386e756a7a1538dc781b1b0bf016889045d350812817".parse().unwrap();
|
||||
let expected_single_nonzero: B256 =
|
||||
"ada35c48d81117f4fd588554cd4c4752356336e84cb41106dea1ceb4cfac8799".parse().unwrap();
|
||||
|
||||
// Empty list
|
||||
let acc_empty = Accumulator::from_header_records(&[]).unwrap();
|
||||
assert_eq!(acc_empty.root, expected_empty);
|
||||
|
||||
// Single record with zero values
|
||||
let records = vec![HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO }];
|
||||
let acc = Accumulator::from_header_records(&records).unwrap();
|
||||
assert_eq!(acc.root, expected_single_zero);
|
||||
|
||||
// Single record with non-zero values
|
||||
let records2 = vec![HeaderRecord {
|
||||
block_hash: B256::from([1u8; 32]),
|
||||
total_difficulty: U256::from(100u64),
|
||||
}];
|
||||
let acc2 = Accumulator::from_header_records(&records2).unwrap();
|
||||
assert_eq!(acc2.root, expected_single_nonzero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accumulator_rejects_oversized_input() {
|
||||
let records = vec![
|
||||
HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO };
|
||||
MAX_BLOCKS_PER_ERA1 + 1
|
||||
];
|
||||
assert!(Accumulator::from_header_records(&records).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_list_compression() {
|
||||
let receipts = create_test_receipts();
|
||||
|
||||
@@ -102,8 +102,8 @@ pub struct Era1Id {
|
||||
/// Number of blocks in the file
|
||||
pub block_count: u32,
|
||||
|
||||
/// Optional hash identifier for this file
|
||||
/// First 4 bytes of the last historical root in the last state in the era file
|
||||
/// Optional hash identifier for this file.
|
||||
/// First 4 bytes of the accumulator root hash.
|
||||
pub hash: Option<[u8; 4]>,
|
||||
|
||||
/// Whether to include era count in filename
|
||||
|
||||
@@ -491,7 +491,6 @@ mod tests {
|
||||
fn parse_env_filter_directives() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
|
||||
let reth = Cli::try_parse_args_from([
|
||||
"reth",
|
||||
"init",
|
||||
|
||||
@@ -48,7 +48,7 @@ tokio.workspace = true
|
||||
|
||||
# revm with required ethereum features
|
||||
# Note: this must be kept to ensure all features are properly enabled/forwarded
|
||||
revm = { workspace = true, features = ["secp256k1", "blst", "c-kzg", "memory_limit"] }
|
||||
revm = { workspace = true, features = ["secp256k1", "blst", "c-kzg", "memory_limit", "p256-aws-lc-rs"] }
|
||||
|
||||
# misc
|
||||
eyre.workspace = true
|
||||
|
||||
@@ -178,8 +178,13 @@ where
|
||||
));
|
||||
}
|
||||
let state = StateProviderDatabase::new(state_provider.as_ref());
|
||||
let mut db =
|
||||
State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
|
||||
let chain_spec = client.chain_spec();
|
||||
let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(attributes.timestamp());
|
||||
let mut db = State::builder()
|
||||
.with_database(cached_reads.as_db_mut(state))
|
||||
.with_bundle_update()
|
||||
.with_bal_builder_if(is_amsterdam)
|
||||
.build();
|
||||
|
||||
let mut builder = evm_config
|
||||
.builder_for_next_block(
|
||||
@@ -198,8 +203,6 @@ where
|
||||
)
|
||||
.map_err(PayloadBuilderError::other)?;
|
||||
|
||||
let chain_spec = client.chain_spec();
|
||||
|
||||
debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
|
||||
let mut cumulative_gas_used = 0;
|
||||
let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
|
||||
@@ -359,6 +362,25 @@ where
|
||||
}
|
||||
continue
|
||||
}
|
||||
// EIP-7778: the executor tracks gas_before_refund while the payload builder's
|
||||
// pre-check uses gas_after_refund. Near-full blocks can pass the pre-check but
|
||||
// fail the executor's check. Skip the tx and continue building.
|
||||
Err(BlockExecutionError::Validation(
|
||||
BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
|
||||
transaction_gas_limit,
|
||||
block_available_gas,
|
||||
},
|
||||
)) => {
|
||||
trace!(target: "payload_builder", %transaction_gas_limit, %block_available_gas, ?tx_hash, "skipping transaction exceeding block gas limit");
|
||||
best_txs.mark_invalid(
|
||||
&pool_tx,
|
||||
&InvalidPoolTransactionError::ExceedsGasLimit(
|
||||
transaction_gas_limit,
|
||||
block_available_gas,
|
||||
),
|
||||
);
|
||||
continue
|
||||
}
|
||||
// this is an error that we should treat as fatal for this attempt
|
||||
Err(err) => return Err(PayloadBuilderError::evm(err)),
|
||||
};
|
||||
|
||||
@@ -225,7 +225,7 @@ impl Discv5 {
|
||||
bootstrap_lookup_interval,
|
||||
bootstrap_lookup_countdown,
|
||||
metrics.clone(),
|
||||
discv5.clone(),
|
||||
Arc::downgrade(&discv5),
|
||||
);
|
||||
|
||||
Ok((
|
||||
@@ -573,14 +573,19 @@ pub fn spawn_populate_kbuckets_bg(
|
||||
bootstrap_lookup_interval: u64,
|
||||
bootstrap_lookup_countdown: u64,
|
||||
metrics: Discv5Metrics,
|
||||
discv5: Arc<discv5::Discv5>,
|
||||
discv5: std::sync::Weak<discv5::Discv5>,
|
||||
) {
|
||||
let local_node_id = discv5.local_enr().node_id();
|
||||
let lookup_interval = Duration::from_secs(lookup_interval);
|
||||
let metrics = metrics.discovered_peers;
|
||||
let mut kbucket_index = MAX_KBUCKET_INDEX;
|
||||
let pulse_lookup_interval = Duration::from_secs(bootstrap_lookup_interval);
|
||||
task::spawn(async move {
|
||||
let Some(discv5_handle) = discv5.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let local_node_id = discv5_handle.local_enr().node_id();
|
||||
drop(discv5_handle);
|
||||
|
||||
// make many fast lookup queries at bootstrap, trying to fill kbuckets at furthest
|
||||
// log2distance from local node
|
||||
for i in (0..bootstrap_lookup_countdown).rev() {
|
||||
@@ -593,7 +598,12 @@ pub fn spawn_populate_kbuckets_bg(
|
||||
"starting bootstrap boost lookup query"
|
||||
);
|
||||
|
||||
lookup(target, &discv5, &metrics).await;
|
||||
{
|
||||
let Some(discv5_handle) = discv5.upgrade() else {
|
||||
return;
|
||||
};
|
||||
lookup(target, &discv5_handle, &metrics).await;
|
||||
}
|
||||
|
||||
tokio::time::sleep(pulse_lookup_interval).await;
|
||||
}
|
||||
@@ -610,7 +620,12 @@ pub fn spawn_populate_kbuckets_bg(
|
||||
"starting periodic lookup query"
|
||||
);
|
||||
|
||||
lookup(target, &discv5, &metrics).await;
|
||||
{
|
||||
let Some(discv5_handle) = discv5.upgrade() else {
|
||||
return;
|
||||
};
|
||||
lookup(target, &discv5_handle, &metrics).await;
|
||||
}
|
||||
|
||||
if kbucket_index > DEFAULT_MIN_TARGET_KBUCKET_INDEX {
|
||||
// try to populate bucket one step closer
|
||||
@@ -698,8 +713,10 @@ mod test {
|
||||
use ::enr::{CombinedKey, EnrKey};
|
||||
use rand_08::thread_rng;
|
||||
use reth_chainspec::MAINNET;
|
||||
use reth_tracing::init_test_tracing;
|
||||
use std::env;
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
fn discv5_noop() -> Discv5 {
|
||||
@@ -738,6 +755,61 @@ mod test {
|
||||
Discv5::start(&secret_key, discv5_config).await.expect("should build discv5")
|
||||
}
|
||||
|
||||
async fn start_discovery_node_with_key(
|
||||
secret_key: &SecretKey,
|
||||
udp_port_discv5: u16,
|
||||
) -> Result<(Discv5, mpsc::Receiver<discv5::Event>), Error> {
|
||||
let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
|
||||
let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
|
||||
|
||||
let discv5_listen_config = ListenConfig::from(discv5_addr);
|
||||
let discv5_config = Config::builder(rlpx_addr)
|
||||
.discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
|
||||
.build();
|
||||
|
||||
Discv5::start(secret_key, discv5_config).await
|
||||
}
|
||||
|
||||
fn unused_udp_port() -> u16 {
|
||||
UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
|
||||
}
|
||||
|
||||
async fn wait_for_udp_port_release(port: u16, timeout: Duration) {
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
loop {
|
||||
match UdpSocket::bind(("127.0.0.1", port)) {
|
||||
Ok(socket) => {
|
||||
drop(socket);
|
||||
return;
|
||||
}
|
||||
Err(err) if Instant::now() < deadline => {
|
||||
trace!(target: "net::discv5::test", %port, %err, "waiting for discv5 port release");
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
Err(err) => panic!("discv5 did not release port {port} before timeout: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn discv5_releases_port_on_drop() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let secret_key = SecretKey::new(&mut thread_rng());
|
||||
let port = unused_udp_port();
|
||||
|
||||
let (node, updates) =
|
||||
start_discovery_node_with_key(&secret_key, port).await.expect("should start discv5");
|
||||
drop(updates);
|
||||
drop(node);
|
||||
|
||||
wait_for_udp_port_release(port, Duration::from_secs(1)).await;
|
||||
|
||||
let restarted = start_discovery_node_with_key(&secret_key, port).await;
|
||||
assert!(restarted.is_ok(), "discv5 failed to rebind dropped port: {restarted:?}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn discv5() {
|
||||
reth_tracing::init_test_tracing();
|
||||
@@ -937,11 +1009,6 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn get_fork_id_with_different_network_stack_ids() {
|
||||
unsafe {
|
||||
env::set_var("RUST_LOG", "net::discv5=trace");
|
||||
}
|
||||
init_test_tracing();
|
||||
|
||||
let fork_id = MAINNET.latest_fork_id();
|
||||
let sk = SecretKey::new(&mut thread_rng());
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! All capability related types
|
||||
|
||||
use crate::{EthMessageID, EthVersion};
|
||||
use crate::{EthMessageID, EthVersion, SnapVersion};
|
||||
use alloc::{borrow::Cow, string::String, vec::Vec};
|
||||
use alloy_primitives::bytes::Bytes;
|
||||
use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
|
||||
@@ -85,6 +85,11 @@ impl Capability {
|
||||
Self::new_static("eth", version as usize)
|
||||
}
|
||||
|
||||
/// Returns the corresponding snap capability for the given version.
|
||||
pub const fn snap(version: SnapVersion) -> Self {
|
||||
Self::new_static("snap", version as usize)
|
||||
}
|
||||
|
||||
/// Returns the [`EthVersion::Eth66`] capability.
|
||||
pub const fn eth_66() -> Self {
|
||||
Self::eth(EthVersion::Eth66)
|
||||
@@ -115,6 +120,16 @@ impl Capability {
|
||||
Self::eth(EthVersion::Eth71)
|
||||
}
|
||||
|
||||
/// Returns the `snap/1` capability.
|
||||
pub const fn snap_1() -> Self {
|
||||
Self::snap(SnapVersion::V1)
|
||||
}
|
||||
|
||||
/// Returns the `snap/2` capability.
|
||||
pub const fn snap_2() -> Self {
|
||||
Self::snap(SnapVersion::V2)
|
||||
}
|
||||
|
||||
/// Whether this is eth v66 protocol.
|
||||
#[inline]
|
||||
pub fn is_eth_v66(&self) -> bool {
|
||||
|
||||
@@ -3,13 +3,41 @@
|
||||
//! facilitating the exchange of Ethereum state snapshots between peers
|
||||
//! Reference: [Ethereum Snapshot Protocol](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#protocol-messages)
|
||||
//!
|
||||
//! Current version: snap/1
|
||||
//! This module currently includes snap/1 plus preparatory snap/2 message definitions.
|
||||
|
||||
use crate::BlockAccessLists;
|
||||
use alloc::vec::Vec;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
|
||||
use reth_codecs_derive::add_arbitrary_tests;
|
||||
|
||||
/// Supported SNAP protocol versions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum SnapVersion {
|
||||
/// The original snapshot protocol.
|
||||
#[default]
|
||||
V1 = 1,
|
||||
/// BAL-based healing as proposed by EIP-8189.
|
||||
V2 = 2,
|
||||
}
|
||||
|
||||
impl SnapVersion {
|
||||
/// Returns the number of messages supported by this version.
|
||||
pub const fn message_count(self) -> u8 {
|
||||
match self {
|
||||
Self::V1 => 8,
|
||||
Self::V2 => 10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the highest supported message id for this version.
|
||||
pub const fn max_message_id(self) -> u8 {
|
||||
self.message_count() - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Message IDs for the snap sync protocol
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapMessageId {
|
||||
@@ -27,9 +55,21 @@ pub enum SnapMessageId {
|
||||
/// Response for the number of requested contract codes.
|
||||
ByteCodes = 0x05,
|
||||
/// Request of the number of state (either account or storage) Merkle trie nodes by path.
|
||||
///
|
||||
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
|
||||
GetTrieNodes = 0x06,
|
||||
/// Response for the number of requested state trie nodes.
|
||||
///
|
||||
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
|
||||
TrieNodes = 0x07,
|
||||
/// Request BALs for a list of block hashes.
|
||||
///
|
||||
/// Only valid for `snap/2`.
|
||||
GetBlockAccessLists = 0x08,
|
||||
/// Response containing BALs for the requested block hashes.
|
||||
///
|
||||
/// Only valid for `snap/2`.
|
||||
BlockAccessLists = 0x09,
|
||||
}
|
||||
|
||||
/// Request for a range of accounts from the state trie.
|
||||
@@ -187,6 +227,30 @@ pub struct TrieNodesMessage {
|
||||
pub nodes: Vec<Bytes>,
|
||||
}
|
||||
|
||||
/// Request BALs for the given block hashes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct GetBlockAccessListsMessage {
|
||||
/// Request ID to match up responses with.
|
||||
pub request_id: u64,
|
||||
/// Block hashes to retrieve BALs for.
|
||||
pub block_hashes: Vec<B256>,
|
||||
/// Soft limit at which to stop returning data (in bytes).
|
||||
pub response_bytes: u64,
|
||||
}
|
||||
|
||||
/// Response containing one BAL per requested block hash.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct BlockAccessListsMessage {
|
||||
/// ID of the request this is a response for.
|
||||
pub request_id: u64,
|
||||
/// Raw BAL payloads in request order.
|
||||
pub block_access_lists: BlockAccessLists,
|
||||
}
|
||||
|
||||
/// Represents all types of messages in the snap sync protocol.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SnapProtocolMessage {
|
||||
@@ -203,9 +267,21 @@ pub enum SnapProtocolMessage {
|
||||
/// Response with contract codes - see [`ByteCodesMessage`]
|
||||
ByteCodes(ByteCodesMessage),
|
||||
/// Request for trie nodes - see [`GetTrieNodesMessage`]
|
||||
///
|
||||
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
|
||||
GetTrieNodes(GetTrieNodesMessage),
|
||||
/// Response with trie nodes - see [`TrieNodesMessage`]
|
||||
///
|
||||
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
|
||||
TrieNodes(TrieNodesMessage),
|
||||
/// Request for block access lists - see [`GetBlockAccessListsMessage`]
|
||||
///
|
||||
/// Only valid for `snap/2`.
|
||||
GetBlockAccessLists(GetBlockAccessListsMessage),
|
||||
/// Response with block access lists - see [`BlockAccessListsMessage`]
|
||||
///
|
||||
/// Only valid for `snap/2`.
|
||||
BlockAccessLists(BlockAccessListsMessage),
|
||||
}
|
||||
|
||||
impl SnapProtocolMessage {
|
||||
@@ -222,6 +298,8 @@ impl SnapProtocolMessage {
|
||||
Self::ByteCodes(_) => SnapMessageId::ByteCodes,
|
||||
Self::GetTrieNodes(_) => SnapMessageId::GetTrieNodes,
|
||||
Self::TrieNodes(_) => SnapMessageId::TrieNodes,
|
||||
Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
|
||||
Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +319,8 @@ impl SnapProtocolMessage {
|
||||
Self::ByteCodes(msg) => msg.encode(&mut buf),
|
||||
Self::GetTrieNodes(msg) => msg.encode(&mut buf),
|
||||
Self::TrieNodes(msg) => msg.encode(&mut buf),
|
||||
Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
|
||||
Self::BlockAccessLists(msg) => msg.encode(&mut buf),
|
||||
}
|
||||
|
||||
Bytes::from(buf)
|
||||
@@ -314,6 +394,20 @@ impl SnapProtocolMessage {
|
||||
TrieNodes,
|
||||
TrieNodesMessage
|
||||
);
|
||||
decode_snap_message_variant!(
|
||||
message_id,
|
||||
buf,
|
||||
SnapMessageId::GetBlockAccessLists,
|
||||
GetBlockAccessLists,
|
||||
GetBlockAccessListsMessage
|
||||
);
|
||||
decode_snap_message_variant!(
|
||||
message_id,
|
||||
buf,
|
||||
SnapMessageId::BlockAccessLists,
|
||||
BlockAccessLists,
|
||||
BlockAccessListsMessage
|
||||
);
|
||||
|
||||
Err(alloy_rlp::Error::Custom("Unknown message ID"))
|
||||
}
|
||||
@@ -344,6 +438,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_all_message_roundtrips() {
|
||||
assert_eq!(SnapVersion::V1.message_count(), 8);
|
||||
assert_eq!(SnapVersion::V2.message_count(), 10);
|
||||
|
||||
test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
|
||||
request_id: 42,
|
||||
root_hash: b256_from_u64(123),
|
||||
@@ -404,6 +501,20 @@ mod tests {
|
||||
request_id: 42,
|
||||
nodes: vec![Bytes::from(vec![1, 2, 3])],
|
||||
}));
|
||||
|
||||
test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
|
||||
request_id: 42,
|
||||
block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
|
||||
response_bytes: 4096,
|
||||
}));
|
||||
|
||||
test_roundtrip(SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
|
||||
request_id: 42,
|
||||
block_access_lists: BlockAccessLists(vec![
|
||||
Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]),
|
||||
Bytes::from_static(&[0xc1, alloy_rlp::EMPTY_LIST_CODE]),
|
||||
]),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::message::MAX_MESSAGE_SIZE;
|
||||
use crate::{
|
||||
message::{EthBroadcastMessage, ProtocolBroadcastMessage},
|
||||
EthMessage, EthMessageID, EthNetworkPrimitives, EthVersion, NetworkPrimitives, ProtocolMessage,
|
||||
RawCapabilityMessage, SnapMessageId, SnapProtocolMessage,
|
||||
RawCapabilityMessage, SnapProtocolMessage, SnapVersion,
|
||||
};
|
||||
use alloy_rlp::{Bytes, BytesMut, Encodable};
|
||||
use core::fmt::Debug;
|
||||
@@ -74,6 +74,18 @@ where
|
||||
Self { eth_snap: EthSnapStreamInner::new(eth_version), inner: stream }
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with an explicit snap version.
|
||||
pub const fn new_with_snap_version(
|
||||
stream: S,
|
||||
eth_version: EthVersion,
|
||||
snap_version: SnapVersion,
|
||||
) -> Self {
|
||||
Self {
|
||||
eth_snap: EthSnapStreamInner::new_with_snap_version(eth_version, snap_version),
|
||||
inner: stream,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with a custom max message size.
|
||||
pub const fn with_max_message_size(
|
||||
stream: S,
|
||||
@@ -86,12 +98,35 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with a custom max message size and snap version.
|
||||
pub const fn with_max_message_size_and_snap_version(
|
||||
stream: S,
|
||||
eth_version: EthVersion,
|
||||
snap_version: SnapVersion,
|
||||
max_message_size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
eth_snap: EthSnapStreamInner::with_max_message_size_and_snap_version(
|
||||
eth_version,
|
||||
snap_version,
|
||||
max_message_size,
|
||||
),
|
||||
inner: stream,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the eth version
|
||||
#[inline]
|
||||
pub const fn eth_version(&self) -> EthVersion {
|
||||
self.eth_snap.eth_version()
|
||||
}
|
||||
|
||||
/// Returns the snap version.
|
||||
#[inline]
|
||||
pub const fn snap_version(&self) -> SnapVersion {
|
||||
self.eth_snap.snap_version()
|
||||
}
|
||||
|
||||
/// Returns the underlying stream
|
||||
#[inline]
|
||||
pub const fn inner(&self) -> &S {
|
||||
@@ -193,13 +228,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream handling combined eth and snap protocol logic
|
||||
/// Snap version is not critical to specify yet,
|
||||
/// Only one version, snap/1, does exist.
|
||||
/// Stream handling combined eth and snap protocol logic.
|
||||
#[derive(Debug, Clone)]
|
||||
struct EthSnapStreamInner<N> {
|
||||
/// Eth protocol version
|
||||
eth_version: EthVersion,
|
||||
/// Snap protocol version
|
||||
snap_version: SnapVersion,
|
||||
/// Maximum allowed ETH/Snap message size.
|
||||
max_message_size: usize,
|
||||
/// Type marker
|
||||
@@ -212,12 +247,26 @@ where
|
||||
{
|
||||
/// Create a new eth and snap protocol stream
|
||||
const fn new(eth_version: EthVersion) -> Self {
|
||||
Self::with_max_message_size(eth_version, MAX_MESSAGE_SIZE)
|
||||
Self::new_with_snap_version(eth_version, SnapVersion::V1)
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with an explicit snap version.
|
||||
const fn new_with_snap_version(eth_version: EthVersion, snap_version: SnapVersion) -> Self {
|
||||
Self::with_max_message_size_and_snap_version(eth_version, snap_version, MAX_MESSAGE_SIZE)
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with a custom max message size.
|
||||
const fn with_max_message_size(eth_version: EthVersion, max_message_size: usize) -> Self {
|
||||
Self { eth_version, max_message_size, _pd: PhantomData }
|
||||
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V1, max_message_size)
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with a custom max message size and snap version.
|
||||
const fn with_max_message_size_and_snap_version(
|
||||
eth_version: EthVersion,
|
||||
snap_version: SnapVersion,
|
||||
max_message_size: usize,
|
||||
) -> Self {
|
||||
Self { eth_version, snap_version, max_message_size, _pd: PhantomData }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -225,6 +274,11 @@ where
|
||||
self.eth_version
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn snap_version(&self) -> SnapVersion {
|
||||
self.snap_version
|
||||
}
|
||||
|
||||
/// Decode a message from the stream
|
||||
fn decode_message(&self, bytes: BytesMut) -> Result<EthSnapMessage<N>, EthSnapStreamError> {
|
||||
if bytes.len() > self.max_message_size {
|
||||
@@ -256,8 +310,9 @@ where
|
||||
}
|
||||
}
|
||||
} else if message_id > EthMessageID::max(self.eth_version) &&
|
||||
message_id <=
|
||||
EthMessageID::message_count(self.eth_version) + SnapMessageId::TrieNodes as u8
|
||||
message_id <
|
||||
EthMessageID::message_count(self.eth_version) +
|
||||
self.snap_version.message_count()
|
||||
{
|
||||
// Checks for multiplexed snap message IDs :
|
||||
// - message_id > EthMessageID::max() : ensures it's not an eth message
|
||||
@@ -313,8 +368,8 @@ mod tests {
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rlp::Encodable;
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, GetAccountRangeMessage, GetBlockAccessLists, GetBlockHeaders,
|
||||
HeadersDirection,
|
||||
message::RequestPair, BlockAccessLists, BlockAccessListsMessage, GetAccountRangeMessage,
|
||||
GetBlockAccessLists, GetBlockAccessListsMessage, GetBlockHeaders, HeadersDirection,
|
||||
};
|
||||
|
||||
// Helper to create eth message and its bytes
|
||||
@@ -352,6 +407,22 @@ mod tests {
|
||||
(snap_msg, BytesMut::from(&encoded[..]))
|
||||
}
|
||||
|
||||
fn create_snap2_message() -> (SnapProtocolMessage, BytesMut) {
|
||||
let snap_msg = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
|
||||
request_id: 1,
|
||||
block_hashes: vec![B256::default()],
|
||||
response_bytes: 1000,
|
||||
});
|
||||
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new_with_snap_version(
|
||||
EthVersion::Eth67,
|
||||
SnapVersion::V2,
|
||||
);
|
||||
let encoded = inner.encode_snap_message(snap_msg.clone());
|
||||
|
||||
(snap_msg, BytesMut::from(&encoded[..]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth_message_roundtrip() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth67);
|
||||
@@ -412,6 +483,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snap2_protocol() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new_with_snap_version(
|
||||
EthVersion::Eth67,
|
||||
SnapVersion::V2,
|
||||
);
|
||||
let (snap_msg, snap_bytes) = create_snap2_message();
|
||||
|
||||
let encoded_bytes = inner.encode_snap_message(snap_msg.clone());
|
||||
assert!(!encoded_bytes.is_empty());
|
||||
|
||||
let decoded_result = inner.decode_message(snap_bytes.clone());
|
||||
assert!(matches!(decoded_result, Ok(EthSnapMessage::Snap(_))));
|
||||
|
||||
if let Ok(EthSnapMessage::Snap(decoded_msg)) = inner.decode_message(snap_bytes) {
|
||||
assert_eq!(decoded_msg, snap_msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_id_boundaries() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth67);
|
||||
@@ -475,4 +565,24 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded_eth, eth_msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snap1_rejects_snap2_message_ids() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth67);
|
||||
let snap2_msg = SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
|
||||
request_id: 1,
|
||||
block_access_lists: BlockAccessLists(vec![alloy_primitives::Bytes::from_static(&[
|
||||
alloy_rlp::EMPTY_LIST_CODE,
|
||||
])]),
|
||||
});
|
||||
|
||||
let encoded = EthSnapStreamInner::<EthNetworkPrimitives>::new_with_snap_version(
|
||||
EthVersion::Eth67,
|
||||
SnapVersion::V2,
|
||||
)
|
||||
.encode_snap_message(snap2_msg);
|
||||
|
||||
let decoded = inner.decode_message(BytesMut::from(&encoded[..]));
|
||||
assert!(matches!(decoded, Err(EthSnapStreamError::UnknownMessageId(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::{
|
||||
errors::{EthHandshakeError, EthStreamError},
|
||||
handshake::EthereumEthHandshake,
|
||||
message::{EthBroadcastMessage, ProtocolBroadcastMessage, MAX_MESSAGE_SIZE},
|
||||
message::{EthBroadcastMessage, EthMessageID, ProtocolBroadcastMessage, MAX_MESSAGE_SIZE},
|
||||
p2pstream::HANDSHAKE_TIMEOUT,
|
||||
CanDisconnect, DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, ProtocolMessage,
|
||||
UnifiedStatus,
|
||||
@@ -108,6 +108,9 @@ pub struct EthStreamInner<N> {
|
||||
version: EthVersion,
|
||||
/// Maximum allowed ETH message size.
|
||||
max_message_size: usize,
|
||||
/// When true, `NewBlock` (0x07) and `NewBlockHashes` (0x01) messages are rejected before RLP
|
||||
/// decoding to avoid any memory impact for non-PoW networks.
|
||||
reject_block_announcements: bool,
|
||||
_pd: std::marker::PhantomData<N>,
|
||||
}
|
||||
|
||||
@@ -122,7 +125,12 @@ where
|
||||
|
||||
/// Creates a new [`EthStreamInner`] with the given eth version and message size limit.
|
||||
pub const fn with_max_message_size(version: EthVersion, max_message_size: usize) -> Self {
|
||||
Self { version, max_message_size, _pd: std::marker::PhantomData }
|
||||
Self {
|
||||
version,
|
||||
max_message_size,
|
||||
reject_block_announcements: false,
|
||||
_pd: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the eth version
|
||||
@@ -131,12 +139,25 @@ where
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Sets whether to reject block announcement messages (`NewBlock`, `NewBlockHashes`) before
|
||||
/// RLP decoding.
|
||||
pub const fn set_reject_block_announcements(&mut self, reject: bool) {
|
||||
self.reject_block_announcements = reject;
|
||||
}
|
||||
|
||||
/// Decodes incoming bytes into an [`EthMessage`].
|
||||
pub fn decode_message(&self, bytes: BytesMut) -> Result<EthMessage<N>, EthStreamError> {
|
||||
if bytes.len() > self.max_message_size {
|
||||
return Err(EthStreamError::MessageTooBig(bytes.len()));
|
||||
}
|
||||
|
||||
if self.reject_block_announcements &&
|
||||
let Some(&id) = bytes.first() &&
|
||||
(id == EthMessageID::NewBlock.to_u8() || id == EthMessageID::NewBlockHashes.to_u8())
|
||||
{
|
||||
return Err(EthStreamError::UnsupportedMessage { message_id: id });
|
||||
}
|
||||
|
||||
let msg = match ProtocolMessage::decode_message(self.version, &mut bytes.as_ref()) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
@@ -208,6 +229,12 @@ impl<S, N: NetworkPrimitives> EthStream<S, N> {
|
||||
self.eth.version()
|
||||
}
|
||||
|
||||
/// Sets whether to reject block announcement messages (`NewBlock`, `NewBlockHashes`) before
|
||||
/// RLP decoding.
|
||||
pub const fn set_reject_block_announcements(&mut self, reject: bool) {
|
||||
self.eth.set_reject_block_announcements(reject);
|
||||
}
|
||||
|
||||
/// Returns the underlying stream.
|
||||
#[inline]
|
||||
pub const fn inner(&self) -> &S {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! A Protocol defines a P2P subprotocol in an `RLPx` connection
|
||||
|
||||
use crate::{Capability, EthMessageID, EthVersion};
|
||||
use crate::{Capability, EthMessageID, EthVersion, SnapVersion};
|
||||
|
||||
/// Type that represents a [Capability] and the number of messages it uses.
|
||||
///
|
||||
@@ -30,6 +30,13 @@ impl Protocol {
|
||||
Self::new(cap, messages)
|
||||
}
|
||||
|
||||
/// Returns the corresponding snap capability for the given version.
|
||||
pub const fn snap(version: SnapVersion) -> Self {
|
||||
let cap = Capability::snap(version);
|
||||
let messages = version.message_count();
|
||||
Self::new(cap, messages)
|
||||
}
|
||||
|
||||
/// Returns the [`EthVersion::Eth66`] capability.
|
||||
pub const fn eth_66() -> Self {
|
||||
Self::eth(EthVersion::Eth66)
|
||||
@@ -45,6 +52,16 @@ impl Protocol {
|
||||
Self::eth(EthVersion::Eth68)
|
||||
}
|
||||
|
||||
/// Returns the `snap/1` capability.
|
||||
pub const fn snap_1() -> Self {
|
||||
Self::snap(SnapVersion::V1)
|
||||
}
|
||||
|
||||
/// Returns the `snap/2` capability.
|
||||
pub const fn snap_2() -> Self {
|
||||
Self::snap(SnapVersion::V2)
|
||||
}
|
||||
|
||||
/// Consumes the type and returns a tuple of the [Capability] and number of messages.
|
||||
#[inline]
|
||||
pub(crate) fn split(self) -> (Capability, u8) {
|
||||
@@ -86,5 +103,7 @@ mod tests {
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20);
|
||||
assert_eq!(Protocol::snap(SnapVersion::V1).messages(), 8);
|
||||
assert_eq!(Protocol::snap(SnapVersion::V2).messages(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
};
|
||||
use reth_eth_wire::{EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_network_api::test_utils::PeersHandleProvider;
|
||||
use reth_storage_api::BalProvider;
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -63,7 +64,10 @@ impl<Tx, Eth, N: NetworkPrimitives> NetworkBuilder<Tx, Eth, N> {
|
||||
pub fn request_handler<Client>(
|
||||
self,
|
||||
client: Client,
|
||||
) -> NetworkBuilder<Tx, EthRequestHandler<Client, N>, N> {
|
||||
) -> NetworkBuilder<Tx, EthRequestHandler<Client, N>, N>
|
||||
where
|
||||
Client: BalProvider,
|
||||
{
|
||||
let Self { mut network, transactions, .. } = self;
|
||||
let (tx, rx) = mpsc::channel(ETH_REQUEST_CHANNEL_CAPACITY);
|
||||
network.set_eth_request_handler(tx);
|
||||
|
||||
@@ -20,7 +20,9 @@ use reth_eth_wire_types::message::MAX_MESSAGE_SIZE;
|
||||
use reth_ethereum_forks::{ForkFilter, Head};
|
||||
use reth_network_peers::{mainnet_nodes, pk2id, sepolia_nodes, PeerId, TrustedPeer};
|
||||
use reth_network_types::{PeersConfig, SessionsConfig};
|
||||
use reth_storage_api::{noop::NoopProvider, BlockNumReader, BlockReader, HeaderProvider};
|
||||
use reth_storage_api::{
|
||||
noop::NoopProvider, BalProvider, BlockNumReader, BlockReader, HeaderProvider,
|
||||
};
|
||||
use reth_tasks::Runtime;
|
||||
use secp256k1::SECP256K1;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
@@ -157,7 +159,8 @@ where
|
||||
impl<C, N> NetworkConfig<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BlockReader<Block = N::Block, Receipt = N::Receipt, Header = N::BlockHeader>
|
||||
C: BalProvider
|
||||
+ BlockReader<Block = N::Block, Receipt = N::Receipt, Header = N::BlockHeader>
|
||||
+ HeaderProvider
|
||||
+ Clone
|
||||
+ Unpin
|
||||
|
||||
@@ -18,7 +18,7 @@ use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::error::RequestResult;
|
||||
use reth_network_peers::PeerId;
|
||||
use reth_primitives_traits::Block;
|
||||
use reth_storage_api::{BlockReader, HeaderProvider};
|
||||
use reth_storage_api::{BalProvider, BlockReader, GetBlockAccessListLimit, HeaderProvider};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
@@ -282,27 +282,6 @@ where
|
||||
let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
|
||||
}
|
||||
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// EIP-8159 defines the final `BlockAccessLists` response semantics:
|
||||
/// <https://eips.ethereum.org/EIPS/eip-8159>
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
// TODO: BAL serving is not fully implemented yet. Per EIP-8159, unavailable BALs are
|
||||
// returned as empty BAL entries while preserving request order, so we currently return
|
||||
// one RLP-encoded empty BAL (`0xc0`) per requested hash.
|
||||
let access_lists = request
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|_| Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]))
|
||||
.collect();
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_receipts_response<T, F>(&self, request: GetReceipts, transform_fn: F) -> Vec<Vec<T>>
|
||||
where
|
||||
@@ -332,13 +311,55 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, N> EthRequestHandler<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BalProvider,
|
||||
{
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// EIP-8159 defines the final `BlockAccessLists` response semantics:
|
||||
/// <https://eips.ethereum.org/EIPS/eip-8159>
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(SOFT_RESPONSE_LIMIT);
|
||||
let access_lists = self
|
||||
.client
|
||||
.bal_store()
|
||||
.get_by_hashes_with_limit(&request.0, limit)
|
||||
.unwrap_or_else(|_| empty_block_access_lists_with_limit(request.0.len(), limit));
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the error fallback response while still enforcing the BAL response soft limit.
|
||||
fn empty_block_access_lists_with_limit(count: usize, limit: GetBlockAccessListLimit) -> Vec<Bytes> {
|
||||
let mut out = Vec::with_capacity(count);
|
||||
let mut size = 0;
|
||||
for _ in 0..count {
|
||||
let bal = Bytes::from_static(&[0xc0]);
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An endless future.
|
||||
///
|
||||
/// This should be spawned or used as part of `tokio::select!`.
|
||||
impl<C, N> Future for EthRequestHandler<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BlockReader<Block = N::Block, Receipt = N::Receipt>
|
||||
C: BalProvider
|
||||
+ BlockReader<Block = N::Block, Receipt = N::Receipt>
|
||||
+ HeaderProvider<Header = N::BlockHeader>
|
||||
+ Unpin,
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::{future, future::Either};
|
||||
use reth_eth_wire::{BlockAccessLists, EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::{
|
||||
block_access_lists::client::BlockAccessListsClient,
|
||||
block_access_lists::client::{BalRequirement, BlockAccessListsClient},
|
||||
bodies::client::{BodiesClient, BodiesFut},
|
||||
download::DownloadClient,
|
||||
error::{PeerRequestResult, RequestError},
|
||||
@@ -135,11 +135,29 @@ impl<N: NetworkPrimitives> BlockAccessListsClient for FetchClient<N> {
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
self.get_block_access_lists_with_priority_and_requirement(
|
||||
hashes,
|
||||
priority,
|
||||
BalRequirement::Mandatory,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_block_access_lists_with_priority_and_requirement(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
priority: Priority,
|
||||
requirement: BalRequirement,
|
||||
) -> Self::Output {
|
||||
let (response, rx) = oneshot::channel();
|
||||
if self
|
||||
.request_tx
|
||||
.send(DownloadRequest::GetBlockAccessLists { request: hashes, response, priority })
|
||||
.send(DownloadRequest::GetBlockAccessLists {
|
||||
request: hashes,
|
||||
response,
|
||||
priority,
|
||||
requirement,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
Box::pin(FlattenedResponse::from(rx))
|
||||
|
||||
@@ -13,6 +13,7 @@ use reth_eth_wire::{
|
||||
};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::{
|
||||
block_access_lists::client::BalRequirement,
|
||||
error::{EthResponseValidator, PeerRequestResult, RequestError, RequestResult},
|
||||
headers::client::HeadersRequest,
|
||||
priority::Priority,
|
||||
@@ -159,15 +160,10 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
/// full history available
|
||||
fn next_best_peer(&self, requirement: BestPeerRequirements) -> Option<PeerId> {
|
||||
// filter out peers that aren't idle or don't meet the requirement
|
||||
let mut idle = self.peers.iter().filter(|(_, peer)| {
|
||||
peer.state.is_idle() &&
|
||||
match &requirement {
|
||||
BestPeerRequirements::EthVersion(ver) => {
|
||||
peer.capabilities.supports_eth_at_least(ver)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
let mut idle = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(_, peer)| peer.state.is_idle() && peer.satisfies(&requirement));
|
||||
|
||||
let mut best_peer = idle.next()?;
|
||||
|
||||
@@ -195,6 +191,14 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
Some(*best_peer.0)
|
||||
}
|
||||
|
||||
/// Returns whether any connected peer can serve BAL requests.
|
||||
fn has_eth71_peer(&self) -> bool {
|
||||
self.peers.values().any(|peer| {
|
||||
!matches!(peer.state, PeerState::Closing) &&
|
||||
peer.capabilities.supports_eth_at_least(&EthVersion::Eth71)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the next action to return
|
||||
fn poll_action(&mut self) -> PollAction {
|
||||
// we only check and not pop here since we don't know yet whether a peer is available.
|
||||
@@ -208,9 +212,15 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
|
||||
let request = self.queued_requests.pop_front().expect("not empty");
|
||||
let Some(peer_id) = self.next_best_peer(request.best_peer_requirements()) else {
|
||||
// no peer matches this request's requirements; requeue at the back so other
|
||||
// queued requests get a chance on the next poll instead of head-of-line blocking.
|
||||
self.queued_requests.push_back(request);
|
||||
// Optional BAL requests can lose their eth/71 peer while queued; complete them
|
||||
// instead of waiting for future peer churn.
|
||||
if request.is_optional_bal() && !self.has_eth71_peer() {
|
||||
request.send_err_response(RequestError::UnsupportedCapability);
|
||||
} else {
|
||||
// no peer matches this request's requirements; requeue at the back so other
|
||||
// queued requests get a chance on the next poll instead of head-of-line blocking.
|
||||
self.queued_requests.push_back(request);
|
||||
}
|
||||
return PollAction::NoPeersAvailable
|
||||
};
|
||||
|
||||
@@ -232,21 +242,30 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
loop {
|
||||
// poll incoming requests
|
||||
match self.download_requests_rx.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(request)) => match request.get_priority() {
|
||||
Priority::High => {
|
||||
// find the first normal request and queue before, add this request to
|
||||
// the back of the high-priority queue
|
||||
let pos = self
|
||||
.queued_requests
|
||||
.iter()
|
||||
.position(|req| req.is_normal_priority())
|
||||
.unwrap_or(0);
|
||||
self.queued_requests.insert(pos, request);
|
||||
Poll::Ready(Some(request)) => {
|
||||
// Optional BAL requests should not wait for future peer churn if no
|
||||
// connected peer can serve them right now.
|
||||
if request.is_optional_bal() && !self.has_eth71_peer() {
|
||||
request.send_err_response(RequestError::UnsupportedCapability);
|
||||
continue
|
||||
}
|
||||
Priority::Normal => {
|
||||
self.queued_requests.push_back(request);
|
||||
|
||||
match request.get_priority() {
|
||||
Priority::High => {
|
||||
// find first normal request and queue before it; add this request
|
||||
// to the back of the high-priority queue
|
||||
let pos = self
|
||||
.queued_requests
|
||||
.iter()
|
||||
.position(|req| req.is_normal_priority())
|
||||
.unwrap_or(0);
|
||||
self.queued_requests.insert(pos, request);
|
||||
}
|
||||
Priority::Normal => {
|
||||
self.queued_requests.push_back(request);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
unreachable!("channel can't close")
|
||||
}
|
||||
@@ -269,6 +288,15 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
peer.state = req.peer_state();
|
||||
}
|
||||
|
||||
self.prepare_inflight_block_request(peer_id, req)
|
||||
}
|
||||
|
||||
/// Tracks an inflight request and converts it into a peer request.
|
||||
fn prepare_inflight_block_request(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
req: DownloadRequest<N>,
|
||||
) -> BlockRequest {
|
||||
match req {
|
||||
DownloadRequest::GetBlockHeaders { request, response, .. } => {
|
||||
let inflight = Request { request: request.clone(), response };
|
||||
@@ -299,12 +327,23 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new followup request for the peer.
|
||||
/// Returns a queued followup request the peer can serve.
|
||||
///
|
||||
/// This is an immediate scheduling shortcut after a successful response. It skips queued
|
||||
/// requests whose hard requirements do not match this peer, leaving them for the regular peer
|
||||
/// selection path.
|
||||
///
|
||||
/// Caution: this expects that the peer is _not_ closed.
|
||||
fn followup_request(&mut self, peer_id: PeerId) -> Option<BlockResponseOutcome> {
|
||||
let req = self.queued_requests.pop_front()?;
|
||||
let req = self.prepare_block_request(peer_id, req);
|
||||
let peer = self.peers.get_mut(&peer_id)?;
|
||||
let req_idx = self.queued_requests.iter().position(|req| {
|
||||
// Find the first queued request this peer can serve.
|
||||
peer.satisfies(&req.best_peer_requirements())
|
||||
})?;
|
||||
let req = self.queued_requests.remove(req_idx).expect("valid request index");
|
||||
|
||||
peer.state = req.peer_state();
|
||||
let req = self.prepare_inflight_block_request(peer_id, req);
|
||||
Some(BlockResponseOutcome::Request(peer_id, req))
|
||||
}
|
||||
|
||||
@@ -476,6 +515,16 @@ impl Peer {
|
||||
self.range_info.as_ref().map(|info| info.range())
|
||||
}
|
||||
|
||||
/// Returns whether this peer can serve requests with the given hard requirements.
|
||||
fn satisfies(&self, requirement: &BestPeerRequirements) -> bool {
|
||||
match requirement {
|
||||
BestPeerRequirements::EthVersion(ver) => self.capabilities.supports_eth_at_least(ver),
|
||||
BestPeerRequirements::None |
|
||||
BestPeerRequirements::FullBlock |
|
||||
BestPeerRequirements::FullBlockRange(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this peer has a better range than the other peer for serving the requested
|
||||
/// range.
|
||||
///
|
||||
@@ -602,6 +651,7 @@ pub(crate) enum DownloadRequest<N: NetworkPrimitives> {
|
||||
request: Vec<B256>,
|
||||
response: oneshot::Sender<PeerRequestResult<BlockAccessLists>>,
|
||||
priority: Priority,
|
||||
requirement: BalRequirement,
|
||||
},
|
||||
/// Download receipts for the given block hashes and send response through channel
|
||||
GetReceipts {
|
||||
@@ -639,6 +689,21 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
|
||||
self.get_priority().is_normal()
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an optional BAL request.
|
||||
const fn is_optional_bal(&self) -> bool {
|
||||
matches!(self, Self::GetBlockAccessLists { requirement: BalRequirement::Optional, .. })
|
||||
}
|
||||
|
||||
/// Sends an error response to the waiting caller.
|
||||
fn send_err_response(self, err: RequestError) {
|
||||
let _ = match self {
|
||||
Self::GetBlockHeaders { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockBodies { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the best peer requirements for this request.
|
||||
fn best_peer_requirements(&self) -> BestPeerRequirements {
|
||||
match self {
|
||||
@@ -1404,6 +1469,98 @@ mod tests {
|
||||
assert!(matches!(outcome, Some(BlockResponseOutcome::Request(pid, _)) if pid == peer_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_followup_skips_request_peer_cannot_serve() {
|
||||
let (mut fetcher, peer_id) = fetcher_with_peer();
|
||||
|
||||
let peer_71 = B512::random();
|
||||
let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
|
||||
fetcher.new_active_peer(
|
||||
peer_71,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_71,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
|
||||
|
||||
let (followup_tx, _followup_rx) = oneshot::channel();
|
||||
fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
|
||||
request: vec![B256::random()],
|
||||
response: followup_tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Optional,
|
||||
});
|
||||
|
||||
let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
|
||||
|
||||
let resp = ReceiptsResponse::new(vec![vec![]]);
|
||||
assert!(fetcher.on_receipts_response(peer_id, Ok(resp)).is_none());
|
||||
assert!(fetcher.peers[&peer_id].state.is_idle());
|
||||
assert!(!fetcher.inflight_bals_requests.contains_key(&peer_id));
|
||||
assert!(matches!(
|
||||
fetcher.queued_requests.front(),
|
||||
Some(DownloadRequest::GetBlockAccessLists {
|
||||
requirement: BalRequirement::Optional,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_followup_uses_first_satisfiable_request() {
|
||||
let (mut fetcher, peer_id) = fetcher_with_peer();
|
||||
|
||||
let peer_71 = B512::random();
|
||||
let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
|
||||
fetcher.new_active_peer(
|
||||
peer_71,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_71,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
|
||||
|
||||
let (bal_tx, _bal_rx) = oneshot::channel();
|
||||
fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
|
||||
request: vec![B256::random()],
|
||||
response: bal_tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Optional,
|
||||
});
|
||||
|
||||
let (bodies_tx, _bodies_rx) = oneshot::channel();
|
||||
fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
|
||||
request: vec![B256::random()],
|
||||
response: bodies_tx,
|
||||
priority: Priority::Normal,
|
||||
range_hint: None,
|
||||
});
|
||||
|
||||
let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
|
||||
|
||||
let resp = ReceiptsResponse::new(vec![vec![]]);
|
||||
let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
|
||||
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
Some(BlockResponseOutcome::Request(pid, BlockRequest::GetBlockBodies(_))) if pid == peer_id
|
||||
));
|
||||
assert!(fetcher.inflight_bodies_requests.contains_key(&peer_id));
|
||||
assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetBlockBodies));
|
||||
assert_eq!(fetcher.queued_requests.len(), 1);
|
||||
assert!(matches!(
|
||||
fetcher.queued_requests.front(),
|
||||
Some(DownloadRequest::GetBlockAccessLists {
|
||||
requirement: BalRequirement::Optional,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_prepare_block_request_creates_inflight_receipts() {
|
||||
let (mut fetcher, peer_id) = fetcher_with_peer();
|
||||
@@ -1541,6 +1698,7 @@ mod tests {
|
||||
request: vec![],
|
||||
response: tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Mandatory,
|
||||
});
|
||||
|
||||
let waker = noop_waker();
|
||||
@@ -1583,4 +1741,138 @@ mod tests {
|
||||
assert_eq!(peer_id, peer_71);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_optional_bal_request_rejected_without_eth71_peer() {
|
||||
use futures::task::noop_waker;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
let manager = PeersManager::new(PeersConfig::default());
|
||||
let mut fetcher =
|
||||
StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
|
||||
|
||||
let peer_old = B512::random();
|
||||
let caps_old = Arc::new(Capabilities::new(vec![]));
|
||||
fetcher.new_active_peer(
|
||||
peer_old,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_old,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
fetcher
|
||||
.download_requests_tx
|
||||
.send(DownloadRequest::GetBlockAccessLists {
|
||||
request: vec![],
|
||||
response: tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Optional,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let waker = noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
|
||||
assert!(fetcher.queued_requests.is_empty());
|
||||
assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_optional_bal_request_waits_for_busy_eth71_peer() {
|
||||
use futures::task::noop_waker;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
let manager = PeersManager::new(PeersConfig::default());
|
||||
let mut fetcher =
|
||||
StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
|
||||
|
||||
let peer_71 = B512::random();
|
||||
let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
|
||||
fetcher.new_active_peer(
|
||||
peer_71,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_71,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
|
||||
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
fetcher
|
||||
.download_requests_tx
|
||||
.send(DownloadRequest::GetBlockAccessLists {
|
||||
request: vec![],
|
||||
response: tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Optional,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let waker = noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
|
||||
assert_eq!(fetcher.queued_requests.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_queued_optional_bal_request_rejected_after_eth71_disconnect() {
|
||||
use futures::task::noop_waker;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
let manager = PeersManager::new(PeersConfig::default());
|
||||
let mut fetcher =
|
||||
StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
|
||||
|
||||
let peer_old = B512::random();
|
||||
let caps_old = Arc::new(Capabilities::new(vec![]));
|
||||
fetcher.new_active_peer(
|
||||
peer_old,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_old,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
|
||||
let peer_71 = B512::random();
|
||||
let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
|
||||
fetcher.new_active_peer(
|
||||
peer_71,
|
||||
B256::random(),
|
||||
100,
|
||||
caps_71,
|
||||
Arc::new(AtomicU64::new(10)),
|
||||
None,
|
||||
);
|
||||
fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
fetcher
|
||||
.download_requests_tx
|
||||
.send(DownloadRequest::GetBlockAccessLists {
|
||||
request: vec![],
|
||||
response: tx,
|
||||
priority: Priority::Normal,
|
||||
requirement: BalRequirement::Optional,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let waker = noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
|
||||
assert_eq!(fetcher.queued_requests.len(), 1);
|
||||
|
||||
fetcher.on_session_closed(&peer_71);
|
||||
|
||||
assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
|
||||
assert!(fetcher.queued_requests.is_empty());
|
||||
assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
extra_protocols,
|
||||
handshake,
|
||||
eth_max_message_size,
|
||||
network_mode.is_stake(),
|
||||
);
|
||||
|
||||
let state = NetworkState::new(
|
||||
|
||||
@@ -577,6 +577,9 @@ pub struct AnnouncedTxTypesMetrics {
|
||||
|
||||
/// Histogram for tracking frequency of EIP-7702 transaction type
|
||||
pub(crate) eip7702: Histogram,
|
||||
|
||||
/// Histogram for tracking frequency of unknown/other transaction types
|
||||
pub(crate) other: Histogram,
|
||||
}
|
||||
|
||||
/// Counts the number of transactions by their type in a block or collection.
|
||||
@@ -599,6 +602,9 @@ pub struct TxTypesCounter {
|
||||
|
||||
/// Count of transactions conforming to EIP-7702 (Restricted Storage Windows).
|
||||
pub(crate) eip7702: usize,
|
||||
|
||||
/// Count of unknown/other transaction types not matching any known EIP.
|
||||
pub(crate) other: usize,
|
||||
}
|
||||
|
||||
impl TxTypesCounter {
|
||||
@@ -621,6 +627,10 @@ impl TxTypesCounter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn increase_other(&mut self) {
|
||||
self.other += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl AnnouncedTxTypesMetrics {
|
||||
@@ -632,5 +642,6 @@ impl AnnouncedTxTypesMetrics {
|
||||
self.eip1559.record(tx_types_counter.eip1559 as f64);
|
||||
self.eip4844.record(tx_types_counter.eip4844 as f64);
|
||||
self.eip7702.record(tx_types_counter.eip7702 as f64);
|
||||
self.other.record(tx_types_counter.other as f64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ const TIMEOUT_SCALING: u32 = 3;
|
||||
/// before reading any more messages from the remote peer, throttling the peer.
|
||||
const MAX_QUEUED_OUTGOING_RESPONSES: usize = 4;
|
||||
|
||||
/// Minimum capacity to retain for buffered incoming requests from the remote peer.
|
||||
const MIN_RECEIVED_REQUESTS_CAPACITY: usize = 1;
|
||||
|
||||
/// Soft limit for the total number of buffered outgoing broadcast items (e.g. transaction hashes).
|
||||
///
|
||||
/// Many small broadcast messages carrying a single tx hash each are equivalent in cost to one
|
||||
@@ -204,8 +207,8 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
|
||||
/// Shrinks the capacity of the internal buffers.
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.received_requests_from_remote.shrink_to_fit();
|
||||
self.queued_outgoing.shrink_to_fit();
|
||||
self.received_requests_from_remote.shrink_to(MIN_RECEIVED_REQUESTS_CAPACITY);
|
||||
self.queued_outgoing.shrink_to(MAX_QUEUED_OUTGOING_RESPONSES);
|
||||
}
|
||||
|
||||
/// Returns how many responses we've currently queued up.
|
||||
@@ -1090,8 +1093,8 @@ impl<N: NetworkPrimitives> QueuedOutgoingMessages<N> {
|
||||
self.count.increment(1);
|
||||
}
|
||||
|
||||
pub(crate) fn shrink_to_fit(&mut self) {
|
||||
self.messages.shrink_to_fit();
|
||||
pub(crate) fn shrink_to(&mut self, min_capacity: usize) {
|
||||
self.messages.shrink_to(min_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,15 @@ impl<N: NetworkPrimitives> EthRlpxConnection<N> {
|
||||
Self::Satellite(conn) => conn.primary_mut().start_send_raw(msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets whether to reject block announcement messages (`NewBlock`, `NewBlockHashes`) before
|
||||
/// RLP decoding to avoid memory amplification from deserializing blocks that will be discarded.
|
||||
pub fn set_reject_block_announcements(&mut self, reject: bool) {
|
||||
match self {
|
||||
Self::EthOnly(conn) => conn.set_reject_block_announcements(reject),
|
||||
Self::Satellite(conn) => conn.primary_mut().set_reject_block_announcements(reject),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: NetworkPrimitives> From<EthPeerConnection<N>> for EthRlpxConnection<N> {
|
||||
|
||||
@@ -123,6 +123,9 @@ pub struct SessionManager<N: NetworkPrimitives> {
|
||||
/// Shared local range information that gets propagated to active sessions.
|
||||
/// This represents the range of blocks that this node can serve to other peers.
|
||||
local_range_info: BlockRangeInfo,
|
||||
/// When true, block announcement messages (`NewBlock`, `NewBlockHashes`) are rejected before
|
||||
/// RLP decoding on new sessions to avoid memory amplification.
|
||||
reject_block_announcements: bool,
|
||||
}
|
||||
|
||||
// === impl SessionManager ===
|
||||
@@ -140,6 +143,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
|
||||
extra_protocols: RlpxSubProtocols,
|
||||
handshake: Arc<dyn EthRlpxHandshake>,
|
||||
eth_max_message_size: usize,
|
||||
reject_block_announcements: bool,
|
||||
) -> Self {
|
||||
let (pending_sessions_tx, pending_sessions_rx) = mpsc::channel(config.session_event_buffer);
|
||||
let (active_session_tx, active_session_rx) = mpsc::channel(config.session_event_buffer);
|
||||
@@ -176,6 +180,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
|
||||
handshake,
|
||||
eth_max_message_size,
|
||||
local_range_info,
|
||||
reject_block_announcements,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +501,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
|
||||
local_addr,
|
||||
peer_id,
|
||||
capabilities,
|
||||
conn,
|
||||
mut conn,
|
||||
status,
|
||||
direction,
|
||||
client_id,
|
||||
@@ -563,6 +568,10 @@ impl<N: NetworkPrimitives> SessionManager<N> {
|
||||
BlockRangeInfo::new(update.earliest, update.latest, update.latest_hash)
|
||||
});
|
||||
|
||||
if self.reject_block_announcements {
|
||||
conn.set_reject_block_announcements(true);
|
||||
}
|
||||
|
||||
let session = ActiveSession {
|
||||
next_id: 0,
|
||||
remote_peer_id: peer_id,
|
||||
|
||||
@@ -27,7 +27,8 @@ use reth_network_api::{
|
||||
};
|
||||
use reth_network_peers::PeerId;
|
||||
use reth_storage_api::{
|
||||
noop::NoopProvider, BlockReader, BlockReaderIdExt, HeaderProvider, StateProviderFactory,
|
||||
noop::NoopProvider, BalProvider, BlockReader, BlockReaderIdExt, HeaderProvider,
|
||||
StateProviderFactory,
|
||||
};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_tokio_util::EventStream;
|
||||
@@ -247,6 +248,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Clone
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
@@ -319,6 +321,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
Pool: TransactionPool<
|
||||
@@ -462,7 +465,10 @@ where
|
||||
}
|
||||
|
||||
/// Set a new request handler that's connected to the peer's network
|
||||
pub fn install_request_handler(&mut self) {
|
||||
pub fn install_request_handler(&mut self)
|
||||
where
|
||||
C: BalProvider,
|
||||
{
|
||||
let (tx, rx) = channel(ETH_REQUEST_CHANNEL_CAPACITY);
|
||||
self.network.set_eth_request_handler(tx);
|
||||
let peers = self.network.peers_handle();
|
||||
@@ -573,6 +579,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
Pool: TransactionPool<
|
||||
|
||||
@@ -700,11 +700,11 @@ impl<Pool: TransactionPool, N: NetworkPrimitives> TransactionsManager<Pool, N> {
|
||||
}
|
||||
};
|
||||
|
||||
if is_eth68_message &&
|
||||
let Some((actual_ty_byte, _)) = *metadata_ref_mut &&
|
||||
let Ok(parsed_tx_type) = TxType::try_from(actual_ty_byte)
|
||||
{
|
||||
tx_types_counter.increase_by_tx_type(parsed_tx_type);
|
||||
if is_eth68_message && let Some((actual_ty_byte, _)) = *metadata_ref_mut {
|
||||
match TxType::try_from(actual_ty_byte) {
|
||||
Ok(parsed_tx_type) => tx_types_counter.increase_by_tx_type(parsed_tx_type),
|
||||
Err(_) => tx_types_counter.increase_other(),
|
||||
}
|
||||
}
|
||||
|
||||
let decision = self
|
||||
|
||||
@@ -2,23 +2,29 @@
|
||||
//! Tests for eth related requests
|
||||
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use rand::Rng;
|
||||
use reth_eth_wire::{EthVersion, HeadersDirection};
|
||||
use reth_eth_wire::{BlockAccessLists, EthVersion, GetBlockAccessLists, HeadersDirection};
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_network::{
|
||||
test_utils::{NetworkEventStream, PeerConfig, Testnet},
|
||||
eth_requests::SOFT_RESPONSE_LIMIT,
|
||||
test_utils::{NetworkEventStream, PeerConfig, Testnet, TestnetHandle},
|
||||
BlockDownloaderProvider, NetworkEventListenerProvider,
|
||||
};
|
||||
use reth_network_api::{NetworkInfo, Peers};
|
||||
use reth_network_p2p::{
|
||||
bodies::client::BodiesClient,
|
||||
error::RequestError,
|
||||
headers::client::{HeadersClient, HeadersRequest},
|
||||
BalRequirement, BlockAccessListsClient,
|
||||
};
|
||||
use reth_provider::test_utils::MockEthProvider;
|
||||
use reth_provider::{test_utils::MockEthProvider, BalStoreHandle, InMemoryBalStore};
|
||||
use reth_transaction_pool::test_utils::{TestPool, TransactionGenerator};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
type BalTestnetHandle = TestnetHandle<Arc<MockEthProvider>, TestPool>;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_body() {
|
||||
reth_tracing::init_test_tracing();
|
||||
@@ -526,3 +532,178 @@ async fn test_eth69_get_receipts() {
|
||||
assert_eq!(receipts_response.0[0][1].cumulative_gas_used, 42000);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
let bal2 = Bytes::from_static(&[0xc1, 0x02]);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash2, 3, bal2.clone()).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1, hash2]).await;
|
||||
assert_eq!(
|
||||
response,
|
||||
BlockAccessLists(vec![bal0, Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]), bal2,])
|
||||
);
|
||||
}
|
||||
|
||||
// Ensures BAL responses stop at the soft response limit while keeping the item that crosses it.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_respects_response_soft_limit() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = raw_bal_with_len(2);
|
||||
let bal1 = raw_bal_with_len(SOFT_RESPONSE_LIMIT);
|
||||
let bal2 = raw_bal_with_len(2);
|
||||
assert!(bal0.len() + bal1.len() > SOFT_RESPONSE_LIMIT);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash1, 2, bal1.clone()).unwrap();
|
||||
bal_store.insert(hash2, 3, bal2).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1, hash2]).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(vec![bal0, bal1]));
|
||||
}
|
||||
|
||||
// Ensures a single BAL larger than the soft limit is still returned.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_returns_single_oversized_bal() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let bal0 = raw_bal_with_len(SOFT_RESPONSE_LIMIT + 1);
|
||||
let bal1 = raw_bal_with_len(2);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash1, 2, bal1).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1]).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(vec![bal0]));
|
||||
}
|
||||
|
||||
// Ensures an empty BAL request roundtrips to an empty response.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_empty_request() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, _) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let response = request_block_access_lists(&net, Vec::new()).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(Vec::new()));
|
||||
}
|
||||
|
||||
// Ensures the fetch client can request BALs through an eth/71 peer.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_fetch_client_get_block_access_lists() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
|
||||
let fetch = net.peers()[0].network().fetch_client().await.unwrap();
|
||||
let response = fetch.get_block_access_lists(vec![hash0, hash1]).await.unwrap().into_data();
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
BlockAccessLists(vec![bal0, Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])])
|
||||
);
|
||||
}
|
||||
|
||||
// Ensures fetch client BAL requests are rejected when no eth/71 peer is available.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth70_fetch_client_rejects_optional_block_access_lists_request() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, _) = spawn_bal_testnet([EthVersion::Eth70, EthVersion::Eth70]).await;
|
||||
|
||||
let fetch = net.peers()[0].network().fetch_client().await.unwrap();
|
||||
let err = fetch
|
||||
.get_block_access_lists_with_requirement(vec![B256::random()], BalRequirement::Optional)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err, RequestError::UnsupportedCapability);
|
||||
}
|
||||
|
||||
async fn spawn_eth71_bal_testnet() -> (BalTestnetHandle, BalStoreHandle) {
|
||||
spawn_bal_testnet([EthVersion::Eth71, EthVersion::Eth71]).await
|
||||
}
|
||||
|
||||
// Spawns a BAL testnet with one peer per requested eth protocol version.
|
||||
async fn spawn_bal_testnet(
|
||||
versions: impl IntoIterator<Item = EthVersion>,
|
||||
) -> (BalTestnetHandle, BalStoreHandle) {
|
||||
let mut mock_provider = MockEthProvider::default();
|
||||
let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
|
||||
mock_provider.bal_store = bal_store.clone();
|
||||
let mock_provider = Arc::new(mock_provider);
|
||||
|
||||
let mut net: Testnet<Arc<MockEthProvider>, TestPool> = Testnet::default();
|
||||
|
||||
for version in versions {
|
||||
let peer = PeerConfig::with_protocols(mock_provider.clone(), Some(version.into()));
|
||||
net.add_peer_with_config(peer).await.unwrap();
|
||||
}
|
||||
|
||||
net.for_each_mut(|peer| peer.install_request_handler());
|
||||
|
||||
let net = net.spawn();
|
||||
net.connect_peers().await;
|
||||
|
||||
(net, bal_store)
|
||||
}
|
||||
|
||||
// Sends a GetBlockAccessLists request from peer 0 to peer 1.
|
||||
async fn request_block_access_lists(net: &BalTestnetHandle, hashes: Vec<B256>) -> BlockAccessLists {
|
||||
let requester = &net.peers()[0];
|
||||
let responder = &net.peers()[1];
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
requester.network().send_request(
|
||||
*responder.peer_id(),
|
||||
reth_network::PeerRequest::GetBlockAccessLists {
|
||||
request: GetBlockAccessLists(hashes),
|
||||
response: tx,
|
||||
},
|
||||
);
|
||||
|
||||
rx.await.unwrap().unwrap()
|
||||
}
|
||||
|
||||
// Builds a complete raw RLP list item with the requested encoded byte length.
|
||||
fn raw_bal_with_len(len: usize) -> Bytes {
|
||||
assert!(len > 0);
|
||||
|
||||
let mut payload_length = len - 1;
|
||||
loop {
|
||||
let header_length = alloy_rlp::Header { list: true, payload_length }.length();
|
||||
let next_payload_length = len.checked_sub(header_length).unwrap();
|
||||
if next_payload_length == payload_length {
|
||||
break
|
||||
}
|
||||
payload_length = next_payload_length;
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(len);
|
||||
alloy_rlp::Header { list: true, payload_length }.encode(&mut out);
|
||||
out.resize(len, alloy_rlp::EMPTY_LIST_CODE);
|
||||
Bytes::from(out)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,17 @@ use auto_impl::auto_impl;
|
||||
use futures::Future;
|
||||
use reth_eth_wire_types::BlockAccessLists;
|
||||
|
||||
/// Controls whether a BAL request must wait for a capable peer or may complete early when none are
|
||||
/// available.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum BalRequirement {
|
||||
/// Keep waiting until an eth/71-capable peer is available.
|
||||
#[default]
|
||||
Mandatory,
|
||||
/// Return early if no connected peer can serve BALs.
|
||||
Optional,
|
||||
}
|
||||
|
||||
/// A client capable of downloading block access lists.
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait BlockAccessListsClient: DownloadClient {
|
||||
@@ -12,7 +23,24 @@ pub trait BlockAccessListsClient: DownloadClient {
|
||||
|
||||
/// Fetches the block access lists for given hashes.
|
||||
fn get_block_access_lists(&self, hashes: Vec<B256>) -> Self::Output {
|
||||
self.get_block_access_lists_with_priority(hashes, Priority::Normal)
|
||||
self.get_block_access_lists_with_priority_and_requirement(
|
||||
hashes,
|
||||
Priority::Normal,
|
||||
BalRequirement::Mandatory,
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetches the block access lists for given hashes with the requested BAL availability policy.
|
||||
fn get_block_access_lists_with_requirement(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
requirement: BalRequirement,
|
||||
) -> Self::Output {
|
||||
self.get_block_access_lists_with_priority_and_requirement(
|
||||
hashes,
|
||||
Priority::Normal,
|
||||
requirement,
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetches the block access lists for given hashes with priority
|
||||
@@ -20,5 +48,19 @@ pub trait BlockAccessListsClient: DownloadClient {
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
self.get_block_access_lists_with_priority_and_requirement(
|
||||
hashes,
|
||||
priority,
|
||||
BalRequirement::Mandatory,
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetches the block access lists for given hashes with priority and BAL availability policy.
|
||||
fn get_block_access_lists_with_priority_and_requirement(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
priority: Priority,
|
||||
requirement: BalRequirement,
|
||||
) -> Self::Output;
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ impl<H: BlockHeader> EthResponseValidator for RequestResult<Vec<H>> {
|
||||
/// [`RequestError::ConnectionDropped`] should be ignored here because this is already handled
|
||||
/// when the dropped connection is handled.
|
||||
///
|
||||
/// [`RequestError::UnsupportedCapability`] is not used yet because we only support active
|
||||
/// session for eth protocol.
|
||||
/// [`RequestError::UnsupportedCapability`] is also used for locally rejected optional requests,
|
||||
/// which should not affect peer reputation.
|
||||
fn reputation_change_err(&self) -> Option<ReputationChangeKind> {
|
||||
if let Err(err) = self {
|
||||
match err {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::headers::client::HeadersRequest;
|
||||
use crate::{
|
||||
block_access_lists::client::BlockAccessListsClient,
|
||||
bodies::client::{BodiesClient, SingleBodyRequest},
|
||||
download::DownloadClient,
|
||||
error::PeerRequestResult,
|
||||
@@ -10,15 +11,17 @@ use crate::{
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_primitives::{Sealable, B256};
|
||||
use core::marker::PhantomData;
|
||||
use futures::FutureExt;
|
||||
use reth_consensus::Consensus;
|
||||
use reth_eth_wire_types::{EthNetworkPrimitives, HeadersDirection, NetworkPrimitives};
|
||||
use reth_eth_wire_types::{
|
||||
BlockAccessLists, EthNetworkPrimitives, HeadersDirection, NetworkPrimitives,
|
||||
};
|
||||
use reth_network_peers::{PeerId, WithPeerId};
|
||||
use reth_primitives_traits::{SealedBlock, SealedHeader};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
hash::Hash,
|
||||
ops::RangeInclusive,
|
||||
pin::Pin,
|
||||
@@ -64,18 +67,7 @@ where
|
||||
/// Caution: This does no validation of body (transactions) response but guarantees that the
|
||||
/// [`SealedHeader`] matches the requested hash.
|
||||
pub fn get_full_block(&self, hash: B256) -> FetchFullBlockFuture<Client> {
|
||||
let client = self.client.clone();
|
||||
FetchFullBlockFuture {
|
||||
hash,
|
||||
consensus: self.consensus.clone(),
|
||||
request: FullBlockRequest {
|
||||
header: Some(client.get_header(hash.into())),
|
||||
body: Some(client.get_block_body(hash)),
|
||||
},
|
||||
client,
|
||||
header: None,
|
||||
body: None,
|
||||
}
|
||||
FetchFullBlockFuture::new(self.client.clone(), self.consensus.clone(), hash)
|
||||
}
|
||||
|
||||
/// Returns a future that fetches [`SealedBlock`]s for the given hash and count.
|
||||
@@ -109,6 +101,49 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> FetchFullBlockFuture<Client>
|
||||
where
|
||||
Client: BlockClient,
|
||||
{
|
||||
fn new(client: Client, consensus: Arc<dyn Consensus<Client::Block>>, hash: B256) -> Self {
|
||||
Self {
|
||||
hash,
|
||||
consensus,
|
||||
request: FullBlockRequest {
|
||||
header: Some(client.get_header(hash.into())),
|
||||
body: Some(client.get_block_body(hash)),
|
||||
},
|
||||
client,
|
||||
header: None,
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> FullBlockClient<Client>
|
||||
where
|
||||
Client: BlockClient + BlockAccessListsClient,
|
||||
{
|
||||
/// Returns a future that fetches the [`SealedBlock`] and its [`BlockAccessLists`] for the
|
||||
/// given hash.
|
||||
///
|
||||
/// Note: this future is cancel safe
|
||||
///
|
||||
/// Caution: This does no validation of body (transactions) response but guarantees that the
|
||||
/// [`SealedHeader`] matches the requested hash.
|
||||
pub fn get_full_block_with_access_lists(
|
||||
&self,
|
||||
hash: B256,
|
||||
) -> FetchFullBlockWithAccessListsFuture<Client> {
|
||||
let client = self.client.clone();
|
||||
FetchFullBlockWithAccessListsFuture {
|
||||
block: FetchFullBlockFuture::new(client.clone(), self.consensus.clone(), hash),
|
||||
block_result: None,
|
||||
bal_request_state: BalRequestState::Pending(client.get_block_access_lists(vec![hash])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that downloads a full block from the network.
|
||||
///
|
||||
/// This will attempt to fetch both the header and body for the given block hash at the same time.
|
||||
@@ -128,7 +163,7 @@ where
|
||||
|
||||
impl<Client> FetchFullBlockFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: BlockHeader>,
|
||||
Client: BlockClient,
|
||||
{
|
||||
/// Returns the hash of the block being requested.
|
||||
pub const fn hash(&self) -> &B256 {
|
||||
@@ -251,6 +286,155 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that downloads a full block and its block access lists from the network.
|
||||
///
|
||||
/// This composes the existing full block downloader with a block access list request so the
|
||||
/// header/body logic stays centralized.
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct FetchFullBlockWithAccessListsFuture<Client>
|
||||
where
|
||||
Client: BlockClient + BlockAccessListsClient,
|
||||
{
|
||||
block: FetchFullBlockFuture<Client>,
|
||||
block_result: Option<SealedBlock<Client::Block>>,
|
||||
bal_request_state: BalRequestState<<Client as BlockAccessListsClient>::Output>,
|
||||
}
|
||||
|
||||
impl<Client> FetchFullBlockWithAccessListsFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: BlockHeader> + BlockAccessListsClient,
|
||||
{
|
||||
/// Returns the hash of the block being requested.
|
||||
pub const fn hash(&self) -> &B256 {
|
||||
self.block.hash()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> FetchFullBlockWithAccessListsFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: BlockHeader + Sealable> + BlockAccessListsClient + 'static,
|
||||
{
|
||||
/// If the header request is already complete, this returns the block number.
|
||||
pub fn block_number(&self) -> Option<u64> {
|
||||
self.block_result.as_ref().map(|block| block.number()).or_else(|| self.block.block_number())
|
||||
}
|
||||
|
||||
fn send_bal_request(&mut self) {
|
||||
let hash = *self.block.hash();
|
||||
self.bal_request_state =
|
||||
BalRequestState::Pending(self.block.client.get_block_access_lists(vec![hash]));
|
||||
}
|
||||
|
||||
// This retries BAL failures inline instead of surfacing them to the outer future, so
|
||||
// `FetchFullBlockWithAccessListsFuture` only makes progress once the BAL request either
|
||||
// becomes pending again or resolves with a single access-list entry.
|
||||
fn poll_bal_request(&mut self, cx: &mut Context<'_>) {
|
||||
loop {
|
||||
let poll = match &mut self.bal_request_state {
|
||||
BalRequestState::Pending(fut) => fut.poll_unpin(cx),
|
||||
BalRequestState::Ready(_) => return,
|
||||
};
|
||||
|
||||
match poll {
|
||||
Poll::Pending => return,
|
||||
Poll::Ready(res) => match res {
|
||||
Ok(bal) => {
|
||||
let (peer, access_lists) = bal.split();
|
||||
if access_lists.0.len() == 1 {
|
||||
self.bal_request_state = BalRequestState::Ready(access_lists);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(
|
||||
target: "downloaders",
|
||||
hash = ?self.block.hash(),
|
||||
expected = 1,
|
||||
received = access_lists.0.len(),
|
||||
"Received wrong access list response",
|
||||
);
|
||||
self.block.client.report_bad_message(peer);
|
||||
self.send_bal_request();
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
target: "downloaders",
|
||||
%err,
|
||||
hash = ?self.block.hash(),
|
||||
"Access list download failed",
|
||||
);
|
||||
self.send_bal_request();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_block_and_access_lists(
|
||||
&mut self,
|
||||
) -> Option<(SealedBlock<Client::Block>, BlockAccessLists)> {
|
||||
if self.block_result.is_some() && self.bal_request_state.is_ready() {
|
||||
let block = self.block_result.take().expect("block result should exist");
|
||||
let access_lists = match &mut self.bal_request_state {
|
||||
BalRequestState::Ready(access_lists) => std::mem::take(access_lists),
|
||||
BalRequestState::Pending(_) => unreachable!("access lists should be ready"),
|
||||
};
|
||||
return Some((block, access_lists))
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> Future for FetchFullBlockWithAccessListsFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: BlockHeader + Sealable> + BlockAccessListsClient + 'static,
|
||||
{
|
||||
type Output = (SealedBlock<Client::Block>, BlockAccessLists);
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
if this.block_result.is_none() &&
|
||||
let Poll::Ready(block) = this.block.poll_unpin(cx)
|
||||
{
|
||||
this.block_result = Some(block);
|
||||
}
|
||||
|
||||
this.poll_bal_request(cx);
|
||||
|
||||
if let Some(res) = this.take_block_and_access_lists() {
|
||||
return Poll::Ready(res)
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> Debug for FetchFullBlockWithAccessListsFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: BlockHeader> + BlockAccessListsClient,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FetchFullBlockWithAccessListsFuture")
|
||||
.field("hash", &self.block.hash())
|
||||
.field("block_ready", &self.block_result.is_some())
|
||||
.field("bal_request_ready", &self.bal_request_state.is_ready())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the BAL request and its completed result.
|
||||
enum BalRequestState<Req> {
|
||||
Pending(Req),
|
||||
Ready(BlockAccessLists),
|
||||
}
|
||||
|
||||
impl<Req> BalRequestState<Req> {
|
||||
const fn is_ready(&self) -> bool {
|
||||
matches!(self, Self::Ready(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client> Debug for FetchFullBlockFuture<Client>
|
||||
where
|
||||
Client: BlockClient<Header: Debug, Body: Debug>,
|
||||
@@ -752,12 +936,21 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::{error::RequestError, test_utils::TestFullBlockClient};
|
||||
use alloy_primitives::Bytes;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Range,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
// RLP encoding for an empty list.
|
||||
const EMPTY_LIST_CODE: u8 = 0xc0;
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_single_full_block() {
|
||||
let client = TestFullBlockClient::default();
|
||||
@@ -783,6 +976,50 @@ mod tests {
|
||||
assert_eq!(*received, SealedBlock::from_sealed_parts(header, body));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_single_full_block_with_access_lists() {
|
||||
let client = FullBlockWithAccessListsClient::default();
|
||||
let header: SealedHeader = SealedHeader::default();
|
||||
let body = BlockBody::default();
|
||||
let access_list = Bytes::from_static(&[EMPTY_LIST_CODE]);
|
||||
client.insert(header.clone(), body.clone(), access_list.clone());
|
||||
|
||||
let request_count = Arc::clone(&client.access_list_requests);
|
||||
let client = FullBlockClient::test_client(client);
|
||||
|
||||
let (received_block, received_access_lists) =
|
||||
client.get_full_block_with_access_lists(header.hash()).await;
|
||||
|
||||
assert_eq!(received_block, SealedBlock::from_sealed_parts(header, body));
|
||||
assert_eq!(received_access_lists, BlockAccessLists(vec![access_list]));
|
||||
assert_eq!(request_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_single_full_block_with_access_lists_retries_after_invalid_response() {
|
||||
let client = FullBlockWithAccessListsClient::default();
|
||||
client.empty_first_response.store(true, Ordering::SeqCst);
|
||||
|
||||
let header: SealedHeader = SealedHeader::default();
|
||||
let body = BlockBody::default();
|
||||
let access_list = Bytes::from_static(&[EMPTY_LIST_CODE]);
|
||||
client.insert(header.clone(), body.clone(), access_list.clone());
|
||||
|
||||
let request_count = Arc::clone(&client.access_list_requests);
|
||||
let bad_messages = Arc::clone(&client.bad_messages);
|
||||
let client = FullBlockClient::test_client(client);
|
||||
|
||||
let (received_block, received_access_lists) =
|
||||
timeout(Duration::from_secs(1), client.get_full_block_with_access_lists(header.hash()))
|
||||
.await
|
||||
.expect("access list request retry should complete");
|
||||
|
||||
assert_eq!(received_block, SealedBlock::from_sealed_parts(header, body));
|
||||
assert_eq!(received_access_lists, BlockAccessLists(vec![access_list]));
|
||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||
assert_eq!(bad_messages.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
/// Inserts headers and returns the last header and block body.
|
||||
fn insert_headers_into_client(
|
||||
client: &TestFullBlockClient,
|
||||
@@ -804,6 +1041,112 @@ mod tests {
|
||||
(sealed_header, body)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FullBlockWithAccessListsClient {
|
||||
inner: TestFullBlockClient,
|
||||
access_lists: Arc<Mutex<HashMap<B256, Bytes>>>,
|
||||
access_list_requests: Arc<AtomicUsize>,
|
||||
bad_messages: Arc<AtomicUsize>,
|
||||
empty_first_response: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Default for FullBlockWithAccessListsClient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: TestFullBlockClient::default(),
|
||||
access_lists: Arc::new(Mutex::new(HashMap::default())),
|
||||
access_list_requests: Arc::new(AtomicUsize::new(0)),
|
||||
bad_messages: Arc::new(AtomicUsize::new(0)),
|
||||
empty_first_response: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FullBlockWithAccessListsClient {
|
||||
fn insert(&self, header: SealedHeader, body: BlockBody, access_list: Bytes) {
|
||||
self.inner.insert(header.clone(), body);
|
||||
self.access_lists.lock().insert(header.hash(), access_list);
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadClient for FullBlockWithAccessListsClient {
|
||||
fn report_bad_message(&self, peer_id: PeerId) {
|
||||
self.bad_messages.fetch_add(1, Ordering::SeqCst);
|
||||
self.inner.report_bad_message(peer_id);
|
||||
}
|
||||
|
||||
fn num_connected_peers(&self) -> usize {
|
||||
self.inner.num_connected_peers()
|
||||
}
|
||||
}
|
||||
|
||||
impl HeadersClient for FullBlockWithAccessListsClient {
|
||||
type Header = <TestFullBlockClient as HeadersClient>::Header;
|
||||
type Output = <TestFullBlockClient as HeadersClient>::Output;
|
||||
|
||||
fn get_headers_with_priority(
|
||||
&self,
|
||||
request: HeadersRequest,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
self.inner.get_headers_with_priority(request, priority)
|
||||
}
|
||||
}
|
||||
|
||||
impl BodiesClient for FullBlockWithAccessListsClient {
|
||||
type Body = <TestFullBlockClient as BodiesClient>::Body;
|
||||
type Output = <TestFullBlockClient as BodiesClient>::Output;
|
||||
|
||||
fn get_block_bodies_with_priority_and_range_hint(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
priority: Priority,
|
||||
range_hint: Option<RangeInclusive<u64>>,
|
||||
) -> Self::Output {
|
||||
self.inner.get_block_bodies_with_priority_and_range_hint(hashes, priority, range_hint)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockAccessListsClient for FullBlockWithAccessListsClient {
|
||||
type Output = futures::future::Ready<PeerRequestResult<BlockAccessLists>>;
|
||||
|
||||
fn get_block_access_lists_with_priority_and_requirement(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
_priority: Priority,
|
||||
_requirement: crate::block_access_lists::client::BalRequirement,
|
||||
) -> Self::Output {
|
||||
self.access_list_requests.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
if self.empty_first_response.swap(false, Ordering::SeqCst) {
|
||||
return futures::future::ready(Ok(WithPeerId::new(
|
||||
PeerId::random(),
|
||||
BlockAccessLists(Vec::new()),
|
||||
)))
|
||||
}
|
||||
|
||||
let access_lists = hashes
|
||||
.into_iter()
|
||||
.map(|hash| {
|
||||
self.access_lists
|
||||
.lock()
|
||||
.get(&hash)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Bytes::from_static(&[EMPTY_LIST_CODE]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
futures::future::ready(Ok(WithPeerId::new(
|
||||
PeerId::random(),
|
||||
BlockAccessLists(access_lists),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockClient for FullBlockWithAccessListsClient {
|
||||
type Block = reth_ethereum_primitives::Block;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FailingBodiesClient {
|
||||
inner: TestFullBlockClient,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub mod snap;
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
pub mod test_utils;
|
||||
|
||||
pub use block_access_lists::client::BlockAccessListsClient;
|
||||
pub use block_access_lists::client::{BalRequirement, BlockAccessListsClient};
|
||||
pub use bodies::client::BodiesClient;
|
||||
pub use headers::client::HeadersClient;
|
||||
pub use receipts::client::ReceiptsClient;
|
||||
|
||||
@@ -167,8 +167,9 @@ impl LaunchContext {
|
||||
|
||||
info!(target: "reth::cli", path = ?config_path, "Configuration loaded");
|
||||
|
||||
// Update the config with the command line arguments
|
||||
toml_config.peers.trusted_nodes_only = config.network.trusted_only;
|
||||
// Update the config with the command line arguments. Only override when the CLI flag is
|
||||
// set, so the TOML value is preserved when the flag is not passed.
|
||||
toml_config.peers.trusted_nodes_only |= config.network.trusted_only;
|
||||
|
||||
// Merge static file CLI arguments with config file, giving priority to CLI
|
||||
toml_config.static_files =
|
||||
|
||||
@@ -205,10 +205,12 @@ impl EngineNodeLauncher {
|
||||
ctx.blockchain_db().clone(),
|
||||
ctx.components().evm_config().clone(),
|
||||
|| async {
|
||||
// Create a separate cache for reorg validator (not shared with main engine)
|
||||
let reorg_cache = ChangesetCache::new();
|
||||
validator_builder
|
||||
.build_tree_validator(&add_ons_ctx, engine_tree_config.clone(), reorg_cache)
|
||||
.build_tree_validator(
|
||||
&add_ons_ctx,
|
||||
engine_tree_config.clone(),
|
||||
changeset_cache.clone(),
|
||||
)
|
||||
.await
|
||||
},
|
||||
node_config.debug.reorg_frequency,
|
||||
|
||||
@@ -46,6 +46,15 @@ pub struct BenchmarkArgs {
|
||||
)]
|
||||
pub engine_rpc_url: String,
|
||||
|
||||
/// The RPC url to use for non-authenticated node RPC requests.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "LOCAL_RPC_URL",
|
||||
verbatim_doc_comment,
|
||||
default_value = "http://localhost:8545"
|
||||
)]
|
||||
pub local_rpc_url: String,
|
||||
|
||||
/// The `WebSocket` RPC URL to use for persistence subscriptions.
|
||||
///
|
||||
/// If not provided, will attempt to derive from engine-rpc-url by:
|
||||
@@ -241,6 +250,7 @@ mod tests {
|
||||
fn test_parse_benchmark_args() {
|
||||
let default_args = BenchmarkArgs {
|
||||
engine_rpc_url: "http://localhost:8551".to_string(),
|
||||
local_rpc_url: "http://localhost:8545".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let args = CommandParser::<BenchmarkArgs>::parse_from(["reth-bench"]).args;
|
||||
|
||||
@@ -60,6 +60,13 @@ pub struct DatabaseArgs {
|
||||
value_parser = value_parser!(SyncMode),
|
||||
)]
|
||||
pub sync_mode: Option<SyncMode>,
|
||||
/// `RocksDB` block cache size (e.g., 512MB, 4GB).
|
||||
///
|
||||
/// Controls the size of the in-memory LRU cache for decompressed `RocksDB` blocks.
|
||||
/// A larger cache reduces repeated decompression of hot blocks, improving read
|
||||
/// performance for history lookups.
|
||||
#[arg(long = "db.rocksdb-block-cache-size", value_parser = parse_byte_size)]
|
||||
pub rocksdb_block_cache_size: Option<usize>,
|
||||
}
|
||||
|
||||
impl DatabaseArgs {
|
||||
|
||||
@@ -4,8 +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_MULTIPROOF_TASK_CHUNK_SIZE, DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS, DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
|
||||
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,
|
||||
};
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
|
||||
@@ -25,6 +26,7 @@ pub struct DefaultEngineValues {
|
||||
persistence_threshold: u64,
|
||||
persistence_backpressure_threshold: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
invalid_header_hit_eviction_threshold: u8,
|
||||
legacy_state_root_task_enabled: bool,
|
||||
state_cache_disabled: bool,
|
||||
prewarming_disabled: bool,
|
||||
@@ -49,6 +51,9 @@ pub struct DefaultEngineValues {
|
||||
state_root_task_timeout: Option<String>,
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
share_sparse_trie_with_payload_builder: bool,
|
||||
suppress_persistence_during_build: bool,
|
||||
bal_parallel_execution_disabled: bool,
|
||||
bal_parallel_state_root_disabled: bool,
|
||||
}
|
||||
|
||||
impl DefaultEngineValues {
|
||||
@@ -80,6 +85,12 @@ impl DefaultEngineValues {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the invalid header cache hit eviction threshold
|
||||
pub const fn with_invalid_header_hit_eviction_threshold(mut self, v: u8) -> Self {
|
||||
self.invalid_header_hit_eviction_threshold = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to enable legacy state root task by default
|
||||
pub const fn with_legacy_state_root_task_enabled(mut self, v: bool) -> Self {
|
||||
self.legacy_state_root_task_enabled = v;
|
||||
@@ -226,6 +237,24 @@ impl DefaultEngineValues {
|
||||
self.share_sparse_trie_with_payload_builder = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to suppress persistence during payload building by default
|
||||
pub const fn with_suppress_persistence_during_build(mut self, v: bool) -> Self {
|
||||
self.suppress_persistence_during_build = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to disable BAL-based parallel execution by default
|
||||
pub const fn with_bal_parallel_execution_disabled(mut self, v: bool) -> Self {
|
||||
self.bal_parallel_execution_disabled = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to disable BAL-driven parallel state root by default
|
||||
pub const fn with_bal_parallel_state_root_disabled(mut self, v: bool) -> Self {
|
||||
self.bal_parallel_state_root_disabled = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefaultEngineValues {
|
||||
@@ -234,6 +263,7 @@ impl Default for DefaultEngineValues {
|
||||
persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
persistence_backpressure_threshold: DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
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,
|
||||
state_cache_disabled: false,
|
||||
prewarming_disabled: false,
|
||||
@@ -258,6 +288,9 @@ impl Default for DefaultEngineValues {
|
||||
state_root_task_timeout: Some("1s".to_string()),
|
||||
share_execution_cache_with_payload_builder: false,
|
||||
share_sparse_trie_with_payload_builder: false,
|
||||
suppress_persistence_during_build: false,
|
||||
bal_parallel_execution_disabled: false,
|
||||
bal_parallel_state_root_disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,6 +318,14 @@ pub struct EngineArgs {
|
||||
#[arg(long = "engine.memory-block-buffer-target", default_value_t = DefaultEngineValues::get_global().memory_block_buffer_target)]
|
||||
pub memory_block_buffer_target: u64,
|
||||
|
||||
/// Configure how many cache hits an invalid header can accumulate before it is evicted and
|
||||
/// reprocessed.
|
||||
///
|
||||
/// Set to `0` to effectively disable the cache because entries are evicted on the first
|
||||
/// lookup.
|
||||
#[arg(long = "engine.invalid-header-cache-hit-eviction-threshold", default_value_t = DefaultEngineValues::get_global().invalid_header_hit_eviction_threshold)]
|
||||
pub invalid_header_hit_eviction_threshold: u8,
|
||||
|
||||
/// Enable legacy state root
|
||||
#[arg(long = "engine.legacy-state-root", default_value_t = DefaultEngineValues::get_global().legacy_state_root_task_enabled)]
|
||||
pub legacy_state_root_task_enabled: bool,
|
||||
@@ -459,6 +500,32 @@ pub struct EngineArgs {
|
||||
)]
|
||||
pub share_sparse_trie_with_payload_builder: bool,
|
||||
|
||||
/// Suppress persistence while building a payload.
|
||||
///
|
||||
/// When enabled, persistence cycles are deferred from the moment an FCU with payload
|
||||
/// attributes arrives until the next FCU clears the build. Useful on chains with short
|
||||
/// block times where persistence I/O can interfere with block building latency.
|
||||
#[arg(
|
||||
long = "engine.suppress-persistence-during-build",
|
||||
default_value_t = DefaultEngineValues::get_global().suppress_persistence_during_build,
|
||||
)]
|
||||
pub suppress_persistence_during_build: bool,
|
||||
|
||||
/// Disable BAL (Block Access List, EIP-7928) based parallel execution. When set, falls back
|
||||
/// to transaction-based prewarming even when a BAL is available.
|
||||
#[arg(long = "engine.disable-bal-parallel-execution", default_value_t = DefaultEngineValues::get_global().bal_parallel_execution_disabled)]
|
||||
pub bal_parallel_execution_disabled: bool,
|
||||
|
||||
/// Disable BAL-driven parallel state root computation. When set, the BAL hashed post state
|
||||
/// is not sent to the multiproof task for early parallel state root computation.
|
||||
#[arg(long = "engine.disable-bal-parallel-state-root", default_value_t = DefaultEngineValues::get_global().bal_parallel_state_root_disabled)]
|
||||
pub bal_parallel_state_root_disabled: bool,
|
||||
|
||||
/// Disable BAL (Block Access List) batched IO during prewarming. When set, falls back
|
||||
/// to individual per-slot storage reads instead of batched cursor reads.
|
||||
#[arg(long = "engine.disable-bal-batch-io", default_value_t = false)]
|
||||
pub disable_bal_batch_io: bool,
|
||||
|
||||
/// Add random jitter before each proof computation (trie-debug only).
|
||||
/// Each proof worker sleeps for a random duration up to this value before
|
||||
/// starting work. Useful for stress-testing timing-sensitive proof logic.
|
||||
@@ -480,6 +547,7 @@ impl Default for EngineArgs {
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
memory_block_buffer_target,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
legacy_state_root_task_enabled,
|
||||
state_cache_disabled,
|
||||
prewarming_disabled,
|
||||
@@ -504,11 +572,15 @@ impl Default for EngineArgs {
|
||||
state_root_task_timeout,
|
||||
share_execution_cache_with_payload_builder,
|
||||
share_sparse_trie_with_payload_builder,
|
||||
suppress_persistence_during_build,
|
||||
bal_parallel_execution_disabled,
|
||||
bal_parallel_state_root_disabled,
|
||||
} = DefaultEngineValues::get_global().clone();
|
||||
Self {
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
memory_block_buffer_target,
|
||||
invalid_header_hit_eviction_threshold,
|
||||
legacy_state_root_task_enabled,
|
||||
state_root_task_compare_updates,
|
||||
caching_and_prewarming_enabled: true,
|
||||
@@ -539,6 +611,10 @@ impl Default for EngineArgs {
|
||||
.map(|s| humantime::parse_duration(s).expect("valid default duration")),
|
||||
share_execution_cache_with_payload_builder,
|
||||
share_sparse_trie_with_payload_builder,
|
||||
suppress_persistence_during_build,
|
||||
bal_parallel_execution_disabled,
|
||||
bal_parallel_state_root_disabled,
|
||||
disable_bal_batch_io: false,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -563,6 +639,7 @@ impl EngineArgs {
|
||||
.with_persistence_threshold(self.persistence_threshold)
|
||||
.with_persistence_backpressure_threshold(self.persistence_backpressure_threshold)
|
||||
.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)
|
||||
.without_state_cache(self.state_cache_disabled)
|
||||
.without_prewarming(self.prewarming_disabled)
|
||||
@@ -588,7 +665,11 @@ impl EngineArgs {
|
||||
)
|
||||
.with_share_sparse_trie_with_payload_builder(
|
||||
self.share_sparse_trie_with_payload_builder,
|
||||
);
|
||||
)
|
||||
.with_suppress_persistence_during_build(self.suppress_persistence_during_build)
|
||||
.without_bal_parallel_execution(self.bal_parallel_execution_disabled)
|
||||
.without_bal_parallel_state_root(self.bal_parallel_state_root_disabled)
|
||||
.without_bal_batch_io(self.disable_bal_batch_io);
|
||||
#[cfg(feature = "trie-debug")]
|
||||
let config = config.with_proof_jitter(self.proof_jitter);
|
||||
config
|
||||
@@ -621,6 +702,7 @@ mod tests {
|
||||
persistence_threshold: 100,
|
||||
persistence_backpressure_threshold: 101,
|
||||
memory_block_buffer_target: 50,
|
||||
invalid_header_hit_eviction_threshold: 7,
|
||||
legacy_state_root_task_enabled: true,
|
||||
caching_and_prewarming_enabled: true,
|
||||
state_cache_disabled: true,
|
||||
@@ -649,6 +731,10 @@ mod tests {
|
||||
state_root_task_timeout: Some(Duration::from_secs(2)),
|
||||
share_execution_cache_with_payload_builder: false,
|
||||
share_sparse_trie_with_payload_builder: false,
|
||||
suppress_persistence_during_build: false,
|
||||
bal_parallel_execution_disabled: true,
|
||||
bal_parallel_state_root_disabled: true,
|
||||
disable_bal_batch_io: true,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
};
|
||||
@@ -661,6 +747,8 @@ mod tests {
|
||||
"101",
|
||||
"--engine.memory-block-buffer-target",
|
||||
"50",
|
||||
"--engine.invalid-header-cache-hit-eviction-threshold",
|
||||
"7",
|
||||
"--engine.legacy-state-root",
|
||||
"--engine.disable-state-cache",
|
||||
"--engine.disable-prewarming",
|
||||
@@ -691,6 +779,9 @@ mod tests {
|
||||
"--engine.disable-sparse-trie-cache-pruning",
|
||||
"--engine.state-root-task-timeout",
|
||||
"2s",
|
||||
"--engine.disable-bal-parallel-execution",
|
||||
"--engine.disable-bal-parallel-state-root",
|
||||
"--engine.disable-bal-batch-io",
|
||||
])
|
||||
.args;
|
||||
|
||||
@@ -740,6 +831,28 @@ mod tests {
|
||||
assert_eq!(args.slow_block_threshold, Some(Duration::from_millis(500)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_header_hit_eviction_threshold() {
|
||||
let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
|
||||
assert_eq!(
|
||||
args.invalid_header_hit_eviction_threshold,
|
||||
DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD
|
||||
);
|
||||
assert_eq!(
|
||||
args.tree_config().invalid_header_hit_eviction_threshold(),
|
||||
DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD
|
||||
);
|
||||
|
||||
let args = CommandParser::<EngineArgs>::parse_from([
|
||||
"reth",
|
||||
"--engine.invalid-header-cache-hit-eviction-threshold",
|
||||
"0",
|
||||
])
|
||||
.args;
|
||||
assert_eq!(args.invalid_header_hit_eviction_threshold, 0);
|
||||
assert_eq!(args.tree_config().invalid_header_hit_eviction_threshold(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_share_sparse_trie_flag() {
|
||||
let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
|
||||
|
||||
@@ -719,9 +719,16 @@ pub struct DiscoveryArgs {
|
||||
pub disable_discv4_discovery: bool,
|
||||
|
||||
/// Enable Discv5 discovery.
|
||||
#[arg(long, conflicts_with = "disable_discovery")]
|
||||
///
|
||||
/// Discv5 is now enabled by default, so this flag is a no-op and will be removed in a future
|
||||
/// release.
|
||||
#[arg(long, conflicts_with = "disable_discovery", hide = true)]
|
||||
pub enable_discv5_discovery: bool,
|
||||
|
||||
/// Disable Discv5 discovery.
|
||||
#[arg(long, conflicts_with = "disable_discovery")]
|
||||
pub disable_discv5_discovery: bool,
|
||||
|
||||
/// Disable Nat discovery.
|
||||
#[arg(long, conflicts_with = "disable_discovery")]
|
||||
pub disable_nat: bool,
|
||||
@@ -852,21 +859,23 @@ impl DiscoveryArgs {
|
||||
.bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
|
||||
}
|
||||
|
||||
/// Returns true if discv5 discovery should be configured
|
||||
/// Returns true if discv5 discovery should be configured.
|
||||
///
|
||||
/// Discv5 is enabled by default and can be disabled with `--disable-discv5-discovery`.
|
||||
const fn should_enable_discv5(&self) -> bool {
|
||||
if self.disable_discovery {
|
||||
if self.disable_discovery || self.disable_discv5_discovery {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.enable_discv5_discovery ||
|
||||
self.discv5_addr.is_some() ||
|
||||
self.discv5_addr_ipv6.is_some()
|
||||
true
|
||||
}
|
||||
|
||||
/// Set the discovery port to zero, to allow the OS to assign a random unused port when
|
||||
/// discovery binds to the socket.
|
||||
/// Set the discovery ports to zero, to allow the OS to assign random unused ports when
|
||||
/// discovery binds to the sockets.
|
||||
pub const fn with_unused_discovery_port(mut self) -> Self {
|
||||
self.port = 0;
|
||||
self.discv5_port = 0;
|
||||
self.discv5_port_ipv6 = 0;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -896,6 +905,7 @@ impl Default for DiscoveryArgs {
|
||||
disable_dns_discovery: false,
|
||||
disable_discv4_discovery: false,
|
||||
enable_discv5_discovery: false,
|
||||
disable_discv5_discovery: false,
|
||||
disable_nat: false,
|
||||
addr: DEFAULT_DISCOVERY_ADDR,
|
||||
port: DEFAULT_DISCOVERY_PORT,
|
||||
|
||||
@@ -115,6 +115,11 @@ pub trait EthState: LoadState + SpawnBlocking {
|
||||
block_id: Option<BlockId>,
|
||||
) -> impl Future<Output = Result<HashMap<Address, Vec<B256>>, Self::Error>> + Send {
|
||||
async move {
|
||||
if requests.is_empty() {
|
||||
return Err(Self::Error::from_eth_err(EthApiError::InvalidParams(
|
||||
"empty request".to_string(),
|
||||
)));
|
||||
}
|
||||
let total_slots: usize = requests.values().map(|slots| slots.len()).sum();
|
||||
if total_slots > DEFAULT_MAX_STORAGE_VALUES_SLOTS {
|
||||
return Err(Self::Error::from_eth_err(EthApiError::InvalidParams(
|
||||
|
||||
@@ -283,6 +283,7 @@ pub trait Trace: LoadState<Error: FromEvmError<Self::Evm>> + Call {
|
||||
let block_hash = block.hash();
|
||||
|
||||
let block_number = evm_env.block_env.number().saturating_to();
|
||||
let block_timestamp = evm_env.block_env.timestamp().saturating_to();
|
||||
let base_fee = evm_env.block_env.basefee();
|
||||
|
||||
this.apply_pre_execution_changes(&block, &mut db)?;
|
||||
@@ -309,8 +310,8 @@ pub trait Trace: LoadState<Error: FromEvmError<Self::Evm>> + Call {
|
||||
index: Some(idx),
|
||||
block_hash: Some(block_hash),
|
||||
block_number: Some(block_number),
|
||||
block_timestamp: Some(block_timestamp),
|
||||
base_fee: Some(base_fee),
|
||||
..Default::default()
|
||||
};
|
||||
idx += 1;
|
||||
|
||||
|
||||
@@ -325,15 +325,16 @@ pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
|
||||
if let Some(block) = self.recovered_block(block_id).await? {
|
||||
let block_hash = block.hash();
|
||||
let block_number = block.number();
|
||||
let block_timestamp = block.timestamp();
|
||||
let base_fee_per_gas = block.base_fee_per_gas();
|
||||
if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
|
||||
let tx_info = TransactionInfo {
|
||||
hash: Some(*tx.tx_hash()),
|
||||
block_hash: Some(block_hash),
|
||||
block_number: Some(block_number),
|
||||
block_timestamp: Some(block_timestamp),
|
||||
base_fee: base_fee_per_gas,
|
||||
index: Some(index as u64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
return Ok(Some(
|
||||
@@ -395,6 +396,7 @@ pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
|
||||
.and_then(|block| {
|
||||
let block_hash = block.hash();
|
||||
let block_number = block.number();
|
||||
let block_timestamp = block.timestamp();
|
||||
let base_fee_per_gas = block.base_fee_per_gas();
|
||||
|
||||
block
|
||||
@@ -406,9 +408,9 @@ pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
|
||||
hash: Some(*tx.tx_hash()),
|
||||
block_hash: Some(block_hash),
|
||||
block_number: Some(block_number),
|
||||
block_timestamp: Some(block_timestamp),
|
||||
base_fee: base_fee_per_gas,
|
||||
index: Some(index as u64),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(self.converter().fill(tx.clone().with_signer(*signer), tx_info)?)
|
||||
})
|
||||
@@ -681,6 +683,7 @@ pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
|
||||
index: meta.index,
|
||||
block_hash: meta.block_hash,
|
||||
block_number: meta.block_number,
|
||||
block_timestamp: meta.timestamp,
|
||||
base_fee: meta.base_fee,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ impl<B: Block, R> CachedTransaction<B, R> {
|
||||
index: self.tx_index as u64,
|
||||
block_hash: self.block.hash(),
|
||||
block_number: self.block.number(),
|
||||
block_timestamp: self.block.timestamp(),
|
||||
base_fee: self.block.base_fee_per_gas(),
|
||||
})
|
||||
}
|
||||
|
||||
110
crates/rpc/rpc-eth-types/src/cache/mod.rs
vendored
110
crates/rpc/rpc-eth-types/src/cache/mod.rs
vendored
@@ -344,17 +344,9 @@ where
|
||||
}
|
||||
|
||||
/// Removes transaction index entries for a reorged block.
|
||||
///
|
||||
/// Only removes entries that still point to this block, preserving mappings for transactions
|
||||
/// that were re-mined in a new canonical block.
|
||||
fn remove_block_transactions(&mut self, block: &RecoveredBlock<Provider::Block>) {
|
||||
let block_hash = block.hash();
|
||||
for tx in block.body().transactions() {
|
||||
if let Some((mapped_hash, _)) = self.tx_hash_index.get(tx.tx_hash()) &&
|
||||
*mapped_hash == block_hash
|
||||
{
|
||||
self.tx_hash_index.remove(tx.tx_hash());
|
||||
}
|
||||
self.tx_hash_index.remove(tx.tx_hash());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +413,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn on_reorg_header(&mut self, block_hash: B256, res: ProviderResult<Provider::Header>) {
|
||||
if let Some(queued) = self.headers_cache.remove(&block_hash) {
|
||||
// send the response to queued senders
|
||||
for tx in queued {
|
||||
let _ = tx.send(res.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrinks the queues but leaves some space for the next requests
|
||||
fn shrink_queues(&mut self) {
|
||||
let min_capacity = 2;
|
||||
@@ -597,9 +598,12 @@ where
|
||||
}
|
||||
CacheAction::RemoveReorgedChain { chain_change } => {
|
||||
for block in chain_change.blocks {
|
||||
let block_hash = block.hash();
|
||||
let header = block.clone_header();
|
||||
// Remove transaction index entries for reorged blocks
|
||||
this.remove_block_transactions(&block);
|
||||
this.on_reorg_block(block.hash(), Ok(Some(block)));
|
||||
this.on_reorg_block(block_hash, Ok(Some(block)));
|
||||
this.on_reorg_header(block_hash, Ok(header));
|
||||
}
|
||||
|
||||
for block_receipts in chain_change.receipts {
|
||||
@@ -825,3 +829,89 @@ pub async fn cache_new_blocks_task<St, N: NodePrimitives>(
|
||||
eth_state_cache.to_service.send(CacheAction::CacheNewCanonicalChain { chain_change });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{Address, Signature};
|
||||
use reth_ethereum_primitives::{
|
||||
Block, BlockBody, EthPrimitives, Transaction, TransactionSigned,
|
||||
};
|
||||
use reth_primitives_traits::RecoveredBlock;
|
||||
use reth_storage_api::noop::NoopProvider;
|
||||
|
||||
fn test_service() -> EthStateCacheService<NoopProvider, Runtime> {
|
||||
let (_cache, service) = EthStateCache::<EthPrimitives>::create(
|
||||
NoopProvider::default(),
|
||||
Runtime::test(),
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
1,
|
||||
16,
|
||||
);
|
||||
service
|
||||
}
|
||||
|
||||
fn test_block() -> RecoveredBlock<Block> {
|
||||
RecoveredBlock::new_unhashed(
|
||||
Block {
|
||||
header: Header { number: 1, ..Default::default() },
|
||||
body: BlockBody {
|
||||
transactions: vec![TransactionSigned::new_unhashed(
|
||||
Transaction::Legacy(Default::default()),
|
||||
Signature::test_signature(),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
vec![Address::ZERO],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorg_evicts_cached_headers() {
|
||||
let mut service = test_service();
|
||||
let block_hash = B256::repeat_byte(0x11);
|
||||
|
||||
assert!(service
|
||||
.headers_cache
|
||||
.insert(block_hash, Header { number: 42, ..Default::default() }));
|
||||
assert!(service.headers_cache.get(&block_hash).is_some());
|
||||
|
||||
service.on_reorg_header(block_hash, Ok(Header { number: 7, ..Default::default() }));
|
||||
|
||||
assert!(service.headers_cache.get(&block_hash).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorg_forwards_header_to_queued_requests() {
|
||||
let mut service = test_service();
|
||||
let block_hash = B256::repeat_byte(0x22);
|
||||
let (response_tx, mut response_rx) = oneshot::channel();
|
||||
let header = Header { number: 7, ..Default::default() };
|
||||
|
||||
assert!(service.headers_cache.queue(block_hash, response_tx));
|
||||
|
||||
service.on_reorg_header(block_hash, Ok(header));
|
||||
|
||||
let header =
|
||||
response_rx.try_recv().expect("queued header response").expect("header result");
|
||||
|
||||
assert_eq!(header.number, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorg_removes_tx_hash_index_entries_unconditionally() {
|
||||
let mut service = test_service();
|
||||
let block = test_block();
|
||||
let tx_hash = *block.body().transactions().next().expect("test transaction").tx_hash();
|
||||
|
||||
service.tx_hash_index.insert(tx_hash, (B256::repeat_byte(0x33), 0));
|
||||
|
||||
service.remove_block_transactions(&block);
|
||||
|
||||
assert!(service.tx_hash_index.get(&tx_hash).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,10 @@ pub fn get_filter_block_range(
|
||||
|
||||
// we cannot query blocks that don't exist yet
|
||||
if to_block_number > info.best_number {
|
||||
return Err(FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
return Err(FilterBlockRangeError::BlockRangeExceedsHead {
|
||||
requested: to_block_number,
|
||||
head: info.best_number,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((from_block_number, to_block_number))
|
||||
@@ -184,8 +187,13 @@ pub enum FilterBlockRangeError {
|
||||
#[error("invalid block range params")]
|
||||
InvalidBlockRange,
|
||||
/// Block range extends beyond current head
|
||||
#[error("block range extends beyond current head block")]
|
||||
BlockRangeExceedsHead,
|
||||
#[error("block range extends beyond current head block: requested {requested}, head {head}")]
|
||||
BlockRangeExceedsHead {
|
||||
/// The requested `toBlock` number
|
||||
requested: u64,
|
||||
/// The current head block number
|
||||
head: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -227,7 +235,10 @@ mod tests {
|
||||
let to = 15000002u64;
|
||||
let info = ChainInfo { best_number: 15000000, ..Default::default() };
|
||||
let err = get_filter_block_range(Some(from), Some(to), info.best_number, info).unwrap_err();
|
||||
assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
assert_eq!(
|
||||
err,
|
||||
FilterBlockRangeError::BlockRangeExceedsHead { requested: to, head: info.best_number }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -263,7 +274,10 @@ mod tests {
|
||||
let to = 200;
|
||||
let info = ChainInfo { best_number: 150, ..Default::default() };
|
||||
let err = get_filter_block_range(Some(from), Some(to), 0, info).unwrap_err();
|
||||
assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
assert_eq!(
|
||||
err,
|
||||
FilterBlockRangeError::BlockRangeExceedsHead { requested: to, head: info.best_number }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,6 +25,8 @@ pub enum TransactionSource<T = TransactionSigned> {
|
||||
block_hash: B256,
|
||||
/// Number of the block.
|
||||
block_number: u64,
|
||||
/// Timestamp of the block.
|
||||
block_timestamp: u64,
|
||||
/// base fee of the block.
|
||||
base_fee: Option<u64>,
|
||||
},
|
||||
@@ -48,14 +50,21 @@ impl<T: SignedTransaction> TransactionSource<T> {
|
||||
{
|
||||
match self {
|
||||
Self::Pool(tx) => resp_builder.fill_pending(tx),
|
||||
Self::Block { transaction, index, block_hash, block_number, base_fee } => {
|
||||
Self::Block {
|
||||
transaction,
|
||||
index,
|
||||
block_hash,
|
||||
block_number,
|
||||
block_timestamp,
|
||||
base_fee,
|
||||
} => {
|
||||
let tx_info = TransactionInfo {
|
||||
hash: Some(transaction.trie_hash()),
|
||||
index: Some(index),
|
||||
block_hash: Some(block_hash),
|
||||
block_number: Some(block_number),
|
||||
block_timestamp: Some(block_timestamp),
|
||||
base_fee,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
resp_builder.fill(transaction, tx_info)
|
||||
@@ -70,7 +79,14 @@ impl<T: SignedTransaction> TransactionSource<T> {
|
||||
let hash = tx.trie_hash();
|
||||
(tx, TransactionInfo { hash: Some(hash), ..Default::default() })
|
||||
}
|
||||
Self::Block { transaction, index, block_hash, block_number, base_fee } => {
|
||||
Self::Block {
|
||||
transaction,
|
||||
index,
|
||||
block_hash,
|
||||
block_number,
|
||||
block_timestamp,
|
||||
base_fee,
|
||||
} => {
|
||||
let hash = transaction.trie_hash();
|
||||
(
|
||||
transaction,
|
||||
@@ -79,8 +95,8 @@ impl<T: SignedTransaction> TransactionSource<T> {
|
||||
index: Some(index),
|
||||
block_hash: Some(block_hash),
|
||||
block_number: Some(block_number),
|
||||
block_timestamp: Some(block_timestamp),
|
||||
base_fee,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -564,7 +564,10 @@ where
|
||||
if let Some(t) = to &&
|
||||
t > info.best_number
|
||||
{
|
||||
return Err(EthFilterError::BlockRangeExceedsHead);
|
||||
return Err(EthFilterError::BlockRangeExceedsHead {
|
||||
requested: t,
|
||||
head: info.best_number,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(f) = from &&
|
||||
@@ -942,8 +945,13 @@ pub enum EthFilterError {
|
||||
#[error("invalid block range params")]
|
||||
InvalidBlockRangeParams,
|
||||
/// Block range extends beyond current head.
|
||||
#[error("block range extends beyond current head block")]
|
||||
BlockRangeExceedsHead,
|
||||
#[error("block range extends beyond current head block: requested {requested}, head {head}")]
|
||||
BlockRangeExceedsHead {
|
||||
/// The requested `toBlock` number
|
||||
requested: u64,
|
||||
/// The current head block number
|
||||
head: u64,
|
||||
},
|
||||
/// Query scope is too broad.
|
||||
#[error("query exceeds max block range {0}")]
|
||||
QueryExceedsMaxBlocks(u64),
|
||||
@@ -979,7 +987,7 @@ impl From<EthFilterError> for jsonrpsee::types::error::ErrorObject<'static> {
|
||||
err @ (EthFilterError::InvalidBlockRangeParams |
|
||||
EthFilterError::QueryExceedsMaxBlocks(_) |
|
||||
EthFilterError::QueryExceedsMaxResults { .. } |
|
||||
EthFilterError::BlockRangeExceedsHead) => {
|
||||
EthFilterError::BlockRangeExceedsHead { .. }) => {
|
||||
rpc_error_with_code(jsonrpsee::types::error::INVALID_PARAMS_CODE, err.to_string())
|
||||
}
|
||||
}
|
||||
@@ -996,7 +1004,9 @@ impl From<logs_utils::FilterBlockRangeError> for EthFilterError {
|
||||
fn from(err: logs_utils::FilterBlockRangeError) -> Self {
|
||||
match err {
|
||||
logs_utils::FilterBlockRangeError::InvalidBlockRange => Self::InvalidBlockRangeParams,
|
||||
logs_utils::FilterBlockRangeError::BlockRangeExceedsHead => Self::BlockRangeExceedsHead,
|
||||
logs_utils::FilterBlockRangeError::BlockRangeExceedsHead { requested, head } => {
|
||||
Self::BlockRangeExceedsHead { requested, head }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,41 +1020,46 @@ mod tests {
|
||||
done: true
|
||||
} if processed == total && total == block.gas_used);
|
||||
|
||||
let provider = factory.provider().unwrap();
|
||||
{
|
||||
let provider = factory.provider().unwrap();
|
||||
|
||||
// check post state
|
||||
let account1 = address!("0x1000000000000000000000000000000000000000");
|
||||
let account1_info =
|
||||
Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: Some(code_hash) };
|
||||
let account2 = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
|
||||
let account2_info = Account {
|
||||
balance: U256::from(0x1bc16d674ece94bau128),
|
||||
nonce: 0x00,
|
||||
bytecode_hash: None,
|
||||
};
|
||||
let account3 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
|
||||
let account3_info = Account {
|
||||
balance: U256::from(0x3635c9adc5de996b46u128),
|
||||
nonce: 0x01,
|
||||
bytecode_hash: None,
|
||||
};
|
||||
// check post state
|
||||
let account1 = address!("0x1000000000000000000000000000000000000000");
|
||||
let account1_info =
|
||||
Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: Some(code_hash) };
|
||||
let account2 = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
|
||||
let account2_info = Account {
|
||||
balance: U256::from(0x1bc16d674ece94bau128),
|
||||
nonce: 0x00,
|
||||
bytecode_hash: None,
|
||||
};
|
||||
let account3 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
|
||||
let account3_info = Account {
|
||||
balance: U256::from(0x3635c9adc5de996b46u128),
|
||||
nonce: 0x01,
|
||||
bytecode_hash: None,
|
||||
};
|
||||
|
||||
// assert accounts
|
||||
assert!(
|
||||
matches!(provider.basic_account(&account1), Ok(Some(acc)) if acc == account1_info)
|
||||
);
|
||||
assert!(
|
||||
matches!(provider.basic_account(&account2), Ok(Some(acc)) if acc == account2_info)
|
||||
);
|
||||
assert!(
|
||||
matches!(provider.basic_account(&account3), Ok(Some(acc)) if acc == account3_info)
|
||||
);
|
||||
// assert storage
|
||||
// Get on dupsort would return only first value. This is good enough for this test.
|
||||
assert!(matches!(
|
||||
provider.tx_ref().get::<tables::PlainStorageState>(account1),
|
||||
Ok(Some(entry)) if entry.key == B256::with_last_byte(1) && entry.value == U256::from(2)
|
||||
));
|
||||
// assert accounts
|
||||
assert!(matches!(
|
||||
provider.basic_account(&account1),
|
||||
Ok(Some(acc)) if acc == account1_info
|
||||
));
|
||||
assert!(matches!(
|
||||
provider.basic_account(&account2),
|
||||
Ok(Some(acc)) if acc == account2_info
|
||||
));
|
||||
assert!(matches!(
|
||||
provider.basic_account(&account3),
|
||||
Ok(Some(acc)) if acc == account3_info
|
||||
));
|
||||
// assert storage
|
||||
// Get on dupsort would return only first value. This is good enough for this test.
|
||||
assert!(matches!(
|
||||
provider.tx_ref().get::<tables::PlainStorageState>(account1),
|
||||
Ok(Some(entry)) if entry.key == B256::with_last_byte(1) && entry.value == U256::from(2)
|
||||
));
|
||||
}
|
||||
|
||||
let mut provider = factory.database_provider_rw().unwrap();
|
||||
let mut stage = stage();
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
use crate::ChangesetOffset;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Write},
|
||||
os::unix::fs::FileExt,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
@@ -177,16 +178,14 @@ impl ChangesetOffsetReader {
|
||||
|
||||
/// Reads a single changeset offset by block index.
|
||||
/// Returns None if index is out of bounds.
|
||||
pub fn get(&mut self, block_index: u64) -> io::Result<Option<ChangesetOffset>> {
|
||||
pub fn get(&self, block_index: u64) -> io::Result<Option<ChangesetOffset>> {
|
||||
if block_index >= self.len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let byte_pos = block_index * Self::RECORD_SIZE as u64;
|
||||
self.file.seek(SeekFrom::Start(byte_pos))?;
|
||||
|
||||
let mut buf = [0u8; Self::RECORD_SIZE];
|
||||
self.file.read_exact(&mut buf)?;
|
||||
self.file.read_exact_at(&mut buf, byte_pos)?;
|
||||
|
||||
let offset = u64::from_le_bytes(buf[..8].try_into().unwrap());
|
||||
let num_changes = u64::from_le_bytes(buf[8..].try_into().unwrap());
|
||||
@@ -195,7 +194,7 @@ impl ChangesetOffsetReader {
|
||||
}
|
||||
|
||||
/// Reads a range of changeset offsets.
|
||||
pub fn get_range(&mut self, start: u64, end: u64) -> io::Result<Vec<ChangesetOffset>> {
|
||||
pub fn get_range(&self, start: u64, end: u64) -> io::Result<Vec<ChangesetOffset>> {
|
||||
let end = end.min(self.len);
|
||||
if start >= end {
|
||||
return Ok(Vec::new());
|
||||
@@ -203,13 +202,13 @@ impl ChangesetOffsetReader {
|
||||
|
||||
let count = (end - start) as usize;
|
||||
let byte_pos = start * Self::RECORD_SIZE as u64;
|
||||
self.file.seek(SeekFrom::Start(byte_pos))?;
|
||||
|
||||
let mut result = Vec::with_capacity(count);
|
||||
let mut buf = [0u8; Self::RECORD_SIZE];
|
||||
|
||||
for _ in 0..count {
|
||||
self.file.read_exact(&mut buf)?;
|
||||
for i in 0..count {
|
||||
let pos = byte_pos + (i as u64) * Self::RECORD_SIZE as u64;
|
||||
self.file.read_exact_at(&mut buf, pos)?;
|
||||
let offset = u64::from_le_bytes(buf[..8].try_into().unwrap());
|
||||
let num_changes = u64::from_le_bytes(buf[8..].try_into().unwrap());
|
||||
result.push(ChangesetOffset::new(offset, num_changes));
|
||||
@@ -251,7 +250,7 @@ mod tests {
|
||||
|
||||
// Read
|
||||
{
|
||||
let mut reader = ChangesetOffsetReader::new(&path, 3).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&path, 3).unwrap();
|
||||
assert_eq!(reader.len(), 3);
|
||||
|
||||
let entry = reader.get(0).unwrap().unwrap();
|
||||
@@ -284,7 +283,7 @@ mod tests {
|
||||
writer.truncate(2).unwrap();
|
||||
assert_eq!(writer.len(), 2);
|
||||
|
||||
let mut reader = ChangesetOffsetReader::new(&path, 2).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&path, 2).unwrap();
|
||||
assert_eq!(reader.len(), 2);
|
||||
assert!(reader.get(2).unwrap().is_none());
|
||||
}
|
||||
@@ -317,7 +316,7 @@ mod tests {
|
||||
assert_eq!(std::fs::metadata(&path).unwrap().len(), 16);
|
||||
|
||||
// Verify the complete record is readable
|
||||
let mut reader = ChangesetOffsetReader::new(&path, 1).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&path, 1).unwrap();
|
||||
assert_eq!(reader.len(), 1);
|
||||
let entry = reader.get(0).unwrap().unwrap();
|
||||
assert_eq!(entry.offset(), 100);
|
||||
@@ -340,7 +339,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Open with len=2, ignoring the 3rd record
|
||||
let mut reader = ChangesetOffsetReader::new(&path, 2).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&path, 2).unwrap();
|
||||
assert_eq!(reader.len(), 2);
|
||||
|
||||
// First two records should be readable
|
||||
@@ -397,7 +396,7 @@ mod tests {
|
||||
|
||||
// Verify the records are correct
|
||||
{
|
||||
let mut reader = ChangesetOffsetReader::new(&path, 3).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&path, 3).unwrap();
|
||||
assert_eq!(reader.len(), 3);
|
||||
|
||||
let entry0 = reader.get(0).unwrap().unwrap();
|
||||
|
||||
@@ -15,9 +15,9 @@ mod compression;
|
||||
mod event;
|
||||
mod segment;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[cfg(all(feature = "std", unix))]
|
||||
mod changeset_offsets;
|
||||
#[cfg(feature = "std")]
|
||||
#[cfg(all(feature = "std", unix))]
|
||||
pub use changeset_offsets::{ChangesetOffsetReader, ChangesetOffsetWriter};
|
||||
|
||||
use alloy_primitives::BlockNumber;
|
||||
|
||||
@@ -17,7 +17,7 @@ use roaring::RoaringTreemap;
|
||||
/// - Direct access: elements can be accessed or queried without needing to decode the entire list.
|
||||
/// - [`RoaringTreemap`] backing: internally backed by [`RoaringTreemap`], which supports 64-bit
|
||||
/// integers.
|
||||
#[derive(Clone, PartialEq, Default, Deref)]
|
||||
#[derive(Clone, PartialEq, Eq, Default, Deref)]
|
||||
pub struct IntegerList(pub RoaringTreemap);
|
||||
|
||||
impl fmt::Debug for IntegerList {
|
||||
|
||||
@@ -45,6 +45,9 @@ parking_lot = { workspace = true, optional = true }
|
||||
# arbitrary utils
|
||||
strum = { workspace = true, features = ["derive"], optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# reth libs with arbitrary
|
||||
reth-primitives-traits = { workspace = true, features = ["reth-codec"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{is_database_empty, TableSet, Tables};
|
||||
use eyre::Context;
|
||||
use reth_tracing::tracing::info;
|
||||
use reth_tracing::tracing::{info, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub use crate::implementation::mdbx::*;
|
||||
@@ -12,12 +12,75 @@ pub use reth_libmdbx::*;
|
||||
/// versions. These will be dropped during database initialization.
|
||||
const ORPHAN_TABLES: &[&str] = &["AccountsTrieChangeSets", "StoragesTrieChangeSets"];
|
||||
|
||||
/// Checks if the given path resides on a ZFS filesystem and logs a warning.
|
||||
///
|
||||
/// ZFS uses copy-on-write (COW) semantics which conflict with MDBX's write patterns, leading to
|
||||
/// significant performance degradation.
|
||||
fn warn_if_zfs(path: &Path) {
|
||||
if matches!(is_zfs(path), Ok(true)) {
|
||||
warn!(
|
||||
target: "reth::db",
|
||||
path = %path.display(),
|
||||
"Database is on a ZFS filesystem. ZFS's copy-on-write behavior causes significant \
|
||||
performance degradation with MDBX. Consider using ext4 or xfs instead."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given path is on a ZFS filesystem.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_zfs(path: &Path) -> std::io::Result<bool> {
|
||||
use std::{ffi::CString, os::unix::ffi::OsStrExt};
|
||||
|
||||
/// ZFS filesystem magic number.
|
||||
const ZFS_SUPER_MAGIC: i64 = 0x2fc12fc1;
|
||||
|
||||
let c_path = CString::new(path.as_os_str().as_bytes())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
unsafe {
|
||||
let mut stat: libc::statfs = std::mem::zeroed();
|
||||
if libc::statfs(c_path.as_ptr(), &raw mut stat) == 0 {
|
||||
Ok(stat.f_type == ZFS_SUPER_MAGIC)
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given path is on a ZFS filesystem.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn is_zfs(path: &Path) -> std::io::Result<bool> {
|
||||
use std::{ffi::CString, os::unix::ffi::OsStrExt};
|
||||
|
||||
let c_path = CString::new(path.as_os_str().as_bytes())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
unsafe {
|
||||
let mut stat: libc::statfs = std::mem::zeroed();
|
||||
if libc::statfs(c_path.as_ptr(), &raw mut stat) == 0 {
|
||||
let fstype = std::ffi::CStr::from_ptr(stat.f_fstypename.as_ptr());
|
||||
Ok(fstype.to_bytes() == b"zfs")
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ZFS detection is unsupported on this platform, always returns `Ok(false)`.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
fn is_zfs(_path: &Path) -> std::io::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Creates a new database at the specified path if it doesn't exist. Does NOT create tables. Check
|
||||
/// [`init_db`].
|
||||
pub fn create_db<P: AsRef<Path>>(path: P, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
|
||||
use crate::version::{check_db_version_file, create_db_version_file, DatabaseVersionError};
|
||||
|
||||
let rpath = path.as_ref();
|
||||
warn_if_zfs(rpath);
|
||||
|
||||
if is_database_empty(rpath) {
|
||||
reth_fs_util::create_dir_all(rpath)
|
||||
.wrap_err_with(|| format!("Could not create database directory {}", rpath.display()))?;
|
||||
|
||||
109
crates/storage/provider/src/bal.rs
Normal file
109
crates/storage/provider/src/bal.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
|
||||
use parking_lot::RwLock;
|
||||
use reth_storage_api::{BalStore, GetBlockAccessListLimit};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
/// Basic in-memory BAL store keyed by block hash.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct InMemoryBalStore {
|
||||
entries: Arc<RwLock<HashMap<BlockHash, Bytes>>>,
|
||||
}
|
||||
|
||||
impl BalStore for InMemoryBalStore {
|
||||
fn insert(
|
||||
&self,
|
||||
block_hash: BlockHash,
|
||||
_block_number: BlockNumber,
|
||||
bal: Bytes,
|
||||
) -> ProviderResult<()> {
|
||||
self.entries.write().insert(block_hash, bal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
|
||||
let entries = self.entries.read();
|
||||
let mut result = Vec::with_capacity(block_hashes.len());
|
||||
|
||||
for hash in block_hashes {
|
||||
result.push(entries.get(hash).cloned());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let entries = self.entries.read();
|
||||
let mut size = 0;
|
||||
|
||||
for hash in block_hashes {
|
||||
let bal = entries.get(hash).cloned().unwrap_or_else(|| Bytes::from_static(&[0xc0]));
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_range(&self, _start: BlockNumber, _count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::B256;
|
||||
|
||||
#[test]
|
||||
fn insert_and_lookup_by_hash() {
|
||||
let store = InMemoryBalStore::default();
|
||||
let hash = B256::random();
|
||||
let missing = B256::random();
|
||||
let bal = Bytes::from_static(b"bal");
|
||||
|
||||
store.insert(hash, 1, bal.clone()).unwrap();
|
||||
|
||||
assert_eq!(store.get_by_hashes(&[hash, missing]).unwrap(), vec![Some(bal), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_lookup_is_empty() {
|
||||
let store = InMemoryBalStore::default();
|
||||
|
||||
assert!(store.get_by_range(1, 10).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limited_lookup_returns_prefix() {
|
||||
let store = InMemoryBalStore::default();
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
let bal1 = Bytes::from_static(&[0xc1, 0x02]);
|
||||
let bal2 = Bytes::from_static(&[0xc1, 0x03]);
|
||||
|
||||
store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
store.insert(hash1, 2, bal1.clone()).unwrap();
|
||||
store.insert(hash2, 3, bal2).unwrap();
|
||||
|
||||
let limited = store
|
||||
.get_by_hashes_with_limit(
|
||||
&[hash0, hash1, hash2],
|
||||
GetBlockAccessListLimit::ResponseSizeSoftLimit(2),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(limited, vec![bal0, bal1]);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ pub mod test_utils;
|
||||
pub mod either_writer;
|
||||
pub use either_writer::*;
|
||||
|
||||
mod bal;
|
||||
pub use bal::InMemoryBalStore;
|
||||
|
||||
pub use reth_chain_state::{
|
||||
CanonStateNotification, CanonStateNotificationSender, CanonStateNotificationStream,
|
||||
CanonStateNotifications, CanonStateSubscriptions,
|
||||
@@ -48,8 +51,9 @@ pub use revm_database::states::OriginalValuesKnown;
|
||||
// reexport traits to avoid breaking changes
|
||||
pub use reth_static_file_types as static_file;
|
||||
pub use reth_storage_api::{
|
||||
HistoryWriter, MetadataProvider, MetadataWriter, StateWriteConfig, StatsReader,
|
||||
StorageSettings, StorageSettingsCache,
|
||||
BalProvider, BalStore, BalStoreHandle, GetBlockAccessListLimit, HistoryWriter,
|
||||
MetadataProvider, MetadataWriter, NoopBalStore, StateWriteConfig, StatsReader, StorageSettings,
|
||||
StorageSettingsCache,
|
||||
};
|
||||
/// Re-export provider error.
|
||||
pub use reth_storage_errors::provider::{ProviderError, ProviderResult};
|
||||
|
||||
@@ -3,13 +3,13 @@ use crate::{
|
||||
ConsistentProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider,
|
||||
StaticFileProviderRWRefMut,
|
||||
},
|
||||
AccountReader, BlockHashReader, BlockIdReader, BlockNumReader, BlockReader, BlockReaderIdExt,
|
||||
BlockSource, CanonChainTracker, CanonStateNotifications, CanonStateSubscriptions,
|
||||
ChainSpecProvider, ChainStateBlockReader, ChangeSetReader, DatabaseProviderFactory,
|
||||
HashedPostStateProvider, HeaderProvider, ProviderError, ProviderFactory, PruneCheckpointReader,
|
||||
ReceiptProvider, ReceiptProviderIdExt, RocksDBProviderFactory, StageCheckpointReader,
|
||||
StateProviderBox, StateProviderFactory, StateReader, StaticFileProviderFactory,
|
||||
TransactionVariant, TransactionsProvider,
|
||||
AccountReader, BalProvider, BalStoreHandle, BlockHashReader, BlockIdReader, BlockNumReader,
|
||||
BlockReader, BlockReaderIdExt, BlockSource, CanonChainTracker, CanonStateNotifications,
|
||||
CanonStateSubscriptions, ChainSpecProvider, ChainStateBlockReader, ChangeSetReader,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, HeaderProvider, InMemoryBalStore,
|
||||
ProviderError, ProviderFactory, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StateProviderBox, StateProviderFactory,
|
||||
StateReader, StaticFileProviderFactory, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
use alloy_consensus::transaction::TransactionMeta;
|
||||
use alloy_eips::{BlockHashOrNumber, BlockId, BlockNumHash, BlockNumberOrTag};
|
||||
@@ -50,6 +50,8 @@ pub struct BlockchainProvider<N: NodeTypesWithDB> {
|
||||
/// Tracks the chain info wrt forkchoice updates and in memory canonical
|
||||
/// state.
|
||||
pub(crate) canonical_in_memory_state: CanonicalInMemoryState<N::Primitives>,
|
||||
/// Store for BALs associated with this provider view.
|
||||
pub(crate) bal_store: BalStoreHandle,
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> Clone for BlockchainProvider<N> {
|
||||
@@ -57,6 +59,7 @@ impl<N: NodeTypesWithDB> Clone for BlockchainProvider<N> {
|
||||
Self {
|
||||
database: self.database.clone(),
|
||||
canonical_in_memory_state: self.canonical_in_memory_state.clone(),
|
||||
bal_store: self.bal_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +111,7 @@ impl<N: ProviderNodeTypes> BlockchainProvider<N> {
|
||||
finalized_header,
|
||||
safe_header,
|
||||
),
|
||||
bal_store: BalStoreHandle::new(InMemoryBalStore::default()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,22 +137,18 @@ impl<N: ProviderNodeTypes> BlockchainProvider<N> {
|
||||
let latest_historical = self.database.history_by_block_hash(anchor_hash)?;
|
||||
Ok(state.state_provider(latest_historical))
|
||||
}
|
||||
|
||||
/// Return the last N blocks of state, recreating the [`ExecutionOutcome`].
|
||||
///
|
||||
/// If the range is empty, or there are no blocks for the given range, then this returns `None`.
|
||||
pub fn get_state(
|
||||
&self,
|
||||
range: RangeInclusive<BlockNumber>,
|
||||
) -> ProviderResult<Option<ExecutionOutcome<ReceiptTy<N>>>> {
|
||||
self.consistent_provider()?.get_state(range)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> NodePrimitivesProvider for BlockchainProvider<N> {
|
||||
type Primitives = N::Primitives;
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> BalProvider for BlockchainProvider<N> {
|
||||
fn bal_store(&self) -> &BalStoreHandle {
|
||||
&self.bal_store
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ProviderNodeTypes> DatabaseProviderFactory for BlockchainProvider<N> {
|
||||
type DB = N::DB;
|
||||
type Provider = <ProviderFactory<N> as DatabaseProviderFactory>::Provider;
|
||||
@@ -778,7 +778,7 @@ impl<N: ProviderNodeTypes> StateReader for BlockchainProvider<N> {
|
||||
&self,
|
||||
block: BlockNumber,
|
||||
) -> ProviderResult<Option<ExecutionOutcome<Self::Receipt>>> {
|
||||
StateReader::get_state(&self.consistent_provider()?, block)
|
||||
self.consistent_provider()?.get_state(block)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,11 @@ use alloy_eips::{
|
||||
eip2718::Encodable2718, BlockHashOrNumber, BlockId, BlockNumHash, BlockNumberOrTag,
|
||||
HashOrNumber,
|
||||
};
|
||||
use alloy_primitives::{
|
||||
map::{hash_map, HashMap},
|
||||
Address, BlockHash, BlockNumber, TxHash, TxNumber, B256,
|
||||
};
|
||||
use alloy_primitives::{Address, BlockHash, BlockNumber, TxHash, TxNumber, B256};
|
||||
use reth_chain_state::{BlockState, CanonicalInMemoryState, MemoryOverlayStateProviderRef};
|
||||
use reth_chainspec::ChainInfo;
|
||||
use reth_db_api::models::{AccountBeforeTx, BlockNumberAddress, StoredBlockBodyIndices};
|
||||
use reth_execution_types::{BundleStateInit, ExecutionOutcome, RevertsInit};
|
||||
use reth_execution_types::ExecutionOutcome;
|
||||
use reth_node_types::{BlockTy, HeaderTy, ReceiptTy, TxTy};
|
||||
use reth_primitives_traits::{Account, BlockBody, RecoveredBlock, SealedHeader, StorageEntry};
|
||||
use reth_prune_types::{PruneCheckpoint, PruneSegment};
|
||||
@@ -130,159 +127,6 @@ impl<N: ProviderNodeTypes> ConsistentProvider<N> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a state provider indexed by the given block number or tag.
|
||||
fn state_by_block_number_ref<'a>(
|
||||
&'a self,
|
||||
number: BlockNumber,
|
||||
) -> ProviderResult<Box<dyn StateProvider + 'a>> {
|
||||
let hash =
|
||||
self.block_hash(number)?.ok_or_else(|| ProviderError::HeaderNotFound(number.into()))?;
|
||||
self.history_by_block_hash_ref(hash)
|
||||
}
|
||||
|
||||
/// Return the last N blocks of state, recreating the [`ExecutionOutcome`].
|
||||
///
|
||||
/// If the range is empty, or there are no blocks for the given range, then this returns `None`.
|
||||
pub fn get_state(
|
||||
&self,
|
||||
range: RangeInclusive<BlockNumber>,
|
||||
) -> ProviderResult<Option<ExecutionOutcome<ReceiptTy<N>>>> {
|
||||
if range.is_empty() {
|
||||
return Ok(None)
|
||||
}
|
||||
let start_block_number = *range.start();
|
||||
let end_block_number = *range.end();
|
||||
|
||||
// We are not removing block meta as it is used to get block changesets.
|
||||
let mut block_bodies = Vec::new();
|
||||
for block_num in range.clone() {
|
||||
let block_body = self
|
||||
.block_body_indices(block_num)?
|
||||
.ok_or(ProviderError::BlockBodyIndicesNotFound(block_num))?;
|
||||
block_bodies.push((block_num, block_body))
|
||||
}
|
||||
|
||||
// get transaction receipts
|
||||
let Some(from_transaction_num) = block_bodies.first().map(|body| body.1.first_tx_num())
|
||||
else {
|
||||
return Ok(None)
|
||||
};
|
||||
let Some(to_transaction_num) = block_bodies.last().map(|body| body.1.last_tx_num()) else {
|
||||
return Ok(None)
|
||||
};
|
||||
|
||||
let mut account_changeset = Vec::new();
|
||||
for block_num in range.clone() {
|
||||
let changeset =
|
||||
self.account_block_changeset(block_num)?.into_iter().map(|elem| (block_num, elem));
|
||||
account_changeset.extend(changeset);
|
||||
}
|
||||
|
||||
let mut storage_changeset = Vec::new();
|
||||
for block_num in range {
|
||||
let changeset = self.storage_changeset(block_num)?;
|
||||
storage_changeset.extend(changeset);
|
||||
}
|
||||
|
||||
let (state, reverts) =
|
||||
self.populate_bundle_state(account_changeset, storage_changeset, end_block_number)?;
|
||||
|
||||
let mut receipt_iter =
|
||||
self.receipts_by_tx_range(from_transaction_num..=to_transaction_num)?.into_iter();
|
||||
|
||||
let mut receipts = Vec::with_capacity(block_bodies.len());
|
||||
// loop break if we are at the end of the blocks.
|
||||
for (_, block_body) in block_bodies {
|
||||
let mut block_receipts = Vec::with_capacity(block_body.tx_count as usize);
|
||||
for tx_num in block_body.tx_num_range() {
|
||||
let receipt = receipt_iter
|
||||
.next()
|
||||
.ok_or_else(|| ProviderError::ReceiptNotFound(tx_num.into()))?;
|
||||
block_receipts.push(receipt);
|
||||
}
|
||||
receipts.push(block_receipts);
|
||||
}
|
||||
|
||||
Ok(Some(ExecutionOutcome::new_init(
|
||||
state,
|
||||
reverts,
|
||||
// We skip new contracts since we never delete them from the database
|
||||
Vec::new(),
|
||||
receipts,
|
||||
start_block_number,
|
||||
Vec::new(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Populate a [`BundleStateInit`] and [`RevertsInit`] based on the given storage and account
|
||||
/// changesets.
|
||||
///
|
||||
/// Storage changeset keys are always plain (unhashed). Current values are read via
|
||||
/// [`StateProvider::storage`], which handles hashing internally when `use_hashed_state` is
|
||||
/// enabled.
|
||||
fn populate_bundle_state(
|
||||
&self,
|
||||
account_changeset: Vec<(u64, AccountBeforeTx)>,
|
||||
storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>,
|
||||
block_range_end: BlockNumber,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)> {
|
||||
let mut state: BundleStateInit = HashMap::default();
|
||||
let mut reverts: RevertsInit = HashMap::default();
|
||||
let state_provider = self.state_by_block_number_ref(block_range_end)?;
|
||||
|
||||
// add account changeset changes
|
||||
for (block_number, account_before) in account_changeset.into_iter().rev() {
|
||||
let AccountBeforeTx { info: old_info, address } = account_before;
|
||||
match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let new_info = state_provider.basic_account(&address)?;
|
||||
entry.insert((old_info, new_info, HashMap::default()));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
// overwrite old account state.
|
||||
entry.get_mut().0 = old_info;
|
||||
}
|
||||
}
|
||||
// insert old info into reverts.
|
||||
reverts.entry(block_number).or_default().entry(address).or_default().0 = Some(old_info);
|
||||
}
|
||||
|
||||
// add storage changeset changes
|
||||
for (block_and_address, old_storage) in storage_changeset.into_iter().rev() {
|
||||
let BlockNumberAddress((block_number, address)) = block_and_address;
|
||||
// get account state or insert from plain state.
|
||||
let account_state = match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let present_info = state_provider.basic_account(&address)?;
|
||||
entry.insert((present_info, present_info, HashMap::default()))
|
||||
}
|
||||
hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
||||
};
|
||||
|
||||
// match storage.
|
||||
match account_state.2.entry(old_storage.key) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let new_storage_value =
|
||||
state_provider.storage(address, old_storage.key)?.unwrap_or_default();
|
||||
entry.insert((old_storage.value, new_storage_value));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().0 = old_storage.value;
|
||||
}
|
||||
};
|
||||
|
||||
reverts
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.entry(address)
|
||||
.or_default()
|
||||
.1
|
||||
.push(old_storage);
|
||||
}
|
||||
|
||||
Ok((state, reverts))
|
||||
}
|
||||
|
||||
/// Fetches a range of data from both in-memory state and persistent storage while a predicate
|
||||
/// is met.
|
||||
///
|
||||
@@ -1641,7 +1485,7 @@ impl<N: ProviderNodeTypes> StateReader for ConsistentProvider<N> {
|
||||
let state = state.block_ref().execution_outcome().clone();
|
||||
Ok(Some(ExecutionOutcome::from((state, block))))
|
||||
} else {
|
||||
Self::get_state(self, block..=block)
|
||||
self.storage_provider.get_state(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1661,7 +1505,7 @@ mod tests {
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, ExecutionOutcome};
|
||||
use reth_primitives_traits::{RecoveredBlock, SealedBlock};
|
||||
use reth_storage_api::{BlockReader, BlockSource, ChangeSetReader};
|
||||
use reth_storage_api::{BlockReader, BlockSource, ChangeSetReader, StateReader};
|
||||
use reth_testing_utils::generators::{
|
||||
self, random_block_range, random_changeset_range, random_eoa_accounts, BlockRangeParams,
|
||||
};
|
||||
@@ -2079,8 +1923,7 @@ mod tests {
|
||||
let provider = BlockchainProvider::new(factory)?;
|
||||
let consistent_provider = provider.consistent_provider()?;
|
||||
|
||||
let outcome =
|
||||
consistent_provider.get_state(1..=1)?.expect("should return execution outcome");
|
||||
let outcome = consistent_provider.get_state(1)?.expect("should return execution outcome");
|
||||
|
||||
let state = &outcome.bundle.state;
|
||||
let account_state = state.get(&address).expect("should have account in bundle state");
|
||||
|
||||
@@ -5,11 +5,11 @@ use crate::{
|
||||
},
|
||||
to_range,
|
||||
traits::{BlockSource, ReceiptProvider},
|
||||
BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider, DatabaseProviderFactory,
|
||||
EitherWriterDestination, HashedPostStateProvider, HeaderProvider, HeaderSyncGapProvider,
|
||||
MetadataProvider, ProviderError, PruneCheckpointReader, RocksDBProviderFactory,
|
||||
StageCheckpointReader, StateProviderBox, StaticFileProviderFactory, StaticFileWriter,
|
||||
TransactionVariant, TransactionsProvider,
|
||||
BalProvider, BalStoreHandle, BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider,
|
||||
DatabaseProviderFactory, EitherWriterDestination, HashedPostStateProvider, HeaderProvider,
|
||||
HeaderSyncGapProvider, MetadataProvider, ProviderError, PruneCheckpointReader,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StateProviderBox, StaticFileProviderFactory,
|
||||
StaticFileWriter, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
use alloy_consensus::transaction::TransactionMeta;
|
||||
use alloy_eips::BlockHashOrNumber;
|
||||
@@ -90,6 +90,8 @@ pub struct ProviderFactory<N: NodeTypesWithDB> {
|
||||
rocksdb_provider: RocksDBProvider,
|
||||
/// Changeset cache for trie unwinding
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Store for block access lists.
|
||||
bal_store: BalStoreHandle,
|
||||
/// Task runtime for spawning parallel I/O work.
|
||||
runtime: reth_tasks::Runtime,
|
||||
/// Minimum distance from tip required before pruning can occur.
|
||||
@@ -152,6 +154,7 @@ impl<N: ProviderNodeTypes> ProviderFactory<N> {
|
||||
storage_settings: Arc::new(RwLock::new(storage_settings)),
|
||||
rocksdb_provider,
|
||||
changeset_cache: ChangesetCache::new(),
|
||||
bal_store: BalStoreHandle::default(),
|
||||
runtime,
|
||||
minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
|
||||
read_only_sync: None,
|
||||
@@ -583,6 +586,12 @@ impl<N: NodeTypesWithDB> NodePrimitivesProvider for ProviderFactory<N> {
|
||||
type Primitives = N::Primitives;
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> BalProvider for ProviderFactory<N> {
|
||||
fn bal_store(&self) -> &BalStoreHandle {
|
||||
&self.bal_store
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ProviderNodeTypes> DatabaseProviderFactory for ProviderFactory<N> {
|
||||
type DB = N::DB;
|
||||
type Provider = DatabaseProvider<<N::DB as Database>::TX, N>;
|
||||
@@ -955,6 +964,7 @@ where
|
||||
storage_settings,
|
||||
rocksdb_provider,
|
||||
changeset_cache,
|
||||
bal_store,
|
||||
runtime,
|
||||
minimum_pruning_distance,
|
||||
read_only_sync,
|
||||
@@ -968,6 +978,7 @@ where
|
||||
.field("storage_settings", &*storage_settings.read())
|
||||
.field("rocksdb_provider", &rocksdb_provider)
|
||||
.field("changeset_cache", &changeset_cache)
|
||||
.field("bal_store", &bal_store)
|
||||
.field("runtime", &runtime)
|
||||
.field("minimum_pruning_distance", &minimum_pruning_distance)
|
||||
.field(
|
||||
@@ -989,6 +1000,7 @@ impl<N: NodeTypesWithDB> Clone for ProviderFactory<N> {
|
||||
storage_settings: self.storage_settings.clone(),
|
||||
rocksdb_provider: self.rocksdb_provider.clone(),
|
||||
changeset_cache: self.changeset_cache.clone(),
|
||||
bal_store: self.bal_store.clone(),
|
||||
runtime: self.runtime.clone(),
|
||||
minimum_pruning_distance: self.minimum_pruning_distance,
|
||||
read_only_sync: self.read_only_sync.clone(),
|
||||
|
||||
@@ -28,7 +28,7 @@ use alloy_eips::BlockHashOrNumber;
|
||||
use alloy_primitives::{
|
||||
keccak256,
|
||||
map::{hash_map, AddressSet, B256Map, HashMap},
|
||||
Address, BlockHash, BlockNumber, TxHash, TxNumber, B256,
|
||||
Address, BlockHash, BlockNumber, StorageKey, StorageValue, TxHash, TxNumber, B256,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
@@ -46,7 +46,7 @@ use reth_db_api::{
|
||||
table::Table,
|
||||
tables,
|
||||
transaction::{DbTx, DbTxMut},
|
||||
BlockNumberList, PlainAccountState, PlainStorageState,
|
||||
BlockNumberList,
|
||||
};
|
||||
use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, ExecutionOutcome};
|
||||
use reth_node_types::{BlockTy, BodyTy, HeaderTy, NodeTypes, ReceiptTy, TxTy};
|
||||
@@ -61,8 +61,8 @@ use reth_stages_types::{StageCheckpoint, StageId};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_storage_api::{
|
||||
BlockBodyIndicesProvider, BlockBodyReader, MetadataProvider, MetadataWriter,
|
||||
NodePrimitivesProvider, StateProvider, StateWriteConfig, StorageChangeSetReader, StoragePath,
|
||||
StorageSettingsCache, TryIntoHistoricalStateProvider, WriteStateInput,
|
||||
NodePrimitivesProvider, StateProvider, StateReader, StateWriteConfig, StorageChangeSetReader,
|
||||
StoragePath, StorageSettingsCache, TryIntoHistoricalStateProvider, WriteStateInput,
|
||||
};
|
||||
use reth_storage_errors::provider::{ProviderResult, StaticFileWriterError};
|
||||
use reth_trie::{
|
||||
@@ -264,8 +264,8 @@ impl<TX: DbTx + 'static, N: NodeTypes> DatabaseProvider<TX, N> {
|
||||
/// This keeps MDBX as the first durable step so an interrupted unwind can be recovered by
|
||||
/// truncating static files from checkpoints on the next startup.
|
||||
///
|
||||
/// For `storage_v2`, this waits after the MDBX commit so readers holding older MDBX-visible
|
||||
/// views cannot overlap the `RocksDB` unwind.
|
||||
/// This waits after the MDBX commit so readers holding older MDBX-visible views cannot overlap
|
||||
/// later cross-store unwind steps.
|
||||
///
|
||||
/// Historical `storage_v2` reads ignore `RocksDB` history entries above their MDBX-visible tip,
|
||||
/// so no additional post-`RocksDB` wait is needed before static-file commit.
|
||||
@@ -274,11 +274,11 @@ impl<TX: DbTx + 'static, N: NodeTypes> DatabaseProvider<TX, N> {
|
||||
let reader_txn_tracker = self.reader_txn_tracker.clone();
|
||||
self.tx.commit()?;
|
||||
|
||||
if storage_v2 {
|
||||
if let Some(reader_txn_tracker) = reader_txn_tracker.as_ref() {
|
||||
reader_txn_tracker.wait_for_pre_commit_readers();
|
||||
}
|
||||
if let Some(reader_txn_tracker) = reader_txn_tracker.as_ref() {
|
||||
reader_txn_tracker.wait_for_pre_commit_readers();
|
||||
}
|
||||
|
||||
if storage_v2 {
|
||||
let batches = std::mem::take(&mut *self.pending_rocksdb_batches.lock());
|
||||
for batch in batches {
|
||||
self.rocksdb_provider.commit_batch(batch)?;
|
||||
@@ -300,8 +300,16 @@ impl<TX: DbTx + 'static, N: NodeTypes> DatabaseProvider<TX, N> {
|
||||
&'a self,
|
||||
block_hash: BlockHash,
|
||||
) -> ProviderResult<Box<dyn StateProvider + 'a>> {
|
||||
let mut block_number =
|
||||
let block_number =
|
||||
self.block_number(block_hash)?.ok_or(ProviderError::BlockHashNotFound(block_hash))?;
|
||||
self.history_by_block_number(block_number)
|
||||
}
|
||||
|
||||
/// Storage provider for state at that given block number
|
||||
pub fn history_by_block_number<'a>(
|
||||
&'a self,
|
||||
mut block_number: BlockNumber,
|
||||
) -> ProviderResult<Box<dyn StateProvider + 'a>> {
|
||||
if block_number == self.best_block_number().unwrap_or_default() &&
|
||||
block_number == self.last_block_number().unwrap_or_default()
|
||||
{
|
||||
@@ -316,8 +324,8 @@ impl<TX: DbTx + 'static, N: NodeTypes> DatabaseProvider<TX, N> {
|
||||
let storage_history_prune_checkpoint =
|
||||
self.get_prune_checkpoint(PruneSegment::StorageHistory)?;
|
||||
|
||||
let mut state_provider = HistoricalStateProviderRef::new(self, block_number);
|
||||
|
||||
let mut state_provider =
|
||||
HistoricalStateProviderRef::new(self, block_number, self.changeset_cache.clone());
|
||||
// If we pruned account or storage history, we can't return state on every historical block.
|
||||
// Instead, we should cap it at the latest prune checkpoint for corresponding prune segment.
|
||||
if let Some(prune_checkpoint_block_number) =
|
||||
@@ -933,8 +941,9 @@ impl<TX: DbTx + 'static, N: NodeTypes> TryIntoHistoricalStateProvider for Databa
|
||||
self.get_prune_checkpoint(PruneSegment::AccountHistory)?;
|
||||
let storage_history_prune_checkpoint =
|
||||
self.get_prune_checkpoint(PruneSegment::StorageHistory)?;
|
||||
let changeset_cache = self.changeset_cache.clone();
|
||||
|
||||
let mut state_provider = HistoricalStateProvider::new(self, block_number);
|
||||
let mut state_provider = HistoricalStateProvider::new(self, block_number, changeset_cache);
|
||||
|
||||
// If we pruned account or storage history, we can't return state on every historical block.
|
||||
// Instead, we should cap it at the latest prune checkpoint for corresponding prune segment.
|
||||
@@ -1245,19 +1254,15 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
}
|
||||
|
||||
/// Populate a [`BundleStateInit`] and [`RevertsInit`] using cursors over the
|
||||
/// [`PlainAccountState`] and [`PlainStorageState`] tables, based on the given storage and
|
||||
/// account changesets.
|
||||
fn populate_bundle_state<A, S>(
|
||||
/// [`tables::PlainAccountState`] and [`tables::PlainStorageState`] tables, based on the given
|
||||
/// storage and account changesets.
|
||||
fn populate_bundle_state(
|
||||
&self,
|
||||
account_changeset: Vec<(u64, AccountBeforeTx)>,
|
||||
storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>,
|
||||
plain_accounts_cursor: &mut A,
|
||||
plain_storage_cursor: &mut S,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)>
|
||||
where
|
||||
A: DbCursorRO<PlainAccountState>,
|
||||
S: DbDupCursorRO<PlainStorageState>,
|
||||
{
|
||||
mut get_account: impl FnMut(Address) -> ProviderResult<Option<Account>>,
|
||||
mut get_storage: impl FnMut(Address, StorageKey) -> ProviderResult<Option<StorageValue>>,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)> {
|
||||
// iterate previous value and get plain state value to create changeset
|
||||
// Double option around Account represent if Account state is know (first option) and
|
||||
// account is removed (Second Option)
|
||||
@@ -1275,7 +1280,7 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
let AccountBeforeTx { info: old_info, address } = account_before;
|
||||
match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let new_info = plain_accounts_cursor.seek_exact(address)?.map(|kv| kv.1);
|
||||
let new_info = get_account(address)?;
|
||||
entry.insert((old_info, new_info, HashMap::default()));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
@@ -1293,7 +1298,7 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
// get account state or insert from plain state.
|
||||
let account_state = match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let present_info = plain_accounts_cursor.seek_exact(address)?.map(|kv| kv.1);
|
||||
let present_info = get_account(address)?;
|
||||
entry.insert((present_info, present_info, HashMap::default()))
|
||||
}
|
||||
hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
||||
@@ -1302,11 +1307,8 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
// match storage.
|
||||
match account_state.2.entry(old_storage.key) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let new_storage = plain_storage_cursor
|
||||
.seek_by_key_subkey(address, old_storage.key)?
|
||||
.filter(|storage| storage.key == old_storage.key)
|
||||
.unwrap_or_default();
|
||||
entry.insert((old_storage.value, new_storage.value));
|
||||
let new_storage = get_storage(address, old_storage.key)?.unwrap_or_default();
|
||||
entry.insert((old_storage.value, new_storage));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().0 = old_storage.value;
|
||||
@@ -1325,6 +1327,28 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
Ok((state, reverts))
|
||||
}
|
||||
|
||||
/// Invokes [`populate_bundle_state`](Self::populate_bundle_state) with the given plain state
|
||||
/// cursors.
|
||||
fn populate_bundle_state_plain(
|
||||
&self,
|
||||
account_changeset: Vec<(u64, AccountBeforeTx)>,
|
||||
storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>,
|
||||
plain_accounts_cursor: &mut impl DbCursorRO<tables::PlainAccountState>,
|
||||
plain_storage_cursor: &mut impl DbDupCursorRO<tables::PlainStorageState>,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)> {
|
||||
self.populate_bundle_state(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
|address| Ok(plain_accounts_cursor.seek_exact(address)?.map(|kv| kv.1)),
|
||||
|address, storage_key| {
|
||||
Ok(plain_storage_cursor
|
||||
.seek_by_key_subkey(address, storage_key)?
|
||||
.filter(|s| s.key == storage_key)
|
||||
.map(|s| s.value))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`populate_bundle_state`](Self::populate_bundle_state), but reads current values from
|
||||
/// `HashedAccounts`/`HashedStorages`. Addresses and storage keys are hashed via `keccak256`
|
||||
/// for DB lookups. The output `BundleStateInit`/`RevertsInit` structures remain keyed by
|
||||
@@ -1336,65 +1360,32 @@ impl<TX: DbTx + 'static, N: NodeTypesForProvider> DatabaseProvider<TX, N> {
|
||||
hashed_accounts_cursor: &mut impl DbCursorRO<tables::HashedAccounts>,
|
||||
hashed_storage_cursor: &mut impl DbDupCursorRO<tables::HashedStorages>,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)> {
|
||||
let mut state: BundleStateInit = HashMap::default();
|
||||
let mut reverts: RevertsInit = HashMap::default();
|
||||
self.populate_bundle_state(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
|address| Ok(hashed_accounts_cursor.seek_exact(keccak256(address))?.map(|kv| kv.1)),
|
||||
|address, storage_key| {
|
||||
let hashed_storage_key = keccak256(storage_key);
|
||||
Ok(hashed_storage_cursor
|
||||
.seek_by_key_subkey(keccak256(address), hashed_storage_key)?
|
||||
.filter(|s| s.key == hashed_storage_key)
|
||||
.map(|s| s.value))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// add account changeset changes
|
||||
for (block_number, account_before) in account_changeset.into_iter().rev() {
|
||||
let AccountBeforeTx { info: old_info, address } = account_before;
|
||||
match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let hashed_address = keccak256(address);
|
||||
let new_info =
|
||||
hashed_accounts_cursor.seek_exact(hashed_address)?.map(|kv| kv.1);
|
||||
entry.insert((old_info, new_info, HashMap::default()));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().0 = old_info;
|
||||
}
|
||||
}
|
||||
reverts.entry(block_number).or_default().entry(address).or_default().0 = Some(old_info);
|
||||
}
|
||||
|
||||
// add storage changeset changes
|
||||
for (block_and_address, old_storage) in storage_changeset.into_iter().rev() {
|
||||
let BlockNumberAddress((block_number, address)) = block_and_address;
|
||||
let account_state = match state.entry(address) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let hashed_address = keccak256(address);
|
||||
let present_info =
|
||||
hashed_accounts_cursor.seek_exact(hashed_address)?.map(|kv| kv.1);
|
||||
entry.insert((present_info, present_info, HashMap::default()))
|
||||
}
|
||||
hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
||||
};
|
||||
|
||||
// Storage keys in changesets are plain; hash them for HashedStorages lookup.
|
||||
let hashed_storage_key = keccak256(old_storage.key);
|
||||
match account_state.2.entry(old_storage.key) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let hashed_address = keccak256(address);
|
||||
let new_storage = hashed_storage_cursor
|
||||
.seek_by_key_subkey(hashed_address, hashed_storage_key)?
|
||||
.filter(|storage| storage.key == hashed_storage_key)
|
||||
.unwrap_or_default();
|
||||
entry.insert((old_storage.value, new_storage.value));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().0 = old_storage.value;
|
||||
}
|
||||
};
|
||||
|
||||
reverts
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.entry(address)
|
||||
.or_default()
|
||||
.1
|
||||
.push(old_storage);
|
||||
}
|
||||
|
||||
Ok((state, reverts))
|
||||
fn populate_bundle_state_with_provider(
|
||||
&self,
|
||||
account_changeset: Vec<(u64, AccountBeforeTx)>,
|
||||
storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>,
|
||||
state_provider: impl StateProvider,
|
||||
) -> ProviderResult<(BundleStateInit, RevertsInit)> {
|
||||
self.populate_bundle_state(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
|address| state_provider.basic_account(&address),
|
||||
|address, storage_key| state_provider.storage(address, storage_key),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1659,6 +1650,43 @@ impl<TX: DbTx, N: NodeTypes> ChangeSetReader for DatabaseProvider<TX, N> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tx: DbTx + 'static, N: NodeTypesForProvider> StateReader for DatabaseProvider<Tx, N> {
|
||||
type Receipt = ReceiptTy<N>;
|
||||
|
||||
fn get_state(
|
||||
&self,
|
||||
block: BlockNumber,
|
||||
) -> ProviderResult<Option<ExecutionOutcome<Self::Receipt>>> {
|
||||
let Some(block_body) = self.block_body_indices(block)? else { return Ok(None) };
|
||||
|
||||
let from_transaction_num = block_body.first_tx_num();
|
||||
let to_transaction_num = block_body.last_tx_num();
|
||||
|
||||
let account_changeset = self.account_changesets_range(block..=block)?;
|
||||
let storage_changeset = self.storage_changeset(block)?;
|
||||
|
||||
let Some(block_hash) = self.block_hash(block)? else { return Ok(None) };
|
||||
let state_provider = self.history_by_block_hash(block_hash)?;
|
||||
let (state, reverts) = self.populate_bundle_state_with_provider(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
state_provider,
|
||||
)?;
|
||||
|
||||
let receipts = self.receipts_by_tx_range(from_transaction_num..=to_transaction_num)?;
|
||||
|
||||
Ok(Some(ExecutionOutcome::new_init(
|
||||
state,
|
||||
reverts,
|
||||
// We skip new contracts since we never delete them from the database
|
||||
Vec::new(),
|
||||
vec![receipts],
|
||||
block,
|
||||
Vec::new(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<TX: DbTx + 'static, N: NodeTypesForProvider> HeaderSyncGapProvider
|
||||
for DatabaseProvider<TX, N>
|
||||
{
|
||||
@@ -2793,7 +2821,7 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypesForProvider> StateWriter
|
||||
let mut plain_storage_cursor =
|
||||
self.tx.cursor_dup_write::<tables::PlainStorageState>()?;
|
||||
|
||||
let (state, _) = self.populate_bundle_state(
|
||||
let (state, _) = self.populate_bundle_state_plain(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
&mut plain_accounts_cursor,
|
||||
@@ -2958,7 +2986,7 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypesForProvider> StateWriter
|
||||
let mut plain_storage_cursor =
|
||||
self.tx.cursor_dup_write::<tables::PlainStorageState>()?;
|
||||
|
||||
let (state, reverts) = self.populate_bundle_state(
|
||||
let (state, reverts) = self.populate_bundle_state_plain(
|
||||
account_changeset,
|
||||
storage_changeset,
|
||||
&mut plain_accounts_cursor,
|
||||
@@ -3941,7 +3969,6 @@ mod tests {
|
||||
#[test]
|
||||
fn unwind_commit_waits_for_pre_commit_readers() {
|
||||
let factory = create_test_provider_factory();
|
||||
factory.set_storage_settings_cache(StorageSettings::v2());
|
||||
|
||||
let reader = factory.provider().unwrap();
|
||||
let provider_rw = factory.unwind_provider_rw().unwrap();
|
||||
@@ -4951,7 +4978,9 @@ mod tests {
|
||||
assert_eq!(account_cs[0].address, address);
|
||||
|
||||
let historical_value =
|
||||
HistoricalStateProviderRef::new(&*provider_rw, 0).storage(address, slot_key).unwrap();
|
||||
HistoricalStateProviderRef::new(&*provider_rw, 0, ChangesetCache::new())
|
||||
.storage(address, slot_key)
|
||||
.unwrap();
|
||||
assert_eq!(historical_value, None);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub use state::{
|
||||
HistoricalStateProviderRef, HistoryInfo, LowestAvailableBlocks,
|
||||
},
|
||||
latest::{LatestStateProvider, LatestStateProviderRef},
|
||||
overlay::{OverlayStateProvider, OverlayStateProviderFactory},
|
||||
overlay::{OverlayBuilder, OverlayStateProvider, OverlayStateProviderFactory},
|
||||
};
|
||||
|
||||
mod consistent_view;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::overlay::{Overlay, OverlayBuilder, OverlaySource};
|
||||
use crate::{
|
||||
AccountReader, BlockHashReader, ChangeSetReader, EitherReader, HashedPostStateProvider,
|
||||
ProviderError, RocksDBProviderFactory, StateProvider, StateRootProvider,
|
||||
@@ -13,8 +14,9 @@ use reth_db_api::{
|
||||
};
|
||||
use reth_primitives_traits::{Account, Bytecode};
|
||||
use reth_storage_api::{
|
||||
BlockNumReader, BytecodeReader, DBProvider, NodePrimitivesProvider, StateProofProvider,
|
||||
StorageChangeSetReader, StorageRootProvider, StorageSettingsCache,
|
||||
BlockNumReader, BytecodeReader, DBProvider, NodePrimitivesProvider, PruneCheckpointReader,
|
||||
StageCheckpointReader, StateProofProvider, StorageChangeSetReader, StorageRootProvider,
|
||||
StorageSettingsCache,
|
||||
};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
use reth_trie::{
|
||||
@@ -23,16 +25,15 @@ use reth_trie::{
|
||||
trie_cursor::InMemoryTrieCursorFactory,
|
||||
updates::TrieUpdates,
|
||||
witness::TrieWitness,
|
||||
AccountProof, ExecutionWitnessMode, HashedPostState, HashedPostStateSorted, HashedStorage,
|
||||
KeccakKeyHasher, MultiProof, MultiProofTargets, StateRoot, StorageMultiProof, StorageRoot,
|
||||
TrieInput, TrieInputSorted,
|
||||
AccountProof, ExecutionWitnessMode, HashedPostState, HashedStorage, KeccakKeyHasher,
|
||||
MultiProof, MultiProofTargets, StateRoot, StorageMultiProof, StorageRoot, TrieInput,
|
||||
TrieInputSorted,
|
||||
};
|
||||
use reth_trie_db::{
|
||||
hashed_storage_from_reverts_with_provider, DatabaseProof, DatabaseStateRoot,
|
||||
DatabaseStorageProof, DatabaseStorageRoot,
|
||||
ChangesetCache, DatabaseProof, DatabaseStateRoot, DatabaseStorageProof, DatabaseStorageRoot,
|
||||
};
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
type DbStateRoot<'a, TX, A> = StateRoot<
|
||||
reth_trie_db::DatabaseTrieCursorFactory<&'a TX, A>,
|
||||
@@ -123,6 +124,8 @@ impl HistoryInfo {
|
||||
pub struct HistoricalStateProviderRef<'b, Provider> {
|
||||
/// Database provider
|
||||
provider: &'b Provider,
|
||||
/// Changeset cache handle for retrieving trie changesets.
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Block number is main index for the history state of accounts and storages.
|
||||
block_number: BlockNumber,
|
||||
/// Lowest blocks at which different parts of the state are available.
|
||||
@@ -133,8 +136,17 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
HistoricalStateProviderRef<'b, Provider>
|
||||
{
|
||||
/// Create new `StateProvider` for historical block number
|
||||
pub fn new(provider: &'b Provider, block_number: BlockNumber) -> Self {
|
||||
Self { provider, block_number, lowest_available_blocks: Default::default() }
|
||||
pub fn new(
|
||||
provider: &'b Provider,
|
||||
block_number: BlockNumber,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
changeset_cache,
|
||||
block_number,
|
||||
lowest_available_blocks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `StateProvider` for historical block number and lowest block numbers at which
|
||||
@@ -143,8 +155,9 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
provider: &'b Provider,
|
||||
block_number: BlockNumber,
|
||||
lowest_available_blocks: LowestAvailableBlocks,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self {
|
||||
Self { provider, block_number, lowest_available_blocks }
|
||||
Self { provider, changeset_cache, block_number, lowest_available_blocks }
|
||||
}
|
||||
|
||||
/// Lookup an account in the `AccountsHistory` table using `EitherReader`.
|
||||
@@ -253,17 +266,11 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
Ok(tip.saturating_sub(self.block_number) > limit)
|
||||
}
|
||||
|
||||
/// Retrieve revert hashed state for this history provider.
|
||||
fn revert_state(&self) -> ProviderResult<HashedPostStateSorted>
|
||||
fn build_overlay(&self, input: TrieInputSorted) -> ProviderResult<TrieInputSorted>
|
||||
where
|
||||
Provider: StorageSettingsCache,
|
||||
Provider:
|
||||
BlockHashReader + PruneCheckpointReader + StageCheckpointReader + StorageSettingsCache,
|
||||
{
|
||||
if !self.lowest_available_blocks.is_account_history_available(self.block_number) ||
|
||||
!self.lowest_available_blocks.is_storage_history_available(self.block_number)
|
||||
{
|
||||
return Err(ProviderError::StateAtBlockPruned(self.block_number))
|
||||
}
|
||||
|
||||
if self.check_distance_against_limit(EPOCH_SLOTS)? {
|
||||
tracing::warn!(
|
||||
target: "providers::historical_sp",
|
||||
@@ -272,27 +279,22 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block
|
||||
);
|
||||
}
|
||||
|
||||
reth_trie_db::from_reverts_auto(self.provider, self.block_number..)
|
||||
}
|
||||
// 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
|
||||
.provider
|
||||
.block_hash(target_block)?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(target_block.into()))?;
|
||||
|
||||
/// Retrieve revert hashed storage for this history provider and target address.
|
||||
fn revert_storage(&self, address: Address) -> ProviderResult<HashedStorage>
|
||||
where
|
||||
Provider: StorageSettingsCache,
|
||||
{
|
||||
if !self.lowest_available_blocks.is_storage_history_available(self.block_number) {
|
||||
return Err(ProviderError::StateAtBlockPruned(self.block_number))
|
||||
}
|
||||
let TrieInputSorted { nodes, state, prefix_sets } = input;
|
||||
let overlay_builder = OverlayBuilder::new(self.changeset_cache.clone())
|
||||
.with_block_hash(Some(block_hash))
|
||||
.with_overlay_source(Some(OverlaySource::Immediate { trie: nodes, state }));
|
||||
let Overlay { trie_updates, hashed_post_state } =
|
||||
overlay_builder.build_overlay(self.provider)?;
|
||||
|
||||
if self.check_distance_against_limit(EPOCH_SLOTS * 10)? {
|
||||
tracing::warn!(
|
||||
target: "providers::historical_sp",
|
||||
target = self.block_number,
|
||||
"Attempt to calculate storage root for an old block might result in OOM"
|
||||
);
|
||||
}
|
||||
|
||||
hashed_storage_from_reverts_with_provider(self.provider, address, self.block_number)
|
||||
Ok(TrieInputSorted::new(trie_updates, hashed_post_state, prefix_sets))
|
||||
}
|
||||
|
||||
/// Set the lowest block number at which the account history is available.
|
||||
@@ -378,26 +380,25 @@ impl<
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StateRootProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
{
|
||||
fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut revert_state = self.revert_state()?;
|
||||
let hashed_state_sorted = hashed_state.into_sorted();
|
||||
revert_state.extend_ref_and_sort(&hashed_state_sorted);
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root(self.tx(), &revert_state)?)
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(
|
||||
TrieInput::from_state(hashed_state),
|
||||
))?;
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes(self.tx(), input)?)
|
||||
})
|
||||
}
|
||||
|
||||
fn state_root_from_nodes(&self, input: TrieInput) -> ProviderResult<B256> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut input = input;
|
||||
input.prepend(self.revert_state()?.into());
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes(
|
||||
self.tx(),
|
||||
TrieInputSorted::from_unsorted(input),
|
||||
)?)
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(input))?;
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes(self.tx(), input)?)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -406,10 +407,10 @@ impl<
|
||||
hashed_state: HashedPostState,
|
||||
) -> ProviderResult<(B256, TrieUpdates)> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut revert_state = self.revert_state()?;
|
||||
let hashed_state_sorted = hashed_state.into_sorted();
|
||||
revert_state.extend_ref_and_sort(&hashed_state_sorted);
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_with_updates(self.tx(), &revert_state)?)
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(
|
||||
TrieInput::from_state(hashed_state),
|
||||
))?;
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes_with_updates(self.tx(), input)?)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -418,12 +419,8 @@ impl<
|
||||
input: TrieInput,
|
||||
) -> ProviderResult<(B256, TrieUpdates)> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut input = input;
|
||||
input.prepend(self.revert_state()?.into());
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes_with_updates(
|
||||
self.tx(),
|
||||
TrieInputSorted::from_unsorted(input),
|
||||
)?)
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(input))?;
|
||||
Ok(<DbStateRoot<'_, _, A>>::overlay_root_from_nodes_with_updates(self.tx(), input)?)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -433,6 +430,9 @@ impl<
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StorageRootProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
{
|
||||
@@ -442,9 +442,20 @@ impl<
|
||||
hashed_storage: HashedStorage,
|
||||
) -> ProviderResult<B256> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut revert_storage = self.revert_storage(address)?;
|
||||
revert_storage.extend(&hashed_storage);
|
||||
<DbStorageRoot<'_, _, A>>::overlay_root(self.tx(), address, revert_storage)
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(
|
||||
TrieInput::from_state(HashedPostState::from_hashed_storage(
|
||||
alloy_primitives::keccak256(address),
|
||||
hashed_storage,
|
||||
)),
|
||||
))?;
|
||||
let hashed_storage = input
|
||||
.state
|
||||
.account_storages()
|
||||
.get(&alloy_primitives::keccak256(address))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
<DbStorageRoot<'_, _, A>>::overlay_root(self.tx(), address, hashed_storage)
|
||||
.map_err(|err| ProviderError::Database(err.into()))
|
||||
})
|
||||
}
|
||||
@@ -456,13 +467,24 @@ impl<
|
||||
hashed_storage: HashedStorage,
|
||||
) -> ProviderResult<reth_trie::StorageProof> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut revert_storage = self.revert_storage(address)?;
|
||||
revert_storage.extend(&hashed_storage);
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(
|
||||
TrieInput::from_state(HashedPostState::from_hashed_storage(
|
||||
alloy_primitives::keccak256(address),
|
||||
hashed_storage,
|
||||
)),
|
||||
))?;
|
||||
let hashed_storage = input
|
||||
.state
|
||||
.account_storages()
|
||||
.get(&alloy_primitives::keccak256(address))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
<DbStorageProof<'_, _, A>>::overlay_storage_proof(
|
||||
self.tx(),
|
||||
address,
|
||||
slot,
|
||||
revert_storage,
|
||||
hashed_storage,
|
||||
)
|
||||
.map_err(ProviderError::from)
|
||||
})
|
||||
@@ -475,13 +497,24 @@ impl<
|
||||
hashed_storage: HashedStorage,
|
||||
) -> ProviderResult<StorageMultiProof> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut revert_storage = self.revert_storage(address)?;
|
||||
revert_storage.extend(&hashed_storage);
|
||||
let input = self.build_overlay(TrieInputSorted::from_unsorted(
|
||||
TrieInput::from_state(HashedPostState::from_hashed_storage(
|
||||
alloy_primitives::keccak256(address),
|
||||
hashed_storage,
|
||||
)),
|
||||
))?;
|
||||
let hashed_storage = input
|
||||
.state
|
||||
.account_storages()
|
||||
.get(&alloy_primitives::keccak256(address))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
<DbStorageProof<'_, _, A>>::overlay_storage_multiproof(
|
||||
self.tx(),
|
||||
address,
|
||||
slots,
|
||||
revert_storage,
|
||||
hashed_storage,
|
||||
)
|
||||
.map_err(ProviderError::from)
|
||||
})
|
||||
@@ -493,6 +526,9 @@ impl<
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ BlockNumReader
|
||||
+ BlockHashReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
> StateProofProvider for HistoricalStateProviderRef<'_, Provider>
|
||||
{
|
||||
@@ -504,8 +540,13 @@ impl<
|
||||
slots: &[B256],
|
||||
) -> ProviderResult<AccountProof> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut input = input;
|
||||
input.prepend(self.revert_state()?.into());
|
||||
let TrieInputSorted { nodes, state, prefix_sets } =
|
||||
self.build_overlay(TrieInputSorted::from_unsorted(input))?;
|
||||
let input = TrieInput::new(
|
||||
Arc::unwrap_or_clone(nodes).into(),
|
||||
Arc::unwrap_or_clone(state).into(),
|
||||
prefix_sets,
|
||||
);
|
||||
let proof = <DbProof<'_, _, A> as DatabaseProof>::from_tx(self.tx());
|
||||
proof.overlay_account_proof(input, address, slots).map_err(ProviderError::from)
|
||||
})
|
||||
@@ -517,8 +558,13 @@ impl<
|
||||
targets: MultiProofTargets,
|
||||
) -> ProviderResult<MultiProof> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut input = input;
|
||||
input.prepend(self.revert_state()?.into());
|
||||
let TrieInputSorted { nodes, state, prefix_sets } =
|
||||
self.build_overlay(TrieInputSorted::from_unsorted(input))?;
|
||||
let input = TrieInput::new(
|
||||
Arc::unwrap_or_clone(nodes).into(),
|
||||
Arc::unwrap_or_clone(state).into(),
|
||||
prefix_sets,
|
||||
);
|
||||
let proof = <DbProof<'_, _, A> as DatabaseProof>::from_tx(self.tx());
|
||||
proof.overlay_multiproof(input, targets).map_err(ProviderError::from)
|
||||
})
|
||||
@@ -531,21 +577,19 @@ impl<
|
||||
mode: ExecutionWitnessMode,
|
||||
) -> ProviderResult<Vec<Bytes>> {
|
||||
reth_trie_db::with_adapter!(self.provider, |A| {
|
||||
let mut input = input;
|
||||
input.prepend(self.revert_state()?.into());
|
||||
let nodes_sorted = input.nodes.into_sorted();
|
||||
let state_sorted = input.state.into_sorted();
|
||||
let TrieInputSorted { nodes, state, prefix_sets } =
|
||||
self.build_overlay(TrieInputSorted::from_unsorted(input))?;
|
||||
let witness = TrieWitness::new(
|
||||
InMemoryTrieCursorFactory::new(
|
||||
reth_trie_db::DatabaseTrieCursorFactory::<_, A>::new(self.tx()),
|
||||
&nodes_sorted,
|
||||
nodes.as_ref(),
|
||||
),
|
||||
HashedPostStateCursorFactory::new(
|
||||
reth_trie_db::DatabaseHashedCursorFactory::new(self.tx()),
|
||||
&state_sorted,
|
||||
state.as_ref(),
|
||||
),
|
||||
)
|
||||
.with_prefix_sets_mut(input.prefix_sets)
|
||||
.with_prefix_sets_mut(prefix_sets)
|
||||
.with_execution_witness_mode(mode);
|
||||
let witness =
|
||||
if mode.is_canonical() { witness } else { witness.always_include_root_node() };
|
||||
@@ -572,6 +616,8 @@ impl<
|
||||
+ BlockHashReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider,
|
||||
@@ -602,6 +648,8 @@ impl<Provider: DBProvider + BlockNumReader> BytecodeReader
|
||||
pub struct HistoricalStateProvider<Provider> {
|
||||
/// Database provider.
|
||||
provider: Provider,
|
||||
/// Changeset cache handle for retrieving trie changesets.
|
||||
changeset_cache: ChangesetCache,
|
||||
/// State at the block number is the main indexer of the state.
|
||||
block_number: BlockNumber,
|
||||
/// Lowest blocks at which different parts of the state are available.
|
||||
@@ -612,8 +660,17 @@ impl<Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + BlockNumR
|
||||
HistoricalStateProvider<Provider>
|
||||
{
|
||||
/// Create new `StateProvider` for historical block number
|
||||
pub fn new(provider: Provider, block_number: BlockNumber) -> Self {
|
||||
Self { provider, block_number, lowest_available_blocks: Default::default() }
|
||||
pub fn new(
|
||||
provider: Provider,
|
||||
block_number: BlockNumber,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
changeset_cache,
|
||||
block_number,
|
||||
lowest_available_blocks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lowest block number at which the account history is available.
|
||||
@@ -636,17 +693,18 @@ impl<Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + BlockNumR
|
||||
|
||||
/// Returns a new provider that takes the `TX` as reference
|
||||
#[inline(always)]
|
||||
const fn as_ref(&self) -> HistoricalStateProviderRef<'_, Provider> {
|
||||
fn as_ref(&self) -> HistoricalStateProviderRef<'_, Provider> {
|
||||
HistoricalStateProviderRef::new_with_lowest_available_blocks(
|
||||
&self.provider,
|
||||
self.block_number,
|
||||
self.lowest_available_blocks,
|
||||
self.changeset_cache.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Delegates all provider impls to [HistoricalStateProviderRef]
|
||||
reth_storage_api::macros::delegate_provider_impls!(HistoricalStateProvider<Provider> where [Provider: DBProvider + BlockNumReader + BlockHashReader + ChangeSetReader + StorageChangeSetReader + StorageSettingsCache + RocksDBProviderFactory + NodePrimitivesProvider]);
|
||||
reth_storage_api::macros::delegate_provider_impls!(HistoricalStateProvider<Provider> where [Provider: DBProvider + BlockNumReader + BlockHashReader + ChangeSetReader + StorageChangeSetReader + PruneCheckpointReader + StageCheckpointReader + StorageSettingsCache + RocksDBProviderFactory + NodePrimitivesProvider]);
|
||||
|
||||
/// Lowest blocks at which different parts of the state are available.
|
||||
/// They may be [Some] if pruning is enabled.
|
||||
@@ -779,9 +837,11 @@ mod tests {
|
||||
use reth_primitives_traits::{Account, StorageEntry};
|
||||
use reth_storage_api::{
|
||||
BlockHashReader, BlockNumReader, ChangeSetReader, DBProvider, DatabaseProviderFactory,
|
||||
NodePrimitivesProvider, StorageChangeSetReader, StorageSettingsCache,
|
||||
NodePrimitivesProvider, PruneCheckpointReader, StageCheckpointReader,
|
||||
StorageChangeSetReader, StorageSettingsCache,
|
||||
};
|
||||
use reth_storage_errors::provider::ProviderError;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
|
||||
const ADDRESS: Address = address!("0x0000000000000000000000000000000000000001");
|
||||
const HIGHER_ADDRESS: Address = address!("0x0000000000000000000000000000000000000005");
|
||||
@@ -796,6 +856,8 @@ mod tests {
|
||||
+ BlockHashReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ PruneCheckpointReader
|
||||
+ StageCheckpointReader
|
||||
+ StorageSettingsCache
|
||||
+ RocksDBProviderFactory
|
||||
+ NodePrimitivesProvider,
|
||||
@@ -870,48 +932,49 @@ mod tests {
|
||||
|
||||
// run
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 1, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 2).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 2, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at3
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 3).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 3, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at3
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 4).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 4, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at7
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 7).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 7, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at7
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 9).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 9, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at10
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 10).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 10, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at10
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 11).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 11, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_at15
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 16).basic_account(&ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 16, ChangesetCache::new()).basic_account(&ADDRESS),
|
||||
Ok(Some(acc)) if acc == acc_plain
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1).basic_account(&HIGHER_ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 1, ChangesetCache::new())
|
||||
.basic_account(&HIGHER_ADDRESS),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1000).basic_account(&HIGHER_ADDRESS),
|
||||
HistoricalStateProviderRef::new(&db, 1000, ChangesetCache::new()).basic_account(&HIGHER_ADDRESS),
|
||||
Ok(Some(acc)) if acc == higher_acc_plain
|
||||
));
|
||||
}
|
||||
@@ -970,43 +1033,46 @@ mod tests {
|
||||
|
||||
// run
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 0).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 0, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 3).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 3, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(Some(U256::ZERO))
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 4).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 4, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at7.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 7).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 7, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at7.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 9).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 9, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at10.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 10).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 10, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at10.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 11).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 11, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at15.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 16).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 16, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_plain.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1, ChangesetCache::new())
|
||||
.storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1000).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1000, ChangesetCache::new()).storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == higher_entry_plain.value
|
||||
));
|
||||
}
|
||||
@@ -1025,6 +1091,7 @@ mod tests {
|
||||
account_history_block_number: Some(3),
|
||||
storage_history_block_number: Some(3),
|
||||
},
|
||||
ChangesetCache::new(),
|
||||
);
|
||||
assert!(matches!(
|
||||
provider.account_history_lookup(ADDRESS),
|
||||
@@ -1044,6 +1111,7 @@ mod tests {
|
||||
account_history_block_number: Some(2),
|
||||
storage_history_block_number: Some(2),
|
||||
},
|
||||
ChangesetCache::new(),
|
||||
);
|
||||
assert!(matches!(
|
||||
provider.account_history_lookup(ADDRESS),
|
||||
@@ -1063,6 +1131,7 @@ mod tests {
|
||||
account_history_block_number: Some(1),
|
||||
storage_history_block_number: Some(1),
|
||||
},
|
||||
ChangesetCache::new(),
|
||||
);
|
||||
assert!(matches!(
|
||||
provider.account_history_lookup(ADDRESS),
|
||||
@@ -1143,43 +1212,46 @@ mod tests {
|
||||
let db = factory.provider().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 0).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 0, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 3).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 3, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(Some(U256::ZERO))
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 4).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 4, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at7.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 7).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 7, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at7.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 9).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 9, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at10.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 10).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 10, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at10.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 11).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 11, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_at15.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 16).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 16, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == entry_plain.value
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1, ChangesetCache::new())
|
||||
.storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1000).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1000, ChangesetCache::new()).storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(Some(expected_value)) if expected_value == higher_entry_plain.value
|
||||
));
|
||||
}
|
||||
@@ -1283,43 +1355,46 @@ mod tests {
|
||||
let db = factory.provider().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 0).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 0, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 3).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 3, ChangesetCache::new())
|
||||
.storage(ADDRESS, STORAGE),
|
||||
Ok(Some(U256::ZERO))
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 4).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 4, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(7)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 7).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 7, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(7)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 9).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 9, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(10)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 10).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 10, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(10)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 11).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 11, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(15)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 16).storage(ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 16, ChangesetCache::new()).storage(ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(100)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1, ChangesetCache::new())
|
||||
.storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
HistoricalStateProviderRef::new(&db, 1000).storage(HIGHER_ADDRESS, STORAGE),
|
||||
HistoricalStateProviderRef::new(&db, 1000, ChangesetCache::new()).storage(HIGHER_ADDRESS, STORAGE),
|
||||
Ok(Some(v)) if v == U256::from(1000)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ pub(crate) struct OverlayStateProviderMetrics {
|
||||
|
||||
/// Contains all fields required to initialize an [`OverlayStateProvider`].
|
||||
#[derive(Debug, Clone)]
|
||||
struct Overlay {
|
||||
trie_updates: Arc<TrieUpdatesSorted>,
|
||||
hashed_post_state: Arc<HashedPostStateSorted>,
|
||||
pub(super) struct Overlay {
|
||||
pub(super) trie_updates: Arc<TrieUpdatesSorted>,
|
||||
pub(super) hashed_post_state: Arc<HashedPostStateSorted>,
|
||||
}
|
||||
|
||||
/// Source of overlay data for [`OverlayStateProviderFactory`].
|
||||
@@ -85,14 +85,12 @@ impl OverlaySource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating overlay state providers with optional reverts and overlays.
|
||||
/// Builder for calculating trie and hashed-state overlays.
|
||||
///
|
||||
/// 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.
|
||||
/// 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 OverlayStateProviderFactory<F> {
|
||||
/// The underlying database provider factory
|
||||
factory: F,
|
||||
pub struct OverlayBuilder {
|
||||
/// Optional block hash for collecting reverts
|
||||
block_hash: Option<B256>,
|
||||
/// Optional overlay source (lazy or immediate).
|
||||
@@ -101,21 +99,16 @@ pub struct OverlayStateProviderFactory<F> {
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Metrics for tracking provider operations
|
||||
metrics: OverlayStateProviderMetrics,
|
||||
/// A cache which maps `db_tip -> Overlay`. If the db tip changes during usage of the factory
|
||||
/// then a new entry will get added to this, but in most cases only one entry is present.
|
||||
overlay_cache: Arc<DashMap<BlockNumber, Overlay>>,
|
||||
}
|
||||
|
||||
impl<F> OverlayStateProviderFactory<F> {
|
||||
/// Create a new overlay state provider factory
|
||||
pub fn new(factory: F, changeset_cache: ChangesetCache) -> Self {
|
||||
impl OverlayBuilder {
|
||||
/// Create a new overlay builder.
|
||||
pub fn new(changeset_cache: ChangesetCache) -> Self {
|
||||
Self {
|
||||
factory,
|
||||
block_hash: None,
|
||||
overlay_source: None,
|
||||
changeset_cache,
|
||||
metrics: OverlayStateProviderMetrics::default(),
|
||||
overlay_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +124,6 @@ impl<F> OverlayStateProviderFactory<F> {
|
||||
/// 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 {
|
||||
self.overlay_source = source;
|
||||
// Clear the overlay cache since we've updated the source.
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -141,8 +132,6 @@ impl<F> OverlayStateProviderFactory<F> {
|
||||
/// Convenience method that wraps the lazy overlay in `OverlaySource::Lazy`.
|
||||
pub fn with_lazy_overlay(mut self, lazy_overlay: Option<LazyOverlay>) -> Self {
|
||||
self.overlay_source = lazy_overlay.map(OverlaySource::Lazy);
|
||||
// Clear the overlay cache since we've updated the source.
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -158,8 +147,6 @@ impl<F> OverlayStateProviderFactory<F> {
|
||||
trie: Arc::new(TrieUpdatesSorted::default()),
|
||||
state,
|
||||
});
|
||||
// Clear the overlay cache since we've updated the source.
|
||||
self.overlay_cache = Default::default();
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -186,23 +173,9 @@ impl<F> OverlayStateProviderFactory<F> {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Clear the overlay cache since we've updated the source.
|
||||
self.overlay_cache = Default::default();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> OverlayStateProviderFactory<F>
|
||||
where
|
||||
F: DatabaseProviderFactory,
|
||||
F::Provider: StageCheckpointReader
|
||||
+ PruneCheckpointReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ DBProvider
|
||||
+ BlockNumReader
|
||||
+ StorageSettingsCache,
|
||||
{
|
||||
/// Resolves the effective overlay (trie updates, hashed state).
|
||||
///
|
||||
/// If an overlay source is set, it is resolved (blocking if lazy).
|
||||
@@ -217,10 +190,13 @@ where
|
||||
}
|
||||
|
||||
/// Returns the block number for [`Self`]'s `block_hash` field, if any.
|
||||
fn get_requested_block_number(
|
||||
fn get_requested_block_number<Provider>(
|
||||
&self,
|
||||
provider: &F::Provider,
|
||||
) -> ProviderResult<Option<BlockNumber>> {
|
||||
provider: &Provider,
|
||||
) -> ProviderResult<Option<BlockNumber>>
|
||||
where
|
||||
Provider: BlockNumReader,
|
||||
{
|
||||
if let Some(block_hash) = self.block_hash {
|
||||
Ok(Some(
|
||||
provider
|
||||
@@ -234,7 +210,10 @@ where
|
||||
|
||||
/// Returns the block which is at the tip of the DB, i.e. the block which the state tables of
|
||||
/// the DB are currently synced to.
|
||||
fn get_db_tip_block_number(&self, provider: &F::Provider) -> ProviderResult<BlockNumber> {
|
||||
fn get_db_tip_block_number<Provider>(&self, provider: &Provider) -> ProviderResult<BlockNumber>
|
||||
where
|
||||
Provider: StageCheckpointReader,
|
||||
{
|
||||
provider
|
||||
.get_stage_checkpoint(StageId::Finish)?
|
||||
.as_ref()
|
||||
@@ -247,12 +226,15 @@ where
|
||||
///
|
||||
/// Takes into account both the stage checkpoint and the prune checkpoint to determine the
|
||||
/// available data range.
|
||||
fn reverts_required(
|
||||
fn reverts_required<Provider>(
|
||||
&self,
|
||||
provider: &F::Provider,
|
||||
provider: &Provider,
|
||||
db_tip_block: BlockNumber,
|
||||
requested_block: BlockNumber,
|
||||
) -> ProviderResult<bool> {
|
||||
) -> ProviderResult<bool>
|
||||
where
|
||||
Provider: 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 {
|
||||
@@ -288,11 +270,20 @@ where
|
||||
skip_all,
|
||||
fields(%db_tip_block)
|
||||
)]
|
||||
fn calculate_overlay(
|
||||
fn calculate_overlay<Provider>(
|
||||
&self,
|
||||
provider: &F::Provider,
|
||||
provider: &Provider,
|
||||
db_tip_block: BlockNumber,
|
||||
) -> ProviderResult<Overlay> {
|
||||
) -> ProviderResult<Overlay>
|
||||
where
|
||||
Provider: ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ DBProvider
|
||||
+ BlockNumReader
|
||||
+ StageCheckpointReader
|
||||
+ PruneCheckpointReader
|
||||
+ StorageSettingsCache,
|
||||
{
|
||||
//
|
||||
// Set up variables we'll use for recording metrics. There's two different code-paths here,
|
||||
// and we want to make sure both record metrics, so we do metrics recording after.
|
||||
@@ -404,23 +395,74 @@ where
|
||||
Ok(Overlay { trie_updates, hashed_post_state })
|
||||
}
|
||||
|
||||
/// Fetches an [`Overlay`] from the cache based on the current db tip block. If there is no
|
||||
/// cached value then this calculates the [`Overlay`] and populates the cache.
|
||||
/// Builds the effective overlay for the given provider.
|
||||
#[instrument(level = "debug", target = "providers::state::overlay", skip_all)]
|
||||
fn get_overlay(&self, provider: &F::Provider) -> ProviderResult<Overlay> {
|
||||
// No anchor block — just resolve the in-memory overlay directly.
|
||||
pub(super) fn build_overlay<Provider>(&self, provider: &Provider) -> ProviderResult<Overlay>
|
||||
where
|
||||
Provider: StageCheckpointReader
|
||||
+ PruneCheckpointReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ DBProvider
|
||||
+ 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating overlay state providers with optional reverts and overlays.
|
||||
///
|
||||
/// 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> {
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
impl<F> OverlayStateProviderFactory<F> {
|
||||
/// Create a new overlay state provider factory
|
||||
pub fn new(factory: F, overlay_builder: OverlayBuilder) -> 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.
|
||||
#[instrument(level = "debug", target = "providers::state::overlay", skip_all)]
|
||||
fn get_overlay<Provider>(&self, provider: &Provider) -> ProviderResult<Overlay>
|
||||
where
|
||||
Provider: StageCheckpointReader
|
||||
+ PruneCheckpointReader
|
||||
+ ChangeSetReader
|
||||
+ StorageChangeSetReader
|
||||
+ DBProvider
|
||||
+ 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 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.metrics.overlay_cache_misses.increment(1);
|
||||
let overlay = self.calculate_overlay(provider, db_tip_block)?;
|
||||
self.overlay_builder.metrics.overlay_cache_misses.increment(1);
|
||||
let overlay = self.overlay_builder.build_overlay(provider)?;
|
||||
entry.insert(overlay.clone());
|
||||
overlay
|
||||
}
|
||||
@@ -451,14 +493,14 @@ where
|
||||
let provider = {
|
||||
let start = Instant::now();
|
||||
let res = self.factory.database_provider_ro()?;
|
||||
self.metrics.create_provider_duration.record(start.elapsed());
|
||||
self.overlay_builder.metrics.create_provider_duration.record(start.elapsed());
|
||||
res
|
||||
};
|
||||
|
||||
let Overlay { trie_updates, hashed_post_state } = self.get_overlay(&provider)?;
|
||||
|
||||
let is_v2 = provider.cached_storage_settings().is_v2();
|
||||
self.metrics.database_provider_ro_duration.record(overall_start.elapsed());
|
||||
self.overlay_builder.metrics.database_provider_ro_duration.record(overall_start.elapsed());
|
||||
Ok(OverlayStateProvider::new(provider, trie_updates, hashed_post_state, is_v2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use reth_db::static_file::{
|
||||
use reth_db_api::table::{Decompress, Value};
|
||||
use reth_node_types::NodePrimitives;
|
||||
use reth_primitives_traits::{SealedHeader, SignedTransaction};
|
||||
use reth_static_file_types::{ChangesetOffset, ChangesetOffsetReader};
|
||||
use reth_static_file_types::ChangesetOffset;
|
||||
use reth_storage_api::range_size_hint;
|
||||
use reth_storage_errors::provider::{ProviderError, ProviderResult};
|
||||
use std::{
|
||||
@@ -111,15 +111,11 @@ impl<'a, N: NodePrimitives> StaticFileJarProvider<'a, N> {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let csoff_path = self.data_path().with_extension("csoff");
|
||||
if !csoff_path.exists() {
|
||||
return Ok(None);
|
||||
if let Some(reader) = self.jar.value().csoff_reader() {
|
||||
reader.get(index).map_err(ProviderError::other)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
let len = header.changeset_offsets_len();
|
||||
let mut reader =
|
||||
ChangesetOffsetReader::new(&csoff_path, len).map_err(ProviderError::other)?;
|
||||
reader.get(index).map_err(ProviderError::other)
|
||||
}
|
||||
|
||||
/// Reads all changeset offsets from the sidecar file.
|
||||
@@ -138,15 +134,12 @@ impl<'a, N: NodePrimitives> StaticFileJarProvider<'a, N> {
|
||||
return Ok(Some(Vec::new()));
|
||||
}
|
||||
|
||||
let csoff_path = self.data_path().with_extension("csoff");
|
||||
if !csoff_path.exists() {
|
||||
return Ok(None);
|
||||
if let Some(reader) = self.jar.value().csoff_reader() {
|
||||
let offsets = reader.get_range(0, len).map_err(ProviderError::other)?;
|
||||
Ok(Some(offsets))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
let mut reader =
|
||||
ChangesetOffsetReader::new(&csoff_path, len).map_err(ProviderError::other)?;
|
||||
let offsets = reader.get_range(0, len).map_err(ProviderError::other)?;
|
||||
Ok(Some(offsets))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ mod metrics;
|
||||
mod writer_tests;
|
||||
|
||||
use reth_nippy_jar::NippyJar;
|
||||
use reth_static_file_types::{SegmentHeader, StaticFileSegment};
|
||||
use reth_static_file_types::{ChangesetOffsetReader, SegmentHeader, StaticFileSegment};
|
||||
use reth_storage_errors::provider::{ProviderError, ProviderResult};
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
use std::{io, ops::Deref, sync::Arc};
|
||||
|
||||
/// Alias type for each specific `NippyJar`.
|
||||
type LoadedJarRef<'a> =
|
||||
@@ -29,6 +29,7 @@ type LoadedJarRef<'a> =
|
||||
pub struct LoadedJar {
|
||||
jar: NippyJar<SegmentHeader>,
|
||||
mmap_handle: Arc<reth_nippy_jar::DataReader>,
|
||||
csoff_reader: Option<ChangesetOffsetReader>,
|
||||
}
|
||||
|
||||
impl LoadedJar {
|
||||
@@ -36,7 +37,20 @@ impl LoadedJar {
|
||||
match jar.open_data_reader() {
|
||||
Ok(data_reader) => {
|
||||
let mmap_handle = Arc::new(data_reader);
|
||||
Ok(Self { jar, mmap_handle })
|
||||
|
||||
let csoff_reader = if jar.user_header().segment().is_change_based() {
|
||||
let csoff_path = jar.data_path().with_extension("csoff");
|
||||
let len = jar.user_header().changeset_offsets_len();
|
||||
match ChangesetOffsetReader::new(&csoff_path, len) {
|
||||
Ok(reader) => Some(reader),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound && len == 0 => None,
|
||||
Err(err) => return Err(ProviderError::other(err)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self { jar, mmap_handle, csoff_reader })
|
||||
}
|
||||
Err(e) => Err(ProviderError::other(e)),
|
||||
}
|
||||
@@ -55,6 +69,11 @@ impl LoadedJar {
|
||||
fn size(&self) -> usize {
|
||||
self.mmap_handle.size() + self.mmap_handle.offsets_size()
|
||||
}
|
||||
|
||||
/// Returns a reference to the cached changeset offset reader.
|
||||
const fn csoff_reader(&self) -> Option<&ChangesetOffsetReader> {
|
||||
self.csoff_reader.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for LoadedJar {
|
||||
|
||||
@@ -400,7 +400,7 @@ impl<N: NodePrimitives> StaticFileProviderRW<N> {
|
||||
|
||||
// Step 2: Validate sidecar offsets against actual NippyJar state
|
||||
let valid_blocks = if actual_sidecar_blocks > 0 {
|
||||
let mut reader = ChangesetOffsetReader::new(&csoff_path, actual_sidecar_blocks)
|
||||
let reader = ChangesetOffsetReader::new(&csoff_path, actual_sidecar_blocks)
|
||||
.map_err(ProviderError::other)?;
|
||||
|
||||
// Find last block where offset + num_changes <= actual_nippy_rows
|
||||
@@ -896,7 +896,7 @@ impl<N: NodePrimitives> StaticFileProviderRW<N> {
|
||||
// Read offset for the block after last_block from sidecar.
|
||||
// Use committed length from header, ignoring any uncommitted records
|
||||
// that may exist in the file after a crash.
|
||||
let mut reader = ChangesetOffsetReader::new(&csoff_path, changeset_offsets_len)
|
||||
let reader = ChangesetOffsetReader::new(&csoff_path, changeset_offsets_len)
|
||||
.map_err(ProviderError::other)?;
|
||||
if let Some(next_offset) = reader.get(blocks_to_keep).map_err(ProviderError::other)? {
|
||||
next_offset.offset()
|
||||
|
||||
@@ -614,7 +614,7 @@ mod tests {
|
||||
assert_eq!(get_header_block_count(&provider, 0), 5);
|
||||
|
||||
// Verify offsets are correct
|
||||
let mut reader = ChangesetOffsetReader::new(&sidecar_path, 5).unwrap();
|
||||
let reader = ChangesetOffsetReader::new(&sidecar_path, 5).unwrap();
|
||||
|
||||
let o0 = reader.get(0).unwrap().unwrap();
|
||||
assert_eq!(o0.offset(), 0);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
traits::{BlockSource, ReceiptProvider},
|
||||
AccountReader, BlockHashReader, BlockIdReader, BlockNumReader, BlockReader, BlockReaderIdExt,
|
||||
ChainSpecProvider, ChangeSetReader, HeaderProvider, PruneCheckpointReader,
|
||||
ReceiptProviderIdExt, StateProvider, StateProviderBox, StateProviderFactory, StateReader,
|
||||
StateRootProvider, TransactionVariant, TransactionsProvider,
|
||||
AccountReader, BalProvider, BalStoreHandle, BlockHashReader, BlockIdReader, BlockNumReader,
|
||||
BlockReader, BlockReaderIdExt, ChainSpecProvider, ChangeSetReader, HeaderProvider,
|
||||
PruneCheckpointReader, ReceiptProviderIdExt, StateProvider, StateProviderBox,
|
||||
StateProviderFactory, StateReader, StateRootProvider, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
use alloy_consensus::{
|
||||
constants::EMPTY_ROOT_HASH,
|
||||
@@ -68,6 +68,8 @@ pub struct MockEthProvider<T: NodePrimitives = EthPrimitives, ChainSpec = reth_c
|
||||
pub state_roots: Arc<Mutex<Vec<B256>>>,
|
||||
/// Local block body indices store
|
||||
pub block_body_indices: Arc<Mutex<HashMap<BlockNumber, StoredBlockBodyIndices>>>,
|
||||
/// Local BAL store handle
|
||||
pub bal_store: BalStoreHandle,
|
||||
tx: TxMock,
|
||||
prune_modes: Arc<PruneModes>,
|
||||
}
|
||||
@@ -85,6 +87,7 @@ where
|
||||
chain_spec: self.chain_spec.clone(),
|
||||
state_roots: self.state_roots.clone(),
|
||||
block_body_indices: self.block_body_indices.clone(),
|
||||
bal_store: self.bal_store.clone(),
|
||||
tx: self.tx.clone(),
|
||||
prune_modes: self.prune_modes.clone(),
|
||||
}
|
||||
@@ -102,6 +105,7 @@ impl<T: NodePrimitives> MockEthProvider<T, reth_chainspec::ChainSpec> {
|
||||
chain_spec: Arc::new(reth_chainspec::ChainSpecBuilder::mainnet().build()),
|
||||
state_roots: Default::default(),
|
||||
block_body_indices: Default::default(),
|
||||
bal_store: Default::default(),
|
||||
tx: Default::default(),
|
||||
prune_modes: Default::default(),
|
||||
}
|
||||
@@ -185,6 +189,7 @@ impl<T: NodePrimitives, ChainSpec> MockEthProvider<T, ChainSpec> {
|
||||
chain_spec: Arc::new(chain_spec),
|
||||
state_roots: self.state_roots,
|
||||
block_body_indices: self.block_body_indices,
|
||||
bal_store: self.bal_store,
|
||||
tx: self.tx,
|
||||
prune_modes: self.prune_modes,
|
||||
}
|
||||
@@ -212,6 +217,12 @@ impl Default for MockEthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: NodePrimitives, ChainSpec> BalProvider for MockEthProvider<T, ChainSpec> {
|
||||
fn bal_store(&self) -> &BalStoreHandle {
|
||||
&self.bal_store
|
||||
}
|
||||
}
|
||||
|
||||
/// An extended account for local store
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtendedAccount {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Helper provider traits to encapsulate all provider traits for simplicity.
|
||||
|
||||
use crate::{
|
||||
AccountReader, BlockReader, BlockReaderIdExt, ChainSpecProvider, ChangeSetReader,
|
||||
AccountReader, BalProvider, BlockReader, BlockReaderIdExt, ChainSpecProvider, ChangeSetReader,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, PruneCheckpointReader,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StateProviderFactory, StateReader,
|
||||
StaticFileProviderFactory,
|
||||
@@ -32,6 +32,7 @@ pub trait FullProvider<N: NodeTypesWithDB>:
|
||||
Receipt = ReceiptTy<N>,
|
||||
Header = HeaderTy<N>,
|
||||
> + AccountReader
|
||||
+ BalProvider
|
||||
+ StateProviderFactory
|
||||
+ StateReader
|
||||
+ HashedPostStateProvider
|
||||
@@ -67,6 +68,7 @@ impl<T, N: NodeTypesWithDB> FullProvider<N> for T where
|
||||
Receipt = ReceiptTy<N>,
|
||||
Header = HeaderTy<N>,
|
||||
> + AccountReader
|
||||
+ BalProvider
|
||||
+ StateProviderFactory
|
||||
+ StateReader
|
||||
+ HashedPostStateProvider
|
||||
|
||||
252
crates/storage/storage-api/src/bal.rs
Normal file
252
crates/storage/storage-api/src/bal.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use alloc::{sync::Arc, vec::Vec};
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// Store for Block Access Lists (BALs).
|
||||
///
|
||||
/// This abstraction intentionally does not prescribe where BALs live. Implementations may keep
|
||||
/// recent BALs in memory, read canonical BALs from static files, or compose multiple tiers behind
|
||||
/// a single interface.
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait BalStore: Send + Sync + 'static {
|
||||
/// Insert the BAL for the given block.
|
||||
fn insert(
|
||||
&self,
|
||||
block_hash: BlockHash,
|
||||
block_number: BlockNumber,
|
||||
bal: Bytes,
|
||||
) -> ProviderResult<()>;
|
||||
|
||||
/// Fetch BALs for the given block hashes.
|
||||
///
|
||||
/// The returned vector must align with `block_hashes`.
|
||||
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>>;
|
||||
|
||||
/// Fetch BAL response entries for the given block hashes, stopping after the soft limit is
|
||||
/// exceeded.
|
||||
///
|
||||
/// Entries are returned in request order. Unavailable BALs are represented as an RLP-encoded
|
||||
/// empty list (`0xc0`). The limit is soft: the entry that exceeds the limit is included.
|
||||
fn get_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
) -> ProviderResult<Vec<Bytes>> {
|
||||
let mut out = Vec::new();
|
||||
self.append_by_hashes_with_limit(block_hashes, limit, &mut out)?;
|
||||
out.shrink_to_fit();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extends the given vector with BAL response entries for the given hashes.
|
||||
///
|
||||
/// This adheres to the expected behavior of [`Self::get_by_hashes_with_limit`].
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut size = 0;
|
||||
for bal in self.get_by_hashes(block_hashes)? {
|
||||
let bal = bal.unwrap_or_else(|| Bytes::from_static(&[0xc0]));
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch BALs for the requested range.
|
||||
///
|
||||
/// Implementations may stop at the first gap and return the contiguous prefix.
|
||||
fn get_by_range(&self, start: BlockNumber, count: u64) -> ProviderResult<Vec<Bytes>>;
|
||||
}
|
||||
|
||||
/// The limit to enforce for [`BalStore::get_by_hashes_with_limit`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GetBlockAccessListLimit {
|
||||
/// No limit, return all BALs.
|
||||
None,
|
||||
/// Enforce a size limit on the returned BALs, for example 2MB.
|
||||
ResponseSizeSoftLimit(usize),
|
||||
}
|
||||
|
||||
impl GetBlockAccessListLimit {
|
||||
/// Returns true if the given size exceeds the limit.
|
||||
#[inline]
|
||||
pub const fn exceeds(&self, size: usize) -> bool {
|
||||
match self {
|
||||
Self::None => false,
|
||||
Self::ResponseSizeSoftLimit(limit) => size > *limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone-friendly façade around a BAL store implementation.
|
||||
#[derive(Clone)]
|
||||
pub struct BalStoreHandle {
|
||||
inner: Arc<dyn BalStore>,
|
||||
}
|
||||
|
||||
impl BalStoreHandle {
|
||||
/// Creates a new [`BalStoreHandle`] from the given implementation.
|
||||
pub fn new(inner: impl BalStore) -> Self {
|
||||
Self { inner: Arc::new(inner) }
|
||||
}
|
||||
|
||||
/// Creates a [`BalStoreHandle`] backed by [`NoopBalStore`].
|
||||
pub fn noop() -> Self {
|
||||
Self::new(NoopBalStore)
|
||||
}
|
||||
|
||||
/// Insert the BAL for the given block.
|
||||
#[inline]
|
||||
pub fn insert(
|
||||
&self,
|
||||
block_hash: BlockHash,
|
||||
block_number: BlockNumber,
|
||||
bal: Bytes,
|
||||
) -> ProviderResult<()> {
|
||||
self.inner.insert(block_hash, block_number, bal)
|
||||
}
|
||||
|
||||
/// Fetch BALs for the given block hashes.
|
||||
#[inline]
|
||||
pub fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
|
||||
self.inner.get_by_hashes(block_hashes)
|
||||
}
|
||||
|
||||
/// Fetch BAL response entries for the given block hashes, stopping after the soft limit is
|
||||
/// exceeded.
|
||||
#[inline]
|
||||
pub fn get_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
) -> ProviderResult<Vec<Bytes>> {
|
||||
self.inner.get_by_hashes_with_limit(block_hashes, limit)
|
||||
}
|
||||
|
||||
/// Extends the given vector with BAL response entries for the given hashes.
|
||||
#[inline]
|
||||
pub fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
self.inner.append_by_hashes_with_limit(block_hashes, limit, out)
|
||||
}
|
||||
|
||||
/// Fetch BALs for the requested range.
|
||||
#[inline]
|
||||
pub fn get_by_range(&self, start: BlockNumber, count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
self.inner.get_by_range(start, count)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BalStoreHandle {
|
||||
fn default() -> Self {
|
||||
Self::noop()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for BalStoreHandle {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("BalStoreHandle").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-side access to BAL storage.
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait BalProvider {
|
||||
/// Returns the configured BAL store handle.
|
||||
fn bal_store(&self) -> &BalStoreHandle;
|
||||
}
|
||||
|
||||
/// No-op BAL store used as the default wiring target until a concrete implementation is injected.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct NoopBalStore;
|
||||
|
||||
impl BalStore for NoopBalStore {
|
||||
fn insert(
|
||||
&self,
|
||||
_block_hash: BlockHash,
|
||||
_block_number: BlockNumber,
|
||||
_bal: Bytes,
|
||||
) -> ProviderResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
|
||||
Ok(block_hashes.iter().map(|_| None).collect())
|
||||
}
|
||||
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut size = 0;
|
||||
for _ in block_hashes {
|
||||
let bal = Bytes::from_static(&[0xc0]);
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_range(&self, _start: BlockNumber, _count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::B256;
|
||||
|
||||
#[test]
|
||||
fn noop_store_returns_empty_results() {
|
||||
let store = BalStoreHandle::default();
|
||||
let hashes = [B256::random(), B256::random()];
|
||||
|
||||
let by_hash = store.get_by_hashes(&hashes).unwrap();
|
||||
let by_range = store.get_by_range(1, 10).unwrap();
|
||||
|
||||
assert_eq!(by_hash, vec![None, None]);
|
||||
assert!(by_range.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noop_store_limited_lookup_returns_prefix() {
|
||||
let store = BalStoreHandle::default();
|
||||
let hashes = [B256::random(), B256::random(), B256::random()];
|
||||
|
||||
let limited = store
|
||||
.get_by_hashes_with_limit(&hashes, GetBlockAccessListLimit::ResponseSizeSoftLimit(1))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(limited, vec![Bytes::from_static(&[0xc0]), Bytes::from_static(&[0xc0])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_access_list_limit() {
|
||||
let limit_none = GetBlockAccessListLimit::None;
|
||||
assert!(!limit_none.exceeds(usize::MAX));
|
||||
|
||||
let size_limit_2mb = GetBlockAccessListLimit::ResponseSizeSoftLimit(2 * 1024 * 1024);
|
||||
assert!(!size_limit_2mb.exceeds(1024 * 1024));
|
||||
assert!(!size_limit_2mb.exceeds(2 * 1024 * 1024));
|
||||
assert!(size_limit_2mb.exceeds(3 * 1024 * 1024));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ extern crate alloc;
|
||||
|
||||
// Re-export used error types.
|
||||
pub use reth_storage_errors as errors;
|
||||
mod bal;
|
||||
pub use bal::*;
|
||||
|
||||
mod account;
|
||||
pub use account::*;
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//! Various noop implementations for traits.
|
||||
|
||||
pub use crate::bal::NoopBalStore;
|
||||
|
||||
use crate::{
|
||||
AccountReader, BlockBodyIndicesProvider, BlockHashReader, BlockIdReader, BlockNumReader,
|
||||
BlockReader, BlockReaderIdExt, BlockSource, BytecodeReader, ChangeSetReader,
|
||||
HashedPostStateProvider, HeaderProvider, NodePrimitivesProvider, PruneCheckpointReader,
|
||||
ReceiptProvider, ReceiptProviderIdExt, StageCheckpointReader, StateProofProvider,
|
||||
StateProvider, StateProviderBox, StateProviderFactory, StateReader, StateRootProvider,
|
||||
StorageRootProvider, TransactionVariant, TransactionsProvider,
|
||||
AccountReader, BalProvider, BalStoreHandle, BlockBodyIndicesProvider, BlockHashReader,
|
||||
BlockIdReader, BlockNumReader, BlockReader, BlockReaderIdExt, BlockSource, BytecodeReader,
|
||||
ChangeSetReader, HashedPostStateProvider, HeaderProvider, NodePrimitivesProvider,
|
||||
PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt, StageCheckpointReader,
|
||||
StateProofProvider, StateProvider, StateProviderBox, StateProviderFactory, StateReader,
|
||||
StateRootProvider, StorageRootProvider, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
|
||||
#[cfg(feature = "db-api")]
|
||||
@@ -44,6 +46,7 @@ use reth_trie_common::{
|
||||
#[non_exhaustive]
|
||||
pub struct NoopProvider<ChainSpec = reth_chainspec::ChainSpec, N = EthPrimitives> {
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
bal_store: BalStoreHandle,
|
||||
#[cfg(feature = "db-api")]
|
||||
tx: TxMock,
|
||||
#[cfg(feature = "db-api")]
|
||||
@@ -56,6 +59,7 @@ impl<ChainSpec, N> NoopProvider<ChainSpec, N> {
|
||||
pub fn new(chain_spec: Arc<ChainSpec>) -> Self {
|
||||
Self {
|
||||
chain_spec,
|
||||
bal_store: BalStoreHandle::default(),
|
||||
#[cfg(feature = "db-api")]
|
||||
tx: TxMock::default(),
|
||||
#[cfg(feature = "db-api")]
|
||||
@@ -70,6 +74,7 @@ impl<ChainSpec> NoopProvider<ChainSpec> {
|
||||
pub fn eth(chain_spec: Arc<ChainSpec>) -> Self {
|
||||
Self {
|
||||
chain_spec,
|
||||
bal_store: BalStoreHandle::default(),
|
||||
#[cfg(feature = "db-api")]
|
||||
tx: TxMock::default(),
|
||||
#[cfg(feature = "db-api")]
|
||||
@@ -96,6 +101,7 @@ impl<ChainSpec, N> Clone for NoopProvider<ChainSpec, N> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
chain_spec: Arc::clone(&self.chain_spec),
|
||||
bal_store: self.bal_store.clone(),
|
||||
#[cfg(feature = "db-api")]
|
||||
tx: self.tx.clone(),
|
||||
#[cfg(feature = "db-api")]
|
||||
@@ -105,6 +111,12 @@ impl<ChainSpec, N> Clone for NoopProvider<ChainSpec, N> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<ChainSpec, N> BalProvider for NoopProvider<ChainSpec, N> {
|
||||
fn bal_store(&self) -> &BalStoreHandle {
|
||||
&self.bal_store
|
||||
}
|
||||
}
|
||||
|
||||
/// Noop implementation for testing purposes
|
||||
impl<ChainSpec: Send + Sync, N: Send + Sync> BlockHashReader for NoopProvider<ChainSpec, N> {
|
||||
fn block_hash(&self, _number: u64) -> ProviderResult<Option<B256>> {
|
||||
|
||||
@@ -547,7 +547,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
ensure_intrinsic_gas(transaction, &self.fork_tracker)?;
|
||||
ensure_intrinsic_gas(transaction, &self.fork_tracker, block_gas_limit)?;
|
||||
|
||||
// light blob tx pre-checks
|
||||
if transaction.is_eip4844() {
|
||||
@@ -1404,6 +1404,7 @@ impl ForkTracker {
|
||||
pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
transaction: &T,
|
||||
fork_tracker: &ForkTracker,
|
||||
block_gas_limit: u64,
|
||||
) -> Result<(), InvalidPoolTransactionError> {
|
||||
use revm_primitives::hardfork::SpecId;
|
||||
let spec_id = if fork_tracker.is_prague_activated() {
|
||||
@@ -1424,6 +1425,7 @@ pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
.map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
|
||||
.unwrap_or_default() as u64,
|
||||
transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
|
||||
revm_primitives::eip8037::cost_per_state_byte(block_gas_limit),
|
||||
);
|
||||
|
||||
let gas_limit = transaction.gas_limit();
|
||||
@@ -1478,11 +1480,11 @@ mod tests {
|
||||
tx_gas_limit_cap: AtomicU64::new(0),
|
||||
};
|
||||
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker, 30_000_000);
|
||||
assert!(res.is_ok());
|
||||
|
||||
fork_tracker.shanghai = true.into();
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker, 30_000_000);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let provider = MockEthProvider::default().with_genesis_block();
|
||||
|
||||
@@ -17,7 +17,6 @@ reth-primitives-traits = { workspace = true, features = ["dashmap", "std"] }
|
||||
reth-execution-errors.workspace = true
|
||||
reth-provider.workspace = true
|
||||
reth-storage-errors.workspace = true
|
||||
reth-trie-sparse = { workspace = true, features = ["std"] }
|
||||
reth-tasks = { workspace = true, features = ["rayon"] }
|
||||
reth-trie.workspace = true
|
||||
|
||||
@@ -46,6 +45,7 @@ reth-metrics = { workspace = true, optional = true }
|
||||
metrics = { workspace = true, optional = true }
|
||||
|
||||
# `trie-debug` feature
|
||||
reth-trie-sparse = { workspace = true, optional = true, features = ["trie-debug"] }
|
||||
rand = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -63,13 +63,17 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["metrics"]
|
||||
metrics = ["reth-metrics", "dep:metrics", "reth-trie/metrics", "reth-trie-sparse/metrics"]
|
||||
trie-debug = ["dep:rand", "reth-trie-sparse/trie-debug"]
|
||||
metrics = ["reth-metrics", "dep:metrics", "reth-trie/metrics"]
|
||||
trie-debug = [
|
||||
"dep:rand",
|
||||
"dep:reth-trie-sparse",
|
||||
"reth-trie-sparse?/trie-debug",
|
||||
]
|
||||
test-utils = [
|
||||
"reth-primitives-traits/test-utils",
|
||||
"reth-provider/test-utils",
|
||||
"reth-trie-db/test-utils",
|
||||
"reth-trie-sparse/test-utils",
|
||||
"reth-trie/test-utils",
|
||||
"reth-tasks/test-utils",
|
||||
"reth-trie-sparse?/test-utils",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! # Architecture
|
||||
//!
|
||||
//! - **Worker Pools**: Pre-spawned workers with dedicated database transactions
|
||||
//! - Storage pool: Handles storage proofs and blinded storage node requests
|
||||
//! - Account pool: Handles account multiproofs and blinded account node requests
|
||||
//! - Storage pool: Handles storage proofs
|
||||
//! - Account pool: Handles account multiproofs
|
||||
//! - **Direct Channel Access**: `ProofWorkerHandle` provides type-safe queue methods with direct
|
||||
//! access to worker channels, eliminating routing overhead
|
||||
//! - **Automatic Shutdown**: Workers terminate gracefully when all handles are dropped
|
||||
@@ -38,26 +38,22 @@ use alloy_primitives::{
|
||||
B256, U256,
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
|
||||
use reth_execution_errors::{SparseTrieError, SparseTrieErrorKind, StateProofError};
|
||||
use reth_execution_errors::StateProofError;
|
||||
use reth_primitives_traits::{dashmap::DashMap, FastInstant as Instant};
|
||||
use reth_provider::{DatabaseProviderROFactory, ProviderError, ProviderResult};
|
||||
use reth_storage_errors::db::DatabaseError;
|
||||
use reth_tasks::Runtime;
|
||||
use reth_trie::{
|
||||
hashed_cursor::{HashedCursorFactory, HashedStorageCursor, InstrumentedHashedCursor},
|
||||
proof::{ProofBlindedAccountProvider, ProofBlindedStorageProvider},
|
||||
proof_v2,
|
||||
trie_cursor::{InstrumentedTrieCursor, TrieCursorFactory, TrieStorageCursor},
|
||||
DecodedMultiProofV2, HashedPostState, MultiProofTargetsV2, Nibbles, ProofTrieNodeV2,
|
||||
ProofV2Target,
|
||||
DecodedMultiProofV2, HashedPostState, MultiProofTargetsV2, ProofTrieNodeV2, ProofV2Target,
|
||||
};
|
||||
use reth_trie_sparse::provider::{RevealedNode, TrieNodeProvider, TrieNodeProviderFactory};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
@@ -69,8 +65,6 @@ use crate::proof_task_metrics::{
|
||||
ProofTaskCursorMetrics, ProofTaskCursorMetricsCache, ProofTaskTrieMetrics,
|
||||
};
|
||||
|
||||
type TrieNodeProviderResult = Result<Option<RevealedNode>, SparseTrieError>;
|
||||
|
||||
/// Type alias for the V2 account proof calculator with instrumented cursors.
|
||||
type V2AccountProofCalculator<'a, Provider> = proof_v2::ProofCalculator<
|
||||
InstrumentedTrieCursor<'a, <Provider as TrieCursorFactory>::AccountTrieCursor<'a>>,
|
||||
@@ -338,15 +332,13 @@ impl ProofWorkerHandle {
|
||||
self.storage_work_tx
|
||||
.send(StorageWorkerJob::StorageProof { input, proof_result_sender })
|
||||
.map_err(|err| {
|
||||
if let StorageWorkerJob::StorageProof { proof_result_sender, .. } = err.0 {
|
||||
let _ = proof_result_sender.send(StorageProofResultMessage {
|
||||
hashed_address,
|
||||
result: Err(DatabaseError::Other(
|
||||
"storage workers unavailable".to_string(),
|
||||
)
|
||||
.into()),
|
||||
});
|
||||
}
|
||||
let StorageWorkerJob::StorageProof { proof_result_sender, .. } = err.0;
|
||||
let _ = proof_result_sender.send(StorageProofResultMessage {
|
||||
hashed_address,
|
||||
result: Err(
|
||||
DatabaseError::Other("storage workers unavailable".to_string()).into()
|
||||
),
|
||||
});
|
||||
|
||||
ProviderError::other(std::io::Error::other("storage workers unavailable"))
|
||||
})
|
||||
@@ -365,51 +357,19 @@ impl ProofWorkerHandle {
|
||||
let error =
|
||||
ProviderError::other(std::io::Error::other("account workers unavailable"));
|
||||
|
||||
if let AccountWorkerJob::AccountMultiproof { input } = err.0 {
|
||||
let ProofResultContext { sender: result_tx, state, start_time: start } =
|
||||
input.into_proof_result_sender();
|
||||
let AccountWorkerJob::AccountMultiproof { input } = err.0;
|
||||
let ProofResultContext { sender: result_tx, state, start_time: start } =
|
||||
input.into_proof_result_sender();
|
||||
|
||||
let _ = result_tx.send(ProofResultMessage {
|
||||
result: Err(ParallelStateRootError::Provider(error.clone())),
|
||||
elapsed: start.elapsed(),
|
||||
state,
|
||||
});
|
||||
}
|
||||
let _ = result_tx.send(ProofResultMessage {
|
||||
result: Err(ParallelStateRootError::Provider(error.clone())),
|
||||
elapsed: start.elapsed(),
|
||||
state,
|
||||
});
|
||||
|
||||
error
|
||||
})
|
||||
}
|
||||
|
||||
/// Dispatch blinded storage node request to storage worker pool
|
||||
pub(crate) fn dispatch_blinded_storage_node(
|
||||
&self,
|
||||
account: B256,
|
||||
path: Nibbles,
|
||||
) -> Result<Receiver<TrieNodeProviderResult>, ProviderError> {
|
||||
let (tx, rx) = channel();
|
||||
self.storage_work_tx
|
||||
.send(StorageWorkerJob::BlindedStorageNode { account, path, result_sender: tx })
|
||||
.map_err(|_| {
|
||||
ProviderError::other(std::io::Error::other("storage workers unavailable"))
|
||||
})?;
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
/// Dispatch blinded account node request to account worker pool
|
||||
pub(crate) fn dispatch_blinded_account_node(
|
||||
&self,
|
||||
path: Nibbles,
|
||||
) -> Result<Receiver<TrieNodeProviderResult>, ProviderError> {
|
||||
let (tx, rx) = channel();
|
||||
self.account_work_tx
|
||||
.send(AccountWorkerJob::BlindedAccountNode { path, result_sender: tx })
|
||||
.map_err(|_| {
|
||||
ProviderError::other(std::io::Error::other("account workers unavailable"))
|
||||
})?;
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Data used for initializing cursor factories that is shared across all proof worker instances.
|
||||
@@ -502,67 +462,6 @@ where
|
||||
|
||||
Ok(StorageProofResult { proof, root })
|
||||
}
|
||||
|
||||
/// Process a blinded storage node request.
|
||||
///
|
||||
/// Used by storage workers to retrieve blinded storage trie nodes for proof construction.
|
||||
fn process_blinded_storage_node(
|
||||
&self,
|
||||
account: B256,
|
||||
path: &Nibbles,
|
||||
) -> TrieNodeProviderResult {
|
||||
let storage_node_provider =
|
||||
ProofBlindedStorageProvider::new(&self.provider, &self.provider, account);
|
||||
storage_node_provider.trie_node(path)
|
||||
}
|
||||
}
|
||||
impl TrieNodeProviderFactory for ProofWorkerHandle {
|
||||
type AccountNodeProvider = ProofTaskTrieNodeProvider;
|
||||
type StorageNodeProvider = ProofTaskTrieNodeProvider;
|
||||
|
||||
fn account_node_provider(&self) -> Self::AccountNodeProvider {
|
||||
ProofTaskTrieNodeProvider::AccountNode { handle: self.clone() }
|
||||
}
|
||||
|
||||
fn storage_node_provider(&self, account: B256) -> Self::StorageNodeProvider {
|
||||
ProofTaskTrieNodeProvider::StorageNode { account, handle: self.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Trie node provider for retrieving trie nodes by path.
|
||||
#[derive(Debug)]
|
||||
pub enum ProofTaskTrieNodeProvider {
|
||||
/// Blinded account trie node provider.
|
||||
AccountNode {
|
||||
/// Handle to the proof worker pools.
|
||||
handle: ProofWorkerHandle,
|
||||
},
|
||||
/// Blinded storage trie node provider.
|
||||
StorageNode {
|
||||
/// Target account.
|
||||
account: B256,
|
||||
/// Handle to the proof worker pools.
|
||||
handle: ProofWorkerHandle,
|
||||
},
|
||||
}
|
||||
|
||||
impl TrieNodeProvider for ProofTaskTrieNodeProvider {
|
||||
fn trie_node(&self, path: &Nibbles) -> Result<Option<RevealedNode>, SparseTrieError> {
|
||||
match self {
|
||||
Self::AccountNode { handle } => {
|
||||
let rx = handle
|
||||
.dispatch_blinded_account_node(*path)
|
||||
.map_err(|error| SparseTrieErrorKind::Other(Box::new(error)))?;
|
||||
rx.recv().map_err(|error| SparseTrieErrorKind::Other(Box::new(error)))?
|
||||
}
|
||||
Self::StorageNode { handle, account } => {
|
||||
let rx = handle
|
||||
.dispatch_blinded_storage_node(*account, *path)
|
||||
.map_err(|error| SparseTrieErrorKind::Other(Box::new(error)))?;
|
||||
rx.recv().map_err(|error| SparseTrieErrorKind::Other(Box::new(error)))?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel used by worker threads to deliver `ProofResultMessage` items back to
|
||||
@@ -647,21 +546,12 @@ pub(crate) enum StorageWorkerJob {
|
||||
/// Context for sending the proof result.
|
||||
proof_result_sender: CrossbeamSender<StorageProofResultMessage>,
|
||||
},
|
||||
/// Blinded storage node retrieval request
|
||||
BlindedStorageNode {
|
||||
/// Target account
|
||||
account: B256,
|
||||
/// Path to the storage node
|
||||
path: Nibbles,
|
||||
/// Channel to send result back to original caller
|
||||
result_sender: Sender<TrieNodeProviderResult>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Worker for storage trie operations.
|
||||
///
|
||||
/// Each worker maintains a dedicated database transaction and processes
|
||||
/// storage proof requests and blinded node lookups.
|
||||
/// storage proof requests.
|
||||
struct StorageProofWorker<Factory> {
|
||||
/// Shared task context with database factory and prefix sets
|
||||
task_ctx: ProofTaskCtx<Factory>,
|
||||
@@ -737,7 +627,6 @@ where
|
||||
);
|
||||
|
||||
let mut storage_proofs_processed = 0u64;
|
||||
let mut storage_nodes_processed = 0u64;
|
||||
let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default();
|
||||
let trie_cursor = proof_tx.provider.storage_trie_cursor(B256::ZERO)?;
|
||||
let hashed_cursor = proof_tx.provider.hashed_storage_cursor(B256::ZERO)?;
|
||||
@@ -787,17 +676,6 @@ where
|
||||
&mut storage_proofs_processed,
|
||||
);
|
||||
}
|
||||
|
||||
StorageWorkerJob::BlindedStorageNode { account, path, result_sender } => {
|
||||
Self::process_blinded_node(
|
||||
self.worker_id,
|
||||
&proof_tx,
|
||||
account,
|
||||
path,
|
||||
result_sender,
|
||||
&mut storage_nodes_processed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark worker as available again.
|
||||
@@ -813,14 +691,12 @@ where
|
||||
target: "trie::proof_task",
|
||||
worker_id = self.worker_id,
|
||||
storage_proofs_processed,
|
||||
storage_nodes_processed,
|
||||
total_idle_time_us = total_idle_time.as_micros(),
|
||||
"Storage worker shutting down"
|
||||
);
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
self.metrics.record_storage_nodes(storage_nodes_processed as usize);
|
||||
self.metrics.record_storage_worker_idle_time(total_idle_time);
|
||||
self.cursor_metrics.record(&mut cursor_metrics_cache);
|
||||
}
|
||||
@@ -883,59 +759,12 @@ where
|
||||
"Storage proof completed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Processes a blinded storage node lookup request.
|
||||
fn process_blinded_node<Provider>(
|
||||
worker_id: usize,
|
||||
proof_tx: &ProofTaskTx<Provider>,
|
||||
account: B256,
|
||||
path: Nibbles,
|
||||
result_sender: Sender<TrieNodeProviderResult>,
|
||||
storage_nodes_processed: &mut u64,
|
||||
) where
|
||||
Provider: TrieCursorFactory + HashedCursorFactory,
|
||||
{
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
worker_id,
|
||||
?account,
|
||||
?path,
|
||||
"Processing blinded storage node"
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let result = proof_tx.process_blinded_storage_node(account, &path);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
*storage_nodes_processed += 1;
|
||||
|
||||
if result_sender.send(result).is_err() {
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
worker_id,
|
||||
?account,
|
||||
?path,
|
||||
storage_nodes_processed,
|
||||
"Blinded storage node receiver dropped, discarding result"
|
||||
);
|
||||
}
|
||||
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
worker_id,
|
||||
?account,
|
||||
?path,
|
||||
elapsed_us = elapsed.as_micros(),
|
||||
total_processed = storage_nodes_processed,
|
||||
"Blinded storage node completed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker for account trie operations.
|
||||
///
|
||||
/// Each worker maintains a dedicated database transaction and processes
|
||||
/// account multiproof requests and blinded node lookups.
|
||||
/// account multiproof requests.
|
||||
struct AccountProofWorker<Factory> {
|
||||
/// Shared task context with database factory and prefix sets
|
||||
task_ctx: ProofTaskCtx<Factory>,
|
||||
@@ -1014,7 +843,6 @@ where
|
||||
);
|
||||
|
||||
let mut account_proofs_processed = 0u64;
|
||||
let mut account_nodes_processed = 0u64;
|
||||
let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default();
|
||||
|
||||
// Create both account and storage calculators for V2 proofs.
|
||||
@@ -1100,16 +928,6 @@ where
|
||||
total_idle_time += value_encoder_stats.storage_wait_time;
|
||||
value_encoder_stats_cache.extend(&value_encoder_stats);
|
||||
}
|
||||
|
||||
AccountWorkerJob::BlindedAccountNode { path, result_sender } => {
|
||||
Self::process_blinded_node(
|
||||
self.worker_id,
|
||||
&provider,
|
||||
path,
|
||||
result_sender,
|
||||
&mut account_nodes_processed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark worker as available again.
|
||||
@@ -1126,14 +944,12 @@ where
|
||||
target: "trie::proof_task",
|
||||
worker_id=self.worker_id,
|
||||
account_proofs_processed,
|
||||
account_nodes_processed,
|
||||
total_idle_time_us = total_idle_time.as_micros(),
|
||||
"Account worker shutting down"
|
||||
);
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
self.metrics.record_account_nodes(account_nodes_processed as usize);
|
||||
self.metrics.record_account_worker_idle_time(total_idle_time);
|
||||
self.cursor_metrics.record(&mut cursor_metrics_cache);
|
||||
self.metrics.record_value_encoder_stats(&value_encoder_stats_cache);
|
||||
@@ -1234,53 +1050,6 @@ where
|
||||
|
||||
value_encoder_stats
|
||||
}
|
||||
|
||||
/// Processes a blinded account node lookup request.
|
||||
fn process_blinded_node<Provider>(
|
||||
worker_id: usize,
|
||||
provider: &Provider,
|
||||
path: Nibbles,
|
||||
result_sender: Sender<TrieNodeProviderResult>,
|
||||
account_nodes_processed: &mut u64,
|
||||
) where
|
||||
Provider: TrieCursorFactory + HashedCursorFactory,
|
||||
{
|
||||
let span = debug_span!(
|
||||
target: "trie::proof_task",
|
||||
"Blinded account node calculation",
|
||||
?path,
|
||||
);
|
||||
let _span_guard = span.enter();
|
||||
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
"Processing blinded account node"
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let account_node_provider = ProofBlindedAccountProvider::new(provider, provider);
|
||||
let result = account_node_provider.trie_node(&path);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
*account_nodes_processed += 1;
|
||||
|
||||
if result_sender.send(result).is_err() {
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
worker_id,
|
||||
?path,
|
||||
account_nodes_processed,
|
||||
"Blinded account node receiver dropped, discarding result"
|
||||
);
|
||||
}
|
||||
|
||||
trace!(
|
||||
target: "trie::proof_task",
|
||||
node_time_us = elapsed.as_micros(),
|
||||
total_processed = account_nodes_processed,
|
||||
"Blinded account node completed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues V2 storage proofs for all accounts in the targets and returns receivers.
|
||||
@@ -1377,13 +1146,6 @@ enum AccountWorkerJob {
|
||||
/// Account multiproof input parameters
|
||||
input: Box<AccountMultiproofInput>,
|
||||
},
|
||||
/// Blinded account node retrieval request
|
||||
BlindedAccountNode {
|
||||
/// Path to the account node
|
||||
path: Nibbles,
|
||||
/// Channel to send result back to original caller
|
||||
result_sender: Sender<TrieNodeProviderResult>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1402,7 +1164,7 @@ mod tests {
|
||||
let changeset_cache = reth_trie_db::ChangesetCache::new();
|
||||
let factory = reth_provider::providers::OverlayStateProviderFactory::new(
|
||||
provider_factory,
|
||||
changeset_cache,
|
||||
reth_provider::providers::OverlayBuilder::new(changeset_cache),
|
||||
);
|
||||
let ctx = test_ctx(factory);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user