mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
66 Commits
t4
...
klkvr/debu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9042a5fb | ||
|
|
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 | ||
|
|
d58c6e3d07 | ||
|
|
d577814eb1 | ||
|
|
8b46f1a6d0 | ||
|
|
c527c2e7d6 | ||
|
|
14570f325a | ||
|
|
41fe41f2f2 | ||
|
|
27bfddeada | ||
|
|
981a7ef99b | ||
|
|
8c826a5cd0 | ||
|
|
6465997ea1 | ||
|
|
03a308da63 | ||
|
|
af84b982c3 | ||
|
|
77c3e86ec6 | ||
|
|
98ebc3454f | ||
|
|
c8979d0a1d | ||
|
|
742a7e7a18 | ||
|
|
99bf7a17c0 | ||
|
|
24436ca9f9 | ||
|
|
c26ec53d7d | ||
|
|
3a136fc8c3 | ||
|
|
d215d16a7d | ||
|
|
b36fff0ab8 | ||
|
|
e4d4ba30cb | ||
|
|
7c219fa955 | ||
|
|
0ac36468c6 | ||
|
|
93b2201c76 | ||
|
|
9990670990 | ||
|
|
1b69c9bb42 | ||
|
|
c2e649fc90 | ||
|
|
cff41bb9c2 | ||
|
|
0a9af7907f | ||
|
|
815d8407ce | ||
|
|
6cf6378e36 | ||
|
|
39f078e40f | ||
|
|
37a23ae169 | ||
|
|
8da8f3e4bc | ||
|
|
f97947b5a5 |
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 --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}"
|
||||
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
|
||||
|
||||
244
.github/scripts/fetch-grafana-dashboard.py
vendored
Normal file
244
.github/scripts/fetch-grafana-dashboard.py
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fetch a Grafana dashboard and convert it to the portable import format.
|
||||
|
||||
Fetches the dashboard via API, replaces internal datasource/variable references
|
||||
with template variables, and adds __inputs/__requires/__elements so the JSON is
|
||||
importable on any Grafana instance.
|
||||
|
||||
Usage:
|
||||
export FETCH_GRAFANA_DASHBOARD_URL=https://<NAMESPACE>.grafana.net
|
||||
export FETCH_GRAFANA_DASHBOARD_TOKEN=glsa_...
|
||||
|
||||
python3 .github/scripts/fetch-grafana-dashboard.py <dashboard-uid> > output.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
PANEL_TYPE_NAMES = {
|
||||
"bargauge": "Bar gauge",
|
||||
"gauge": "Gauge",
|
||||
"heatmap": "Heatmap",
|
||||
"piechart": "Pie chart",
|
||||
"stat": "Stat",
|
||||
"table": "Table",
|
||||
"timeseries": "Time series",
|
||||
"barchart": "Bar chart",
|
||||
"text": "Text",
|
||||
"dashlist": "Dashboard list",
|
||||
"logs": "Logs",
|
||||
"nodeGraph": "Node Graph",
|
||||
"histogram": "Histogram",
|
||||
"candlestick": "Candlestick",
|
||||
"state-timeline": "State timeline",
|
||||
"status-history": "Status history",
|
||||
"geomap": "Geomap",
|
||||
"canvas": "Canvas",
|
||||
"news": "News",
|
||||
"xychart": "XY Chart",
|
||||
"trend": "Trend",
|
||||
"datagrid": "Datagrid",
|
||||
"flamegraph": "Flame Graph",
|
||||
"traces": "Traces",
|
||||
}
|
||||
|
||||
|
||||
def fetch_json(base_url: str, token: str, path: str) -> dict:
|
||||
url = f"{base_url}{path}"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def fetch_dashboard(base_url: str, token: str, uid: str) -> dict:
|
||||
return fetch_json(base_url, token, f"/api/dashboards/uid/{uid}")
|
||||
|
||||
|
||||
def fetch_grafana_version(base_url: str) -> str:
|
||||
req = urllib.request.Request(f"{base_url}/api/health")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read())
|
||||
# version string like "13.0.0-23940615780.patch2" -> take just the semver part
|
||||
version = data.get("version", "")
|
||||
# strip build metadata after the first hyphen if it looks like a pre-release
|
||||
parts = version.split("-")
|
||||
return parts[0] if parts else version
|
||||
|
||||
|
||||
def collect_panel_types(panels: list) -> set[str]:
|
||||
types = set()
|
||||
for panel in panels:
|
||||
ptype = panel.get("type", "")
|
||||
if ptype and ptype != "row":
|
||||
types.add(ptype)
|
||||
# nested panels inside collapsed rows
|
||||
for sub in panel.get("panels", []):
|
||||
sub_type = sub.get("type", "")
|
||||
if sub_type and sub_type != "row":
|
||||
types.add(sub_type)
|
||||
return types
|
||||
|
||||
|
||||
def has_expression_datasource(dashboard: dict) -> bool:
|
||||
return "__expr__" in json.dumps(dashboard)
|
||||
|
||||
|
||||
def make_exportable(dashboard: dict, grafana_version: str = "") -> dict:
|
||||
dash = json.loads(json.dumps(dashboard)) # deep copy
|
||||
|
||||
# --- Strip internal fields ---
|
||||
dash.pop("id", None)
|
||||
|
||||
# --- Rewrite links: point to the public repo instead of internal ---
|
||||
dash["links"] = [
|
||||
{
|
||||
"asDropdown": False,
|
||||
"icon": "external link",
|
||||
"includeVars": False,
|
||||
"keepTime": False,
|
||||
"tags": [],
|
||||
"targetBlank": True,
|
||||
"title": "Source (GitHub)",
|
||||
"tooltip": "View source file in repository",
|
||||
"type": "link",
|
||||
"url": "https://github.com/paradigmxyz/reth/tree/main/etc/grafana/dashboards",
|
||||
}
|
||||
]
|
||||
|
||||
# --- Datasource: victoriametrics -> prometheus ---
|
||||
dash_str = json.dumps(dash)
|
||||
dash_str = dash_str.replace("victoriametrics-metrics-datasource", "prometheus")
|
||||
dash = json.loads(dash_str)
|
||||
|
||||
# --- Templating: instance_label constant -> ${VAR_INSTANCE_LABEL} ---
|
||||
# Also strip default-value fields the API returns that are not needed for import
|
||||
STRIP_VAR_DEFAULTS = {"allowCustomValue", "regexApplyTo"}
|
||||
for var in dash.get("templating", {}).get("list", []):
|
||||
if var.get("name") == "instance_label" and var.get("type") == "constant":
|
||||
var["query"] = "${VAR_INSTANCE_LABEL}"
|
||||
var["current"] = {
|
||||
"value": "${VAR_INSTANCE_LABEL}",
|
||||
"text": "${VAR_INSTANCE_LABEL}",
|
||||
"selected": False,
|
||||
}
|
||||
var["options"] = [
|
||||
{
|
||||
"value": "${VAR_INSTANCE_LABEL}",
|
||||
"text": "${VAR_INSTANCE_LABEL}",
|
||||
"selected": False,
|
||||
}
|
||||
]
|
||||
# Clear current values for query/datasource vars (not meaningful for import)
|
||||
elif var.get("type") in ("query", "datasource"):
|
||||
var["current"] = {}
|
||||
# Remove noisy default fields
|
||||
for field in STRIP_VAR_DEFAULTS:
|
||||
var.pop(field, None)
|
||||
# Strip falsy defaults on query/datasource vars (API returns them, export omits them)
|
||||
if var.get("type") in ("query", "datasource"):
|
||||
for field in ("hide", "multi", "skipUrlSync"):
|
||||
if not var.get(field):
|
||||
var.pop(field, None)
|
||||
|
||||
# --- Build __inputs ---
|
||||
inputs = [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus",
|
||||
},
|
||||
]
|
||||
|
||||
if has_expression_datasource(dash):
|
||||
inputs.append(
|
||||
{
|
||||
"name": "DS_EXPRESSION",
|
||||
"label": "Expression",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "__expr__",
|
||||
}
|
||||
)
|
||||
|
||||
inputs.append(
|
||||
{
|
||||
"name": "VAR_INSTANCE_LABEL",
|
||||
"type": "constant",
|
||||
"label": "Instance Label",
|
||||
"value": "job",
|
||||
"description": "",
|
||||
}
|
||||
)
|
||||
|
||||
# --- Build __requires ---
|
||||
requires = []
|
||||
|
||||
if has_expression_datasource(dash):
|
||||
requires.append({"type": "datasource", "id": "__expr__", "version": "1.0.0"})
|
||||
|
||||
panel_types = collect_panel_types(dash.get("panels", []))
|
||||
for pt in sorted(panel_types):
|
||||
requires.append(
|
||||
{
|
||||
"type": "panel",
|
||||
"id": pt,
|
||||
"name": PANEL_TYPE_NAMES.get(pt, pt),
|
||||
"version": "",
|
||||
}
|
||||
)
|
||||
|
||||
requires.append(
|
||||
{"type": "grafana", "id": "grafana", "name": "Grafana", "version": grafana_version}
|
||||
)
|
||||
requires.append(
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
)
|
||||
|
||||
# --- Assemble output (with __inputs/__requires/__elements first) ---
|
||||
output = {
|
||||
"__inputs": inputs,
|
||||
"__elements": {},
|
||||
"__requires": requires,
|
||||
}
|
||||
output.update(dash)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <dashboard-uid>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
uid = sys.argv[1]
|
||||
base_url = os.environ.get("FETCH_GRAFANA_DASHBOARD_URL", "").rstrip("/")
|
||||
token = os.environ.get("FETCH_GRAFANA_DASHBOARD_TOKEN", "")
|
||||
|
||||
if not base_url or not token:
|
||||
print(
|
||||
"Error: FETCH_GRAFANA_DASHBOARD_URL and FETCH_GRAFANA_DASHBOARD_TOKEN env vars required",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
resp = fetch_dashboard(base_url, token, uid)
|
||||
dashboard = resp["dashboard"]
|
||||
|
||||
grafana_version = fetch_grafana_version(base_url)
|
||||
exported = make_exportable(dashboard, grafana_version)
|
||||
print(json.dumps(exported, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
.github/workflows/bench-scheduled.yml
vendored
52
.github/workflows/bench-scheduled.yml
vendored
@@ -112,7 +112,7 @@ jobs:
|
||||
|
||||
- name: Alert on long-running hourly
|
||||
if: steps.mode.outputs.mode == 'hourly' && steps.refs.outputs.long-running == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Alert on stale nightly
|
||||
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -278,7 +278,7 @@ jobs:
|
||||
|
||||
- name: Resolve job URL
|
||||
id: job-url
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||
@@ -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,19 +360,9 @@ 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: |
|
||||
@@ -418,15 +402,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: |
|
||||
@@ -680,7 +655,7 @@ jobs:
|
||||
|
||||
- name: Write job summary
|
||||
if: success()
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -759,7 +734,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (success)
|
||||
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -914,7 +889,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (failure)
|
||||
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -925,8 +900,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 }}'],
|
||||
@@ -948,7 +923,7 @@ jobs:
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>\n@ai investigate this` },
|
||||
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>\n<@U0AAA8F0JEM> investigate this` },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
@@ -975,6 +950,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: |
|
||||
|
||||
188
.github/workflows/bench.yml
vendored
188
.github/workflows/bench.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
blocks:
|
||||
description: "Number of blocks to benchmark"
|
||||
required: false
|
||||
default: "200"
|
||||
default: "500"
|
||||
type: string
|
||||
big_blocks:
|
||||
description: "Use big blocks mode (pre-generated merged payloads with reth-bb)"
|
||||
@@ -34,7 +34,7 @@ on:
|
||||
warmup:
|
||||
description: "Number of warmup blocks"
|
||||
required: false
|
||||
default: "100"
|
||||
default: "200"
|
||||
type: string
|
||||
baseline:
|
||||
description: "Baseline git ref (default: merge-base)"
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check org membership
|
||||
if: github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Parse arguments
|
||||
id: args
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -164,9 +164,9 @@ jobs:
|
||||
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
actor = '${{ github.actor }}';
|
||||
blocks = '${{ github.event.inputs.blocks }}' || '200';
|
||||
warmup = '${{ github.event.inputs.warmup }}' || '100';
|
||||
if (warmup !== '100') explicitWarmup = true;
|
||||
blocks = '${{ github.event.inputs.blocks }}' || '500';
|
||||
warmup = '${{ github.event.inputs.warmup }}' || '200';
|
||||
if (warmup !== '200') explicitWarmup = true;
|
||||
baseline = '${{ github.event.inputs.baseline }}';
|
||||
feature = '${{ github.event.inputs.feature }}';
|
||||
samply = '${{ github.event.inputs.samply }}' === 'true' ? 'true' : 'false';
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
const enumArgs = new Map([['bal', validBalModes], ['slack', validSlackModes]]);
|
||||
const durationArgs = new Set(['wait-time']);
|
||||
const stringArgs = new Set(['baseline-args', 'feature-args']);
|
||||
const defaults = { blocks: '200', warmup: '100', baseline: '', feature: '', samply: 'false', slack: 'always', 'big-blocks': 'false', bal: 'false', cores: '0', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
|
||||
const defaults = { blocks: '500', warmup: '200', baseline: '', feature: '', samply: 'false', slack: 'always', 'big-blocks': 'false', bal: 'false', cores: '0', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
|
||||
const unknown = [];
|
||||
const invalid = [];
|
||||
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
|
||||
@@ -359,7 +359,7 @@ jobs:
|
||||
|
||||
- name: Acknowledge request
|
||||
id: ack
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -445,7 +445,7 @@ jobs:
|
||||
|
||||
- name: Poll queue position
|
||||
if: steps.ack.outputs.comment-id && steps.ack.outputs.queue-position != '0'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -560,7 +560,7 @@ jobs:
|
||||
|
||||
- name: Resolve checkout ref
|
||||
id: checkout-ref
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (!process.env.BENCH_PR) {
|
||||
@@ -586,7 +586,7 @@ jobs:
|
||||
|
||||
- name: Resolve job URL and update status
|
||||
if: env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -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
|
||||
@@ -702,7 +696,7 @@ jobs:
|
||||
# Build binaries
|
||||
- name: Resolve PR head branch
|
||||
id: pr-info
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (process.env.BENCH_PR) {
|
||||
@@ -720,7 +714,7 @@ jobs:
|
||||
|
||||
- name: Resolve refs
|
||||
id: refs
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { execSync } = require('child_process');
|
||||
@@ -766,61 +760,45 @@ 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@v8
|
||||
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: |
|
||||
@@ -862,15 +840,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: |
|
||||
@@ -910,8 +879,11 @@ jobs:
|
||||
for p in /sys/kernel/mm/transparent_hugepage /sys/kernel/mm/transparent_hugepages; do
|
||||
[ -d "$p" ] && echo never | sudo tee "$p/enabled" && echo never | sudo tee "$p/defrag" && break
|
||||
done || true
|
||||
# Replace any stale PM QoS holders left behind by earlier benchmark jobs.
|
||||
sudo pkill -f '^bench-cpu-dma-latency' 2>/dev/null || true
|
||||
# Prevent deep C-states (avoids wake-up latency jitter)
|
||||
sudo sh -c 'exec 3<>/dev/cpu_dma_latency; echo -ne "\x00\x00\x00\x00" >&3; sleep infinity' &
|
||||
sudo bash -c 'exec 3<>/dev/cpu_dma_latency; printf "\0\0\0\0" >&3; exec -a bench-cpu-dma-latency sleep infinity' &
|
||||
echo "BENCH_CPU_DMA_LATENCY_PID=$!" >> "$GITHUB_ENV"
|
||||
# Move all IRQs to core 0 (housekeeping core)
|
||||
for irq in /proc/irq/*/smp_affinity_list; do
|
||||
echo 0 | sudo tee "$irq" 2>/dev/null || true
|
||||
@@ -938,45 +910,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 }}"
|
||||
@@ -999,7 +932,7 @@ jobs:
|
||||
|
||||
- name: Update status (running benchmarks)
|
||||
if: success() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -1261,7 +1194,7 @@ jobs:
|
||||
|
||||
- name: Compare & comment
|
||||
if: success() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -1341,7 +1274,7 @@ jobs:
|
||||
|
||||
- name: Write job summary
|
||||
if: success()
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const jobSummary = require('./.github/scripts/bench-job-summary.js');
|
||||
@@ -1355,7 +1288,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (success)
|
||||
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -1366,14 +1299,16 @@ jobs:
|
||||
|
||||
- name: Update status (failed)
|
||||
if: failure() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
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 }}']] : []),
|
||||
@@ -1400,16 +1335,18 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (failure)
|
||||
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
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 }}']] : []),
|
||||
@@ -1422,7 +1359,7 @@ jobs:
|
||||
|
||||
- name: Update status (cancelled)
|
||||
if: cancelled() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -1433,6 +1370,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: |
|
||||
@@ -1445,5 +1387,9 @@ jobs:
|
||||
done
|
||||
# Restore amd-pstate to active (EPP) mode with powersave governor
|
||||
echo active | sudo tee /sys/devices/system/cpu/amd_pstate/status 2>/dev/null || true
|
||||
if [ -n "${BENCH_CPU_DMA_LATENCY_PID:-}" ]; then
|
||||
sudo kill "$BENCH_CPU_DMA_LATENCY_PID" 2>/dev/null || true
|
||||
fi
|
||||
sudo pkill -f '^bench-cpu-dma-latency' 2>/dev/null || true
|
||||
sudo cpupower frequency-set -g powersave 2>/dev/null || true
|
||||
sudo systemctl start irqbalance cron atd 2>/dev/null || true
|
||||
|
||||
2
.github/workflows/book.yml
vendored
2
.github/workflows/book.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: "./docs/vocs/docs/dist"
|
||||
|
||||
|
||||
62
.github/workflows/fetch-grafana-dashboard.yml
vendored
Normal file
62
.github/workflows/fetch-grafana-dashboard.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Fetch Grafana Dashboard
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dashboard_uid:
|
||||
description: "Grafana dashboard UID to export"
|
||||
required: true
|
||||
default: "2k8BXz24x"
|
||||
target_path:
|
||||
description: "Target file path in the repo (e.g. etc/grafana/dashboards/overview.json)"
|
||||
required: true
|
||||
default: "etc/grafana/dashboards/overview.json"
|
||||
|
||||
jobs:
|
||||
fetch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Fetch dashboard from Grafana
|
||||
env:
|
||||
FETCH_GRAFANA_DASHBOARD_URL: ${{ secrets.FETCH_GRAFANA_DASHBOARD_URL }}
|
||||
FETCH_GRAFANA_DASHBOARD_TOKEN: ${{ secrets.FETCH_GRAFANA_DASHBOARD_TOKEN }}
|
||||
run: |
|
||||
python3 .github/scripts/fetch-grafana-dashboard.py "${{ inputs.dashboard_uid }}" \
|
||||
> "${{ inputs.target_path }}"
|
||||
|
||||
- name: Check for changes
|
||||
id: diff
|
||||
run: |
|
||||
if git diff --quiet "${{ inputs.target_path }}"; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes detected."
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.diff.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TARGET="${{ inputs.target_path }}"
|
||||
FILENAME="$(basename "$TARGET")"
|
||||
BRANCH="chore/sync-grafana-${FILENAME%.*}-$(date +%Y%m%d-%H%M%S)"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
git add "$TARGET"
|
||||
git commit -m "chore: update Grafana dashboard ${FILENAME}"
|
||||
git push origin "$BRANCH"
|
||||
gh pr create \
|
||||
--title "chore: update Grafana dashboard ${FILENAME}" \
|
||||
--body "Automated export from Grafana (dashboard UID: \`${{ inputs.dashboard_uid }}\`, target: \`${TARGET}\`)."
|
||||
25
.github/workflows/grafana.yml
vendored
25
.github/workflows/grafana.yml
vendored
@@ -11,11 +11,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check for ${DS_PROMETHEUS} in overview.json
|
||||
- name: Validate dashboard format
|
||||
run: |
|
||||
if grep -Fn '${DS_PROMETHEUS}' etc/grafana/dashboards/overview.json; then
|
||||
echo "Error: overview.json contains '\${DS_PROMETHEUS}' placeholder"
|
||||
echo "Please replace it with '\${datasource}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ overview.json does not contain '\${DS_PROMETHEUS}' placeholder"
|
||||
python3 -c "
|
||||
import json, sys
|
||||
with open('etc/grafana/dashboards/overview.json') as f:
|
||||
d = json.load(f)
|
||||
errors = []
|
||||
if '__inputs' not in d:
|
||||
errors.append('missing __inputs')
|
||||
if '__requires' not in d:
|
||||
errors.append('missing __requires')
|
||||
if d.get('id') is not None:
|
||||
errors.append('contains internal id field — use export-dashboard.py')
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print('✓ overview.json is a valid exported dashboard')
|
||||
"
|
||||
|
||||
2
.github/workflows/label-pr.yml
vendored
2
.github/workflows/label-pr.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Label PRs
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const label_pr = require('./.github/scripts/label_pr.js')
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -195,7 +195,7 @@ jobs:
|
||||
fi
|
||||
|
||||
body=$(cat <<- "ENDBODY"
|
||||

|
||||

|
||||
|
||||
## Testing Checklist (DELETE ME)
|
||||
|
||||
|
||||
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
66
Cargo.toml
66
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace.package]
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.93"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -129,6 +129,7 @@ members = [
|
||||
"examples/custom-node-components/",
|
||||
"examples/custom-payload-builder/",
|
||||
"examples/custom-rlpx-subprotocol",
|
||||
"examples/custom-auth-http-middleware",
|
||||
"examples/custom-rpc-middleware",
|
||||
"examples/db-access",
|
||||
"examples/exex-subscription",
|
||||
@@ -432,14 +433,14 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.0", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "37.0.0", default-features = false }
|
||||
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.38.0"
|
||||
revm-inspectors = "0.39.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -448,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-evm = { version = "0.32.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 }
|
||||
@@ -506,6 +507,7 @@ eyre = "0.6"
|
||||
fdlimit = "0.3.0"
|
||||
fixed-map = { version = "0.9", default-features = false }
|
||||
humantime = "2.1"
|
||||
imbl = "7"
|
||||
humantime-serde = "1.1"
|
||||
itertools = { version = "0.14", default-features = false }
|
||||
linked_hash_set = "0.1"
|
||||
|
||||
@@ -238,6 +238,7 @@ where
|
||||
withdrawals: prev_segment.ctx.withdrawals.clone(),
|
||||
extra_data: prev_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: prev_segment.ctx.tx_count_hint,
|
||||
slot_number: prev_segment.ctx.slot_number,
|
||||
};
|
||||
|
||||
// Clone the next segment's data before we consume inner.
|
||||
@@ -252,6 +253,7 @@ where
|
||||
withdrawals: new_segment.ctx.withdrawals.clone(),
|
||||
extra_data: new_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: new_segment.ctx.tx_count_hint,
|
||||
slot_number: new_segment.ctx.slot_number,
|
||||
};
|
||||
|
||||
plan.next_segment += 1;
|
||||
@@ -364,6 +366,7 @@ where
|
||||
withdrawals: seg0.ctx.withdrawals.clone(),
|
||||
extra_data: seg0.ctx.extra_data.clone(),
|
||||
tx_count_hint: seg0.ctx.tx_count_hint,
|
||||
slot_number: seg0.ctx.slot_number,
|
||||
};
|
||||
|
||||
let inner = self.inner_mut();
|
||||
@@ -422,6 +425,7 @@ where
|
||||
withdrawals: last_seg.ctx.withdrawals.clone(),
|
||||
extra_data: last_seg.ctx.extra_data.clone(),
|
||||
tx_count_hint: last_seg.ctx.tx_count_hint,
|
||||
slot_number: last_seg.ctx.slot_number,
|
||||
};
|
||||
self.inner_mut().ctx = last_ctx;
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ where
|
||||
ommers: &[],
|
||||
withdrawals: ctx.withdrawals.map(|w| std::borrow::Cow::Owned(w.into_owned())),
|
||||
extra_data: ctx.extra_data,
|
||||
slot_number: ctx.slot_number,
|
||||
};
|
||||
BigBlockSegment { start_tx, evm_env, ctx }
|
||||
})
|
||||
|
||||
@@ -176,6 +176,7 @@ impl BbAddOns {
|
||||
BasicEngineApiBuilder::default(),
|
||||
BasicEngineValidatorBuilder::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use alloy_eips::{
|
||||
eip1559::BaseFeeParams,
|
||||
eip7840::BlobParams,
|
||||
eip7928::{AccountChanges, BlockAccessList, SlotChanges},
|
||||
BlockNumberOrTag, Typed2718,
|
||||
Typed2718,
|
||||
};
|
||||
use alloy_primitives::{Bloom, Bytes, B256};
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
@@ -32,6 +32,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, future::Future};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::bench::helpers::fetch_block_access_list;
|
||||
|
||||
/// A single transaction with its gas used and raw encoded bytes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawTransaction {
|
||||
@@ -669,20 +671,6 @@ impl Command {
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_block_access_list(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
block_number: u64,
|
||||
) -> eyre::Result<BlockAccessList> {
|
||||
provider
|
||||
.client()
|
||||
.request("eth_getBlockAccessListByBlockNumber", (BlockNumberOrTag::Number(block_number),))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.and_then(|block_access_list: Option<BlockAccessList>| {
|
||||
block_access_list.ok_or_else(|| eyre::eyre!("BAL not found for block {block_number}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_block_access_list(
|
||||
merged: &mut BlockAccessList,
|
||||
incoming: BlockAccessList,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Common helpers for reth-bench commands.
|
||||
|
||||
use alloy_eips::{eip7928::BlockAccessList, BlockNumberOrTag};
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use eyre::Result;
|
||||
use std::{
|
||||
io::{BufReader, Read},
|
||||
@@ -69,6 +71,21 @@ pub(crate) fn parse_duration(s: &str) -> eyre::Result<Duration> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the block access list for a given block number using the provided provider.
|
||||
pub(crate) async fn fetch_block_access_list(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
block_number: u64,
|
||||
) -> eyre::Result<BlockAccessList> {
|
||||
provider
|
||||
.client()
|
||||
.request("eth_getBlockAccessListByBlockNumber", (BlockNumberOrTag::Number(block_number),))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.and_then(|block_access_list: Option<BlockAccessList>| {
|
||||
block_access_list.ok_or_else(|| eyre::eyre!("BAL not found for block {block_number}"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::{
|
||||
bench::{
|
||||
context::BenchContext,
|
||||
helpers::parse_duration,
|
||||
helpers::{fetch_block_access_list, parse_duration},
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
|
||||
@@ -75,6 +75,10 @@ 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,
|
||||
}
|
||||
@@ -198,12 +202,21 @@ impl Command {
|
||||
finalized_block_hash: finalized,
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let (version, params) = block_to_new_payload(
|
||||
block,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
bal,
|
||||
)?;
|
||||
let start = Instant::now();
|
||||
let server_timings =
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::{
|
||||
bench::{
|
||||
context::BenchContext,
|
||||
helpers::fetch_block_access_list,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
NewPayloadResult, TotalGasOutput, TotalGasRow, GAS_OUTPUT_SUFFIX,
|
||||
@@ -69,7 +70,9 @@ impl Command {
|
||||
let (error_sender, mut error_receiver) = tokio::sync::oneshot::channel();
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel(buffer_size);
|
||||
|
||||
let block_provider_clone = block_provider.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let block_provider = block_provider_clone;
|
||||
while benchmark_mode.contains(next_block) {
|
||||
let block_res = block_provider
|
||||
.get_block_by_number(next_block.into())
|
||||
@@ -123,12 +126,19 @@ impl Command {
|
||||
|
||||
debug!(target: "reth-bench", number=?block.header.number, "Sending payload to engine");
|
||||
|
||||
let bal = if rlp.is_none() && block.header.block_access_list_hash.is_some() {
|
||||
Some(fetch_block_access_list(&block_provider, block.header.number).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (version, params) = block_to_new_payload(
|
||||
block,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
bal,
|
||||
)?;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
@@ -18,7 +18,7 @@ use alloy_primitives::B256;
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{
|
||||
CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV4,
|
||||
CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV6,
|
||||
ExecutionPayloadSidecar, ExecutionPayloadV4, ForkchoiceState, JwtSecret, PraguePayloadFields,
|
||||
};
|
||||
use clap::Parser;
|
||||
@@ -315,7 +315,7 @@ impl Command {
|
||||
let requests =
|
||||
execution_data.sidecar.requests().cloned().unwrap_or_default().to_vec();
|
||||
(
|
||||
Some(EngineApiMessageVersion::V4),
|
||||
Some(EngineApiMessageVersion::V6),
|
||||
serde_json::to_value((
|
||||
execution_data.payload.clone(),
|
||||
Vec::<B256>::new(),
|
||||
@@ -423,7 +423,7 @@ impl Command {
|
||||
/// Load and parse all payload files from the directory.
|
||||
///
|
||||
/// Tries to load each file as a [`BigBlockPayload`] first (which includes `env_switches`),
|
||||
/// falling back to [`ExecutionPayloadEnvelopeV4`] for backwards compatibility.
|
||||
/// falling back to [`ExecutionPayloadEnvelopeV6`] for backwards compatibility.
|
||||
fn load_payloads(&self) -> eyre::Result<Vec<LoadedPayload>> {
|
||||
let mut payloads = Vec::new();
|
||||
|
||||
@@ -450,12 +450,11 @@ impl Command {
|
||||
let name_str = name.to_string_lossy();
|
||||
let index = if let Some(rest) = name_str.strip_prefix("payload_block_") {
|
||||
rest.strip_suffix(".json")?.parse::<u64>().ok()?
|
||||
} else if let Some(rest) = name_str.strip_prefix("big_block_") {
|
||||
} else {
|
||||
let rest = name_str.strip_prefix("big_block_")?;
|
||||
// "big_block_FROM_to_TO.json" — use FROM as the index
|
||||
let rest = rest.strip_suffix(".json")?;
|
||||
rest.split("_to_").next()?.parse::<u64>().ok()?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some((index, e.path()))
|
||||
})
|
||||
@@ -481,10 +480,10 @@ impl Command {
|
||||
{
|
||||
(big_block.execution_data, big_block.big_block_data, big_block.block_access_list)
|
||||
} else {
|
||||
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
|
||||
let envelope: ExecutionPayloadEnvelopeV6 = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
let execution_data = ExecutionData {
|
||||
payload: envelope.envelope_inner.execution_payload.clone().into(),
|
||||
payload: envelope.execution_payload.clone().into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use alloy_eips::eip4895::Withdrawal;
|
||||
use alloy_primitives::{Address, Bloom, Bytes, B256, U256};
|
||||
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ExecutionPayloadV4,
|
||||
};
|
||||
|
||||
/// Configuration for invalidating payload fields
|
||||
#[derive(Debug, Default)]
|
||||
@@ -21,6 +23,7 @@ pub(super) struct InvalidationConfig {
|
||||
pub(super) block_hash: Option<B256>,
|
||||
pub(super) blob_gas_used: Option<u64>,
|
||||
pub(super) excess_blob_gas: Option<u64>,
|
||||
pub(super) slot_number: Option<u64>,
|
||||
|
||||
// Auto-invalidation flags
|
||||
pub(super) invalidate_parent_hash: bool,
|
||||
@@ -35,6 +38,8 @@ pub(super) struct InvalidationConfig {
|
||||
pub(super) invalidate_withdrawals: bool,
|
||||
pub(super) invalidate_blob_gas_used: bool,
|
||||
pub(super) invalidate_excess_blob_gas: bool,
|
||||
pub(super) invalidate_block_access_list: bool,
|
||||
pub(super) invalidate_slot_number: bool,
|
||||
}
|
||||
|
||||
impl InvalidationConfig {
|
||||
@@ -216,4 +221,30 @@ impl InvalidationConfig {
|
||||
|
||||
changes
|
||||
}
|
||||
|
||||
/// Applies invalidations to a V4 payload, returns list of what was changed.
|
||||
pub(super) fn apply_to_payload_v4(&self, payload: &mut ExecutionPayloadV4) -> Vec<String> {
|
||||
let mut changes = self.apply_to_payload_v3(&mut payload.payload_inner);
|
||||
|
||||
// Explicit override for slot_number
|
||||
if let Some(slot_number) = self.slot_number {
|
||||
payload.slot_number = slot_number;
|
||||
changes.push(format!("slot_number = {slot_number}"));
|
||||
}
|
||||
|
||||
// Handle block access list invalidation (V4+)
|
||||
if self.invalidate_block_access_list {
|
||||
let fake_bal = Bytes::from_static(&[0x01, 0x02, 0x03]);
|
||||
payload.block_access_list = fake_bal.clone();
|
||||
changes.push(format!("block_access_list = {fake_bal} (auto-invalidated)"));
|
||||
}
|
||||
|
||||
// Handle slot number invalidation (V4+)
|
||||
if self.invalidate_slot_number {
|
||||
payload.slot_number = u64::MAX;
|
||||
changes.push("slot_number = MAX (auto-invalidated)".to_string());
|
||||
}
|
||||
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
//! Command for sending invalid payloads to test Engine API rejection.
|
||||
|
||||
mod invalidation;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use invalidation::InvalidationConfig;
|
||||
|
||||
use crate::bench::helpers::fetch_block_access_list;
|
||||
|
||||
use super::helpers::{load_jwt_secret, read_input};
|
||||
use alloy_consensus::TxEnvelope;
|
||||
use alloy_primitives::{Address, B256};
|
||||
use alloy_provider::network::AnyRpcBlock;
|
||||
use alloy_primitives::{Address, Bytes, B256};
|
||||
use alloy_provider::{
|
||||
network::{AnyNetwork, AnyRpcBlock},
|
||||
RootProvider,
|
||||
};
|
||||
use alloy_rpc_types_engine::ExecutionPayload;
|
||||
use clap::Parser;
|
||||
use eyre::{OptionExt, Result};
|
||||
@@ -105,6 +111,9 @@ pub struct Command {
|
||||
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||
requests_hash: Option<B256>,
|
||||
|
||||
/// Override the slot number with a specific value.
|
||||
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||
slot_number: Option<u64>,
|
||||
// ==================== Auto-Invalidation Flags ====================
|
||||
/// Invalidate the parent hash by setting it to a random value.
|
||||
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||
@@ -158,6 +167,14 @@ pub struct Command {
|
||||
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||
invalidate_requests_hash: bool,
|
||||
|
||||
/// Invalidate the block access list by setting it to a random value (EIP-7928).
|
||||
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||
invalidate_block_access_list: bool,
|
||||
|
||||
/// Invalidate the slot number by setting it to an random value.(EIP-7843).
|
||||
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||
invalidate_slot_number: bool,
|
||||
|
||||
// ==================== Meta Flags ====================
|
||||
/// Skip block hash recalculation after modifications.
|
||||
#[arg(long, default_value_t = false, help_heading = "Meta Flags")]
|
||||
@@ -199,6 +216,7 @@ impl Command {
|
||||
block_hash: self.block_hash,
|
||||
blob_gas_used: self.blob_gas_used,
|
||||
excess_blob_gas: self.excess_blob_gas,
|
||||
slot_number: self.slot_number,
|
||||
invalidate_parent_hash: self.invalidate_parent_hash,
|
||||
invalidate_state_root: self.invalidate_state_root,
|
||||
invalidate_receipts_root: self.invalidate_receipts_root,
|
||||
@@ -211,6 +229,8 @@ impl Command {
|
||||
invalidate_withdrawals: self.invalidate_withdrawals,
|
||||
invalidate_blob_gas_used: self.invalidate_blob_gas_used,
|
||||
invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
|
||||
invalidate_block_access_list: self.invalidate_block_access_list,
|
||||
invalidate_slot_number: self.invalidate_slot_number,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,15 +254,21 @@ impl Command {
|
||||
let blob_versioned_hashes =
|
||||
block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
|
||||
let use_v4 = block.header.requests_hash.is_some();
|
||||
let use_v5 = block.header.block_access_list_hash.is_some();
|
||||
let requests_hash = self.requests_hash.or(block.header.requests_hash);
|
||||
|
||||
let mut execution_payload = ExecutionPayload::from_block_slow(&block).0;
|
||||
let mut execution_payload = if use_v5 {
|
||||
let encoded_bal = self.fetch_encoded_block_access_list(block.header.number).await?;
|
||||
ExecutionPayload::from_block_slow_with_bal(&block, encoded_bal).0
|
||||
} else {
|
||||
ExecutionPayload::from_block_slow(&block).0
|
||||
};
|
||||
|
||||
let changes = match &mut execution_payload {
|
||||
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
|
||||
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
|
||||
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
|
||||
ExecutionPayload::V4(p) => config.apply_to_payload_v3(&mut p.payload_inner),
|
||||
ExecutionPayload::V4(p) => config.apply_to_payload_v4(p),
|
||||
};
|
||||
|
||||
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
|
||||
@@ -312,7 +338,13 @@ impl Command {
|
||||
match self.mode {
|
||||
Mode::Execute => {
|
||||
let mut command = std::process::Command::new("cast");
|
||||
let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
|
||||
let method = if use_v5 {
|
||||
"engine_newPayloadV5"
|
||||
} else if use_v4 {
|
||||
"engine_newPayloadV4"
|
||||
} else {
|
||||
"engine_newPayloadV3"
|
||||
};
|
||||
command.arg("rpc").arg(method).arg("--raw");
|
||||
if let Some(rpc_url) = self.rpc_url {
|
||||
command.arg("--rpc-url").arg(rpc_url);
|
||||
@@ -353,4 +385,17 @@ impl Command {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_encoded_block_access_list(&self, block_number: u64) -> Result<Bytes> {
|
||||
let rpc_url = self
|
||||
.rpc_url
|
||||
.as_deref()
|
||||
.ok_or_eyre("--rpc-url is required to fetch the block access list for V5 payloads")?;
|
||||
let client = ClientBuilder::default()
|
||||
.layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
|
||||
.http(rpc_url.parse()?);
|
||||
let provider = RootProvider::<AnyNetwork>::new(client);
|
||||
let bal = fetch_block_access_list(&provider, block_number).await?;
|
||||
Ok(alloy_rlp::encode(bal).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use super::helpers::{load_jwt_secret, read_input};
|
||||
use super::helpers::{fetch_block_access_list, load_jwt_secret, read_input};
|
||||
use alloy_consensus::TxEnvelope;
|
||||
use alloy_provider::network::AnyRpcBlock;
|
||||
use alloy_primitives::Bytes;
|
||||
use alloy_provider::{
|
||||
network::{AnyNetwork, AnyRpcBlock},
|
||||
RootProvider,
|
||||
};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::ExecutionPayload;
|
||||
use clap::Parser;
|
||||
use eyre::{OptionExt, Result};
|
||||
@@ -69,6 +74,9 @@ impl Command {
|
||||
})?
|
||||
.into_consensus();
|
||||
|
||||
let use_v4 = block.header.requests_hash.is_some();
|
||||
let use_v5 = block.header.block_access_list_hash.is_some();
|
||||
|
||||
// Extract parent beacon block root
|
||||
let parent_beacon_block_root = block.header.parent_beacon_block_root;
|
||||
|
||||
@@ -76,10 +84,14 @@ impl Command {
|
||||
let blob_versioned_hashes =
|
||||
block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
|
||||
|
||||
// Convert to execution payload
|
||||
let execution_payload = ExecutionPayload::from_block_slow(&block).0;
|
||||
|
||||
let use_v4 = block.header.requests_hash.is_some();
|
||||
// V5 payloads must carry the full RLP-encoded block access list, not just the hash stored
|
||||
// in the header.
|
||||
let execution_payload = if use_v5 {
|
||||
let encoded_bal = self.fetch_encoded_block_access_list(block.header.number).await?;
|
||||
ExecutionPayload::from_block_slow_with_bal(&block, encoded_bal).0
|
||||
} else {
|
||||
ExecutionPayload::from_block_slow(&block).0
|
||||
};
|
||||
|
||||
// Create JSON request data
|
||||
let json_request = if use_v4 {
|
||||
@@ -102,7 +114,13 @@ impl Command {
|
||||
Mode::Execute => {
|
||||
// Create cast command
|
||||
let mut command = std::process::Command::new("cast");
|
||||
let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
|
||||
let method = if use_v5 {
|
||||
"engine_newPayloadV5"
|
||||
} else if use_v4 {
|
||||
"engine_newPayloadV4"
|
||||
} else {
|
||||
"engine_newPayloadV3"
|
||||
};
|
||||
command.arg("rpc").arg(method).arg("--raw");
|
||||
if let Some(rpc_url) = self.rpc_url {
|
||||
command.arg("--rpc-url").arg(rpc_url);
|
||||
@@ -146,4 +164,17 @@ impl Command {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_encoded_block_access_list(&self, block_number: u64) -> Result<Bytes> {
|
||||
let rpc_url = self
|
||||
.rpc_url
|
||||
.as_deref()
|
||||
.ok_or_eyre("--rpc-url is required to fetch the block access list for V5 payloads")?;
|
||||
let client = ClientBuilder::default()
|
||||
.layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
|
||||
.http(rpc_url.parse()?);
|
||||
let provider = RootProvider::<AnyNetwork>::new(client);
|
||||
let bal = fetch_block_access_list(&provider, block_number).await?;
|
||||
Ok(alloy_rlp::encode(bal).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! before sending additional calls.
|
||||
|
||||
use alloy_consensus::TxEnvelope;
|
||||
use alloy_eips::eip7928::BlockAccessList;
|
||||
use alloy_primitives::Bytes;
|
||||
use alloy_provider::{ext::EngineApi, network::AnyRpcBlock, Network, Provider};
|
||||
use alloy_rpc_types_engine::{
|
||||
@@ -43,6 +44,14 @@ pub trait EngineApiValidWaitExt<N>: Send + Sync {
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated>;
|
||||
|
||||
/// Calls `engine_forkChoiceUpdatedV4` with the given [`ForkchoiceState`] and optional
|
||||
/// [`PayloadAttributes`], and waits until the response is VALID.
|
||||
async fn fork_choice_updated_v4_wait(
|
||||
&self,
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -162,6 +171,40 @@ where
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
async fn fork_choice_updated_v4_wait(
|
||||
&self,
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
method = "engine_forkchoiceUpdatedV3",
|
||||
?fork_choice_state,
|
||||
?payload_attributes,
|
||||
"Sending forkchoiceUpdated"
|
||||
);
|
||||
|
||||
let mut status =
|
||||
self.fork_choice_updated_v4(fork_choice_state, payload_attributes.clone()).await?;
|
||||
|
||||
while !status.is_valid() {
|
||||
if status.is_invalid() {
|
||||
error!(
|
||||
target: "reth-bench",
|
||||
?status,
|
||||
?fork_choice_state,
|
||||
?payload_attributes,
|
||||
"Invalid forkchoiceUpdatedV4 message",
|
||||
);
|
||||
panic!("Invalid forkchoiceUpdatedV4: {status:?}");
|
||||
}
|
||||
status =
|
||||
self.fork_choice_updated_v4(fork_choice_state, payload_attributes.clone()).await?;
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an RPC block into versioned engine API params and an [`ExecutionData`].
|
||||
@@ -176,6 +219,7 @@ pub(crate) fn block_to_new_payload(
|
||||
reth_new_payload: bool,
|
||||
wait_for_persistence: WaitForPersistence,
|
||||
no_wait_for_caches: bool,
|
||||
bal: Option<BlockAccessList>,
|
||||
) -> eyre::Result<(Option<EngineApiMessageVersion>, serde_json::Value)> {
|
||||
let block_number = block.header.number;
|
||||
let wait_for_persistence = wait_for_persistence.rpc_value(block_number);
|
||||
@@ -198,7 +242,11 @@ pub(crate) fn block_to_new_payload(
|
||||
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
|
||||
})?
|
||||
.into_consensus();
|
||||
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
|
||||
|
||||
let block_access_list = alloy_rlp::encode(bal.unwrap_or_default());
|
||||
|
||||
let (payload, sidecar) =
|
||||
ExecutionPayload::from_block_slow_with_bal(&block, block_access_list.into());
|
||||
let (version, params, execution_data) = payload_to_new_payload(payload, sidecar, None)?;
|
||||
|
||||
if reth_new_payload {
|
||||
@@ -227,6 +275,22 @@ pub(crate) fn payload_to_new_payload(
|
||||
let execution_data = ExecutionData { payload: payload.clone(), sidecar: sidecar.clone() };
|
||||
|
||||
let (version, params) = match payload {
|
||||
ExecutionPayload::V4(payload) => {
|
||||
let cancun = sidecar
|
||||
.cancun()
|
||||
.ok_or_else(|| eyre::eyre!("missing cancun sidecar for V4 payload"))?;
|
||||
let version = target_version.unwrap_or(EngineApiMessageVersion::V6);
|
||||
let requests = sidecar.prague().map(|p| p.requests.clone()).unwrap_or_default();
|
||||
(
|
||||
version,
|
||||
serde_json::to_value((
|
||||
payload,
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
}
|
||||
ExecutionPayload::V3(payload) => {
|
||||
let cancun = sidecar
|
||||
.cancun()
|
||||
@@ -266,22 +330,6 @@ pub(crate) fn payload_to_new_payload(
|
||||
ExecutionPayload::V1(payload) => {
|
||||
(EngineApiMessageVersion::V1, serde_json::to_value((payload,))?)
|
||||
}
|
||||
ExecutionPayload::V4(payload) => {
|
||||
let cancun = sidecar
|
||||
.cancun()
|
||||
.ok_or_else(|| eyre::eyre!("missing cancun sidecar for V4 payload"))?;
|
||||
let version = target_version.unwrap_or(EngineApiMessageVersion::V4);
|
||||
let requests = sidecar.prague().map(|p| p.requests.clone()).unwrap_or_default();
|
||||
(
|
||||
version,
|
||||
serde_json::to_value((
|
||||
payload,
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok((version, params, execution_data))
|
||||
@@ -386,10 +434,10 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
|
||||
match message_version {
|
||||
EngineApiMessageVersion::V3 |
|
||||
EngineApiMessageVersion::V4 |
|
||||
EngineApiMessageVersion::V5 |
|
||||
EngineApiMessageVersion::V6 => {
|
||||
provider.fork_choice_updated_v4_wait(forkchoice_state, payload_attributes).await
|
||||
}
|
||||
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
|
||||
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
|
||||
}
|
||||
EngineApiMessageVersion::V2 => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use alloy_primitives::{hex, BlockHash};
|
||||
use alloy_primitives::{hex, Address, BlockHash, B256};
|
||||
use clap::Parser;
|
||||
use reth_db::{
|
||||
static_file::{
|
||||
@@ -10,16 +10,20 @@ use reth_db::{
|
||||
use reth_db_api::{
|
||||
cursor::{DbCursorRO, DbDupCursorRO},
|
||||
database::Database,
|
||||
models::{storage_sharded_key::StorageShardedKey, ShardedKey},
|
||||
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};
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_primitives_traits::ValueWithSubKey;
|
||||
use reth_provider::{providers::ProviderNodeTypes, ChangeSetReader, StaticFileProviderFactory};
|
||||
use reth_provider::{
|
||||
providers::ProviderNodeTypes, ChangeSetReader, RocksDBProviderFactory,
|
||||
StaticFileProviderFactory,
|
||||
};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_storage_api::StorageChangeSetReader;
|
||||
use tracing::error;
|
||||
@@ -73,6 +77,55 @@ enum Subcommand {
|
||||
#[arg(long)]
|
||||
raw: bool,
|
||||
},
|
||||
/// Gets the content of a RocksDB table for the given key
|
||||
///
|
||||
/// For history tables (accounts-history, storages-history), you can pass a plain address
|
||||
/// instead of a full JSON ShardedKey. Use --block to query a specific block number
|
||||
/// (seeks to the shard containing that block), or --all-shards to list all shards for
|
||||
/// the address.
|
||||
///
|
||||
/// Examples:
|
||||
/// reth db get rocksdb accounts-history 0xdBBE3D8c2d2b22A2611c5A94A9a12C2fCD49Eb29
|
||||
/// reth db get rocksdb accounts-history 0xdBBE...Eb29 --block 1000000
|
||||
/// reth db get rocksdb accounts-history 0xdBBE...Eb29 --all-shards
|
||||
/// reth db get rocksdb storages-history 0xdBBE...Eb29 --storage-key 0x0000...0003
|
||||
Rocksdb {
|
||||
/// The RocksDB table
|
||||
#[arg(value_enum)]
|
||||
table: RocksDbTable,
|
||||
|
||||
/// The key to get content for. For history tables, this can be a plain address.
|
||||
#[arg(value_parser = maybe_json_value_parser)]
|
||||
key: String,
|
||||
|
||||
/// Target block number for history tables. Seeks to the shard containing this block.
|
||||
/// Defaults to the latest shard if not specified.
|
||||
#[arg(long)]
|
||||
block: Option<u64>,
|
||||
|
||||
/// Storage key for storages-history table lookups.
|
||||
#[arg(long)]
|
||||
storage_key: Option<String>,
|
||||
|
||||
/// List all shards for the given key (history tables only).
|
||||
#[arg(long)]
|
||||
all_shards: bool,
|
||||
|
||||
/// Output bytes instead of human-readable decoded value
|
||||
#[arg(long)]
|
||||
raw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// RocksDB tables that can be queried.
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum RocksDbTable {
|
||||
/// Transaction hash to transaction number mapping
|
||||
TransactionHashNumbers,
|
||||
/// Account history indices
|
||||
AccountsHistory,
|
||||
/// Storage history indices
|
||||
StoragesHistory,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -82,6 +135,9 @@ impl Command {
|
||||
Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
|
||||
table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
|
||||
}
|
||||
Subcommand::Rocksdb { table, key, block, storage_key, all_shards, raw } => {
|
||||
get_rocksdb(tool, table, &key, block, storage_key.as_deref(), all_shards, raw)?;
|
||||
}
|
||||
Subcommand::StaticFile { segment, key, subkey, raw } => {
|
||||
if let StaticFileSegment::StorageChangeSets = segment {
|
||||
let storage_key =
|
||||
@@ -208,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 => {
|
||||
@@ -246,6 +299,208 @@ impl Command {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a value from a RocksDB table by key.
|
||||
fn get_rocksdb<N: ProviderNodeTypes>(
|
||||
tool: &DbTool<N>,
|
||||
table: RocksDbTable,
|
||||
key: &str,
|
||||
block: Option<u64>,
|
||||
storage_key: Option<&str>,
|
||||
all_shards: bool,
|
||||
raw: bool,
|
||||
) -> eyre::Result<()> {
|
||||
let rocksdb = tool.provider_factory.rocksdb_provider();
|
||||
|
||||
match table {
|
||||
RocksDbTable::TransactionHashNumbers => {
|
||||
if block.is_some() || all_shards || storage_key.is_some() {
|
||||
return Err(eyre::eyre!(
|
||||
"--block, --all-shards, and --storage-key are only supported for history tables"
|
||||
));
|
||||
}
|
||||
get_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, key, raw)
|
||||
}
|
||||
RocksDbTable::AccountsHistory => {
|
||||
if storage_key.is_some() {
|
||||
return Err(eyre::eyre!("--storage-key is only supported for storages-history"));
|
||||
}
|
||||
get_rocksdb_account_history(&rocksdb, key, block, all_shards, raw)
|
||||
}
|
||||
RocksDbTable::StoragesHistory => {
|
||||
get_rocksdb_storage_history(&rocksdb, key, storage_key, block, all_shards, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse a key string as a plain address, falling back to JSON `ShardedKey` parsing.
|
||||
fn parse_address(key: &str) -> eyre::Result<Address> {
|
||||
// Strip surrounding quotes that `maybe_json_value_parser` may have added
|
||||
let stripped = key.trim_matches('"');
|
||||
stripped.parse::<Address>().map_err(|e| eyre::eyre!("failed to parse address: {e}"))
|
||||
}
|
||||
|
||||
/// Gets account history from RocksDB with ergonomic key parsing.
|
||||
///
|
||||
/// Accepts a plain address and uses seek to find the relevant shard.
|
||||
fn get_rocksdb_account_history(
|
||||
rocksdb: &reth_provider::providers::RocksDBProvider,
|
||||
key: &str,
|
||||
block: Option<u64>,
|
||||
all_shards: bool,
|
||||
raw: bool,
|
||||
) -> eyre::Result<()> {
|
||||
// Try parsing as a plain address first, fall back to full JSON ShardedKey
|
||||
match parse_address(key) {
|
||||
Ok(address) => {
|
||||
let block_number = block.unwrap_or(u64::MAX);
|
||||
let seek_key = ShardedKey::new(address, block_number);
|
||||
|
||||
if all_shards {
|
||||
// Iterate all shards: seek from (address, 0) until address changes
|
||||
let start = ShardedKey::new(address, 0);
|
||||
let iter = rocksdb.iter_from::<tables::AccountsHistory>(start)?;
|
||||
for result in iter {
|
||||
let (k, v) = result?;
|
||||
if k.key != address {
|
||||
break;
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"highest_block_number": k.highest_block_number,
|
||||
"value": v,
|
||||
}))?
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Seek to the first shard with highest_block_number >= target
|
||||
let mut iter = rocksdb.iter_from::<tables::AccountsHistory>(seek_key)?;
|
||||
match iter.next() {
|
||||
Some(Ok((k, v))) if k.key == address => {
|
||||
if raw {
|
||||
let raw_val = rocksdb.get_raw::<tables::AccountsHistory>(k)?;
|
||||
if let Some(bytes) = raw_val {
|
||||
println!("{}", hex::encode_prefixed(&bytes));
|
||||
}
|
||||
} else {
|
||||
println!("{}", serde_json::to_string_pretty(&v)?);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!(target: "reth::cli", "No content for the given table key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall back to full JSON key parsing (e.g.
|
||||
// `{"key":"0x...","highest_block_number":...}`)
|
||||
if all_shards || block.is_some() {
|
||||
return Err(eyre::eyre!(
|
||||
"--block and --all-shards require a plain address, not a JSON key"
|
||||
));
|
||||
}
|
||||
get_rocksdb_table::<tables::AccountsHistory>(rocksdb, key, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets storage history from RocksDB with ergonomic key parsing.
|
||||
///
|
||||
/// Accepts a plain address + optional `--storage-key` and uses seek.
|
||||
fn get_rocksdb_storage_history(
|
||||
rocksdb: &reth_provider::providers::RocksDBProvider,
|
||||
key: &str,
|
||||
storage_key: Option<&str>,
|
||||
block: Option<u64>,
|
||||
all_shards: bool,
|
||||
raw: bool,
|
||||
) -> eyre::Result<()> {
|
||||
match parse_address(key) {
|
||||
Ok(address) => {
|
||||
let storage_key = storage_key
|
||||
.map(|s| s.trim_matches('"').parse::<B256>())
|
||||
.transpose()
|
||||
.map_err(|e| eyre::eyre!("failed to parse storage key: {e}"))?
|
||||
.unwrap_or_default();
|
||||
let block_number = block.unwrap_or(u64::MAX);
|
||||
let seek_key = StorageShardedKey::new(address, storage_key, block_number);
|
||||
|
||||
if all_shards {
|
||||
let start = StorageShardedKey::new(address, storage_key, 0);
|
||||
let iter = rocksdb.iter_from::<tables::StoragesHistory>(start)?;
|
||||
for result in iter {
|
||||
let (k, v) = result?;
|
||||
if k.address != address || k.sharded_key.key != storage_key {
|
||||
break;
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"highest_block_number": k.sharded_key.highest_block_number,
|
||||
"value": v,
|
||||
}))?
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let mut iter = rocksdb.iter_from::<tables::StoragesHistory>(seek_key)?;
|
||||
match iter.next() {
|
||||
Some(Ok((k, v)))
|
||||
if k.address == address && k.sharded_key.key == storage_key =>
|
||||
{
|
||||
if raw {
|
||||
let raw_val = rocksdb.get_raw::<tables::StoragesHistory>(k)?;
|
||||
if let Some(bytes) = raw_val {
|
||||
println!("{}", hex::encode_prefixed(&bytes));
|
||||
}
|
||||
} else {
|
||||
println!("{}", serde_json::to_string_pretty(&v)?);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!(target: "reth::cli", "No content for the given table key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
if all_shards || block.is_some() || storage_key.is_some() {
|
||||
return Err(eyre::eyre!(
|
||||
"--block, --all-shards, and --storage-key require a plain address, not a JSON key"
|
||||
));
|
||||
}
|
||||
get_rocksdb_table::<tables::StoragesHistory>(rocksdb, key, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a value from a specific RocksDB table by exact key and prints it.
|
||||
fn get_rocksdb_table<T: Table>(
|
||||
rocksdb: &reth_provider::providers::RocksDBProvider,
|
||||
key_str: &str,
|
||||
raw: bool,
|
||||
) -> eyre::Result<()> {
|
||||
let key = table_key::<T>(key_str)?;
|
||||
|
||||
if raw {
|
||||
let content = rocksdb.get_raw::<T>(key)?;
|
||||
match content {
|
||||
Some(bytes) => println!("{}", hex::encode_prefixed(&bytes)),
|
||||
None => error!(target: "reth::cli", "No content for the given table key."),
|
||||
}
|
||||
} else {
|
||||
let content = rocksdb.get::<T>(key)?;
|
||||
match content {
|
||||
Some(value) => println!("{}", serde_json::to_string_pretty(&value)?),
|
||||
None => error!(target: "reth::cli", "No content for the given table key."),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an instance of key for given table
|
||||
pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
|
||||
serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
|
||||
|
||||
361
crates/cli/commands/src/db/migrate_v2.rs
Normal file
361
crates/cli/commands/src/db/migrate_v2.rs
Normal file
@@ -0,0 +1,361 @@
|
||||
//! `reth db migrate-v2` command for migrating v1 storage layout to v2.
|
||||
//!
|
||||
//! Migrates data that cannot be recomputed (changesets + receipts) from MDBX to
|
||||
//! static files, clears recomputable tables (senders, indices, trie, plain
|
||||
//! state), compacts MDBX, then runs the pipeline to rebuild them.
|
||||
|
||||
use crate::common::CliNodeTypes;
|
||||
use clap::Parser;
|
||||
use reth_db::{
|
||||
mdbx::{self, ffi},
|
||||
models::StorageBeforeTx,
|
||||
DatabaseEnv,
|
||||
};
|
||||
use reth_db_api::{
|
||||
cursor::DbCursorRO,
|
||||
database::Database,
|
||||
table::Table,
|
||||
tables,
|
||||
transaction::{DbTx, DbTxMut},
|
||||
};
|
||||
use reth_node_builder::NodeTypesWithDBAdapter;
|
||||
use reth_provider::{
|
||||
providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory, MetadataProvider,
|
||||
MetadataWriter, ProviderFactory, PruneCheckpointReader, StageCheckpointWriter,
|
||||
StaticFileProviderFactory, StaticFileWriter, StorageSettings,
|
||||
};
|
||||
use reth_prune_types::PruneSegment;
|
||||
use reth_stages_types::{StageCheckpoint, StageId};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_storage_api::StageCheckpointReader;
|
||||
use tracing::info;
|
||||
|
||||
/// `reth db migrate-v2` command
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command;
|
||||
|
||||
impl Command {
|
||||
/// Execute the full v1 → v2 migration:
|
||||
///
|
||||
/// 1. Migrate changesets + receipts to static files
|
||||
/// 2. Flip `StorageSettings` to v2
|
||||
/// 3. Clear recomputable MDBX tables + reset stage checkpoints
|
||||
/// 4. Compact MDBX
|
||||
pub async fn execute<N: CliNodeTypes>(
|
||||
self,
|
||||
provider_factory: ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N::Primitives: reth_primitives_traits::NodePrimitives<
|
||||
Receipt: reth_db_api::table::Value + reth_codecs::Compact,
|
||||
>,
|
||||
{
|
||||
// === Phase 0: Preflight ===
|
||||
info!(target: "reth::cli", "Starting v1 → v2 storage migration");
|
||||
|
||||
let provider = provider_factory.provider()?;
|
||||
let current_settings = provider.storage_settings()?;
|
||||
|
||||
if current_settings.is_some_and(|s| s.is_v2()) {
|
||||
info!(target: "reth::cli", "Storage is already v2, nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tip =
|
||||
provider.get_stage_checkpoint(StageId::Execution)?.map(|c| c.block_number).unwrap_or(0);
|
||||
|
||||
info!(target: "reth::cli", tip, "Chain tip block number");
|
||||
|
||||
let sf_provider = provider_factory.static_file_provider();
|
||||
|
||||
for segment in [StaticFileSegment::AccountChangeSets, StaticFileSegment::StorageChangeSets]
|
||||
{
|
||||
if sf_provider.get_highest_static_file_block(segment).is_some() {
|
||||
eyre::bail!(
|
||||
"Static file segment {segment:?} already contains data. \
|
||||
Cannot migrate — target must be empty."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
drop(provider);
|
||||
|
||||
// === Phase 1: Migrate changesets → static files ===
|
||||
Self::migrate_account_changesets(&provider_factory, tip)?;
|
||||
Self::migrate_storage_changesets(&provider_factory, tip)?;
|
||||
|
||||
// === Phase 2: Migrate receipts → static files ===
|
||||
Self::migrate_receipts::<NodeTypesWithDBAdapter<N, DatabaseEnv>>(&provider_factory, tip)?;
|
||||
|
||||
// === Phase 3: Flip metadata to v2 ===
|
||||
info!(target: "reth::cli", "Writing StorageSettings v2 metadata");
|
||||
{
|
||||
let provider_rw = provider_factory.database_provider_rw()?;
|
||||
provider_rw.write_storage_settings(StorageSettings::v2())?;
|
||||
provider_rw.commit()?;
|
||||
}
|
||||
info!(target: "reth::cli", "Storage settings updated to v2");
|
||||
|
||||
// === Phase 4: Clear recomputable tables ===
|
||||
Self::clear_recomputable_tables(&provider_factory)?;
|
||||
|
||||
// === Phase 5: Compact MDBX (before pipeline, so it runs on a smaller DB) ===
|
||||
let db_path = provider_factory.db_ref().path();
|
||||
Self::compact_mdbx(provider_factory.db_ref())?;
|
||||
|
||||
// Drop to release DB handle for swap
|
||||
drop(provider_factory);
|
||||
|
||||
let compact_path = db_path.with_file_name("db_compact");
|
||||
Self::swap_compacted_db(&db_path, &compact_path)?;
|
||||
|
||||
// === Phase 6: Reopen DB and run pipeline ===
|
||||
// The caller will reopen the environment and run the pipeline.
|
||||
// We return here — the pipeline step is handled in mod.rs after
|
||||
// reopening the database with the compacted copy.
|
||||
info!(target: "reth::cli", "Migration complete. You should now restart the node and let it run the pipeline to rebuild the remaining data.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_account_changesets<N: ProviderNodeTypes>(
|
||||
factory: &ProviderFactory<N>,
|
||||
tip: u64,
|
||||
) -> eyre::Result<()> {
|
||||
info!(target: "reth::cli", "Migrating AccountChangeSets → static files");
|
||||
let provider = factory.provider()?.disable_long_read_transaction_safety();
|
||||
let sf_provider = factory.static_file_provider();
|
||||
|
||||
let mut cursor = provider.tx_ref().cursor_read::<tables::AccountChangeSets>()?;
|
||||
|
||||
let first_block = provider
|
||||
.get_prune_checkpoint(PruneSegment::AccountHistory)?
|
||||
.and_then(|cp| cp.block_number)
|
||||
.map_or(0, |b| b + 1);
|
||||
|
||||
let mut writer =
|
||||
sf_provider.get_writer(first_block, StaticFileSegment::AccountChangeSets)?;
|
||||
|
||||
let mut count = 0u64;
|
||||
let mut walker = cursor.walk(Some(first_block))?.peekable();
|
||||
|
||||
for block in first_block..=tip {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
while let Some(Ok((block_number, _))) = walker.peek() {
|
||||
if *block_number != block {
|
||||
break;
|
||||
}
|
||||
let (_, entry) = walker.next().expect("peeked")?;
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
count += entries.len() as u64;
|
||||
writer.append_account_changeset(entries, block)?;
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
|
||||
info!(target: "reth::cli", count, "AccountChangeSets migrated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_storage_changesets<N: ProviderNodeTypes>(
|
||||
factory: &ProviderFactory<N>,
|
||||
tip: u64,
|
||||
) -> eyre::Result<()> {
|
||||
info!(target: "reth::cli", "Migrating StorageChangeSets → static files");
|
||||
let provider = factory.provider()?.disable_long_read_transaction_safety();
|
||||
let sf_provider = factory.static_file_provider();
|
||||
|
||||
let mut cursor = provider.tx_ref().cursor_read::<tables::StorageChangeSets>()?;
|
||||
|
||||
let first_block = provider
|
||||
.get_prune_checkpoint(PruneSegment::StorageHistory)?
|
||||
.and_then(|cp| cp.block_number)
|
||||
.map_or(0, |b| b + 1);
|
||||
|
||||
let mut writer =
|
||||
sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?;
|
||||
|
||||
let mut count = 0u64;
|
||||
let mut walker = cursor.walk(Some(Default::default()))?.peekable();
|
||||
|
||||
for block in first_block..=tip {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
while let Some(Ok((key, _))) = walker.peek() {
|
||||
if key.block_number() != block {
|
||||
break;
|
||||
}
|
||||
let (key, entry) = walker.next().expect("peeked")?;
|
||||
entries.push(StorageBeforeTx {
|
||||
address: key.address(),
|
||||
key: entry.key,
|
||||
value: entry.value,
|
||||
});
|
||||
}
|
||||
|
||||
count += entries.len() as u64;
|
||||
writer.append_storage_changeset(entries, block)?;
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
|
||||
info!(target: "reth::cli", count, "StorageChangeSets migrated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_receipts<N: ProviderNodeTypes>(
|
||||
factory: &ProviderFactory<N>,
|
||||
tip: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N::Primitives: reth_primitives_traits::NodePrimitives<
|
||||
Receipt: reth_db_api::table::Value + reth_codecs::Compact,
|
||||
>,
|
||||
{
|
||||
let provider = factory.provider()?;
|
||||
if !provider.prune_modes_ref().receipts_log_filter.is_empty() {
|
||||
info!(target: "reth::cli", "Receipt log filter pruning is enabled, keeping receipts in MDBX");
|
||||
return Ok(());
|
||||
}
|
||||
drop(provider);
|
||||
|
||||
let sf_provider = factory.static_file_provider();
|
||||
let existing = sf_provider.get_highest_static_file_block(StaticFileSegment::Receipts);
|
||||
|
||||
if existing.is_some_and(|b| b >= tip) {
|
||||
info!(target: "reth::cli", "Receipts already in static files, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "Migrating Receipts → static files");
|
||||
|
||||
let provider = factory.provider()?.disable_long_read_transaction_safety();
|
||||
let prune_start = provider
|
||||
.get_prune_checkpoint(PruneSegment::Receipts)?
|
||||
.and_then(|cp| cp.block_number)
|
||||
.map_or(0, |b| b + 1);
|
||||
let first_block = prune_start.max(existing.map_or(0, |b| b + 1));
|
||||
|
||||
let block_range = first_block..=tip;
|
||||
|
||||
let segment = reth_static_file::segments::Receipts;
|
||||
reth_static_file::segments::Segment::copy_to_static_files(&segment, provider, block_range)?;
|
||||
|
||||
sf_provider.commit()?;
|
||||
|
||||
info!(target: "reth::cli", "Receipts migrated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears tables that can be recomputed by the pipeline and resets their
|
||||
/// stage checkpoints.
|
||||
fn clear_recomputable_tables<N: ProviderNodeTypes>(
|
||||
factory: &ProviderFactory<N>,
|
||||
) -> eyre::Result<()> {
|
||||
info!(target: "reth::cli", "Clearing recomputable MDBX tables");
|
||||
let db = factory.db_ref();
|
||||
|
||||
macro_rules! clear_table {
|
||||
($table:ty) => {{
|
||||
let tx = db.tx_mut()?;
|
||||
tx.clear::<$table>()?;
|
||||
tx.commit()?;
|
||||
info!(target: "reth::cli", table = <$table as Table>::NAME, "Cleared");
|
||||
}};
|
||||
}
|
||||
|
||||
// Migrated changeset tables (now in static files)
|
||||
clear_table!(tables::AccountChangeSets);
|
||||
clear_table!(tables::StorageChangeSets);
|
||||
|
||||
// Senders — rebuilt by SenderRecovery
|
||||
clear_table!(tables::TransactionSenders);
|
||||
|
||||
// Indices — rebuilt by TransactionLookup / IndexAccountHistory / IndexStorageHistory
|
||||
clear_table!(tables::TransactionHashNumbers);
|
||||
clear_table!(tables::AccountsHistory);
|
||||
clear_table!(tables::StoragesHistory);
|
||||
|
||||
// Plain state — superseded by hashed state in v2
|
||||
clear_table!(tables::PlainAccountState);
|
||||
clear_table!(tables::PlainStorageState);
|
||||
|
||||
// Trie — rebuilt by MerkleExecute
|
||||
clear_table!(tables::AccountsTrie);
|
||||
clear_table!(tables::StoragesTrie);
|
||||
|
||||
// Reset stage checkpoints so the pipeline rebuilds everything
|
||||
info!(target: "reth::cli", "Resetting stage checkpoints");
|
||||
let provider_rw = factory.database_provider_rw()?;
|
||||
for stage in [
|
||||
StageId::SenderRecovery,
|
||||
StageId::TransactionLookup,
|
||||
StageId::IndexAccountHistory,
|
||||
StageId::IndexStorageHistory,
|
||||
StageId::MerkleExecute,
|
||||
StageId::MerkleUnwind,
|
||||
] {
|
||||
provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(0))?;
|
||||
info!(target: "reth::cli", %stage, "Checkpoint reset to 0");
|
||||
}
|
||||
provider_rw.save_stage_checkpoint_progress(StageId::MerkleExecute, vec![])?;
|
||||
provider_rw.commit()?;
|
||||
|
||||
info!(target: "reth::cli", "Recomputable tables cleared");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a compacted copy of the MDBX database.
|
||||
fn compact_mdbx(db: &mdbx::DatabaseEnv) -> eyre::Result<()> {
|
||||
let db_path = db.path();
|
||||
let compact_path = db_path.with_file_name("db_compact");
|
||||
|
||||
reth_fs_util::create_dir_all(&compact_path)?;
|
||||
|
||||
info!(target: "reth::cli", ?db_path, ?compact_path, "Compacting MDBX database");
|
||||
|
||||
let compact_dest = compact_path.join("mdbx.dat");
|
||||
let dest_cstr = std::ffi::CString::new(
|
||||
compact_dest.to_str().ok_or_else(|| eyre::eyre!("compact path must be valid UTF-8"))?,
|
||||
)?;
|
||||
|
||||
let flags = ffi::MDBX_CP_COMPACT | ffi::MDBX_CP_FORCE_DYNAMIC_SIZE;
|
||||
|
||||
let rc = db.with_raw_env_ptr(|env_ptr| unsafe {
|
||||
ffi::mdbx_env_copy(env_ptr, dest_cstr.as_ptr(), flags)
|
||||
});
|
||||
|
||||
if rc != 0 {
|
||||
eyre::bail!("mdbx_env_copy failed with error code {rc}: {}", unsafe {
|
||||
std::ffi::CStr::from_ptr(ffi::mdbx_strerror(rc)).to_string_lossy()
|
||||
});
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "MDBX compaction complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Swaps the original MDBX database with a compacted copy.
|
||||
fn swap_compacted_db(
|
||||
db_path: &std::path::Path,
|
||||
compact_path: &std::path::Path,
|
||||
) -> eyre::Result<()> {
|
||||
let backup_path = db_path.with_file_name("db_pre_compact");
|
||||
|
||||
info!(target: "reth::cli", ?db_path, ?compact_path, "Swapping compacted database");
|
||||
|
||||
std::fs::rename(db_path, &backup_path)?;
|
||||
|
||||
if let Err(e) = std::fs::rename(compact_path, db_path) {
|
||||
let _ = std::fs::rename(&backup_path, db_path);
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(&backup_path)?;
|
||||
|
||||
info!(target: "reth::cli", "Database compaction swap complete");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ mod copy;
|
||||
mod diff;
|
||||
mod get;
|
||||
mod list;
|
||||
mod migrate_v2;
|
||||
mod prune_checkpoints;
|
||||
mod repair_trie;
|
||||
mod settings;
|
||||
@@ -77,6 +78,9 @@ pub enum Subcommands {
|
||||
AccountStorage(account_storage::Command),
|
||||
/// Gets account state and storage at a specific block
|
||||
State(state::Command),
|
||||
/// Migrate storage layout from v1 (MDBX-only) to v2 (static files + RocksDB)
|
||||
#[command(name = "migrate-v2")]
|
||||
MigrateV2(migrate_v2::Command),
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
|
||||
@@ -231,6 +235,13 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::MigrateV2(command) => {
|
||||
let Environment { provider_factory, .. } =
|
||||
self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
|
||||
|
||||
// Migrate changesets+receipts, clear tables, compact MDBX
|
||||
command.execute::<N>(provider_factory).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
368
crates/cli/commands/src/download/archive.rs
Normal file
368
crates/cli/commands/src/download/archive.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
use super::{
|
||||
extract::{extract_archive_raw, streaming_download_and_extract, CompressionFormat},
|
||||
fetch::ArchiveFetcher,
|
||||
manifest::SnapshotArchive,
|
||||
planning::{PlannedArchive, PlannedDownloads},
|
||||
progress::{
|
||||
spawn_progress_display, ArchiveDownloadProgress, ArchiveExtractionProgress,
|
||||
ArchiveVerificationProgress, DownloadRequestLimiter, SharedProgress,
|
||||
},
|
||||
session::{ArchiveProcessContext, DownloadSession},
|
||||
verify::OutputVerifier,
|
||||
MAX_DOWNLOAD_RETRIES, RETRY_BACKOFF_SECS,
|
||||
};
|
||||
use eyre::Result;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use reth_fs_util as fs;
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::task;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
const DOWNLOAD_CACHE_DIR: &str = ".download-cache";
|
||||
|
||||
/// Runs all planned modular archive downloads for one command invocation.
|
||||
pub(crate) async fn run_modular_downloads(
|
||||
planned_downloads: PlannedDownloads,
|
||||
target_dir: &Path,
|
||||
download_concurrency: usize,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let download_cache_dir = target_dir.join(DOWNLOAD_CACHE_DIR);
|
||||
fs::create_dir_all(&download_cache_dir)?;
|
||||
|
||||
let shared = SharedProgress::new(
|
||||
planned_downloads.total_download_size,
|
||||
planned_downloads.total_output_size,
|
||||
planned_downloads.total_archives() as u64,
|
||||
cancel_token.clone(),
|
||||
);
|
||||
let session = DownloadSession::new(
|
||||
Some(Arc::clone(&shared)),
|
||||
Some(DownloadRequestLimiter::new(download_concurrency)),
|
||||
cancel_token,
|
||||
);
|
||||
let ctx =
|
||||
ArchiveProcessContext::new(target_dir.to_path_buf(), Some(download_cache_dir), session);
|
||||
|
||||
ModularDownloadJob::new(ctx, download_concurrency).run(planned_downloads).await
|
||||
}
|
||||
|
||||
/// Schedules modular archive work for one run of `reth download`.
|
||||
struct ModularDownloadJob {
|
||||
/// Shared paths and session state for each archive in this job.
|
||||
ctx: ArchiveProcessContext,
|
||||
/// Maximum number of archives processed at once.
|
||||
archive_concurrency: usize,
|
||||
}
|
||||
|
||||
impl ModularDownloadJob {
|
||||
/// Creates the modular download job for one command run.
|
||||
const fn new(ctx: ArchiveProcessContext, archive_concurrency: usize) -> Self {
|
||||
Self { ctx, archive_concurrency }
|
||||
}
|
||||
|
||||
/// Runs all planned archives and waits for the shared progress task to finish.
|
||||
async fn run(self, planned_downloads: PlannedDownloads) -> Result<()> {
|
||||
let shared = Arc::clone(
|
||||
self.ctx.session().progress().expect("modular downloads always use shared progress"),
|
||||
);
|
||||
let progress_handle = spawn_progress_display(Arc::clone(&shared));
|
||||
let ctx = self.ctx.clone();
|
||||
let results: Vec<Result<()>> = stream::iter(planned_downloads.archives)
|
||||
.map(move |archive| {
|
||||
let ctx = ctx.clone();
|
||||
async move { Self::process_archive(ctx, archive).await }
|
||||
})
|
||||
.buffer_unordered(self.archive_concurrency)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
shared.done.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
for result in results {
|
||||
result?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs one archive on the blocking pool so fetch and extraction stay off the async executor.
|
||||
async fn process_archive(ctx: ArchiveProcessContext, archive: PlannedArchive) -> Result<()> {
|
||||
task::spawn_blocking(move || ArchiveProcessor::new(archive, ctx).run()).await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicit retry states for one modular archive.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ArchiveAttemptState {
|
||||
/// Start or restart one full archive attempt.
|
||||
RunAttempt,
|
||||
/// Check whether the extracted outputs verify.
|
||||
VerifyOutputs,
|
||||
/// Wait and decide whether another full attempt should run.
|
||||
RetryAttempt,
|
||||
/// Finish successfully.
|
||||
Complete,
|
||||
/// Stop with an error after retries are exhausted.
|
||||
Fail,
|
||||
}
|
||||
|
||||
/// Processes one modular archive from reuse check through extraction and verification.
|
||||
struct ArchiveProcessor {
|
||||
/// The concrete archive and component being processed.
|
||||
archive: PlannedArchive,
|
||||
/// Shared paths and session state for this archive attempt.
|
||||
ctx: ArchiveProcessContext,
|
||||
}
|
||||
|
||||
impl ArchiveProcessor {
|
||||
/// Creates a processor for one archive and the shared download context.
|
||||
fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
|
||||
Self { archive, ctx }
|
||||
}
|
||||
|
||||
/// Runs the archive retry state machine until outputs are verified or retries are exhausted.
|
||||
fn run(self) -> Result<()> {
|
||||
let archive = self.archive();
|
||||
if self.try_reuse_outputs()? {
|
||||
info!(target: "reth::cli", file = %archive.file_name, component = %self.archive.component, "Skipping already verified plain files");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = ArchiveMode::new(&self.ctx)?;
|
||||
let format = CompressionFormat::from_url(&archive.file_name)?;
|
||||
let mut attempt = 1;
|
||||
let mut last_error: Option<eyre::Error> = None;
|
||||
let mut state = ArchiveAttemptState::RunAttempt;
|
||||
|
||||
loop {
|
||||
match state {
|
||||
ArchiveAttemptState::RunAttempt => {
|
||||
self.cleanup_outputs();
|
||||
|
||||
if attempt > 1 {
|
||||
info!(target: "reth::cli",
|
||||
file = %archive.file_name,
|
||||
component = %self.archive.component,
|
||||
attempt,
|
||||
max = MAX_DOWNLOAD_RETRIES,
|
||||
"Retrying archive from scratch"
|
||||
);
|
||||
}
|
||||
|
||||
match self.run_attempt(mode, format) {
|
||||
Ok(()) => state = ArchiveAttemptState::VerifyOutputs,
|
||||
Err(error) if mode.retries_fetch_errors() => {
|
||||
warn!(target: "reth::cli",
|
||||
file = %archive.file_name,
|
||||
component = %self.archive.component,
|
||||
attempt,
|
||||
err = %format_args!("{error:#}"),
|
||||
"Archive attempt failed, retrying from scratch"
|
||||
);
|
||||
last_error = Some(error);
|
||||
state = ArchiveAttemptState::RetryAttempt;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
ArchiveAttemptState::VerifyOutputs => {
|
||||
if self.verify_outputs_with_progress()? {
|
||||
state = ArchiveAttemptState::Complete;
|
||||
} else {
|
||||
warn!(target: "reth::cli", file = %archive.file_name, component = %self.archive.component, attempt, "Archive extracted, but output verification failed, retrying");
|
||||
state = ArchiveAttemptState::RetryAttempt;
|
||||
}
|
||||
}
|
||||
ArchiveAttemptState::RetryAttempt => {
|
||||
if attempt >= MAX_DOWNLOAD_RETRIES {
|
||||
state = ArchiveAttemptState::Fail;
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
attempt += 1;
|
||||
state = ArchiveAttemptState::RunAttempt;
|
||||
}
|
||||
}
|
||||
ArchiveAttemptState::Complete => return Ok(()),
|
||||
ArchiveAttemptState::Fail => {
|
||||
if let Some(error) = last_error {
|
||||
return Err(error.wrap_err(format!(
|
||||
"Failed after {} attempts for {}",
|
||||
MAX_DOWNLOAD_RETRIES, archive.file_name
|
||||
)));
|
||||
}
|
||||
|
||||
eyre::bail!(
|
||||
"Failed integrity validation after {} attempts for {}",
|
||||
MAX_DOWNLOAD_RETRIES,
|
||||
archive.file_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the concrete archive being fetched or verified.
|
||||
fn archive(&self) -> &SnapshotArchive {
|
||||
&self.archive.archive
|
||||
}
|
||||
|
||||
/// Returns the verifier for this archive's output files.
|
||||
fn output_verifier(&self) -> OutputVerifier<'_> {
|
||||
OutputVerifier::new(self.ctx.target_dir())
|
||||
}
|
||||
|
||||
/// Returns `true` if this archive can be reused from existing verified outputs.
|
||||
/// Returns `false` if a fresh archive attempt is still needed.
|
||||
fn try_reuse_outputs(&self) -> Result<bool> {
|
||||
if self.verify_outputs()? {
|
||||
self.mark_complete();
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Removes any partial outputs before a fresh archive attempt.
|
||||
fn cleanup_outputs(&self) {
|
||||
self.output_verifier().cleanup(&self.archive().output_files);
|
||||
}
|
||||
|
||||
/// Returns `true` if all declared plain outputs verify.
|
||||
/// Returns `false` if any output is missing or does not match.
|
||||
fn verify_outputs(&self) -> Result<bool> {
|
||||
self.output_verifier().verify(&self.archive().output_files)
|
||||
}
|
||||
|
||||
/// Records archive completion in shared progress once outputs verify.
|
||||
fn mark_complete(&self) {
|
||||
self.ctx.session().record_reused_archive(self.archive().size, self.archive().output_size());
|
||||
}
|
||||
|
||||
/// Executes one archive attempt according to the selected cache-vs-stream mode.
|
||||
fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
|
||||
mode.execute(self, format)
|
||||
}
|
||||
|
||||
/// Downloads the archive into the cache, then extracts from the cached file.
|
||||
fn run_cached_attempt(&self, format: CompressionFormat) -> Result<()> {
|
||||
let cache_dir =
|
||||
self.ctx.cache_dir().ok_or_else(|| eyre::eyre!("Missing download cache directory"))?;
|
||||
let fetcher =
|
||||
ArchiveFetcher::new(self.archive().url.clone(), cache_dir, self.ctx.session().clone());
|
||||
|
||||
if self.archive.ty == super::manifest::SnapshotComponentType::State {
|
||||
debug!(target: "reth::cli", url = %self.archive().url, "Downloading state snapshot archive");
|
||||
}
|
||||
|
||||
let download_result = {
|
||||
let mut download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
|
||||
let result = fetcher.download(Some(&mut download_progress));
|
||||
if let Ok(ref downloaded) = result &&
|
||||
download_progress.has_tracked_bytes()
|
||||
{
|
||||
download_progress.complete(downloaded.size);
|
||||
}
|
||||
result
|
||||
};
|
||||
|
||||
let downloaded = match download_result {
|
||||
Ok(downloaded) => downloaded,
|
||||
Err(error) => {
|
||||
fetcher.cleanup_downloaded_files();
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
info!(target: "reth::cli",
|
||||
file = %self.archive().file_name,
|
||||
component = %self.archive.component,
|
||||
size = %super::progress::DownloadProgress::format_size(downloaded.size),
|
||||
"Archive download complete"
|
||||
);
|
||||
|
||||
let extract_result = self.extract_cached_archive(&downloaded.path, format);
|
||||
fetcher.cleanup_downloaded_files();
|
||||
extract_result
|
||||
}
|
||||
|
||||
/// Streams the archive directly into extraction without keeping a cached copy.
|
||||
fn run_streaming_attempt(&self, format: CompressionFormat) -> Result<()> {
|
||||
let _download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
|
||||
streaming_download_and_extract(
|
||||
&self.archive().url,
|
||||
format,
|
||||
self.ctx.target_dir(),
|
||||
self.ctx.session(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Extracts a cached archive file while updating shared extraction activity.
|
||||
fn extract_cached_archive(&self, archive_path: &Path, format: CompressionFormat) -> Result<()> {
|
||||
let mut extraction_progress = ArchiveExtractionProgress::new(self.ctx.session().progress());
|
||||
let file = fs::open(archive_path)?;
|
||||
let result = extract_archive_raw(
|
||||
file,
|
||||
format,
|
||||
self.ctx.target_dir(),
|
||||
Some(&mut extraction_progress),
|
||||
);
|
||||
extraction_progress.finish();
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns `true` if all declared plain outputs verify while updating shared verification
|
||||
/// progress.
|
||||
fn verify_outputs_with_progress(&self) -> Result<bool> {
|
||||
let mut verification_progress =
|
||||
ArchiveVerificationProgress::new(self.ctx.session().progress());
|
||||
let verified = self
|
||||
.output_verifier()
|
||||
.verify_with_progress(&self.archive().output_files, Some(&mut verification_progress))?;
|
||||
if verified {
|
||||
verification_progress.complete(self.archive().output_size());
|
||||
}
|
||||
Ok(verified)
|
||||
}
|
||||
}
|
||||
|
||||
/// Chooses whether an archive attempt uses the cache or streams directly.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ArchiveMode {
|
||||
/// Download the archive to the cache, then extract it.
|
||||
Cached,
|
||||
/// Stream the archive directly into extraction.
|
||||
Streaming,
|
||||
}
|
||||
|
||||
impl ArchiveMode {
|
||||
/// Picks the archive mode from the process context.
|
||||
fn new(ctx: &ArchiveProcessContext) -> Result<Self> {
|
||||
if ctx.cache_dir().is_some() {
|
||||
ctx.session().require_request_limiter()?;
|
||||
return Ok(Self::Cached)
|
||||
}
|
||||
|
||||
Ok(Self::Streaming)
|
||||
}
|
||||
|
||||
/// Returns `true` when fetch failures should retry the whole archive attempt.
|
||||
const fn retries_fetch_errors(&self) -> bool {
|
||||
matches!(self, Self::Cached)
|
||||
}
|
||||
|
||||
/// Runs the selected archive mode for a single attempt.
|
||||
fn execute(&self, processor: &ArchiveProcessor, format: CompressionFormat) -> Result<()> {
|
||||
match self {
|
||||
Self::Cached => processor.run_cached_attempt(format),
|
||||
Self::Streaming => processor.run_streaming_attempt(format),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +270,7 @@ pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Formats one prune mode for the generated config summary.
|
||||
fn format_mode(mode: &PruneMode) -> String {
|
||||
match mode {
|
||||
PruneMode::Full => "\"full\"".to_string(),
|
||||
|
||||
490
crates/cli/commands/src/download/extract.rs
Normal file
490
crates/cli/commands/src/download/extract.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
use super::{
|
||||
fetch::{ArchiveFetcher, DownloadedArchive},
|
||||
progress::{
|
||||
ArchiveExtractionProgress, ArchiveExtractionProgressHandle, DownloadProgress,
|
||||
DownloadRequestLimiter, ProgressReader, SharedProgress, SharedProgressReader,
|
||||
},
|
||||
session::DownloadSession,
|
||||
MAX_DOWNLOAD_RETRIES, RETRY_BACKOFF_SECS,
|
||||
};
|
||||
use eyre::{Result, WrapErr};
|
||||
use lz4::Decoder;
|
||||
use reqwest::blocking::Client as BlockingClient;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use reth_fs_util as fs;
|
||||
use std::{
|
||||
io::Read,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tar::Archive;
|
||||
use tokio::task;
|
||||
use tracing::{info, warn};
|
||||
use url::Url;
|
||||
use zstd::stream::read::Decoder as ZstdDecoder;
|
||||
|
||||
const EXTENSION_TAR_LZ4: &str = ".tar.lz4";
|
||||
const EXTENSION_TAR_ZSTD: &str = ".tar.zst";
|
||||
const STREAMING_EXTRACTION_PROGRESS_MIN_FILE_SIZE: u64 = 64 * 1024 * 1024;
|
||||
const EXTRACTION_PROGRESS_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Supported compression formats for snapshots
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum CompressionFormat {
|
||||
/// LZ4-compressed tar archive.
|
||||
Lz4,
|
||||
/// Zstandard-compressed tar archive.
|
||||
Zstd,
|
||||
}
|
||||
|
||||
impl CompressionFormat {
|
||||
/// Detect compression format from file extension
|
||||
pub(crate) fn from_url(url: &str) -> Result<Self> {
|
||||
let path =
|
||||
Url::parse(url).map(|u| u.path().to_string()).unwrap_or_else(|_| url.to_string());
|
||||
|
||||
if path.ends_with(EXTENSION_TAR_LZ4) {
|
||||
Ok(Self::Lz4)
|
||||
} else if path.ends_with(EXTENSION_TAR_ZSTD) {
|
||||
Ok(Self::Zstd)
|
||||
} else {
|
||||
Err(eyre::eyre!(
|
||||
"Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}",
|
||||
path
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a compressed tar archive to the target directory with progress tracking.
|
||||
fn extract_archive<R: Read>(
|
||||
reader: R,
|
||||
total_size: u64,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let progress_reader = ProgressReader::new(reader, total_size, cancel_token);
|
||||
|
||||
match format {
|
||||
CompressionFormat::Lz4 => {
|
||||
let decoder = Decoder::new(progress_reader)?;
|
||||
Archive::new(decoder).unpack(target_dir)?;
|
||||
}
|
||||
CompressionFormat::Zstd => {
|
||||
let decoder = ZstdDecoder::new(progress_reader)?;
|
||||
Archive::new(decoder).unpack(target_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts a compressed tar archive without progress tracking.
|
||||
pub(crate) fn extract_archive_raw<R: Read>(
|
||||
reader: R,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
progress: Option<&mut ArchiveExtractionProgress>,
|
||||
) -> Result<()> {
|
||||
match format {
|
||||
CompressionFormat::Lz4 => {
|
||||
unpack_archive(Archive::new(Decoder::new(reader)?), target_dir, progress)?;
|
||||
}
|
||||
CompressionFormat::Zstd => {
|
||||
unpack_archive(Archive::new(ZstdDecoder::new(reader)?), target_dir, progress)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unpack_archive<R: Read>(
|
||||
mut archive: Archive<R>,
|
||||
target_dir: &Path,
|
||||
mut progress: Option<&mut ArchiveExtractionProgress>,
|
||||
) -> Result<()> {
|
||||
let entries = archive.entries().wrap_err_with(|| {
|
||||
format!("failed to read archive entries for `{}`", target_dir.display())
|
||||
})?;
|
||||
|
||||
for entry in entries {
|
||||
let mut entry = entry.wrap_err_with(|| {
|
||||
format!("failed to read archive entry for `{}`", target_dir.display())
|
||||
})?;
|
||||
extract_entry_with_progress(&mut entry, target_dir, progress.as_deref_mut())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_entry_with_progress<R: Read>(
|
||||
entry: &mut tar::Entry<'_, R>,
|
||||
target_dir: &Path,
|
||||
progress: Option<&mut ArchiveExtractionProgress>,
|
||||
) -> Result<()> {
|
||||
let size = entry.header().entry_size().unwrap_or(0);
|
||||
let entry_type = entry.header().entry_type();
|
||||
|
||||
if !entry_type.is_file() || size == 0 {
|
||||
entry.unpack_in(target_dir).wrap_err_with(|| {
|
||||
format!("failed to extract archive into `{}`", target_dir.display())
|
||||
})?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if size < STREAMING_EXTRACTION_PROGRESS_MIN_FILE_SIZE {
|
||||
entry.unpack_in(target_dir).wrap_err_with(|| {
|
||||
format!("failed to extract archive into `{}`", target_dir.display())
|
||||
})?;
|
||||
if let Some(progress) = progress {
|
||||
progress.record_extracted(size);
|
||||
}
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let Some(progress_handle) = progress.as_ref().and_then(|progress| progress.handle()) else {
|
||||
entry.unpack_in(target_dir).wrap_err_with(|| {
|
||||
format!("failed to extract archive into `{}`", target_dir.display())
|
||||
})?;
|
||||
return Ok(())
|
||||
};
|
||||
|
||||
let Some(entry_path) = entry_destination_path(entry, target_dir)? else {
|
||||
entry.unpack_in(target_dir).wrap_err_with(|| {
|
||||
format!("failed to extract archive into `{}`", target_dir.display())
|
||||
})?;
|
||||
return Ok(())
|
||||
};
|
||||
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let monitor = spawn_extraction_progress_monitor(entry_path, progress_handle, Arc::clone(&stop));
|
||||
let unpack_result = entry
|
||||
.unpack_in(target_dir)
|
||||
.wrap_err_with(|| format!("failed to extract archive into `{}`", target_dir.display()));
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
|
||||
let monitor_result = monitor.join();
|
||||
unpack_result?;
|
||||
|
||||
monitor_result.map_err(|_| eyre::eyre!("extraction progress monitor panicked"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn entry_destination_path<R: Read>(
|
||||
entry: &tar::Entry<'_, R>,
|
||||
target_dir: &Path,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
let mut file_dst = target_dir.to_path_buf();
|
||||
let path = entry.path().wrap_err("invalid path in archive entry")?;
|
||||
|
||||
for part in path.components() {
|
||||
match part {
|
||||
Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
|
||||
Component::ParentDir => return Ok(None),
|
||||
Component::Normal(part) => file_dst.push(part),
|
||||
}
|
||||
}
|
||||
|
||||
if file_dst == target_dir {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
Ok(Some(file_dst))
|
||||
}
|
||||
|
||||
fn spawn_extraction_progress_monitor(
|
||||
entry_path: PathBuf,
|
||||
progress: ArchiveExtractionProgressHandle,
|
||||
stop: Arc<AtomicBool>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut extracted = 0_u64;
|
||||
|
||||
loop {
|
||||
record_extracted_file_bytes(&entry_path, &progress, &mut extracted);
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(EXTRACTION_PROGRESS_POLL_INTERVAL);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn record_extracted_file_bytes(
|
||||
entry_path: &Path,
|
||||
progress: &ArchiveExtractionProgressHandle,
|
||||
extracted: &mut u64,
|
||||
) {
|
||||
let Ok(meta) = fs::metadata(entry_path) else { return };
|
||||
let len = meta.len();
|
||||
if len > *extracted {
|
||||
progress.record_extracted(len - *extracted);
|
||||
*extracted = len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a snapshot from a local file.
|
||||
fn extract_from_file(path: &Path, format: CompressionFormat, target_dir: &Path) -> Result<()> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let total_size = file.metadata()?.len();
|
||||
info!(target: "reth::cli",
|
||||
file = %path.display(),
|
||||
size = %DownloadProgress::format_size(total_size),
|
||||
"Extracting local archive"
|
||||
);
|
||||
let start = Instant::now();
|
||||
extract_archive(file, total_size, format, target_dir, CancellationToken::new())?;
|
||||
info!(target: "reth::cli",
|
||||
file = %path.display(),
|
||||
elapsed = %DownloadProgress::format_duration(start.elapsed()),
|
||||
"Local extraction complete"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Streams a remote archive directly into the extractor without writing to disk.
|
||||
///
|
||||
/// On failure, retries from scratch up to [`MAX_DOWNLOAD_RETRIES`] times.
|
||||
pub(crate) fn streaming_download_and_extract(
|
||||
url: &str,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
session: &DownloadSession,
|
||||
) -> Result<()> {
|
||||
let shared = session.progress();
|
||||
let quiet = session.progress().is_some();
|
||||
let mut last_error: Option<eyre::Error> = None;
|
||||
|
||||
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
|
||||
if attempt > 1 {
|
||||
info!(target: "reth::cli",
|
||||
url = %url,
|
||||
attempt,
|
||||
max = MAX_DOWNLOAD_RETRIES,
|
||||
"Retrying streaming download from scratch"
|
||||
);
|
||||
}
|
||||
|
||||
let client = BlockingClient::builder().connect_timeout(Duration::from_secs(30)).build()?;
|
||||
let _request_permit = session
|
||||
.request_limiter()
|
||||
.map(|limiter| limiter.acquire(session.progress(), session.cancel_token()))
|
||||
.transpose()?;
|
||||
|
||||
let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
|
||||
Ok(r) => r,
|
||||
Err(error) => {
|
||||
let err = eyre::Error::from(error);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
warn!(target: "reth::cli",
|
||||
url = %url,
|
||||
attempt,
|
||||
max = MAX_DOWNLOAD_RETRIES,
|
||||
err = %err,
|
||||
"Streaming request failed, retrying"
|
||||
);
|
||||
}
|
||||
last_error = Some(err);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !quiet && let Some(size) = response.content_length() {
|
||||
info!(target: "reth::cli",
|
||||
url = %url,
|
||||
size = %DownloadProgress::format_size(size),
|
||||
"Streaming archive"
|
||||
);
|
||||
}
|
||||
|
||||
let result = if let Some(progress) = shared {
|
||||
let reader = SharedProgressReader { inner: response, progress: Arc::clone(progress) };
|
||||
extract_archive_raw(reader, format, target_dir, None)
|
||||
} else {
|
||||
let total_size = response.content_length().unwrap_or(0);
|
||||
extract_archive(
|
||||
response,
|
||||
total_size,
|
||||
format,
|
||||
target_dir,
|
||||
session.cancel_token().clone(),
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => {
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
warn!(target: "reth::cli",
|
||||
url = %url,
|
||||
attempt,
|
||||
max = MAX_DOWNLOAD_RETRIES,
|
||||
err = %error,
|
||||
"Streaming extraction failed, retrying"
|
||||
);
|
||||
}
|
||||
last_error = Some(error);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or_else(|| {
|
||||
eyre::eyre!("Streaming download failed after {MAX_DOWNLOAD_RETRIES} attempts")
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fetches the snapshot from a remote URL with resume support, then extracts it.
|
||||
fn download_and_extract(
|
||||
url: &str,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
session: DownloadSession,
|
||||
) -> Result<()> {
|
||||
let quiet = session.progress().is_some();
|
||||
let fetcher = ArchiveFetcher::new(url.to_string(), target_dir, session.clone());
|
||||
let DownloadedArchive { path: downloaded_path, size: total_size } = fetcher.download(None)?;
|
||||
|
||||
let file_name =
|
||||
downloaded_path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
|
||||
|
||||
if !quiet {
|
||||
info!(target: "reth::cli",
|
||||
file = %file_name,
|
||||
size = %DownloadProgress::format_size(total_size),
|
||||
"Extracting archive"
|
||||
);
|
||||
}
|
||||
let file = fs::open(&downloaded_path)?;
|
||||
|
||||
if quiet {
|
||||
extract_archive_raw(file, format, target_dir, None)?;
|
||||
} else {
|
||||
extract_archive(file, total_size, format, target_dir, session.cancel_token().clone())?;
|
||||
info!(target: "reth::cli",
|
||||
file = %file_name,
|
||||
"Extraction complete"
|
||||
);
|
||||
}
|
||||
|
||||
fetcher.cleanup_downloaded_files();
|
||||
session.record_archive_output_complete(total_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads and extracts a snapshot, blocking until finished.
|
||||
///
|
||||
/// Supports `file://` URLs for local files and HTTP(S) URLs for remote downloads.
|
||||
/// When `resumable` is true, downloads to a `.part` file first with HTTP Range resume
|
||||
/// support. Otherwise streams directly into the extractor.
|
||||
fn blocking_download_and_extract(
|
||||
url: &str,
|
||||
target_dir: &Path,
|
||||
shared: Option<Arc<SharedProgress>>,
|
||||
resumable: bool,
|
||||
request_limiter: Option<Arc<DownloadRequestLimiter>>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let format = CompressionFormat::from_url(url)?;
|
||||
|
||||
if let Ok(parsed_url) = Url::parse(url) &&
|
||||
parsed_url.scheme() == "file"
|
||||
{
|
||||
let session = DownloadSession::new(shared, request_limiter, cancel_token);
|
||||
let file_path = parsed_url
|
||||
.to_file_path()
|
||||
.map_err(|_| eyre::eyre!("Invalid file:// URL path: {}", url))?;
|
||||
let result = extract_from_file(&file_path, format, target_dir);
|
||||
if result.is_ok() {
|
||||
session.record_archive_output_complete(file_path.metadata()?.len());
|
||||
}
|
||||
result
|
||||
} else if let Some(request_limiter) = request_limiter {
|
||||
download_and_extract(
|
||||
url,
|
||||
format,
|
||||
target_dir,
|
||||
DownloadSession::new(shared, Some(request_limiter), cancel_token),
|
||||
)
|
||||
} else if resumable {
|
||||
let session =
|
||||
DownloadSession::new(shared, Some(DownloadRequestLimiter::new(1)), cancel_token);
|
||||
download_and_extract(url, format, target_dir, session)
|
||||
} else {
|
||||
let session = DownloadSession::new(shared, None, cancel_token);
|
||||
let result = streaming_download_and_extract(url, format, target_dir, &session);
|
||||
if result.is_ok() {
|
||||
session.record_archive_output_complete(0);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads and extracts a snapshot archive asynchronously.
|
||||
///
|
||||
/// When `shared` is provided, download progress is reported to the shared
|
||||
/// counter for aggregated display. Otherwise uses a local progress bar.
|
||||
/// When `resumable` is true, uses two-phase download with `.part` files.
|
||||
pub(crate) async fn stream_and_extract(
|
||||
url: &str,
|
||||
target_dir: &Path,
|
||||
shared: Option<Arc<SharedProgress>>,
|
||||
resumable: bool,
|
||||
request_limiter: Option<Arc<DownloadRequestLimiter>>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let target_dir = target_dir.to_path_buf();
|
||||
let url = url.to_string();
|
||||
task::spawn_blocking(move || {
|
||||
blocking_download_and_extract(
|
||||
&url,
|
||||
&target_dir,
|
||||
shared,
|
||||
resumable,
|
||||
request_limiter,
|
||||
cancel_token,
|
||||
)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compression_format_detection() {
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
|
||||
}
|
||||
}
|
||||
1018
crates/cli/commands/src/download/fetch.rs
Normal file
1018
crates/cli/commands/src/download/fetch.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,10 @@ use std::{
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
fn is_zero(value: &u64) -> bool {
|
||||
*value == 0
|
||||
}
|
||||
|
||||
/// A snapshot manifest describes available components for a snapshot at a given block height.
|
||||
///
|
||||
/// Each component is either a single archive (state) or a set of chunked archives (static file
|
||||
@@ -62,6 +66,12 @@ pub struct SingleArchive {
|
||||
pub file: String,
|
||||
/// Compressed archive size in bytes.
|
||||
pub size: u64,
|
||||
/// Total extracted plain-output size in bytes.
|
||||
///
|
||||
/// Older manifests may omit this, in which case downloaders should derive it from
|
||||
/// `output_files`.
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
pub decompressed_size: u64,
|
||||
/// Optional BLAKE3 checksum of the compressed archive.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blake3: Option<String>,
|
||||
@@ -83,6 +93,12 @@ pub struct ChunkedArchive {
|
||||
/// Computed during manifest generation. Older manifests may omit this.
|
||||
#[serde(default)]
|
||||
pub chunk_sizes: Vec<u64>,
|
||||
/// Extracted plain-output size of each chunk in bytes, ordered from first to last.
|
||||
///
|
||||
/// Older manifests may omit this, in which case downloaders should derive it from
|
||||
/// `chunk_output_files`.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub chunk_decompressed_sizes: Vec<u64>,
|
||||
/// Expected extracted plain files per chunk, ordered from first to last.
|
||||
///
|
||||
/// This is the authoritative integrity source for the modular download path.
|
||||
@@ -101,9 +117,9 @@ pub struct OutputFileChecksum {
|
||||
pub blake3: String,
|
||||
}
|
||||
|
||||
/// A single archive with concrete URL and optional integrity metadata.
|
||||
/// A concrete snapshot archive with its download and verification metadata.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ArchiveDescriptor {
|
||||
pub struct SnapshotArchive {
|
||||
pub url: String,
|
||||
pub file_name: String,
|
||||
pub size: u64,
|
||||
@@ -111,6 +127,13 @@ pub struct ArchiveDescriptor {
|
||||
pub output_files: Vec<OutputFileChecksum>,
|
||||
}
|
||||
|
||||
impl SnapshotArchive {
|
||||
/// Returns the total extracted plain-output size for this archive.
|
||||
pub fn output_size(&self) -> u64 {
|
||||
self.output_files.iter().map(|file| file.size).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// How much of a component to download.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ComponentSelection {
|
||||
@@ -315,19 +338,19 @@ impl SnapshotManifest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns concrete archive descriptors for a component, optionally limited to distance.
|
||||
pub fn archive_descriptors_for_distance(
|
||||
/// Returns concrete snapshot archives for a component, optionally limited to distance.
|
||||
pub fn snapshot_archives_for_distance(
|
||||
&self,
|
||||
ty: SnapshotComponentType,
|
||||
distance: Option<u64>,
|
||||
) -> Vec<ArchiveDescriptor> {
|
||||
) -> Vec<SnapshotArchive> {
|
||||
let Some(component) = self.component(ty) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
match component {
|
||||
ComponentManifest::Single(single) => {
|
||||
vec![ArchiveDescriptor {
|
||||
vec![SnapshotArchive {
|
||||
url: format!("{}/{}", self.base_url_or_empty(), single.file),
|
||||
file_name: single.file.clone(),
|
||||
size: single.size,
|
||||
@@ -357,7 +380,7 @@ impl SnapshotManifest {
|
||||
let output_files =
|
||||
chunked.chunk_output_files.get(i as usize).cloned().unwrap_or_default();
|
||||
|
||||
ArchiveDescriptor {
|
||||
SnapshotArchive {
|
||||
url: format!("{}/{}", self.base_url_or_empty(), file_name),
|
||||
file_name,
|
||||
size,
|
||||
@@ -398,6 +421,36 @@ impl SnapshotManifest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the exact extracted plain-output size for a component given a distance selection.
|
||||
pub fn output_size_for_distance(
|
||||
&self,
|
||||
ty: SnapshotComponentType,
|
||||
distance: Option<u64>,
|
||||
) -> u64 {
|
||||
let Some(component) = self.component(ty) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
match component {
|
||||
ComponentManifest::Single(single) => single.output_size(),
|
||||
ComponentManifest::Chunked(chunked) => {
|
||||
let num_chunks = chunked.num_chunks();
|
||||
let start_chunk = match distance {
|
||||
Some(dist) => {
|
||||
let needed = dist.min(chunked.total_blocks);
|
||||
let needed_chunks = needed.div_ceil(chunked.blocks_per_file);
|
||||
num_chunks.saturating_sub(needed_chunks)
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
(start_chunk..num_chunks)
|
||||
.map(|index| chunked.chunk_output_size(index as usize))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of chunks that would be downloaded for a given distance.
|
||||
pub fn chunks_for_distance(&self, ty: SnapshotComponentType, distance: Option<u64>) -> u64 {
|
||||
let Some(ComponentManifest::Chunked(chunked)) = self.component(ty) else {
|
||||
@@ -421,6 +474,14 @@ impl ComponentManifest {
|
||||
Self::Chunked(c) => c.chunk_sizes.iter().sum(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total extracted plain-output size for this component.
|
||||
pub fn total_output_size(&self) -> u64 {
|
||||
match self {
|
||||
Self::Single(single) => single.output_size(),
|
||||
Self::Chunked(chunked) => chunked.total_output_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChunkedArchive {
|
||||
@@ -428,6 +489,39 @@ impl ChunkedArchive {
|
||||
pub fn num_chunks(&self) -> u64 {
|
||||
self.total_blocks.div_ceil(self.blocks_per_file)
|
||||
}
|
||||
|
||||
/// Returns the extracted plain-output size for one chunk.
|
||||
pub fn chunk_output_size(&self, index: usize) -> u64 {
|
||||
self.chunk_decompressed_sizes.get(index).copied().unwrap_or_else(|| {
|
||||
self.chunk_output_files
|
||||
.get(index)
|
||||
.map(|files| files.iter().map(|file| file.size).sum())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the total extracted plain-output size across all chunks.
|
||||
pub fn total_output_size(&self) -> u64 {
|
||||
if !self.chunk_decompressed_sizes.is_empty() {
|
||||
self.chunk_decompressed_sizes.iter().sum()
|
||||
} else {
|
||||
self.chunk_output_files
|
||||
.iter()
|
||||
.map(|files| files.iter().map(|file| file.size).sum::<u64>())
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SingleArchive {
|
||||
/// Returns the total extracted plain-output size for this archive.
|
||||
pub fn output_size(&self) -> u64 {
|
||||
if self.decompressed_size != 0 {
|
||||
self.decompressed_size
|
||||
} else {
|
||||
self.output_files.iter().map(|file| file.size).sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a snapshot manifest from a URL.
|
||||
@@ -516,6 +610,10 @@ pub fn generate_manifest(
|
||||
blocks_per_file,
|
||||
total_blocks: block,
|
||||
chunk_sizes,
|
||||
chunk_decompressed_sizes: chunk_output_files
|
||||
.iter()
|
||||
.map(|files| files.iter().map(|file| file.size).sum())
|
||||
.collect(),
|
||||
chunk_output_files,
|
||||
}),
|
||||
);
|
||||
@@ -532,6 +630,7 @@ pub fn generate_manifest(
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "state.tar.zst".to_string(),
|
||||
size: state_size,
|
||||
decompressed_size: state_output_files.iter().map(|file| file.size).sum(),
|
||||
blake3: None,
|
||||
output_files: state_output_files,
|
||||
}),
|
||||
@@ -546,6 +645,7 @@ pub fn generate_manifest(
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "rocksdb_indices.tar.zst".to_string(),
|
||||
size: rocksdb_size,
|
||||
decompressed_size: rocksdb_output_files.iter().map(|file| file.size).sum(),
|
||||
blake3: None,
|
||||
output_files: rocksdb_output_files,
|
||||
}),
|
||||
@@ -814,6 +914,7 @@ mod tests {
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "state.tar.zst".to_string(),
|
||||
size: 100,
|
||||
decompressed_size: 0,
|
||||
blake3: None,
|
||||
output_files: vec![],
|
||||
}),
|
||||
@@ -824,6 +925,7 @@ mod tests {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 1_500_000,
|
||||
chunk_sizes: vec![80_000, 100_000, 120_000],
|
||||
chunk_decompressed_sizes: vec![],
|
||||
chunk_output_files: vec![vec![], vec![], vec![]],
|
||||
}),
|
||||
);
|
||||
@@ -833,6 +935,7 @@ mod tests {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 1_500_000,
|
||||
chunk_sizes: vec![40_000, 50_000, 60_000],
|
||||
chunk_decompressed_sizes: vec![],
|
||||
chunk_output_files: vec![vec![], vec![], vec![]],
|
||||
}),
|
||||
);
|
||||
@@ -883,6 +986,7 @@ mod tests {
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "rocksdb_indices.tar.zst".to_string(),
|
||||
size: 777,
|
||||
decompressed_size: 0,
|
||||
blake3: None,
|
||||
output_files: vec![],
|
||||
}),
|
||||
@@ -955,6 +1059,7 @@ mod tests {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 24_396_822,
|
||||
chunk_sizes: vec![100; 49], // 49 chunks
|
||||
chunk_decompressed_sizes: vec![],
|
||||
chunk_output_files: vec![vec![]; 49],
|
||||
}),
|
||||
);
|
||||
@@ -997,6 +1102,68 @@ mod tests {
|
||||
assert_eq!(m.size_for_distance(SnapshotComponentType::Receipts, None), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_size_for_distance_uses_manifest_or_output_files() {
|
||||
let m = test_manifest();
|
||||
assert_eq!(m.output_size_for_distance(SnapshotComponentType::Transactions, None), 0);
|
||||
|
||||
let mut components = BTreeMap::new();
|
||||
components.insert(
|
||||
"state".to_string(),
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "state.tar.zst".to_string(),
|
||||
size: 100,
|
||||
decompressed_size: 1_000,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "db/mdbx.dat".to_string(),
|
||||
size: 1_000,
|
||||
blake3: "h0".to_string(),
|
||||
}],
|
||||
}),
|
||||
);
|
||||
components.insert(
|
||||
"transactions".to_string(),
|
||||
ComponentManifest::Chunked(ChunkedArchive {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 1_000_000,
|
||||
chunk_sizes: vec![80_000, 120_000],
|
||||
chunk_decompressed_sizes: vec![111, 222],
|
||||
chunk_output_files: vec![
|
||||
vec![OutputFileChecksum {
|
||||
path: "static_files/static_file_transactions_0_499999.bin".to_string(),
|
||||
size: 111,
|
||||
blake3: "h0".to_string(),
|
||||
}],
|
||||
vec![OutputFileChecksum {
|
||||
path: "static_files/static_file_transactions_500000_999999.bin".to_string(),
|
||||
size: 222,
|
||||
blake3: "h1".to_string(),
|
||||
}],
|
||||
],
|
||||
}),
|
||||
);
|
||||
let manifest = SnapshotManifest {
|
||||
block: 1_000_000,
|
||||
chain_id: 1,
|
||||
storage_version: 2,
|
||||
timestamp: 0,
|
||||
base_url: Some("https://example.com".to_string()),
|
||||
reth_version: None,
|
||||
components,
|
||||
};
|
||||
|
||||
assert_eq!(manifest.output_size_for_distance(SnapshotComponentType::State, None), 1_000);
|
||||
assert_eq!(
|
||||
manifest.output_size_for_distance(SnapshotComponentType::Transactions, None),
|
||||
333
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.output_size_for_distance(SnapshotComponentType::Transactions, Some(500_000)),
|
||||
222
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_descriptors_include_checksum_metadata() {
|
||||
let mut components = BTreeMap::new();
|
||||
@@ -1005,6 +1172,7 @@ mod tests {
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "state.tar.zst".to_string(),
|
||||
size: 100,
|
||||
decompressed_size: 1_000,
|
||||
blake3: Some("abc123".to_string()),
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "db/mdbx.dat".to_string(),
|
||||
@@ -1019,6 +1187,7 @@ mod tests {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 1_000_000,
|
||||
chunk_sizes: vec![80_000, 120_000],
|
||||
chunk_decompressed_sizes: vec![111, 222],
|
||||
chunk_output_files: vec![
|
||||
vec![OutputFileChecksum {
|
||||
path: "static_files/static_file_transactions_0_499999.bin".to_string(),
|
||||
@@ -1044,13 +1213,13 @@ mod tests {
|
||||
components,
|
||||
};
|
||||
|
||||
let state = m.archive_descriptors_for_distance(SnapshotComponentType::State, None);
|
||||
let state = m.snapshot_archives_for_distance(SnapshotComponentType::State, None);
|
||||
assert_eq!(state.len(), 1);
|
||||
assert_eq!(state[0].file_name, "state.tar.zst");
|
||||
assert_eq!(state[0].blake3.as_deref(), Some("abc123"));
|
||||
assert_eq!(state[0].output_files.len(), 1);
|
||||
|
||||
let tx = m.archive_descriptors_for_distance(SnapshotComponentType::Transactions, None);
|
||||
let tx = m.snapshot_archives_for_distance(SnapshotComponentType::Transactions, None);
|
||||
assert_eq!(tx.len(), 2);
|
||||
assert_eq!(tx[0].blake3, None);
|
||||
assert_eq!(tx[1].blake3, None);
|
||||
@@ -1073,6 +1242,7 @@ mod tests {
|
||||
panic!("state should be a single archive")
|
||||
};
|
||||
assert_eq!(state.file, "state.tar.zst");
|
||||
assert!(state.decompressed_size > 0);
|
||||
assert!(!state.output_files.is_empty());
|
||||
assert_eq!(state.output_files[0].path, "db/mdbx.dat");
|
||||
assert!(output.path().join("state.tar.zst").exists());
|
||||
@@ -1097,6 +1267,7 @@ mod tests {
|
||||
panic!("rocksdb indices should be a single archive")
|
||||
};
|
||||
assert_eq!(rocksdb.file, "rocksdb_indices.tar.zst");
|
||||
assert!(rocksdb.decompressed_size > 0);
|
||||
assert!(!rocksdb.output_files.is_empty());
|
||||
assert_eq!(rocksdb.output_files[0].path, "rocksdb/CURRENT");
|
||||
assert!(output.path().join("rocksdb_indices.tar.zst").exists());
|
||||
|
||||
@@ -45,6 +45,7 @@ pub struct SnapshotManifestCommand {
|
||||
}
|
||||
|
||||
impl SnapshotManifestCommand {
|
||||
/// Packages snapshot archives and writes the manifest file.
|
||||
pub fn execute(self) -> Result<()> {
|
||||
let block = match self.block {
|
||||
Some(block) => block,
|
||||
@@ -88,6 +89,7 @@ impl SnapshotManifestCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// Infers the snapshot block from the source datadir.
|
||||
fn infer_snapshot_block(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
if let Ok(block) = infer_snapshot_block_from_db(source_datadir) {
|
||||
return Ok(block);
|
||||
@@ -102,6 +104,7 @@ fn infer_snapshot_block(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Reads the snapshot block from the source database Finish stage checkpoint.
|
||||
fn infer_snapshot_block_from_db(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let candidates = [source_datadir.join("db"), source_datadir.to_path_buf()];
|
||||
|
||||
@@ -126,6 +129,7 @@ fn infer_snapshot_block_from_db(source_datadir: &std::path::Path) -> Result<u64>
|
||||
)
|
||||
}
|
||||
|
||||
/// Infers the snapshot block from the highest header static-file range.
|
||||
fn infer_snapshot_block_from_headers(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let max_end = header_ranges(source_datadir)?
|
||||
.into_iter()
|
||||
@@ -135,6 +139,7 @@ fn infer_snapshot_block_from_headers(source_datadir: &std::path::Path) -> Result
|
||||
Ok(max_end)
|
||||
}
|
||||
|
||||
/// Infers the static-file block span from header file ranges.
|
||||
fn infer_blocks_per_file(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let mut inferred = None;
|
||||
for (start, end) in header_ranges(source_datadir)? {
|
||||
@@ -161,6 +166,7 @@ fn infer_blocks_per_file(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Collects header static-file ranges from the source datadir.
|
||||
fn header_ranges(source_datadir: &std::path::Path) -> Result<Vec<(u64, u64)>> {
|
||||
let static_files_dir = source_datadir.join("static_files");
|
||||
let static_files_dir =
|
||||
@@ -183,6 +189,7 @@ fn header_ranges(source_datadir: &std::path::Path) -> Result<Vec<(u64, u64)>> {
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
/// Parses the block range from a header static-file name.
|
||||
fn parse_headers_range(file_name: &str) -> Option<(u64, u64)> {
|
||||
let remainder = file_name.strip_prefix("static_file_headers_")?;
|
||||
let (start, end_with_suffix) = remainder.split_once('_')?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
322
crates/cli/commands/src/download/planning.rs
Normal file
322
crates/cli/commands/src/download/planning.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
use super::{manifest::*, verify::OutputVerifier};
|
||||
use eyre::Result;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
use tracing::info;
|
||||
|
||||
/// One archive selected from the manifest, along with its component name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PlannedArchive {
|
||||
/// Snapshot component type this archive belongs to.
|
||||
pub(crate) ty: SnapshotComponentType,
|
||||
/// User-facing component name used in logs.
|
||||
pub(crate) component: String,
|
||||
/// Concrete snapshot archive metadata resolved from the manifest.
|
||||
pub(crate) archive: SnapshotArchive,
|
||||
}
|
||||
|
||||
/// The archive list for a modular snapshot download.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PlannedDownloads {
|
||||
/// Concrete archives that still need reuse checks or processing.
|
||||
pub(crate) archives: Vec<PlannedArchive>,
|
||||
/// Total compressed download size of all planned archives.
|
||||
pub(crate) total_download_size: u64,
|
||||
/// Total extracted plain-output size of all planned archives.
|
||||
pub(crate) total_output_size: u64,
|
||||
}
|
||||
|
||||
impl PlannedDownloads {
|
||||
/// Returns the number of concrete archives queued for this snapshot selection.
|
||||
pub(crate) const fn total_archives(&self) -> usize {
|
||||
self.archives.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sort priority used to schedule archives.
|
||||
pub(crate) const fn archive_priority_rank(ty: SnapshotComponentType) -> u8 {
|
||||
match ty {
|
||||
SnapshotComponentType::State => 0,
|
||||
SnapshotComponentType::RocksdbIndices => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup summary showing how much of the selected work can be reused.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct DownloadStartupSummary {
|
||||
/// Archives whose declared outputs already verify on disk.
|
||||
pub(crate) reusable: usize,
|
||||
/// Archives that still need to be downloaded or retried.
|
||||
pub(crate) needs_download: usize,
|
||||
}
|
||||
|
||||
/// Checks selected archives against existing output files before work begins.
|
||||
pub(crate) fn summarize_download_startup(
|
||||
all_downloads: &[PlannedArchive],
|
||||
target_dir: &Path,
|
||||
) -> Result<DownloadStartupSummary> {
|
||||
let mut summary = DownloadStartupSummary::default();
|
||||
let verifier = OutputVerifier::new(target_dir);
|
||||
|
||||
for planned in all_downloads {
|
||||
if verifier.verify(&planned.archive.output_files)? {
|
||||
summary.reusable += 1;
|
||||
} else {
|
||||
summary.needs_download += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
/// Converts a selection into the manifest distance form used for archive lookup.
|
||||
fn selection_archive_distance(
|
||||
selection: &ComponentSelection,
|
||||
snapshot_block: u64,
|
||||
) -> Option<Option<u64>> {
|
||||
match selection {
|
||||
ComponentSelection::All => Some(None),
|
||||
ComponentSelection::Distance(distance) => Some(Some(*distance)),
|
||||
ComponentSelection::Since(block) => Some(Some(snapshot_block.saturating_sub(*block) + 1)),
|
||||
ComponentSelection::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts planned archives into a stable processing order.
|
||||
fn sort_planned_archives(all_downloads: &mut [PlannedArchive]) {
|
||||
all_downloads.sort_by(|a, b| {
|
||||
archive_priority_rank(a.ty)
|
||||
.cmp(&archive_priority_rank(b.ty))
|
||||
.then_with(|| a.component.cmp(&b.component))
|
||||
.then_with(|| a.archive.file_name.cmp(&b.archive.file_name))
|
||||
});
|
||||
}
|
||||
|
||||
/// Expands component selections into the archives that need to be processed.
|
||||
pub(crate) fn collect_planned_archives(
|
||||
manifest: &SnapshotManifest,
|
||||
selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
) -> Result<PlannedDownloads> {
|
||||
let mut archives = Vec::new();
|
||||
let mut total_download_size = 0;
|
||||
let mut total_output_size = 0;
|
||||
|
||||
for (ty, selection) in selections {
|
||||
let Some(distance) = selection_archive_distance(selection, manifest.block) else {
|
||||
continue;
|
||||
};
|
||||
total_download_size += manifest.size_for_distance(*ty, distance);
|
||||
total_output_size += manifest.output_size_for_distance(*ty, distance);
|
||||
|
||||
let snapshot_archives = manifest.snapshot_archives_for_distance(*ty, distance);
|
||||
let component = ty.display_name().to_string();
|
||||
if !snapshot_archives.is_empty() {
|
||||
info!(target: "reth::cli",
|
||||
component = %component,
|
||||
archives = snapshot_archives.len(),
|
||||
selection = %selection,
|
||||
"Queued component for download"
|
||||
);
|
||||
}
|
||||
|
||||
for archive in snapshot_archives {
|
||||
if archive.output_files.is_empty() {
|
||||
eyre::bail!(
|
||||
"Invalid modular manifest: {} is missing plain output checksum metadata",
|
||||
archive.file_name
|
||||
);
|
||||
}
|
||||
|
||||
archives.push(PlannedArchive { ty: *ty, component: component.clone(), archive });
|
||||
}
|
||||
}
|
||||
|
||||
sort_planned_archives(&mut archives);
|
||||
Ok(PlannedDownloads { archives, total_download_size, total_output_size })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn summarize_download_startup_counts_reusable_and_needs_download() {
|
||||
let dir = tempdir().unwrap();
|
||||
let target_dir = dir.path();
|
||||
let ok_file = target_dir.join("ok.bin");
|
||||
std::fs::write(&ok_file, vec![1_u8; 4]).unwrap();
|
||||
let ok_hash = blake3::hash(&[1_u8; 4]).to_hex().to_string();
|
||||
|
||||
let planned = vec![
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::State,
|
||||
component: "State".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "https://example.com/ok.tar.zst".to_string(),
|
||||
file_name: "ok.tar.zst".to_string(),
|
||||
size: 10,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "ok.bin".to_string(),
|
||||
size: 4,
|
||||
blake3: ok_hash,
|
||||
}],
|
||||
},
|
||||
},
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::Headers,
|
||||
component: "Headers".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "https://example.com/missing.tar.zst".to_string(),
|
||||
file_name: "missing.tar.zst".to_string(),
|
||||
size: 10,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "missing.bin".to_string(),
|
||||
size: 1,
|
||||
blake3: "deadbeef".to_string(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::Transactions,
|
||||
component: "Transactions".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "https://example.com/bad-size.tar.zst".to_string(),
|
||||
file_name: "bad-size.tar.zst".to_string(),
|
||||
size: 10,
|
||||
blake3: None,
|
||||
output_files: vec![],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let summary = summarize_download_startup(&planned, target_dir).unwrap();
|
||||
assert_eq!(summary.reusable, 1);
|
||||
assert_eq!(summary.needs_download, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_priority_prefers_state_then_rocksdb() {
|
||||
let mut planned = [
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::Transactions,
|
||||
component: "Transactions".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "u3".to_string(),
|
||||
file_name: "t.tar.zst".to_string(),
|
||||
size: 1,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "a".to_string(),
|
||||
size: 1,
|
||||
blake3: "x".to_string(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::RocksdbIndices,
|
||||
component: "RocksDB Indices".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "u2".to_string(),
|
||||
file_name: "rocksdb_indices.tar.zst".to_string(),
|
||||
size: 1,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "b".to_string(),
|
||||
size: 1,
|
||||
blake3: "y".to_string(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
PlannedArchive {
|
||||
ty: SnapshotComponentType::State,
|
||||
component: "State (mdbx)".to_string(),
|
||||
archive: SnapshotArchive {
|
||||
url: "u1".to_string(),
|
||||
file_name: "state.tar.zst".to_string(),
|
||||
size: 1,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "c".to_string(),
|
||||
size: 1,
|
||||
blake3: "z".to_string(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
planned.sort_by(|a, b| {
|
||||
archive_priority_rank(a.ty)
|
||||
.cmp(&archive_priority_rank(b.ty))
|
||||
.then_with(|| a.component.cmp(&b.component))
|
||||
.then_with(|| a.archive.file_name.cmp(&b.archive.file_name))
|
||||
});
|
||||
|
||||
assert_eq!(planned[0].ty, SnapshotComponentType::State);
|
||||
assert_eq!(planned[1].ty, SnapshotComponentType::RocksdbIndices);
|
||||
assert_eq!(planned[2].ty, SnapshotComponentType::Transactions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_planned_archives_tracks_download_and_output_totals() {
|
||||
let mut components = BTreeMap::new();
|
||||
components.insert(
|
||||
"state".to_string(),
|
||||
ComponentManifest::Single(SingleArchive {
|
||||
file: "state.tar.zst".to_string(),
|
||||
size: 10,
|
||||
decompressed_size: 100,
|
||||
blake3: None,
|
||||
output_files: vec![OutputFileChecksum {
|
||||
path: "db/mdbx.dat".to_string(),
|
||||
size: 100,
|
||||
blake3: "h0".to_string(),
|
||||
}],
|
||||
}),
|
||||
);
|
||||
components.insert(
|
||||
"transactions".to_string(),
|
||||
ComponentManifest::Chunked(ChunkedArchive {
|
||||
blocks_per_file: 500_000,
|
||||
total_blocks: 1_000_000,
|
||||
chunk_sizes: vec![20, 30],
|
||||
chunk_decompressed_sizes: vec![200, 300],
|
||||
chunk_output_files: vec![
|
||||
vec![OutputFileChecksum {
|
||||
path: "static_files/tx-0".to_string(),
|
||||
size: 200,
|
||||
blake3: "h1".to_string(),
|
||||
}],
|
||||
vec![OutputFileChecksum {
|
||||
path: "static_files/tx-1".to_string(),
|
||||
size: 300,
|
||||
blake3: "h2".to_string(),
|
||||
}],
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
let manifest = SnapshotManifest {
|
||||
block: 1_000_000,
|
||||
chain_id: 1,
|
||||
storage_version: 2,
|
||||
timestamp: 0,
|
||||
base_url: Some("https://example.com".to_string()),
|
||||
reth_version: None,
|
||||
components,
|
||||
};
|
||||
|
||||
let selections = BTreeMap::from([
|
||||
(SnapshotComponentType::State, ComponentSelection::All),
|
||||
(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000)),
|
||||
]);
|
||||
|
||||
let planned = collect_planned_archives(&manifest, &selections).unwrap();
|
||||
|
||||
assert_eq!(planned.total_download_size, 40);
|
||||
assert_eq!(planned.total_output_size, 400);
|
||||
assert_eq!(planned.archives.len(), 2);
|
||||
}
|
||||
}
|
||||
844
crates/cli/commands/src/download/progress.rs
Normal file
844
crates/cli/commands/src/download/progress.rs
Normal file
@@ -0,0 +1,844 @@
|
||||
use eyre::Result;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc, Condvar, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
const BYTE_UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
|
||||
/// Tracks download progress and throttles display updates to every 100ms.
|
||||
pub(crate) struct DownloadProgress {
|
||||
/// Bytes copied so far for this single download.
|
||||
pub(crate) downloaded: u64,
|
||||
/// Total bytes expected for this single download.
|
||||
total_size: u64,
|
||||
/// Time when the progress line was last printed.
|
||||
last_displayed: Instant,
|
||||
/// Time when this progress tracker started.
|
||||
started_at: Instant,
|
||||
}
|
||||
|
||||
impl DownloadProgress {
|
||||
/// Creates new progress tracker with given total size
|
||||
pub(crate) fn new(total_size: u64) -> Self {
|
||||
let now = Instant::now();
|
||||
Self { downloaded: 0, total_size, last_displayed: now, started_at: now }
|
||||
}
|
||||
|
||||
/// Converts bytes to human readable format (B, KB, MB, GB)
|
||||
pub(crate) fn format_size(size: u64) -> String {
|
||||
let mut size = size as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1024.0 && unit_index < BYTE_UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, BYTE_UNITS[unit_index])
|
||||
}
|
||||
|
||||
/// Format duration as human readable string
|
||||
pub(crate) fn format_duration(duration: Duration) -> String {
|
||||
let secs = duration.as_secs();
|
||||
if secs < 60 {
|
||||
format!("{secs}s")
|
||||
} else if secs < 3600 {
|
||||
format!("{}m {}s", secs / 60, secs % 60)
|
||||
} else {
|
||||
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates progress bar (for single-archive legacy downloads)
|
||||
pub(crate) fn update(&mut self, chunk_size: u64) -> Result<()> {
|
||||
self.downloaded += chunk_size;
|
||||
|
||||
if self.last_displayed.elapsed() >= Duration::from_millis(100) {
|
||||
let formatted_downloaded = Self::format_size(self.downloaded);
|
||||
let formatted_total = Self::format_size(self.total_size);
|
||||
let progress = (self.downloaded as f64 / self.total_size as f64) * 100.0;
|
||||
|
||||
let elapsed = self.started_at.elapsed();
|
||||
let eta = if self.downloaded > 0 {
|
||||
let remaining = self.total_size.saturating_sub(self.downloaded);
|
||||
let speed = self.downloaded as f64 / elapsed.as_secs_f64();
|
||||
if speed > 0.0 {
|
||||
Duration::from_secs_f64(remaining as f64 / speed)
|
||||
} else {
|
||||
Duration::ZERO
|
||||
}
|
||||
} else {
|
||||
Duration::ZERO
|
||||
};
|
||||
let eta_str = Self::format_duration(eta);
|
||||
|
||||
print!(
|
||||
"\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total}) ETA: {eta_str} ",
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
self.last_displayed = Instant::now();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PhaseStart {
|
||||
started_at: Instant,
|
||||
baseline_bytes: u64,
|
||||
}
|
||||
|
||||
/// Shared progress counters for parallel downloads.
|
||||
pub(crate) struct SharedProgress {
|
||||
/// Raw HTTP bytes fetched during this session, including retries.
|
||||
pub(crate) session_fetched_bytes: AtomicU64,
|
||||
/// Compressed bytes from archives that have fully downloaded.
|
||||
pub(crate) completed_download_bytes: AtomicU64,
|
||||
/// Compressed bytes written for currently active archive download attempts.
|
||||
pub(crate) active_download_bytes: AtomicU64,
|
||||
/// Total compressed bytes expected across all planned archives.
|
||||
pub(crate) total_download_bytes: u64,
|
||||
/// Plain-output bytes from archives that have fully verified.
|
||||
pub(crate) completed_output_bytes: AtomicU64,
|
||||
/// Plain-output bytes unpacked by currently active extractions.
|
||||
pub(crate) active_extracted_output_bytes: AtomicU64,
|
||||
/// Plain-output bytes hashed by currently active verifications.
|
||||
pub(crate) active_verified_output_bytes: AtomicU64,
|
||||
/// Total plain-output bytes expected across all planned archives.
|
||||
pub(crate) total_output_bytes: u64,
|
||||
/// Total number of planned archives.
|
||||
pub(crate) total_archives: u64,
|
||||
/// Time when the modular download job started.
|
||||
pub(crate) started_at: Instant,
|
||||
/// Time and baseline when the current extraction phase started.
|
||||
extraction_phase: Mutex<Option<PhaseStart>>,
|
||||
/// Time and baseline when the current verification phase started.
|
||||
verification_phase: Mutex<Option<PhaseStart>>,
|
||||
/// Number of archives that have fully finished.
|
||||
pub(crate) archives_done: AtomicU64,
|
||||
/// Number of archives currently in the fetch phase.
|
||||
pub(crate) active_downloads: AtomicU64,
|
||||
/// Number of in-flight HTTP requests.
|
||||
pub(crate) active_download_requests: AtomicU64,
|
||||
/// Number of archives currently extracting.
|
||||
pub(crate) active_extractions: AtomicU64,
|
||||
/// Number of archives currently verifying extracted outputs.
|
||||
pub(crate) active_verifications: AtomicU64,
|
||||
/// Signals the background progress task to exit.
|
||||
pub(crate) done: AtomicBool,
|
||||
/// Cancellation token shared by the whole command.
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl SharedProgress {
|
||||
/// Creates the shared progress state for a modular download job.
|
||||
pub(crate) fn new(
|
||||
total_download_bytes: u64,
|
||||
total_output_bytes: u64,
|
||||
total_archives: u64,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
session_fetched_bytes: AtomicU64::new(0),
|
||||
completed_download_bytes: AtomicU64::new(0),
|
||||
active_download_bytes: AtomicU64::new(0),
|
||||
total_download_bytes,
|
||||
completed_output_bytes: AtomicU64::new(0),
|
||||
active_extracted_output_bytes: AtomicU64::new(0),
|
||||
active_verified_output_bytes: AtomicU64::new(0),
|
||||
total_output_bytes,
|
||||
total_archives,
|
||||
started_at: Instant::now(),
|
||||
extraction_phase: Mutex::new(None),
|
||||
verification_phase: Mutex::new(None),
|
||||
archives_done: AtomicU64::new(0),
|
||||
active_downloads: AtomicU64::new(0),
|
||||
active_download_requests: AtomicU64::new(0),
|
||||
active_extractions: AtomicU64::new(0),
|
||||
active_verifications: AtomicU64::new(0),
|
||||
done: AtomicBool::new(false),
|
||||
cancel_token,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the whole command has been cancelled.
|
||||
pub(crate) fn is_cancelled(&self) -> bool {
|
||||
self.cancel_token.is_cancelled()
|
||||
}
|
||||
|
||||
/// Adds raw session traffic bytes without affecting logical progress.
|
||||
pub(crate) fn record_session_fetched_bytes(&self, bytes: u64) {
|
||||
self.session_fetched_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn add_active_download_bytes(&self, bytes: u64) {
|
||||
self.active_download_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn sub_active_download_bytes(&self, bytes: u64) {
|
||||
sub_bytes(&self.active_download_bytes, bytes);
|
||||
}
|
||||
|
||||
fn add_active_extracted_output_bytes(&self, bytes: u64) {
|
||||
self.active_extracted_output_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn sub_active_extracted_output_bytes(&self, bytes: u64) {
|
||||
sub_bytes(&self.active_extracted_output_bytes, bytes);
|
||||
}
|
||||
|
||||
fn add_active_verified_output_bytes(&self, bytes: u64) {
|
||||
self.active_verified_output_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn sub_active_verified_output_bytes(&self, bytes: u64) {
|
||||
sub_bytes(&self.active_verified_output_bytes, bytes);
|
||||
}
|
||||
|
||||
/// Records an archive whose outputs were already present locally.
|
||||
pub(crate) fn record_reused_archive(&self, download_bytes: u64, output_bytes: u64) {
|
||||
self.completed_download_bytes.fetch_add(download_bytes, Ordering::Relaxed);
|
||||
self.completed_output_bytes.fetch_add(output_bytes, Ordering::Relaxed);
|
||||
self.archives_done.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Records an archive whose compressed download completed successfully.
|
||||
pub(crate) fn record_archive_download_complete(&self, bytes: u64) {
|
||||
self.completed_download_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Records an archive whose extracted outputs have fully verified.
|
||||
pub(crate) fn record_archive_output_complete(&self, bytes: u64) {
|
||||
self.completed_output_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
self.archives_done.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns logical compressed download progress.
|
||||
pub(crate) fn logical_downloaded_bytes(&self) -> u64 {
|
||||
(self.completed_download_bytes.load(Ordering::Relaxed) +
|
||||
self.active_download_bytes.load(Ordering::Relaxed))
|
||||
.min(self.total_download_bytes)
|
||||
}
|
||||
|
||||
/// Returns verified plain-output bytes.
|
||||
pub(crate) fn verified_output_bytes(&self) -> u64 {
|
||||
self.completed_output_bytes.load(Ordering::Relaxed).min(self.total_output_bytes)
|
||||
}
|
||||
|
||||
/// Returns plain-output bytes currently represented by extraction progress.
|
||||
pub(crate) fn extracting_output_bytes(&self) -> u64 {
|
||||
(self.completed_output_bytes.load(Ordering::Relaxed) +
|
||||
self.active_extracted_output_bytes.load(Ordering::Relaxed))
|
||||
.min(self.total_output_bytes)
|
||||
}
|
||||
|
||||
/// Returns plain-output bytes currently represented by verification progress.
|
||||
pub(crate) fn verifying_output_bytes(&self) -> u64 {
|
||||
(self.completed_output_bytes.load(Ordering::Relaxed) +
|
||||
self.active_verified_output_bytes.load(Ordering::Relaxed))
|
||||
.min(self.total_output_bytes)
|
||||
}
|
||||
|
||||
fn restart_phase(slot: &Mutex<Option<PhaseStart>>, baseline_bytes: u64) {
|
||||
*slot.lock().unwrap() = Some(PhaseStart { started_at: Instant::now(), baseline_bytes });
|
||||
}
|
||||
|
||||
fn phase_eta(
|
||||
slot: &Mutex<Option<PhaseStart>>,
|
||||
current_bytes: u64,
|
||||
total_bytes: u64,
|
||||
) -> Option<Duration> {
|
||||
let phase = *slot.lock().unwrap();
|
||||
let phase = phase?;
|
||||
let done = current_bytes.saturating_sub(phase.baseline_bytes);
|
||||
let total = total_bytes.saturating_sub(phase.baseline_bytes);
|
||||
eta_from_progress(phase.started_at.elapsed(), done, total)
|
||||
}
|
||||
|
||||
fn extraction_eta(&self, current_bytes: u64) -> Option<Duration> {
|
||||
Self::phase_eta(&self.extraction_phase, current_bytes, self.total_output_bytes)
|
||||
}
|
||||
|
||||
fn verification_eta(&self, current_bytes: u64) -> Option<Duration> {
|
||||
Self::phase_eta(&self.verification_phase, current_bytes, self.total_output_bytes)
|
||||
}
|
||||
|
||||
/// Marks one archive as actively downloading.
|
||||
pub(crate) fn download_started(&self) {
|
||||
self.active_downloads.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Marks one archive download as finished.
|
||||
pub(crate) fn download_finished(&self) {
|
||||
sub_bytes(&self.active_downloads, 1);
|
||||
}
|
||||
|
||||
/// Marks one HTTP request as in flight.
|
||||
pub(crate) fn request_started(&self) {
|
||||
self.active_download_requests.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Marks one HTTP request as finished.
|
||||
pub(crate) fn request_finished(&self) {
|
||||
sub_bytes(&self.active_download_requests, 1);
|
||||
}
|
||||
|
||||
/// Marks one archive as actively extracting.
|
||||
pub(crate) fn extraction_started(&self) {
|
||||
if self.active_extractions.fetch_add(1, Ordering::Relaxed) == 0 {
|
||||
Self::restart_phase(
|
||||
&self.extraction_phase,
|
||||
self.completed_output_bytes.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks one archive extraction as finished.
|
||||
pub(crate) fn extraction_finished(&self) {
|
||||
sub_bytes(&self.active_extractions, 1);
|
||||
}
|
||||
|
||||
/// Marks one archive as actively verifying outputs.
|
||||
pub(crate) fn verification_started(&self) {
|
||||
if self.active_verifications.fetch_add(1, Ordering::Relaxed) == 0 {
|
||||
Self::restart_phase(
|
||||
&self.verification_phase,
|
||||
self.completed_output_bytes.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks one archive verification as finished.
|
||||
pub(crate) fn verification_finished(&self) {
|
||||
sub_bytes(&self.active_verifications, 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn sub_bytes(counter: &AtomicU64, bytes: u64) {
|
||||
let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
|
||||
Some(current.saturating_sub(bytes))
|
||||
});
|
||||
}
|
||||
|
||||
fn eta_from_progress(elapsed: Duration, done: u64, total: u64) -> Option<Duration> {
|
||||
if done == 0 || done >= total {
|
||||
return None;
|
||||
}
|
||||
|
||||
let secs = elapsed.as_secs_f64();
|
||||
if secs <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let speed = done as f64 / secs;
|
||||
if speed <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Duration::from_secs_f64((total - done) as f64 / speed))
|
||||
}
|
||||
|
||||
fn format_percent(done: u64, total: u64) -> String {
|
||||
if total == 0 {
|
||||
return "100.0%".to_string();
|
||||
}
|
||||
|
||||
format!("{:.1}%", (done as f64 / total as f64) * 100.0)
|
||||
}
|
||||
|
||||
fn format_eta(eta: Option<Duration>) -> String {
|
||||
eta.map(DownloadProgress::format_duration).unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
/// Global request limit for the blocking downloader.
|
||||
///
|
||||
/// This uses `Mutex + Condvar` because the segmented path runs blocking reqwest
|
||||
/// clients on OS threads.
|
||||
pub(crate) struct DownloadRequestLimiter {
|
||||
/// Maximum number of in-flight HTTP requests.
|
||||
limit: usize,
|
||||
/// Current number of acquired request slots.
|
||||
active: Mutex<usize>,
|
||||
/// Wakes blocked threads when a slot is released.
|
||||
notify: Condvar,
|
||||
}
|
||||
|
||||
impl DownloadRequestLimiter {
|
||||
/// Creates the shared request limiter.
|
||||
pub(crate) fn new(limit: usize) -> Arc<Self> {
|
||||
Arc::new(Self { limit: limit.max(1), active: Mutex::new(0), notify: Condvar::new() })
|
||||
}
|
||||
|
||||
/// Returns the configured request limit.
|
||||
pub(crate) fn max_concurrency(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub(crate) fn acquire<'a>(
|
||||
&'a self,
|
||||
progress: Option<&'a Arc<SharedProgress>>,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<DownloadRequestPermit<'a>> {
|
||||
let mut active = self.active.lock().unwrap();
|
||||
loop {
|
||||
if cancel_token.is_cancelled() {
|
||||
return Err(eyre::eyre!("Download cancelled"));
|
||||
}
|
||||
|
||||
if *active < self.limit {
|
||||
*active += 1;
|
||||
if let Some(progress) = progress {
|
||||
progress.request_started();
|
||||
}
|
||||
return Ok(DownloadRequestPermit { limiter: self, progress });
|
||||
}
|
||||
|
||||
// Wake periodically so cancellation can interrupt waiters even if
|
||||
// no request finishes.
|
||||
let (next_active, _) =
|
||||
self.notify.wait_timeout(active, Duration::from_millis(100)).unwrap();
|
||||
active = next_active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII permit for one in-flight HTTP request.
|
||||
///
|
||||
/// Dropping the permit releases a slot in the shared request limit and updates
|
||||
/// the live progress counters.
|
||||
pub(crate) struct DownloadRequestPermit<'a> {
|
||||
/// Limiter that owns the request slot.
|
||||
limiter: &'a DownloadRequestLimiter,
|
||||
/// Shared progress counters updated when the permit drops.
|
||||
progress: Option<&'a Arc<SharedProgress>>,
|
||||
}
|
||||
|
||||
impl Drop for DownloadRequestPermit<'_> {
|
||||
/// Releases the request slot and updates shared progress counters.
|
||||
fn drop(&mut self) {
|
||||
let mut active = self.limiter.active.lock().unwrap();
|
||||
*active = active.saturating_sub(1);
|
||||
drop(active);
|
||||
self.limiter.notify.notify_one();
|
||||
|
||||
if let Some(progress) = self.progress {
|
||||
progress.request_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks one active archive download attempt.
|
||||
pub(crate) struct ArchiveDownloadProgress<'a> {
|
||||
progress: Option<&'a Arc<SharedProgress>>,
|
||||
downloaded: u64,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl<'a> ArchiveDownloadProgress<'a> {
|
||||
/// Starts tracking one archive download attempt.
|
||||
pub(crate) fn new(progress: Option<&'a Arc<SharedProgress>>) -> Self {
|
||||
if let Some(progress) = progress {
|
||||
progress.download_started();
|
||||
}
|
||||
Self { progress, downloaded: 0, completed: false }
|
||||
}
|
||||
|
||||
/// Adds logical compressed bytes written by this attempt.
|
||||
pub(crate) fn record_downloaded(&mut self, bytes: u64) {
|
||||
self.downloaded += bytes;
|
||||
if let Some(progress) = self.progress {
|
||||
progress.add_active_download_bytes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this tracker has recorded any logical bytes itself.
|
||||
pub(crate) fn has_tracked_bytes(&self) -> bool {
|
||||
self.downloaded > 0
|
||||
}
|
||||
|
||||
/// Moves this archive from active download bytes into completed download bytes.
|
||||
pub(crate) fn complete(&mut self, total_bytes: u64) {
|
||||
if self.completed {
|
||||
return;
|
||||
}
|
||||
if let Some(progress) = self.progress {
|
||||
progress.sub_active_download_bytes(self.downloaded);
|
||||
progress.record_archive_download_complete(total_bytes);
|
||||
}
|
||||
self.downloaded = 0;
|
||||
self.completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ArchiveDownloadProgress<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(progress) = self.progress {
|
||||
progress.sub_active_download_bytes(self.downloaded);
|
||||
progress.download_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks one active archive extraction attempt.
|
||||
pub(crate) struct ArchiveExtractionProgress {
|
||||
progress: Option<Arc<SharedProgress>>,
|
||||
extracted: Arc<AtomicU64>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
/// Cloneable handle for reporting extracted bytes from background monitoring.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ArchiveExtractionProgressHandle {
|
||||
progress: Arc<SharedProgress>,
|
||||
extracted: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ArchiveExtractionProgress {
|
||||
/// Starts tracking one archive extraction attempt.
|
||||
pub(crate) fn new(progress: Option<&Arc<SharedProgress>>) -> Self {
|
||||
if let Some(progress) = progress {
|
||||
progress.extraction_started();
|
||||
}
|
||||
Self {
|
||||
progress: progress.cloned(),
|
||||
extracted: Arc::new(AtomicU64::new(0)),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a cloneable handle that can report extraction progress from another thread.
|
||||
pub(crate) fn handle(&self) -> Option<ArchiveExtractionProgressHandle> {
|
||||
Some(ArchiveExtractionProgressHandle {
|
||||
progress: Arc::clone(self.progress.as_ref()?),
|
||||
extracted: Arc::clone(&self.extracted),
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds plain-output bytes extracted by this attempt.
|
||||
pub(crate) fn record_extracted(&mut self, bytes: u64) {
|
||||
if let Some(handle) = self.handle() {
|
||||
handle.record_extracted(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends extraction tracking before verification begins.
|
||||
pub(crate) fn finish(&mut self) {
|
||||
if self.finished {
|
||||
return;
|
||||
}
|
||||
if let Some(progress) = &self.progress {
|
||||
progress.sub_active_extracted_output_bytes(self.extracted.swap(0, Ordering::Relaxed));
|
||||
}
|
||||
self.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ArchiveExtractionProgress {
|
||||
fn drop(&mut self) {
|
||||
if let Some(progress) = &self.progress {
|
||||
progress.sub_active_extracted_output_bytes(self.extracted.swap(0, Ordering::Relaxed));
|
||||
progress.extraction_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchiveExtractionProgressHandle {
|
||||
/// Adds plain-output bytes extracted by this attempt.
|
||||
pub(crate) fn record_extracted(&self, bytes: u64) {
|
||||
self.extracted.fetch_add(bytes, Ordering::Relaxed);
|
||||
self.progress.add_active_extracted_output_bytes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks one active archive verification attempt.
|
||||
pub(crate) struct ArchiveVerificationProgress<'a> {
|
||||
progress: Option<&'a Arc<SharedProgress>>,
|
||||
verified: u64,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl<'a> ArchiveVerificationProgress<'a> {
|
||||
/// Starts tracking one archive verification attempt.
|
||||
pub(crate) fn new(progress: Option<&'a Arc<SharedProgress>>) -> Self {
|
||||
if let Some(progress) = progress {
|
||||
progress.verification_started();
|
||||
}
|
||||
Self { progress, verified: 0, completed: false }
|
||||
}
|
||||
|
||||
/// Adds plain-output bytes hashed by this verification attempt.
|
||||
pub(crate) fn record_verified(&mut self, bytes: u64) {
|
||||
self.verified += bytes;
|
||||
if let Some(progress) = self.progress {
|
||||
progress.add_active_verified_output_bytes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves this archive from active verification bytes into completed output bytes.
|
||||
pub(crate) fn complete(&mut self, total_bytes: u64) {
|
||||
if self.completed {
|
||||
return;
|
||||
}
|
||||
if let Some(progress) = self.progress {
|
||||
progress.sub_active_verified_output_bytes(self.verified);
|
||||
progress.record_archive_output_complete(total_bytes);
|
||||
}
|
||||
self.verified = 0;
|
||||
self.completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ArchiveVerificationProgress<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(progress) = self.progress {
|
||||
progress.sub_active_verified_output_bytes(self.verified);
|
||||
progress.verification_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to track progress while reading (used for extraction in legacy path)
|
||||
pub(crate) struct ProgressReader<R> {
|
||||
/// Wrapped reader that provides archive bytes.
|
||||
reader: R,
|
||||
/// Per-download progress tracker for legacy paths.
|
||||
progress: DownloadProgress,
|
||||
/// Cancellation token checked between reads.
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl<R: Read> ProgressReader<R> {
|
||||
/// Wraps a reader with per-download progress tracking.
|
||||
pub(crate) fn new(reader: R, total_size: u64, cancel_token: CancellationToken) -> Self {
|
||||
Self { reader, progress: DownloadProgress::new(total_size), cancel_token }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for ProgressReader<R> {
|
||||
/// Reads bytes, checks cancellation, and updates the local progress bar.
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.cancel_token.is_cancelled() {
|
||||
return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
|
||||
}
|
||||
let bytes = self.reader.read(buf)?;
|
||||
if bytes > 0 &&
|
||||
let Err(error) = self.progress.update(bytes as u64)
|
||||
{
|
||||
return Err(io::Error::other(error));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper that bumps a shared atomic counter while writing data.
|
||||
/// Used for parallel downloads where a single display task shows aggregated progress.
|
||||
pub(crate) struct SharedProgressWriter<'a, W> {
|
||||
/// Wrapped writer receiving downloaded bytes.
|
||||
pub(crate) inner: W,
|
||||
/// Shared counters updated as bytes are written.
|
||||
pub(crate) progress: Arc<SharedProgress>,
|
||||
/// Optional callback for logical bytes written by the current archive attempt.
|
||||
pub(crate) on_written: Option<&'a mut dyn FnMut(u64)>,
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SharedProgressWriter<'_, W> {
|
||||
/// Writes bytes and records them in shared progress.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
if self.progress.is_cancelled() {
|
||||
return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
|
||||
}
|
||||
let n = self.inner.write(buf)?;
|
||||
self.progress.record_session_fetched_bytes(n as u64);
|
||||
if let Some(on_written) = self.on_written.as_deref_mut() {
|
||||
on_written(n as u64);
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Flushes the wrapped writer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper that bumps a shared atomic counter while reading data.
|
||||
/// Used for streaming downloads where a single display task shows aggregated progress.
|
||||
pub(crate) struct SharedProgressReader<R> {
|
||||
/// Wrapped reader producing streamed bytes.
|
||||
pub(crate) inner: R,
|
||||
/// Shared counters updated as bytes are read.
|
||||
pub(crate) progress: Arc<SharedProgress>,
|
||||
}
|
||||
|
||||
impl<R: Read> Read for SharedProgressReader<R> {
|
||||
/// Reads bytes and records them in shared progress.
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.progress.is_cancelled() {
|
||||
return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
|
||||
}
|
||||
let n = self.inner.read(buf)?;
|
||||
self.progress.record_session_fetched_bytes(n as u64);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a background task that prints aggregated download progress.
|
||||
/// Returns a handle; drop it (or call `.abort()`) to stop.
|
||||
pub(crate) fn spawn_progress_display(progress: Arc<SharedProgress>) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(3));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if progress.done.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let download_total = progress.total_download_bytes;
|
||||
let output_total = progress.total_output_bytes;
|
||||
if download_total == 0 && output_total == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let done = progress.archives_done.load(Ordering::Relaxed);
|
||||
let all = progress.total_archives;
|
||||
let active_downloads = progress.active_downloads.load(Ordering::Relaxed);
|
||||
let active_requests = progress.active_download_requests.load(Ordering::Relaxed);
|
||||
let active_extractions = progress.active_extractions.load(Ordering::Relaxed);
|
||||
let active_verifications = progress.active_verifications.load(Ordering::Relaxed);
|
||||
let downloaded = progress.logical_downloaded_bytes();
|
||||
let extracted = progress.extracting_output_bytes();
|
||||
let verified = progress.verifying_output_bytes();
|
||||
let elapsed = DownloadProgress::format_duration(progress.started_at.elapsed());
|
||||
let download_total_display = DownloadProgress::format_size(download_total);
|
||||
let output_total_display = DownloadProgress::format_size(output_total);
|
||||
let downloaded_display = DownloadProgress::format_size(downloaded);
|
||||
let extracted_display = DownloadProgress::format_size(extracted);
|
||||
let active_download_phase = active_downloads > 0 || active_requests > 0;
|
||||
|
||||
if active_download_phase {
|
||||
info!(target: "reth::cli",
|
||||
archives = format_args!("{done}/{all}"),
|
||||
progress = %format_percent(downloaded, download_total),
|
||||
elapsed = %elapsed,
|
||||
eta = %format_eta(eta_from_progress(progress.started_at.elapsed(), downloaded, download_total)),
|
||||
bytes = format_args!("{downloaded_display}/{download_total_display}"),
|
||||
"Downloading snapshot archives"
|
||||
);
|
||||
} else if active_extractions > 0 {
|
||||
info!(target: "reth::cli",
|
||||
archives = format_args!("{done}/{all}"),
|
||||
progress = %format_percent(extracted, output_total),
|
||||
elapsed = %elapsed,
|
||||
eta = %format_eta(progress.extraction_eta(extracted)),
|
||||
bytes = format_args!("{extracted_display}/{output_total_display}"),
|
||||
"Extracting snapshot archives"
|
||||
);
|
||||
} else if active_verifications > 0 {
|
||||
info!(target: "reth::cli",
|
||||
archives = format_args!("{done}/{all}"),
|
||||
progress = %format_percent(verified, output_total),
|
||||
elapsed = %elapsed,
|
||||
eta = %format_eta(progress.verification_eta(verified)),
|
||||
bytes = format_args!("{}/{output_total_display}", DownloadProgress::format_size(verified)),
|
||||
"Verifying snapshot archives"
|
||||
);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let completed = progress.verified_output_bytes();
|
||||
let completed_display = DownloadProgress::format_size(completed);
|
||||
let output_total = DownloadProgress::format_size(progress.total_output_bytes);
|
||||
info!(target: "reth::cli",
|
||||
archives = format_args!("{}/{}", progress.total_archives, progress.total_archives),
|
||||
progress = "100.0%",
|
||||
elapsed = %DownloadProgress::format_duration(progress.started_at.elapsed()),
|
||||
eta = "0s",
|
||||
bytes = format_args!("{completed_display}/{output_total}"),
|
||||
"Snapshot archive processing complete"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[test]
|
||||
fn shared_progress_separates_session_fetch_from_logical_progress() {
|
||||
let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
|
||||
|
||||
progress.record_session_fetched_bytes(10);
|
||||
progress.record_session_fetched_bytes(10);
|
||||
progress.record_archive_download_complete(10);
|
||||
progress.record_archive_output_complete(20);
|
||||
|
||||
assert_eq!(progress.session_fetched_bytes.load(Ordering::Relaxed), 20);
|
||||
assert_eq!(progress.logical_downloaded_bytes(), 10);
|
||||
assert_eq!(progress.verified_output_bytes(), 20);
|
||||
assert_eq!(progress.archives_done.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_download_progress_rolls_back_unfinished_attempts() {
|
||||
let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
|
||||
|
||||
{
|
||||
let mut download = ArchiveDownloadProgress::new(Some(&progress));
|
||||
download.record_downloaded(4);
|
||||
assert_eq!(progress.logical_downloaded_bytes(), 4);
|
||||
}
|
||||
|
||||
assert_eq!(progress.logical_downloaded_bytes(), 0);
|
||||
assert_eq!(progress.active_downloads.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extraction_phase_baseline_restarts_after_idle() {
|
||||
let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
|
||||
|
||||
progress.extraction_started();
|
||||
assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
|
||||
|
||||
progress.completed_output_bytes.store(25, Ordering::Relaxed);
|
||||
progress.extraction_started();
|
||||
assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
|
||||
|
||||
progress.extraction_finished();
|
||||
progress.extraction_finished();
|
||||
progress.extraction_started();
|
||||
assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_phase_baseline_restarts_after_idle() {
|
||||
let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
|
||||
|
||||
progress.verification_started();
|
||||
assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
|
||||
|
||||
progress.completed_output_bytes.store(40, Ordering::Relaxed);
|
||||
progress.verification_started();
|
||||
assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
|
||||
|
||||
progress.verification_finished();
|
||||
progress.verification_finished();
|
||||
progress.verification_started();
|
||||
assert_eq!(
|
||||
progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes,
|
||||
40
|
||||
);
|
||||
}
|
||||
}
|
||||
100
crates/cli/commands/src/download/session.rs
Normal file
100
crates/cli/commands/src/download/session.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use super::progress::{DownloadRequestLimiter, SharedProgress};
|
||||
use eyre::Result;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Shared state for one run of `reth download`.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DownloadSession {
|
||||
/// Shared progress counters for this command, when enabled.
|
||||
progress: Option<Arc<SharedProgress>>,
|
||||
/// Shared limit for concurrent HTTP requests, when enabled.
|
||||
request_limiter: Option<Arc<DownloadRequestLimiter>>,
|
||||
/// Cancellation token shared by the whole command.
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl DownloadSession {
|
||||
/// Stores the shared progress, request limiter, and cancellation token.
|
||||
pub(crate) fn new(
|
||||
progress: Option<Arc<SharedProgress>>,
|
||||
request_limiter: Option<Arc<DownloadRequestLimiter>>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Self {
|
||||
Self { progress, request_limiter, cancel_token }
|
||||
}
|
||||
|
||||
/// Returns the shared progress tracker, if this flow uses one.
|
||||
pub(crate) fn progress(&self) -> Option<&Arc<SharedProgress>> {
|
||||
self.progress.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the shared HTTP request limiter, if this flow uses one.
|
||||
pub(crate) fn request_limiter(&self) -> Option<&Arc<DownloadRequestLimiter>> {
|
||||
self.request_limiter.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the request limiter or errors if the caller needs one.
|
||||
pub(crate) fn require_request_limiter(&self) -> Result<&Arc<DownloadRequestLimiter>> {
|
||||
self.request_limiter().ok_or_else(|| eyre::eyre!("Missing download request limiter"))
|
||||
}
|
||||
|
||||
/// Returns the cancellation token for this command.
|
||||
pub(crate) fn cancel_token(&self) -> &CancellationToken {
|
||||
&self.cancel_token
|
||||
}
|
||||
|
||||
/// Records one archive whose outputs were already reusable on disk.
|
||||
pub(crate) fn record_reused_archive(&self, download_bytes: u64, output_bytes: u64) {
|
||||
if let Some(progress) = self.progress() {
|
||||
progress.record_reused_archive(download_bytes, output_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records one archive whose extracted outputs fully verified.
|
||||
pub(crate) fn record_archive_output_complete(&self, bytes: u64) {
|
||||
if let Some(progress) = self.progress() {
|
||||
progress.record_archive_output_complete(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paths used while processing one archive, plus the shared download session.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ArchiveProcessContext {
|
||||
/// Directory where extracted output files are written.
|
||||
target_dir: PathBuf,
|
||||
/// Directory used for cached archive downloads, when enabled.
|
||||
cache_dir: Option<PathBuf>,
|
||||
/// Shared command-scoped download state.
|
||||
session: DownloadSession,
|
||||
}
|
||||
|
||||
impl ArchiveProcessContext {
|
||||
/// Creates the context used while processing modular archives.
|
||||
pub(crate) fn new(
|
||||
target_dir: PathBuf,
|
||||
cache_dir: Option<PathBuf>,
|
||||
session: DownloadSession,
|
||||
) -> Self {
|
||||
Self { target_dir, cache_dir, session }
|
||||
}
|
||||
|
||||
/// Returns the directory where extracted outputs should be written.
|
||||
pub(crate) fn target_dir(&self) -> &Path {
|
||||
&self.target_dir
|
||||
}
|
||||
|
||||
/// Returns the cache directory for two-phase downloads, if enabled.
|
||||
pub(crate) fn cache_dir(&self) -> Option<&Path> {
|
||||
self.cache_dir.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the shared download session.
|
||||
pub(crate) fn session(&self) -> &DownloadSession {
|
||||
&self.session
|
||||
}
|
||||
}
|
||||
230
crates/cli/commands/src/download/source.rs
Normal file
230
crates/cli/commands/src/download/source.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use super::{manifest::SnapshotManifest, progress::DownloadProgress, DownloadDefaults};
|
||||
use eyre::{Result, WrapErr};
|
||||
use reqwest::Client;
|
||||
use reth_fs_util as fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
/// An entry from the snapshot discovery API listing.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SnapshotApiEntry {
|
||||
#[serde(deserialize_with = "deserialize_string_or_u64")]
|
||||
chain_id: u64,
|
||||
#[serde(deserialize_with = "deserialize_string_or_u64")]
|
||||
block: u64,
|
||||
#[serde(default)]
|
||||
date: Option<String>,
|
||||
#[serde(default)]
|
||||
profile: Option<String>,
|
||||
metadata_url: String,
|
||||
#[serde(default)]
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl SnapshotApiEntry {
|
||||
/// Returns whether this discovery entry points to a modular manifest.
|
||||
fn is_modular(&self) -> bool {
|
||||
self.metadata_url.ends_with("manifest.json")
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers the latest snapshot manifest URL for the given chain from the snapshots API.
|
||||
///
|
||||
/// Queries the configured snapshot API and returns the manifest URL for the most
|
||||
/// recent modular snapshot matching the requested chain.
|
||||
pub(crate) async fn discover_manifest_url(chain_id: u64) -> Result<String> {
|
||||
let defaults = DownloadDefaults::get_global();
|
||||
let api_url = &*defaults.snapshot_api_url;
|
||||
|
||||
info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
|
||||
|
||||
let entries = fetch_snapshot_api_entries(chain_id).await?;
|
||||
let entry =
|
||||
entries.iter().filter(|s| s.is_modular()).max_by_key(|s| s.block).ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"No modular snapshot manifest found for chain \
|
||||
{chain_id} at {api_url}\n\n\
|
||||
You can provide a manifest URL directly with --manifest-url, or\n\
|
||||
use a direct snapshot URL with -u from:\n\
|
||||
\t- {}\n\n\
|
||||
Use --list to see all available snapshots.",
|
||||
api_url.trim_end_matches("/api/snapshots"),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!(target: "reth::cli",
|
||||
block = entry.block,
|
||||
url = %entry.metadata_url,
|
||||
"Found latest snapshot manifest"
|
||||
);
|
||||
|
||||
Ok(entry.metadata_url.clone())
|
||||
}
|
||||
|
||||
/// Deserializes a JSON value that may be either a number or a string-encoded number.
|
||||
fn deserialize_string_or_u64<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
match &value {
|
||||
serde_json::Value::Number(n) => {
|
||||
n.as_u64().ok_or_else(|| serde::de::Error::custom("expected u64"))
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
s.parse::<u64>().map_err(|_| serde::de::Error::custom("expected numeric string"))
|
||||
}
|
||||
_ => Err(serde::de::Error::custom("expected number or string")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the full snapshot listing from the snapshots API, filtered by chain ID.
|
||||
pub(crate) async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntry>> {
|
||||
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
|
||||
|
||||
let entries: Vec<SnapshotApiEntry> = Client::new()
|
||||
.get(api_url)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.wrap_err_with(|| format!("Failed to fetch snapshot listing from {api_url}"))?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(entries.into_iter().filter(|entry| entry.chain_id == chain_id).collect())
|
||||
}
|
||||
|
||||
/// Prints a formatted table of available modular snapshots.
|
||||
pub(crate) fn print_snapshot_listing(entries: &[SnapshotApiEntry], chain_id: u64) {
|
||||
let modular: Vec<_> = entries.iter().filter(|entry| entry.is_modular()).collect();
|
||||
|
||||
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
|
||||
println!(
|
||||
"Available snapshots for chain {chain_id} ({}):\n",
|
||||
api_url.trim_end_matches("/api/snapshots"),
|
||||
);
|
||||
println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
|
||||
println!("{}", "-".repeat(100));
|
||||
|
||||
for entry in &modular {
|
||||
let date = entry.date.as_deref().unwrap_or("-");
|
||||
let profile = entry.profile.as_deref().unwrap_or("-");
|
||||
let size = if entry.size > 0 {
|
||||
DownloadProgress::format_size(entry.size)
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
println!(
|
||||
"{date:<12} {:>10} {profile:<10} {size:>10} {}",
|
||||
entry.block, entry.metadata_url
|
||||
);
|
||||
}
|
||||
|
||||
if modular.is_empty() {
|
||||
println!(" (no modular snapshots found)");
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nTo download a specific snapshot, copy its manifest URL and run:\n \
|
||||
reth download --manifest-url <URL>"
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads a manifest from an HTTP(S) URL, `file://` URL, or local path.
|
||||
pub(crate) async fn fetch_manifest_from_source(source: &str) -> Result<SnapshotManifest> {
|
||||
if let Ok(parsed) = Url::parse(source) {
|
||||
return match parsed.scheme() {
|
||||
"http" | "https" => {
|
||||
let response = Client::new()
|
||||
.get(source)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.wrap_err_with(|| {
|
||||
let sources = DownloadDefaults::get_global()
|
||||
.available_snapshots
|
||||
.iter()
|
||||
.map(|snapshot| format!("\t- {snapshot}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!(
|
||||
"Failed to fetch snapshot manifest from {source}\n\n\
|
||||
The manifest endpoint may not be available for this snapshot source.\n\
|
||||
You can use a direct snapshot URL instead:\n\n\
|
||||
\treth download -u <snapshot-url>\n\n\
|
||||
Available snapshot sources:\n{sources}"
|
||||
)
|
||||
})?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
"file" => {
|
||||
let path = parsed
|
||||
.to_file_path()
|
||||
.map_err(|_| eyre::eyre!("Invalid file:// manifest path: {source}"))?;
|
||||
let content = fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
_ => Err(eyre::eyre!("Unsupported manifest URL scheme: {}", parsed.scheme())),
|
||||
};
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(source)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
/// Resolves the base URL used to join relative archive paths in a manifest.
|
||||
pub(crate) fn resolve_manifest_base_url(
|
||||
manifest: &SnapshotManifest,
|
||||
source: &str,
|
||||
) -> Result<String> {
|
||||
if let Some(base_url) = manifest.base_url.as_deref() &&
|
||||
!base_url.is_empty()
|
||||
{
|
||||
return Ok(base_url.trim_end_matches('/').to_string());
|
||||
}
|
||||
|
||||
if let Ok(mut url) = Url::parse(source) {
|
||||
if url.scheme() == "file" {
|
||||
let mut path = url
|
||||
.to_file_path()
|
||||
.map_err(|_| eyre::eyre!("Invalid file:// manifest path: {source}"))?;
|
||||
path.pop();
|
||||
let mut base = Url::from_directory_path(path)
|
||||
.map_err(|_| eyre::eyre!("Invalid manifest directory for source: {source}"))?
|
||||
.to_string();
|
||||
if base.ends_with('/') {
|
||||
base.pop();
|
||||
}
|
||||
return Ok(base);
|
||||
}
|
||||
|
||||
{
|
||||
let mut segments = url
|
||||
.path_segments_mut()
|
||||
.map_err(|_| eyre::eyre!("manifest_url must have a hierarchical path"))?;
|
||||
segments.pop_if_empty();
|
||||
segments.pop();
|
||||
}
|
||||
return Ok(url.as_str().trim_end_matches('/').to_string());
|
||||
}
|
||||
|
||||
let path = Path::new(source);
|
||||
let manifest_dir = if path.is_absolute() {
|
||||
path.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."))
|
||||
} else {
|
||||
let joined = std::env::current_dir()?.join(path);
|
||||
joined.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."))
|
||||
};
|
||||
|
||||
let mut base = Url::from_directory_path(&manifest_dir)
|
||||
.map_err(|_| eyre::eyre!("Invalid manifest directory: {}", manifest_dir.display()))?
|
||||
.to_string();
|
||||
if base.ends_with('/') {
|
||||
base.pop();
|
||||
}
|
||||
Ok(base)
|
||||
}
|
||||
84
crates/cli/commands/src/download/verify.rs
Normal file
84
crates/cli/commands/src/download/verify.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use super::{manifest::OutputFileChecksum, progress::ArchiveVerificationProgress};
|
||||
use blake3::Hasher;
|
||||
use eyre::Result;
|
||||
use reth_fs_util as fs;
|
||||
use std::{io::Read, path::Path};
|
||||
|
||||
/// Verifies and cleans up extracted output files in one target directory.
|
||||
pub(crate) struct OutputVerifier<'a> {
|
||||
/// Directory containing the output files declared by the manifest.
|
||||
target_dir: &'a Path,
|
||||
}
|
||||
|
||||
impl<'a> OutputVerifier<'a> {
|
||||
/// Creates a verifier for one extraction target directory.
|
||||
pub(crate) const fn new(target_dir: &'a Path) -> Self {
|
||||
Self { target_dir }
|
||||
}
|
||||
|
||||
/// Returns `true` only when every declared output file exists and matches size and BLAKE3.
|
||||
/// Returns `false` if any file is missing, mismatched, or no outputs were declared.
|
||||
pub(crate) fn verify(&self, output_files: &[OutputFileChecksum]) -> Result<bool> {
|
||||
self.verify_with_progress(output_files, None)
|
||||
}
|
||||
|
||||
/// Returns `true` only when every declared output file exists and matches size and BLAKE3,
|
||||
/// updating the optional verification progress as file bytes are hashed.
|
||||
pub(crate) fn verify_with_progress(
|
||||
&self,
|
||||
output_files: &[OutputFileChecksum],
|
||||
mut progress: Option<&mut ArchiveVerificationProgress<'_>>,
|
||||
) -> Result<bool> {
|
||||
if output_files.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
for expected in output_files {
|
||||
let output_path = self.target_dir.join(&expected.path);
|
||||
let meta = match fs::metadata(&output_path) {
|
||||
Ok(meta) => meta,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
if meta.len() != expected.size {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let actual = Self::file_blake3_hex(&output_path, progress.as_deref_mut())?;
|
||||
if !actual.eq_ignore_ascii_case(&expected.blake3) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Removes any declared output files so a fresh archive attempt can restart cleanly.
|
||||
pub(crate) fn cleanup(&self, output_files: &[OutputFileChecksum]) {
|
||||
for output in output_files {
|
||||
let _ = fs::remove_file(self.target_dir.join(&output.path));
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the hex-encoded BLAKE3 checksum for one plain output file.
|
||||
fn file_blake3_hex(
|
||||
path: &Path,
|
||||
mut progress: Option<&mut ArchiveVerificationProgress<'_>>,
|
||||
) -> Result<String> {
|
||||
let mut file = fs::open(path)?;
|
||||
let mut hasher = Hasher::new();
|
||||
let mut buf = [0_u8; 64 * 1024];
|
||||
|
||||
loop {
|
||||
let n = file.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buf[..n]);
|
||||
if let Some(progress) = progress.as_deref_mut() {
|
||||
progress.record_verified(n as u64);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hasher.finalize().to_hex().to_string())
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use reth_provider::{
|
||||
};
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_stages::stages::calculate_gas_used_from_headers;
|
||||
use reth_storage_api::{DBProvider, TryIntoHistoricalStateProvider};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
@@ -112,7 +113,12 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let provider_factory = provider_factory.clone();
|
||||
move |block_number: u64| {
|
||||
StateProviderDatabase(
|
||||
provider_factory.history_by_block_number(block_number).unwrap(),
|
||||
provider_factory
|
||||
.provider()
|
||||
.unwrap()
|
||||
.disable_long_read_transaction_safety()
|
||||
.try_into_history_at_block(block_number)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -138,7 +144,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let cancellation = cancellation.clone();
|
||||
let next_block = Arc::clone(&next_block);
|
||||
tasks.spawn_blocking(move || {
|
||||
let executor_lifetime = Duration::from_secs(120);
|
||||
let executor_lifetime = Duration::from_secs(600);
|
||||
|
||||
loop {
|
||||
if cancellation.is_cancelled() {
|
||||
@@ -245,7 +251,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let _ = stats_tx.send(block.gas_used());
|
||||
|
||||
// Reset DB once in a while to avoid OOM or read tx timeouts
|
||||
if executor.size_hint() > 1_000_000 ||
|
||||
if executor.size_hint() > 5_000_000 ||
|
||||
executor_created.elapsed() > executor_lifetime
|
||||
{
|
||||
executor =
|
||||
|
||||
@@ -29,7 +29,10 @@ use execution::dump_execution_stage;
|
||||
mod merkle;
|
||||
use merkle::dump_merkle_stage;
|
||||
|
||||
/// `reth dump-stage` command
|
||||
/// `reth dump-stage` command.
|
||||
///
|
||||
/// Note: mutates the source datadir (unwinds hashing/merkle/execution before copying tables).
|
||||
/// Stop the node and back up the datadir first.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command<C: ChainSpecParser> {
|
||||
#[command(flatten)]
|
||||
@@ -100,8 +103,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
Comp: CliNodeComponents<N>,
|
||||
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
|
||||
{
|
||||
// `unwind_and_copy` opens a RW provider on the source datadir, so open RW here.
|
||||
let Environment { provider_factory, .. } =
|
||||
self.env.init::<N>(AccessRights::RO, runtime.clone())?;
|
||||
self.env.init::<N>(AccessRights::RW, runtime.clone())?;
|
||||
let tool = DbTool::new(provider_factory)?;
|
||||
let components = components(tool.chain());
|
||||
let evm_config = components.evm_config().clone();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,11 @@ pub struct LocalMiner<T: PayloadTypes, B, Pool: TransactionPool + Unpin> {
|
||||
last_header: SealedHeaderFor<<T::BuiltPayload as BuiltPayload>::Primitives>,
|
||||
/// Stores latest mined blocks.
|
||||
last_block_hashes: VecDeque<B256>,
|
||||
/// Optional sleep duration between initiating payload building and resolving.
|
||||
///
|
||||
/// When set, the miner sleeps after `fork_choice_updated` before calling
|
||||
/// `resolve_kind`, giving the payload job time for multiple rebuild attempts.
|
||||
payload_wait_time: Option<Duration>,
|
||||
}
|
||||
|
||||
impl<T, B, Pool> LocalMiner<T, B, Pool>
|
||||
@@ -170,9 +175,16 @@ where
|
||||
payload_builder,
|
||||
last_block_hashes: VecDeque::from([last_header.hash()]),
|
||||
last_header,
|
||||
payload_wait_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the payload wait time, if any.
|
||||
pub const fn with_payload_wait_time_opt(mut self, wait_time: Option<Duration>) -> Self {
|
||||
self.payload_wait_time = wait_time;
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs the [`LocalMiner`] in a loop, polling the miner and building payloads.
|
||||
pub async fn run(mut self) {
|
||||
let mut fcu_interval = tokio::time::interval(Duration::from_secs(1));
|
||||
@@ -238,6 +250,10 @@ where
|
||||
|
||||
let payload_id = res.payload_id.ok_or_eyre("No payload id")?;
|
||||
|
||||
if let Some(wait_time) = self.payload_wait_time {
|
||||
tokio::time::sleep(wait_time).await;
|
||||
}
|
||||
|
||||
let Some(Ok(payload)) =
|
||||
self.payload_builder.resolve_kind(payload_id, PayloadKind::WaitForPending).await
|
||||
else {
|
||||
|
||||
@@ -57,7 +57,7 @@ where
|
||||
.chain_spec
|
||||
.is_cancun_active_at_timestamp(timestamp)
|
||||
.then(B256::random),
|
||||
slot_number: None,
|
||||
slot_number: self.chain_spec.is_amsterdam_active_at_timestamp(timestamp).then_some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1693,6 +1704,7 @@ where
|
||||
let gas_used = payload.gas_used();
|
||||
let num_hash = payload.num_hash();
|
||||
let mut output = self.on_new_payload(payload);
|
||||
let latency = start.elapsed();
|
||||
self.metrics.engine.new_payload.update_response_metrics(
|
||||
start,
|
||||
&mut self.metrics.engine.forkchoice_updated.latest_finish_at,
|
||||
@@ -1700,12 +1712,6 @@ where
|
||||
gas_used,
|
||||
);
|
||||
|
||||
// Latency measures time from enqueue to completion, excluding
|
||||
// only the explicit persistence wait. This means backpressure
|
||||
// (time spent queued due to the engine being busy) is included,
|
||||
// reflecting real-world engine responsiveness.
|
||||
let latency = enqueued_at.elapsed() - explicit_persistence_wait;
|
||||
|
||||
let maybe_event =
|
||||
output.as_mut().ok().and_then(|out| out.event.take());
|
||||
|
||||
@@ -2010,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
|
||||
@@ -3014,8 +3024,14 @@ 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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1238,7 +1252,6 @@ mod tests {
|
||||
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
|
||||
OverlayStateProviderFactory::new(provider_factory, 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,11 @@ enum SparseTrieTaskMessage {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{keccak256, Address, B256, U256};
|
||||
use reth_provider::{
|
||||
providers::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 +978,43 @@ 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, 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};
|
||||
@@ -487,6 +490,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 +504,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,14 +519,6 @@ 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());
|
||||
@@ -532,10 +534,9 @@ where
|
||||
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
|
||||
@@ -763,10 +764,19 @@ where
|
||||
}
|
||||
|
||||
let (root, updates) = ensure_ok_post_block!(
|
||||
Self::compute_state_root_serial(overlay_factory.clone(), &hashed_state),
|
||||
Self::compute_state_root_serial_with_provider(
|
||||
provider_builder.clone(),
|
||||
&hashed_state
|
||||
),
|
||||
block
|
||||
);
|
||||
|
||||
self.compare_trie_updates_with_serial(
|
||||
overlay_factory.clone(),
|
||||
&hashed_state,
|
||||
updates.clone(),
|
||||
);
|
||||
|
||||
if state_root_task_failed {
|
||||
self.metrics.block_validation.state_root_task_fallback_success_total.increment(1);
|
||||
}
|
||||
@@ -1125,6 +1135,14 @@ where
|
||||
.root_with_updates()?)
|
||||
}
|
||||
|
||||
fn compute_state_root_serial_with_provider(
|
||||
provider_builder: StateProviderBuilder<N, P>,
|
||||
hashed_state: &LazyHashedPostState,
|
||||
) -> ProviderResult<(B256, TrieUpdates)> {
|
||||
let provider = provider_builder.build()?;
|
||||
provider.state_root_with_updates(hashed_state.get().clone())
|
||||
}
|
||||
|
||||
/// Awaits the state root from the background task, with an optional timeout fallback.
|
||||
///
|
||||
/// If a timeout is configured (`state_root_task_timeout`), this method first waits for the
|
||||
@@ -1439,7 +1457,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 +1476,6 @@ where
|
||||
provider_builder,
|
||||
overlay_factory,
|
||||
&self.config,
|
||||
block_access_list,
|
||||
);
|
||||
|
||||
// record prewarming initialization duration
|
||||
@@ -1472,12 +1488,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
|
||||
@@ -2110,6 +2122,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
|
||||
@@ -2142,4 +2165,15 @@ impl<T: PayloadTypes> BlockOrPayload<T> {
|
||||
Self::Block(block) => block.gas_used(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the gas limit used by the block.
|
||||
pub fn gas_limit(&self) -> u64
|
||||
where
|
||||
T::ExecutionData: ExecutionPayload,
|
||||
{
|
||||
match self {
|
||||
Self::Payload(payload) => payload.gas_limit(),
|
||||
Self::Block(block) => block.gas_limit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use reth_primitives_traits::dashmap::DashMap;
|
||||
use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
|
||||
use revm_primitives::Address;
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
use tracing::error;
|
||||
|
||||
/// Default max cache size for [`PrecompileCache`]
|
||||
const MAX_CACHE_SIZE: u32 = 10_000;
|
||||
@@ -87,8 +88,14 @@ impl<S> CacheEntry<S> {
|
||||
self.output.gas_used
|
||||
}
|
||||
|
||||
fn to_precompile_result(&self) -> PrecompileResult {
|
||||
Ok(self.output.clone())
|
||||
/// Converts the cache entry to a precompile result. Accepts state gas reservoir as input.
|
||||
///
|
||||
/// All cached precompiles are not expected to access/created state and thus reservoir is always
|
||||
/// kept as is.
|
||||
fn to_precompile_result(&self, reservoir: u64) -> PrecompileResult {
|
||||
let mut output = self.output.clone();
|
||||
output.reservoir = reservoir;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,22 +182,34 @@ where
|
||||
input.gas >= entry.gas_used()
|
||||
{
|
||||
self.increment_by_one_precompile_cache_hits();
|
||||
return entry.to_precompile_result()
|
||||
return entry.to_precompile_result(input.reservoir);
|
||||
}
|
||||
|
||||
let calldata = input.data;
|
||||
let reservoir = input.reservoir;
|
||||
let result = self.precompile.call(input);
|
||||
|
||||
match &result {
|
||||
// Only successful outputs are cacheable. Non-success statuses and errors must execute
|
||||
// again instead of poisoning the cache for subsequent calls.
|
||||
Ok(output) if output.is_success() => {
|
||||
let size = self.cache.insert(
|
||||
Bytes::copy_from_slice(calldata),
|
||||
CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
|
||||
);
|
||||
self.set_precompile_cache_size_metric(size as f64);
|
||||
self.increment_by_one_precompile_cache_misses();
|
||||
// Sanity-check precompile output to ensure that it does not affect state gas in any
|
||||
// way.
|
||||
//
|
||||
// This does not fully protect us from caching stateful precompiles but might make
|
||||
// it obvious when the node is misconfigured.
|
||||
if output.reservoir != reservoir {
|
||||
error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile decremented reservoir, skipping cache insertion");
|
||||
} else if output.state_gas_used != 0 {
|
||||
error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile used state gas, skipping cache insertion");
|
||||
} else {
|
||||
let size = self.cache.insert(
|
||||
Bytes::copy_from_slice(calldata),
|
||||
CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
|
||||
);
|
||||
self.set_precompile_cache_size_metric(size as f64);
|
||||
self.increment_by_one_precompile_cache_misses();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.increment_by_one_precompile_errors();
|
||||
@@ -246,6 +265,7 @@ mod tests {
|
||||
gas_used: 0,
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
bytes: Bytes::default(),
|
||||
})
|
||||
})
|
||||
@@ -259,6 +279,7 @@ mod tests {
|
||||
gas_used: 50,
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
|
||||
};
|
||||
|
||||
@@ -292,6 +313,7 @@ mod tests {
|
||||
gas_used: 5000,
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
|
||||
})
|
||||
}
|
||||
@@ -308,6 +330,7 @@ mod tests {
|
||||
gas_used: 7000,
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -195,6 +195,7 @@ where
|
||||
ommers: &block.body().ommers,
|
||||
withdrawals: block.body().withdrawals.as_ref().map(|w| Cow::Borrowed(w.as_slice())),
|
||||
extra_data: block.header().extra_data.clone(),
|
||||
slot_number: block.header().slot_number,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,6 +211,7 @@ where
|
||||
ommers: &[],
|
||||
withdrawals: attributes.withdrawals.map(|w| Cow::Owned(w.into_inner())),
|
||||
extra_data: attributes.extra_data,
|
||||
slot_number: attributes.slot_number,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -274,7 +276,7 @@ where
|
||||
gas_limit: payload.payload.gas_limit(),
|
||||
basefee: payload.payload.saturated_base_fee_per_gas(),
|
||||
blob_excess_gas_and_price,
|
||||
slot_num: 0,
|
||||
slot_num: payload.payload.as_v4().map(|v4| v4.slot_number).unwrap_or_default(),
|
||||
};
|
||||
|
||||
Ok(EvmEnv { cfg_env, block_env })
|
||||
@@ -291,6 +293,7 @@ where
|
||||
ommers: &[],
|
||||
withdrawals: payload.payload.withdrawals().map(|w| Cow::Borrowed(w.as_slice())),
|
||||
extra_data: payload.payload.as_v1().extra_data.clone(),
|
||||
slot_number: payload.payload.as_v4().map(|v4| v4.slot_number),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ reth-rpc-eth-types.workspace = true
|
||||
reth-engine-local.workspace = true
|
||||
reth-engine-primitives = { workspace = true, features = ["std"] }
|
||||
reth-payload-primitives.workspace = true
|
||||
|
||||
# ethereum
|
||||
alloy-eips.workspace = true
|
||||
alloy-network.workspace = true
|
||||
|
||||
@@ -26,9 +26,10 @@ use reth_node_builder::{
|
||||
},
|
||||
node::{FullNodeTypes, NodeTypes},
|
||||
rpc::{
|
||||
BasicEngineApiBuilder, BasicEngineValidatorBuilder, EngineApiBuilder, EngineValidatorAddOn,
|
||||
EngineValidatorBuilder, EthApiBuilder, EthApiCtx, Identity, PayloadValidatorBuilder,
|
||||
RethRpcAddOns, RpcAddOns, RpcHandle,
|
||||
BasicEngineApiBuilder, BasicEngineValidatorBuilder, Either, EngineApiBuilder,
|
||||
EngineValidatorAddOn, EngineValidatorBuilder, EthApiBuilder, EthApiCtx, Identity,
|
||||
PayloadValidatorBuilder, RethAuthHttpMiddleware, RethRpcAddOns, RethRpcMiddleware,
|
||||
RpcAddOns, RpcHandle, Stack,
|
||||
},
|
||||
BuilderContext, DebugNode, Node, NodeAdapter,
|
||||
};
|
||||
@@ -39,7 +40,7 @@ use reth_rpc::{
|
||||
TestingApi, ValidationApi,
|
||||
};
|
||||
use reth_rpc_api::servers::{BlockSubmissionValidationApiServer, TestingApiServer};
|
||||
use reth_rpc_builder::{config::RethRpcServerConfig, middleware::RethRpcMiddleware};
|
||||
use reth_rpc_builder::config::RethRpcServerConfig;
|
||||
use reth_rpc_eth_api::{
|
||||
helpers::{
|
||||
config::{EthConfigApiServer, EthConfigHandler},
|
||||
@@ -165,17 +166,21 @@ pub struct EthereumAddOns<
|
||||
EB = BasicEngineApiBuilder<PVB>,
|
||||
EVB = BasicEngineValidatorBuilder<PVB>,
|
||||
RpcMiddleware = Identity,
|
||||
AuthHttpMiddleware = Identity,
|
||||
> {
|
||||
inner: RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>,
|
||||
inner: RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>,
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
EthB: EthApiBuilder<N>,
|
||||
{
|
||||
/// Creates a new instance from the inner `RpcAddOns`.
|
||||
pub const fn new(inner: RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>) -> Self {
|
||||
pub const fn new(
|
||||
inner: RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>,
|
||||
) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
@@ -199,11 +204,13 @@ where
|
||||
BasicEngineApiBuilder::default(),
|
||||
BasicEngineValidatorBuilder::default(),
|
||||
Default::default(),
|
||||
Identity::new(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
EthB: EthApiBuilder<N>,
|
||||
@@ -212,7 +219,7 @@ where
|
||||
pub fn with_engine_api<T>(
|
||||
self,
|
||||
engine_api_builder: T,
|
||||
) -> EthereumAddOns<N, EthB, PVB, T, EVB, RpcMiddleware>
|
||||
) -> EthereumAddOns<N, EthB, PVB, T, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
T: Send,
|
||||
{
|
||||
@@ -224,7 +231,7 @@ where
|
||||
pub fn with_payload_validator<V, T>(
|
||||
self,
|
||||
payload_validator_builder: T,
|
||||
) -> EthereumAddOns<N, EthB, T, EB, EVB, RpcMiddleware> {
|
||||
) -> EthereumAddOns<N, EthB, T, EB, EVB, RpcMiddleware, AuthHttpMiddleware> {
|
||||
let Self { inner } = self;
|
||||
EthereumAddOns::new(inner.with_payload_validator(payload_validator_builder))
|
||||
}
|
||||
@@ -233,7 +240,7 @@ where
|
||||
pub fn with_rpc_middleware<T>(
|
||||
self,
|
||||
rpc_middleware: T,
|
||||
) -> EthereumAddOns<N, EthB, PVB, EB, EVB, T>
|
||||
) -> EthereumAddOns<N, EthB, PVB, EB, EVB, T, AuthHttpMiddleware>
|
||||
where
|
||||
T: Send,
|
||||
{
|
||||
@@ -241,6 +248,45 @@ where
|
||||
EthereumAddOns::new(inner.with_rpc_middleware(rpc_middleware))
|
||||
}
|
||||
|
||||
/// Configures the HTTP transport middleware for the auth / Engine API server.
|
||||
pub fn with_auth_http_middleware<T>(
|
||||
self,
|
||||
auth_http_middleware: T,
|
||||
) -> EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, T>
|
||||
where
|
||||
T: Send,
|
||||
{
|
||||
let Self { inner } = self;
|
||||
EthereumAddOns::new(inner.with_auth_http_middleware(auth_http_middleware))
|
||||
}
|
||||
|
||||
/// Stacks an additional HTTP transport middleware layer for the auth / Engine API server.
|
||||
pub fn layer_auth_http_middleware<T>(
|
||||
self,
|
||||
layer: T,
|
||||
) -> EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, Stack<AuthHttpMiddleware, T>> {
|
||||
let Self { inner } = self;
|
||||
EthereumAddOns::new(inner.layer_auth_http_middleware(layer))
|
||||
}
|
||||
|
||||
/// Conditionally stacks an HTTP transport middleware layer for the auth / Engine API server.
|
||||
#[expect(clippy::type_complexity)]
|
||||
pub fn option_layer_auth_http_middleware<T>(
|
||||
self,
|
||||
layer: Option<T>,
|
||||
) -> EthereumAddOns<
|
||||
N,
|
||||
EthB,
|
||||
PVB,
|
||||
EB,
|
||||
EVB,
|
||||
RpcMiddleware,
|
||||
Stack<AuthHttpMiddleware, Either<T, Identity>>,
|
||||
> {
|
||||
let Self { inner } = self;
|
||||
EthereumAddOns::new(inner.option_layer_auth_http_middleware(layer))
|
||||
}
|
||||
|
||||
/// Sets the tokio runtime for the RPC servers.
|
||||
///
|
||||
/// Caution: This runtime must not be created from within asynchronous context.
|
||||
@@ -250,8 +296,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> NodeAddOns<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> NodeAddOns<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
@@ -268,6 +314,7 @@ where
|
||||
EthApiError: FromEvmError<N::Evm>,
|
||||
EvmFactoryFor<N::Evm>: EvmFactory<Tx = TxEnv>,
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
AuthHttpMiddleware: RethAuthHttpMiddleware<Identity>,
|
||||
{
|
||||
type Handle = RpcHandle<N, EthB::EthApi>;
|
||||
|
||||
@@ -323,8 +370,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> RethRpcAddOns<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> RethRpcAddOns<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
@@ -341,6 +388,7 @@ where
|
||||
EthApiError: FromEvmError<N::Evm>,
|
||||
EvmFactoryFor<N::Evm>: EvmFactory<Tx = TxEnv>,
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
AuthHttpMiddleware: RethAuthHttpMiddleware<Identity>,
|
||||
{
|
||||
type EthApi = EthB::EthApi;
|
||||
|
||||
@@ -349,8 +397,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> EngineValidatorAddOn<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> EngineValidatorAddOn<N>
|
||||
for EthereumAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
@@ -367,6 +415,7 @@ where
|
||||
EthApiError: FromEvmError<N::Evm>,
|
||||
EvmFactoryFor<N::Evm>: EvmFactory<Tx = TxEnv>,
|
||||
RpcMiddleware: Send,
|
||||
AuthHttpMiddleware: Send,
|
||||
{
|
||||
type ValidatorBuilder = EVB;
|
||||
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
@@ -248,7 +248,7 @@ impl Discv5 {
|
||||
discv5::Event::SocketUpdated(_) | discv5::Event::TalkRequest(_) |
|
||||
// `Discovered` not unique discovered peers
|
||||
discv5::Event::Discovered(_) => None,
|
||||
discv5::Event::NodeInserted { replaced: _, .. } => {
|
||||
discv5::Event::NodeInserted { .. } => {
|
||||
|
||||
// node has been inserted into kbuckets
|
||||
|
||||
@@ -698,8 +698,6 @@ 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 tracing::trace;
|
||||
|
||||
fn discv5_noop() -> Discv5 {
|
||||
@@ -937,11 +935,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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! `RLPx` disconnect reason sent to/received from peer
|
||||
|
||||
use alloc::vec;
|
||||
use alloy_primitives::bytes::{Buf, BufMut};
|
||||
use alloy_rlp::{Decodable, Encodable, Header};
|
||||
use derive_more::Display;
|
||||
@@ -84,10 +83,10 @@ impl Encodable for DisconnectReason {
|
||||
/// The [`Encodable`] implementation for [`DisconnectReason`] encodes the disconnect reason in
|
||||
/// a single-element RLP list.
|
||||
fn encode(&self, out: &mut dyn BufMut) {
|
||||
vec![*self as u8].encode(out);
|
||||
alloy_rlp::encode_list(&[*self as u8], out);
|
||||
}
|
||||
fn length(&self) -> usize {
|
||||
vec![*self as u8].length()
|
||||
alloy_rlp::list_length(&[*self as u8])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,111 @@ 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(
|
||||
&self,
|
||||
hashes: Vec<B256>,
|
||||
_priority: Priority,
|
||||
) -> 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,
|
||||
|
||||
@@ -307,6 +307,14 @@ where
|
||||
|
||||
let dev_mining_mode =
|
||||
mining_mode.unwrap_or_else(|| handle.node.config.dev_mining_mode(pool));
|
||||
let payload_wait_time = config.dev.payload_wait_time;
|
||||
if let (Some(wait_time), Some(block_time)) = (payload_wait_time, config.dev.block_time)
|
||||
{
|
||||
eyre::ensure!(
|
||||
wait_time <= block_time,
|
||||
"--dev.payload-wait-time ({wait_time:?}) must be <= --dev.block-time ({block_time:?})"
|
||||
);
|
||||
}
|
||||
handle.node.task_executor.spawn_critical_task("local engine", async move {
|
||||
LocalMiner::new(
|
||||
blockchain_db,
|
||||
@@ -315,6 +323,7 @@ where
|
||||
dev_mining_mode,
|
||||
payload_builder_handle,
|
||||
)
|
||||
.with_payload_wait_time_opt(payload_wait_time)
|
||||
.run()
|
||||
.await
|
||||
});
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
//! Builder support for rpc components.
|
||||
|
||||
pub use jsonrpsee::server::middleware::rpc::{RpcService, RpcServiceBuilder};
|
||||
pub use jsonrpsee::{
|
||||
core::middleware::layer::Either,
|
||||
server::middleware::rpc::{RpcService, RpcServiceBuilder},
|
||||
};
|
||||
use reth_engine_tree::tree::WaitForCaches;
|
||||
pub use reth_engine_tree::tree::{BasicEngineValidator, EngineValidator};
|
||||
pub use reth_rpc_builder::{middleware::RethRpcMiddleware, Identity, Stack};
|
||||
pub use reth_rpc_builder::{
|
||||
middleware::{RethAuthHttpMiddleware, RethRpcMiddleware},
|
||||
Identity, Stack,
|
||||
};
|
||||
pub use reth_trie_db::ChangesetCache;
|
||||
|
||||
use crate::{
|
||||
@@ -12,7 +18,7 @@ use crate::{
|
||||
};
|
||||
use alloy_rpc_types::engine::ClientVersionV1;
|
||||
use alloy_rpc_types_engine::ExecutionData;
|
||||
use jsonrpsee::{core::middleware::layer::Either, RpcModule};
|
||||
use jsonrpsee::RpcModule;
|
||||
use parking_lot::Mutex;
|
||||
use reth_chain_state::CanonStateSubscriptions;
|
||||
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks, Hardforks};
|
||||
@@ -517,6 +523,7 @@ pub struct RpcAddOns<
|
||||
EB = BasicEngineApiBuilder<PVB>,
|
||||
EVB = BasicEngineValidatorBuilder<PVB>,
|
||||
RpcMiddleware = Identity,
|
||||
AuthHttpMiddleware = Identity,
|
||||
> {
|
||||
/// Additional RPC add-ons.
|
||||
pub hooks: RpcHooks<Node, EthB::EthApi>,
|
||||
@@ -533,12 +540,17 @@ pub struct RpcAddOns<
|
||||
/// This middleware is applied to all RPC requests across all transports (HTTP, WS, IPC).
|
||||
/// See [`RpcAddOns::with_rpc_middleware`] for more details.
|
||||
rpc_middleware: RpcMiddleware,
|
||||
/// Configurable HTTP transport middleware for the auth server.
|
||||
///
|
||||
/// This middleware is applied after JWT authentication and before JSON-RPC parsing on the
|
||||
/// auth / Engine API server, giving access to the raw HTTP request.
|
||||
auth_http_middleware: AuthHttpMiddleware,
|
||||
/// Optional custom tokio runtime for the RPC server.
|
||||
tokio_runtime: Option<tokio::runtime::Handle>,
|
||||
}
|
||||
|
||||
impl<Node, EthB, PVB, EB, EVB, RpcMiddleware> Debug
|
||||
for RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<Node, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> Debug
|
||||
for RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
Node: FullNodeComponents,
|
||||
EthB: EthApiBuilder<Node>,
|
||||
@@ -558,7 +570,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Node, EthB, PVB, EB, EVB, RpcMiddleware> RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<Node, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
Node: FullNodeComponents,
|
||||
EthB: EthApiBuilder<Node>,
|
||||
@@ -570,6 +583,7 @@ where
|
||||
engine_api_builder: EB,
|
||||
engine_validator_builder: EVB,
|
||||
rpc_middleware: RpcMiddleware,
|
||||
auth_http_middleware: AuthHttpMiddleware,
|
||||
) -> Self {
|
||||
Self {
|
||||
hooks: RpcHooks::default(),
|
||||
@@ -578,6 +592,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime: None,
|
||||
}
|
||||
}
|
||||
@@ -586,13 +601,14 @@ where
|
||||
pub fn with_engine_api<T>(
|
||||
self,
|
||||
engine_api_builder: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, T, EVB, RpcMiddleware> {
|
||||
) -> RpcAddOns<Node, EthB, PVB, T, EVB, RpcMiddleware, AuthHttpMiddleware> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
..
|
||||
} = self;
|
||||
@@ -603,6 +619,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
@@ -611,13 +628,14 @@ where
|
||||
pub fn with_payload_validator<T>(
|
||||
self,
|
||||
payload_validator_builder: T,
|
||||
) -> RpcAddOns<Node, EthB, T, EB, EVB, RpcMiddleware> {
|
||||
) -> RpcAddOns<Node, EthB, T, EB, EVB, RpcMiddleware, AuthHttpMiddleware> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
..
|
||||
} = self;
|
||||
@@ -628,6 +646,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
@@ -636,13 +655,14 @@ where
|
||||
pub fn with_engine_validator<T>(
|
||||
self,
|
||||
engine_validator_builder: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, T, RpcMiddleware> {
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, T, RpcMiddleware, AuthHttpMiddleware> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
..
|
||||
} = self;
|
||||
@@ -653,6 +673,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
@@ -698,13 +719,14 @@ where
|
||||
pub fn with_rpc_middleware<T>(
|
||||
self,
|
||||
rpc_middleware: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, T> {
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, T, AuthHttpMiddleware> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
..
|
||||
} = self;
|
||||
@@ -715,10 +737,87 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the HTTP transport middleware for the auth / Engine API server.
|
||||
///
|
||||
/// This middleware is applied after JWT authentication and before JSON-RPC parsing,
|
||||
/// giving access to the raw HTTP request (headers, body, etc.).
|
||||
pub fn with_auth_http_middleware<T>(
|
||||
self,
|
||||
auth_http_middleware: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware, T> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
tokio_runtime,
|
||||
..
|
||||
} = self;
|
||||
RpcAddOns {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stacks an additional HTTP transport middleware layer for the auth / Engine API server.
|
||||
pub fn layer_auth_http_middleware<T>(
|
||||
self,
|
||||
layer: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, RpcMiddleware, Stack<AuthHttpMiddleware, T>> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
} = self;
|
||||
let auth_http_middleware = Stack::new(auth_http_middleware, layer);
|
||||
RpcAddOns {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
payload_validator_builder,
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
|
||||
/// Conditionally stacks an HTTP transport middleware layer for the auth / Engine API server.
|
||||
#[expect(clippy::type_complexity)]
|
||||
pub fn option_layer_auth_http_middleware<T>(
|
||||
self,
|
||||
layer: Option<T>,
|
||||
) -> RpcAddOns<
|
||||
Node,
|
||||
EthB,
|
||||
PVB,
|
||||
EB,
|
||||
EVB,
|
||||
RpcMiddleware,
|
||||
Stack<AuthHttpMiddleware, Either<T, Identity>>,
|
||||
> {
|
||||
let layer = layer.map(Either::Left).unwrap_or(Either::Right(Identity::new()));
|
||||
self.layer_auth_http_middleware(layer)
|
||||
}
|
||||
|
||||
/// Sets the tokio runtime for the RPC servers.
|
||||
///
|
||||
/// Caution: This runtime must not be created from within asynchronous context.
|
||||
@@ -730,6 +829,7 @@ where
|
||||
engine_validator_builder,
|
||||
engine_api_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
..
|
||||
} = self;
|
||||
Self {
|
||||
@@ -739,6 +839,7 @@ where
|
||||
engine_validator_builder,
|
||||
engine_api_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
@@ -747,7 +848,7 @@ where
|
||||
pub fn layer_rpc_middleware<T>(
|
||||
self,
|
||||
layer: T,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, Stack<RpcMiddleware, T>> {
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, Stack<RpcMiddleware, T>, AuthHttpMiddleware> {
|
||||
let Self {
|
||||
hooks,
|
||||
eth_api_builder,
|
||||
@@ -755,6 +856,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
} = self;
|
||||
let rpc_middleware = Stack::new(rpc_middleware, layer);
|
||||
@@ -765,6 +867,7 @@ where
|
||||
engine_api_builder,
|
||||
engine_validator_builder,
|
||||
rpc_middleware,
|
||||
auth_http_middleware,
|
||||
tokio_runtime,
|
||||
}
|
||||
}
|
||||
@@ -774,7 +877,15 @@ where
|
||||
pub fn option_layer_rpc_middleware<T>(
|
||||
self,
|
||||
layer: Option<T>,
|
||||
) -> RpcAddOns<Node, EthB, PVB, EB, EVB, Stack<RpcMiddleware, Either<T, Identity>>> {
|
||||
) -> RpcAddOns<
|
||||
Node,
|
||||
EthB,
|
||||
PVB,
|
||||
EB,
|
||||
EVB,
|
||||
Stack<RpcMiddleware, Either<T, Identity>>,
|
||||
AuthHttpMiddleware,
|
||||
> {
|
||||
let layer = layer.map(Either::Left).unwrap_or(Either::Right(Identity::new()));
|
||||
self.layer_rpc_middleware(layer)
|
||||
}
|
||||
@@ -800,7 +911,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Node, EthB, EV, EB, Engine> Default for RpcAddOns<Node, EthB, EV, EB, Engine, Identity>
|
||||
impl<Node, EthB, EV, EB, Engine> Default
|
||||
for RpcAddOns<Node, EthB, EV, EB, Engine, Identity, Identity>
|
||||
where
|
||||
Node: FullNodeComponents,
|
||||
EthB: EthApiBuilder<Node>,
|
||||
@@ -815,11 +927,13 @@ where
|
||||
EB::default(),
|
||||
Engine::default(),
|
||||
Default::default(),
|
||||
Identity::new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
N::Provider: ChainSpecProvider<ChainSpec: EthereumHardforks>,
|
||||
@@ -827,6 +941,7 @@ where
|
||||
EB: EngineApiBuilder<N>,
|
||||
EVB: EngineValidatorBuilder<N>,
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
AuthHttpMiddleware: RethAuthHttpMiddleware<Identity>,
|
||||
{
|
||||
/// Launches only the regular RPC server (HTTP/WS/IPC), without the authenticated Engine API
|
||||
/// server.
|
||||
@@ -913,6 +1028,7 @@ where
|
||||
F: FnOnce(RpcModuleContainer<'_, N, EthB::EthApi>) -> eyre::Result<()>,
|
||||
{
|
||||
let rpc_middleware = self.rpc_middleware.clone();
|
||||
let auth_http_middleware = self.auth_http_middleware.clone();
|
||||
let tokio_runtime = self.tokio_runtime.clone();
|
||||
let setup_ctx = self.setup_rpc_components(ctx, ext).await?;
|
||||
let RpcSetupContext {
|
||||
@@ -933,6 +1049,8 @@ where
|
||||
.set_rpc_middleware(rpc_middleware)
|
||||
.with_tokio_runtime(tokio_runtime);
|
||||
|
||||
let auth_config = auth_config.with_http_middleware(auth_http_middleware);
|
||||
|
||||
let (rpc, auth) = if disable_auth {
|
||||
// Only launch the RPC server, use a noop auth handle
|
||||
let rpc = Self::launch_rpc_server_internal(server_config, &modules).await?;
|
||||
@@ -942,7 +1060,7 @@ where
|
||||
// launch servers concurrently
|
||||
let (rpc, auth) = futures::future::try_join(
|
||||
Self::launch_rpc_server_internal(server_config, &modules),
|
||||
Self::launch_auth_server_internal(auth_module_clone, auth_config),
|
||||
Self::launch_auth_server_internal(auth_config.start(auth_module_clone)),
|
||||
)
|
||||
.await?;
|
||||
(rpc, auth)
|
||||
@@ -1087,10 +1205,9 @@ where
|
||||
|
||||
/// Helper to launch the auth server
|
||||
async fn launch_auth_server_internal(
|
||||
auth_module: AuthRpcModule,
|
||||
auth_config: reth_rpc_builder::auth::AuthServerConfig,
|
||||
start_fut: impl Future<Output = Result<AuthServerHandle, reth_rpc_builder::error::RpcError>>,
|
||||
) -> eyre::Result<AuthServerHandle> {
|
||||
auth_module.start_server(auth_config)
|
||||
start_fut
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.inspect(|handle| {
|
||||
@@ -1120,8 +1237,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> NodeAddOns<N>
|
||||
for RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> NodeAddOns<N>
|
||||
for RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
<N as FullNodeTypes>::Provider: ChainSpecProvider<ChainSpec: EthereumHardforks>,
|
||||
@@ -1130,6 +1247,7 @@ where
|
||||
EB: EngineApiBuilder<N>,
|
||||
EVB: EngineValidatorBuilder<N>,
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
AuthHttpMiddleware: RethAuthHttpMiddleware<Identity>,
|
||||
{
|
||||
type Handle = RpcHandle<N, EthB::EthApi>;
|
||||
|
||||
@@ -1150,8 +1268,8 @@ pub trait RethRpcAddOns<N: FullNodeComponents>:
|
||||
fn hooks_mut(&mut self) -> &mut RpcHooks<N, Self::EthApi>;
|
||||
}
|
||||
|
||||
impl<N: FullNodeComponents, EthB, EV, EB, Engine, RpcMiddleware> RethRpcAddOns<N>
|
||||
for RpcAddOns<N, EthB, EV, EB, Engine, RpcMiddleware>
|
||||
impl<N: FullNodeComponents, EthB, EV, EB, Engine, RpcMiddleware, AuthHttpMiddleware>
|
||||
RethRpcAddOns<N> for RpcAddOns<N, EthB, EV, EB, Engine, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
Self: NodeAddOns<N, Handle = RpcHandle<N, EthB::EthApi>>,
|
||||
EthB: EthApiBuilder<N>,
|
||||
@@ -1221,8 +1339,8 @@ pub trait EngineValidatorAddOn<Node: FullNodeComponents>: Send {
|
||||
fn engine_validator_builder(&self) -> Self::ValidatorBuilder;
|
||||
}
|
||||
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware> EngineValidatorAddOn<N>
|
||||
for RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware>
|
||||
impl<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware> EngineValidatorAddOn<N>
|
||||
for RpcAddOns<N, EthB, PVB, EB, EVB, RpcMiddleware, AuthHttpMiddleware>
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
EthB: EthApiBuilder<N>,
|
||||
@@ -1230,6 +1348,7 @@ where
|
||||
EB: EngineApiBuilder<N>,
|
||||
EVB: EngineValidatorBuilder<N>,
|
||||
RpcMiddleware: Send,
|
||||
AuthHttpMiddleware: Send,
|
||||
{
|
||||
type ValidatorBuilder = EVB;
|
||||
|
||||
|
||||
@@ -42,6 +42,22 @@ pub struct DevArgs {
|
||||
)]
|
||||
pub block_time: Option<Duration>,
|
||||
|
||||
/// Time to wait after initiating payload building before resolving.
|
||||
///
|
||||
/// Introduces a sleep between `fork_choice_updated` and `resolve_kind` in the
|
||||
/// local miner, giving the payload job time for multiple rebuild attempts with
|
||||
/// new transactions from the pool.
|
||||
///
|
||||
/// Parses strings using [`humantime::parse_duration`]
|
||||
/// --dev.payload-wait-time 450ms
|
||||
#[arg(
|
||||
long = "dev.payload-wait-time",
|
||||
help_heading = "Dev testnet",
|
||||
value_parser = parse_duration,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub payload_wait_time: Option<Duration>,
|
||||
|
||||
/// Derive dev accounts from a fixed mnemonic instead of random ones.
|
||||
#[arg(
|
||||
long = "dev.mnemonic",
|
||||
@@ -60,6 +76,7 @@ impl Default for DevArgs {
|
||||
dev: false,
|
||||
block_max_transactions: None,
|
||||
block_time: None,
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -86,6 +103,7 @@ mod tests {
|
||||
dev: false,
|
||||
block_max_transactions: None,
|
||||
block_time: None,
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
);
|
||||
@@ -97,6 +115,7 @@ mod tests {
|
||||
dev: true,
|
||||
block_max_transactions: None,
|
||||
block_time: None,
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
);
|
||||
@@ -108,6 +127,7 @@ mod tests {
|
||||
dev: true,
|
||||
block_max_transactions: None,
|
||||
block_time: None,
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
);
|
||||
@@ -125,6 +145,7 @@ mod tests {
|
||||
dev: true,
|
||||
block_max_transactions: Some(2),
|
||||
block_time: None,
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
);
|
||||
@@ -137,6 +158,7 @@ mod tests {
|
||||
dev: true,
|
||||
block_max_transactions: None,
|
||||
block_time: Some(std::time::Duration::from_secs(1)),
|
||||
payload_wait_time: None,
|
||||
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -126,9 +126,9 @@ pub enum VersionSpecificValidationError {
|
||||
/// root after Cancun
|
||||
#[error("no parent beacon block root post-cancun")]
|
||||
NoParentBeaconBlockRootPostCancun,
|
||||
/// Thrown if the pre-V6 `PayloadAttributes` or `ExecutionPayload` contains a block access list
|
||||
#[error("block access list not before V6")]
|
||||
BlockAccessListNotSupportedBeforeV6,
|
||||
/// Thrown if the current engine method version does not support a block access list
|
||||
#[error("block access list not supported in this engine API version")]
|
||||
BlockAccessListNotSupported,
|
||||
/// Thrown if `engine_newPayload` contains no block access list
|
||||
/// after Amsterdam
|
||||
#[error("no block access list post-Amsterdam")]
|
||||
@@ -137,9 +137,9 @@ pub enum VersionSpecificValidationError {
|
||||
/// before Amsterdam
|
||||
#[error("block access list pre-Amsterdam")]
|
||||
HasBlockAccessListPreAmsterdam,
|
||||
/// Thrown if the pre-V6 `PayloadAttributes` or `ExecutionPayload` contains a slot number
|
||||
#[error("slot number not before V6")]
|
||||
SlotNumberNotSupportedBeforeV6,
|
||||
/// Thrown if the current engine method version does not support a slot number
|
||||
#[error("slot number not supported in this engine API version")]
|
||||
SlotNumberNotSupported,
|
||||
/// Thrown if `engine_newPayload` contains no slot number
|
||||
/// after Amsterdam
|
||||
#[error("no slot number post-Amsterdam")]
|
||||
|
||||
@@ -62,8 +62,10 @@ pub trait PayloadTypes: Send + Sync + Unpin + core::fmt::Debug + Clone + 'static
|
||||
/// * If V3, this ensures that the payload timestamp is within the Cancun timestamp.
|
||||
/// * If V4, this ensures that the payload timestamp is within the Prague timestamp.
|
||||
/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp.
|
||||
/// * If V6, this ensures that the payload timestamp is within the Amsterdam timestamp.
|
||||
///
|
||||
/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload.
|
||||
/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload and that
|
||||
/// staggered endpoint upgrades reject the next fork once a newer method version is required.
|
||||
///
|
||||
/// Otherwise, this will return [`EngineObjectValidationError::UnsupportedFork`].
|
||||
pub fn validate_payload_timestamp(
|
||||
@@ -151,12 +153,26 @@ pub fn validate_payload_timestamp(
|
||||
return Err(EngineObjectValidationError::UnsupportedFork)
|
||||
}
|
||||
|
||||
let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
|
||||
|
||||
// Staggered endpoint upgrades must reject Amsterdam payloads until the Amsterdam-specific
|
||||
// method version is used.
|
||||
if is_amsterdam &&
|
||||
matches!(
|
||||
(version, kind),
|
||||
(EngineApiMessageVersion::V3, MessageValidationKind::PayloadAttributes) |
|
||||
(EngineApiMessageVersion::V4, MessageValidationKind::Payload) |
|
||||
(EngineApiMessageVersion::V5, MessageValidationKind::GetPayload)
|
||||
)
|
||||
{
|
||||
return Err(EngineObjectValidationError::UnsupportedFork)
|
||||
}
|
||||
|
||||
// `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
|
||||
if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
|
||||
return Err(EngineObjectValidationError::UnsupportedFork)
|
||||
}
|
||||
|
||||
let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
|
||||
if version.is_v6() && !is_amsterdam {
|
||||
// From the Engine API spec:
|
||||
// <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
|
||||
@@ -183,16 +199,30 @@ pub fn validate_block_access_list_presence<T: EthereumHardforks>(
|
||||
has_block_access_list: bool,
|
||||
) -> Result<(), EngineObjectValidationError> {
|
||||
let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
|
||||
|
||||
match version {
|
||||
EngineApiMessageVersion::V1 |
|
||||
EngineApiMessageVersion::V2 |
|
||||
EngineApiMessageVersion::V3 |
|
||||
EngineApiMessageVersion::V4 |
|
||||
EngineApiMessageVersion::V5 => {
|
||||
EngineApiMessageVersion::V4 => {
|
||||
if has_block_access_list {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::BlockAccessListNotSupportedBeforeV6))
|
||||
.to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
EngineApiMessageVersion::V5 => {
|
||||
if message_validation_kind == MessageValidationKind::Payload {
|
||||
if is_amsterdam_active && !has_block_access_list {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
|
||||
}
|
||||
if !is_amsterdam_active && has_block_access_list {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
|
||||
}
|
||||
} else if has_block_access_list {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,14 +254,42 @@ pub fn validate_slot_number_presence<T: EthereumHardforks>(
|
||||
let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
|
||||
|
||||
match version {
|
||||
EngineApiMessageVersion::V1 |
|
||||
EngineApiMessageVersion::V2 |
|
||||
EngineApiMessageVersion::V3 |
|
||||
EngineApiMessageVersion::V4 |
|
||||
EngineApiMessageVersion::V5 => {
|
||||
EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => {
|
||||
if has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::SlotNumberNotSupportedBeforeV6))
|
||||
.to_error(VersionSpecificValidationError::SlotNumberNotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
EngineApiMessageVersion::V4 => {
|
||||
if message_validation_kind == MessageValidationKind::PayloadAttributes {
|
||||
if is_amsterdam_active && !has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
|
||||
}
|
||||
if !is_amsterdam_active && has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
|
||||
}
|
||||
} else if has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::SlotNumberNotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
EngineApiMessageVersion::V5 => {
|
||||
if message_validation_kind == MessageValidationKind::Payload {
|
||||
if is_amsterdam_active && !has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
|
||||
}
|
||||
if !is_amsterdam_active && has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
|
||||
}
|
||||
} else if has_slot_number {
|
||||
return Err(message_validation_kind
|
||||
.to_error(VersionSpecificValidationError::SlotNumberNotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,6 +709,75 @@ mod tests {
|
||||
assert_matches!(res, Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_amsterdam_staggered_version_restrictions() {
|
||||
let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
|
||||
|
||||
let res = validate_payload_timestamp(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V3,
|
||||
0,
|
||||
MessageValidationKind::PayloadAttributes,
|
||||
);
|
||||
assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
|
||||
|
||||
let res = validate_payload_timestamp(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V4,
|
||||
0,
|
||||
MessageValidationKind::Payload,
|
||||
);
|
||||
assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
|
||||
|
||||
let res = validate_payload_timestamp(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V5,
|
||||
0,
|
||||
MessageValidationKind::GetPayload,
|
||||
);
|
||||
assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
|
||||
|
||||
let res = validate_payload_timestamp(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V6,
|
||||
0,
|
||||
MessageValidationKind::GetPayload,
|
||||
);
|
||||
assert_matches!(res, Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_amsterdam_slot_and_bal_presence() {
|
||||
let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
|
||||
|
||||
let res = validate_slot_number_presence(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V4,
|
||||
MessageValidationKind::PayloadAttributes,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
assert_matches!(res, Ok(()));
|
||||
|
||||
let res = validate_slot_number_presence(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V5,
|
||||
MessageValidationKind::Payload,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
assert_matches!(res, Ok(()));
|
||||
|
||||
let res = validate_block_access_list_presence(
|
||||
&chain_spec,
|
||||
EngineApiMessageVersion::V5,
|
||||
MessageValidationKind::Payload,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
assert_matches!(res, Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_requests_validation() {
|
||||
assert_matches!(validate_execution_requests(&[]), Ok(()));
|
||||
|
||||
@@ -21,6 +21,7 @@ use alloy_rpc_types_eth::{
|
||||
use alloy_serde::JsonStorageKey;
|
||||
use jsonrpsee::{core::RpcResult, proc_macros::rpc, RpcModule};
|
||||
use reth_engine_primitives::EngineTypes;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Helper trait for the engine api server.
|
||||
///
|
||||
@@ -392,4 +393,19 @@ pub trait EngineEthApi<TxReq: RpcObject, B: RpcObject, R: RpcObject> {
|
||||
keys: Vec<JsonStorageKey>,
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<EIP1186AccountProofResponse>;
|
||||
|
||||
/// Returns the EIP-7928 block access list for a block by hash.
|
||||
#[method(name = "getBlockAccessListByBlockHash")]
|
||||
async fn block_access_list_by_block_hash(&self, hash: B256) -> RpcResult<Option<Value>>;
|
||||
|
||||
/// Returns the EIP-7928 block access list for a block by number.
|
||||
#[method(name = "getBlockAccessListByBlockNumber")]
|
||||
async fn block_access_list_by_block_number(
|
||||
&self,
|
||||
number: BlockNumberOrTag,
|
||||
) -> RpcResult<Option<Value>>;
|
||||
|
||||
/// Returns the EIP-7928 block access list bytes for a block by number.
|
||||
#[method(name = "getBlockAccessListRaw")]
|
||||
async fn block_access_list_raw(&self, block: BlockId) -> RpcResult<Option<Bytes>>;
|
||||
}
|
||||
|
||||
@@ -77,3 +77,4 @@ alloy-rpc-types-engine.workspace = true
|
||||
|
||||
serde_json.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
reqwest.workspace = true
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
error::{RpcError, ServerKind},
|
||||
middleware::RethRpcMiddleware,
|
||||
middleware::{RethAuthHttpMiddleware, RethRpcMiddleware},
|
||||
};
|
||||
use http::header::AUTHORIZATION;
|
||||
use jsonrpsee::{
|
||||
core::{client::SubscriptionClientT, RegisterMethodError},
|
||||
http_client::HeaderMap,
|
||||
server::{AlreadyStoppedError, RpcModule},
|
||||
server::{AlreadyStoppedError, RpcModule, ServerConfig, ServerConfigBuilder},
|
||||
ws_client::RpcServiceBuilder,
|
||||
Methods,
|
||||
};
|
||||
@@ -20,12 +20,11 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use tower::layer::util::Identity;
|
||||
|
||||
pub use jsonrpsee::server::ServerBuilder;
|
||||
use jsonrpsee::server::{ServerConfig, ServerConfigBuilder};
|
||||
pub use reth_ipc::server::Builder as IpcServerBuilder;
|
||||
|
||||
/// Server configuration for the auth server.
|
||||
#[derive(Debug)]
|
||||
pub struct AuthServerConfig<RpcMiddleware = Identity> {
|
||||
pub struct AuthServerConfig<RpcMiddleware = Identity, HttpMiddleware = Identity> {
|
||||
/// Where the server should listen.
|
||||
pub(crate) socket_addr: SocketAddr,
|
||||
/// The secret for the auth layer of the server.
|
||||
@@ -38,6 +37,8 @@ pub struct AuthServerConfig<RpcMiddleware = Identity> {
|
||||
pub(crate) ipc_endpoint: Option<String>,
|
||||
/// Configurable RPC middleware
|
||||
pub(crate) rpc_middleware: RpcMiddleware,
|
||||
/// Configurable HTTP transport middleware, applied after JWT authentication.
|
||||
pub(crate) http_middleware: HttpMiddleware,
|
||||
}
|
||||
|
||||
// === impl AuthServerConfig ===
|
||||
@@ -48,15 +49,23 @@ impl AuthServerConfig {
|
||||
AuthServerConfigBuilder::new(secret)
|
||||
}
|
||||
}
|
||||
impl<RpcMiddleware> AuthServerConfig<RpcMiddleware> {
|
||||
impl<RpcMiddleware, HttpMiddleware> AuthServerConfig<RpcMiddleware, HttpMiddleware> {
|
||||
/// Returns the address the server will listen on.
|
||||
pub const fn address(&self) -> SocketAddr {
|
||||
self.socket_addr
|
||||
}
|
||||
|
||||
/// Configures the rpc middleware.
|
||||
pub fn with_rpc_middleware<T>(self, rpc_middleware: T) -> AuthServerConfig<T> {
|
||||
let Self { socket_addr, secret, server_config, ipc_server_config, ipc_endpoint, .. } = self;
|
||||
pub fn with_rpc_middleware<T>(self, rpc_middleware: T) -> AuthServerConfig<T, HttpMiddleware> {
|
||||
let Self {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
http_middleware,
|
||||
..
|
||||
} = self;
|
||||
AuthServerConfig {
|
||||
socket_addr,
|
||||
secret,
|
||||
@@ -64,13 +73,44 @@ impl<RpcMiddleware> AuthServerConfig<RpcMiddleware> {
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
http_middleware,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the HTTP transport middleware.
|
||||
///
|
||||
/// This middleware is applied after JWT authentication and before JSON-RPC parsing,
|
||||
/// giving access to the raw HTTP request (headers, body, etc.).
|
||||
pub fn with_http_middleware<T>(self, http_middleware: T) -> AuthServerConfig<RpcMiddleware, T> {
|
||||
let Self {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
..
|
||||
} = self;
|
||||
AuthServerConfig {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
http_middleware,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to start a server in one step.
|
||||
///
|
||||
/// The `HttpMiddleware` type parameter configures additional HTTP transport middleware
|
||||
/// that runs after JWT authentication. When set to `Identity` (the default), only JWT
|
||||
/// authentication is applied.
|
||||
pub async fn start(self, module: AuthRpcModule) -> Result<AuthServerHandle, RpcError>
|
||||
where
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
HttpMiddleware: RethAuthHttpMiddleware<RpcMiddleware>,
|
||||
{
|
||||
let Self {
|
||||
socket_addr,
|
||||
@@ -79,11 +119,13 @@ impl<RpcMiddleware> AuthServerConfig<RpcMiddleware> {
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
http_middleware,
|
||||
} = self;
|
||||
|
||||
// Create auth middleware.
|
||||
// Create auth middleware with JWT authentication in front of the user-provided
|
||||
// transport middleware.
|
||||
let middleware =
|
||||
tower::ServiceBuilder::new().layer(AuthLayer::new(JwtAuthValidator::new(secret)));
|
||||
tower::ServiceBuilder::new().layer(AuthHttpLayer::new(secret, http_middleware));
|
||||
|
||||
let rpc_middleware = RpcServiceBuilder::default().layer(rpc_middleware);
|
||||
|
||||
@@ -117,15 +159,47 @@ impl<RpcMiddleware> AuthServerConfig<RpcMiddleware> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A combined tower layer that applies JWT authentication before custom HTTP middleware.
|
||||
///
|
||||
/// This composes `AuthLayer<JwtAuthValidator>` around a user-provided `HttpMiddleware` into a
|
||||
/// single `tower::Layer`. Requests first pass through JWT validation and only authenticated
|
||||
/// requests are forwarded into the custom middleware.
|
||||
struct AuthHttpLayer<HttpMiddleware> {
|
||||
auth_layer: AuthLayer<JwtAuthValidator>,
|
||||
http_middleware: HttpMiddleware,
|
||||
}
|
||||
|
||||
impl<HttpMiddleware> AuthHttpLayer<HttpMiddleware> {
|
||||
const fn new(secret: JwtSecret, http_middleware: HttpMiddleware) -> Self {
|
||||
Self { auth_layer: AuthLayer::new(JwtAuthValidator::new(secret)), http_middleware }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, HttpMiddleware> tower::Layer<S> for AuthHttpLayer<HttpMiddleware>
|
||||
where
|
||||
HttpMiddleware: tower::Layer<S> + Clone,
|
||||
AuthLayer<JwtAuthValidator>: tower::Layer<<HttpMiddleware as tower::Layer<S>>::Service>,
|
||||
{
|
||||
type Service = <AuthLayer<JwtAuthValidator> as tower::Layer<
|
||||
<HttpMiddleware as tower::Layer<S>>::Service,
|
||||
>>::Service;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
let http_service = self.http_middleware.layer(inner);
|
||||
self.auth_layer.layer(http_service)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder type for configuring an `AuthServerConfig`.
|
||||
#[derive(Debug)]
|
||||
pub struct AuthServerConfigBuilder<RpcMiddleware = Identity> {
|
||||
pub struct AuthServerConfigBuilder<RpcMiddleware = Identity, HttpMiddleware = Identity> {
|
||||
socket_addr: Option<SocketAddr>,
|
||||
secret: JwtSecret,
|
||||
server_config: Option<ServerConfigBuilder>,
|
||||
ipc_server_config: Option<IpcServerBuilder<Identity, Identity>>,
|
||||
ipc_endpoint: Option<String>,
|
||||
rpc_middleware: RpcMiddleware,
|
||||
http_middleware: HttpMiddleware,
|
||||
}
|
||||
|
||||
// === impl AuthServerConfigBuilder ===
|
||||
@@ -140,14 +214,26 @@ impl AuthServerConfigBuilder {
|
||||
ipc_server_config: None,
|
||||
ipc_endpoint: None,
|
||||
rpc_middleware: Identity::new(),
|
||||
http_middleware: Identity::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<RpcMiddleware> AuthServerConfigBuilder<RpcMiddleware> {
|
||||
impl<RpcMiddleware, HttpMiddleware> AuthServerConfigBuilder<RpcMiddleware, HttpMiddleware> {
|
||||
/// Configures the rpc middleware.
|
||||
pub fn with_rpc_middleware<T>(self, rpc_middleware: T) -> AuthServerConfigBuilder<T> {
|
||||
let Self { socket_addr, secret, server_config, ipc_server_config, ipc_endpoint, .. } = self;
|
||||
pub fn with_rpc_middleware<T>(
|
||||
self,
|
||||
rpc_middleware: T,
|
||||
) -> AuthServerConfigBuilder<T, HttpMiddleware> {
|
||||
let Self {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
http_middleware,
|
||||
..
|
||||
} = self;
|
||||
AuthServerConfigBuilder {
|
||||
socket_addr,
|
||||
secret,
|
||||
@@ -155,6 +241,35 @@ impl<RpcMiddleware> AuthServerConfigBuilder<RpcMiddleware> {
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
http_middleware,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the HTTP transport middleware.
|
||||
///
|
||||
/// This middleware is applied after JWT authentication and before JSON-RPC parsing,
|
||||
/// giving access to the raw HTTP request (headers, body, etc.).
|
||||
pub fn with_http_middleware<T>(
|
||||
self,
|
||||
http_middleware: T,
|
||||
) -> AuthServerConfigBuilder<RpcMiddleware, T> {
|
||||
let Self {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
..
|
||||
} = self;
|
||||
AuthServerConfigBuilder {
|
||||
socket_addr,
|
||||
secret,
|
||||
server_config,
|
||||
ipc_server_config,
|
||||
ipc_endpoint,
|
||||
rpc_middleware,
|
||||
http_middleware,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +315,7 @@ impl<RpcMiddleware> AuthServerConfigBuilder<RpcMiddleware> {
|
||||
}
|
||||
|
||||
/// Build the `AuthServerConfig`.
|
||||
pub fn build(self) -> AuthServerConfig<RpcMiddleware> {
|
||||
pub fn build(self) -> AuthServerConfig<RpcMiddleware, HttpMiddleware> {
|
||||
AuthServerConfig {
|
||||
socket_addr: self.socket_addr.unwrap_or_else(|| {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), constants::DEFAULT_AUTH_PORT)
|
||||
@@ -233,6 +348,7 @@ impl<RpcMiddleware> AuthServerConfigBuilder<RpcMiddleware> {
|
||||
}),
|
||||
ipc_endpoint: self.ipc_endpoint,
|
||||
rpc_middleware: self.rpc_middleware,
|
||||
http_middleware: self.http_middleware,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,10 +405,14 @@ impl AuthRpcModule {
|
||||
}
|
||||
|
||||
/// Convenience function for starting a server
|
||||
pub async fn start_server(
|
||||
pub async fn start_server<RpcMiddleware, HttpMiddleware>(
|
||||
self,
|
||||
config: AuthServerConfig,
|
||||
) -> Result<AuthServerHandle, RpcError> {
|
||||
config: AuthServerConfig<RpcMiddleware, HttpMiddleware>,
|
||||
) -> Result<AuthServerHandle, RpcError>
|
||||
where
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
HttpMiddleware: RethAuthHttpMiddleware<RpcMiddleware>,
|
||||
{
|
||||
config.start(self).await
|
||||
}
|
||||
}
|
||||
@@ -397,7 +517,7 @@ impl AuthServerHandle {
|
||||
.build(ipc_endpoint)
|
||||
.await
|
||||
.expect("Failed to create ipc client"),
|
||||
)
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use jsonrpsee::server::middleware::rpc::RpcService;
|
||||
use tower::Layer;
|
||||
use jsonrpsee::server::{
|
||||
middleware::rpc::RpcService, HttpRequest, HttpResponse, TowerServiceNoHttp,
|
||||
};
|
||||
use tower::{
|
||||
layer::util::{Identity, Stack},
|
||||
Layer,
|
||||
};
|
||||
|
||||
/// A Helper alias trait for the RPC middleware supported by the server.
|
||||
pub trait RethRpcMiddleware:
|
||||
@@ -35,3 +40,39 @@ impl<T> RethRpcMiddleware for T where
|
||||
+ 'static
|
||||
{
|
||||
}
|
||||
|
||||
/// Inner HTTP transport service type for auth-server middleware.
|
||||
pub type AuthHttpService<RM> = TowerServiceNoHttp<Stack<RM, Identity>>;
|
||||
|
||||
/// Helper alias trait for auth-server HTTP transport middleware layers.
|
||||
pub trait RethAuthHttpMiddleware<RM>:
|
||||
tower::Layer<
|
||||
AuthHttpService<RM>,
|
||||
Service: tower::Service<
|
||||
HttpRequest,
|
||||
Response = HttpResponse,
|
||||
Error = tower::BoxError,
|
||||
Future: Send,
|
||||
> + Send
|
||||
+ Clone,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
{
|
||||
}
|
||||
|
||||
impl<T, RM> RethAuthHttpMiddleware<RM> for T where
|
||||
T: tower::Layer<
|
||||
AuthHttpService<RM>,
|
||||
Service: tower::Service<
|
||||
HttpRequest,
|
||||
Response = HttpResponse,
|
||||
Error = tower::BoxError,
|
||||
Future: Send,
|
||||
> + Send
|
||||
+ Clone,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,16 +1,90 @@
|
||||
//! Auth server tests
|
||||
|
||||
use crate::utils::launch_auth;
|
||||
use crate::utils::{launch_auth, launch_auth_with_config, test_address};
|
||||
use alloy_primitives::U64;
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionPayloadInputV2, ExecutionPayloadV1, ForkchoiceState, PayloadId,
|
||||
};
|
||||
use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
|
||||
use http::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||
use jsonrpsee::{
|
||||
core::client::{ClientT, SubscriptionClientT},
|
||||
server::{HttpRequest, HttpResponse},
|
||||
};
|
||||
use reth_ethereum_engine_primitives::EthEngineTypes;
|
||||
use reth_ethereum_primitives::{Block, TransactionSigned};
|
||||
use reth_primitives_traits::block::Block as _;
|
||||
use reth_rpc_api::clients::EngineApiClient;
|
||||
use reth_rpc_layer::JwtSecret;
|
||||
use reth_rpc_builder::auth::AuthServerConfig;
|
||||
use reth_rpc_layer::{secret_to_bearer_header, JwtSecret};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct CountingAuthHttpLayer {
|
||||
count: Arc<AtomicUsize>,
|
||||
content_types: Arc<Mutex<Vec<Option<String>>>>,
|
||||
}
|
||||
|
||||
impl CountingAuthHttpLayer {
|
||||
fn count(&self) -> usize {
|
||||
self.count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn seen_content_types(&self) -> Vec<Option<String>> {
|
||||
self.content_types.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for CountingAuthHttpLayer {
|
||||
type Service = CountingAuthHttpService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
CountingAuthHttpService {
|
||||
inner,
|
||||
count: self.count.clone(),
|
||||
content_types: self.content_types.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CountingAuthHttpService<S> {
|
||||
inner: S,
|
||||
count: Arc<AtomicUsize>,
|
||||
content_types: Arc<Mutex<Vec<Option<String>>>>,
|
||||
}
|
||||
|
||||
impl<S> Service<HttpRequest> for CountingAuthHttpService<S>
|
||||
where
|
||||
S: Service<HttpRequest, Response = HttpResponse, Error = tower::BoxError> + Send + Clone,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
type Response = HttpResponse;
|
||||
type Error = tower::BoxError;
|
||||
type Future = S::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: HttpRequest) -> Self::Future {
|
||||
self.count.fetch_add(1, Ordering::Relaxed);
|
||||
self.content_types.lock().unwrap().push(
|
||||
request
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_owned),
|
||||
);
|
||||
self.inner.call(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(unused_must_use)]
|
||||
async fn test_basic_engine_calls<C>(client: &C)
|
||||
@@ -58,3 +132,55 @@ async fn test_auth_endpoints_ws() {
|
||||
let client = handle.ws_client().await;
|
||||
test_basic_engine_calls(&client).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_auth_http_middleware_runs_only_after_jwt() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let secret = JwtSecret::random();
|
||||
let layer = CountingAuthHttpLayer::default();
|
||||
let config = AuthServerConfig::builder(secret).socket_addr(test_address()).build();
|
||||
let handle = launch_auth_with_config(config.with_http_middleware(layer.clone())).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(handle.http_url())
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "engine_exchangeCapabilities",
|
||||
"params": [[]],
|
||||
"id": 1
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(layer.count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_auth_http_middleware_sees_transport_headers_before_json_rpc_parsing() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let secret = JwtSecret::random();
|
||||
let layer = CountingAuthHttpLayer::default();
|
||||
let config = AuthServerConfig::builder(secret).socket_addr(test_address()).build();
|
||||
let handle = launch_auth_with_config(config.with_http_middleware(layer.clone())).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(handle.http_url())
|
||||
.header(AUTHORIZATION, secret_to_bearer_header(&secret))
|
||||
.header(CONTENT_TYPE, "application/ssz")
|
||||
.body("not-json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(response.status().is_success() || response.status().is_client_error());
|
||||
assert_eq!(layer.count(), 1);
|
||||
assert_eq!(layer.seen_content_types(), vec![Some("application/ssz".to_owned())]);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use reth_payload_builder::test_utils::spawn_test_payload_service;
|
||||
use reth_provider::test_utils::NoopProvider;
|
||||
use reth_rpc_builder::{
|
||||
auth::{AuthRpcModule, AuthServerConfig, AuthServerHandle},
|
||||
middleware::{RethAuthHttpMiddleware, RethRpcMiddleware},
|
||||
RpcModuleBuilder, RpcServerConfig, RpcServerHandle, TransportRpcModuleConfig,
|
||||
};
|
||||
use reth_rpc_engine_api::{capabilities::EngineCapabilities, EngineApi};
|
||||
@@ -28,12 +29,23 @@ use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
/// Localhost with port 0 so a free port is used.
|
||||
pub const fn test_address() -> SocketAddr {
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
|
||||
}
|
||||
|
||||
/// Launches a new server for the auth module
|
||||
pub async fn launch_auth(secret: JwtSecret) -> AuthServerHandle {
|
||||
let config = AuthServerConfig::builder(secret).socket_addr(test_address()).build();
|
||||
launch_auth_with_config(config).await
|
||||
}
|
||||
|
||||
/// Launches a new server for the auth module with the given config.
|
||||
pub async fn launch_auth_with_config<RpcMiddleware, HttpMiddleware>(
|
||||
config: AuthServerConfig<RpcMiddleware, HttpMiddleware>,
|
||||
) -> AuthServerHandle
|
||||
where
|
||||
RpcMiddleware: RethRpcMiddleware,
|
||||
HttpMiddleware: RethAuthHttpMiddleware<RpcMiddleware>,
|
||||
{
|
||||
let (tx, _rx) = unbounded_channel();
|
||||
let beacon_engine_handle = ConsensusEngineHandle::<EthEngineTypes>::new(tx);
|
||||
let client = ClientVersionV1 {
|
||||
|
||||
@@ -479,7 +479,6 @@ impl<Network, Evm, Receipt, Header, Map, SimTx, RpcTx, TxEnv>
|
||||
evm,
|
||||
sim_tx_converter,
|
||||
rpc_tx_converter,
|
||||
tx_env_converter: _,
|
||||
..
|
||||
} = self;
|
||||
RpcConverter {
|
||||
|
||||
@@ -269,7 +269,7 @@ where
|
||||
>::from_execution_payload(&payload);
|
||||
self.inner
|
||||
.validator
|
||||
.validate_version_specific_fields(EngineApiMessageVersion::V6, payload_or_attrs)?;
|
||||
.validate_version_specific_fields(EngineApiMessageVersion::V5, payload_or_attrs)?;
|
||||
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ where
|
||||
state: ForkchoiceState,
|
||||
payload_attrs: Option<EngineT::PayloadAttributes>,
|
||||
) -> EngineApiResult<ForkchoiceUpdated> {
|
||||
self.validate_and_execute_forkchoice(EngineApiMessageVersion::V6, state, payload_attrs)
|
||||
self.validate_and_execute_forkchoice(EngineApiMessageVersion::V4, state, payload_attrs)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1438,9 +1438,10 @@ struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSp
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{Address, B256};
|
||||
use alloy_eips::eip7685::Requests;
|
||||
use alloy_primitives::{Address, Bytes, B256};
|
||||
use alloy_rpc_types_engine::{
|
||||
ClientCode, ClientVersionV1, PayloadAttributes, PayloadStatusEnum,
|
||||
ClientCode, ClientVersionV1, ExecutionPayloadV2, PayloadAttributes, PayloadStatusEnum,
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
|
||||
@@ -1532,6 +1533,63 @@ mod tests {
|
||||
assert_matches!(handle.from_api.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_payload_v5_accepts_amsterdam_payloads() {
|
||||
let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
|
||||
let provider = Arc::new(MockEthProvider::default());
|
||||
let payload_store = spawn_test_payload_service::<EthEngineTypes>();
|
||||
let (to_engine, mut engine_rx) = unbounded_channel();
|
||||
|
||||
let api = EngineApi::new(
|
||||
provider,
|
||||
chain_spec.clone(),
|
||||
ConsensusEngineHandle::new(to_engine),
|
||||
payload_store.into(),
|
||||
NoopTransactionPool::default(),
|
||||
Runtime::test(),
|
||||
ClientVersionV1 {
|
||||
code: ClientCode::RH,
|
||||
name: "Reth".to_string(),
|
||||
version: "v0.0.0-test".to_string(),
|
||||
commit: "test".to_string(),
|
||||
},
|
||||
EngineCapabilities::default(),
|
||||
EthereumEngineValidator::new(chain_spec),
|
||||
false,
|
||||
NoopNetwork::default(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
|
||||
let payload = ExecutionPayloadV4 {
|
||||
payload_inner: ExecutionPayloadV3 {
|
||||
payload_inner: ExecutionPayloadV2 {
|
||||
payload_inner: payload_v1,
|
||||
withdrawals: Vec::new(),
|
||||
},
|
||||
blob_gas_used: 0,
|
||||
excess_blob_gas: 0,
|
||||
},
|
||||
block_access_list: Bytes::from_static(b"bal"),
|
||||
slot_number: 1,
|
||||
};
|
||||
let execution_data = ExecutionData {
|
||||
payload: payload.into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
parent_beacon_block_root: B256::ZERO,
|
||||
},
|
||||
PraguePayloadFields { requests: RequestsOrHash::Requests(Requests::default()) },
|
||||
),
|
||||
};
|
||||
|
||||
api.new_payload_v5(execution_data).await.unwrap();
|
||||
});
|
||||
|
||||
assert_matches!(engine_rx.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestNetworkInfo {
|
||||
syncing: bool,
|
||||
|
||||
@@ -121,12 +121,12 @@ impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
|
||||
VersionSpecificValidationError::WithdrawalsNotSupportedInV1 |
|
||||
VersionSpecificValidationError::NoWithdrawalsPostShanghai |
|
||||
VersionSpecificValidationError::HasWithdrawalsPreShanghai |
|
||||
VersionSpecificValidationError::BlockAccessListNotSupportedBeforeV6 |
|
||||
VersionSpecificValidationError::BlockAccessListNotSupported |
|
||||
VersionSpecificValidationError::HasBlockAccessListPreAmsterdam |
|
||||
VersionSpecificValidationError::NoBlockAccessListPostAmsterdam |
|
||||
VersionSpecificValidationError::HasSlotNumberPreAmsterdam |
|
||||
VersionSpecificValidationError::NoSlotNumberPostAmsterdam |
|
||||
VersionSpecificValidationError::SlotNumberNotSupportedBeforeV6,
|
||||
VersionSpecificValidationError::SlotNumberNotSupported,
|
||||
),
|
||||
) |
|
||||
EngineApiError::UnexpectedRequestsHash => {
|
||||
|
||||
@@ -419,7 +419,7 @@ pub trait EthApi<
|
||||
|
||||
/// Returns the EIP-7928 block access list bytes for a block by number.
|
||||
#[method(name = "getBlockAccessListRaw")]
|
||||
async fn block_access_list_raw(&self, number: BlockNumberOrTag) -> RpcResult<Option<Bytes>>;
|
||||
async fn block_access_list_raw(&self, block: BlockId) -> RpcResult<Option<Bytes>>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -942,10 +942,10 @@ where
|
||||
Ok(Some(json))
|
||||
}
|
||||
/// Handler for: `eth_getBlockAccessListRaw`
|
||||
async fn block_access_list_raw(&self, number: BlockNumberOrTag) -> RpcResult<Option<Bytes>> {
|
||||
trace!(target: "rpc::eth", ?number, "Serving eth_getBlockAccessListRaw");
|
||||
async fn block_access_list_raw(&self, block: BlockId) -> RpcResult<Option<Bytes>> {
|
||||
trace!(target: "rpc::eth", ?block, "Serving eth_getBlockAccessListRaw");
|
||||
|
||||
let bal = self.get_block_access_list(number.into()).await?;
|
||||
let bal = self.get_block_access_list(block).await?;
|
||||
Ok(bal.map(|b: BlockAccessList| alloy_rlp::encode(b).into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use reth_rpc_eth_types::{
|
||||
use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO};
|
||||
use revm::{
|
||||
context::Block,
|
||||
context_interface::{result::ExecutionResult, Transaction},
|
||||
context_interface::{result::ExecutionResult, Cfg, Transaction},
|
||||
primitives::KECCAK_EMPTY,
|
||||
};
|
||||
use tracing::trace;
|
||||
@@ -74,10 +74,15 @@ pub trait EstimateCall: Call {
|
||||
let tx_request_gas_limit = request.as_ref().gas_limit();
|
||||
let tx_request_gas_price = request.as_ref().gas_price();
|
||||
// the gas limit of the corresponding block
|
||||
let max_gas_limit = evm_env.cfg_env.tx_gas_limit_cap.map_or_else(
|
||||
|| evm_env.block_env.gas_limit(),
|
||||
|cap| cap.min(evm_env.block_env.gas_limit()),
|
||||
);
|
||||
let max_gas_limit = evm_env
|
||||
.cfg_env
|
||||
.tx_gas_limit_cap
|
||||
// If EIP-8037 is enabled, the transaction gas limit cap is not applicable
|
||||
.filter(|_| !evm_env.cfg_env.is_amsterdam_eip8037_enabled())
|
||||
.map_or_else(
|
||||
|| evm_env.block_env.gas_limit(),
|
||||
|cap| cap.min(evm_env.block_env.gas_limit()),
|
||||
);
|
||||
|
||||
// Determine the highest possible gas limit, considering both the request's specified limit
|
||||
// and the block's limit.
|
||||
|
||||
@@ -377,7 +377,7 @@ pub trait LoadPendingBlock:
|
||||
}
|
||||
}
|
||||
|
||||
let BlockBuilderOutcome { execution_result, block, hashed_state, trie_updates } =
|
||||
let BlockBuilderOutcome { execution_result, block, hashed_state, trie_updates, .. } =
|
||||
builder.finish(NoopProvider::default(), None).map_err(Self::Error::from_eth_err)?;
|
||||
|
||||
let execution_outcome =
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub use reth_rpc_engine_api::EngineApi;
|
||||
use reth_rpc_eth_api::{
|
||||
EngineEthFilter, FullEthApiTypes, QueryLimits, RpcBlock, RpcHeader, RpcReceipt, RpcTransaction,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tracing_futures::Instrument;
|
||||
|
||||
macro_rules! engine_span {
|
||||
@@ -145,4 +146,22 @@ where
|
||||
) -> Result<EIP1186AccountProofResponse> {
|
||||
self.eth.get_proof(address, keys, block_number).instrument(engine_span!()).await
|
||||
}
|
||||
|
||||
/// Handler for `eth_getBlockAccessListByBlockHash`
|
||||
async fn block_access_list_by_block_hash(&self, hash: B256) -> Result<Option<Value>> {
|
||||
self.eth.block_access_list_by_block_hash(hash).instrument(engine_span!()).await
|
||||
}
|
||||
|
||||
/// Handler for `eth_getBlockAccessListByBlockNumber`
|
||||
async fn block_access_list_by_block_number(
|
||||
&self,
|
||||
block_number: BlockNumberOrTag,
|
||||
) -> Result<Option<Value>> {
|
||||
self.eth.block_access_list_by_block_number(block_number).instrument(engine_span!()).await
|
||||
}
|
||||
|
||||
/// Handler for `getBlockAccessListRaw`
|
||||
async fn block_access_list_raw(&self, block: BlockId) -> Result<Option<Bytes>> {
|
||||
self.eth.block_access_list_raw(block).instrument(engine_span!()).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -48,8 +48,8 @@ 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, 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, 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::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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user