Compare commits

..

2 Commits

Author SHA1 Message Date
rakita
d3771d341a add reth-core t4 patch (6b12498) 2026-04-16 10:58:25 +02:00
rakita
9936061e5f bump revm, revm-inspectors, and alloy-evm to t4 branches
revm: 89ecb25dbe49e1c3a10d99529e42f027d0bd2386
revm-inspectors: c6f88bbe7186d863f4667dd43c42608eb7a8ba5c
alloy-evm: ff0bbec9ccaa818155e25003a77f4d73d350bbd7
2026-04-16 10:26:28 +02:00
273 changed files with 15548 additions and 24949 deletions

View File

@@ -4,14 +4,10 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
labels:
- "A-dependencies"
commit-message:

View File

@@ -1,21 +1,24 @@
#!/usr/bin/env bash
#
# Builds reth binaries for benchmarking from local source only.
# Builds (or fetches from cache) reth binaries for benchmarking.
#
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit>
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit> [branch-sha]
#
# baseline — build the baseline binary at <commit> (merge-base)
# baseline — build/fetch the baseline binary at <commit> (merge-base)
# source-dir must be checked out at <commit>
# feature — build the candidate binary + reth-bench at <commit>
# feature — build/fetch 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"
@@ -39,38 +42,103 @@ if [ "${BENCH_TRACY:-off}" != "off" ]; then
EXTRA_RUSTFLAGS=" -C force-frame-pointers=yes"
fi
# Build the requested node binary with the benchmark profile.
build_node_binary() {
local features_arg=""
local workspace_arg=""
# 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
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"
# 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
fi
# shellcheck disable=SC2086
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --locked --profile profiling $NODE_PKG $workspace_arg $features_arg
if [ "$binary_sha" = "$expected_commit" ]; then
return 0
fi
echo "Cache mismatch: binary built from ${binary_sha} but expected ${expected_commit}"
return 1
}
case "$MODE" in
baseline|main)
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
build_node_binary
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
;;
feature|branch)
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
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 "Usage: $0 <baseline|feature> <source-dir> <commit>"
echo "Usage: $0 <baseline|feature> <source-dir> <commit> [branch-sha]"
exit 1
;;
esac

View File

@@ -2,7 +2,7 @@
#
# local-reth-bench.sh — Run the reth Engine API benchmark locally.
#
# Replicates the CI bench.yml workflow (build, local snapshot validation, system tuning,
# Replicates the CI bench.yml workflow (build, snapshot, system tuning,
# interleaved B-F-F-B execution, summary, charts) without any GitHub
# Actions glue (no PR comments, no artifact upload, no Slack).
#
@@ -21,17 +21,15 @@
# Requires: the reth repo at RETH_REPO (default: ~/reth)
#
# Dependencies (install before first run):
# schelk, cpupower, taskset, stdbuf, python3, curl,
# make, uv, jq, Rust toolchain (cargo/rustup)
# Optional:
# mc for Tracy profile upload
# mc (MinIO client), schelk, cpupower, taskset, stdbuf, python3, curl,
# make, uv, pzstd, jq, Rust toolchain (cargo/rustup)
#
# 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 (uv) are visible
# Ensure cargo and user-local bins (mc, uv) are visible
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
# ── Defaults ──────────────────────────────────────────────────────────
@@ -108,7 +106,7 @@ fi
# ── Check dependencies ───────────────────────────────────────────────
missing=()
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq cargo; do
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq cargo; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
@@ -240,14 +238,19 @@ echo " Baseline src : $BASELINE_SRC"
echo " Feature src : $FEATURE_SRC"
echo
# ── Step 3: Validate local snapshot ──────────────────────────────────
echo "▸ Validating local snapshot..."
# ── Step 3: Check / download snapshot ────────────────────────────────
echo "▸ Checking snapshot..."
cd "$RETH_REPO"
"${SCRIPTS_DIR}/bench-reth-snapshot.sh"
echo " Snapshot is ready."
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
echo
# ── Step 4: Build binaries in parallel ───────────────────────────────
# ── Step 4: Build binaries (+ snapshot download) in parallel ─────────
echo "▸ Building binaries (parallel)..."
cd "$RETH_REPO"
@@ -259,11 +262,19 @@ 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 build tasks failed"
echo "Error: one or more parallel tasks failed (builds / snapshot)"
exit 1
fi
echo " Binaries built successfully."

View File

@@ -88,16 +88,10 @@ 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 || sudo schelk full-recover -y || true
sudo schelk recover -y --kill || true
# Mount
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
sudo schelk mount -y
sync
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
echo "=== Cache state after drop ==="
@@ -323,18 +317,13 @@ if [ "$BIG_BLOCKS" = "true" ]; then
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
else
# Standard mode: warmup + new-payload-fcu
WARMUP="${BENCH_WARMUP_BLOCKS:-50}"
if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
# Warm up the node before measuring the benchmark window.
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
--rpc-url "$BENCH_RPC_URL" \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret "$DATADIR/jwt.hex" \
--advance "$WARMUP" \
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
else
echo "Skipping warmup (0 blocks)..."
fi
# Warmup
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
--rpc-url "$BENCH_RPC_URL" \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret "$DATADIR/jwt.hex" \
--advance "${BENCH_WARMUP_BLOCKS:-50}" \
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
# Start tracy-capture after warmup so profile only covers the benchmark
if [ "${BENCH_TRACY:-off}" != "off" ]; then

View File

@@ -1,56 +1,129 @@
#!/usr/bin/env bash
#
# Validates that the benchmark snapshot has already been populated into the
# local schelk volume.
# 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).
#
# Usage: bench-reth-snapshot.sh [--check]
# --check Exit 0 if the local snapshot is ready, 10 if it is missing.
# --check Only check if a download is needed; exits 0 if up-to-date, 10 if not.
#
# Required env:
# 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
# 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
set -euxo pipefail
: "${SCHELK_MOUNT:?SCHELK_MOUNT must be set}"
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"
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}"
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
# 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
}
REMOTE_HASH=$(echo "$MANIFEST_CONTENT" | sha256sum | awk '{print $1}')
snapshot_ready() {
[ -d "$DATADIR/db" ] && [ -d "$DATADIR/static_files" ]
}
LOCAL_HASH=""
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
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}"
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
echo "Snapshot is up-to-date (manifest hash: ${REMOTE_HASH:0:16}…)"
exit 0
fi
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
echo "Snapshot needs update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
if [ "${1:-}" = "--check" ]; then
exit 10
fi
exit 1
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}…)"

View File

@@ -1,244 +0,0 @@
#!/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()

View File

@@ -1,23 +1,6 @@
#!/usr/bin/env bash
set -eo pipefail
fixture_variant="${1:-osaka}"
case "${fixture_variant}" in
amsterdam)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/bal@v6.0.0/fixtures_bal.tar.gz"
eels_branch="devnets/snøbal/4"
;;
osaka)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz"
eels_branch="forks/osaka"
;;
*)
echo "unknown hive fixture variant: ${fixture_variant}"
exit 1
;;
esac
# Create the hive_assets directory
mkdir hive_assets/
@@ -29,12 +12,12 @@ go build .
# Run each hive command in the background for each simulator and wait
echo "Building images"
./hive -client reth --sim "ethereum/eels/consume-engine" \
--sim.buildarg fixtures="${eels_fixtures}" \
--sim.buildarg branch="${eels_branch}" \
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
--sim.buildarg branch=forks/osaka \
--sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/eels/consume-rlp" \
--sim.buildarg fixtures="${eels_fixtures}" \
--sim.buildarg branch="${eels_branch}" \
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
--sim.buildarg branch=forks/osaka \
--sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &

View File

@@ -13,15 +13,15 @@ rpc-compat:
# syncing mode, the test expects syncing to be false on start
- eth_syncing/check-syncing (reth)
engine-withdrawals: []
engine-withdrawals: [ ]
engine-api: []
engine-api: [ ]
engine-cancun: []
engine-cancun: [ ]
sync: []
sync: [ ]
engine-auth: []
engine-auth: [ ]
# EIP-7610 related tests (Revert creation in case of non-empty storage):
#
@@ -99,40 +99,6 @@ eels/consume-engine:
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Prague-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Shanghai-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Paris-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Cancun-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Osaka-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-empty-initcode]-reth
# Blob limit tests:
#
@@ -227,37 +193,3 @@ eels/consume-rlp:
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Prague-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Shanghai-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Paris-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Cancun-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Osaka-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-initcode-with-deploy]-reth

View File

@@ -5,12 +5,6 @@ cd hivetests/
sim="${1}"
limit="${2}"
fixture_variant="${3:-}"
if [[ "${fixture_variant}" == "osaka" && "${sim}" == *"eels"* && "${limit}" == *"tests/amsterdam"* ]]; then
echo "osaka fixtures do not support amsterdam tests"
exit 1
fi
# Use lower parallelism for eels tests to avoid OOM-killing the runner
parallelism=16

View File

@@ -54,7 +54,9 @@ env:
name: bench-scheduled
permissions: {}
permissions:
contents: read
actions: read
jobs:
# ---------------------------------------------------------------------------
@@ -63,9 +65,6 @@ jobs:
resolve-refs:
name: resolve-refs
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
outputs:
mode: ${{ steps.mode.outputs.mode }}
baseline-ref: ${{ steps.refs.outputs.baseline-ref }}
@@ -77,26 +76,21 @@ jobs:
long-running: ${{ steps.refs.outputs.long-running }}
release-tag: ${{ steps.refs.outputs.release-tag }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
fetch-depth: 2
- name: Detect mode
id: mode
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_MODE: ${{ inputs.mode }}
SCHEDULE: ${{ github.event.schedule }}
run: |
# Maps cron schedules to modes (must match the schedule entries above)
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
MODE="${INPUT_MODE:-nightly}"
elif [ "$SCHEDULE" = "30 5 * * *" ]; then
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
MODE="${{ inputs.mode || 'nightly' }}"
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
MODE="nightly"
elif [ "$SCHEDULE" = "0 9 * * *" ]; then
elif [ "${{ github.event.schedule }}" = "0 9 * * *" ]; then
MODE="release"
else
MODE="hourly"
@@ -111,15 +105,14 @@ jobs:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
INPUT_FORCE: ${{ inputs.force || 'false' }}
run: |
FORCE="${INPUT_FORCE:-false}"
FORCE="${{ inputs.force || 'false' }}"
MODE="${{ steps.mode.outputs.mode }}"
.github/scripts/bench-scheduled-refs.sh "$FORCE" "$MODE"
- 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -161,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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -249,9 +242,6 @@ jobs:
needs.resolve-refs.outputs.is-stale != 'true'
name: bench-scheduled
runs-on: [self-hosted, Linux, X64, available]
permissions:
contents: read
actions: read
timeout-minutes: 120
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
@@ -280,16 +270,15 @@ jobs:
- name: Clean up previous bench-work
run: sudo rm -rf "$BENCH_WORK_DIR" 2>/dev/null || true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
fetch-depth: 0
ref: ${{ needs.resolve-refs.outputs.feature-ref }}
- name: Resolve job URL
id: job-url
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
@@ -302,9 +291,8 @@ jobs:
core.exportVariable('BENCH_JOB_URL', jobUrl);
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Install dependencies
env:
@@ -319,6 +307,12 @@ 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
@@ -346,7 +340,7 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
missing=()
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq; do
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
@@ -372,30 +366,35 @@ jobs:
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
- name: Validate local snapshot
- name: Check if snapshot needs update
id: snapshot-check
run: .github/scripts/bench-reth-snapshot.sh
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
- name: Prepare source dirs
run: |
prepare_source_dir() {
local dir="$1"
local ref="$2"
if [ -d ../reth-baseline ]; then
git -C ../reth-baseline fetch origin "$BASELINE_REF"
else
git clone . ../reth-baseline
fi
git -C ../reth-baseline checkout "$BASELINE_REF"
if [ -d "$dir" ]; then
git -C "$dir" reset --hard HEAD
git -C "$dir" clean -fdx
git -C "$dir" fetch origin "$ref"
else
git clone . "$dir"
fi
git -C "$dir" checkout --force "$ref"
}
prepare_source_dir ../reth-baseline "$BASELINE_REF"
prepare_source_dir ../reth-feature "$FEATURE_REF"
if [ -d ../reth-feature ]; then
git -C ../reth-feature fetch origin "$FEATURE_REF"
else
git clone . ../reth-feature
fi
git -C ../reth-feature checkout "$FEATURE_REF"
- name: Build binaries
id: build
@@ -419,6 +418,15 @@ 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: |
@@ -640,7 +648,7 @@ jobs:
- name: Upload results
if: "!cancelled()"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: bench-scheduled-results
path: ${{ env.BENCH_WORK_DIR }}
@@ -648,12 +656,10 @@ jobs:
- name: Push charts
id: push-charts
if: success() && env.BENCH_MODE != 'hourly'
env:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
RUN_ID: ${{ github.run_id }}
run: |
RUN_ID=${{ github.run_id }}
CHART_DIR="${BENCH_MODE}/${RUN_ID}"
CHARTS_REPO="https://x-access-token:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
CHARTS_REPO="https://x-access-token:${{ secrets.DEREK_TOKEN }}@github.com/decofe/reth-bench-charts.git"
TMP_DIR=$(mktemp -d)
if git clone --depth 1 "${CHARTS_REPO}" "${TMP_DIR}" 2>/dev/null; then
@@ -674,7 +680,7 @@ jobs:
- name: Write job summary
if: success()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
@@ -753,7 +759,7 @@ jobs:
- name: Send Slack notification (success)
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -908,7 +914,7 @@ jobs:
- name: Send Slack notification (failure)
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -919,8 +925,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 }}'],
@@ -942,7 +948,7 @@ jobs:
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>\n<@U0AAA8F0JEM> investigate this` },
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>\n@ai investigate this` },
},
{
type: 'actions',
@@ -969,11 +975,6 @@ 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: |

View File

@@ -14,7 +14,7 @@ on:
blocks:
description: "Number of blocks to benchmark"
required: false
default: "500"
default: "200"
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: "200"
default: "100"
type: string
baseline:
description: "Baseline git ref (default: merge-base)"
@@ -82,7 +82,7 @@ on:
- on-error
- never
abba:
description: "Run ABBA (FBBF) interleaved order; false = single FB pass"
description: "Run ABBA (BFFB) interleaved order; false = single AB pass"
required: false
default: "true"
type: boolean
@@ -99,7 +99,9 @@ env:
name: bench
permissions: {}
permissions:
contents: read
pull-requests: write
jobs:
reth-bench-ack:
@@ -108,9 +110,6 @@ jobs:
github.event_name == 'workflow_dispatch'
name: reth-bench-ack
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
outputs:
pr: ${{ steps.args.outputs.pr }}
actor: ${{ steps.args.outputs.actor }}
@@ -134,7 +133,7 @@ jobs:
steps:
- name: Check org membership
if: github.event_name == 'issue_comment'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -153,7 +152,7 @@ jobs:
- name: Parse arguments
id: args
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -165,9 +164,9 @@ jobs:
if (context.eventName === 'workflow_dispatch') {
actor = '${{ github.actor }}';
blocks = '${{ github.event.inputs.blocks }}' || '500';
warmup = '${{ github.event.inputs.warmup }}' || '200';
if (warmup !== '200') explicitWarmup = true;
blocks = '${{ github.event.inputs.blocks }}' || '200';
warmup = '${{ github.event.inputs.warmup }}' || '100';
if (warmup !== '100') explicitWarmup = true;
baseline = '${{ github.event.inputs.baseline }}';
feature = '${{ github.event.inputs.feature }}';
samply = '${{ github.event.inputs.samply }}' === 'true' ? 'true' : 'false';
@@ -206,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: '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 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 unknown = [];
const invalid = [];
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
@@ -360,7 +359,7 @@ jobs:
- name: Acknowledge request
id: ack
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -446,7 +445,7 @@ jobs:
- name: Poll queue position
if: steps.ack.outputs.comment-id && steps.ack.outputs.queue-position != '0'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -530,9 +529,6 @@ jobs:
needs: reth-bench-ack
name: reth-bench
runs-on: [self-hosted, Linux, X64, available]
permissions:
contents: read
pull-requests: write
timeout-minutes: 120
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
@@ -564,7 +560,7 @@ jobs:
- name: Resolve checkout ref
id: checkout-ref
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
if (!process.env.BENCH_PR) {
@@ -582,16 +578,15 @@ jobs:
core.info(`PR #${process.env.BENCH_PR} (${pr.state}), using head SHA ${pr.head.sha}`);
core.setOutput('ref', pr.head.sha);
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.ref }}
- name: Resolve job URL and update status
if: env.BENCH_COMMENT_ID
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -639,9 +634,8 @@ jobs:
});
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Install dependencies
env:
@@ -656,6 +650,12 @@ 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 +690,7 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
missing=()
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq; do
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
@@ -702,7 +702,7 @@ jobs:
# Build binaries
- name: Resolve PR head branch
id: pr-info
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
if (process.env.BENCH_PR) {
@@ -720,7 +720,7 @@ jobs:
- name: Resolve refs
id: refs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const { execSync } = require('child_process');
@@ -766,68 +766,79 @@ jobs:
core.setOutput('feature-ref', featureRef);
core.setOutput('feature-name', featureName);
- name: Validate local big blocks
- name: Check big-blocks freshness
if: env.BENCH_BIG_BLOCKS == 'true'
id: big-blocks-check
run: |
set -euo pipefail
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"
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"
exit 1
fi
BASE_SNAPSHOT=$(jq -r '.base_snapshot // empty' "$MANIFEST")
}
BASE_SNAPSHOT=$(echo "$BB_MANIFEST" | jq -r '.base_snapshot // empty')
if [ -z "$BASE_SNAPSHOT" ]; then
echo "::error::Big-blocks manifest missing base_snapshot field"
exit 1
fi
if [ ! -d "$PAYLOAD_DIR" ]; then
echo "::error::Missing local big-block payload directory at $PAYLOAD_DIR"
exit 1
fi
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
echo "Big-blocks base snapshot: $BASE_SNAPSHOT"
echo "Payload files: $PAYLOAD_COUNT"
echo "BENCH_SNAPSHOT_NAME=${BASE_SNAPSHOT}" >> "$GITHUB_ENV"
- name: Validate local snapshot
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"
fi
- name: Check if snapshot needs update
id: snapshot-check
run: .github/scripts/bench-reth-snapshot.sh
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
- 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)...'});
- name: Prepare source dirs
run: |
prepare_source_dir() {
local dir="$1"
local ref="$2"
if [ -d "$dir" ]; then
git -C "$dir" reset --hard HEAD
git -C "$dir" clean -fdx
git -C "$dir" fetch origin "$ref"
else
git clone . "$dir"
fi
git -C "$dir" checkout --force "$ref"
}
BASELINE_REF="${{ steps.refs.outputs.baseline-ref }}"
prepare_source_dir ../reth-baseline "$BASELINE_REF"
if [ -d ../reth-baseline ]; then
git -C ../reth-baseline fetch origin "$BASELINE_REF"
else
git clone . ../reth-baseline
fi
git -C ../reth-baseline checkout "$BASELINE_REF"
FEATURE_REF="${{ steps.refs.outputs.feature-ref }}"
prepare_source_dir ../reth-feature "$FEATURE_REF"
if [ -d ../reth-feature ]; then
git -C ../reth-feature fetch origin "$FEATURE_REF"
else
git clone . ../reth-feature
fi
git -C ../reth-feature checkout "$FEATURE_REF"
- name: Build binaries
id: build
@@ -851,6 +862,15 @@ 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: |
@@ -890,11 +910,8 @@ 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 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"
sudo sh -c 'exec 3<>/dev/cpu_dma_latency; echo -ne "\x00\x00\x00\x00" >&3; sleep infinity' &
# 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
@@ -921,6 +938,45 @@ 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 }}"
@@ -943,28 +999,15 @@ jobs:
- name: Update status (running benchmarks)
if: success() && env.BENCH_COMMENT_ID
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
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: 'Running benchmarks...'});
# Interleaved run order (F-B-B-F) to reduce systematic bias from
# Interleaved run order (B-F-F-B) to reduce systematic bias from
# thermal drift and cache warming.
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
env:
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-1,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"feature-1","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: baseline (1/2)"
id: run-baseline-1
env:
@@ -978,19 +1021,18 @@ jobs:
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: baseline (2/2)"
if: env.BENCH_ABBA != 'false'
id: run-baseline-2
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
env:
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-2,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-1,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
{"benchmark_run":"feature-1","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: feature (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -1006,6 +1048,20 @@ jobs:
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
- name: "Run benchmark: baseline (2/2)"
if: env.BENCH_ABBA != 'false'
id: run-baseline-2
env:
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-2,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
- name: Stop metrics proxy & generate Grafana URL
id: metrics
if: "!cancelled()"
@@ -1172,7 +1228,7 @@ jobs:
- name: Upload results
if: "!cancelled()"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: bench-reth-results
path: ${{ env.BENCH_WORK_DIR }}
@@ -1180,13 +1236,11 @@ jobs:
- name: Push charts
id: push-charts
if: success()
env:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
RUN_ID: ${{ github.run_id }}
run: |
PR_NUMBER="${BENCH_PR:-0}"
RUN_ID=${{ github.run_id }}
CHART_DIR="pr/${PR_NUMBER}/${RUN_ID}"
CHARTS_REPO="https://x-access-token:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
CHARTS_REPO="https://x-access-token:${{ secrets.DEREK_TOKEN }}@github.com/decofe/reth-bench-charts.git"
TMP_DIR=$(mktemp -d)
if git clone --depth 1 "${CHARTS_REPO}" "${TMP_DIR}" 2>/dev/null; then
@@ -1199,35 +1253,15 @@ jobs:
mkdir -p "${TMP_DIR}/${CHART_DIR}"
cp "$BENCH_WORK_DIR"/charts/*.png "${TMP_DIR}/${CHART_DIR}/"
git -C "${TMP_DIR}" add "${CHART_DIR}"
if git -C "${TMP_DIR}" diff --cached --quiet; then
echo "Charts for ${CHART_DIR} are already present, skipping push"
echo "sha=$(git -C "${TMP_DIR}" rev-parse HEAD)" >> "$GITHUB_OUTPUT"
rm -rf "${TMP_DIR}"
exit 0
fi
git -C "${TMP_DIR}" -c user.name="github-actions" -c user.email="github-actions@github.com" \
commit -m "bench charts for PR #${PR_NUMBER} run ${RUN_ID}"
for attempt in 1 2 3 4 5; do
if git -C "${TMP_DIR}" push origin HEAD:main; then
break
fi
if [ "$attempt" -eq 5 ]; then
echo "::error::Failed to push charts after ${attempt} attempts"
rm -rf "${TMP_DIR}"
exit 1
fi
sleep "$attempt"
git -C "${TMP_DIR}" fetch origin main
git -C "${TMP_DIR}" rebase origin/main
done
git -C "${TMP_DIR}" push origin HEAD:main
echo "sha=$(git -C "${TMP_DIR}" rev-parse HEAD)" >> "$GITHUB_OUTPUT"
rm -rf "${TMP_DIR}"
- name: Compare & comment
if: success() && env.BENCH_COMMENT_ID
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -1263,7 +1297,7 @@ jobs:
// Samply profile links (URLs point directly to Firefox Profiler)
if (process.env.BENCH_SAMPLY === 'true') {
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
const runs = abba ? ['feature-1', 'baseline-1', 'baseline-2', 'feature-2'] : ['feature-1', 'baseline-1'];
const runs = abba ? ['baseline-1', 'feature-1', 'feature-2', 'baseline-2'] : ['baseline-1', 'feature-1'];
const links = [];
for (const run of runs) {
try {
@@ -1307,7 +1341,7 @@ jobs:
- name: Write job summary
if: success()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const jobSummary = require('./.github/scripts/bench-job-summary.js');
@@ -1321,7 +1355,7 @@ jobs:
- name: Send Slack notification (success)
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -1332,20 +1366,18 @@ jobs:
- name: Update status (failed)
if: failure() && env.BENCH_COMMENT_ID
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
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 }}'],
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
];
const failed = steps_status.find(([, o]) => o === 'failure');
const failedStep = failed ? failed[0] : 'unknown step';
@@ -1368,22 +1400,20 @@ jobs:
- name: Send Slack notification (failure)
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
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 }}'],
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
];
const failed = steps_status.find(([, o]) => o === 'failure');
const failedStep = failed ? failed[0] : 'unknown step';
@@ -1392,7 +1422,7 @@ jobs:
- name: Update status (cancelled)
if: cancelled() && env.BENCH_COMMENT_ID
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -1403,11 +1433,6 @@ 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: |
@@ -1420,9 +1445,5 @@ 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

View File

@@ -10,22 +10,19 @@ on:
types: [opened, reopened, synchronize, closed]
merge_group:
permissions: {}
env:
RUSTC_WRAPPER: "sccache"
jobs:
build:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v6
- name: Install bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
uses: oven-sh/setup-bun@v2
with:
bun-version: v1.2.23
@@ -39,6 +36,8 @@ jobs:
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Build docs
run: cd docs/vocs && bash scripts/build-cargo-docs.sh
@@ -48,10 +47,10 @@ jobs:
echo "Vocs Build Complete"
- name: Setup Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
uses: actions/upload-pages-artifact@v4
with:
path: "./docs/vocs/docs/dist"
@@ -75,4 +74,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
uses: actions/deploy-pages@v5

View File

@@ -22,41 +22,31 @@ on:
env:
CARGO_TERM_COLOR: always
permissions: {}
jobs:
check:
name: Check compilation with patched alloy
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Apply alloy patches
env:
ALLOY_BRANCH: ${{ inputs.alloy_branch }}
ALLOY_EVM_BRANCH: ${{ inputs.alloy_evm_branch }}
OP_ALLOY_BRANCH: ${{ inputs.op_alloy_branch }}
run: |
ARGS=""
if [ -n "$ALLOY_BRANCH" ]; then
ARGS="$ARGS --alloy $ALLOY_BRANCH"
if [ -n "${{ inputs.alloy_branch }}" ]; then
ARGS="$ARGS --alloy ${{ inputs.alloy_branch }}"
fi
if [ -n "$ALLOY_EVM_BRANCH" ]; then
ARGS="$ARGS --evm $ALLOY_EVM_BRANCH"
if [ -n "${{ inputs.alloy_evm_branch }}" ]; then
ARGS="$ARGS --evm ${{ inputs.alloy_evm_branch }}"
fi
if [ -n "$OP_ALLOY_BRANCH" ]; then
ARGS="$ARGS --op $OP_ALLOY_BRANCH"
if [ -n "${{ inputs.op_alloy_branch }}" ]; then
ARGS="$ARGS --op ${{ inputs.op_alloy_branch }}"
fi
if [ -z "$ARGS" ]; then

View File

@@ -16,38 +16,32 @@ env:
RUSTC_WRAPPER: "sccache"
name: compact-codec
permissions: {}
jobs:
compact-codec:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
strategy:
matrix:
bin:
- cargo run --bin reth --features "dev"
steps:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Checkout base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
ref: ${{ github.base_ref || 'main' }}
persist-credentials: false
# On `main` branch, generates test vectors and serializes them to disk using `Compact`.
- name: Generate compact vectors
run: |
${{ matrix.bin }} -- test-vectors compact --write
- name: Checkout PR
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
clean: false
persist-credentials: false
# On incoming merge try to read and decode previously generated vectors with `Compact`
- name: Read vectors
run: ${{ matrix.bin }} -- test-vectors compact --read

View File

@@ -9,14 +9,13 @@ on:
workflow_dispatch:
# Needed so we can run it manually
permissions: {}
permissions:
contents: write
pull-requests: write
jobs:
update:
if: github.repository == 'paradigmxyz/reth'
permissions:
contents: write
pull-requests: write
uses: tempoxyz/ci/.github/workflows/cargo-update-pr.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -17,8 +17,6 @@ on:
env:
DOCKER_USERNAME: ${{ github.actor }}
permissions: {}
jobs:
tag-reth-latest:
name: Tag reth as latest
@@ -29,22 +27,16 @@ jobs:
contents: read
steps:
- name: Log in to Docker
env:
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "$DOCKER_PASSWORD" | docker login ghcr.io --username "${DOCKER_USERNAME}" --password-stdin
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
- name: Pull reth release image
env:
VERSION: ${{ inputs.version }}
run: |
docker pull ghcr.io/${{ github.repository_owner }}/reth:${VERSION}
docker pull ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }}
- name: Tag reth as latest
env:
VERSION: ${{ inputs.version }}
run: |
docker tag ghcr.io/${{ github.repository_owner }}/reth:${VERSION} ghcr.io/${{ github.repository_owner }}/reth:latest
docker tag ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }} ghcr.io/${{ github.repository_owner }}/reth:latest
- name: Push reth latest tag
run: |

View File

@@ -13,8 +13,6 @@ on:
default: "artifacts"
description: "Name for the uploaded artifact"
permissions: {}
jobs:
build:
timeout-minutes: 45
@@ -23,9 +21,7 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- run: mkdir -p artifacts
- name: Get git info
@@ -36,12 +32,8 @@ jobs:
- name: Detect fork
id: fork
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
REPO: ${{ github.repository }}
run: |
if [ "$EVENT_NAME" = "pull_request" ] && [ "$HEAD_REPO" != "$REPO" ]; then
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "is_fork=true" >> "$GITHUB_OUTPUT"
else
echo "is_fork=false" >> "$GITHUB_OUTPUT"
@@ -50,11 +42,11 @@ jobs:
# Depot build (upstream only)
- name: Set up Depot CLI
if: steps.fork.outputs.is_fork == 'false'
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
uses: depot/setup-action@v1
- name: Build reth image (Depot)
if: steps.fork.outputs.is_fork == 'false'
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
uses: depot/bake-action@v1
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
@@ -68,11 +60,11 @@ jobs:
# Docker build (forks)
- name: Set up Docker Buildx
if: steps.fork.outputs.is_fork == 'true'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v3
- name: Build reth image (Docker)
if: steps.fork.outputs.is_fork == 'true'
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0
uses: docker/bake-action@v6
env:
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
@@ -84,7 +76,7 @@ jobs:
*.dockerfile=Dockerfile
- name: Upload reth image
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact_name }}
path: ./artifacts

View File

@@ -29,8 +29,6 @@ on:
type: boolean
default: false
permissions: {}
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
@@ -41,15 +39,13 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Set up Depot CLI
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
uses: depot/setup-action@v1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -64,13 +60,10 @@ jobs:
- name: Determine build parameters
id: params
env:
EVENT_NAME: ${{ github.event_name }}
BUILD_TYPE: ${{ inputs.build_type }}
run: |
REGISTRY="ghcr.io/${{ github.repository_owner }}"
if [[ "${EVENT_NAME}" == "push" ]]; then
if [[ "${{ github.event_name }}" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
@@ -88,7 +81,7 @@ jobs:
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
fi
elif [[ "${EVENT_NAME}" == "schedule" ]] || [[ "${BUILD_TYPE}" == "nightly" ]]; then
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
echo "targets=nightly" >> "$GITHUB_OUTPUT"
echo "ethereum_tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
@@ -101,7 +94,7 @@ jobs:
fi
- name: Build and push images
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
uses: depot/bake-action@v1
env:
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
@@ -112,8 +105,6 @@ jobs:
files: docker-bake.hcl
targets: ${{ steps.params.outputs.targets }}
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
save: false
load: false
set: |
${{ steps.params.outputs.ethereum_set }}
@@ -133,7 +124,7 @@ jobs:
if: failure() && github.event_name == 'schedule'
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
uses: rtCamp/action-slack-notify@v2
env:
SLACK_COLOR: danger
SLACK_ICON_EMOJI: ":rotating_light:"

View File

@@ -17,27 +17,19 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: e2e-testsuite
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run e2e tests
@@ -56,21 +48,15 @@ jobs:
rocksdb:
name: e2e-rocksdb
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run RocksDB e2e tests

View File

@@ -1,72 +0,0 @@
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"
permissions: {}
jobs:
fetch:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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 }}
DASHBOARD_UID: ${{ inputs.dashboard_uid }}
TARGET_PATH: ${{ inputs.target_path }}
run: |
python3 .github/scripts/fetch-grafana-dashboard.py "${DASHBOARD_UID}" \
> "${TARGET_PATH}"
- name: Check for changes
id: diff
env:
TARGET_PATH: ${{ inputs.target_path }}
run: |
if git diff --quiet "${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 }}
DASHBOARD_UID: ${{ inputs.dashboard_uid }}
TARGET_PATH: ${{ inputs.target_path }}
run: |
TARGET="${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: \`${DASHBOARD_UID}\`, target: \`${TARGET}\`)."

View File

@@ -6,33 +6,16 @@ on:
push:
branches: [main]
permissions: {}
jobs:
check-dashboard:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate dashboard format
- uses: actions/checkout@v6
- name: Check for ${DS_PROMETHEUS} in overview.json
run: |
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')
"
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"

View File

@@ -6,9 +6,6 @@ on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
pull_request:
branches:
- "**"
env:
CARGO_TERM_COLOR: always
@@ -17,13 +14,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
build-reth:
permissions:
contents: read
id-token: write
uses: ./.github/workflows/docker-test.yml
with:
hive_target: hive
@@ -31,48 +23,36 @@ jobs:
secrets: inherit
prepare-hive:
if: github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth'
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
variant:
- amsterdam
- osaka
name: Prepare Hive - ${{ matrix.variant == 'amsterdam' && 'Amsterdam' || 'Osaka' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Checkout hive tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
repository: ethereum/hive
path: hivetests
persist-credentials: false
- name: Get hive commit hash
id: hive-commit
run: echo "hash=$(cd hivetests && git rev-parse HEAD)" >> $GITHUB_OUTPUT
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@v6
with:
go-version: "^1.13.1"
- run: go version
- name: Restore hive assets cache
id: cache-hive
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@v5
with:
path: ./hive_assets
key: hive-assets-${{ matrix.variant }}-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
- name: Build hive assets
if: steps.cache-hive.outputs.cache-hit != 'true'
run: .github/scripts/hive/build_simulators.sh ${{ matrix.variant }}
run: .github/scripts/hive/build_simulators.sh
- name: Load cached Docker images
if: steps.cache-hive.outputs.cache-hit == 'true'
@@ -88,195 +68,11 @@ jobs:
chmod +x hive
- name: Upload hive assets
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: hive_assets_${{ matrix.variant }}
name: hive_assets
path: ./hive_assets
test-amsterdam:
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
# ethereum/rpc to be deprecated:
# https://github.com/ethereum/hive/pull/1117
scenario:
- sim: smoke/genesis
- sim: smoke/network
- sim: ethereum/sync
- sim: devp2p
limit: discv4
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
# action on our side, remove from here when we get unexpected passes on these tests
# - sim: devp2p
# limit: eth
# include:
# - MaliciousHandshake
# # failures tracked in https://github.com/paradigmxyz/reth/issues/14825
# - Status
# - GetBlockHeaders
# - ZeroRequestID
# - GetBlockBodies
# - Transaction
# - NewPooledTxs
- sim: devp2p
limit: discv5
include:
# failures tracked at https://github.com/paradigmxyz/reth/issues/14825
- PingLargeRequestID
- sim: ethereum/engine
limit: engine-exchange-capabilities
- sim: ethereum/engine
limit: engine-withdrawals
- sim: ethereum/engine
limit: engine-auth
- sim: ethereum/engine
limit: engine-api
- sim: ethereum/engine
limit: cancun
# eth_ rpc methods
- sim: ethereum/rpc-compat
include:
- eth_blockNumber
- eth_call
- eth_chainId
- eth_createAccessList
- eth_estimateGas
- eth_feeHistory
- eth_getBalance
- eth_getBlockBy
- eth_getBlockTransactionCountBy
- eth_getCode
- eth_getProof
- eth_getStorage
- eth_getTransactionBy
- eth_getTransactionCount
- eth_getTransactionReceipt
- eth_sendRawTransaction
- eth_syncing
# debug_ rpc methods
- debug_
# consume-engine
- sim: ethereum/eels/consume-engine
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-engine
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-engine
limit: .*tests/prague.*
- sim: ethereum/eels/consume-engine
limit: .*tests/cancun.*
- sim: ethereum/eels/consume-engine
limit: .*tests/shanghai.*
- sim: ethereum/eels/consume-engine
limit: .*tests/berlin.*
- sim: ethereum/eels/consume-engine
limit: .*tests/istanbul.*
- sim: ethereum/eels/consume-engine
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-engine
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-engine
limit: .*tests/paris.*
# consume-rlp
- sim: ethereum/eels/consume-rlp
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/prague.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/cancun.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/shanghai.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/berlin.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/istanbul.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/paris.*
needs:
- build-reth
- prepare-hive
name: Hive-Amsterdam / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
# Use larger runners for eels tests to avoid OOM runner crashes
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: hive_assets_amsterdam
path: /tmp
- name: Download reth image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reth
path: /tmp
- name: Load Docker images
run: .github/scripts/hive/load_images.sh
- name: Move hive binary
run: |
mv /tmp/hive /usr/local/bin
chmod +x /usr/local/bin/hive
- name: Checkout hive tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ethereum/hive
ref: master
path: hivetests
persist-credentials: false
- name: Run simulator
env:
SCENARIO_SIM: ${{ matrix.scenario.sim }}
SCENARIO_LIMIT: ${{ matrix.scenario.limit }}
SCENARIO_TESTS: ${{ join(matrix.scenario.include, '|') }}
run: |
LIMIT="$SCENARIO_LIMIT"
TESTS="$SCENARIO_TESTS"
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
FILTER="$LIMIT/$TESTS"
elif [ -n "$LIMIT" ]; then
FILTER="$LIMIT"
elif [ -n "$TESTS" ]; then
FILTER="/$TESTS"
else
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "amsterdam"
- name: Parse hive output
run: |
find hivetests/workspace/logs -type f -name "*.json" ! -name "hive.json" | xargs -I {} python .github/scripts/hive/parse.py {} --exclusion .github/scripts/hive/expected_failures.yaml --ignored .github/scripts/hive/ignored_tests.yaml
- name: Print simulator output
if: ${{ failure() }}
run: |
cat hivetests/workspace/logs/*simulator*.log
- name: Print reth client logs
if: ${{ failure() }}
run: |
cat hivetests/workspace/logs/reth/client-*.log
test-osaka:
test:
timeout-minutes: 120
strategy:
fail-fast: false
@@ -382,26 +178,24 @@ jobs:
needs:
- build-reth
- prepare-hive
name: Hive-Osaka / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
# Use larger runners for eels tests to avoid OOM runner crashes
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
with:
name: hive_assets_osaka
name: hive_assets
path: /tmp
- name: Download reth image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
with:
name: reth
path: /tmp
@@ -415,21 +209,16 @@ jobs:
chmod +x /usr/local/bin/hive
- name: Checkout hive tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
repository: ethereum/hive
ref: master
path: hivetests
persist-credentials: false
- name: Run simulator
env:
SCENARIO_SIM: ${{ matrix.scenario.sim }}
SCENARIO_LIMIT: ${{ matrix.scenario.limit }}
SCENARIO_TESTS: ${{ join(matrix.scenario.include, '|') }}
run: |
LIMIT="$SCENARIO_LIMIT"
TESTS="$SCENARIO_TESTS"
LIMIT="${{ matrix.scenario.limit }}"
TESTS="${{ join(matrix.scenario.include, '|') }}"
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
FILTER="$LIMIT/$TESTS"
elif [ -n "$LIMIT" ]; then
@@ -440,7 +229,7 @@ jobs:
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "osaka"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER"
- name: Parse hive output
run: |
@@ -456,14 +245,12 @@ jobs:
run: |
cat hivetests/workspace/logs/reth/client-*.log
notify-on-error:
needs:
- test-amsterdam
- test-osaka
needs: test
if: failure()
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
uses: rtCamp/action-slack-notify@v2
env:
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"

View File

@@ -20,15 +20,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: test / ${{ matrix.network }}
if: github.event_name != 'schedule'
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
strategy:
@@ -36,18 +32,14 @@ jobs:
network: ["ethereum"]
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- name: Install Geth
run: .github/scripts/install_geth.sh
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run tests
@@ -66,7 +58,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
@@ -74,19 +66,13 @@ jobs:
name: era1 file integration tests once a day
if: github.event_name == 'schedule' && github.repository == 'paradigmxyz/reth'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: run era1 files integration tests

View File

@@ -18,14 +18,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
build-reth:
if: github.repository == 'paradigmxyz/reth'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/docker-test.yml
with:
hive_target: kurtosis
@@ -37,18 +32,15 @@ jobs:
fail-fast: false
name: run kurtosis
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
needs:
- build-reth
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Download reth image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
with:
name: artifacts
path: /tmp
@@ -60,7 +52,7 @@ jobs:
docker image ls -a
- name: Run kurtosis
uses: ethpandaops/kurtosis-assertoor-github-action@f64942cbc780df731a731ea9f45765b161d2c8df # v1.0.1
uses: ethpandaops/kurtosis-assertoor-github-action@v1
with:
ethereum_package_args: ".github/assets/kurtosis_network_params.yaml"
@@ -70,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
uses: rtCamp/action-slack-notify@v2
env:
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"

View File

@@ -4,23 +4,19 @@ on:
pull_request:
types: [opened]
permissions: {}
jobs:
label_prs:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Label PRs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const label_pr = require('./.github/scripts/label_pr.js')

View File

@@ -8,17 +8,11 @@ on:
paths:
- '.github/**'
permissions: {}
jobs:
actionlint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)

View File

@@ -10,14 +10,10 @@ env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
permissions: {}
jobs:
clippy-binaries:
name: clippy binaries / ${{ matrix.type }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
strategy:
matrix:
@@ -26,19 +22,17 @@ jobs:
args: --workspace --lib --examples --tests --benches --locked
features: "ethereum asm-keccak jemalloc jemalloc-prof min-error-logs min-warn-logs min-info-logs min-debug-logs min-trace-logs"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@clippy
with:
components: clippy
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- if: "${{ matrix.type == 'book' }}"
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run clippy on binaries
@@ -49,19 +43,15 @@ jobs:
clippy:
name: clippy
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
with:
components: clippy
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
@@ -70,25 +60,19 @@ jobs:
wasm:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-wasip1
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- uses: dcarbone/install-jq-action@v3
- name: Run Wasm checks
run: |
sudo apt update && sudo apt install gcc-multilib
@@ -96,49 +80,37 @@ jobs:
riscv:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
with:
target: riscv32imac-unknown-none-elf
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- uses: dcarbone/install-jq-action@v3
- name: Run RISC-V checks
run: .github/scripts/check_rv32imac.sh
crate-checks:
name: crate-checks (${{ matrix.partition }}/${{ matrix.total_partitions }})
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
strategy:
matrix:
partition: [1, 2, 3]
total_partitions: [3]
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo hack check --workspace --partition ${{ matrix.partition }}/${{ matrix.total_partitions }}
@@ -146,19 +118,15 @@ jobs:
msrv:
name: MSRV
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.93" # MSRV
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo build --bin reth --workspace
@@ -168,17 +136,13 @@ jobs:
docs:
name: docs
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo docs --document-private-items
@@ -190,56 +154,42 @@ jobs:
fmt:
name: fmt
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Run fmt
run: cargo fmt --all --check
udeps:
name: udeps
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-udeps
- uses: taiki-e/install-action@cargo-udeps
- run: cargo udeps --workspace --lib --examples --tests --benches --all-features --locked
book:
name: book
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo build --bin reth --workspace
@@ -251,54 +201,38 @@ jobs:
typos:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1
check-toml:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@v6
- name: Run dprint
uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3
uses: dprint/check@v2.3
with:
config-path: dprint.json
grafana:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Check dashboard JSON with jq
uses: sergeysova/jq-action@a3f0d4ff59cc1dddf023fc0b325dd75b10deec58 # v2.3.0
uses: sergeysova/jq-action@v2
with:
cmd: jq empty etc/grafana/dashboards/overview.json
no-test-deps:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- name: Ensure no arbitrary or proptest dependency on default build
run: cargo tree --package reth -e=features,no-dev | grep -Eq "arbitrary|proptest" && exit 1 || exit 0
@@ -306,17 +240,13 @@ jobs:
# Check crates correctly propagate features
feature-propagation:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: taiki-e/cache-cargo-install-action@a8b9ecf8e0c0ea09d7481cfc583a5203ecd585b5 # v3.0.5
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: rui314/setup-mold@v1
- uses: taiki-e/cache-cargo-install-action@v3
with:
tool: zepter
- name: Eagerly pull dependencies
@@ -324,8 +254,6 @@ jobs:
- run: zepter run check
deny:
permissions:
contents: read
uses: tempoxyz/ci/.github/workflows/deny.yml@main
lint-success:
@@ -349,6 +277,6 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -4,36 +4,27 @@ on:
pull_request:
types: [labeled]
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
if: github.event.label.name == 'cyclops'
permissions: {}
steps:
- name: Publish event
env:
EVENTS_KEY: ${{ secrets.EVENTS_KEY }}
EVENTS_CERT: ${{ secrets.EVENTS_CERT }}
EVENTS_ARGS: ${{ secrets.EVENTS_ARGS }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
echo "$EVENTS_KEY" > "${{ runner.temp }}/key"
echo "$EVENTS_CERT" > "${{ runner.temp }}/cert"
echo "${{ secrets.EVENTS_KEY }}" > ${{ runner.temp }}/key
echo "${{ secrets.EVENTS_CERT }}" > ${{ runner.temp }}/cert
curl -sf -o /dev/null -X POST $EVENTS_ARGS \
curl -sf -o /dev/null -X POST ${{ secrets.EVENTS_ARGS }} \
-H "Content-Type: application/json" \
--key "${{ runner.temp }}/key" \
--cert "${{ runner.temp }}/cert" \
--key ${{ runner.temp }}/key \
--cert ${{ runner.temp }}/cert \
-d '{
"repository": "${{ github.repository }}",
"event": "pr_audit",
"data": {
"pr_number": '"$PR_NUMBER"',
"sha": "'"$PR_SHA"'"
"pr_number": ${{ github.event.pull_request.number }},
"sha": "${{ github.event.pull_request.head.sha }}"
}
}'

View File

@@ -8,19 +8,20 @@ on:
- edited
- synchronize
permissions: {}
permissions:
pull-requests: read
contents: read
jobs:
conventional-title:
name: Validate PR title is Conventional Commit
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Check title
id: lint_pr_title
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -39,7 +40,7 @@ jobs:
continue-on-error: true
- name: Add PR Comment for Invalid Title
if: steps.lint_pr_title.outcome == 'failure'
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
message: |
@@ -75,7 +76,7 @@ jobs:
- name: Remove Comment for Valid Title
if: steps.lint_pr_title.outcome == 'success'
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true

View File

@@ -7,15 +7,12 @@ on:
release:
types: [published]
permissions: {}
jobs:
release-homebrew:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@1446dca236b0440c6f02723a3f14f13be2c04ab0 # v7
uses: dawidd6/action-homebrew-bump-formula@v7
with:
token: ${{ secrets.HOMEBREW }}
no_fork: true

View File

@@ -2,8 +2,6 @@
name: release-reproducible
permissions: {}
on:
workflow_run:
workflows: [release]
@@ -17,23 +15,20 @@ jobs:
name: extract version
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Extract version from triggering tag
id: extract_version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# Get the tag that points to the head SHA of the triggering workflow
TAG=$(gh api /repos/${{ github.repository }}/git/refs/tags \
--jq ".[] | select(.object.sha == \"${HEAD_SHA}\") | .ref" \
--jq '.[] | select(.object.sha == "${{ github.event.workflow_run.head_sha }}") | .ref' \
| head -1 \
| sed 's|refs/tags/||')
if [ -z "$TAG" ]; then
echo "No tag found for SHA ${HEAD_SHA}"
echo "No tag found for SHA ${{ github.event.workflow_run.head_sha }}"
exit 1
fi
@@ -49,16 +44,15 @@ jobs:
packages: write
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.extract-version.outputs.VERSION }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -71,7 +65,7 @@ jobs:
echo "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" >> $GITHUB_OUTPUT
- name: Build reproducible artifacts
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@v6
id: docker_build
with:
context: .
@@ -81,11 +75,13 @@ jobs:
VERSION=${{ needs.extract-version.outputs.VERSION }}
target: artifacts
outputs: type=local,dest=./docker-artifacts
cache-from: type=gha
cache-to: type=gha,mode=max
env:
DOCKER_BUILD_RECORD_UPLOAD: false
- name: Build and push final image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.reproducible
@@ -96,6 +92,8 @@ jobs:
tags: |
${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${{ needs.extract-version.outputs.VERSION }}
${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
env:
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -3,8 +3,6 @@
name: release
permissions: {}
on:
push:
tags:
@@ -22,24 +20,21 @@ env:
REPRODUCIBLE_IMAGE_NAME: ${{ github.repository_owner }}/reth-reproducible
CARGO_TERM_COLOR: always
DOCKER_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/reth
RUSTC_WRAPPER: "sccache"
jobs:
dry-run:
name: check dry run
runs-on: ubuntu-latest
permissions: {}
steps:
- env:
DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
echo "Dry run value: ${DRY_RUN}"
echo "Dry run enabled: $( [ "${DRY_RUN}" = 'true' ] && echo true || echo false )"
echo "Dry run disabled: $( [ "${DRY_RUN}" != 'true' ] && echo true || echo false )"
- run: |
echo "Dry run value: ${{ github.event.inputs.dry_run }}"
echo "Dry run enabled: ${{ github.event.inputs.dry_run == 'true'}}"
echo "Dry run disabled: ${{ github.event.inputs.dry_run != 'true'}}"
extract-version:
name: extract version
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Extract version
run: echo "VERSION=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT
@@ -50,15 +45,12 @@ jobs:
check-version:
name: check version
runs-on: ubuntu-latest
permissions:
contents: read
needs: extract-version
if: ${{ github.event.inputs.dry_run != 'true' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Verify crate version matches tag
# Check that the Cargo version starts with the tag,
# so that Cargo version 1.4.8 can be matched against both v1.4.8 and v1.4.8-rc.1
@@ -71,8 +63,6 @@ jobs:
build:
name: build release
runs-on: ${{ matrix.configs.os }}
permissions:
contents: read
needs: extract-version
continue-on-error: ${{ matrix.configs.allow_fail }}
strategy:
@@ -105,20 +95,20 @@ jobs:
- command: build
binary: reth
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
with:
target: ${{ matrix.configs.target }}
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Install cross main
if: ${{ !matrix.configs.native }}
id: cross_main
run: |
cargo install cross --locked \
--git https://github.com/cross-rs/cross \
--rev 65fe72b0cdb1e7e0cc0652517498d4389cc8f5cf
cargo install cross --locked --git https://github.com/cross-rs/cross
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Apple M1 setup
if: matrix.configs.target == 'aarch64-apple-darwin'
@@ -155,14 +145,14 @@ jobs:
- name: Upload artifact
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
- name: Upload signature
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
@@ -181,12 +171,11 @@ jobs:
steps:
# This is necessary for generating the changelog.
# It has to come before "Download Artifacts" or else it deletes the artifacts.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
- name: Generate full changelog
id: changelog
run: |
@@ -206,7 +195,7 @@ jobs:
fi
body=$(cat <<- "ENDBODY"
![image](https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-2.png)
![image](https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-prod.png)
## Testing Checklist (DELETE ME)
@@ -272,7 +261,6 @@ jobs:
dry-run-summary:
name: dry run summary
runs-on: ubuntu-latest
permissions: {}
needs: [build, extract-version]
if: ${{ github.event.inputs.dry_run == 'true' }}
env:

View File

@@ -5,15 +5,11 @@ on:
schedule:
- cron: "0 1 */2 * *"
permissions: {}
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
name: build reproducible binaries
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
matrix:
include:
@@ -22,16 +18,14 @@ jobs:
- runner: ubuntu-22.04
machine: machine-2
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-linux-gnu
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v3
- name: Build reproducible binary with Docker
run: |
@@ -49,7 +43,7 @@ jobs:
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
- name: Upload the hash
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@v7
with:
name: checksum-${{ matrix.machine }}
path: |
@@ -62,12 +56,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download artifacts from machine-1
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
with:
name: checksum-machine-1
path: machine-1/
- name: Download artifacts from machine-2
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v8
with:
name: checksum-machine-2
path: machine-2/

View File

@@ -18,28 +18,22 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
stage:
name: stage-run-test
# Only run stage commands test in merge groups
if: github.event_name == 'merge_group'
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Build reth

View File

@@ -7,8 +7,6 @@ on:
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
close-issues:
if: github.repository == 'paradigmxyz/reth'
@@ -17,7 +15,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
- uses: actions/stale@v10
with:
days-before-stale: 21
days-before-close: 7

View File

@@ -15,15 +15,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
@@ -38,13 +34,11 @@ jobs:
block: 100000
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Build ${{ matrix.chain.bin }}

View File

@@ -15,15 +15,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
@@ -38,13 +34,11 @@ jobs:
block: 100000
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Build ${{ matrix.chain.bin }}

View File

@@ -17,14 +17,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: test / ${{ matrix.type }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
strategy:
@@ -36,20 +32,16 @@ jobs:
exclude_args: ""
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: taiki-e/install-action@nextest
- if: "${{ matrix.type == 'book' }}"
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run tests
@@ -64,25 +56,20 @@ jobs:
state:
name: Ethereum state tests
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Checkout ethereum/tests
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
repository: ethereum/tests
ref: 81862e4848585a438d64f911a19b3825f0f4cd95
path: testing/ef-tests/ethereum-tests
submodules: recursive
fetch-depth: 1
persist-credentials: false
- name: Download & extract EEST fixtures (public)
shell: bash
env:
@@ -92,13 +79,11 @@ jobs:
mkdir -p testing/ef-tests/execution-spec-tests
URL="https://github.com/ethereum/execution-spec-tests/releases/download/${EEST_TESTS_TAG}/fixtures_stable.tar.gz"
curl -L "$URL" | tar -xz --strip-components=1 -C testing/ef-tests/execution-spec-tests
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo nextest run --no-fail-fast --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
@@ -106,19 +91,15 @@ jobs:
doc:
name: doc tests
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run doctests
@@ -132,6 +113,6 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

797
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace.package]
version = "2.1.0"
version = "2.0.0"
edition = "2024"
rust-version = "1.93"
license = "MIT OR Apache-2.0"
@@ -129,7 +129,6 @@ 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",
@@ -326,8 +325,8 @@ reth-cli = { path = "crates/cli/cli" }
reth-cli-commands = { path = "crates/cli/commands" }
reth-cli-runner = { path = "crates/cli/runner" }
reth-cli-util = { path = "crates/cli/util" }
reth-codecs = { version = "0.3.1", default-features = false }
reth-codecs-derive = "0.3.1"
reth-codecs = { version = "0.3.0", default-features = false }
reth-codecs-derive = "0.3.0"
reth-config = { path = "crates/config", default-features = false }
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
@@ -395,7 +394,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
reth-payload-primitives = { path = "crates/payload/primitives" }
reth-payload-validator = { path = "crates/payload/validator" }
reth-payload-util = { path = "crates/payload/util" }
reth-primitives-traits = { version = "0.3.1", default-features = false }
reth-primitives-traits = { version = "0.3.0", default-features = false }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune/prune" }
reth-prune-types = { path = "crates/prune/types", default-features = false }
@@ -411,7 +410,7 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
reth-rpc-traits = { version = "0.3.1", default-features = false }
reth-rpc-traits = { version = "0.3.0", default-features = false }
reth-stages = { path = "crates/stages/stages" }
reth-stages-api = { path = "crates/stages/api" }
reth-stages-types = { path = "crates/stages/types", default-features = false }
@@ -430,17 +429,17 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
reth-trie-db = { path = "crates/trie/db" }
reth-trie-parallel = { path = "crates/trie/parallel" }
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-zstd-compressors = { version = "0.3.1", default-features = false }
reth-zstd-compressors = { version = "0.3.0", default-features = false }
# revm
revm = { version = "=38.0.0", default-features = false }
revm-bytecode = { version = "=10.0.0", default-features = false }
revm-database = { version = "=13.0.1", default-features = false }
revm-state = { version = "=11.0.1", default-features = false }
revm-primitives = { version = "=23.0.0", default-features = false }
revm-interpreter = { version = "=35.0.1", default-features = false }
revm-database-interface = { version = "=11.0.1", default-features = false }
revm-inspectors = "=0.39.0"
revm = { version = "37.0.0", default-features = false }
revm-bytecode = { version = "10.0.0", default-features = false }
revm-database = { version = "13.0.0", default-features = false }
revm-state = { version = "11.0.0", default-features = false }
revm-primitives = { version = "23.0.0", default-features = false }
revm-interpreter = { version = "35.0.0", default-features = false }
revm-database-interface = { version = "11.0.0", default-features = false }
revm-inspectors = "0.38.0"
# eth
alloy-dyn-abi = "1.5.6"
@@ -449,40 +448,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.4", default-features = false }
alloy-evm = { version = "0.34.0", default-features = false }
alloy-eip7928 = { version = "0.3.0", default-features = false }
alloy-evm = { version = "0.32.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.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 }
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 }
# misc
either = { version = "1.15.0", default-features = false }
@@ -507,7 +506,6 @@ 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"
@@ -581,7 +579,7 @@ tower = "0.5"
tower-http = "0.6"
# p2p
discv5 = { git = "https://github.com/sigp/discv5", rev = "7663c00" }
discv5 = "0.10"
if-addrs = "0.14"
# rpc
@@ -702,22 +700,17 @@ vergen-git2 = "9.1.0"
ipnet = "2.11"
[patch.crates-io]
revm = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "5eebb56819ee6bec5bfbc69a415276ee1a784fec" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet-4" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
revm = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "c6f88bbe7186d863f4667dd43c42608eb7a8ba5c" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "ff0bbec9ccaa818155e25003a77f4d73d350bbd7" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }

View File

@@ -33,24 +33,15 @@ ENV FEATURES=$FEATURES
RUN cargo chef cook --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path recipe.json
# Build application
# Platform-specific RUSTFLAGS: amd64 uses x86-64-v3 (Haswell+) with pclmulqdq for rocksdb
#
# TARGETPLATFORM is set by BuildKit: https://docs.docker.com/reference/dockerfile#automatic-platform-args-in-the-global-scope
ARG TARGETPLATFORM
COPY --exclude=dist . .
RUN if [ -n "$RUSTFLAGS" ]; then \
export RUSTFLAGS="$RUSTFLAGS"; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
RUN cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
# ARG is not resolved in COPY so we have to hack around it by copying the
# binary to a temporary location
RUN cp /app/target/$BUILD_PROFILE/reth /app/reth
# Use Ubuntu as the release image
FROM ubuntu:24.04 AS runtime
FROM ubuntu AS runtime
WORKDIR /app
# Copy reth over from the build stage

View File

@@ -69,7 +69,6 @@ default = [
"jemalloc",
"reth-cli-util/jemalloc",
"asm-keccak",
"keccak-cache-global",
"min-debug-logs",
]
@@ -90,12 +89,6 @@ asm-keccak = [
"revm-primitives/asm-keccak",
]
keccak-cache-global = [
"reth-node-core/keccak-cache-global",
"reth-node-ethereum/keccak-cache-global",
"alloy-primitives/keccak-cache-global",
]
min-debug-logs = [
"tracing/release_max_level_debug",
"reth-ethereum-cli/min-debug-logs",

View File

@@ -7,16 +7,15 @@
//! `execute_transaction` to apply segment-boundary changes.
use crate::evm_config::BigBlockSegment;
use alloy_consensus::TransactionEnvelope;
use alloy_eips::eip7685::Requests;
use alloy_evm::{
block::{
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
BlockExecutorFor, ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
precompiles::PrecompilesMap,
Database, EthEvm, EthEvmFactory, Evm, EvmFactory, FromRecoveredTx, FromTxWithEncoded,
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
};
use alloy_primitives::B256;
use reth_ethereum_primitives::{Receipt, TransactionSigned};
@@ -37,7 +36,6 @@ use tracing::{debug, trace};
// ---------------------------------------------------------------------------
/// Runtime state for segment boundary tracking.
#[derive(Clone)]
pub(crate) struct BbEvmPlan {
/// The segment boundaries and environments.
pub(crate) segments: Vec<BigBlockSegment>,
@@ -75,10 +73,6 @@ impl BbEvmPlan {
.filter(|(n, _)| *n >= min && *n < block_number)
.collect()
}
pub(crate) fn segment_index_for_tx(&self, tx_index: usize) -> usize {
self.segments.partition_point(|segment| segment.start_tx <= tx_index).saturating_sub(1)
}
}
impl std::fmt::Debug for BbEvmPlan {
@@ -103,9 +97,6 @@ impl std::fmt::Debug for BbEvmPlan {
/// segment boundaries without requiring additional trait bounds on `DB`.
pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
/// Function pointer that reads the BAL index from the DB.
pub(crate) type BalIndexReader<DB> = fn(&DB) -> u64;
/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
/// changes for big-block execution.
///
@@ -117,8 +108,7 @@ pub(crate) type BalIndexReader<DB> = fn(&DB) -> u64;
/// Gas counters reset at each boundary so that each segment's real gas limit
/// is used (preserving correct GASLIMIT opcode behavior). Accumulated offsets
/// are applied to receipts and totals in `finish()`.
#[expect(missing_debug_implementations)]
pub struct BbBlockExecutor<'a, DB, I, P, Spec>
pub(crate) struct BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: Database,
{
@@ -141,10 +131,6 @@ where
/// Callback to reseed block hashes into the DB's cache at segment
/// boundaries. See [`BlockHashSeeder`].
block_hash_seeder: Option<BlockHashSeeder<DB>>,
/// Callback to read the BAL index from the DB.
bal_index_reader: Option<BalIndexReader<DB>>,
/// Whether the executor has selected its starting segment.
initialized: bool,
}
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
@@ -170,7 +156,6 @@ where
receipt_builder: RethReceiptBuilder,
plan: Option<BbEvmPlan>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
bal_index_reader: Option<BalIndexReader<DB>>,
) -> Self {
let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
Self {
@@ -181,63 +166,9 @@ where
blob_gas_used_offset: 0,
shared_hook: Arc::new(Mutex::new(None)),
block_hash_seeder,
bal_index_reader,
initialized: false,
}
}
fn initialize(&mut self) -> Result<(), BlockExecutionError> {
if self.initialized {
return Ok(());
}
let plan = match &self.plan {
Some(plan) => plan,
None => return Ok(()),
};
self.initialized = true;
let bal_index =
self.bal_index_reader.map(|reader| reader(self.inner().evm().db())).unwrap_or(0);
let segment_idx =
if bal_index == 0 { 0 } else { plan.segment_index_for_tx((bal_index - 1) as usize) };
let segment = &plan.segments[segment_idx];
// Swap the EVM's block_env and executor ctx to the selected segment's
// values so that EIP-2935/EIP-4788 system calls use the correct block
// number and parent hash. Without this, the outer big block header's
// block_number (which is synthetic) would be used, writing to wrong
// EIP-2935 slots and corrupting state.
let block_env = segment.evm_env.block_env.clone();
let block_number = block_env.number.saturating_to::<u64>();
let mut cfg_env = segment.evm_env.cfg_env.clone();
cfg_env.disable_base_fee = true;
let ctx = EthBlockExecutionCtx {
parent_hash: segment.ctx.parent_hash,
parent_beacon_block_root: segment.ctx.parent_beacon_block_root,
ommers: segment.ctx.ommers,
withdrawals: segment.ctx.withdrawals.clone(),
extra_data: segment.ctx.extra_data.clone(),
tx_count_hint: segment.ctx.tx_count_hint,
slot_number: segment.ctx.slot_number,
};
let inner = self.inner_mut();
let evm_ctx = inner.evm.ctx_mut();
evm_ctx.block = block_env;
evm_ctx.cfg = cfg_env;
inner.ctx = ctx;
self.reseed_block_hashes_for(block_number);
if bal_index > 0 {
self.plan = None;
}
Ok(())
}
/// Creates a forwarding `OnStateHook` that delegates to the shared hook.
fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
let shared = self.shared_hook.clone();
@@ -307,7 +238,6 @@ 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.
@@ -322,7 +252,6 @@ 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;
@@ -418,9 +347,34 @@ where
type Result = EthTxResult<HaltReason, alloy_consensus::TxType>;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
// The outer big-block header uses a synthetic block number, so start
// system calls must run against the selected real segment env.
self.initialize()?;
// Swap the EVM's block_env and executor ctx to the first segment's
// values so that the initial EIP-2935/EIP-4788 system calls use the
// correct block number and parent hash. Without this, the outer big
// block header's block_number (which is synthetic) would be used,
// writing to wrong EIP-2935 slots and corrupting state.
if let Some(seg0) = self.plan.as_ref().map(|p| &p.segments[0]) {
let block_env = seg0.evm_env.block_env.clone();
let block_number = block_env.number.saturating_to::<u64>();
let mut cfg_env = seg0.evm_env.cfg_env.clone();
cfg_env.disable_base_fee = true;
let seg0_ctx = EthBlockExecutionCtx {
parent_hash: seg0.ctx.parent_hash,
parent_beacon_block_root: seg0.ctx.parent_beacon_block_root,
ommers: seg0.ctx.ommers,
withdrawals: seg0.ctx.withdrawals.clone(),
extra_data: seg0.ctx.extra_data.clone(),
tx_count_hint: seg0.ctx.tx_count_hint,
};
let inner = self.inner_mut();
let evm_ctx = inner.evm.ctx_mut();
evm_ctx.block = block_env;
evm_ctx.cfg = cfg_env;
inner.ctx = seg0_ctx;
self.reseed_block_hashes_for(block_number);
}
self.inner_mut().apply_pre_execution_changes()
}
@@ -428,16 +382,15 @@ where
&mut self,
tx: impl ExecutableTx<Self>,
) -> Result<Self::Result, BlockExecutionError> {
self.initialize()?;
self.maybe_apply_boundary()?;
self.inner_mut().execute_transaction_without_commit(tx)
}
fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
self.maybe_apply_boundary()
.expect("segment boundary application must succeed before committing transaction");
let gas_used = self.inner_mut().commit_transaction(output);
fn commit_transaction(
&mut self,
output: Self::Result,
) -> Result<GasOutput, BlockExecutionError> {
let gas_used = self.inner_mut().commit_transaction(output)?;
// Fix up cumulative_gas_used on the just-committed receipt so that
// the receipt root task (which reads receipts incrementally) sees
@@ -452,7 +405,7 @@ where
if let Some(plan) = &mut self.plan {
plan.tx_counter += 1;
}
gas_used
Ok(gas_used)
}
fn finish(
@@ -469,7 +422,6 @@ 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;
}
@@ -543,7 +495,7 @@ pub struct BbBlockExecutorFactory<Spec> {
receipt_builder: RethReceiptBuilder,
spec: Spec,
evm_factory: EthEvmFactory,
/// Staged plan cloned into each [`BbBlockExecutor`].
/// Staged plan consumed by the next [`BbBlockExecutor`].
pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
}
@@ -572,12 +524,8 @@ impl<Spec> BbBlockExecutorFactory<Spec> {
*self.staged_plan.lock().unwrap() = Some(plan);
}
pub(crate) fn clear_staged_plan(&self) {
*self.staged_plan.lock().unwrap() = None;
}
fn peek_plan(&self) -> Option<BbEvmPlan> {
self.staged_plan.lock().unwrap().clone()
fn take_plan(&self) -> Option<BbEvmPlan> {
self.staged_plan.lock().unwrap().take()
}
pub(crate) fn create_executor_with_seeder<'a, DB, I>(
@@ -585,23 +533,14 @@ impl<Spec> BbBlockExecutorFactory<Spec> {
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
bal_index_reader: Option<BalIndexReader<DB>>,
) -> BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>
where
Spec: alloy_evm::eth::spec::EthExecutorSpec,
DB: StateDB + 'a,
I: Inspector<EthEvmContext<DB>> + 'a,
{
let plan = self.peek_plan();
BbBlockExecutor::new(
evm,
ctx,
&self.spec,
self.receipt_builder,
plan,
block_hash_seeder,
bal_index_reader,
)
let plan = self.take_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, block_hash_seeder)
}
}
@@ -614,12 +553,6 @@ where
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
type Transaction = TransactionSigned;
type Receipt = Receipt;
type TxExecutionResult = EthTxResult<
<EthEvmFactory as EvmFactory>::HaltReason,
<TransactionSigned as TransactionEnvelope>::TxType,
>;
type Executor<'a, DB: StateDB, I: Inspector<EthEvmContext<DB>>> =
BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>;
fn evm_factory(&self) -> &Self::EvmFactory {
&self.evm_factory
@@ -629,12 +562,12 @@ where
&'a self,
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
) -> Self::Executor<'a, DB, I>
) -> impl BlockExecutorFor<'a, Self, DB, I>
where
DB: StateDB,
I: Inspector<EthEvmContext<DB>>,
DB: StateDB + 'a,
I: Inspector<EthEvmContext<DB>> + 'a,
{
let plan = self.peek_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None, None)
let plan = self.take_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
}
}

View File

@@ -8,14 +8,11 @@
pub(crate) use reth_engine_primitives::BigBlockData;
use crate::{
evm::{BalIndexReader, BbBlockExecutorFactory, BbEvmPlan},
evm::{BbBlockExecutorFactory, BbEvmPlan},
BigBlockMap,
};
use alloy_consensus::Header;
use alloy_evm::{
eth::{spec::EthExecutorSpec, EthBlockExecutionCtx},
EthEvmFactory,
};
use alloy_evm::eth::EthBlockExecutionCtx;
use alloy_primitives::B256;
use alloy_rpc_types::engine::ExecutionData;
use core::convert::Infallible;
@@ -23,8 +20,8 @@ use reth_chainspec::{ChainSpec, EthChainSpec};
use reth_ethereum_forks::Hardforks;
use reth_ethereum_primitives::EthPrimitives;
use reth_evm::{
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, EvmEnvFor, ExecutableTxIterator,
ExecutionCtxFor, NextBlockEnvAttributes,
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
NextBlockEnvAttributes,
};
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
use reth_primitives_traits::{SealedBlock, SealedHeader};
@@ -32,6 +29,9 @@ use revm::primitives::hardfork::SpecId;
use std::sync::Arc;
use tracing::debug;
use alloy_evm::{eth::spec::EthExecutorSpec, EthEvmFactory};
use reth_evm::{EvmEnvFor, ExecutionCtxFor};
// ---------------------------------------------------------------------------
// Execution plan types
// ---------------------------------------------------------------------------
@@ -55,7 +55,7 @@ pub(crate) struct BigBlockSegment {
///
/// Wraps [`EthEvmConfig`] and a shared [`BigBlockMap`]. When a big-block
/// payload is received, the plan is staged on the [`BbBlockExecutorFactory`]
/// and cloned when executors are created. Block hashes for inter-segment
/// and consumed when the executor is created. Block hashes for inter-segment
/// BLOCKHASH resolution are reseeded into `State::block_hashes` at each
/// segment boundary via a [`BlockHashSeeder`](crate::evm::BlockHashSeeder)
/// callback injected in [`ConfigureEvm::create_executor`].
@@ -106,10 +106,6 @@ fn seed_state_block_hashes<DB>(state: &mut &mut revm::database::State<DB>, hashe
}
}
fn read_bal_index<DB>(state: &&mut revm::database::State<DB>) -> u64 {
state.bal_state.bal_index()
}
// ---------------------------------------------------------------------------
// ConfigureEvm
// ---------------------------------------------------------------------------
@@ -148,12 +144,6 @@ where
&self,
block: &'a SealedBlock<reth_ethereum_primitives::Block>,
) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
if let Some(plan) = self.plan_for_payload_hash(&block.hash()) {
self.executor_factory.stage_plan(plan);
} else {
self.executor_factory.clear_staged_plan();
}
self.inner.context_for_block(block)
}
@@ -169,7 +159,7 @@ where
&'a self,
evm: reth_evm::EvmFor<Self, &'a mut revm::database::State<DB>, I>,
ctx: EthBlockExecutionCtx<'a>,
) -> alloy_evm::block::BlockExecutorFor<
) -> impl alloy_evm::block::BlockExecutorFor<
'a,
Self::BlockExecutorFactory,
&'a mut revm::database::State<DB>,
@@ -179,16 +169,15 @@ where
DB: Database,
I: reth_evm::InspectorFor<Self, &'a mut revm::database::State<DB>> + 'a,
{
let bal_index_reader: Option<BalIndexReader<&'a mut revm::database::State<DB>>> =
Some(read_bal_index::<DB>);
// Inject concrete function pointers that know the `State<DB>` type so
// the generic executor can reseed block hashes and read `bal_index`.
// Use create_executor_with_seeder to inject a concrete seeder that
// can reseed State::block_hashes at segment boundaries. The seeder
// is a function pointer that knows the concrete State<DB> type,
// allowing the generic BbBlockExecutor to reseed without additional
// trait bounds on DB.
self.executor_factory.create_executor_with_seeder(
evm,
ctx,
Some(seed_state_block_hashes::<DB>),
bal_index_reader,
)
}
}
@@ -225,7 +214,6 @@ where
Ok(env)
} else {
self.executor_factory.clear_staged_plan();
self.inner.evm_env_for_payload(payload)
}
}
@@ -260,12 +248,10 @@ where
/// In practice, this is called from `evm_env_for_payload` in the
/// engine pipeline.
pub fn stage_plan_for_payload(&self, payload_hash: &B256) {
let Some(plan) = self.plan_for_payload_hash(payload_hash) else { return };
self.executor_factory.stage_plan(plan);
}
fn plan_for_payload_hash(&self, payload_hash: &B256) -> Option<BbEvmPlan> {
let bb = self.pending.lock().unwrap().remove(payload_hash)?;
let bb = match self.pending.lock().unwrap().remove(payload_hash) {
Some(bb) => bb,
None => return,
};
let segments: Vec<_> = bb
.env_switches
@@ -280,7 +266,6 @@ 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 }
})
@@ -301,6 +286,6 @@ where
plan.block_hashes_to_seed.sort_unstable_by_key(|(n, _)| *n);
Some(plan)
self.executor_factory.stage_plan(plan);
}
}

View File

@@ -176,7 +176,6 @@ impl BbAddOns {
BasicEngineApiBuilder::default(),
BasicEngineValidatorBuilder::default(),
Default::default(),
Default::default(),
)
}
}

View File

@@ -21,8 +21,6 @@ pub(crate) struct BenchContext {
pub(crate) auth_provider: RootProvider<AnyNetwork>,
/// The block provider is used for block queries.
pub(crate) block_provider: RootProvider<AnyNetwork>,
/// The local regular RPC provider is used for non-authenticated node RPCs like `testing_*`.
pub(crate) local_rpc_provider: RootProvider<AnyNetwork>,
/// The benchmark mode, which defines whether the benchmark should run for a closed or open
/// range of blocks.
pub(crate) benchmark_mode: BenchMode,
@@ -85,11 +83,6 @@ impl BenchContext {
let client = ClientBuilder::default().connect_with(auth_transport).await?;
let auth_provider = RootProvider::<AnyNetwork>::new(client);
let local_rpc_url = Url::parse(&bench_args.local_rpc_url)?;
info!(target: "reth-bench", "Connecting to local regular RPC at {} for testing namespace calls", local_rpc_url);
let local_rpc_provider =
RootProvider::<AnyNetwork>::new(ClientBuilder::default().http(local_rpc_url));
// Computes the block range for the benchmark.
//
// - If `--advance` is provided, fetches the latest block from the engine and sets:
@@ -166,7 +159,6 @@ impl BenchContext {
Ok(Self {
auth_provider,
block_provider,
local_rpc_provider,
benchmark_mode,
next_block,
use_reth_namespace,

View File

@@ -10,7 +10,7 @@ use alloy_eips::{
eip1559::BaseFeeParams,
eip7840::BlobParams,
eip7928::{AccountChanges, BlockAccessList, SlotChanges},
Typed2718,
BlockNumberOrTag, Typed2718,
};
use alloy_primitives::{Bloom, Bytes, B256};
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
@@ -29,14 +29,9 @@ use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
use reth_ethereum_primitives::Receipt;
use reth_primitives_traits::proofs;
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
future::Future,
};
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 {
@@ -674,6 +669,20 @@ 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,
@@ -720,17 +729,6 @@ fn merge_account_changes(existing: &mut AccountChanges, incoming: AccountChanges
existing.balance_changes.extend(incoming.balance_changes);
existing.nonce_changes.extend(incoming.nonce_changes);
existing.code_changes.extend(incoming.code_changes);
// EIP-7928 invariant: a slot must appear in either storage_changes or storage_reads,
// not both. Per-block BALs respect this, but merging blocks can produce a slot
// that is read in one block and changed in another. Without this normalization,
// an empty read entry can shadow the real writes during BAL deserialization,
// making reads of that slot fall through to stale snapshot state.
let written: HashSet<_> =
existing.storage_changes.iter().map(|slot_changes| slot_changes.slot).collect();
existing.storage_reads.retain(|slot| !written.contains(slot));
let mut seen = HashSet::with_capacity(existing.storage_reads.len());
existing.storage_reads.retain(|slot| seen.insert(*slot));
}
fn merge_slot_changes(existing: &mut Vec<SlotChanges>, incoming: Vec<SlotChanges>) {
@@ -850,54 +848,4 @@ mod tests {
assert_eq!(other.address, Address::repeat_byte(0x22));
assert_eq!(other.storage_changes[0].changes[0].block_access_index, 3);
}
#[test]
fn merge_account_changes_normalizes_storage_reads_after_cross_block_merge() {
let address = Address::repeat_byte(0x33);
const A: U256 = U256::from_limbs([1, 0, 0, 0]);
const B: U256 = U256::from_limbs([2, 0, 0, 0]);
const C: U256 = U256::from_limbs([3, 0, 0, 0]);
const D: U256 = U256::from_limbs([4, 0, 0, 0]);
// Each AccountChanges value is valid on its own: storage slots only appear in
// either reads or changes. The invalid read/change overlap is introduced when
// these per-block BAL entries are merged for a standalone big block.
let mut existing = AccountChanges {
address,
storage_changes: vec![SlotChanges::new(A, vec![StorageChange::new(0, U256::from(10))])],
storage_reads: vec![B, C],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// B is read before it is written by the incoming block, and A is written before
// it appears as a read in the incoming block. C is read in both blocks, so the
// merge should also dedupe it. D remains read-only.
let incoming = AccountChanges {
address,
storage_changes: vec![SlotChanges::new(B, vec![StorageChange::new(1, U256::from(20))])],
storage_reads: vec![A, C, D],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
merge_account_changes(&mut existing, incoming);
// Written slots remain represented by storage_changes, while storage_reads only
// keeps unique read-only slots in first-seen order.
assert_eq!(
existing
.storage_changes
.iter()
.map(|slot_changes| slot_changes.slot)
.collect::<Vec<_>>(),
vec![A, B]
);
assert_eq!(existing.storage_reads, vec![C, D]);
assert!(existing.storage_reads.iter().all(|read_slot| {
!existing.storage_changes.iter().any(|slot_changes| slot_changes.slot == *read_slot)
}));
}
}

View File

@@ -1,7 +1,5 @@
//! 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},
@@ -71,21 +69,6 @@ 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::*;

View File

@@ -4,7 +4,7 @@
use crate::{
bench::{
context::BenchContext,
helpers::{fetch_block_access_list, parse_duration},
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
@@ -14,24 +14,14 @@ use crate::{
block_to_new_payload, call_forkchoice_updated_with_reth, call_new_payload_with_reth,
},
};
use alloy_consensus::TxEnvelope;
use alloy_eips::Encodable2718;
use alloy_primitives::B256;
use alloy_provider::{
ext::DebugApi,
network::{AnyNetwork, AnyRpcBlock},
Provider, RootProvider,
};
use alloy_rpc_types_engine::{
ExecutionData, ExecutionPayloadEnvelopeV5, ForkchoiceState, PayloadAttributes,
};
use alloy_provider::{ext::DebugApi, Provider};
use alloy_rpc_types_engine::ForkchoiceState;
use clap::Parser;
use eyre::{bail, ensure, Context, OptionExt};
use eyre::{Context, OptionExt};
use futures::{stream, StreamExt, TryStreamExt};
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_core::args::BenchmarkArgs;
use reth_rpc_api::{RethNewPayloadInput, TestingBuildBlockRequestV1};
use std::time::{Duration, Instant};
use tracing::{debug, info, warn};
@@ -42,22 +32,6 @@ pub struct Command {
#[arg(long, value_name = "RPC_URL", verbatim_doc_comment)]
rpc_url: String,
/// Build a separate fork with `testing_buildBlockV1` and alternate forkchoice updates between
/// the canonical chain and that fork on every block while the fork grows up to the configured
/// depth.
///
/// This requires enabling the hidden `testing` RPC module on the target node,
/// for example with `reth node --http --http.api eth,testing`.
#[arg(
long,
value_name = "DEPTH",
num_args = 0..=1,
default_missing_value = "8",
value_parser = parse_reorg_depth,
verbatim_doc_comment
)]
reorg: Option<usize>,
/// How long to wait after a forkchoice update before sending the next payload.
///
/// Accepts a duration string (e.g. `100ms`, `2s`) or a bare integer treated as
@@ -101,87 +75,22 @@ pub struct Command {
)]
rpc_block_buffer_size: usize,
/// Weather to enable bal by default or not.
#[arg(long, default_value = "false", verbatim_doc_comment)]
enable_bal: bool,
#[command(flatten)]
benchmark: BenchmarkArgs,
}
#[derive(Debug)]
struct PreparedBuiltBlock {
block_hash: B256,
params: serde_json::Value,
}
#[derive(Debug)]
struct QueuedForkBlock {
block_number: u64,
prepared: PreparedBuiltBlock,
}
#[derive(Debug)]
struct ReorgState {
depth: usize,
fork_length: usize,
branch_point_hash: Option<B256>,
fork_parent_hash: Option<B256>,
}
impl ReorgState {
const fn new(depth: usize) -> Self {
Self { depth, fork_length: 0, branch_point_hash: None, fork_parent_hash: None }
}
const fn push_fork_head(&mut self, canonical_parent_hash: B256, fork_head_hash: B256) {
if self.fork_length == 0 {
self.branch_point_hash = Some(canonical_parent_hash);
}
self.fork_length += 1;
self.fork_parent_hash = Some(fork_head_hash);
}
fn forkchoice_state(&self, fork_head_hash: B256) -> eyre::Result<ForkchoiceState> {
let branch_point_hash = self.branch_point_hash.ok_or_eyre("missing reorg branch point")?;
Ok(ForkchoiceState {
head_block_hash: fork_head_hash,
safe_block_hash: branch_point_hash,
finalized_block_hash: branch_point_hash,
})
}
const fn reset(&mut self) {
self.fork_length = 0;
self.branch_point_hash = None;
self.fork_parent_hash = None;
}
}
impl Command {
/// Execute `benchmark new-payload-fcu` command
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
if self.reorg.is_some() && self.benchmark.rlp_blocks {
bail!("--reorg cannot be combined with --rlp-blocks")
}
if self.reorg.is_some() && self.enable_bal {
bail!("--reorg cannot be combined with --enable-bal")
}
// Log mode configuration
if let Some(duration) = self.wait_time {
info!(target: "reth-bench", "Using wait-time mode with {}ms minimum interval between blocks", duration.as_millis());
}
if let Some(depth) = self.reorg {
info!(target: "reth-bench", depth, "Using testing_buildBlockV1 reorg mode");
}
let BenchContext {
benchmark_mode,
block_provider,
auth_provider,
local_rpc_provider,
next_block,
use_reth_namespace,
rlp_blocks,
@@ -269,8 +178,7 @@ impl Command {
let mut blocks_processed = 0u64;
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
let mut reorg_state = self.reorg.map(ReorgState::new);
let mut queued_fork_block = None;
while let Some((block, head, safe, finalized, rlp)) = {
let wait_start = Instant::now();
let result = blocks.try_next().await?;
@@ -280,39 +188,27 @@ impl Command {
let gas_used = block.header.gas_used;
let gas_limit = block.header.gas_limit;
let block_number = block.header.number;
let canonical_parent_hash = block.header.parent_hash;
let transaction_count = block.transactions.len() as u64;
let deferred_branch_start_block = reorg_state
.as_ref()
.filter(|state| state.fork_length == 0 && queued_fork_block.is_none())
.map(|_| block.clone());
let canonical_forkchoice_state = ForkchoiceState {
debug!(target: "reth-bench", ?block_number, "Sending payload");
let forkchoice_state = ForkchoiceState {
head_block_hash: head,
safe_block_hash: safe,
finalized_block_hash: finalized,
};
let bal = if rlp.is_none() &&
(block.header.block_access_list_hash.is_some() || 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,
)?;
debug!(target: "reth-bench", ?block_number, "Sending payload");
let start = Instant::now();
let server_timings =
call_new_payload_with_reth(&auth_provider, version, params).await?;
let np_latency =
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
let new_payload_result = NewPayloadResult {
@@ -333,12 +229,17 @@ impl Command {
};
let fcu_start = Instant::now();
call_forkchoice_updated_with_reth(&auth_provider, version, canonical_forkchoice_state)
.await?;
call_forkchoice_updated_with_reth(&auth_provider, version, forkchoice_state).await?;
let fcu_latency = fcu_start.elapsed();
let total_latency =
if server_timings.is_some() { np_latency + fcu_latency } else { start.elapsed() };
let total_latency = if server_timings.is_some() {
// When using server-side latency for newPayload, derive total from the
// independently measured components to avoid mixing server-side and
// client-side (network-inclusive) timings.
np_latency + fcu_latency
} else {
start.elapsed()
};
let combined_result = CombinedResult {
block_number,
gas_limit,
@@ -348,88 +249,6 @@ impl Command {
total_latency,
};
if let Some(reorg_state) = reorg_state.as_mut() {
if queued_fork_block.is_none() && reorg_state.fork_length == 0 {
// A branch start uses a canonical parent, so it can be built lazily here
// instead of being queued ahead of time.
let block = deferred_branch_start_block
.as_ref()
.ok_or_eyre("missing deferred fork block for reorg branch start")?;
queued_fork_block = Some(QueuedForkBlock {
block_number,
prepared: prepare_built_block(
&local_rpc_provider,
block,
canonical_parent_hash,
no_wait_for_caches,
)
.await?,
});
}
let queued = queued_fork_block
.take()
.ok_or_eyre("missing queued fork block for reorg replay")?;
ensure!(
queued.block_number == block_number,
"queued fork block {} does not match source block {}",
queued.block_number,
block_number
);
let prepared = queued.prepared;
call_new_payload_with_reth(&auth_provider, None, prepared.params).await?;
reorg_state.push_fork_head(canonical_parent_hash, prepared.block_hash);
let forkchoice_state = reorg_state.forkchoice_state(prepared.block_hash)?;
info!(
target: "reth-bench",
block_number,
branch_point = %forkchoice_state.safe_block_hash,
fork_head = %prepared.block_hash,
fork_depth = reorg_state.fork_length,
max_reorg_depth = reorg_state.depth,
"Switching forkchoice to reorg branch"
);
let fcu_start = Instant::now();
call_forkchoice_updated_with_reth(&auth_provider, None, forkchoice_state).await?;
let _fork_fcu_latency = fcu_start.elapsed();
let next_fork_block_number = block_number + 1;
if reorg_state.fork_length < reorg_state.depth {
queued_fork_block = queue_fork_block(
&block_provider,
&local_rpc_provider,
&benchmark_mode,
next_fork_block_number,
Some(prepared.block_hash),
no_wait_for_caches,
)
.await?;
} else {
info!(
target: "reth-bench",
block_number,
reorg_depth = reorg_state.depth,
"Resetting reorg branch after reaching max depth"
);
// `testing_buildBlockV1` resolves the parent from canonical state, so switch
// back to the source chain before reseeding the next queued fork block.
call_forkchoice_updated_with_reth(
&auth_provider,
version,
canonical_forkchoice_state,
)
.await?;
reorg_state.reset();
queued_fork_block = None;
}
}
// Exclude time spent waiting on the block prefetch channel from the benchmark duration.
// We want to measure engine throughput, not RPC fetch latency.
blocks_processed += 1;
@@ -486,155 +305,3 @@ impl Command {
Ok(())
}
}
async fn prepare_built_block(
block_provider: &RootProvider<AnyNetwork>,
block: &AnyRpcBlock,
parent_block_hash: B256,
no_wait_for_caches: bool,
) -> eyre::Result<PreparedBuiltBlock> {
const MAX_BUILD_ATTEMPTS: usize = 10;
const BUILD_RETRY_INTERVAL: Duration = Duration::from_millis(100);
let request = build_block_request(block, parent_block_hash)?;
let built_payload: ExecutionPayloadEnvelopeV5 = {
let mut attempts_remaining = MAX_BUILD_ATTEMPTS;
loop {
match block_provider.client().request("testing_buildBlockV1", [request.clone()]).await {
Ok(payload) => break payload,
Err(err) if attempts_remaining > 1 && is_retryable_build_block_error(&err) => {
warn!(
target: "reth-bench",
block_number = block.header.number,
%parent_block_hash,
attempts_remaining,
error = %err,
"Retrying testing_buildBlockV1 after transient fork build failure"
);
attempts_remaining -= 1;
tokio::time::sleep(BUILD_RETRY_INTERVAL).await;
}
Err(err) => {
return Err(err).wrap_err_with(|| {
format!(
"Failed to build block {} via testing_buildBlockV1",
block.header.number
)
})
}
}
}
};
let payload = &built_payload.execution_payload.payload_inner.payload_inner;
let block_hash = payload.block_hash;
let (payload, sidecar) = built_payload
.into_payload_and_sidecar(block.header.parent_beacon_block_root.unwrap_or_default());
// Fork payloads are built immediately before the next `testing_buildBlockV1` call. Leaving
// reth's default persistence wait enabled here gives the regular RPC side a consistent base
// state for the next synthetic fork block build.
let params = serde_json::to_value((
RethNewPayloadInput::ExecutionData(ExecutionData { payload, sidecar }),
None::<bool>,
no_wait_for_caches.then_some(false),
))?;
Ok(PreparedBuiltBlock { block_hash, params })
}
#[allow(clippy::too_many_arguments)]
async fn queue_fork_block(
block_provider: &RootProvider<AnyNetwork>,
local_rpc_provider: &RootProvider<AnyNetwork>,
benchmark_mode: &crate::bench_mode::BenchMode,
block_number: u64,
parent_block_hash: Option<B256>,
no_wait_for_caches: bool,
) -> eyre::Result<Option<QueuedForkBlock>> {
if !benchmark_mode.contains(block_number) {
return Ok(None)
}
let future_block = block_provider
.get_block_by_number(alloy_eips::BlockNumberOrTag::Number(block_number))
.full()
.await
.wrap_err_with(|| format!("Failed to fetch block by number {block_number}"))?
.ok_or_eyre("Block not found")?;
let parent_block_hash = parent_block_hash.unwrap_or(future_block.header.parent_hash);
Ok(Some(QueuedForkBlock {
block_number,
prepared: prepare_built_block(
local_rpc_provider,
&future_block,
parent_block_hash,
no_wait_for_caches,
)
.await?,
}))
}
fn is_retryable_build_block_error(err: &alloy_transport::TransportError) -> bool {
let message = err.to_string();
message.contains("block not found: hash") ||
message.contains("block hash not found for block number")
}
fn build_block_request(
block: &AnyRpcBlock,
parent_block_hash: B256,
) -> eyre::Result<TestingBuildBlockRequestV1> {
let mut transactions = block
.clone()
.try_into_transactions()
.map_err(|_| eyre::eyre!("Block transactions must be fetched in full for --reorg"))?
.into_iter()
.map(|tx| {
let tx: TxEnvelope =
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type in RPC block"))?;
if tx.is_eip4844() {
return Ok(None)
}
Ok(Some(tx.encoded_2718().into()))
})
.filter_map(|tx| tx.transpose())
.collect::<eyre::Result<Vec<_>>>()?;
// `testing_buildBlockV1` only takes raw transaction bytes, so we exclude blob transactions
// from the synthetic fork blocks rather than trying to reconstruct their sidecars.
// Keep only 90% of the remaining transactions so the alternate branch produces a materially
// different post-state instead of only differing by header data.
let keep = transactions.len().saturating_mul(9) / 10;
transactions.truncate(keep);
let rpc_block = block.clone().into_inner();
Ok(TestingBuildBlockRequestV1 {
parent_block_hash,
payload_attributes: PayloadAttributes {
timestamp: block.header.timestamp,
prev_randao: block.header.mix_hash.unwrap_or_default(),
suggested_fee_recipient: block.header.beneficiary,
withdrawals: rpc_block.withdrawals.map(|withdrawals| withdrawals.into_inner()),
parent_beacon_block_root: block.header.parent_beacon_block_root,
slot_number: block.header.slot_number,
},
transactions,
extra_data: Some(block.header.extra_data.clone()),
})
}
fn parse_reorg_depth(value: &str) -> Result<usize, String> {
let depth = value
.trim()
.parse::<usize>()
.map_err(|_| format!("invalid reorg depth {value:?}, expected a positive integer"))?;
if depth == 0 {
return Err("reorg depth must be greater than 0".to_string())
}
Ok(depth)
}

View File

@@ -3,7 +3,6 @@
use crate::{
bench::{
context::BenchContext,
helpers::fetch_block_access_list,
metrics_scraper::MetricsScraper,
output::{
NewPayloadResult, TotalGasOutput, TotalGasRow, GAS_OUTPUT_SUFFIX,
@@ -54,7 +53,6 @@ impl Command {
rlp_blocks,
wait_for_persistence,
no_wait_for_caches,
..
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
let total_blocks = benchmark_mode.total_blocks();
@@ -71,9 +69,7 @@ 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())
@@ -127,19 +123,12 @@ 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();

View File

@@ -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, ExecutionPayloadEnvelopeV6,
CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV4,
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::V6),
Some(EngineApiMessageVersion::V4),
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 [`ExecutionPayloadEnvelopeV6`] for backwards compatibility.
/// falling back to [`ExecutionPayloadEnvelopeV4`] for backwards compatibility.
fn load_payloads(&self) -> eyre::Result<Vec<LoadedPayload>> {
let mut payloads = Vec::new();
@@ -450,11 +450,12 @@ 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 {
let rest = name_str.strip_prefix("big_block_")?;
} else if let Some(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()))
})
@@ -480,10 +481,10 @@ impl Command {
{
(big_block.execution_data, big_block.big_block_data, big_block.block_access_list)
} else {
let envelope: ExecutionPayloadEnvelopeV6 = serde_json::from_str(&content)
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
let execution_data = ExecutionData {
payload: envelope.execution_payload.clone().into(),
payload: envelope.envelope_inner.execution_payload.clone().into(),
sidecar: ExecutionPayloadSidecar::v4(
CancunPayloadFields {
versioned_hashes: Vec::new(),

View File

@@ -1,8 +1,6 @@
use alloy_eips::eip4895::Withdrawal;
use alloy_primitives::{Address, Bloom, Bytes, B256, U256};
use alloy_rpc_types_engine::{
ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ExecutionPayloadV4,
};
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
/// Configuration for invalidating payload fields
#[derive(Debug, Default)]
@@ -23,7 +21,6 @@ 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,
@@ -38,8 +35,6 @@ 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 {
@@ -221,30 +216,4 @@ 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
}
}

View File

@@ -1,18 +1,12 @@
//! 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, Bytes, B256};
use alloy_provider::{
network::{AnyNetwork, AnyRpcBlock},
RootProvider,
};
use alloy_primitives::{Address, B256};
use alloy_provider::network::AnyRpcBlock;
use alloy_rpc_types_engine::ExecutionPayload;
use clap::Parser;
use eyre::{OptionExt, Result};
@@ -111,9 +105,6 @@ 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")]
@@ -167,14 +158,6 @@ 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")]
@@ -216,7 +199,6 @@ 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,
@@ -229,8 +211,6 @@ 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,
}
}
@@ -254,21 +234,15 @@ 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 = 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 mut execution_payload = 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_v4(p),
ExecutionPayload::V4(p) => config.apply_to_payload_v3(&mut p.payload_inner),
};
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
@@ -338,13 +312,7 @@ impl Command {
match self.mode {
Mode::Execute => {
let mut command = std::process::Command::new("cast");
let method = if use_v5 {
"engine_newPayloadV5"
} else if use_v4 {
"engine_newPayloadV4"
} else {
"engine_newPayloadV3"
};
let method = 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);
@@ -385,17 +353,4 @@ 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())
}
}

View File

@@ -1,11 +1,6 @@
use super::helpers::{fetch_block_access_list, load_jwt_secret, read_input};
use super::helpers::{load_jwt_secret, read_input};
use alloy_consensus::TxEnvelope;
use alloy_primitives::Bytes;
use alloy_provider::{
network::{AnyNetwork, AnyRpcBlock},
RootProvider,
};
use alloy_rpc_client::ClientBuilder;
use alloy_provider::network::AnyRpcBlock;
use alloy_rpc_types_engine::ExecutionPayload;
use clap::Parser;
use eyre::{OptionExt, Result};
@@ -74,9 +69,6 @@ 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;
@@ -84,14 +76,10 @@ impl Command {
let blob_versioned_hashes =
block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
// 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
};
// Convert to execution payload
let execution_payload = ExecutionPayload::from_block_slow(&block).0;
let use_v4 = block.header.requests_hash.is_some();
// Create JSON request data
let json_request = if use_v4 {
@@ -114,13 +102,7 @@ impl Command {
Mode::Execute => {
// Create cast command
let mut command = std::process::Command::new("cast");
let method = if use_v5 {
"engine_newPayloadV5"
} else if use_v4 {
"engine_newPayloadV4"
} else {
"engine_newPayloadV3"
};
let method = 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);
@@ -164,17 +146,4 @@ 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())
}
}

View File

@@ -3,7 +3,6 @@
//! 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::{
@@ -44,14 +43,6 @@ 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]
@@ -171,40 +162,6 @@ 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`].
@@ -219,7 +176,6 @@ 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);
@@ -242,11 +198,7 @@ pub(crate) fn block_to_new_payload(
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
})?
.into_consensus();
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 (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
let (version, params, execution_data) = payload_to_new_payload(payload, sidecar, None)?;
if reth_new_payload {
@@ -275,22 +227,6 @@ 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()
@@ -330,6 +266,22 @@ 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))
@@ -434,10 +386,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 => {

View File

@@ -18,7 +18,7 @@ reth-errors.workspace = true
reth-execution-types.workspace = true
reth-metrics.workspace = true
reth-ethereum-primitives.workspace = true
reth-primitives-traits = { workspace = true, features = ["dashmap"] }
reth-primitives-traits.workspace = true
reth-storage-api.workspace = true
reth-trie.workspace = true

View File

@@ -4,32 +4,26 @@
//! lazily on first access. This allows execution to start before the trie overlay
//! is fully computed.
use crate::{EthPrimitives, ExecutedBlock};
use crate::DeferredTrieData;
use alloy_primitives::B256;
use reth_primitives_traits::{
dashmap::{self, DashMap},
AlloyBlockHeader, NodePrimitives,
};
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
use std::sync::Arc;
use std::sync::{Arc, OnceLock};
use tracing::{debug, trace};
/// Inputs captured for lazy overlay computation.
#[derive(Clone)]
struct LazyOverlayInputs<N: NodePrimitives = EthPrimitives> {
/// In-memory blocks from tip to anchor child.
///
/// Blocks must be provided in reverse chain order (newest to oldest).
blocks: Vec<ExecutedBlock<N>>,
struct LazyOverlayInputs {
/// The persisted ancestor hash (anchor) this overlay should be built on.
anchor_hash: B256,
/// Deferred trie data handles for all in-memory blocks (newest to oldest).
blocks: Vec<DeferredTrieData>,
}
/// Lazily computed trie overlay.
///
/// Captures the inputs needed to compute a [`TrieInputSorted`] and defers the actual
/// computation until first access.
///
/// Blocks must be provided in reverse chain order (newest to oldest), so the first block is the
/// chain tip and the last block is the oldest in-memory block in the chain segment.
/// computation until first access. This is conceptually similar to [`DeferredTrieData`]
/// but for overlay computation.
///
/// # Fast Path vs Slow Path
///
@@ -37,41 +31,37 @@ struct LazyOverlayInputs<N: NodePrimitives = EthPrimitives> {
/// matches our expected anchor, we can reuse it directly (O(1)).
/// - **Slow path**: Otherwise, we merge all ancestor blocks' trie data into a new overlay.
#[derive(Clone)]
pub struct LazyOverlay<N: NodePrimitives = EthPrimitives> {
/// Computed results, cached by requested anchor hash.
inner: Arc<DashMap<B256, Arc<TrieInputSorted>>>,
pub struct LazyOverlay {
/// Computed result, cached after first access.
inner: Arc<OnceLock<TrieInputSorted>>,
/// Inputs for lazy computation.
inputs: LazyOverlayInputs<N>,
inputs: LazyOverlayInputs,
}
impl<N: NodePrimitives> std::fmt::Debug for LazyOverlay<N> {
impl std::fmt::Debug for LazyOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyOverlay")
.field(
"oldest_block_parent_hash",
&self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash()),
)
.field("anchor_hash", &self.inputs.anchor_hash)
.field("num_blocks", &self.inputs.blocks.len())
.field("cached_anchors", &self.inner.len())
.field("computed", &self.inner.get().is_some())
.finish()
}
}
impl<N: NodePrimitives> LazyOverlay<N> {
/// Create a new lazy overlay from in-memory blocks.
impl LazyOverlay {
/// Create a new lazy overlay with the given anchor hash and block handles.
///
/// # Arguments
///
/// * `blocks` - Executed blocks in reverse chain order (newest to oldest)
pub fn new(blocks: Vec<ExecutedBlock<N>>) -> Self {
debug_assert!(
blocks.windows(2).all(|window| {
window[0].recovered_block().parent_hash() == window[1].recovered_block().hash()
}),
"LazyOverlay blocks must be ordered newest to oldest along a single chain"
);
/// * `anchor_hash` - The persisted ancestor hash this overlay is built on top of
/// * `blocks` - Deferred trie data handles for in-memory blocks (newest to oldest)
pub fn new(anchor_hash: B256, blocks: Vec<DeferredTrieData>) -> Self {
Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { anchor_hash, blocks } }
}
Self { inner: Default::default(), inputs: LazyOverlayInputs { blocks } }
/// Returns the anchor hash this overlay is built on.
pub const fn anchor_hash(&self) -> B256 {
self.inputs.anchor_hash
}
/// Returns the number of in-memory blocks this overlay covers.
@@ -79,75 +69,43 @@ impl<N: NodePrimitives> LazyOverlay<N> {
self.inputs.blocks.len()
}
/// Returns the oldest anchor hash this overlay can serve.
///
/// This is the parent hash of the oldest block in the stored newest-to-oldest chain segment.
pub fn anchor_hash(&self) -> Option<B256> {
self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash())
/// Returns true if the overlay has already been computed.
pub fn is_computed(&self) -> bool {
self.inner.get().is_some()
}
/// Returns true if there are no blocks in the overlay, or if one of the blocks has the given
/// hash as a parent hash.
pub fn has_anchor_hash(&self, hash: B256) -> bool {
self.inputs.blocks.is_empty() ||
self.inputs.blocks.iter().any(|b| b.recovered_block().parent_hash() == hash)
}
#[cfg(test)]
/// Returns true if the overlay has already been computed for the requested anchor.
pub fn is_computed(&self, anchor_hash: B256) -> bool {
self.inner.contains_key(&anchor_hash)
}
/// Returns the computed trie input for the requested anchor, computing it if necessary.
/// Returns the computed trie input, computing it if necessary.
///
/// The first call triggers computation (which may block waiting for deferred data).
/// Subsequent calls for the same anchor return the cached result immediately.
pub fn get(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
match self.inner.entry(anchor_hash) {
dashmap::Entry::Occupied(entry) => Arc::clone(entry.get()),
dashmap::Entry::Vacant(entry) => {
let input = self.compute(anchor_hash);
entry.insert(Arc::clone(&input));
input
}
}
/// Subsequent calls return the cached result immediately.
pub fn get(&self) -> &TrieInputSorted {
self.inner.get_or_init(|| self.compute())
}
/// Returns the overlay as (nodes, state) tuple for use with `OverlayStateProviderFactory`.
pub fn as_overlay(
&self,
anchor_hash: B256,
) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
let input = self.get(anchor_hash);
pub fn as_overlay(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
let input = self.get();
(Arc::clone(&input.nodes), Arc::clone(&input.state))
}
/// Compute the trie input overlay.
fn compute(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
fn compute(&self) -> TrieInputSorted {
let anchor_hash = self.inputs.anchor_hash;
let blocks = &self.inputs.blocks;
if blocks.is_empty() {
return Default::default()
debug!(target: "chain_state::lazy_overlay", "No in-memory blocks, returning empty overlay");
return TrieInputSorted::default();
}
let Some(last_index) =
blocks.iter().position(|block| block.recovered_block().parent_hash() == anchor_hash)
else {
panic!(
"LazyOverlay does not contain a block whose parent hash matches requested anchor {anchor_hash}"
);
};
let blocks = &blocks[..=last_index];
// Fast path: Check if tip block's overlay is ready and anchor matches.
// The tip block (first in list) has the cumulative overlay from all ancestors up to the
// requested anchor.
// The tip block (first in list) has the cumulative overlay from all ancestors.
if let Some(tip) = blocks.first() {
let data = tip.trie_data();
let data = tip.wait_cloned();
if let Some(anchored) = &data.anchored_trie_input {
if anchored.anchor_hash == anchor_hash {
trace!(target: "chain_state::lazy_overlay", %anchor_hash, "Reusing tip block's cached overlay (fast path)");
return Arc::clone(&anchored.trie_input);
return (*anchored.trie_input).clone();
}
debug!(
target: "chain_state::lazy_overlay",
@@ -158,30 +116,23 @@ impl<N: NodePrimitives> LazyOverlay<N> {
}
}
// Slow path: Merge the prefix of blocks from the tip back to the requested anchor.
debug!(
target: "chain_state::lazy_overlay",
%anchor_hash,
num_blocks = blocks.len(),
"Merging blocks (slow path)"
);
Arc::new(Self::merge_blocks(blocks))
// Slow path: Merge all blocks' trie data into a new overlay.
debug!(target: "chain_state::lazy_overlay", num_blocks = blocks.len(), "Merging blocks (slow path)");
Self::merge_blocks(blocks)
}
/// Merge all blocks' trie data into a single [`TrieInputSorted`].
///
/// Blocks are ordered newest to oldest.
fn merge_blocks(blocks: &[ExecutedBlock<N>]) -> TrieInputSorted {
fn merge_blocks(blocks: &[DeferredTrieData]) -> TrieInputSorted {
if blocks.is_empty() {
return TrieInputSorted::default();
}
let state = HashedPostStateSorted::merge_batch(
blocks.iter().map(|block| block.trie_data().hashed_state),
);
let nodes = TrieUpdatesSorted::merge_batch(
blocks.iter().map(|block| block.trie_data().trie_updates),
);
let state =
HashedPostStateSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().hashed_state));
let nodes =
TrieUpdatesSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().trie_updates));
TrieInputSorted { state, nodes, prefix_sets: Default::default() }
}
@@ -190,138 +141,46 @@ impl<N: NodePrimitives> LazyOverlay<N> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_utils::TestBlockBuilder, ComputedTrieData, EthPrimitives, ExecutedBlock};
use alloy_primitives::U256;
use reth_primitives_traits::Account;
use reth_trie::{updates::TrieUpdatesSorted, HashedPostState, HashedStorage};
use std::sync::Arc;
use reth_trie::{updates::TrieUpdates, HashedPostState};
fn with_unique_state(
block: &ExecutedBlock<EthPrimitives>,
id: u8,
) -> ExecutedBlock<EthPrimitives> {
let hashed_address = B256::with_last_byte(id);
let hashed_slot = B256::with_last_byte(id.saturating_add(32));
let hashed_state = HashedPostState::default()
.with_accounts([(hashed_address, Some(Account::default()))])
.with_storages([(
hashed_address,
HashedStorage::from_iter(false, [(hashed_slot, U256::from(id))]),
)])
.into_sorted();
ExecutedBlock::new(
Arc::clone(&block.recovered_block),
Arc::clone(&block.execution_output),
ComputedTrieData::without_trie_input(
Arc::new(hashed_state),
Arc::new(TrieUpdatesSorted::default()),
),
fn empty_deferred(anchor: B256) -> DeferredTrieData {
DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor,
Vec::new(),
)
}
fn test_blocks() -> Vec<ExecutedBlock<EthPrimitives>> {
TestBlockBuilder::eth()
.get_executed_blocks(1..4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.enumerate()
.map(|(index, block)| with_unique_state(&block, index as u8 + 1))
.collect()
#[test]
fn empty_blocks_returns_default() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
let result = overlay.get();
assert!(result.state.is_empty());
assert!(result.nodes.is_empty());
}
#[test]
fn single_block_uses_data_directly() {
let block = TestBlockBuilder::eth().get_executed_block_with_number(1, B256::random());
let anchor_hash = block.recovered_block().parent_hash();
let overlay = LazyOverlay::new(vec![block]);
let anchor = B256::random();
let deferred = empty_deferred(anchor);
let overlay = LazyOverlay::new(anchor, vec![deferred]);
assert!(!overlay.is_computed(anchor_hash));
let _ = overlay.get(anchor_hash);
assert!(overlay.is_computed(anchor_hash));
assert!(!overlay.is_computed());
let _ = overlay.get();
assert!(overlay.is_computed());
}
#[test]
fn caches_results_per_anchor() {
let blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let full_anchor = blocks[2].recovered_block().parent_hash();
let overlay = LazyOverlay::new(blocks);
fn cached_after_first_access() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
let prefix = overlay.get(prefix_anchor);
let full = overlay.get(full_anchor);
// First access computes
let _ = overlay.get();
assert!(overlay.is_computed());
assert!(overlay.is_computed(prefix_anchor));
assert!(overlay.is_computed(full_anchor));
assert!(!Arc::ptr_eq(&prefix, &full));
assert!(Arc::ptr_eq(&prefix, &overlay.get(prefix_anchor)));
assert!(Arc::ptr_eq(&full, &overlay.get(full_anchor)));
}
#[test]
fn requested_anchor_limits_the_merged_prefix() {
let blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let expected = LazyOverlay::merge_blocks(&blocks[..2]);
let overlay = LazyOverlay::new(blocks);
let actual = overlay.get(prefix_anchor);
assert_eq!(actual.nodes.as_ref(), expected.nodes.as_ref());
assert_eq!(actual.state.as_ref(), expected.state.as_ref());
}
#[test]
fn anchor_hash_returns_oldest_served_anchor() {
let blocks = test_blocks();
let expected_anchor = blocks.last().unwrap().recovered_block().parent_hash();
let overlay = LazyOverlay::new(blocks);
assert_eq!(overlay.anchor_hash(), Some(expected_anchor));
}
#[test]
fn reuses_tip_overlay_when_anchor_matches() {
let mut blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let tip_overlay = Arc::new(LazyOverlay::merge_blocks(&blocks[..2]));
let tip_data = blocks[0].trie_data();
blocks[0] = ExecutedBlock::new(
Arc::clone(&blocks[0].recovered_block),
Arc::clone(&blocks[0].execution_output),
ComputedTrieData::with_trie_input(
tip_data.hashed_state,
tip_data.trie_updates,
prefix_anchor,
Arc::clone(&tip_overlay),
),
);
let overlay = LazyOverlay::new(blocks);
let actual = overlay.get(prefix_anchor);
assert!(Arc::ptr_eq(&actual, &tip_overlay));
}
#[test]
#[should_panic(
expected = "LazyOverlay does not contain a block whose parent hash matches requested anchor"
)]
fn missing_anchor_panics() {
let blocks = test_blocks();
let missing_anchor = blocks[0].recovered_block().hash();
let overlay = LazyOverlay::new(blocks);
let _ = overlay.get(missing_anchor);
}
#[test]
#[should_panic(
expected = "LazyOverlay blocks must be ordered newest to oldest along a single chain"
)]
fn misordered_blocks_panic() {
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
let _ = LazyOverlay::new(blocks);
// Second access uses cache
let _ = overlay.get();
assert!(overlay.is_computed());
}
}

View File

@@ -150,22 +150,16 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
// commands can proceed.
debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
reth_fs_util::create_dir_all(&rocksdb_path)?;
let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level);
if let Some(cache_size) = self.db.rocksdb_block_cache_size {
builder = builder.with_block_cache_size(cache_size);
}
builder.build()?
} else {
let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.with_read_only(!access.is_read_write());
if let Some(cache_size) = self.db.rocksdb_block_cache_size {
builder = builder.with_block_cache_size(cache_size);
}
builder.build()?
.build()?
} else {
RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.with_read_only(!access.is_read_write())
.build()?
};
let provider_factory =

View File

@@ -1,4 +1,4 @@
use alloy_primitives::{hex, Address, BlockHash, B256};
use alloy_primitives::{hex, BlockHash};
use clap::Parser;
use reth_db::{
static_file::{
@@ -10,20 +10,16 @@ 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, TableViewer,
RawKey, RawTable, Receipts, TableViewer, Transactions,
};
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, RocksDBProviderFactory,
StaticFileProviderFactory,
};
use reth_provider::{providers::ProviderNodeTypes, ChangeSetReader, StaticFileProviderFactory};
use reth_static_file_types::StaticFileSegment;
use reth_storage_api::StorageChangeSetReader;
use tracing::error;
@@ -77,55 +73,6 @@ 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 {
@@ -135,9 +82,6 @@ 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 =
@@ -264,12 +208,15 @@ impl Command {
);
}
StaticFileSegment::Transactions => {
let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
let transaction = <<Transactions as Table>::Value>::decompress(
content[0].as_slice(),
)?;
println!("{}", serde_json::to_string_pretty(&transaction)?);
}
StaticFileSegment::Receipts => {
let receipt =
ReceiptTy::<N>::decompress(content[0].as_slice())?;
let receipt = <<Receipts as Table>::Value>::decompress(
content[0].as_slice(),
)?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
}
StaticFileSegment::TransactionSenders => {
@@ -299,208 +246,6 @@ 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))

View File

@@ -1,386 +0,0 @@
//! `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 alloy_primitives::Address;
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);
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
let mut writer = sf_provider.latest_writer(StaticFileSegment::AccountChangeSets)?;
if first_block > 0 {
writer.ensure_at_block(first_block - 1)?;
}
let mut count = 0u64;
let mut walker = cursor.walk(Some(first_block))?.peekable();
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);
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
let mut writer = sf_provider.latest_writer(StaticFileSegment::StorageChangeSets)?;
if first_block > 0 {
writer.ensure_at_block(first_block - 1)?;
}
let mut count = 0u64;
let mut walker = cursor.walk(Some((first_block, Address::ZERO).into()))?.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));
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
if first_block > 0 {
let mut writer = sf_provider.latest_writer(StaticFileSegment::Receipts)?;
writer.ensure_at_block(first_block - 1)?;
writer.commit()?;
}
let before = sf_provider
.get_highest_static_file_tx(StaticFileSegment::Receipts)
.map_or(0, |tx| tx + 1);
let block_range = first_block..=tip;
let segment = reth_static_file::segments::Receipts;
reth_static_file::segments::Segment::copy_to_static_files(&segment, provider, block_range)?;
sf_provider.commit()?;
let after = sf_provider
.get_highest_static_file_tx(StaticFileSegment::Receipts)
.map_or(0, |tx| tx + 1);
let count = after - before;
info!(target: "reth::cli", count, "Receipts migrated");
Ok(())
}
/// 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(())
}
}

View File

@@ -16,7 +16,6 @@ mod copy;
mod diff;
mod get;
mod list;
mod migrate_v2;
mod prune_checkpoints;
mod repair_trie;
mod settings;
@@ -78,9 +77,6 @@ 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> {
@@ -235,13 +231,6 @@ 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(())

View File

@@ -1,368 +0,0 @@
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),
}
}
}

View File

@@ -270,7 +270,6 @@ 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(),

View File

@@ -1,490 +0,0 @@
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());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,6 @@ 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
@@ -66,12 +62,6 @@ 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>,
@@ -93,12 +83,6 @@ 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.
@@ -117,9 +101,9 @@ pub struct OutputFileChecksum {
pub blake3: String,
}
/// A concrete snapshot archive with its download and verification metadata.
/// A single archive with concrete URL and optional integrity metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotArchive {
pub struct ArchiveDescriptor {
pub url: String,
pub file_name: String,
pub size: u64,
@@ -127,13 +111,6 @@ pub struct SnapshotArchive {
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 {
@@ -338,19 +315,19 @@ impl SnapshotManifest {
}
}
/// Returns concrete snapshot archives for a component, optionally limited to distance.
pub fn snapshot_archives_for_distance(
/// Returns concrete archive descriptors for a component, optionally limited to distance.
pub fn archive_descriptors_for_distance(
&self,
ty: SnapshotComponentType,
distance: Option<u64>,
) -> Vec<SnapshotArchive> {
) -> Vec<ArchiveDescriptor> {
let Some(component) = self.component(ty) else {
return vec![];
};
match component {
ComponentManifest::Single(single) => {
vec![SnapshotArchive {
vec![ArchiveDescriptor {
url: format!("{}/{}", self.base_url_or_empty(), single.file),
file_name: single.file.clone(),
size: single.size,
@@ -380,7 +357,7 @@ impl SnapshotManifest {
let output_files =
chunked.chunk_output_files.get(i as usize).cloned().unwrap_or_default();
SnapshotArchive {
ArchiveDescriptor {
url: format!("{}/{}", self.base_url_or_empty(), file_name),
file_name,
size,
@@ -421,36 +398,6 @@ 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 {
@@ -474,14 +421,6 @@ 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 {
@@ -489,39 +428,6 @@ 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.
@@ -610,10 +516,6 @@ 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,
}),
);
@@ -630,7 +532,6 @@ 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,
}),
@@ -645,7 +546,6 @@ 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,
}),
@@ -914,7 +814,6 @@ mod tests {
ComponentManifest::Single(SingleArchive {
file: "state.tar.zst".to_string(),
size: 100,
decompressed_size: 0,
blake3: None,
output_files: vec![],
}),
@@ -925,7 +824,6 @@ 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![]],
}),
);
@@ -935,7 +833,6 @@ 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![]],
}),
);
@@ -986,7 +883,6 @@ mod tests {
ComponentManifest::Single(SingleArchive {
file: "rocksdb_indices.tar.zst".to_string(),
size: 777,
decompressed_size: 0,
blake3: None,
output_files: vec![],
}),
@@ -1059,7 +955,6 @@ 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],
}),
);
@@ -1102,68 +997,6 @@ 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();
@@ -1172,7 +1005,6 @@ 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(),
@@ -1187,7 +1019,6 @@ 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(),
@@ -1213,13 +1044,13 @@ mod tests {
components,
};
let state = m.snapshot_archives_for_distance(SnapshotComponentType::State, None);
let state = m.archive_descriptors_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.snapshot_archives_for_distance(SnapshotComponentType::Transactions, None);
let tx = m.archive_descriptors_for_distance(SnapshotComponentType::Transactions, None);
assert_eq!(tx.len(), 2);
assert_eq!(tx[0].blake3, None);
assert_eq!(tx[1].blake3, None);
@@ -1242,7 +1073,6 @@ 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());
@@ -1267,7 +1097,6 @@ 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());

View File

@@ -45,7 +45,6 @@ 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,
@@ -89,7 +88,6 @@ 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);
@@ -104,7 +102,6 @@ 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()];
@@ -129,7 +126,6 @@ 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()
@@ -139,7 +135,6 @@ 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)? {
@@ -166,7 +161,6 @@ 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 =
@@ -189,7 +183,6 @@ 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

View File

@@ -1,322 +0,0 @@
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);
}
}

View File

@@ -1,844 +0,0 @@
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
);
}
}

View File

@@ -1,100 +0,0 @@
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
}
}

View File

@@ -1,230 +0,0 @@
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)
}

View File

@@ -1,84 +0,0 @@
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())
}
}

View File

@@ -50,13 +50,8 @@ where
info!(target: "reth::cli", new_tip = ?header.num_hash(), "Setting up dummy EVM chain before importing state.");
let static_file_provider = provider_rw.static_file_provider();
// Write EVM dummy data up to `header - 1` block. Skip when the supplied
// header is at block 0: `header.number() - 1` would underflow in u64 to
// `u64::MAX`, sending `append_dummy_chain` into a 1..=u64::MAX loop that
// exhausts memory before failing.
if header.number() > 0 {
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
}
// Write EVM dummy data up to `header - 1` block
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
info!(target: "reth::cli", "Appending first valid block.");
@@ -196,13 +191,7 @@ mod tests {
use alloy_primitives::{address, b256};
use reth_db_common::init::init_genesis;
use reth_provider::{test_utils::create_test_provider_factory, DatabaseProviderFactory};
use std::{
io::Write,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
@@ -275,45 +264,4 @@ mod tests {
assert_eq!(actual_next_height, expected_next_height);
}
/// Regression: a header at block 0 used to send `append_dummy_chain` into
/// a `1..=u64::MAX` loop because `header.number() - 1` underflowed in
/// u64. The guard `if header.number() > 0` skips the dummy-chain step
/// when there is no pre-genesis range to backfill, so `header_factory`
/// is never invoked.
#[test]
fn test_setup_without_evm_skips_dummy_chain_for_genesis_header() {
let header = Header { number: 0, ..Default::default() };
let header_hash = header.hash_slow();
let provider_factory = create_test_provider_factory();
init_genesis(&provider_factory).unwrap();
let provider_rw = provider_factory.database_provider_rw().unwrap();
let factory_calls = Arc::new(AtomicU64::new(0));
let factory_calls_inner = Arc::clone(&factory_calls);
// The Result of `setup_without_evm` itself is not asserted: with
// `number == 0` plus a genesis already written by `init_genesis`,
// the subsequent `append_first_block` may legitimately fail. The
// bug under test is the OOM in the dummy-chain loop, observable
// through the factory-call counter below.
let _ = setup_without_evm(
&provider_rw,
SealedHeader::new(header, header_hash),
move |number| {
// Bound calls so a regression cannot exhaust the test
// runner's memory; the only correct value here is 0.
let n = factory_calls_inner.fetch_add(1, Ordering::Relaxed);
assert!(n < 8, "header_factory must not be invoked for a genesis-block header");
Header { number, ..Default::default() }
},
);
assert_eq!(
factory_calls.load(Ordering::Relaxed),
0,
"append_dummy_chain must be skipped when header.number() == 0"
);
}
}

View File

@@ -188,7 +188,7 @@ impl<C: ChainSpecParser> DownloadArgs<C> {
)
}
config.peers.trusted_nodes_only |= self.network.trusted_only;
config.peers.trusted_nodes_only = self.network.trusted_only;
let default_secret_key_path = data_dir.p2p_secret();
let p2p_secret_key = self.network.secret_key(default_secret_key_path)?;

View File

@@ -5,7 +5,6 @@ use crate::common::{
EnvironmentArgs,
};
use alloy_consensus::{transaction::TxHashRef, BlockHeader, TxReceipt};
use alloy_primitives::{Address, B256, U256};
use clap::Parser;
use eyre::WrapErr;
use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
@@ -13,19 +12,14 @@ use reth_cli::chainspec::ChainSpecParser;
use reth_cli_util::cancellation::CancellationToken;
use reth_consensus::FullConsensus;
use reth_evm::{execute::Executor, ConfigureEvm};
use reth_primitives_traits::{format_gas_throughput, Account, BlockBody, GotExpected};
use reth_primitives_traits::{format_gas_throughput, BlockBody, GotExpected};
use reth_provider::{
BlockNumReader, BlockReader, ChainSpecProvider, DatabaseProviderFactory, ReceiptProvider,
StaticFileProviderFactory, TransactionVariant,
};
use reth_revm::{
database::StateProviderDatabase,
db::{states::reverts::AccountInfoRevert, BundleState},
};
use reth_revm::database::StateProviderDatabase;
use reth_stages::stages::calculate_gas_used_from_headers;
use reth_storage_api::{ChangeSetReader, DBProvider, StorageChangeSetReader};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
@@ -74,18 +68,13 @@ impl<C: ChainSpecParser> Command<C> {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
/// Execute `re-execute` command
pub async fn execute<N>(
mut self,
self,
components: impl CliComponentsBuilder<N>,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
{
// Default to 4GB RocksDB block cache for re-execute unless explicitly set.
if self.env.db.rocksdb_block_cache_size.is_none() {
self.env.db.rocksdb_block_cache_size = Some(4 << 30);
}
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
let components = components(provider_factory.chain_spec());
@@ -119,6 +108,15 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
min_block..=max_block,
)?;
let db_at = {
let provider_factory = provider_factory.clone();
move |block_number: u64| {
StateProviderDatabase(
provider_factory.history_by_block_number(block_number).unwrap(),
)
}
};
let skip_invalid_blocks = self.skip_invalid_blocks;
let blocks_per_chunk = self.blocks_per_chunk;
let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
@@ -134,23 +132,13 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
let provider_factory = provider_factory.clone();
let evm_config = components.evm_config().clone();
let consensus = components.consensus().clone();
let db_at = db_at.clone();
let stats_tx = stats_tx.clone();
let info_tx = info_tx.clone();
let cancellation = cancellation.clone();
let next_block = Arc::clone(&next_block);
tasks.spawn_blocking(move || {
let executor_lifetime = Duration::from_secs(600);
let provider = provider_factory.database_provider_ro()?.disable_long_read_transaction_safety();
let db_at = {
|block_number: u64| {
StateProviderDatabase(
provider
.history_by_block_number(block_number)
.unwrap(),
)
}
};
let executor_lifetime = Duration::from_secs(120);
loop {
if cancellation.is_cancelled() {
@@ -191,10 +179,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
}
};
let bal= executor.take_bal();
if let Err(err) = consensus
.validate_block_post_execution(&block, &result, None,bal)
.validate_block_post_execution(&block, &result, None)
.wrap_err_with(|| {
format!(
"Failed to validate block {} {}",
@@ -259,31 +245,14 @@ 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() > 5_000_000 ||
if executor.size_hint() > 1_000_000 ||
executor_created.elapsed() > executor_lifetime
{
let last_block = block.number();
let old_executor = std::mem::replace(
&mut executor,
evm_config.batch_executor(db_at(last_block)),
);
let bundle = old_executor.into_state().take_bundle();
verify_bundle_against_changesets(
&provider,
&bundle,
last_block,
)?;
executor =
evm_config.batch_executor(db_at(block.number()));
executor_created = Instant::now();
}
}
// Full verification at chunk end for remaining unverified blocks
let bundle = executor.into_state().take_bundle();
verify_bundle_against_changesets(
&provider,
&bundle,
chunk_end - 1,
)?;
}
eyre::Ok(())
@@ -364,98 +333,3 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
Ok(())
}
}
/// Verifies reverts against database changesets.
///
/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
/// (from DB storage wipe) absent from reverts.
fn verify_bundle_against_changesets<P>(
provider: &P,
bundle: &BundleState,
last_block: u64,
) -> eyre::Result<()>
where
P: ChangeSetReader + StorageChangeSetReader,
{
// Verify reverts against changesets per block
for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
let block_number = last_block - i as u64;
let mut cs_accounts: HashMap<Address, Option<Account>> = provider
.account_block_changeset(block_number)?
.into_iter()
.map(|cs| (cs.address, cs.info))
.collect();
let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
for (bna, entry) in provider.storage_changeset(block_number)? {
cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
}
for (addr, revert) in block_reverts {
// Verify account info
match &revert.account {
AccountInfoRevert::DoNothing => {
eyre::ensure!(
!cs_accounts.contains_key(addr),
"Block {block_number}: account {addr} in changeset but revert is DoNothing",
);
}
AccountInfoRevert::DeleteIt => {
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
})?;
eyre::ensure!(
cs_info.is_none(),
"Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
);
}
AccountInfoRevert::RevertTo(info) => {
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
})?;
let revert_acct = Some(Account::from(info));
eyre::ensure!(
revert_acct == cs_info,
"Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
);
}
}
// Verify storage slots — remove matched changeset entries as we go
let mut cs_slots = cs_storage.get_mut(addr);
for (slot_key, revert_slot) in &revert.storage {
let b256_key = B256::from(*slot_key);
match cs_slots.as_mut().and_then(|s| s.remove(&b256_key)) {
Some(cs_value) => eyre::ensure!(
revert_slot.to_previous_value() == cs_value,
"Block {block_number}: {addr} slot {b256_key} mismatch: \
revert={} cs={cs_value}",
revert_slot.to_previous_value(),
),
None => eyre::ensure!(
revert.wipe_storage,
"Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
),
}
}
// Any remaining cs_storage slots for this address must be from a destroyed account
if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
eyre::ensure!(
revert.wipe_storage,
"Block {block_number}: {addr} has {} unmatched storage slots in changeset",
remaining.len(),
);
}
}
// Any remaining cs_accounts entries had no corresponding revert
if let Some(addr) = cs_accounts.keys().next() {
eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
}
}
Ok(())
}

View File

@@ -6,7 +6,7 @@ use reth_db_api::{
};
use reth_db_common::DbTool;
use reth_evm::ConfigureEvm;
use reth_node_api::{HeaderTy, TxTy};
use reth_node_api::HeaderTy;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
@@ -88,7 +88,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
)
})??;
output_db.update(|tx| {
tx.import_table_with_range::<tables::BlockOmmers<HeaderTy<N>>, _>(
tx.import_table_with_range::<tables::BlockOmmers, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from),
to,
@@ -110,7 +110,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
})??;
output_db.update(|tx| {
tx.import_table_with_range::<tables::Transactions<TxTy<N>>, _>(
tx.import_table_with_range::<tables::Transactions, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from_tx),
to_tx,

View File

@@ -29,10 +29,7 @@ use execution::dump_execution_stage;
mod merkle;
use merkle::dump_merkle_stage;
/// `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.
/// `reth dump-stage` command
#[derive(Debug, Parser)]
pub struct Command<C: ChainSpecParser> {
#[command(flatten)]
@@ -103,9 +100,8 @@ 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::RW, runtime.clone())?;
self.env.init::<N>(AccessRights::RO, runtime.clone())?;
let tool = DbTool::new(provider_factory)?;
let components = components(tool.chain());
let evm_config = components.evm_config().clone();

View File

@@ -210,7 +210,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
let consensus = Arc::new(components.consensus().clone());
let mut config = config;
config.peers.trusted_nodes_only |= self.network.trusted_only;
config.peers.trusted_nodes_only = self.network.trusted_only;
config.peers.trusted_nodes.extend(self.network.trusted_peers.clone());
let network_secret_path = self

View File

@@ -18,7 +18,6 @@ reth-primitives-traits.workspace = true
# ethereum
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-eip7928.workspace = true
# misc
auto_impl.workspace = true
@@ -30,9 +29,10 @@ std = [
"reth-primitives-traits/std",
"alloy-primitives/std",
"alloy-consensus/std",
"alloy-eip7928/std",
"reth-primitives-traits/std",
"reth-execution-types/std",
"thiserror/std",
"alloy-eip7928/std",
]
test-utils = ["reth-primitives-traits/test-utils"]
test-utils = [
"reth-primitives-traits/test-utils",
]

View File

@@ -30,17 +30,10 @@
extern crate alloc;
use alloc::{
boxed::Box,
fmt::Debug,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
use alloy_consensus::Header;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
use core::{error::Error, fmt::Display};
use core::error::Error;
/// Pre-computed receipt root and logs bloom.
///
@@ -86,7 +79,6 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError>;
}
@@ -112,18 +104,6 @@ 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
@@ -476,72 +456,19 @@ pub enum ConsensusError {
/// EIP-7825: Transaction gas limit exceeds maximum allowed
#[error(transparent)]
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
/// Error when an unexpected block access list cost is encountered.
#[error("block access list exceeds gas limit")]
BlockAccessListExceedsGasLimit,
/// Error when the block access list hash doesn't match the expected value.
#[error("block access list hash mismatch: {0}")]
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
/// Any additional consensus error, for example L2-specific errors.
/// Other, likely an injected L2 error.
#[error("{0}")]
Other(String),
/// Other unspecified error.
#[error(transparent)]
Other(#[from] Arc<dyn Error + Send + Sync>),
Custom(#[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)
}
}
/// Validates the block access list against the gas limit.
///
/// EIP-7925 specifies that the total cost of the block access list items must not exceed
/// the gas limit. Each item costs `ITEM_COST` gas.
pub fn validate_block_access_list_gas(
block_access_list: Option<&alloy_eip7928::BlockAccessList>,
gas_limit: u64,
) -> Result<(), ConsensusError> {
if let Some(bal) = block_access_list {
let bal_items = alloy_eip7928::total_bal_items(bal);
if bal_items > gas_limit / alloy_eip7928::ITEM_COST as u64 {
return Err(ConsensusError::BlockAccessListExceedsGasLimit)
}
}
Ok(())
}
impl From<InvalidTransactionError> for ConsensusError {
@@ -573,10 +500,6 @@ pub struct TxGasLimitTooHighErr {
pub max_allowed: u64,
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
struct MessageError(String);
#[cfg(test)]
mod tests {
use super::*;
@@ -586,31 +509,24 @@ mod tests {
struct CustomL2Error;
#[test]
fn test_other_error_conversion() {
let consensus_err = ConsensusError::other(CustomL2Error);
assert!(matches!(consensus_err, ConsensusError::Other(_)));
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(_)));
}
#[test]
fn test_other_error_display() {
let consensus_err = ConsensusError::other(CustomL2Error);
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
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());
}
}

View File

@@ -20,7 +20,6 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloc::sync::Arc;
use alloy_eip7928::BlockAccessList;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -78,7 +77,6 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloy_eip7928::BlockAccessList;
use core::sync::atomic::{AtomicBool, Ordering};
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -53,7 +52,6 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)

View File

@@ -141,11 +141,6 @@ 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>
@@ -175,16 +170,9 @@ 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));
@@ -250,10 +238,6 @@ 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 {

View File

@@ -57,7 +57,7 @@ where
.chain_spec
.is_cancun_active_at_timestamp(timestamp)
.then(B256::random),
slot_number: self.chain_spec.is_amsterdam_active_at_timestamp(timestamp).then_some(0),
slot_number: None,
}
}
}

View File

@@ -15,9 +15,6 @@ 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;
@@ -105,11 +102,6 @@ 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
@@ -178,23 +170,6 @@ 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.
@@ -214,7 +189,6 @@ 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,
@@ -238,10 +212,6 @@ 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: true,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -257,7 +227,6 @@ 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,
@@ -291,7 +260,6 @@ 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,
@@ -315,10 +283,6 @@ 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: true,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -349,14 +313,6 @@ 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
@@ -487,15 +443,6 @@ 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,
@@ -699,56 +646,6 @@ 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> {

View File

@@ -8,28 +8,25 @@ 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.
///
/// 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(),
}
pub fn new(max_length: u32) -> Self {
Self { headers: LruMap::new(ByLength::new(max_length)), metrics: Default::default() }
}
fn insert_entry(&mut self, hash: B256, header: BlockWithParent) {
@@ -44,7 +41,7 @@ impl InvalidHeaderCache {
{
let entry = self.headers.get(hash)?;
entry.hit_count += 1;
if entry.hit_count < self.hit_eviction_threshold {
if entry.hit_count < INVALID_HEADER_HIT_EVICTION_THRESHOLD {
return Some(entry.header)
}
}
@@ -113,28 +110,17 @@ mod tests {
#[test]
fn test_hit_eviction() {
let hit_eviction_threshold = 3;
let mut cache = InvalidHeaderCache::new(10, hit_eviction_threshold);
let mut cache = InvalidHeaderCache::new(10);
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..hit_eviction_threshold {
for hit in 1..INVALID_HEADER_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);
}
}

View File

@@ -11,7 +11,7 @@ use alloy_primitives::B256;
use alloy_rpc_types_engine::{
ForkchoiceState, PayloadStatus, PayloadStatusEnum, PayloadValidationError,
};
use error::{InsertBlockError, InsertBlockFatalError, InsertBlockValidationError};
use error::{InsertBlockError, InsertBlockFatalError};
use reth_chain_state::{
CanonicalInMemoryState, ComputedTrieData, ExecutedBlock, ExecutionTimingStats,
MemoryOverlayStateProvider, NewCanonicalChain,
@@ -151,15 +151,11 @@ 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_header_hit_eviction_threshold,
),
invalid_headers: InvalidHeaderCache::new(max_invalid_header_cache_length),
buffer: BlockBuffer::new(block_buffer_limit),
tree_state: TreeState::new(canonical_block, engine_kind),
forkchoice_state_tracker: ForkchoiceStateTracker::default(),
@@ -309,9 +305,6 @@ 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,
}
@@ -403,7 +396,6 @@ where
evm_config,
changeset_cache,
execution_timing_stats: HashMap::new(),
building_payload: false,
runtime,
}
}
@@ -440,7 +432,6 @@ 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,
);
@@ -1121,8 +1112,6 @@ 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();
@@ -1511,9 +1500,9 @@ where
// Re-prepare overlay for the current canonical head with the new anchor.
// Spawn a background task to trigger computation so it's ready when the next payload
// arrives.
if let Some(prepared) = self.state.tree_state.prepare_canonical_overlay() {
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
self.runtime.spawn_blocking_named("prepare-overlay", move || {
let _ = prepared.overlay.get(prepared.anchor_hash);
let _ = overlay.get();
});
}
@@ -1704,7 +1693,6 @@ 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,
@@ -1712,6 +1700,12 @@ 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());
@@ -2016,13 +2010,9 @@ where
}
/// Returns true if the canonical chain length minus the last persisted
/// block is greater than or equal to the persistence threshold,
/// backfill is not running, and no payload is currently being built.
/// block is greater than or equal to the persistence threshold and
/// backfill is not running.
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
@@ -3024,22 +3014,8 @@ where
);
let latest_valid_hash = self.latest_valid_hash_for_invalid_payload(block.parent_hash())?;
// keep track of the invalid header unless the consensus impl considers it transient
let is_transient = match &validation_err {
InsertBlockValidationError::Consensus(err) => self.consensus.is_transient_error(err),
_ => false,
};
if is_transient {
warn!(
target: "engine::tree",
invalid_hash=%block.hash(),
invalid_number=block.number(),
%validation_err,
"Skipping invalid header cache insert for transient validation error",
);
} else {
self.state.invalid_headers.insert(block.block_with_parent());
}
// keep track of the invalid header
self.state.invalid_headers.insert(block.block_with_parent());
self.emit_event(EngineApiEvent::BeaconConsensus(ConsensusEngineEvent::InvalidBlock(
Box::new(block),
)));

View File

@@ -7,7 +7,7 @@ use crate::tree::{
CacheWaitDurations, CachedStateMetrics, CachedStateMetricsSource, ExecutionCache,
PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig, WaitForCaches,
};
use alloy_eip7928::bal::DecodedBal;
use alloy_eip7928::BlockAccessList;
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal};
use alloy_primitives::B256;
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
@@ -122,13 +122,6 @@ 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).
#[allow(unused)]
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>
@@ -164,9 +157,6 @@ 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(),
}
}
}
@@ -251,6 +241,7 @@ 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,
@@ -273,14 +264,13 @@ where
halve_workers,
config,
);
// If no BALs are present or we have them explicitly disabled, we use sparse trie task and
// need to send the updates to it via state hook
let install_state_hook = env.decoded_bal.is_none() || self.disable_bal_parallel_state_root;
let install_state_hook = bal.is_none();
let prewarm_handle = self.spawn_caching_with(
env,
prewarm_rx,
provider_builder,
Some(state_root_handle.updates_tx().clone()),
bal,
);
PayloadHandle {
@@ -301,13 +291,14 @@ 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);
let prewarm_handle = self.spawn_caching_with(env, prewarm_rx, provider_builder, None, bal);
PayloadHandle {
state_root_handle: None,
install_state_hook: false,
@@ -465,7 +456,7 @@ where
level = "debug",
target = "engine::tree::payload_processor",
skip_all,
fields(bal=%env.decoded_bal.is_some())
fields(bal=%bal.is_some())
)]
fn spawn_caching_with<P>(
&self,
@@ -473,6 +464,7 @@ 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,
@@ -483,7 +475,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,
@@ -496,8 +488,6 @@ 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(
@@ -506,16 +496,14 @@ where
prewarm_ctx,
to_sparse_trie_task,
);
{
let to_prewarm_task = to_prewarm_task.clone();
let disable_bal_parallel_state_root = self.disable_bal_parallel_state_root;
self.executor.spawn_blocking_named("prewarm", move || {
let mode = if let Some(decoded_bal) =
maybe_decoded_bal.filter(|_| !disable_bal_parallel_state_root)
{
PrewarmMode::BlockAccessList(decoded_bal)
} else if skip_prewarm {
let mode = if skip_prewarm {
PrewarmMode::Skipped
} else if let Some(bal) = bal {
PrewarmMode::BlockAccessList(bal)
} else {
PrewarmMode::Transactions(transactions)
};
@@ -609,7 +597,6 @@ where
proof_worker_handle,
trie_metrics.clone(),
sparse_state_trie,
parent_state_root,
chunk_size,
);
@@ -801,7 +788,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
/// Returns a state hook to stream execution state updates to the sparse trie cache task.
///
/// Returns `None` when BAL-driven hashed state streaming feeds the sparse trie task.
/// Returns `None` when execution should not send state updates, such as BAL-driven execution.
pub fn state_hook(&self) -> Option<impl OnStateHook> {
self.install_state_hook
.then(|| self.state_root_handle.as_ref().map(|handle| handle.state_hook()))
@@ -936,9 +923,6 @@ 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>
@@ -956,7 +940,6 @@ where
transaction_count: 0,
gas_used: 0,
withdrawals: None,
decoded_bal: None,
}
}
}
@@ -973,12 +956,12 @@ mod tests {
use rand::Rng;
use reth_chainspec::ChainSpec;
use reth_db_common::init::init_genesis;
use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
use reth_ethereum_primitives::TransactionSigned;
use reth_evm::OnStateHook;
use reth_evm_ethereum::EthEvmConfig;
use reth_primitives_traits::{Account, Recovered, StorageEntry};
use reth_provider::{
providers::{BlockchainProvider, OverlayBuilder, OverlayStateProviderFactory},
providers::{BlockchainProvider, OverlayStateProviderFactory},
test_utils::create_test_provider_factory_with_chain_spec,
ChainSpecProvider, HashingWriter,
};
@@ -1162,16 +1145,19 @@ mod tests {
}
}
let mut account = revm_state::Account::default();
account.info = AccountInfo {
balance: U256::from(rng.random::<u64>()),
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
account_id: None,
let account = revm_state::Account {
info: AccountInfo {
balance: U256::from(rng.random::<u64>()),
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
account_id: None,
},
original_info: Box::new(AccountInfo::default()),
storage,
status: AccountStatus::Touched,
transaction_id: 0,
};
account.storage = storage;
account.status = AccountStatus::Touched;
state_update.insert(address, account);
}
@@ -1250,11 +1236,9 @@ mod tests {
std::convert::identity,
),
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::<EthPrimitives>::new(genesis_hash, ChangesetCache::new()),
),
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");

View File

@@ -18,7 +18,7 @@ use crate::tree::{
StateProviderBuilder,
};
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::bal::DecodedBal;
use alloy_eip7928::BlockAccessList;
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<DecodedBal>),
BlockAccessList(Arc<BlockAccessList>),
/// Transaction prewarming is skipped (e.g. small blocks where the overhead exceeds the
/// benefit). No workers are spawned.
Skipped,
@@ -331,10 +331,9 @@ where
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn run_bal_prewarm(
&self,
decoded_bal: Arc<DecodedBal>,
bal: Arc<BlockAccessList>,
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);
@@ -356,63 +355,27 @@ where
let parent_span = Span::current();
let prefetch_parent_span = parent_span.clone();
let stream_parent_span = parent_span;
let prefetch_bal = Arc::clone(&decoded_bal);
let stream_bal = Arc::clone(&decoded_bal);
let prefetch_bal = Arc::clone(&bal);
let stream_bal = Arc::clone(&bal);
let (prefetch_tx, prefetch_rx) = oneshot::channel();
let (stream_tx, stream_rx) = oneshot::channel();
if let Some(to_sparse_trie_task) = to_sparse_trie_task {
let stream_ctx = ctx.clone();
executor.bal_streaming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &stream_parent_span,
"bal_hashed_state_stream",
bal_accounts = stream_bal.as_bal().len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
stream_bal.as_bal().par_iter().for_each_init(
|| {
(
stream_ctx.clone(),
None::<Box<dyn AccountReader>>,
provider_parent_span.clone(),
)
},
|(ctx, provider, parent_span), account_changes| {
ctx.send_bal_hashed_state(
parent_span,
provider,
account_changes,
&to_sparse_trie_task,
);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
let _ = stream_tx.send(());
});
} else {
let _ = stream_tx.send(());
}
if ctx.saved_cache.is_some() {
let prefetch_ctx = ctx.clone();
executor.prewarming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &prefetch_parent_span,
"bal_prefetch_storage",
bal_accounts = prefetch_bal.as_bal().len(),
bal_accounts = prefetch_bal.len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
prefetch_bal.as_bal().par_iter().for_each_init(
prefetch_bal.par_iter().for_each_init(
|| {
(
ctx.clone(),
prefetch_ctx.clone(),
None::<CachedStateProvider<reth_provider::StateProviderBox, true>>,
provider_parent_span.clone(),
)
@@ -431,6 +394,36 @@ where
let _ = prefetch_tx.send(());
}
if let Some(to_sparse_trie_task) = to_sparse_trie_task {
executor.bal_streaming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &stream_parent_span,
"bal_hashed_state_stream",
bal_accounts = stream_bal.len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
stream_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(
parent_span,
provider,
account_changes,
&to_sparse_trie_task,
);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
let _ = stream_tx.send(());
});
} else {
let _ = stream_tx.send(());
}
prefetch_rx
.blocking_recv()
.expect("BAL prefetch task dropped without signaling completion");
@@ -543,10 +536,6 @@ 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
@@ -642,9 +631,6 @@ 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;
@@ -757,9 +743,7 @@ where
provider: &mut Option<CachedStateProvider<reth_provider::StateProviderBox, true>>,
account: &alloy_eip7928::AccountChanges,
) {
if self.disable_bal_batch_io ||
(account.storage_changes.is_empty() && account.storage_reads.is_empty())
{
if account.storage_changes.is_empty() && account.storage_reads.is_empty() {
return;
}

View File

@@ -27,9 +27,8 @@ use reth_trie_parallel::{
root::ParallelStateRootError,
};
use reth_trie_sparse::{
errors::{SparseStateTrieErrorKind, SparseTrieErrorKind, SparseTrieResult},
ConfigurableSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
SparseTrie,
errors::SparseTrieResult, ConfigurableSparseTrie, DeferredDrops, LeafUpdate,
RevealableSparseTrie, SparseStateTrie, SparseTrie,
};
use revm_primitives::{hash_map::Entry, B256Map};
use tracing::{debug, debug_span, error, instrument, trace_span};
@@ -47,8 +46,6 @@ 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,
@@ -123,7 +120,6 @@ 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();
@@ -142,7 +138,6 @@ 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(),
@@ -364,25 +359,10 @@ where
debug!(target: "engine::root", "All proofs processed, ending calculation");
let start = Instant::now();
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:?}"
)))
}
};
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:?}"))
})?;
#[cfg(feature = "trie-debug")]
let debug_recorders = self.trie.take_debug_recorders();
@@ -893,13 +873,6 @@ enum SparseTrieTaskMessage {
mod tests {
use super::*;
use alloy_primitives::{keccak256, Address, B256, U256};
use reth_provider::{
providers::{OverlayBuilder, OverlayStateProviderFactory},
test_utils::create_test_provider_factory,
ChainSpecProvider,
};
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::proof_task::ProofTaskCtx;
use reth_trie_sparse::ArenaParallelSparseTrie;
#[test]
@@ -980,49 +953,4 @@ 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 anchor_hash = provider_factory.chain_spec().genesis_hash();
let overlay_factory = OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::<reth_chain_state::EthPrimitives>::new(
anchor_hash,
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");
}
}

View File

@@ -48,10 +48,7 @@ use crate::tree::{
PayloadHandle, StateProviderBuilder, StateProviderDatabase, TreeConfig, WaitForCaches,
};
use alloy_consensus::transaction::{Either, TxHashRef};
use alloy_eip7928::{
bal::{Bal, DecodedBal},
BlockAccessList,
};
use alloy_eip7928::{bal::Bal, BlockAccessList};
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
use alloy_evm::Evm;
use alloy_primitives::{map::B256Set, B256};
@@ -62,9 +59,7 @@ use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptR
use reth_chain_state::{
CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, ExecutionTimingStats, LazyOverlay,
};
use reth_consensus::{
validate_block_access_list_gas, ConsensusError, FullConsensus, ReceiptRootBloom,
};
use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
use reth_engine_primitives::{
ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
};
@@ -82,14 +77,13 @@ use reth_primitives_traits::{
RecoveredBlock, SealedBlock, SealedHeader, SignerRecoverable,
};
use reth_provider::{
providers::{OverlayBuilder, OverlayStateProviderFactory},
BlockExecutionOutput, BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory,
DatabaseProviderROFactory, HashedPostStateProvider, ProviderError, PruneCheckpointReader,
StageCheckpointReader, StateProvider, StateProviderBox, StateProviderFactory, StateReader,
StorageChangeSetReader, StorageSettingsCache,
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockNumReader, BlockReader,
ChangeSetReader, DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
StateProviderFactory, StateReader, StorageChangeSetReader, StorageSettingsCache,
};
use reth_revm::db::{states::bundle_state::BundleRetention, BundleAccount, State};
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState};
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState, StateRoot};
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
use revm_primitives::{Address, KECCAK_EMPTY};
@@ -493,12 +487,6 @@ 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(),
@@ -507,7 +495,6 @@ 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.
@@ -522,25 +509,33 @@ where
// Get an iterator over the transactions in the payload
let txs = self.tx_iterator_for(&input)?;
// Extract the BAL, if valid and available
let block_access_list = ensure_ok!(input
.block_access_list()
.transpose()
// Eventually gets converted to a `InsertBlockErrorKind::Other`
.map_err(Box::<dyn std::error::Error + Send + Sync>::from))
.map(Arc::new);
// Create lazy overlay from ancestors - this doesn't block, allowing execution to start
// before the trie data is ready. The overlay will be computed on first access.
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, ctx.state());
// Create overlay factory for payload processor (StateRootTask path needs it for
// multiproofs)
let provider_factory = self.provider.clone();
let overlay_builder = OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
.with_lazy_overlay(lazy_overlay);
let overlay_factory =
OverlayStateProviderFactory::new(provider_factory.clone(), overlay_builder.clone());
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
// Spawn the appropriate processor based on strategy
let mut handle = ensure_ok!(self.spawn_payload_processor(
env.clone(),
txs,
provider_builder.clone(),
provider_builder,
overlay_factory.clone(),
strategy,
block_access_list,
));
// Create optional cache stats for detailed block logging
@@ -570,7 +565,7 @@ where
// The receipt root task is spawned before execution and receives receipts incrementally
// as transactions complete, allowing parallel computation during execution.
let execute_block_start = Instant::now();
let (output, senders, receipt_root_rx, built_bal) =
let (output, senders, receipt_root_rx) =
match self.execute_block(state_provider, env, &input, &mut handle) {
Ok(output) => output,
Err(err) => return self.handle_execution_error(input, err, &parent_block),
@@ -652,7 +647,6 @@ where
transaction_root,
receipt_root_bloom,
hashed_state,
built_bal
),
block
);
@@ -670,7 +664,7 @@ where
let task_result = ensure_ok_post_block!(
self.await_state_root_with_timeout(
&mut handle,
provider_builder.clone(),
overlay_factory.clone(),
&hashed_state,
),
block
@@ -694,9 +688,7 @@ where
// Compare trie updates with serial computation if configured
if self.config.always_compare_trie_updates() {
let _has_diff = self.compare_trie_updates_with_serial(
provider_builder.clone(),
provider_factory,
overlay_builder,
overlay_factory.clone(),
&hashed_state,
trie_updates.as_ref().clone(),
);
@@ -735,11 +727,7 @@ where
}
StateRootStrategy::Parallel => {
debug!(target: "engine::tree::payload_validator", "Using parallel state root algorithm");
match self.compute_state_root_parallel(
provider_factory,
overlay_builder,
&hashed_state,
) {
match self.compute_state_root_parallel(overlay_factory.clone(), &hashed_state) {
Ok(result) => {
let elapsed = root_time.elapsed();
info!(
@@ -775,9 +763,7 @@ where
}
let (root, updates) = ensure_ok_post_block!(
provider_builder
.build()
.and_then(|provider| Self::compute_state_root_serial(provider, &hashed_state)),
Self::compute_state_root_serial(overlay_factory.clone(), &hashed_state),
block
);
@@ -909,7 +895,6 @@ where
BlockExecutionOutput<N::Receipt>,
Vec<Address>,
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
Option<BlockAccessList>,
),
InsertBlockErrorKind,
>
@@ -917,29 +902,15 @@ where
S: StateProvider + Send,
Err: core::error::Error + Send + Sync + 'static,
V: PayloadValidator<T, Block = N::Block>,
T: PayloadTypes<
BuiltPayload: BuiltPayload<Primitives = N>,
ExecutionData: ExecutionPayload,
>,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
debug!(target: "engine::tree::payload_validator", "Executing block");
if let Some(bal_opt) = input.block_access_list() {
let bal = bal_opt.map_err(BlockExecutionError::other)?;
validate_block_access_list_gas(Some(&bal), input.gas_limit())
.map_err(|e| {
debug!(target: "engine::tree::payload_validator", "BAL is invalid since it contains more items than the gas limit allows");
InsertBlockErrorKind::Consensus(e)
})?
}
let has_bal = input.block_access_list().is_some();
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.with_bal_builder_if(has_bal)
.build()
});
@@ -998,7 +969,6 @@ where
handle.iter_transactions(),
&receipt_tx,
&executed_tx_index,
has_bal,
)?;
drop(receipt_tx);
@@ -1013,11 +983,6 @@ where
debug_span!(target: "engine::tree", "merge_transitions")
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
// Extract the built bal if payload has bal
let built_bal = if has_bal { db.take_built_alloy_bal() } else { None };
tracing::debug!(has_bal = built_bal.is_some(), "Built BAL");
let output = BlockExecutionOutput { result, state: db.take_bundle() };
let execution_duration = execution_start.elapsed();
@@ -1025,7 +990,7 @@ where
self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
Ok((output, senders, result_rx, built_bal))
Ok((output, senders, result_rx))
}
/// Executes transactions and collects senders, streaming receipts to a background task.
@@ -1037,20 +1002,18 @@ where
/// - Collecting transaction senders for later use
///
/// Returns the executor (for finalization) and the collected senders.
fn execute_transactions<'a, E, Tx, InnerTx, Err, DB>(
fn execute_transactions<E, Tx, InnerTx, Err>(
&self,
mut executor: E,
transaction_count: usize,
transactions: impl Iterator<Item = Result<Tx, Err>>,
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
executed_tx_index: &AtomicUsize,
has_bal: bool,
) -> Result<(E, Vec<Address>), BlockExecutionError>
where
E: BlockExecutor<Receipt = N::Receipt, Evm: alloy_evm::Evm<DB = &'a mut State<DB>>>,
E: BlockExecutor<Receipt = N::Receipt>,
Tx: alloy_evm::block::ExecutableTx<E> + alloy_evm::RecoveredTx<InnerTx>,
InnerTx: TxHashRef,
DB: revm::Database + 'a,
Err: core::error::Error + Send + Sync + 'static,
{
let mut senders = Vec::with_capacity(transaction_count);
@@ -1061,11 +1024,6 @@ where
.in_scope(|| executor.apply_pre_execution_changes())?;
self.metrics.record_pre_execution(pre_exec_start.elapsed());
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
// Execute transactions
let exec_span = debug_span!(target: "engine::tree", "execution").entered();
let mut transactions = transactions.into_iter();
@@ -1110,10 +1068,6 @@ where
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
}
}
// Bump BAL index after each transaction (EIP-7928)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
}
drop(exec_span);
@@ -1133,8 +1087,7 @@ where
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn compute_state_root_parallel(
&self,
provider_factory: P,
overlay_builder: OverlayBuilder<N>,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
@@ -1142,24 +1095,34 @@ where
// need to use the prefix sets which were generated from it to indicate to the
// ParallelStateRoot which parts of the trie need to be recomputed.
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
let overlay_factory = OverlayStateProviderFactory::new(
provider_factory,
overlay_builder.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted()),
);
let overlay_factory =
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());
ParallelStateRoot::new(overlay_factory, prefix_sets, self.runtime.clone())
.incremental_root_with_updates()
}
/// Compute state root for the given hashed post state in serial.
///
/// Uses the same provider construction path as main execution and computes the state root and
/// trie updates for this block directly via
/// [`reth_provider::StateRootProvider::state_root_with_updates`].
/// Uses an overlay factory which provides the state of the parent block, along with the
/// [`HashedPostState`] containing the changes of this block, to compute the state root and
/// trie updates for this block.
fn compute_state_root_serial(
state_provider: StateProviderBox,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<(B256, TrieUpdates)> {
state_provider.state_root_with_updates(hashed_state.get().clone())
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// StateRoot which parts of the trie need to be recomputed.
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
let overlay_factory =
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());
let provider = overlay_factory.database_provider_ro()?;
Ok(StateRoot::new(&provider, &provider)
.with_prefix_sets(prefix_sets)
.root_with_updates()?)
}
/// Awaits the state root from the background task, with an optional timeout fallback.
@@ -1184,7 +1147,7 @@ where
fn await_state_root_with_timeout<Tx, Err, R: Send + Sync + 'static>(
&self,
handle: &mut PayloadHandle<Tx, Err, R>,
state_provider_builder: StateProviderBuilder<N, P>,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
let Some(timeout) = self.config.state_root_task_timeout() else {
@@ -1209,11 +1172,10 @@ where
let (seq_tx, seq_rx) =
std::sync::mpsc::channel::<ProviderResult<(B256, TrieUpdates)>>();
let seq_overlay = overlay_factory;
let seq_hashed_state = hashed_state.clone();
self.payload_processor.executor().spawn_blocking_named("serial-root", move || {
let result = state_provider_builder.build().and_then(|provider| {
Self::compute_state_root_serial(provider, &seq_hashed_state)
});
let result = Self::compute_state_root_serial(seq_overlay, &seq_hashed_state);
let _ = seq_tx.send(result);
});
@@ -1277,18 +1239,13 @@ where
/// updates.
fn compare_trie_updates_with_serial(
&self,
state_provider_builder: StateProviderBuilder<N, P>,
provider_factory: P,
overlay_builder: OverlayBuilder<N>,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
debug!(target: "engine::tree::payload_validator", "Comparing trie updates with serial computation");
match state_provider_builder
.build()
.and_then(|provider| Self::compute_state_root_serial(provider, hashed_state))
{
match Self::compute_state_root_serial(overlay_factory.clone(), hashed_state) {
Ok((serial_root, serial_trie_updates)) => {
debug!(
target: "engine::tree::payload_validator",
@@ -1297,8 +1254,6 @@ where
);
// Get a database provider to use as trie cursor factory
let overlay_factory =
OverlayStateProviderFactory::new(provider_factory, overlay_builder);
match overlay_factory.database_provider_ro() {
Ok(provider) => {
match super::trie_updates::compare_trie_updates(
@@ -1397,7 +1352,6 @@ where
transaction_root: Option<B256>,
receipt_root_bloom: Option<ReceiptRootBloom>,
hashed_state: LazyHashedPostState,
built_bal: Option<BlockAccessList>,
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -1424,13 +1378,9 @@ where
let _enter =
debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution")
.entered();
if let Err(err) = self.consensus.validate_block_post_execution(
block,
output,
receipt_root_bloom,
built_bal,
) {
if let Err(err) =
self.consensus.validate_block_post_execution(block, output, receipt_root_bloom)
{
// call post-block hook
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
return Err(err.into())
@@ -1487,8 +1437,9 @@ where
env: ExecutionEnv<Evm>,
txs: T,
provider_builder: StateProviderBuilder<N, P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
overlay_factory: OverlayStateProviderFactory<P>,
strategy: StateRootStrategy,
block_access_list: Option<Arc<BlockAccessList>>,
) -> Result<
PayloadHandle<
impl ExecutableTxFor<Evm> + use<N, P, Evm, V, T>,
@@ -1508,6 +1459,7 @@ where
provider_builder,
overlay_factory,
&self.config,
block_access_list,
);
// record prewarming initialization duration
@@ -1520,8 +1472,12 @@ where
}
StateRootStrategy::Parallel | StateRootStrategy::Synchronous => {
let start = Instant::now();
let handle =
self.payload_processor.spawn_cache_exclusive(env, txs, provider_builder);
let handle = self.payload_processor.spawn_cache_exclusive(
env,
txs,
provider_builder,
block_access_list,
);
// Record prewarming initialization duration
self.metrics
@@ -1607,7 +1563,7 @@ where
fn get_parent_lazy_overlay(
parent_hash: B256,
state: &EngineApiTreeState<N>,
) -> (Option<LazyOverlay<N>>, B256) {
) -> (Option<LazyOverlay>, B256) {
// Get blocks leading to the parent to determine the anchor
let (anchor_hash, blocks) =
state.tree_state.blocks_by_hash(parent_hash).unwrap_or_else(|| (parent_hash, vec![]));
@@ -1635,7 +1591,10 @@ where
"Creating lazy overlay for in-memory blocks"
);
(Some(LazyOverlay::new(blocks)), anchor_hash)
// Extract deferred trie data handles (non-blocking)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
(Some(LazyOverlay::new(anchor_hash, handles)), anchor_hash)
}
/// Spawns a background task to compute and sort trie data for the executed block.
@@ -2067,11 +2026,10 @@ where
state: &EngineApiTreeState<N>,
) -> Option<StateRootHandle> {
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
let overlay_factory = OverlayStateProviderFactory::new(
self.provider.clone(),
OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
.with_lazy_overlay(lazy_overlay),
);
let overlay_factory =
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
Some(self.payload_processor.spawn_state_root(
overlay_factory,
@@ -2152,17 +2110,6 @@ 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
@@ -2195,15 +2142,4 @@ 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(),
}
}
}

View File

@@ -10,7 +10,6 @@ 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;
@@ -88,14 +87,8 @@ impl<S> CacheEntry<S> {
self.output.gas_used
}
/// 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)
fn to_precompile_result(&self) -> PrecompileResult {
Ok(self.output.clone())
}
}
@@ -182,34 +175,22 @@ where
input.gas >= entry.gas_used()
{
self.increment_by_one_precompile_cache_hits();
return entry.to_precompile_result(input.reservoir);
return entry.to_precompile_result()
}
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() => {
// 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();
}
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();
@@ -265,8 +246,6 @@ mod tests {
gas_used: 0,
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: Bytes::default(),
})
})
@@ -280,8 +259,6 @@ mod tests {
gas_used: 50,
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
};
@@ -315,8 +292,6 @@ mod tests {
gas_used: 5000,
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
})
}
@@ -333,8 +308,6 @@ mod tests {
gas_used: 7000,
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
})
}

View File

@@ -6,7 +6,7 @@ use alloy_primitives::{
map::{B256Map, B256Set},
BlockNumber, B256,
};
use reth_chain_state::{EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_chain_state::{DeferredTrieData, EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader};
use std::{
collections::{btree_map, hash_map, BTreeMap, VecDeque},
@@ -43,7 +43,7 @@ pub struct TreeState<N: NodePrimitives = EthPrimitives> {
/// This is optimistically prepared after the canonical head changes, so that
/// the next payload building on the canonical head can use it immediately
/// without recomputing.
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay<N>>,
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay>,
}
impl<N: NodePrimitives> TreeState<N> {
@@ -106,10 +106,10 @@ impl<N: NodePrimitives> TreeState<N> {
/// This should be called after the canonical head changes to optimistically
/// prepare the overlay for the next payload that will likely build on it.
///
/// Returns a clone of the prepared overlay so the caller can spawn a background
/// task to trigger computation via [`LazyOverlay::get`] for the cached anchor.
/// This ensures the overlay is actually computed before the next payload arrives.
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<PreparedCanonicalOverlay<N>> {
/// Returns a clone of the [`LazyOverlay`] so the caller can spawn a background
/// task to trigger computation via [`LazyOverlay::get`]. This ensures the overlay
/// is actually computed before the next payload arrives.
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<LazyOverlay> {
let canonical_hash = self.current_canonical_head.hash;
// Get blocks leading to the canonical head
@@ -119,23 +119,25 @@ impl<N: NodePrimitives> TreeState<N> {
return None;
};
let num_blocks = blocks.len();
let prepared = PreparedCanonicalOverlay {
// Extract deferred trie data handles from blocks (newest to oldest)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
let overlay = LazyOverlay::new(anchor_hash, handles);
self.cached_canonical_overlay = Some(PreparedCanonicalOverlay {
parent_hash: canonical_hash,
overlay: LazyOverlay::new(blocks),
overlay: overlay.clone(),
anchor_hash,
};
self.cached_canonical_overlay = Some(prepared.clone());
});
debug!(
target: "engine::tree",
%canonical_hash,
%anchor_hash,
num_blocks,
num_blocks = blocks.len(),
"Prepared cached canonical overlay"
);
Some(prepared)
Some(overlay)
}
/// Returns the cached overlay if it matches the requested parent hash and anchor.
@@ -146,7 +148,7 @@ impl<N: NodePrimitives> TreeState<N> {
&self,
parent_hash: B256,
expected_anchor: B256,
) -> Option<&PreparedCanonicalOverlay<N>> {
) -> Option<&PreparedCanonicalOverlay> {
self.cached_canonical_overlay.as_ref().filter(|cached| {
cached.parent_hash == parent_hash && cached.anchor_hash == expected_anchor
})
@@ -427,10 +429,10 @@ impl<N: NodePrimitives> TreeState<N> {
/// the next payload (which typically builds on the canonical head) to reuse
/// the pre-computed overlay immediately without re-traversing in-memory blocks.
///
/// The overlay captures executed blocks from all in-memory blocks
/// The overlay captures deferred trie data handles from all in-memory blocks
/// between the canonical head and the persisted anchor. When a new payload
/// arrives building on the canonical head, this cached overlay can be used
/// directly instead of calling `blocks_by_hash` again.
/// directly instead of calling `blocks_by_hash` and collecting handles again.
///
/// # Invalidation
///
@@ -438,16 +440,16 @@ impl<N: NodePrimitives> TreeState<N> {
/// - Persistence completes (anchor changes)
/// - The canonical head changes to a different block
#[derive(Debug, Clone)]
pub struct PreparedCanonicalOverlay<N: NodePrimitives = EthPrimitives> {
pub struct PreparedCanonicalOverlay {
/// The block hash for which this overlay is prepared as a parent.
///
/// When a payload arrives with this parent hash, the overlay can be reused.
pub parent_hash: B256,
/// The pre-computed lazy overlay containing executed blocks for the canonical segment.
/// The pre-computed lazy overlay containing deferred trie data handles.
///
/// This is computed optimistically after `set_canonical_head` so subsequent payloads don't
/// need to walk the in-memory chain again.
pub overlay: LazyOverlay<N>,
/// This is computed optimistically after `set_canonical_head` so subsequent
/// payloads don't need to re-collect the handles.
pub overlay: LazyOverlay,
/// The anchor hash (persisted ancestor) this overlay is based on.
///
/// Used to verify the overlay is still valid (anchor hasn't changed due to persistence).

View File

@@ -184,18 +184,11 @@ 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,
tree_config.invalid_header_hit_eviction_threshold(),
header.num_hash(),
EngineApiKind::Ethereum,
);
let engine_api_tree_state =
EngineApiTreeState::new(10, 10, 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();
@@ -224,7 +217,8 @@ impl TestHarness {
persistence_handle,
PersistenceState { last_persisted_block: BlockNumHash::default(), rx: None },
payload_builder,
tree_config,
// always assume enough parallelism for tests
TreeConfig::default().with_legacy_state_root(false).with_has_enough_parallelism(true),
EngineApiKind::Ethereum,
evm_config,
changeset_cache,

View File

@@ -287,7 +287,7 @@ where
let tx_recovered =
tx.try_into_recovered().map_err(|_| ProviderError::SenderRecoveryError)?;
let gas_used = match builder.execute_transaction(tx_recovered) {
Ok(gas_used) => gas_used.tx_gas_used(),
Ok(gas_used) => gas_used,
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
hash,
error,

View File

@@ -2,8 +2,8 @@
//! and injecting them into era1 files with `Era1Writer`.
use crate::calculate_td_by_number;
use alloy_consensus::{BlockHeader, Sealable, TxReceipt};
use alloy_primitives::{BlockNumber, U256};
use alloy_consensus::BlockHeader;
use alloy_primitives::{BlockNumber, B256, 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,
HeaderRecord, TotalDifficulty, MAX_BLOCKS_PER_ERA1,
TotalDifficulty, MAX_BLOCKS_PER_ERA1,
},
group::{BlockIndex, Era1Id},
},
@@ -139,21 +139,17 @@ where
let headers = provider.headers_range(start_block..=end_block)?;
// 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 }
// 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]]
})
.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();
.unwrap_or([0u8; 4]);
let era1_id =
Era1Id::new(&config.network, start_block, block_count as u32).with_hash(file_hash);
let era1_id = Era1Id::new(&config.network, start_block, block_count as u32)
.with_hash(historical_root);
let era1_id = if config.max_blocks_per_file == MAX_BLOCKS_PER_ERA1 as u64 {
era1_id
@@ -170,6 +166,7 @@ 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;
@@ -181,6 +178,11 @@ 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;
@@ -216,12 +218,10 @@ where
}
}
if blocks_written > 0 {
// 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);
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);
writer.write_accumulator(&accumulator)?;
writer.write_block_index(&block_index)?;
@@ -310,9 +310,7 @@ where
let compressed_header = CompressedHeader::from_header(&header)?;
let compressed_body = CompressedBody::from_body(&body)?;
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)
let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts)
.map_err(|e| eyre!("Failed to compress receipts: {}", e))?;
Ok((compressed_header, compressed_body, compressed_receipts))

View File

@@ -24,7 +24,6 @@ snap.workspace = true
# ssz encoding and decoding
ethereum_ssz.workspace = true
ethereum_ssz_derive.workspace = true
sha2.workspace = true
[dev-dependencies]
eyre.workspace = true

View File

@@ -76,7 +76,6 @@ 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},
@@ -494,73 +493,6 @@ 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
@@ -759,44 +691,6 @@ 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();

View File

@@ -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 accumulator root hash.
/// Optional hash identifier for this file
/// First 4 bytes of the last historical root in the last state in the era file
pub hash: Option<[u8; 4]>,
/// Whether to include era count in filename

Some files were not shown because too many files have changed in this diff Show More