mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
1 Commits
dan/static
...
yk/pb-cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
815efc5927 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
reth-cli-commands: patch
|
||||
---
|
||||
|
||||
Added `snapshot_api_url` field to `DownloadDefaults` so downstream projects can override the snapshot discovery API endpoint. Previously, `discover_manifest_url`, `fetch_snapshot_api_entries`, and `print_snapshot_listing` used a hardcoded `snapshots.reth.rs` URL that bypassed the `DownloadDefaults` override mechanism.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
reth-primitives-traits: major
|
||||
reth-downloaders: patch
|
||||
---
|
||||
|
||||
Removed the local `size` module from `reth-primitives-traits` and replaced it with `alloy_consensus::InMemorySize`. Simplified `SignedTransaction` to a blanket impl covering all types satisfying the required bounds, removing `is_system_tx`, `auto_impl` attributes, and explicit impls for `EthereumTxEnvelope` and OP types. Updated import paths in `reth-downloaders` accordingly.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
reth-primitives-traits: minor
|
||||
reth-engine-local: patch
|
||||
reth-evm: patch
|
||||
reth-node-builder: patch
|
||||
reth-payload-primitives: patch
|
||||
reth-rpc-convert: patch
|
||||
reth-rpc-eth-api: patch
|
||||
reth-db-api: patch
|
||||
reth-db: patch
|
||||
---
|
||||
|
||||
Removed the unused `Extended` type and `op` feature (including `op-alloy-consensus` dependency) from `reth-primitives-traits`. Updated all dependent crates to remove the now-unnecessary `reth-primitives-traits/op` feature flag propagation.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
reth-engine-local: patch
|
||||
reth-node-builder: patch
|
||||
---
|
||||
|
||||
Removed the `op` feature flag and `OpPayloadAttributes` `PayloadAttributesBuilder` implementation from `reth-engine-local`, along with the `op-alloy-rpc-types-engine` dependency. Updated `reth-node-builder` to no longer enable the removed `op` feature on `reth-engine-local`.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
reth-payload-primitives: patch
|
||||
reth-engine-local: patch
|
||||
---
|
||||
|
||||
Removed the `op` feature and `op-alloy-rpc-types-engine` dependency from `reth-payload-primitives`, along with the `ExecutionPayload` impl for `OpExecutionData`. Updated `reth-engine-local` to drop the corresponding feature flag dependency.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
reth-engine-tree: patch
|
||||
---
|
||||
|
||||
Downgraded per-transaction prewarm span from `debug_span!` to `trace_span!` to reduce noise in debug-level logging.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
reth-trie-sparse: patch
|
||||
---
|
||||
|
||||
Fixed a panic in `ParallelSparseTrie::reveal_nodes` when a boundary node's upper parent is absent or non-branch (e.g. when an upper extension crosses the boundary). The code now skips gracefully instead of unwrapping. Added a regression test covering this case.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
reth-transaction-pool: minor
|
||||
---
|
||||
|
||||
Added `TransactionValidationTaskExecutor::spawn` as a dedicated constructor that encapsulates spawning validation tasks on a runtime, and refactored `EthTransactionValidatorBuilder::build_with_tasks` to use it.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
reth-engine-primitives: patch
|
||||
reth-engine-tree: patch
|
||||
reth-node-core: patch
|
||||
---
|
||||
|
||||
Removed `--engine.enable-arena-sparse-trie` CLI flag and made the arena-based sparse trie the default implementation. The hash-map-based `ParallelSparseTrie` variant is no longer selectable.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
reth-engine-primitives: minor
|
||||
reth-engine-tree: major
|
||||
reth-node-core: minor
|
||||
reth-cli-commands: minor
|
||||
---
|
||||
|
||||
Added persistence backpressure to the engine tree: when the canonical-minus-persisted block gap exceeds a configurable threshold (`--engine.persistence-backpressure-threshold`, default 16), the engine loop stalls on the persistence receiver instead of processing new incoming messages. Added CLI argument, cross-field validation, metrics (`backpressure_active`, `backpressure_stall_duration`), and tests.
|
||||
@@ -12,7 +12,7 @@ workflows:
|
||||
# Check that `A` activates the features of `B`.
|
||||
"propagate-feature",
|
||||
# These are the features to check:
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,tracy,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,otlp-logs,js-tracer,portable,keccak-cache-global,trie-debug",
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,tracy,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,otlp-logs,js-tracer,portable,keccak-cache-global",
|
||||
# Do not try to add a new section to `[features]` of `A` only because `B` exposes that feature. There are edge-cases where this is still needed, but we can add them manually.
|
||||
"--left-side-feature-missing=ignore",
|
||||
# Ignore the case that `A` it outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on.
|
||||
|
||||
49
.github/scripts/bench-reth-build.sh
vendored
49
.github/scripts/bench-reth-build.sh
vendored
@@ -11,11 +11,10 @@
|
||||
# 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
|
||||
# baseline: <source-dir>/target/profiling/reth
|
||||
# feature: <source-dir>/target/profiling/reth, 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 -euo pipefail
|
||||
|
||||
MC="mc"
|
||||
@@ -23,16 +22,6 @@ MODE="$1"
|
||||
SOURCE_DIR="$2"
|
||||
COMMIT="$3"
|
||||
|
||||
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
|
||||
# The node binary to build: reth-bb for big blocks, reth otherwise
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
NODE_BIN="reth-bb"
|
||||
NODE_PKG="-p reth-bb"
|
||||
else
|
||||
NODE_BIN="reth"
|
||||
NODE_PKG="--bin reth"
|
||||
fi
|
||||
|
||||
# Tracy support: when BENCH_TRACY is "on" or "full", add Tracy cargo features
|
||||
# and frame pointers for accurate stack traces.
|
||||
EXTRA_FEATURES=""
|
||||
@@ -73,18 +62,18 @@ case "$MODE" in
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/${NODE_BIN}" &>/dev/null; then
|
||||
echo "Cache hit for baseline (${COMMIT}), downloading ${NODE_BIN}..."
|
||||
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${COMMIT}"; then
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null; then
|
||||
echo "Cache hit for baseline (${COMMIT}), downloading binary..."
|
||||
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/reth"
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached baseline binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
|
||||
echo "Building baseline (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
FEATURES_ARG=""
|
||||
WORKSPACE_ARG=""
|
||||
@@ -95,8 +84,8 @@ case "$MODE" in
|
||||
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}"
|
||||
cargo build --profile profiling --bin reth $WORKSPACE_ARG $FEATURES_ARG
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -105,34 +94,32 @@ case "$MODE" in
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/${NODE_BIN}" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
|
||||
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
|
||||
$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
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${COMMIT}"; then
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached feature binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
|
||||
echo "Building feature (${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}"
|
||||
cargo build --profile profiling --workspace --bin reth --features "${EXTRA_FEATURES}"
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling $NODE_PKG
|
||||
make profiling
|
||||
fi
|
||||
make install-reth-bench
|
||||
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
|
||||
fi
|
||||
;;
|
||||
|
||||
62
.github/scripts/bench-reth-run.sh
vendored
62
.github/scripts/bench-reth-run.sh
vendored
@@ -18,11 +18,7 @@ set -euo pipefail
|
||||
LABEL="$1"
|
||||
BINARY="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
DATADIR_NAME="datadir"
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
LOG="${OUTPUT_DIR}/node.log"
|
||||
|
||||
@@ -124,12 +120,9 @@ RETH_ARGS=(
|
||||
--no-persist-peers
|
||||
)
|
||||
|
||||
# Gate flag on binary support (older baselines may not have it).
|
||||
# Uses --help which exits immediately via clap without node init.
|
||||
SYNC_STATE_IDLE=false
|
||||
if "$BINARY" node --help 2>/dev/null | grep -qF -- '--debug.startup-sync-state-idle'; then
|
||||
RETH_ARGS+=(--debug.startup-sync-state-idle)
|
||||
SYNC_STATE_IDLE=true
|
||||
# Big blocks mode requires the testing API and skip-invalid-transactions
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
RETH_ARGS+=(--http.api eth,net,web3,reth,testing --testing.skip-invalid-transactions)
|
||||
fi
|
||||
|
||||
# Append per-label extra node args (baseline or feature)
|
||||
@@ -201,7 +194,7 @@ for i in $(seq 1 60); do
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
|
||||
> /dev/null 2>&1; then
|
||||
echo "reth (${LABEL}) RPC is up after ${i}s"
|
||||
echo "reth (${LABEL}) is ready after ${i}s"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
@@ -212,29 +205,6 @@ for i in $(seq 1 60); do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait for the pipeline to finish (eth_syncing returns false) so the
|
||||
# engine is in live mode and can accept newPayload calls.
|
||||
# Only possible when --debug.startup-sync-state-idle is supported.
|
||||
if [ "$SYNC_STATE_IDLE" = "true" ]; then
|
||||
for i in $(seq 1 300); do
|
||||
SYNC_RESULT=$(curl -sf http://127.0.0.1:8545 -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' 2>/dev/null || true)
|
||||
if [ -n "$SYNC_RESULT" ] && jq -e '.result == false' <<< "$SYNC_RESULT" > /dev/null 2>&1; then
|
||||
echo "reth (${LABEL}) pipeline finished after ${i}s, engine is live"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 300 ]; then
|
||||
echo "::error::reth (${LABEL}) pipeline did not finish within 300s"
|
||||
cat "$LOG"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "reth (${LABEL}) binary does not support --debug.startup-sync-state-idle, skipping sync wait"
|
||||
fi
|
||||
|
||||
# Run reth-bench with high priority but as the current user so output
|
||||
# files are not root-owned (avoids EACCES on next checkout).
|
||||
BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
|
||||
@@ -242,15 +212,19 @@ BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
|
||||
# Build optional flags
|
||||
EXTRA_BENCH_ARGS=()
|
||||
if [ "${BENCH_RETH_NEW_PAYLOAD:-true}" != "false" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--reth-new-payload --wait-for-persistence)
|
||||
EXTRA_BENCH_ARGS+=(--reth-new-payload)
|
||||
fi
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
fi
|
||||
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
# Big blocks mode: replay pre-generated payloads
|
||||
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
|
||||
# Big blocks mode: replay pre-generated payloads with gas ramp
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
# Count gas ramp blocks for reporting
|
||||
GAS_RAMP_COUNT=$(find "$BIG_BLOCKS_DIR/gas-ramp-dir" -name '*.json' | wc -l)
|
||||
echo "$GAS_RAMP_COUNT" > "$OUTPUT_DIR/gas_ramp_blocks.txt"
|
||||
echo "Gas ramp blocks: $GAS_RAMP_COUNT"
|
||||
|
||||
# Start tracy-capture so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
@@ -260,18 +234,10 @@ if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
sleep 0.5 # give tracy-capture time to connect
|
||||
fi
|
||||
|
||||
BB_BENCH_ARGS=(--reth-new-payload --wait-for-persistence)
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
BB_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
fi
|
||||
# Limit number of payloads if blocks count is specified
|
||||
if [ "${BENCH_BLOCKS:-0}" -gt 0 ] 2>/dev/null; then
|
||||
BB_BENCH_ARGS+=(--count "$BENCH_BLOCKS")
|
||||
fi
|
||||
|
||||
echo "Running big blocks benchmark (replay-payloads)..."
|
||||
$BENCH_NICE "$RETH_BENCH" replay-payloads \
|
||||
"${BB_BENCH_ARGS[@]}" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" \
|
||||
--gas-ramp-dir "$BIG_BLOCKS_DIR/gas-ramp-dir" \
|
||||
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
|
||||
140
.github/scripts/bench-reth-snapshot.sh
vendored
140
.github/scripts/bench-reth-snapshot.sh
vendored
@@ -1,17 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Downloads the latest snapshot into the schelk volume using
|
||||
# `reth download` with progress reporting to the GitHub PR comment.
|
||||
# Downloads the latest nightly snapshot into the schelk volume 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).
|
||||
# Skips the download if the local ETag marker matches the remote one.
|
||||
#
|
||||
# Usage: bench-reth-snapshot.sh [--check]
|
||||
# --check Only check if a download is needed; exits 0 if up-to-date, 10 if not.
|
||||
# --check Only check if a download is needed; exits 0 if up-to-date, 1 if not.
|
||||
#
|
||||
# Required env:
|
||||
# SCHELK_MOUNT – schelk mount point (e.g. /reth-bench)
|
||||
# BENCH_RETH_BINARY – path to the reth binary
|
||||
# GITHUB_TOKEN – token for GitHub API calls (only for download)
|
||||
# BENCH_COMMENT_ID – PR comment ID to update (optional)
|
||||
# BENCH_REPO – owner/repo (e.g. paradigmxyz/reth)
|
||||
@@ -20,74 +18,52 @@
|
||||
# BENCH_CONFIG – config summary line
|
||||
set -euo pipefail
|
||||
|
||||
MC="mc"
|
||||
BUCKET="minio/reth-snapshots"
|
||||
# Allow overriding the snapshot name (e.g. for big-blocks mode where the
|
||||
# big-blocks manifest specifies which base snapshot to use).
|
||||
SNAPSHOT_NAME="${BENCH_SNAPSHOT_NAME:-reth-1-minimal-stable}"
|
||||
MANIFEST_PATH="${SNAPSHOT_NAME}/manifest.json"
|
||||
DATADIR_NAME="datadir"
|
||||
HASH_MODE_SUFFIX=""
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
HASH_MODE_SUFFIX="-big-blocks"
|
||||
BUCKET="minio/reth-snapshots/reth-1-minimal-nightly-previous.tar.zst"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
ETAG_FILE="$HOME/.reth-bench-snapshot-etag"
|
||||
|
||||
# Get remote metadata via JSON for reliable parsing
|
||||
MC_STAT=$(mc stat --json "$BUCKET" 2>/dev/null || true)
|
||||
REMOTE_ETAG=$(echo "$MC_STAT" | jq -r '.etag // empty')
|
||||
if [ -z "$REMOTE_ETAG" ]; then
|
||||
echo "::warning::Failed to get ETag from mc stat, will re-download"
|
||||
REMOTE_ETAG="unknown-$(date +%s)"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
HASH_FILE="$HOME/.reth-bench-snapshot-hash${HASH_MODE_SUFFIX}"
|
||||
|
||||
# Fetch manifest and compute content hash for reliable freshness check
|
||||
MANIFEST_CONTENT=$($MC cat "${BUCKET}/${MANIFEST_PATH}" 2>/dev/null) || {
|
||||
echo "::error::Failed to fetch snapshot manifest from ${BUCKET}/${MANIFEST_PATH}"
|
||||
exit 2
|
||||
}
|
||||
REMOTE_HASH=$(echo "$MANIFEST_CONTENT" | sha256sum | awk '{print $1}')
|
||||
LOCAL_ETAG=""
|
||||
[ -f "$ETAG_FILE" ] && LOCAL_ETAG=$(cat "$ETAG_FILE")
|
||||
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
|
||||
|
||||
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo "Snapshot is up-to-date (manifest hash: ${REMOTE_HASH:0:16}…)"
|
||||
if [ "$REMOTE_ETAG" = "$LOCAL_ETAG" ]; then
|
||||
echo "Snapshot is up-to-date (ETag: ${REMOTE_ETAG})"
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Snapshot needs update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}…}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
|
||||
echo "Snapshot needs update (local: ${LOCAL_ETAG:-<none>}, remote: ${REMOTE_ETAG})"
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 10
|
||||
fi
|
||||
|
||||
RETH="${BENCH_RETH_BINARY:?BENCH_RETH_BINARY must be set}"
|
||||
if [ ! -x "$RETH" ]; then
|
||||
echo "::error::reth binary not found or not executable at $RETH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve the MinIO HTTP endpoint from the mc alias so reth can
|
||||
# fetch archives over HTTP (the manifest's embedded base_url points
|
||||
# to the cluster-internal address which is unreachable from runners).
|
||||
MINIO_ENDPOINT=$($MC alias list minio --json 2>/dev/null | jq -r '.URL // empty') || true
|
||||
if [ -z "$MINIO_ENDPOINT" ]; then
|
||||
echo "::error::Failed to resolve MinIO endpoint from mc alias 'minio'"
|
||||
# Get compressed size for progress tracking
|
||||
TOTAL_BYTES=$(echo "$MC_STAT" | jq -r '.size // empty')
|
||||
if [ -z "$TOTAL_BYTES" ] || [ "$TOTAL_BYTES" = "0" ]; then
|
||||
echo "::error::Failed to get snapshot size from mc stat"
|
||||
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"
|
||||
echo "Snapshot size: $TOTAL_BYTES bytes ($(numfmt --to=iec "$TOTAL_BYTES"))"
|
||||
|
||||
# Prepare mount
|
||||
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || 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"
|
||||
local pct="$1"
|
||||
[ -z "${BENCH_COMMENT_ID:-}" ] && return 0
|
||||
local status="Building binaries & downloading snapshot… ${pct}%"
|
||||
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")"
|
||||
@@ -99,31 +75,53 @@ update_comment() {
|
||||
> /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
update_comment "Downloading snapshot…"
|
||||
# Track compressed bytes flowing through the pipe
|
||||
DL_BYTES_FILE=$(mktemp)
|
||||
echo 0 > "$DL_BYTES_FILE"
|
||||
|
||||
# Download using reth download (manifest-path with rewritten base_url)
|
||||
"$RETH" download \
|
||||
--manifest-path "$MANIFEST_TMP" \
|
||||
-y \
|
||||
--minimal \
|
||||
--datadir "$DATADIR"
|
||||
# Start progress reporter in background
|
||||
(
|
||||
while true; do
|
||||
sleep 10
|
||||
CURRENT=$(cat "$DL_BYTES_FILE" 2>/dev/null || echo 0)
|
||||
if [ "$TOTAL_BYTES" -gt 0 ]; then
|
||||
PCT=$(( CURRENT * 100 / TOTAL_BYTES ))
|
||||
[ "$PCT" -gt 100 ] && PCT=100
|
||||
echo "Snapshot download: $(numfmt --to=iec "$CURRENT") / $(numfmt --to=iec "$TOTAL_BYTES") (${PCT}%)"
|
||||
update_comment "$PCT"
|
||||
fi
|
||||
done
|
||||
) &
|
||||
PROGRESS_PID=$!
|
||||
trap 'kill $PROGRESS_PID 2>/dev/null || true; rm -f "$DL_BYTES_FILE"' EXIT
|
||||
|
||||
update_comment "Downloading snapshot… done"
|
||||
# Download and extract; python byte counter tracks compressed bytes received
|
||||
mc cat "$BUCKET" | python3 -c "
|
||||
import sys
|
||||
count = 0
|
||||
while True:
|
||||
data = sys.stdin.buffer.read(1048576)
|
||||
if not data:
|
||||
break
|
||||
count += len(data)
|
||||
sys.stdout.buffer.write(data)
|
||||
with open('$DL_BYTES_FILE', 'w') as f:
|
||||
f.write(str(count))
|
||||
" | pzstd -d -p 6 | sudo tar -xf - -C "$DATADIR"
|
||||
|
||||
# Stop progress reporter
|
||||
kill $PROGRESS_PID 2>/dev/null || true
|
||||
wait $PROGRESS_PID 2>/dev/null || true
|
||||
|
||||
update_comment "100"
|
||||
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}…)"
|
||||
# Save ETag marker
|
||||
echo "$REMOTE_ETAG" > "$ETAG_FILE"
|
||||
echo "Snapshot promoted to schelk baseline (ETag: ${REMOTE_ETAG})"
|
||||
|
||||
2
.github/scripts/bench-reth-summary.py
vendored
2
.github/scripts/bench-reth-summary.py
vendored
@@ -472,6 +472,7 @@ def main():
|
||||
parser.add_argument("--feature-ref", "--branch-sha", "--feature-sha", default=None, help="Feature commit SHA")
|
||||
parser.add_argument("--behind-baseline", "--behind-main", type=int, default=0, help="Commits behind baseline")
|
||||
parser.add_argument("--big-blocks", action="store_true", default=False, help="Big blocks mode")
|
||||
parser.add_argument("--gas-ramp-blocks", type=int, default=0, help="Number of gas ramp blocks (big blocks mode)")
|
||||
parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -553,6 +554,7 @@ def main():
|
||||
summary = {
|
||||
"blocks": paired_stats["blocks"],
|
||||
"big_blocks": args.big_blocks,
|
||||
"gas_ramp_blocks": args.gas_ramp_blocks,
|
||||
"baseline": {
|
||||
"name": baseline_name,
|
||||
"ref": baseline_ref,
|
||||
|
||||
125
.github/scripts/bench-scheduled-refs.sh
vendored
125
.github/scripts/bench-scheduled-refs.sh
vendored
@@ -1,130 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Resolves baseline and feature refs for scheduled benchmark runs.
|
||||
# Resolves baseline and feature refs for nightly regression benchmark runs.
|
||||
#
|
||||
# Supports two modes:
|
||||
# nightly — Queries the latest successful scheduled docker.yml run via
|
||||
# GitHub API to find the nightly Docker image commit. Compares
|
||||
# with the last successful feature ref to detect staleness.
|
||||
# hourly — Compares origin/main HEAD against the last successfully
|
||||
# benchmarked commit (falls back to HEAD~1 on first run).
|
||||
# Checks for in-progress sibling runs to avoid overlap.
|
||||
# Queries the latest successful scheduled docker.yml run via GitHub API
|
||||
# to find the commit that built the nightly Docker image. Compares with
|
||||
# the last successful feature ref (from GH Actions cache) to determine
|
||||
# baseline, detect staleness, and decide whether to skip.
|
||||
#
|
||||
# Usage: bench-scheduled-refs.sh <force> <mode>
|
||||
# force — "true" to run even if no new commit (bypass skip logic)
|
||||
# mode — "nightly" or "hourly"
|
||||
# Usage: bench-nightly-refs.sh [--force]
|
||||
#
|
||||
# Outputs (via GITHUB_OUTPUT):
|
||||
# baseline-ref — commit SHA for baseline
|
||||
# feature-ref — commit SHA for feature
|
||||
# should-skip — "true" if no new commit since last run or sibling in progress
|
||||
# is-stale — "true" if latest nightly build is >24h old (nightly only)
|
||||
# stale-age-hours — age of the nightly build in hours (nightly only)
|
||||
# nightly-created — ISO timestamp of the nightly build (nightly only)
|
||||
# feature-ref — commit SHA for feature (current nightly)
|
||||
# should-skip — "true" if no new nightly since last run
|
||||
# is-stale — "true" if latest nightly build is >24h old
|
||||
# stale-age-hours — age of the nightly build in hours (only if stale)
|
||||
# nightly-created — ISO timestamp of the nightly build
|
||||
#
|
||||
# Reads:
|
||||
# .nightly-state/last-feature-ref (nightly, from GH Actions cache)
|
||||
# .hourly-state/last-feature-ref (hourly, from GH Actions cache)
|
||||
# .nightly-state/last-feature-ref (from GH Actions cache, may not exist)
|
||||
#
|
||||
# Requires: gh (GitHub CLI), jq, date, git (hourly mode)
|
||||
# Requires: gh (GitHub CLI), jq, date
|
||||
set -euo pipefail
|
||||
|
||||
FORCE="${1:-false}"
|
||||
MODE="${2:-nightly}"
|
||||
REPO="${GITHUB_REPOSITORY:-paradigmxyz/reth}"
|
||||
|
||||
echo "Mode: $MODE, Force: $FORCE"
|
||||
|
||||
# ==========================================================================
|
||||
# Hourly mode: compare origin/main HEAD vs HEAD~1
|
||||
# ==========================================================================
|
||||
if [ "$MODE" = "hourly" ]; then
|
||||
|
||||
# --- Step 1: Resolve feature ref from git ---
|
||||
echo "::group::Resolving hourly refs from git"
|
||||
git fetch origin main --quiet
|
||||
FEATURE_REF=$(git rev-parse origin/main)
|
||||
echo "Feature (HEAD): $FEATURE_REF"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 2: Check for in-progress sibling runs ---
|
||||
echo "::group::Checking for in-progress sibling runs"
|
||||
CURRENT_RUN_ID="${GITHUB_RUN_ID:-0}"
|
||||
IN_PROGRESS=$(gh run list \
|
||||
-R "$REPO" \
|
||||
--workflow=bench-scheduled.yml \
|
||||
--status=in_progress \
|
||||
--json databaseId \
|
||||
--jq "[.[] | select(.databaseId != $CURRENT_RUN_ID)] | length")
|
||||
|
||||
SHOULD_SKIP="false"
|
||||
if [ "$IN_PROGRESS" -gt 0 ]; then
|
||||
echo "::warning::Previous bench run still in progress ($IN_PROGRESS sibling run(s) found). Skipping."
|
||||
SHOULD_SKIP="true"
|
||||
# Output a flag so the workflow can send a Slack alert
|
||||
echo "long-running=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No in-progress sibling runs"
|
||||
echo "long-running=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 3: Read last successful feature ref from cache ---
|
||||
echo "::group::Reading cached state"
|
||||
LAST_FEATURE_REF=""
|
||||
STATE_FILE=".hourly-state/last-feature-ref"
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
LAST_FEATURE_REF=$(tr -d '[:space:]' < "$STATE_FILE")
|
||||
echo "Previous feature ref: $LAST_FEATURE_REF"
|
||||
else
|
||||
echo "No cached state found (first run)"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 4: Determine baseline and skip logic ---
|
||||
echo "::group::Resolving baseline and skip logic"
|
||||
if [ "$SHOULD_SKIP" = "true" ]; then
|
||||
BASELINE_REF=$(git rev-parse origin/main~1)
|
||||
echo "Already marked skip (sibling in progress)"
|
||||
elif [ -z "$LAST_FEATURE_REF" ]; then
|
||||
# First run: no previous state, fall back to HEAD~1
|
||||
BASELINE_REF=$(git rev-parse origin/main~1)
|
||||
echo "First run — using HEAD~1 as baseline"
|
||||
elif [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
|
||||
echo "No new commits on main, but force=true — running anyway"
|
||||
else
|
||||
SHOULD_SKIP="true"
|
||||
echo "No new commits on main since last run — will skip"
|
||||
fi
|
||||
else
|
||||
# Normal case: use last benchmarked commit as baseline
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
echo "New commit(s) on main detected — comparing against last benchmarked commit"
|
||||
fi
|
||||
|
||||
echo "Baseline: $BASELINE_REF"
|
||||
echo "Feature: $FEATURE_REF"
|
||||
echo "Skip: $SHOULD_SKIP"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 5: Write outputs ---
|
||||
{
|
||||
echo "baseline-ref=$BASELINE_REF"
|
||||
echo "feature-ref=$FEATURE_REF"
|
||||
echo "should-skip=$SHOULD_SKIP"
|
||||
echo "is-stale=false"
|
||||
echo "stale-age-hours=0"
|
||||
echo "nightly-created="
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# Nightly mode: query latest Docker nightly build (original logic)
|
||||
# ==========================================================================
|
||||
|
||||
# --- Step 1: Query latest successful scheduled docker.yml run ---
|
||||
echo "::group::Querying latest nightly docker build"
|
||||
|
||||
|
||||
3
.github/scripts/bench-slack-users.json
vendored
3
.github/scripts/bench-slack-users.json
vendored
@@ -20,6 +20,5 @@
|
||||
"SuperFluffy": "U095BKHB2Q4",
|
||||
"kamsz": "U0A2563UBRD",
|
||||
"zerosnacks": "U09FARPMN74",
|
||||
"samczsun": "U096R14E4H3",
|
||||
"laibe": "U09FARE0B9Q"
|
||||
"samczsun": "U096R14E4H3"
|
||||
}
|
||||
|
||||
1
.github/scripts/bench-utils.js
vendored
1
.github/scripts/bench-utils.js
vendored
@@ -42,6 +42,7 @@ function loadSamplyUrls(workDir) {
|
||||
function blocksLabel(summary) {
|
||||
const parts = [];
|
||||
if (summary.big_blocks) {
|
||||
if (summary.gas_ramp_blocks) parts.push({ key: 'Gas Ramp', value: summary.gas_ramp_blocks });
|
||||
parts.push({ key: 'Big Blocks', value: summary.blocks });
|
||||
} else {
|
||||
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
|
||||
|
||||
3
.github/scripts/check_rv32imac.sh
vendored
3
.github/scripts/check_rv32imac.sh
vendored
@@ -2,6 +2,9 @@
|
||||
set -uo pipefail
|
||||
|
||||
crates_to_check=(
|
||||
reth-codecs-derive
|
||||
reth-primitives
|
||||
reth-primitives-traits
|
||||
reth-network-peers
|
||||
reth-trie-common
|
||||
reth-trie-sparse
|
||||
|
||||
2
.github/scripts/check_wasm.sh
vendored
2
.github/scripts/check_wasm.sh
vendored
@@ -22,7 +22,6 @@ exclude_crates=(
|
||||
reth-downloaders
|
||||
reth-e2e-test-utils
|
||||
reth-engine-service
|
||||
reth-execution-cache
|
||||
reth-engine-tree
|
||||
reth-engine-util
|
||||
reth-eth-wire
|
||||
@@ -56,7 +55,6 @@ exclude_crates=(
|
||||
reth-ress-provider
|
||||
# The following are not supposed to be working
|
||||
reth # all of the crates below
|
||||
reth-bb # binary-only, uses tokio features unsupported on wasm
|
||||
reth-storage-rpc-provider
|
||||
reth-invalid-block-hooks # reth-provider
|
||||
reth-libmdbx # mdbx
|
||||
|
||||
2
.github/scripts/hive/Dockerfile
vendored
2
.github/scripts/hive/Dockerfile
vendored
@@ -7,7 +7,7 @@ FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y libclang-dev pkg-config
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
#
|
||||
# We prepare the build plan
|
||||
|
||||
10
.github/scripts/hive/build_simulators.sh
vendored
10
.github/scripts/hive/build_simulators.sh
vendored
@@ -11,14 +11,8 @@ go build .
|
||||
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
echo "Building images"
|
||||
./hive -client reth --sim "ethereum/eels/consume-engine" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/eels/consume-rlp" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.timelimit 1s || true &
|
||||
# TODO: test code has been moved from https://github.com/ethereum/execution-spec-tests to https://github.com/ethereum/execution-specs we need to pin eels branch with `--sim.buildarg branch=<release-branch-name>` once we have the fusaka release tagged on the new repo
|
||||
./hive -client reth --sim "ethereum/eels" --sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/rpc-compat" -sim.timelimit 1s || true &
|
||||
|
||||
3
.github/scripts/hive/expected_failures.yaml
vendored
3
.github/scripts/hive/expected_failures.yaml
vendored
@@ -1,6 +1,9 @@
|
||||
# tracked by https://github.com/paradigmxyz/reth/issues/13879
|
||||
rpc-compat:
|
||||
- debug_getRawBlock/get-invalid-number (reth)
|
||||
- debug_getRawHeader/get-invalid-number (reth)
|
||||
- debug_getRawReceipts/get-invalid-number (reth)
|
||||
- debug_getRawReceipts/get-block-n (reth)
|
||||
- debug_getRawTransaction/get-invalid-hash (reth)
|
||||
|
||||
- eth_getStorageAt/get-storage-invalid-key-too-large (reth)
|
||||
|
||||
8
.github/scripts/hive/run_simulator.sh
vendored
8
.github/scripts/hive/run_simulator.sh
vendored
@@ -13,13 +13,7 @@ if [[ "${sim}" == *"eels"* ]]; then
|
||||
fi
|
||||
|
||||
run_hive() {
|
||||
hive \
|
||||
--sim "${sim}" \
|
||||
--sim.limit "${limit}" \
|
||||
--sim.limit.exact=false \
|
||||
--sim.parallelism "${parallelism}" \
|
||||
--client reth \
|
||||
2>&1 | tee /tmp/log || true
|
||||
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism "${parallelism}" --client reth 2>&1 | tee /tmp/log || true
|
||||
}
|
||||
|
||||
check_log() {
|
||||
|
||||
201
.github/workflows/bench-scheduled.yml
vendored
201
.github/workflows/bench-scheduled.yml
vendored
@@ -1,25 +1,19 @@
|
||||
# Scheduled regression benchmarks (nightly + hourly).
|
||||
# Nightly regression benchmark.
|
||||
#
|
||||
# Two modes:
|
||||
# nightly — Compares the previous nightly Docker build against the current one.
|
||||
# Runs daily after docker.yml produces a new nightly image.
|
||||
# hourly — Compares main HEAD against the last benchmarked commit to catch
|
||||
# regressions quickly. Falls back to HEAD~1 on first run.
|
||||
# Skips if no new commits or if a previous run is still in progress.
|
||||
# Compares the previous nightly build against the current nightly build to
|
||||
# detect performance regressions. Runs daily after docker.yml produces a new
|
||||
# nightly image at 01:00 UTC.
|
||||
#
|
||||
# State is persisted between runs via GitHub Actions cache: each successful
|
||||
# run saves the feature commit SHA so the next run knows what to compare against.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Nightly: compares previous vs current nightly Docker build
|
||||
- cron: "30 5 * * *"
|
||||
# Hourly: compares main HEAD vs last benchmarked commit, skips if no new commits
|
||||
- cron: "0 * * * *"
|
||||
- cron: "30 5 * * *" # 06:30 UTC daily
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: "Force run even if no new commit (bypass skip logic)"
|
||||
description: "Force run even if no new nightly (bypass skip logic)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -28,14 +22,6 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
mode:
|
||||
description: "Benchmark mode"
|
||||
required: false
|
||||
default: "nightly"
|
||||
type: choice
|
||||
options:
|
||||
- nightly
|
||||
- hourly
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -49,105 +35,44 @@ permissions:
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: Resolve refs, check staleness, manage state
|
||||
# Job 1: Resolve nightly refs, check staleness, manage state
|
||||
# ---------------------------------------------------------------------------
|
||||
resolve-refs:
|
||||
name: resolve-refs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
mode: ${{ steps.mode.outputs.mode }}
|
||||
baseline-ref: ${{ steps.refs.outputs.baseline-ref }}
|
||||
feature-ref: ${{ steps.refs.outputs.feature-ref }}
|
||||
should-skip: ${{ steps.refs.outputs.should-skip }}
|
||||
is-stale: ${{ steps.refs.outputs.is-stale }}
|
||||
stale-age-hours: ${{ steps.refs.outputs.stale-age-hours }}
|
||||
nightly-created: ${{ steps.refs.outputs.nightly-created }}
|
||||
long-running: ${{ steps.refs.outputs.long-running }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect mode
|
||||
id: mode
|
||||
run: |
|
||||
# Maps cron schedules to modes (must match the schedule entries above)
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
MODE="${{ inputs.mode || 'nightly' }}"
|
||||
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
|
||||
MODE="nightly"
|
||||
else
|
||||
MODE="hourly"
|
||||
fi
|
||||
echo "mode=$MODE" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected mode: $MODE"
|
||||
|
||||
- name: Restore state cache
|
||||
- name: Restore nightly state
|
||||
id: state-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .${{ steps.mode.outputs.mode == 'hourly' && 'hourly' || 'nightly' }}-state
|
||||
key: bench-${{ steps.mode.outputs.mode }}-state-dummy
|
||||
path: .nightly-state
|
||||
key: bench-scheduled-state-dummy
|
||||
restore-keys: |
|
||||
bench-${{ steps.mode.outputs.mode }}-state-
|
||||
bench-scheduled-state-
|
||||
|
||||
- name: Resolve refs
|
||||
- name: Resolve nightly refs
|
||||
id: refs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
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'
|
||||
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 token = process.env.SLACK_BENCH_BOT_TOKEN;
|
||||
const channel = process.env.SLACK_BENCH_CHANNEL;
|
||||
if (!token || !channel) return;
|
||||
|
||||
const repo = '${{ github.repository }}';
|
||||
const runUrl = `${context.serverUrl}/${repo}/actions/runs/${context.runId}`;
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: ':warning: Hourly Bench: previous run still in progress', emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'A previous hourly benchmark run is still in progress. This invocation will be skipped.\nThis may indicate a long-running or stuck job.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: [{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'View Run :github:', emoji: true },
|
||||
url: runUrl,
|
||||
action_id: 'ci_button',
|
||||
}],
|
||||
},
|
||||
];
|
||||
await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ channel, blocks, text: 'Hourly bench: previous run still in progress', unfurl_links: false }),
|
||||
});
|
||||
.github/scripts/bench-scheduled-refs.sh "$FORCE"
|
||||
|
||||
- name: Alert on stale nightly
|
||||
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
|
||||
if: steps.refs.outputs.is-stale == 'true'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
@@ -221,7 +146,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Fail on stale nightly
|
||||
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
|
||||
if: steps.refs.outputs.is-stale == 'true'
|
||||
run: |
|
||||
echo "::error::Nightly build is stale (>24h old). Aborting."
|
||||
exit 1
|
||||
@@ -242,8 +167,7 @@ jobs:
|
||||
SCHELK_MOUNT: /reth-bench
|
||||
BENCH_WORK_DIR: ${{ github.workspace }}/bench-work
|
||||
BENCH_PR: ""
|
||||
BENCH_MODE: ${{ needs.resolve-refs.outputs.mode }}
|
||||
BENCH_ACTOR: "${{ needs.resolve-refs.outputs.mode }}-regression"
|
||||
BENCH_ACTOR: "nightly-regression"
|
||||
BENCH_BLOCKS: "2000"
|
||||
BENCH_WARMUP_BLOCKS: "500"
|
||||
BENCH_SAMPLY: "false"
|
||||
@@ -349,24 +273,19 @@ jobs:
|
||||
run: |
|
||||
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
|
||||
FEATURE_SHORT=$(echo "$FEATURE_REF" | cut -c1-8)
|
||||
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "baseline-name=nightly-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-name=nightly-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if snapshot needs update
|
||||
id: snapshot-check
|
||||
run: |
|
||||
set +e
|
||||
.github/scripts/bench-reth-snapshot.sh --check
|
||||
rc=$?
|
||||
set -e
|
||||
case "$rc" in
|
||||
0) echo "needed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
10) echo "needed=true" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Snapshot check failed (exit $rc)"
|
||||
exit "$rc" ;;
|
||||
esac
|
||||
if .github/scripts/bench-reth-snapshot.sh --check; then
|
||||
echo "needed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "needed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
@@ -384,11 +303,12 @@ jobs:
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
- name: Build binaries and download snapshot in parallel
|
||||
id: build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BENCH_REPO: ${{ github.repository }}
|
||||
SNAPSHOT_NEEDED: ${{ steps.snapshot-check.outputs.needed }}
|
||||
run: |
|
||||
BASELINE_DIR="$(cd ../reth-baseline && pwd)"
|
||||
FEATURE_DIR="$(cd ../reth-feature && pwd)"
|
||||
@@ -398,23 +318,21 @@ jobs:
|
||||
.github/scripts/bench-reth-build.sh feature "${FEATURE_DIR}" "$FEATURE_REF" &
|
||||
PID_FEATURE=$!
|
||||
|
||||
PID_SNAPSHOT=
|
||||
if [ "$SNAPSHOT_NEEDED" = "true" ]; then
|
||||
.github/scripts/bench-reth-snapshot.sh &
|
||||
PID_SNAPSHOT=$!
|
||||
fi
|
||||
|
||||
FAIL=0
|
||||
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 download)"
|
||||
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: |
|
||||
@@ -466,7 +384,7 @@ jobs:
|
||||
|
||||
- name: Start metrics proxy
|
||||
run: |
|
||||
BENCH_ID="${BENCH_MODE}-${{ github.run_id }}"
|
||||
BENCH_ID="nightly-${{ github.run_id }}"
|
||||
BENCH_REFERENCE_EPOCH=$(date +%s)
|
||||
echo "BENCH_ID=${BENCH_ID}" >> "$GITHUB_ENV"
|
||||
echo "BENCH_REFERENCE_EPOCH=${BENCH_REFERENCE_EPOCH}" >> "$GITHUB_ENV"
|
||||
@@ -595,7 +513,7 @@ jobs:
|
||||
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
|
||||
|
||||
- name: Generate charts
|
||||
if: success() && env.BENCH_MODE != 'hourly'
|
||||
if: success()
|
||||
env:
|
||||
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
|
||||
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
|
||||
@@ -621,7 +539,7 @@ jobs:
|
||||
|
||||
- name: Push charts
|
||||
id: push-charts
|
||||
if: success() && env.BENCH_MODE != 'hourly'
|
||||
if: success()
|
||||
run: |
|
||||
RUN_ID=${{ github.run_id }}
|
||||
CHART_DIR="nightly/${RUN_ID}"
|
||||
@@ -667,9 +585,7 @@ jobs:
|
||||
const featureLink = `[\`${summary.feature.name}\`](${commitUrl}/${summary.feature.ref})`;
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
|
||||
const mode = process.env.BENCH_MODE || 'nightly';
|
||||
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
|
||||
let md = `# ${emoji} ${modeLabel}: ${label}\n\n`;
|
||||
let md = `# ${emoji} Nightly Regression: ${label}\n\n`;
|
||||
md += `**Baseline:** ${baselineLink}\n`;
|
||||
md += `**Feature:** ${featureLink} ([diff](${diffUrl}))\n`;
|
||||
md += blocksLabel(summary).map(p => `**${p.key}:** ${p.value}`).join(' · ') + '\n\n';
|
||||
@@ -749,19 +665,9 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter notifications based on mode
|
||||
// Only notify on significant changes (regression OR improvement)
|
||||
const changes = summary.changes || {};
|
||||
const mode = process.env.BENCH_MODE || 'nightly';
|
||||
const hasRegression = Object.values(changes).some(c => c.sig === 'bad');
|
||||
const hasSignificant = Object.values(changes).some(c => c.sig === 'good' || c.sig === 'bad');
|
||||
|
||||
// Hourly mode: only notify on regressions
|
||||
if (mode === 'hourly' && !hasRegression) {
|
||||
core.info('Hourly mode: no regression detected, skipping Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
// Nightly mode: notify on any significant change (regression or improvement)
|
||||
if (!hasSignificant) {
|
||||
core.info('No significant changes detected, skipping nightly Slack notification');
|
||||
return;
|
||||
@@ -785,9 +691,8 @@ jobs:
|
||||
|
||||
function cell(text) { return { type: 'raw_text', text: String(text) || ' ' }; }
|
||||
|
||||
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
|
||||
const sectionText = [
|
||||
`*${modeLabel}*`,
|
||||
'*Nightly Regression*',
|
||||
'',
|
||||
`*Baseline:* ${baselineLink}`,
|
||||
`*Feature:* ${featureLink}`,
|
||||
@@ -803,7 +708,7 @@ jobs:
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: `${headerEmoji} ${modeLabel}: ${label}`, emoji: true },
|
||||
text: { type: 'plain_text', text: `${headerEmoji} Nightly: ${label}`, emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -833,7 +738,7 @@ jobs:
|
||||
},
|
||||
];
|
||||
|
||||
const text = `${modeLabel}: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
const text = `Nightly Regression: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
const resp = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -888,8 +793,7 @@ jobs:
|
||||
if (!token || !channel) return;
|
||||
|
||||
const steps_status = [
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.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 }}'],
|
||||
@@ -901,17 +805,14 @@ jobs:
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const jobUrl = process.env.BENCH_JOB_URL || `${context.serverUrl}/${repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const mode = process.env.BENCH_MODE || 'nightly';
|
||||
const modeLabel = mode === 'hourly' ? 'Hourly' : 'Nightly';
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: `:rotating_light: ${modeLabel} Bench Failed`, emoji: true },
|
||||
text: { type: 'plain_text', text: ':rotating_light: Nightly Bench Failed', emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>` },
|
||||
text: { type: 'mrkdwn', text: `*Nightly regression* failed while *${failedStep}*` },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
@@ -933,7 +834,7 @@ jobs:
|
||||
body: JSON.stringify({
|
||||
channel,
|
||||
blocks,
|
||||
text: `${modeLabel} bench failed while ${failedStep}`,
|
||||
text: `Nightly bench failed while ${failedStep}`,
|
||||
unfurl_links: false,
|
||||
}),
|
||||
});
|
||||
@@ -954,13 +855,11 @@ jobs:
|
||||
steps:
|
||||
- name: Write state file
|
||||
run: |
|
||||
MODE="${{ needs.resolve-refs.outputs.mode }}"
|
||||
STATE_DIR=".${MODE}-state"
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > "$STATE_DIR/last-feature-ref"
|
||||
mkdir -p .nightly-state
|
||||
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > .nightly-state/last-feature-ref
|
||||
|
||||
- name: Save state
|
||||
- name: Save nightly state
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: .${{ needs.resolve-refs.outputs.mode }}-state
|
||||
key: bench-${{ needs.resolve-refs.outputs.mode }}-state-${{ needs.resolve-refs.outputs.feature-ref }}
|
||||
path: .nightly-state
|
||||
key: bench-scheduled-state-${{ needs.resolve-refs.outputs.feature-ref }}
|
||||
|
||||
154
.github/workflows/bench.yml
vendored
154
.github/workflows/bench.yml
vendored
@@ -12,15 +12,10 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
blocks:
|
||||
description: "Number of blocks to benchmark"
|
||||
description: "Number of blocks to benchmark (or 'big' for big blocks mode)"
|
||||
required: false
|
||||
default: "500"
|
||||
type: string
|
||||
big_blocks:
|
||||
description: "Use big blocks mode (pre-generated merged payloads with reth-bb)"
|
||||
required: false
|
||||
default: "false"
|
||||
type: boolean
|
||||
warmup:
|
||||
description: "Number of warmup blocks"
|
||||
required: false
|
||||
@@ -84,6 +79,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
BASELINE: base
|
||||
SEED: reth
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
BENCH_RUNNERS: 2
|
||||
|
||||
@@ -157,7 +154,7 @@ jobs:
|
||||
samply = '${{ github.event.inputs.samply }}' === 'true' ? 'true' : 'false';
|
||||
var noSlack = '${{ github.event.inputs.no_slack }}' !== 'false' ? 'true' : 'false';
|
||||
cores = '${{ github.event.inputs.cores }}' || '0';
|
||||
bigBlocks = '${{ github.event.inputs.big_blocks }}' === 'true' ? 'true' : 'false';
|
||||
bigBlocks = blocks === 'big' ? 'true' : 'false';
|
||||
var rethNewPayload = '${{ github.event.inputs.reth_newPayload }}' !== 'false' ? 'true' : 'false';
|
||||
var abba = '${{ github.event.inputs.abba }}' !== 'false' ? 'true' : 'false';
|
||||
var otlp = '${{ github.event.inputs.otlp }}' !== 'false' ? 'true' : 'false';
|
||||
@@ -183,13 +180,14 @@ jobs:
|
||||
actor = context.payload.comment.user.login;
|
||||
|
||||
const body = context.payload.comment.body.trim();
|
||||
const intArgs = new Set(['warmup', 'cores', 'blocks']);
|
||||
const intArgs = new Set(['warmup', 'cores']);
|
||||
const intOrKeywordArgs = new Map([['blocks', new Set(['big'])]]);
|
||||
const refArgs = new Set(['baseline', 'feature']);
|
||||
const boolArgs = new Set(['samply', 'no-slack', 'big-blocks']);
|
||||
const boolArgs = new Set(['samply', 'no-slack']);
|
||||
const boolDefaultTrue = new Set(['reth_newPayload', 'abba', 'otlp']);
|
||||
const durationArgs = new Set(['wait-time']);
|
||||
const stringArgs = new Set(['baseline-args', 'feature-args']);
|
||||
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', 'big-blocks': 'false', cores: '0', reth_newPayload: 'true', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
|
||||
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', cores: '0', reth_newPayload: 'true', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
|
||||
const unknown = [];
|
||||
const invalid = [];
|
||||
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
|
||||
@@ -234,6 +232,15 @@ jobs:
|
||||
} else {
|
||||
defaults[key] = value;
|
||||
}
|
||||
} else if (intOrKeywordArgs.has(key)) {
|
||||
const keywords = intOrKeywordArgs.get(key);
|
||||
if (keywords.has(value)) {
|
||||
defaults[key] = value;
|
||||
} else if (/^\d+$/.test(value)) {
|
||||
defaults[key] = value;
|
||||
} else {
|
||||
invalid.push(`\`${key}=${value}\` (must be a positive integer or one of: ${[...keywords].join(', ')})`);
|
||||
}
|
||||
} else if (refArgs.has(key)) {
|
||||
if (!value) {
|
||||
invalid.push(`\`${key}=\` (must be a git ref)`);
|
||||
@@ -250,7 +257,7 @@ jobs:
|
||||
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
|
||||
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
|
||||
if (errors.length) {
|
||||
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N] [big-blocks] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [reth_newPayload=true|false] [abba=true|false] [otlp=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]\``;
|
||||
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N|big] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [reth_newPayload=true|false] [abba=true|false] [otlp=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]\``;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
@@ -267,7 +274,7 @@ jobs:
|
||||
samply = defaults.samply;
|
||||
var noSlack = defaults['no-slack'];
|
||||
cores = defaults.cores;
|
||||
bigBlocks = defaults['big-blocks'];
|
||||
bigBlocks = blocks === 'big' ? 'true' : 'false';
|
||||
var rethNewPayload = defaults.reth_newPayload;
|
||||
var abba = defaults.abba;
|
||||
var otlp = defaults.otlp;
|
||||
@@ -503,7 +510,6 @@ jobs:
|
||||
BENCH_OTLP: ${{ needs.reth-bench-ack.outputs.otlp }}
|
||||
BENCH_COMMENT_ID: ${{ needs.reth-bench-ack.outputs.comment-id }}
|
||||
BENCH_NO_SLACK: ${{ needs.reth-bench-ack.outputs.no-slack }}
|
||||
BENCH_NODE_BIN: ${{ needs.reth-bench-ack.outputs.big-blocks == 'true' && 'reth-bb' || 'reth' }}
|
||||
BENCH_METRICS_ADDR: "127.0.0.1:9100"
|
||||
BENCH_OTLP_TRACES_ENDPOINT: ${{ needs.reth-bench-ack.outputs.otlp != 'false' && secrets.BENCH_OTLP_TRACES_ENDPOINT || '' }}
|
||||
BENCH_OTLP_LOGS_ENDPOINT: ${{ needs.reth-bench-ack.outputs.otlp != 'false' && secrets.BENCH_OTLP_LOGS_ENDPOINT || '' }}
|
||||
@@ -719,52 +725,14 @@ jobs:
|
||||
core.setOutput('feature-ref', featureRef);
|
||||
core.setOutput('feature-name', featureName);
|
||||
|
||||
- name: Check big-blocks freshness
|
||||
if: env.BENCH_BIG_BLOCKS == 'true'
|
||||
id: big-blocks-check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MC="mc --config-dir /home/ubuntu/.mc"
|
||||
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
|
||||
HASH_FILE="$HOME/.reth-bench-big-blocks-hash"
|
||||
echo "Fetching big-blocks manifest from $MANIFEST..."
|
||||
BB_MANIFEST=$($MC cat "$MANIFEST" 2>/dev/null) || {
|
||||
echo "::error::Failed to fetch big-blocks manifest from $MANIFEST"
|
||||
exit 1
|
||||
}
|
||||
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
|
||||
echo "Big-blocks base snapshot: $BASE_SNAPSHOT"
|
||||
echo "BENCH_SNAPSHOT_NAME=${BASE_SNAPSHOT}" >> "$GITHUB_ENV"
|
||||
|
||||
REMOTE_HASH=$(echo "$BB_MANIFEST" | sha256sum | awk '{print $1}')
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
|
||||
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo "Big blocks up-to-date (hash: ${REMOTE_HASH:0:16}…)"
|
||||
echo "needed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Big blocks need update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}…}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
|
||||
echo "needed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "remote-hash=${REMOTE_HASH}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if snapshot needs update
|
||||
id: snapshot-check
|
||||
run: |
|
||||
set +e
|
||||
.github/scripts/bench-reth-snapshot.sh --check
|
||||
rc=$?
|
||||
set -e
|
||||
case "$rc" in
|
||||
0) echo "needed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
10) echo "needed=true" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Snapshot check failed (exit $rc)"
|
||||
exit "$rc" ;;
|
||||
esac
|
||||
if .github/scripts/bench-reth-snapshot.sh --check; then
|
||||
echo "needed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "needed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Update status (snapshot needed)
|
||||
if: env.BENCH_COMMENT_ID && steps.snapshot-check.outputs.needed == 'true'
|
||||
@@ -773,7 +741,7 @@ jobs:
|
||||
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)...'});
|
||||
await s({github, context, status: 'Building binaries & downloading snapshot...'});
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
@@ -793,11 +761,12 @@ jobs:
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
- name: Build binaries and download snapshot in parallel
|
||||
id: build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BENCH_REPO: ${{ github.repository }}
|
||||
SNAPSHOT_NEEDED: ${{ steps.snapshot-check.outputs.needed }}
|
||||
run: |
|
||||
BASELINE_DIR="$(cd ../reth-baseline && pwd)"
|
||||
FEATURE_DIR="$(cd ../reth-feature && pwd)"
|
||||
@@ -807,23 +776,21 @@ jobs:
|
||||
.github/scripts/bench-reth-build.sh feature "${FEATURE_DIR}" "${{ steps.refs.outputs.feature-ref }}" &
|
||||
PID_FEATURE=$!
|
||||
|
||||
PID_SNAPSHOT=
|
||||
if [ "$SNAPSHOT_NEEDED" = "true" ]; then
|
||||
.github/scripts/bench-reth-snapshot.sh &
|
||||
PID_SNAPSHOT=$!
|
||||
fi
|
||||
|
||||
FAIL=0
|
||||
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 download)"
|
||||
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: |
|
||||
@@ -879,40 +846,20 @@ jobs:
|
||||
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"
|
||||
BUCKET="minio/reth-snapshots/reth-1-minimal-nightly-previous-big-blocks.tar.zst"
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
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 "Downloading big blocks from $BUCKET..."
|
||||
$MC cat "$BUCKET" | 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"
|
||||
if [ ! -d "$BIG_BLOCKS_DIR/gas-ramp-dir" ] || [ ! -d "$BIG_BLOCKS_DIR/payloads" ]; then
|
||||
echo "::error::Big blocks archive missing expected gas-ramp-dir/ or payloads/ directories"
|
||||
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: |
|
||||
@@ -954,7 +901,7 @@ jobs:
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"baseline-1","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","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-1"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
|
||||
|
||||
- name: "Run benchmark: feature (1/2)"
|
||||
id: run-feature-1
|
||||
@@ -965,7 +912,7 @@ jobs:
|
||||
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":"$(date +%s)","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"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
|
||||
|
||||
- name: "Run benchmark: feature (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
@@ -977,7 +924,7 @@ jobs:
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"feature-2","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","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-2"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
- name: "Run benchmark: baseline (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
@@ -991,7 +938,7 @@ jobs:
|
||||
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"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
- name: Stop metrics proxy & generate Grafana URL
|
||||
id: metrics
|
||||
@@ -1120,6 +1067,11 @@ jobs:
|
||||
fi
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --big-blocks"
|
||||
# Read gas ramp blocks count from first baseline run (same for all runs)
|
||||
GAS_RAMP_FILE="$BENCH_WORK_DIR/baseline-1/gas_ramp_blocks.txt"
|
||||
if [ -f "$GAS_RAMP_FILE" ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --gas-ramp-blocks $(cat "$GAS_RAMP_FILE" | tr -d '[:space:]')"
|
||||
fi
|
||||
fi
|
||||
GRAFANA_URL='${{ steps.metrics.outputs.grafana-url }}'
|
||||
if [ -n "$GRAFANA_URL" ]; then
|
||||
@@ -1294,8 +1246,7 @@ jobs:
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const steps_status = [
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
@@ -1330,8 +1281,7 @@ jobs:
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const steps_status = [
|
||||
['building binaries', '${{ steps.build.outcome }}'],
|
||||
['downloading snapshot', '${{ steps.snapshot-download.outcome }}'],
|
||||
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -183,8 +183,6 @@ jobs:
|
||||
*Run:* <https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}|View logs>
|
||||
|
||||
*Action required:* Re-run the workflow or investigate the build failure.
|
||||
|
||||
<@U0AAA8F0JEM> investigate and re-run if flaky
|
||||
SLACK_FOOTER: "paradigmxyz/reth · docker.yml"
|
||||
MSG_MINIMAL: true
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
26
.github/workflows/lint.yml
vendored
26
.github/workflows/lint.yml
vendored
@@ -237,6 +237,31 @@ jobs:
|
||||
- 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
|
||||
|
||||
# Checks that selected crates can compile with power set of features
|
||||
features:
|
||||
name: features
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@clippy
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: cargo install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
- run: |
|
||||
cargo hack check \
|
||||
--package reth-codecs \
|
||||
--package reth-primitives-traits \
|
||||
--package reth-primitives \
|
||||
--feature-powerset \
|
||||
--depth 2
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
# Check crates correctly propagate features
|
||||
feature-propagation:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
@@ -272,6 +297,7 @@ jobs:
|
||||
- typos
|
||||
- grafana
|
||||
- no-test-deps
|
||||
- features
|
||||
- feature-propagation
|
||||
- deny
|
||||
timeout-minutes: 30
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -227,14 +227,14 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
|
||||
- name: Upload signature
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
|
||||
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
|
||||
|
||||
549
AGENTS.md
549
AGENTS.md
@@ -1,549 +0,0 @@
|
||||
# Reth Development Guide for AI Agents
|
||||
|
||||
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
|
||||
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
|
||||
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
|
||||
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
|
||||
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
|
||||
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
|
||||
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
|
||||
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
|
||||
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
- **Modularity**: Each crate can be used as a standalone library
|
||||
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
|
||||
- **Extensibility**: Traits and generic types allow for different chain implementations
|
||||
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style and Standards
|
||||
|
||||
1. **Formatting**: Always use nightly rustfmt
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
```
|
||||
|
||||
2. **Linting**: Run clippy with all features
|
||||
```bash
|
||||
cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features
|
||||
```
|
||||
|
||||
3. **Testing**: Use nextest for faster test execution
|
||||
```bash
|
||||
cargo nextest run --workspace
|
||||
```
|
||||
|
||||
### Common Contribution Types
|
||||
|
||||
Based on actual recent PRs, here are typical contribution patterns:
|
||||
|
||||
#### 1. Small Bug Fixes (1-10 lines)
|
||||
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
|
||||
```rust
|
||||
// Changed a single line to fix logic error
|
||||
- parent_beacon_block_root: parent.parent_beacon_block_root(),
|
||||
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
|
||||
```
|
||||
|
||||
#### 2. Integration with Upstream Changes
|
||||
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
|
||||
```rust
|
||||
// Update code to use new APIs from dependencies
|
||||
- if self.fork_tracker.is_shanghai_activated() {
|
||||
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
|
||||
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
|
||||
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
|
||||
```
|
||||
|
||||
#### 3. Adding Comprehensive Tests
|
||||
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
|
||||
```rust
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth69_peers_can_connect() {
|
||||
// Create test network with specific protocol versions
|
||||
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
|
||||
// Test connection and version negotiation
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Making Components Generic
|
||||
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
|
||||
```rust
|
||||
// Before: Hardcoded to ChainSpec
|
||||
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
|
||||
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
|
||||
|
||||
// After: Generic over any chain spec type
|
||||
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
|
||||
+ where
|
||||
+ C: EthereumHardforks,
|
||||
+ {
|
||||
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
|
||||
```
|
||||
|
||||
#### 5. Resource Management Improvements
|
||||
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
|
||||
```rust
|
||||
// Add cleanup logic on startup
|
||||
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
|
||||
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
|
||||
+ }
|
||||
```
|
||||
|
||||
#### 6. Feature Additions
|
||||
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
|
||||
```rust
|
||||
// Add new filtering policies for transaction announcements
|
||||
pub struct ShardedMempoolAnnouncementFilter<T> {
|
||||
pub inner: T,
|
||||
pub shard_bits: u8,
|
||||
pub node_id: Option<B256>,
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
1. **Unit Tests**: Test individual functions and components
|
||||
2. **Integration Tests**: Test interactions between components
|
||||
3. **Benchmarks**: For performance-critical code
|
||||
4. **Fuzz Tests**: For parsing and serialization code
|
||||
5. **Property Tests**: For checking component correctness on a wide variety of inputs
|
||||
|
||||
Example test structure:
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_component_behavior() {
|
||||
// Arrange
|
||||
let component = Component::new();
|
||||
|
||||
// Act
|
||||
let result = component.operation();
|
||||
|
||||
// Assert
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
|
||||
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
|
||||
3. **Async/Await**: Use tokio for I/O-bound operations
|
||||
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
|
||||
2. **Handle Errors Properly**: Use `?` operator and proper error types
|
||||
|
||||
### What to Avoid
|
||||
|
||||
Based on PR patterns, avoid:
|
||||
|
||||
1. **Large, sweeping changes**: Keep PRs focused and reviewable
|
||||
2. **Mixing unrelated changes**: One logical change per PR
|
||||
3. **Ignoring CI failures**: All checks must pass
|
||||
4. **Incomplete implementations**: Finish features before submitting
|
||||
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
|
||||
|
||||
### CI Requirements
|
||||
|
||||
Before submitting changes, ensure:
|
||||
|
||||
1. **Format Check**: `cargo +nightly fmt --all --check`
|
||||
2. **Clippy**: No warnings
|
||||
3. **Tests Pass**: All unit and integration tests
|
||||
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
|
||||
5. **CLI Docs** (if CLI changed): Run `make update-book-cli` (see below)
|
||||
6. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
|
||||
### CLI Reference Docs (`book` CI Job)
|
||||
|
||||
The CLI reference pages under `docs/vocs/docs/pages/cli/` are **auto-generated** from the `reth` binary's `--help` output. **Do not edit these files manually** — any hand edits will be overwritten and CI will fail regardless.
|
||||
|
||||
When you add, remove, or modify CLI commands, subcommands, or flags, regenerate the CLI docs by running:
|
||||
|
||||
```bash
|
||||
make update-book-cli
|
||||
```
|
||||
|
||||
This builds `reth` in debug mode and runs `docs/cli/update.sh` to regenerate all CLI pages. Commit the resulting changes.
|
||||
|
||||
The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating the docs and running `git diff --exit-code`. If the committed docs don't match the generated output, CI fails. Manually editing these pages is never productive — always use `make update-book-cli`.
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
#### Titles
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
|
||||
|
||||
Examples:
|
||||
- `fix(rpc): correct gas estimation for ERC-20 transfers`
|
||||
- `perf: batch trie updates to reduce cursor overhead`
|
||||
- `feat(engine): add new_payload_interval metric`
|
||||
|
||||
#### Descriptions
|
||||
|
||||
Keep it short. Say what changed and why — nothing more.
|
||||
|
||||
**Do:**
|
||||
- Write 1–3 sentences summarizing the change
|
||||
- Explain _why_ if the diff doesn't make it obvious
|
||||
- Link related issues or EIPs
|
||||
- Include benchmark numbers for perf changes
|
||||
|
||||
**Don't:**
|
||||
- List every file changed — that's what the diff is for
|
||||
- Repeat the title in the body
|
||||
- Add "Files changed" or "Changes" sections
|
||||
- Write walls of text that go stale when the diff is updated
|
||||
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
Closes #<issue>
|
||||
|
||||
<what changed, 1-3 sentences>
|
||||
|
||||
<why, if not obvious from the diff>
|
||||
```
|
||||
|
||||
**Good example:**
|
||||
|
||||
```
|
||||
Closes #16800
|
||||
|
||||
Adds fallback for external IP resolution so node startup doesn't fail
|
||||
when STUN is unreachable. Falls back to the configured default.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```
|
||||
## Summary
|
||||
This PR introduces comprehensive improvements to the IP resolution system.
|
||||
|
||||
## Changes
|
||||
- Modified `crates/net/discv4/src/lib.rs` to add fallback
|
||||
- Modified `crates/net/discv4/src/config.rs` to add default IP
|
||||
- Added tests in `crates/net/discv4/src/tests/ip.rs`
|
||||
|
||||
## Files Changed
|
||||
- crates/net/discv4/src/lib.rs
|
||||
- crates/net/discv4/src/config.rs
|
||||
- crates/net/discv4/src/tests/ip.rs
|
||||
```
|
||||
|
||||
#### Labels and CI
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
* ... and so on, check the available labels for more options.
|
||||
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
|
||||
|
||||
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Logging**: Use `tracing` crate with appropriate levels
|
||||
```rust
|
||||
tracing::debug!(target: "reth::component", ?value, "description");
|
||||
```
|
||||
|
||||
2. **Metrics**: Add metrics for monitoring
|
||||
```rust
|
||||
metrics::counter!("reth_component_operations").increment(1);
|
||||
```
|
||||
|
||||
3. **Test Isolation**: Use separate test databases/directories
|
||||
|
||||
### Finding Where to Contribute
|
||||
|
||||
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
|
||||
2. **Review TODOs**: Search for `TODO` comments in the codebase
|
||||
3. **Improve Tests**: Areas with low test coverage are good targets
|
||||
4. **Documentation**: Improve code comments and documentation
|
||||
5. **Performance**: Profile and optimize hot paths (with benchmarks)
|
||||
|
||||
### Common PR Patterns
|
||||
|
||||
#### Small, Focused Changes
|
||||
Most PRs change only 1-5 files. Examples:
|
||||
- Single-line bug fixes
|
||||
- Adding a missing trait implementation
|
||||
- Updating error messages
|
||||
- Adding test cases for edge conditions
|
||||
|
||||
#### Integration Work
|
||||
When dependencies update (especially revm), code needs updating:
|
||||
- Check for breaking API changes
|
||||
- Update to use new features (like EIP implementations)
|
||||
- Ensure compatibility with new versions
|
||||
|
||||
#### Test Improvements
|
||||
Tests often need expansion for:
|
||||
- New protocol versions (ETH68, ETH69)
|
||||
- Edge cases in state transitions
|
||||
- Network behavior under specific conditions
|
||||
- Concurrent operations
|
||||
|
||||
#### Making Code More Generic
|
||||
Common refactoring pattern:
|
||||
- Replace concrete types with generics
|
||||
- Add trait bounds for flexibility
|
||||
- Enable reuse across different chain types
|
||||
|
||||
#### When to Comment
|
||||
|
||||
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
|
||||
|
||||
##### ✅ DO: Add Value
|
||||
|
||||
**Explain WHY and non-obvious behavior:**
|
||||
```rust
|
||||
// Process must handle allocations atomically to prevent race conditions
|
||||
// between dealloc on drop and concurrent limit checks
|
||||
unsafe impl GlobalAlloc for LimitedAllocator { ... }
|
||||
|
||||
// Binary search requires sorted input. Panics on unsorted slices.
|
||||
fn find_index(items: &[Item], target: &Item) -> Option<usize>
|
||||
|
||||
// Timeout set to 5s to match EVM block processing limits
|
||||
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
```
|
||||
|
||||
**Document constraints and assumptions:**
|
||||
```rust
|
||||
/// Returns heap size estimate.
|
||||
///
|
||||
/// Note: May undercount shared references (Rc/Arc). For precise
|
||||
/// accounting, combine with an allocator-based approach.
|
||||
fn deep_size_of(&self) -> usize
|
||||
```
|
||||
|
||||
**Explain complex logic:**
|
||||
```rust
|
||||
// We reset limits at task start because tokio reuses threads in
|
||||
// spawn_blocking pool. Without reset, second task inherits first
|
||||
// task's allocation count and immediately hits limit.
|
||||
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Describe Changes
|
||||
```rust
|
||||
// ❌ BAD - Describes the change, not the code
|
||||
// Changed from Vec to HashMap for O(1) lookups
|
||||
|
||||
// ✅ GOOD - Explains the decision
|
||||
// HashMap provides O(1) symbol lookups during trace replay
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - PR-specific context
|
||||
// Fix for issue #234 where memory wasn't freed
|
||||
|
||||
// ✅ GOOD - Documents the actual behavior
|
||||
// Explicitly drop allocations before limit check to ensure
|
||||
// accurate accounting
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - States the obvious
|
||||
// Increment counter
|
||||
counter += 1;
|
||||
|
||||
// ✅ GOOD - Explains non-obvious purpose
|
||||
// Track allocations across all threads for global limit enforcement
|
||||
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
```
|
||||
|
||||
✅ **Comment when:**
|
||||
- Non-obvious behavior or edge cases
|
||||
- Performance trade-offs
|
||||
- Safety requirements (unsafe blocks must always be documented)
|
||||
- Limitations or gotchas
|
||||
- Why simpler alternatives don't work
|
||||
|
||||
❌ **Don't comment when:**
|
||||
- Code is self-explanatory
|
||||
- Just restating the code in English
|
||||
- Describing what changed in this PR
|
||||
|
||||
##### The Test: "Will this make sense in 6 months?"
|
||||
|
||||
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
|
||||
|
||||
|
||||
#### Rust Style Guides
|
||||
|
||||
##### Type Ordering in Files
|
||||
|
||||
When defining structs, traits, and functions in a file, follow this ordering convention. The file's primary type (matching the file name) comes first, followed by supporting public types, then private types and helpers.
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
/// The primary type of this file (matches filename).
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// Followed by public auxiliary types that support the primary type
|
||||
|
||||
/// Configuration for the processor.
|
||||
pub struct PayloadProcessorConfig { ... }
|
||||
|
||||
/// Result type returned by processor operations.
|
||||
pub struct ProcessorResult { ... }
|
||||
|
||||
// Followed by public traits related to the primary type
|
||||
|
||||
pub trait ProcessorExt { ... }
|
||||
|
||||
// Followed by private helper types
|
||||
|
||||
struct InternalState { ... }
|
||||
|
||||
// Followed by private helper functions
|
||||
|
||||
fn validate_input() { ... }
|
||||
```
|
||||
|
||||
❌ **Bad**: Adding new traits and auxiliary types **above** the file's primary type (see [#22133](https://github.com/paradigmxyz/reth/pull/22133)):
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ❌ BAD - new auxiliary struct added before the file's main type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
// ❌ BAD - new trait added before the file's main type
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
// The file's primary type is buried below unrelated additions
|
||||
pub struct PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
✅ **Good**: New types go **after** the primary type:
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ✅ The file's primary type stays at the top
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// ✅ Auxiliary types follow the primary type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
impl WaitForCaches for PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
### Example Contribution Workflow
|
||||
|
||||
Let's say you want to fix a bug where external IP resolution fails on startup:
|
||||
|
||||
1. **Create a branch**:
|
||||
```bash
|
||||
git checkout -b fix-external-ip-resolution
|
||||
```
|
||||
|
||||
2. **Find the relevant code**:
|
||||
```bash
|
||||
# Search for IP resolution code
|
||||
rg "external.*ip" --type rust
|
||||
```
|
||||
|
||||
3. **Reason about the problem, when the problem is identified, make the fix**:
|
||||
```rust
|
||||
// In crates/net/discv4/src/lib.rs
|
||||
pub fn resolve_external_ip() -> Option<IpAddr> {
|
||||
// Add fallback mechanism
|
||||
nat::external_ip()
|
||||
.or_else(|| nat::external_ip_from_stun())
|
||||
.or_else(|| Some(DEFAULT_IP))
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add a test**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_external_ip_fallback() {
|
||||
// Test that resolution has proper fallbacks
|
||||
}
|
||||
```
|
||||
|
||||
5. **Run checks** (IMPORTANT!):
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
cargo clippy --workspace --all-features # Make sure WHOLE WORKSPACE compiles!
|
||||
cargo nextest run -p reth-discv4
|
||||
```
|
||||
|
||||
6. **Commit with clear message**:
|
||||
```bash
|
||||
git commit -m "fix: add fallback for external IP resolution
|
||||
|
||||
Previously, node startup could fail if external IP resolution
|
||||
failed. This adds fallback mechanisms to ensure the node can
|
||||
always start with a reasonable default."
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
cargo +nightly fmt --all
|
||||
|
||||
# Run lints
|
||||
cargo +nightly clippy --workspace --all-features
|
||||
|
||||
# Run tests
|
||||
cargo nextest run --workspace
|
||||
|
||||
# Run specific benchmark
|
||||
cargo bench --bench bench_name
|
||||
|
||||
# Build optimized binary
|
||||
cargo build --release
|
||||
|
||||
# Check compilation for all features
|
||||
cargo check --workspace --all-features
|
||||
|
||||
# Check documentation
|
||||
cargo docs --document-private-items
|
||||
|
||||
# Regenerate CLI reference docs (after CLI changes)
|
||||
make update-book-cli
|
||||
```
|
||||
549
CLAUDE.md
Normal file
549
CLAUDE.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# Reth Development Guide for AI Agents
|
||||
|
||||
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
|
||||
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
|
||||
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
|
||||
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
|
||||
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
|
||||
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
|
||||
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
|
||||
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
|
||||
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
- **Modularity**: Each crate can be used as a standalone library
|
||||
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
|
||||
- **Extensibility**: Traits and generic types allow for different chain implementations
|
||||
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style and Standards
|
||||
|
||||
1. **Formatting**: Always use nightly rustfmt
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
```
|
||||
|
||||
2. **Linting**: Run clippy with all features
|
||||
```bash
|
||||
cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features
|
||||
```
|
||||
|
||||
3. **Testing**: Use nextest for faster test execution
|
||||
```bash
|
||||
cargo nextest run --workspace
|
||||
```
|
||||
|
||||
### Common Contribution Types
|
||||
|
||||
Based on actual recent PRs, here are typical contribution patterns:
|
||||
|
||||
#### 1. Small Bug Fixes (1-10 lines)
|
||||
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
|
||||
```rust
|
||||
// Changed a single line to fix logic error
|
||||
- parent_beacon_block_root: parent.parent_beacon_block_root(),
|
||||
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
|
||||
```
|
||||
|
||||
#### 2. Integration with Upstream Changes
|
||||
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
|
||||
```rust
|
||||
// Update code to use new APIs from dependencies
|
||||
- if self.fork_tracker.is_shanghai_activated() {
|
||||
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
|
||||
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
|
||||
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
|
||||
```
|
||||
|
||||
#### 3. Adding Comprehensive Tests
|
||||
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
|
||||
```rust
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth69_peers_can_connect() {
|
||||
// Create test network with specific protocol versions
|
||||
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
|
||||
// Test connection and version negotiation
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Making Components Generic
|
||||
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
|
||||
```rust
|
||||
// Before: Hardcoded to ChainSpec
|
||||
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
|
||||
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
|
||||
|
||||
// After: Generic over any chain spec type
|
||||
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
|
||||
+ where
|
||||
+ C: EthereumHardforks,
|
||||
+ {
|
||||
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
|
||||
```
|
||||
|
||||
#### 5. Resource Management Improvements
|
||||
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
|
||||
```rust
|
||||
// Add cleanup logic on startup
|
||||
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
|
||||
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
|
||||
+ }
|
||||
```
|
||||
|
||||
#### 6. Feature Additions
|
||||
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
|
||||
```rust
|
||||
// Add new filtering policies for transaction announcements
|
||||
pub struct ShardedMempoolAnnouncementFilter<T> {
|
||||
pub inner: T,
|
||||
pub shard_bits: u8,
|
||||
pub node_id: Option<B256>,
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
1. **Unit Tests**: Test individual functions and components
|
||||
2. **Integration Tests**: Test interactions between components
|
||||
3. **Benchmarks**: For performance-critical code
|
||||
4. **Fuzz Tests**: For parsing and serialization code
|
||||
5. **Property Tests**: For checking component correctness on a wide variety of inputs
|
||||
|
||||
Example test structure:
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_component_behavior() {
|
||||
// Arrange
|
||||
let component = Component::new();
|
||||
|
||||
// Act
|
||||
let result = component.operation();
|
||||
|
||||
// Assert
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
|
||||
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
|
||||
3. **Async/Await**: Use tokio for I/O-bound operations
|
||||
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
|
||||
2. **Handle Errors Properly**: Use `?` operator and proper error types
|
||||
|
||||
### What to Avoid
|
||||
|
||||
Based on PR patterns, avoid:
|
||||
|
||||
1. **Large, sweeping changes**: Keep PRs focused and reviewable
|
||||
2. **Mixing unrelated changes**: One logical change per PR
|
||||
3. **Ignoring CI failures**: All checks must pass
|
||||
4. **Incomplete implementations**: Finish features before submitting
|
||||
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
|
||||
|
||||
### CI Requirements
|
||||
|
||||
Before submitting changes, ensure:
|
||||
|
||||
1. **Format Check**: `cargo +nightly fmt --all --check`
|
||||
2. **Clippy**: No warnings
|
||||
3. **Tests Pass**: All unit and integration tests
|
||||
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
|
||||
5. **CLI Docs** (if CLI changed): Run `make update-book-cli` (see below)
|
||||
6. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
|
||||
### CLI Reference Docs (`book` CI Job)
|
||||
|
||||
The CLI reference pages under `docs/vocs/docs/pages/cli/` are **auto-generated** from the `reth` binary's `--help` output. **Do not edit these files manually** — any hand edits will be overwritten and CI will fail regardless.
|
||||
|
||||
When you add, remove, or modify CLI commands, subcommands, or flags, regenerate the CLI docs by running:
|
||||
|
||||
```bash
|
||||
make update-book-cli
|
||||
```
|
||||
|
||||
This builds `reth` in debug mode and runs `docs/cli/update.sh` to regenerate all CLI pages. Commit the resulting changes.
|
||||
|
||||
The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating the docs and running `git diff --exit-code`. If the committed docs don't match the generated output, CI fails. Manually editing these pages is never productive — always use `make update-book-cli`.
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
#### Titles
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
|
||||
|
||||
Examples:
|
||||
- `fix(rpc): correct gas estimation for ERC-20 transfers`
|
||||
- `perf: batch trie updates to reduce cursor overhead`
|
||||
- `feat(engine): add new_payload_interval metric`
|
||||
|
||||
#### Descriptions
|
||||
|
||||
Keep it short. Say what changed and why — nothing more.
|
||||
|
||||
**Do:**
|
||||
- Write 1–3 sentences summarizing the change
|
||||
- Explain _why_ if the diff doesn't make it obvious
|
||||
- Link related issues or EIPs
|
||||
- Include benchmark numbers for perf changes
|
||||
|
||||
**Don't:**
|
||||
- List every file changed — that's what the diff is for
|
||||
- Repeat the title in the body
|
||||
- Add "Files changed" or "Changes" sections
|
||||
- Write walls of text that go stale when the diff is updated
|
||||
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
Closes #<issue>
|
||||
|
||||
<what changed, 1-3 sentences>
|
||||
|
||||
<why, if not obvious from the diff>
|
||||
```
|
||||
|
||||
**Good example:**
|
||||
|
||||
```
|
||||
Closes #16800
|
||||
|
||||
Adds fallback for external IP resolution so node startup doesn't fail
|
||||
when STUN is unreachable. Falls back to the configured default.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```
|
||||
## Summary
|
||||
This PR introduces comprehensive improvements to the IP resolution system.
|
||||
|
||||
## Changes
|
||||
- Modified `crates/net/discv4/src/lib.rs` to add fallback
|
||||
- Modified `crates/net/discv4/src/config.rs` to add default IP
|
||||
- Added tests in `crates/net/discv4/src/tests/ip.rs`
|
||||
|
||||
## Files Changed
|
||||
- crates/net/discv4/src/lib.rs
|
||||
- crates/net/discv4/src/config.rs
|
||||
- crates/net/discv4/src/tests/ip.rs
|
||||
```
|
||||
|
||||
#### Labels and CI
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
* ... and so on, check the available labels for more options.
|
||||
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
|
||||
|
||||
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Logging**: Use `tracing` crate with appropriate levels
|
||||
```rust
|
||||
tracing::debug!(target: "reth::component", ?value, "description");
|
||||
```
|
||||
|
||||
2. **Metrics**: Add metrics for monitoring
|
||||
```rust
|
||||
metrics::counter!("reth_component_operations").increment(1);
|
||||
```
|
||||
|
||||
3. **Test Isolation**: Use separate test databases/directories
|
||||
|
||||
### Finding Where to Contribute
|
||||
|
||||
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
|
||||
2. **Review TODOs**: Search for `TODO` comments in the codebase
|
||||
3. **Improve Tests**: Areas with low test coverage are good targets
|
||||
4. **Documentation**: Improve code comments and documentation
|
||||
5. **Performance**: Profile and optimize hot paths (with benchmarks)
|
||||
|
||||
### Common PR Patterns
|
||||
|
||||
#### Small, Focused Changes
|
||||
Most PRs change only 1-5 files. Examples:
|
||||
- Single-line bug fixes
|
||||
- Adding a missing trait implementation
|
||||
- Updating error messages
|
||||
- Adding test cases for edge conditions
|
||||
|
||||
#### Integration Work
|
||||
When dependencies update (especially revm), code needs updating:
|
||||
- Check for breaking API changes
|
||||
- Update to use new features (like EIP implementations)
|
||||
- Ensure compatibility with new versions
|
||||
|
||||
#### Test Improvements
|
||||
Tests often need expansion for:
|
||||
- New protocol versions (ETH68, ETH69)
|
||||
- Edge cases in state transitions
|
||||
- Network behavior under specific conditions
|
||||
- Concurrent operations
|
||||
|
||||
#### Making Code More Generic
|
||||
Common refactoring pattern:
|
||||
- Replace concrete types with generics
|
||||
- Add trait bounds for flexibility
|
||||
- Enable reuse across different chain types
|
||||
|
||||
#### When to Comment
|
||||
|
||||
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
|
||||
|
||||
##### ✅ DO: Add Value
|
||||
|
||||
**Explain WHY and non-obvious behavior:**
|
||||
```rust
|
||||
// Process must handle allocations atomically to prevent race conditions
|
||||
// between dealloc on drop and concurrent limit checks
|
||||
unsafe impl GlobalAlloc for LimitedAllocator { ... }
|
||||
|
||||
// Binary search requires sorted input. Panics on unsorted slices.
|
||||
fn find_index(items: &[Item], target: &Item) -> Option<usize>
|
||||
|
||||
// Timeout set to 5s to match EVM block processing limits
|
||||
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
```
|
||||
|
||||
**Document constraints and assumptions:**
|
||||
```rust
|
||||
/// Returns heap size estimate.
|
||||
///
|
||||
/// Note: May undercount shared references (Rc/Arc). For precise
|
||||
/// accounting, combine with an allocator-based approach.
|
||||
fn deep_size_of(&self) -> usize
|
||||
```
|
||||
|
||||
**Explain complex logic:**
|
||||
```rust
|
||||
// We reset limits at task start because tokio reuses threads in
|
||||
// spawn_blocking pool. Without reset, second task inherits first
|
||||
// task's allocation count and immediately hits limit.
|
||||
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Describe Changes
|
||||
```rust
|
||||
// ❌ BAD - Describes the change, not the code
|
||||
// Changed from Vec to HashMap for O(1) lookups
|
||||
|
||||
// ✅ GOOD - Explains the decision
|
||||
// HashMap provides O(1) symbol lookups during trace replay
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - PR-specific context
|
||||
// Fix for issue #234 where memory wasn't freed
|
||||
|
||||
// ✅ GOOD - Documents the actual behavior
|
||||
// Explicitly drop allocations before limit check to ensure
|
||||
// accurate accounting
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - States the obvious
|
||||
// Increment counter
|
||||
counter += 1;
|
||||
|
||||
// ✅ GOOD - Explains non-obvious purpose
|
||||
// Track allocations across all threads for global limit enforcement
|
||||
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
```
|
||||
|
||||
✅ **Comment when:**
|
||||
- Non-obvious behavior or edge cases
|
||||
- Performance trade-offs
|
||||
- Safety requirements (unsafe blocks must always be documented)
|
||||
- Limitations or gotchas
|
||||
- Why simpler alternatives don't work
|
||||
|
||||
❌ **Don't comment when:**
|
||||
- Code is self-explanatory
|
||||
- Just restating the code in English
|
||||
- Describing what changed in this PR
|
||||
|
||||
##### The Test: "Will this make sense in 6 months?"
|
||||
|
||||
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
|
||||
|
||||
|
||||
#### Rust Style Guides
|
||||
|
||||
##### Type Ordering in Files
|
||||
|
||||
When defining structs, traits, and functions in a file, follow this ordering convention. The file's primary type (matching the file name) comes first, followed by supporting public types, then private types and helpers.
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
/// The primary type of this file (matches filename).
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// Followed by public auxiliary types that support the primary type
|
||||
|
||||
/// Configuration for the processor.
|
||||
pub struct PayloadProcessorConfig { ... }
|
||||
|
||||
/// Result type returned by processor operations.
|
||||
pub struct ProcessorResult { ... }
|
||||
|
||||
// Followed by public traits related to the primary type
|
||||
|
||||
pub trait ProcessorExt { ... }
|
||||
|
||||
// Followed by private helper types
|
||||
|
||||
struct InternalState { ... }
|
||||
|
||||
// Followed by private helper functions
|
||||
|
||||
fn validate_input() { ... }
|
||||
```
|
||||
|
||||
❌ **Bad**: Adding new traits and auxiliary types **above** the file's primary type (see [#22133](https://github.com/paradigmxyz/reth/pull/22133)):
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ❌ BAD - new auxiliary struct added before the file's main type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
// ❌ BAD - new trait added before the file's main type
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
// The file's primary type is buried below unrelated additions
|
||||
pub struct PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
✅ **Good**: New types go **after** the primary type:
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ✅ The file's primary type stays at the top
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// ✅ Auxiliary types follow the primary type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
impl WaitForCaches for PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
### Example Contribution Workflow
|
||||
|
||||
Let's say you want to fix a bug where external IP resolution fails on startup:
|
||||
|
||||
1. **Create a branch**:
|
||||
```bash
|
||||
git checkout -b fix-external-ip-resolution
|
||||
```
|
||||
|
||||
2. **Find the relevant code**:
|
||||
```bash
|
||||
# Search for IP resolution code
|
||||
rg "external.*ip" --type rust
|
||||
```
|
||||
|
||||
3. **Reason about the problem, when the problem is identified, make the fix**:
|
||||
```rust
|
||||
// In crates/net/discv4/src/lib.rs
|
||||
pub fn resolve_external_ip() -> Option<IpAddr> {
|
||||
// Add fallback mechanism
|
||||
nat::external_ip()
|
||||
.or_else(|| nat::external_ip_from_stun())
|
||||
.or_else(|| Some(DEFAULT_IP))
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add a test**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_external_ip_fallback() {
|
||||
// Test that resolution has proper fallbacks
|
||||
}
|
||||
```
|
||||
|
||||
5. **Run checks** (IMPORTANT!):
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
cargo clippy --workspace --all-features # Make sure WHOLE WORKSPACE compiles!
|
||||
cargo nextest run -p reth-discv4
|
||||
```
|
||||
|
||||
6. **Commit with clear message**:
|
||||
```bash
|
||||
git commit -m "fix: add fallback for external IP resolution
|
||||
|
||||
Previously, node startup could fail if external IP resolution
|
||||
failed. This adds fallback mechanisms to ensure the node can
|
||||
always start with a reasonable default."
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
cargo +nightly fmt --all
|
||||
|
||||
# Run lints
|
||||
cargo +nightly clippy --workspace --all-features
|
||||
|
||||
# Run tests
|
||||
cargo nextest run --workspace
|
||||
|
||||
# Run specific benchmark
|
||||
cargo bench --bench bench_name
|
||||
|
||||
# Build optimized binary
|
||||
cargo build --release
|
||||
|
||||
# Check compilation for all features
|
||||
cargo check --workspace --all-features
|
||||
|
||||
# Check documentation
|
||||
cargo docs --document-private-items
|
||||
|
||||
# Regenerate CLI reference docs (after CLI changes)
|
||||
make update-book-cli
|
||||
```
|
||||
1181
Cargo.lock
generated
1181
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
87
Cargo.toml
87
Cargo.toml
@@ -9,8 +9,8 @@ exclude = [".github/"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"bin/reth-bb/",
|
||||
"bin/reth-bench/",
|
||||
"bin/reth-bench-compare/",
|
||||
"bin/reth/",
|
||||
"crates/storage/rpc-provider/",
|
||||
"crates/chain-state/",
|
||||
@@ -27,7 +27,6 @@ members = [
|
||||
"crates/engine/invalid-block-hooks/",
|
||||
"crates/engine/local",
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/execution-cache/",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
@@ -78,6 +77,8 @@ members = [
|
||||
"crates/payload/primitives/",
|
||||
"crates/payload/validator/",
|
||||
"crates/payload/util/",
|
||||
"crates/primitives-traits/",
|
||||
"crates/primitives/",
|
||||
"crates/prune/db",
|
||||
"crates/prune/prune",
|
||||
"crates/prune/types",
|
||||
@@ -99,6 +100,8 @@ members = [
|
||||
"crates/stages/types/",
|
||||
"crates/static-file/static-file",
|
||||
"crates/static-file/types/",
|
||||
"crates/storage/codecs/",
|
||||
"crates/storage/codecs/derive/",
|
||||
"crates/storage/db-api/",
|
||||
"crates/storage/db-common",
|
||||
"crates/storage/db-models/",
|
||||
@@ -109,6 +112,7 @@ members = [
|
||||
"crates/storage/nippy-jar/",
|
||||
"crates/storage/provider/",
|
||||
"crates/storage/storage-api/",
|
||||
"crates/storage/zstd-compressors/",
|
||||
"crates/tasks/",
|
||||
"crates/tokio-util/",
|
||||
"crates/tracing/",
|
||||
@@ -320,14 +324,15 @@ reth = { path = "bin/reth" }
|
||||
reth-storage-rpc-provider = { path = "crates/storage/rpc-provider" }
|
||||
reth-basic-payload-builder = { path = "crates/payload/basic" }
|
||||
reth-bench = { path = "bin/reth-bench" }
|
||||
reth-bench-compare = { path = "bin/reth-bench-compare" }
|
||||
reth-chain-state = { path = "crates/chain-state" }
|
||||
reth-chainspec = { path = "crates/chainspec", default-features = false }
|
||||
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.1.0", default-features = false }
|
||||
reth-codecs-derive = "0.1.0"
|
||||
reth-codecs = { path = "crates/storage/codecs" }
|
||||
reth-codecs-derive = { path = "crates/storage/codecs/derive" }
|
||||
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 }
|
||||
@@ -343,7 +348,6 @@ reth-downloaders = { path = "crates/net/downloaders" }
|
||||
reth-e2e-test-utils = { path = "crates/e2e-test-utils" }
|
||||
reth-ecies = { path = "crates/net/ecies" }
|
||||
reth-engine-local = { path = "crates/engine/local" }
|
||||
reth-execution-cache = { path = "crates/engine/execution-cache" }
|
||||
reth-engine-primitives = { path = "crates/engine/primitives", default-features = false }
|
||||
reth-engine-tree = { path = "crates/engine/tree" }
|
||||
reth-engine-util = { path = "crates/engine/util" }
|
||||
@@ -395,7 +399,8 @@ 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.1.0", default-features = false }
|
||||
reth-primitives = { path = "crates/primitives", default-features = false, features = ["__internal"] }
|
||||
reth-primitives-traits = { path = "crates/primitives-traits", 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 +416,6 @@ 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.1.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,7 +434,7 @@ 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.1.0", default-features = false }
|
||||
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "36.0.0", default-features = false }
|
||||
@@ -447,42 +451,42 @@ alloy-dyn-abi = "1.5.6"
|
||||
alloy-primitives = { version = "1.5.6", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-sol-types = { version = "1.5.6", default-features = false }
|
||||
|
||||
alloy-chains = { version = "0.2.33", default-features = false }
|
||||
alloy-chains = { version = "0.2.5", default-features = false }
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.0", default-features = false }
|
||||
alloy-evm = { version = "0.30.0", default-features = false }
|
||||
alloy-evm = { version = "0.29.2", 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.5"
|
||||
|
||||
alloy-consensus = { version = "1.8.2", default-features = false }
|
||||
alloy-contract = { version = "1.8.2", default-features = false }
|
||||
alloy-eips = { version = "1.8.2", default-features = false }
|
||||
alloy-genesis = { version = "1.8.2", default-features = false }
|
||||
alloy-json-rpc = { version = "1.8.2", default-features = false }
|
||||
alloy-network = { version = "1.8.2", default-features = false }
|
||||
alloy-network-primitives = { version = "1.8.2", default-features = false }
|
||||
alloy-provider = { version = "1.8.2", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-client = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types = { version = "1.8.2", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.8.2", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.8.2", default-features = false }
|
||||
alloy-serde = { version = "1.8.2", default-features = false }
|
||||
alloy-signer = { version = "1.8.2", default-features = false }
|
||||
alloy-signer-local = { version = "1.8.2", default-features = false }
|
||||
alloy-transport = { version = "1.8.2" }
|
||||
alloy-transport-http = { version = "1.8.2", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.8.2", default-features = false }
|
||||
alloy-transport-ws = { version = "1.8.2", default-features = false }
|
||||
alloy-consensus = { version = "1.7.3", default-features = false }
|
||||
alloy-contract = { version = "1.7.3", default-features = false }
|
||||
alloy-eips = { version = "1.7.3", default-features = false }
|
||||
alloy-genesis = { version = "1.7.3", default-features = false }
|
||||
alloy-json-rpc = { version = "1.7.3", default-features = false }
|
||||
alloy-network = { version = "1.7.3", default-features = false }
|
||||
alloy-network-primitives = { version = "1.7.3", default-features = false }
|
||||
alloy-provider = { version = "1.7.3", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-client = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types = { version = "1.7.3", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.7.3", default-features = false }
|
||||
alloy-serde = { version = "1.7.3", default-features = false }
|
||||
alloy-signer = { version = "1.7.3", default-features = false }
|
||||
alloy-signer-local = { version = "1.7.3", default-features = false }
|
||||
alloy-transport = { version = "1.7.3" }
|
||||
alloy-transport-http = { version = "1.7.3", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.7.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.7.3", default-features = false }
|
||||
|
||||
# op
|
||||
op-alloy-rpc-types = { version = "0.24.0", default-features = false }
|
||||
@@ -534,7 +538,8 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
|
||||
serde_with = { version = "3", default-features = false, features = ["macros"] }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
shlex = "1.3"
|
||||
slotmap = "1"
|
||||
# https://github.com/orlp/slotmap/pull/148
|
||||
slotmap = { git = "https://github.com/DaniPopes/slotmap.git", branch = "dani/shrink-methods" }
|
||||
smallvec = "1"
|
||||
strum = { version = "0.27", default-features = false }
|
||||
strum_macros = "0.27"
|
||||
@@ -579,7 +584,7 @@ futures-util = { version = "0.3", default-features = false }
|
||||
hyper = "1.3"
|
||||
hyper-util = "0.1.5"
|
||||
pin-project = "1.0.12"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "stream"] }
|
||||
tracing-futures = "0.2"
|
||||
tower = "0.5"
|
||||
tower-http = "0.6"
|
||||
@@ -645,7 +650,6 @@ ethereum_ssz_derive = "0.10.1"
|
||||
# allocators
|
||||
jemalloc_pprof = { version = "0.8", default-features = false }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
tikv-jemalloc-sys = "0.6"
|
||||
tikv-jemallocator = "0.6"
|
||||
tracy-client = { version = "0.18.0", features = ["demangle"] }
|
||||
snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
|
||||
@@ -680,6 +684,7 @@ memmap2 = "0.9.4"
|
||||
mev-share-sse = { version = "0.5.0", default-features = false }
|
||||
num-traits = "0.2.15"
|
||||
page_size = "0.6.0"
|
||||
parity-scale-codec = "3.2.1"
|
||||
plain_hasher = "0.2"
|
||||
pretty_assertions = "1.4"
|
||||
ratatui = { version = "0.30", default-features = false }
|
||||
@@ -692,7 +697,7 @@ snap = "1.1.1"
|
||||
socket2 = { version = "0.6", default-features = false }
|
||||
sysinfo = { version = "0.38", default-features = false }
|
||||
tracing-journald = "0.3"
|
||||
tracing-logfmt = "0.3.7"
|
||||
tracing-logfmt = "=0.3.5"
|
||||
tracing-samply = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
tracing-tracy = "0.11"
|
||||
|
||||
2
Makefile
2
Makefile
@@ -70,7 +70,7 @@ build-%-reproducible:
|
||||
LC_ALL=C \
|
||||
TZ=UTC \
|
||||
JEMALLOC_OVERRIDE=/usr/lib/x86_64-linux-gnu/libjemalloc.a \
|
||||
cargo build --bin reth --features "$(FEATURES)" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
cargo build --bin reth --features "$(FEATURES) jemalloc-unprefixed" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
|
||||
.PHONY: build-debug
|
||||
build-debug: ## Build the reth binary into `target/debug` directory.
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
[package]
|
||||
name = "reth-bb"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Reth node configured for big block payload execution"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-ethereum-cli.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-node-ethereum.workspace = true
|
||||
reth-node-builder.workspace = true
|
||||
reth-node-api.workspace = true
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-engine-primitives = { workspace = true, features = ["std"] }
|
||||
reth-engine-tree.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-payload-primitives.workspace = true
|
||||
reth-provider.workspace = true
|
||||
reth-rpc-api.workspace = true
|
||||
reth-rpc-engine-api.workspace = true
|
||||
reth-evm.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-ethereum-forks.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
reth-chain-state.workspace = true
|
||||
reth-errors.workspace = true
|
||||
reth-storage-errors.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-rpc-types = { workspace = true, features = ["engine"] }
|
||||
alloy-primitives.workspace = true
|
||||
alloy-rlp.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eips.workspace = true
|
||||
alloy-evm.workspace = true
|
||||
|
||||
# tracing
|
||||
tracing.workspace = true
|
||||
|
||||
# misc
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
jsonrpsee = { workspace = true, features = ["server", "macros"] }
|
||||
async-trait.workspace = true
|
||||
derive_more.workspace = true
|
||||
crossbeam-channel.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
revm.workspace = true
|
||||
revm-primitives.workspace = true
|
||||
alloy-hardforks.workspace = true
|
||||
metrics.workspace = true
|
||||
|
||||
# std
|
||||
eyre.workspace = true
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc",
|
||||
"asm-keccak",
|
||||
"min-debug-logs",
|
||||
]
|
||||
|
||||
jemalloc = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-node-core/jemalloc",
|
||||
"reth-ethereum-cli/jemalloc",
|
||||
"reth-provider/jemalloc",
|
||||
]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"reth-ethereum-cli/asm-keccak",
|
||||
"reth-node-ethereum/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
"alloy-evm/asm-keccak",
|
||||
"revm/asm-keccak",
|
||||
"revm-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-ethereum-cli/min-debug-logs",
|
||||
"reth-node-core/min-debug-logs",
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
name = "reth-bb"
|
||||
path = "src/main.rs"
|
||||
@@ -1,67 +0,0 @@
|
||||
# reth-bb
|
||||
|
||||
A modified reth node for benchmarking **big block** execution — payloads that merge transactions from multiple consecutive blocks into a single block to simulate high-gas workloads.
|
||||
|
||||
> **Not for production use.** reth-bb disables some consensus-related validations to allow artificially large blocks. It is intended solely for performance benchmarking.
|
||||
|
||||
## How it works
|
||||
|
||||
reth-bb extends the standard Ethereum node with:
|
||||
|
||||
1. **Multi-segment execution** — a custom `reth_newPayload` handler that accepts optional `BigBlockData` alongside the payload. When present, the block is executed in multiple segments, each with its own EVM environment (matching the original blocks that were merged).
|
||||
|
||||
2. **Relaxed consensus** — the gas-limit bound-divisor check and blob gas validation are skipped, since merged blocks exceed single-block limits.
|
||||
|
||||
## Quick start
|
||||
|
||||
The full workflow has four steps: **build** binaries, **generate** big blocks, **start** reth-bb, and **replay** the payloads.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A synced reth datadir for the target chain (e.g. hoodi)
|
||||
- Rust toolchain
|
||||
|
||||
### 1. Build
|
||||
|
||||
```bash
|
||||
cargo build --profile profiling -p reth-bb -p reth-bench
|
||||
```
|
||||
|
||||
### 2. Generate big blocks
|
||||
|
||||
Fetch consecutive blocks from an RPC and merge them until a target gas is reached. Use `--from-block` set to the block number following the one the node is currently synced to (i.e. the next block the node would process):
|
||||
|
||||
```bash
|
||||
reth-bench generate-big-block \
|
||||
--rpc-url https://rpc.hoodi.ethpandaops.io \
|
||||
--chain hoodi \
|
||||
--from-block 910020 \
|
||||
--target-gas 2G \
|
||||
--num-big-blocks 5 \
|
||||
--output-dir /tmp/payloads
|
||||
```
|
||||
|
||||
This produces one JSON file per big block in the output directory.
|
||||
|
||||
### 3. Start reth-bb
|
||||
|
||||
```bash
|
||||
reth-bb node \
|
||||
--datadir /data/reth/hoodi \
|
||||
--chain hoodi \
|
||||
--http --http.api debug,eth \
|
||||
--authrpc.jwtsecret /tmp/jwt.hex \
|
||||
-d
|
||||
```
|
||||
|
||||
### 4. Replay payloads
|
||||
|
||||
```bash
|
||||
reth-bench replay-payloads \
|
||||
--engine-rpc-url http://localhost:8551 \
|
||||
--jwt-secret /tmp/jwt.hex \
|
||||
--payload-dir /tmp/payloads \
|
||||
--reth-new-payload
|
||||
```
|
||||
|
||||
The `--reth-new-payload` flag is required for big blocks — it uses the `reth_newPayload` endpoint which carries the multi-segment execution metadata.
|
||||
@@ -1,570 +0,0 @@
|
||||
//! Big-block executor.
|
||||
//!
|
||||
//! Provides [`BbBlockExecutor`] and [`BbBlockExecutorFactory`] which handle
|
||||
//! segment boundaries within big-block payloads.
|
||||
//!
|
||||
//! [`BbBlockExecutor`] wraps [`EthBlockExecutor`] and intercepts
|
||||
//! `execute_transaction` to apply segment-boundary changes.
|
||||
|
||||
use crate::evm_config::BigBlockSegment;
|
||||
use alloy_eips::eip7685::Requests;
|
||||
use alloy_evm::{
|
||||
block::{
|
||||
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
|
||||
BlockExecutorFor, ExecutableTx, OnStateHook, StateChangeSource, StateDB,
|
||||
},
|
||||
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
|
||||
precompiles::PrecompilesMap,
|
||||
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
|
||||
};
|
||||
use alloy_primitives::B256;
|
||||
use reth_ethereum_primitives::{Receipt, TransactionSigned};
|
||||
use reth_evm_ethereum::RethReceiptBuilder;
|
||||
use revm::{
|
||||
context::{BlockEnv, TxEnv},
|
||||
context_interface::result::{EVMError, HaltReason},
|
||||
handler::PrecompileProvider,
|
||||
interpreter::InterpreterResult,
|
||||
primitives::hardfork::SpecId,
|
||||
Inspector,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbEvmPlan — runtime segment tracking state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Runtime state for segment boundary tracking.
|
||||
pub(crate) struct BbEvmPlan {
|
||||
/// The segment boundaries and environments.
|
||||
pub(crate) segments: Vec<BigBlockSegment>,
|
||||
/// Index of the next segment to switch to (starts at 1).
|
||||
pub(crate) next_segment: usize,
|
||||
/// Number of user transactions executed so far.
|
||||
pub(crate) tx_counter: usize,
|
||||
/// Block hashes to seed for inter-segment BLOCKHASH resolution.
|
||||
/// Includes both prior block hashes and inter-segment hashes.
|
||||
pub(crate) block_hashes_to_seed: Vec<(u64, B256)>,
|
||||
}
|
||||
|
||||
impl BbEvmPlan {
|
||||
/// Creates a new `BbEvmPlan` from segments and hardfork flags.
|
||||
pub(crate) fn new(segments: Vec<BigBlockSegment>) -> Self {
|
||||
// Pre-compute all inter-segment block hashes.
|
||||
let mut block_hashes_to_seed = Vec::new();
|
||||
for seg in segments.iter().skip(1) {
|
||||
let finished_block_number = seg.evm_env.block_env.number.saturating_to::<u64>() - 1;
|
||||
let finished_block_hash = seg.ctx.parent_hash;
|
||||
block_hashes_to_seed.push((finished_block_number, finished_block_hash));
|
||||
}
|
||||
|
||||
Self { segments, next_segment: 1, tx_counter: 0, block_hashes_to_seed }
|
||||
}
|
||||
|
||||
/// Returns the 256 block hashes relevant to a segment with the given block
|
||||
/// number. BLOCKHASH can look back 256 blocks, so we select entries in
|
||||
/// `[block_number - 256, block_number)`.
|
||||
pub(crate) fn hashes_for_block(&self, block_number: u64) -> Vec<(u64, B256)> {
|
||||
let min = block_number.saturating_sub(256);
|
||||
self.block_hashes_to_seed
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(n, _)| *n >= min && *n < block_number)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BbEvmPlan {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BbEvmPlan")
|
||||
.field("segments", &self.segments)
|
||||
.field("next_segment", &self.next_segment)
|
||||
.field("tx_counter", &self.tx_counter)
|
||||
.field("block_hashes_to_seed", &self.block_hashes_to_seed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbBlockExecutor — handles segment boundaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Function pointer that seeds block hashes into the DB's block hash cache.
|
||||
///
|
||||
/// Injected from `ConfigureEvm::create_executor` where the concrete `State<DB>`
|
||||
/// type is known, allowing `BbBlockExecutor` to reseed the ring buffer at
|
||||
/// segment boundaries without requiring additional trait bounds on `DB`.
|
||||
pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
|
||||
|
||||
/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
|
||||
/// changes for big-block execution.
|
||||
///
|
||||
/// At segment boundaries, the inner executor is finished (applying its
|
||||
/// end-of-block logic: post-execution system calls, withdrawal balance
|
||||
/// increments) and a new one is constructed for the next segment (applying
|
||||
/// its start-of-block logic: EIP-2935/EIP-4788 system calls).
|
||||
///
|
||||
/// 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()`.
|
||||
pub(crate) struct BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: Database,
|
||||
{
|
||||
/// The inner executor. `None` transiently during `apply_segment_boundary`.
|
||||
inner: Option<EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder>>,
|
||||
plan: Option<BbEvmPlan>,
|
||||
/// Requests accumulated from segments that have been finished at
|
||||
/// boundaries. Merged into the final result in `finish()`.
|
||||
accumulated_requests: Requests,
|
||||
/// Cumulative gas used by all segments that have been finished at
|
||||
/// boundaries. Added to receipts and the final gas total in `finish()`.
|
||||
gas_used_offset: u64,
|
||||
/// Cumulative blob gas used by all segments that have been finished at
|
||||
/// boundaries.
|
||||
blob_gas_used_offset: u64,
|
||||
/// Shared state hook that survives inner executor finish/reconstruct
|
||||
/// cycles at segment boundaries. Each inner executor receives a
|
||||
/// forwarding hook that delegates to this shared instance.
|
||||
shared_hook: Arc<Mutex<Option<Box<dyn OnStateHook>>>>,
|
||||
/// Callback to reseed block hashes into the DB's cache at segment
|
||||
/// boundaries. See [`BlockHashSeeder`].
|
||||
block_hash_seeder: Option<BlockHashSeeder<DB>>,
|
||||
}
|
||||
|
||||
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: StateDB,
|
||||
I: Inspector<EthEvmContext<DB>>,
|
||||
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
|
||||
EthEvm<DB, I, P>: Evm<
|
||||
DB = DB,
|
||||
Tx = TxEnv,
|
||||
HaltReason = HaltReason,
|
||||
Error = EVMError<DB::Error>,
|
||||
Spec = SpecId,
|
||||
BlockEnv = BlockEnv,
|
||||
>,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
evm: EthEvm<DB, I, P>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
spec: Spec,
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
plan: Option<BbEvmPlan>,
|
||||
block_hash_seeder: Option<BlockHashSeeder<DB>>,
|
||||
) -> Self {
|
||||
let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
|
||||
Self {
|
||||
inner: Some(inner),
|
||||
plan,
|
||||
accumulated_requests: Requests::default(),
|
||||
gas_used_offset: 0,
|
||||
blob_gas_used_offset: 0,
|
||||
shared_hook: Arc::new(Mutex::new(None)),
|
||||
block_hash_seeder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a forwarding `OnStateHook` that delegates to the shared hook.
|
||||
fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
|
||||
let shared = self.shared_hook.clone();
|
||||
Some(Box::new(move |source: StateChangeSource, state: &revm::state::EvmState| {
|
||||
if let Some(hook) = shared.lock().unwrap().as_mut() {
|
||||
hook.on_state(source, state);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const fn inner(&self) -> &EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
|
||||
self.inner.as_ref().expect("inner executor must exist")
|
||||
}
|
||||
|
||||
const fn inner_mut(
|
||||
&mut self,
|
||||
) -> &mut EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
|
||||
self.inner.as_mut().expect("inner executor must exist")
|
||||
}
|
||||
|
||||
fn reseed_block_hashes_for(&mut self, block_number: u64) {
|
||||
let Some(seeder) = self.block_hash_seeder else { return };
|
||||
let hashes = match &self.plan {
|
||||
Some(plan) => plan.hashes_for_block(block_number),
|
||||
None => return,
|
||||
};
|
||||
seeder(self.inner_mut().evm_mut().db_mut(), &hashes);
|
||||
}
|
||||
|
||||
fn maybe_apply_boundary(&mut self) -> Result<(), BlockExecutionError> {
|
||||
loop {
|
||||
let plan = match &self.plan {
|
||||
Some(p) => p,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if plan.next_segment >= plan.segments.len() ||
|
||||
plan.tx_counter != plan.segments[plan.next_segment].start_tx
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.apply_segment_boundary()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_segment_boundary(&mut self) -> Result<(), BlockExecutionError> {
|
||||
let plan = self.plan.as_mut().expect("plan must exist");
|
||||
let seg_idx = plan.next_segment;
|
||||
let prev_seg_idx = seg_idx - 1;
|
||||
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
seg_idx,
|
||||
tx_counter = plan.tx_counter,
|
||||
"Applying segment boundary"
|
||||
);
|
||||
|
||||
// Swap the inner executor's ctx to the finished segment's values so
|
||||
// that finish() applies the correct withdrawals and post-execution
|
||||
// system calls for that segment.
|
||||
let prev_segment = &plan.segments[prev_seg_idx];
|
||||
let prev_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: prev_segment.ctx.parent_hash,
|
||||
parent_beacon_block_root: prev_segment.ctx.parent_beacon_block_root,
|
||||
ommers: prev_segment.ctx.ommers,
|
||||
withdrawals: prev_segment.ctx.withdrawals.clone(),
|
||||
extra_data: prev_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: prev_segment.ctx.tx_count_hint,
|
||||
};
|
||||
|
||||
// Clone the next segment's data before we consume inner.
|
||||
let new_segment = &plan.segments[seg_idx];
|
||||
let new_block_env = new_segment.evm_env.block_env.clone();
|
||||
let mut new_cfg_env = new_segment.evm_env.cfg_env.clone();
|
||||
new_cfg_env.disable_base_fee = true;
|
||||
let new_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: new_segment.ctx.parent_hash,
|
||||
parent_beacon_block_root: new_segment.ctx.parent_beacon_block_root,
|
||||
ommers: new_segment.ctx.ommers,
|
||||
withdrawals: new_segment.ctx.withdrawals.clone(),
|
||||
extra_data: new_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: new_segment.ctx.tx_count_hint,
|
||||
};
|
||||
|
||||
plan.next_segment += 1;
|
||||
|
||||
// Finish the inner executor for the completed segment. This applies
|
||||
// post-execution system calls (EIP-7002/7251) and withdrawal balance
|
||||
// increments via EthBlockExecutor::finish().
|
||||
let mut inner = self.inner.take().expect("inner executor must exist");
|
||||
inner.ctx = prev_ctx;
|
||||
let spec = inner.spec.clone();
|
||||
let receipt_builder = inner.receipt_builder;
|
||||
let (mut evm, result) = inner.finish()?;
|
||||
|
||||
// Receipts already have globally-correct cumulative_gas_used (fixed
|
||||
// up in commit_transaction). Update the offset with this segment's
|
||||
// gas so that subsequent segments' receipts are adjusted correctly.
|
||||
self.gas_used_offset += result.gas_used;
|
||||
self.blob_gas_used_offset += result.blob_gas_used;
|
||||
self.accumulated_requests.extend(result.requests);
|
||||
|
||||
let last_receipt_cumulative =
|
||||
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
|
||||
let seg_block_number = prev_segment.evm_env.block_env.number.saturating_to::<u64>();
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
prev_seg_idx,
|
||||
seg_block_number,
|
||||
segment_gas_used = result.gas_used,
|
||||
gas_used_offset = self.gas_used_offset,
|
||||
last_receipt_cumulative,
|
||||
receipt_count = result.receipts.len(),
|
||||
"Finished segment"
|
||||
);
|
||||
|
||||
// Swap EVM env to the next segment's values (using real gas_limit).
|
||||
let ctx = evm.ctx_mut();
|
||||
ctx.block = new_block_env;
|
||||
ctx.cfg = new_cfg_env;
|
||||
|
||||
// Build a new inner executor for the next segment. gas_used starts
|
||||
// at 0 so the per-transaction gas check uses this segment's real
|
||||
// gas_limit correctly.
|
||||
let mut new_inner = EthBlockExecutor::new(evm, new_ctx, spec, receipt_builder);
|
||||
|
||||
// Carry forward receipts from prior segments.
|
||||
new_inner.receipts = result.receipts;
|
||||
|
||||
// Re-install the forwarding state hook so the parallel state root
|
||||
// task continues to receive state changes.
|
||||
if self.shared_hook.lock().unwrap().is_some() {
|
||||
new_inner.set_state_hook(self.forwarding_hook());
|
||||
}
|
||||
|
||||
self.inner = Some(new_inner);
|
||||
|
||||
// Reseed the block hash cache for the new segment's 256-block window
|
||||
// before applying pre-execution changes (which may use BLOCKHASH).
|
||||
let new_block_number = self.plan.as_ref().unwrap().segments[seg_idx]
|
||||
.evm_env
|
||||
.block_env
|
||||
.number
|
||||
.saturating_to::<u64>();
|
||||
self.reseed_block_hashes_for(new_block_number);
|
||||
|
||||
// Apply pre-execution changes for the new segment (EIP-2935, EIP-4788).
|
||||
self.inner_mut().apply_pre_execution_changes()?;
|
||||
|
||||
trace!(target: "engine::bb::evm", "Started segment {seg_idx}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, DB, I, P, Spec> BlockExecutor for BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: StateDB,
|
||||
I: Inspector<EthEvmContext<DB>>,
|
||||
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
|
||||
EthEvm<DB, I, P>: Evm<
|
||||
DB = DB,
|
||||
Tx = TxEnv,
|
||||
HaltReason = HaltReason,
|
||||
Error = EVMError<DB::Error>,
|
||||
Spec = SpecId,
|
||||
BlockEnv = BlockEnv,
|
||||
>,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
type Transaction = TransactionSigned;
|
||||
type Receipt = Receipt;
|
||||
type Evm = EthEvm<DB, I, P>;
|
||||
type Result = EthTxResult<HaltReason, alloy_consensus::TxType>;
|
||||
|
||||
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
|
||||
// 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()
|
||||
}
|
||||
|
||||
fn execute_transaction_without_commit(
|
||||
&mut self,
|
||||
tx: impl ExecutableTx<Self>,
|
||||
) -> Result<Self::Result, BlockExecutionError> {
|
||||
self.maybe_apply_boundary()?;
|
||||
self.inner_mut().execute_transaction_without_commit(tx)
|
||||
}
|
||||
|
||||
fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, 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
|
||||
// globally-correct values across all segments.
|
||||
let offset = self.gas_used_offset;
|
||||
if offset > 0 &&
|
||||
let Some(receipt) = self.inner_mut().receipts.last_mut()
|
||||
{
|
||||
receipt.cumulative_gas_used += offset;
|
||||
}
|
||||
|
||||
if let Some(plan) = &mut self.plan {
|
||||
plan.tx_counter += 1;
|
||||
}
|
||||
Ok(gas_used)
|
||||
}
|
||||
|
||||
fn finish(
|
||||
mut self,
|
||||
) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
|
||||
// Swap the inner executor's ctx to the last segment's ctx so that
|
||||
// EthBlockExecutor::finish() applies the correct withdrawal balance
|
||||
// increments and post-execution system calls.
|
||||
if let Some(last_seg) = self.plan.as_ref().map(|p| p.segments.last().unwrap()) {
|
||||
let last_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: last_seg.ctx.parent_hash,
|
||||
parent_beacon_block_root: last_seg.ctx.parent_beacon_block_root,
|
||||
ommers: last_seg.ctx.ommers,
|
||||
withdrawals: last_seg.ctx.withdrawals.clone(),
|
||||
extra_data: last_seg.ctx.extra_data.clone(),
|
||||
tx_count_hint: last_seg.ctx.tx_count_hint,
|
||||
};
|
||||
self.inner_mut().ctx = last_ctx;
|
||||
}
|
||||
let inner = self.inner.take().expect("inner executor must exist");
|
||||
let (evm, mut result) = inner.finish()?;
|
||||
|
||||
// Receipts already have globally-correct cumulative_gas_used (fixed
|
||||
// up in commit_transaction). Add the offset to the totals so they
|
||||
// reflect gas across all segments.
|
||||
let last_segment_gas = result.gas_used;
|
||||
result.gas_used += self.gas_used_offset;
|
||||
result.blob_gas_used += self.blob_gas_used_offset;
|
||||
|
||||
let last_receipt_cumulative =
|
||||
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
last_segment_gas,
|
||||
gas_used_offset = self.gas_used_offset,
|
||||
total_gas_used = result.gas_used,
|
||||
last_receipt_cumulative,
|
||||
receipt_count = result.receipts.len(),
|
||||
"Finished final segment"
|
||||
);
|
||||
|
||||
// Merge requests accumulated from earlier segment boundaries into
|
||||
// the final result.
|
||||
if !self.accumulated_requests.is_empty() {
|
||||
let mut merged = self.accumulated_requests;
|
||||
merged.extend(result.requests);
|
||||
result.requests = merged;
|
||||
}
|
||||
|
||||
Ok((evm, result))
|
||||
}
|
||||
|
||||
fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
|
||||
if self.plan.is_some() {
|
||||
// Store the real hook in the shared slot and give the inner
|
||||
// executor a lightweight forwarder. This way the hook survives
|
||||
// inner executor finish/reconstruct cycles at segment boundaries.
|
||||
*self.shared_hook.lock().unwrap() = hook;
|
||||
let fwd = self.forwarding_hook();
|
||||
self.inner_mut().set_state_hook(fwd);
|
||||
} else {
|
||||
self.inner_mut().set_state_hook(hook);
|
||||
}
|
||||
}
|
||||
|
||||
fn evm_mut(&mut self) -> &mut Self::Evm {
|
||||
self.inner_mut().evm_mut()
|
||||
}
|
||||
|
||||
fn evm(&self) -> &Self::Evm {
|
||||
self.inner().evm()
|
||||
}
|
||||
|
||||
fn receipts(&self) -> &[Self::Receipt] {
|
||||
self.inner().receipts()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbBlockExecutorFactory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Block executor factory that produces [`BbBlockExecutor`] for
|
||||
/// boundary-aware big-block execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbBlockExecutorFactory<Spec> {
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
spec: Spec,
|
||||
evm_factory: EthEvmFactory,
|
||||
/// Staged plan consumed by the next [`BbBlockExecutor`].
|
||||
pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
|
||||
}
|
||||
|
||||
impl<Spec> BbBlockExecutorFactory<Spec> {
|
||||
pub fn new(
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
spec: Spec,
|
||||
evm_factory: EthEvmFactory,
|
||||
) -> Self {
|
||||
Self { receipt_builder, spec, evm_factory, staged_plan: Arc::new(Mutex::new(None)) }
|
||||
}
|
||||
|
||||
pub const fn evm_factory(&self) -> &EthEvmFactory {
|
||||
&self.evm_factory
|
||||
}
|
||||
|
||||
pub const fn spec(&self) -> &Spec {
|
||||
&self.spec
|
||||
}
|
||||
|
||||
pub const fn receipt_builder(&self) -> &RethReceiptBuilder {
|
||||
&self.receipt_builder
|
||||
}
|
||||
|
||||
pub(crate) fn stage_plan(&self, plan: BbEvmPlan) {
|
||||
*self.staged_plan.lock().unwrap() = Some(plan);
|
||||
}
|
||||
|
||||
fn take_plan(&self) -> Option<BbEvmPlan> {
|
||||
self.staged_plan.lock().unwrap().take()
|
||||
}
|
||||
|
||||
pub(crate) fn create_executor_with_seeder<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: EthEvm<DB, I, PrecompilesMap>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
block_hash_seeder: Option<BlockHashSeeder<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.take_plan();
|
||||
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, block_hash_seeder)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Spec> BlockExecutorFactory for BbBlockExecutorFactory<Spec>
|
||||
where
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + 'static,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
type EvmFactory = EthEvmFactory;
|
||||
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
|
||||
type Transaction = TransactionSigned;
|
||||
type Receipt = Receipt;
|
||||
|
||||
fn evm_factory(&self) -> &Self::EvmFactory {
|
||||
&self.evm_factory
|
||||
}
|
||||
|
||||
fn create_executor<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: EthEvm<DB, I, PrecompilesMap>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
) -> impl BlockExecutorFor<'a, Self, DB, I>
|
||||
where
|
||||
DB: StateDB + 'a,
|
||||
I: Inspector<EthEvmContext<DB>> + 'a,
|
||||
{
|
||||
let plan = self.take_plan();
|
||||
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
//! Big-block EVM configuration.
|
||||
//!
|
||||
//! Wraps [`EthEvmConfig`] to create executors that handle multi-segment
|
||||
//! big-block execution internally. At transaction boundaries defined by
|
||||
//! [`BigBlockData`], the executor swaps the EVM environment (block env,
|
||||
//! cfg env) and applies pre/post execution changes for each segment.
|
||||
|
||||
pub(crate) use reth_engine_primitives::BigBlockData;
|
||||
|
||||
use crate::{
|
||||
evm::{BbBlockExecutorFactory, BbEvmPlan},
|
||||
BigBlockMap,
|
||||
};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_evm::eth::EthBlockExecutionCtx;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rpc_types::engine::ExecutionData;
|
||||
use core::convert::Infallible;
|
||||
use reth_chainspec::{ChainSpec, EthChainSpec};
|
||||
use reth_ethereum_forks::Hardforks;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_evm::{
|
||||
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
|
||||
NextBlockEnvAttributes,
|
||||
};
|
||||
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
|
||||
use reth_primitives_traits::{SealedBlock, SealedHeader};
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A single execution segment within a big block.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BigBlockSegment {
|
||||
/// Transaction index at which this segment starts.
|
||||
pub start_tx: usize,
|
||||
/// The EVM environment for this segment.
|
||||
pub evm_env: EvmEnv,
|
||||
/// The execution context for this segment.
|
||||
pub ctx: EthBlockExecutionCtx<'static>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbEvmConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// EVM configuration for big-block execution.
|
||||
///
|
||||
/// Wraps [`EthEvmConfig`] and a shared [`BigBlockMap`]. When a big-block
|
||||
/// payload is received, the plan is staged on the [`BbBlockExecutorFactory`]
|
||||
/// 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`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbEvmConfig<C = ChainSpec> {
|
||||
/// The inner Ethereum EVM configuration (used for env computation).
|
||||
pub inner: EthEvmConfig<C>,
|
||||
/// Shared map of pending big-block metadata.
|
||||
pub pending: BigBlockMap,
|
||||
/// Block executor factory for big-block execution.
|
||||
executor_factory: BbBlockExecutorFactory<Arc<C>>,
|
||||
/// Block assembler.
|
||||
block_assembler: EthBlockAssembler<C>,
|
||||
}
|
||||
|
||||
impl<C> BbEvmConfig<C> {
|
||||
/// Creates a new big-block EVM configuration.
|
||||
pub fn new(inner: EthEvmConfig<C>, pending: BigBlockMap) -> Self
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
let chain_spec = inner.chain_spec().clone();
|
||||
let executor_factory = BbBlockExecutorFactory::new(
|
||||
RethReceiptBuilder::default(),
|
||||
chain_spec,
|
||||
EthEvmFactory::default(),
|
||||
);
|
||||
let block_assembler = inner.block_assembler.clone();
|
||||
|
||||
Self { inner, pending, executor_factory, block_assembler }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block hash seeder for State<DB>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Reseeds `State::block_hashes` with the given hashes.
|
||||
///
|
||||
/// This is used as a [`BlockHashSeeder`](crate::evm::BlockHashSeeder) callback,
|
||||
/// injected into [`BbBlockExecutor`](crate::evm::BbBlockExecutor) from
|
||||
/// `ConfigureEvm::create_executor` where the concrete `State<DB>` type is known.
|
||||
/// At each segment boundary the executor calls this to populate the ring buffer
|
||||
/// with the 256 block hashes relevant to the new segment's block number window.
|
||||
fn seed_state_block_hashes<DB>(state: &mut &mut revm::database::State<DB>, hashes: &[(u64, B256)]) {
|
||||
for &(number, hash) in hashes {
|
||||
state.block_hashes.insert(number, hash);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConfigureEvm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> ConfigureEvm for BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
type Primitives = EthPrimitives;
|
||||
type Error = Infallible;
|
||||
type NextBlockEnvCtx = NextBlockEnvAttributes;
|
||||
type BlockExecutorFactory = BbBlockExecutorFactory<Arc<C>>;
|
||||
type BlockAssembler = EthBlockAssembler<C>;
|
||||
|
||||
fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
|
||||
&self.executor_factory
|
||||
}
|
||||
|
||||
fn block_assembler(&self) -> &Self::BlockAssembler {
|
||||
&self.block_assembler
|
||||
}
|
||||
|
||||
fn evm_env(&self, header: &Header) -> Result<EvmEnv<SpecId>, Self::Error> {
|
||||
self.inner.evm_env(header)
|
||||
}
|
||||
|
||||
fn next_evm_env(
|
||||
&self,
|
||||
parent: &Header,
|
||||
attributes: &NextBlockEnvAttributes,
|
||||
) -> Result<EvmEnv, Self::Error> {
|
||||
self.inner.next_evm_env(parent, attributes)
|
||||
}
|
||||
|
||||
fn context_for_block<'a>(
|
||||
&self,
|
||||
block: &'a SealedBlock<reth_ethereum_primitives::Block>,
|
||||
) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
|
||||
self.inner.context_for_block(block)
|
||||
}
|
||||
|
||||
fn context_for_next_block(
|
||||
&self,
|
||||
parent: &SealedHeader,
|
||||
attributes: Self::NextBlockEnvCtx,
|
||||
) -> Result<EthBlockExecutionCtx<'_>, Self::Error> {
|
||||
self.inner.context_for_next_block(parent, attributes)
|
||||
}
|
||||
|
||||
fn create_executor<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: reth_evm::EvmFor<Self, &'a mut revm::database::State<DB>, I>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
) -> impl alloy_evm::block::BlockExecutorFor<
|
||||
'a,
|
||||
Self::BlockExecutorFactory,
|
||||
&'a mut revm::database::State<DB>,
|
||||
I,
|
||||
>
|
||||
where
|
||||
DB: Database,
|
||||
I: reth_evm::InspectorFor<Self, &'a mut revm::database::State<DB>> + 'a,
|
||||
{
|
||||
// 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>),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConfigureEngineEvm — intercepts payload methods for big blocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> ConfigureEngineEvm<ExecutionData> for BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result<EvmEnvFor<Self>, Self::Error> {
|
||||
let payload_hash = payload.block_hash();
|
||||
let has_plan = self.pending.lock().unwrap().contains_key(&payload_hash);
|
||||
|
||||
if has_plan {
|
||||
// Compute the env from the first segment BEFORE removing the
|
||||
// entry (stage_plan_for_payload removes it).
|
||||
let first_exec_data = {
|
||||
let pending = self.pending.lock().unwrap();
|
||||
let bb_data = pending.get(&payload_hash).unwrap();
|
||||
bb_data.env_switches[0].1.clone()
|
||||
};
|
||||
let mut env = self.inner.evm_env_for_payload(&first_exec_data)?;
|
||||
|
||||
// Disable basefee validation: transactions from different
|
||||
// original blocks may have gas prices below the big block's
|
||||
// effective basefee.
|
||||
env.cfg_env.disable_base_fee = true;
|
||||
|
||||
// Now stage the plan on the factory (removes the entry).
|
||||
self.stage_plan_for_payload(&payload_hash);
|
||||
|
||||
Ok(env)
|
||||
} else {
|
||||
self.inner.evm_env_for_payload(payload)
|
||||
}
|
||||
}
|
||||
|
||||
fn context_for_payload<'a>(
|
||||
&self,
|
||||
payload: &'a ExecutionData,
|
||||
) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
|
||||
self.inner.context_for_payload(payload)
|
||||
}
|
||||
|
||||
fn tx_iterator_for_payload(
|
||||
&self,
|
||||
payload: &ExecutionData,
|
||||
) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
|
||||
self.inner.tx_iterator_for_payload(payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plan construction and staging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
/// Takes the big-block plan for a payload hash, builds a [`BbEvmPlan`],
|
||||
/// and stages it on the factory.
|
||||
///
|
||||
/// Must be called before `evm_with_env` is invoked for this payload.
|
||||
/// 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 bb = match self.pending.lock().unwrap().remove(payload_hash) {
|
||||
Some(bb) => bb,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let segments: Vec<_> = bb
|
||||
.env_switches
|
||||
.into_iter()
|
||||
.map(|(start_tx, exec_data)| {
|
||||
let evm_env = self.inner.evm_env_for_payload(&exec_data).unwrap();
|
||||
let ctx = self.inner.context_for_payload(&exec_data).unwrap();
|
||||
let ctx = EthBlockExecutionCtx {
|
||||
tx_count_hint: ctx.tx_count_hint,
|
||||
parent_hash: ctx.parent_hash,
|
||||
parent_beacon_block_root: ctx.parent_beacon_block_root,
|
||||
ommers: &[],
|
||||
withdrawals: ctx.withdrawals.map(|w| std::borrow::Cow::Owned(w.into_owned())),
|
||||
extra_data: ctx.extra_data,
|
||||
};
|
||||
BigBlockSegment { start_tx, evm_env, ctx }
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
target: "engine::bb",
|
||||
?payload_hash,
|
||||
segments = segments.len(),
|
||||
seed_hashes = bb.prior_block_hashes.len(),
|
||||
"Staging multi-segment plan"
|
||||
);
|
||||
|
||||
let mut plan = BbEvmPlan::new(segments);
|
||||
|
||||
// Add prior block hashes to the seeding list.
|
||||
plan.block_hashes_to_seed.extend(bb.prior_block_hashes);
|
||||
|
||||
plan.block_hashes_to_seed.sort_unstable_by_key(|(n, _)| *n);
|
||||
|
||||
self.executor_factory.stage_plan(plan);
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
//! reth-bb: a modified reth node for benchmarking big block execution.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
mod evm;
|
||||
mod evm_config;
|
||||
|
||||
use alloy_primitives::B256;
|
||||
|
||||
use alloy_rpc_types::engine::{ExecutionData, ForkchoiceState, ForkchoiceUpdated};
|
||||
use async_trait::async_trait;
|
||||
use clap::Parser;
|
||||
use evm_config::{BbEvmConfig, BigBlockData};
|
||||
use jsonrpsee::core::RpcResult;
|
||||
use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardforks, Hardforks};
|
||||
use reth_engine_primitives::ConsensusEngineHandle;
|
||||
use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, interface::Cli};
|
||||
use reth_ethereum_consensus::EthBeaconConsensus;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_node_api::{AddOnsContext, FullNodeComponents, NodeTypes, PayloadTypes};
|
||||
use reth_node_builder::{
|
||||
components::{
|
||||
BasicPayloadServiceBuilder, ComponentsBuilder, ConsensusBuilder, ExecutorBuilder,
|
||||
},
|
||||
node::FullNodeTypes,
|
||||
rpc::{
|
||||
BasicEngineApiBuilder, BasicEngineValidatorBuilder, EngineApiBuilder, EngineValidatorAddOn,
|
||||
EngineValidatorBuilder, PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, RpcHandle,
|
||||
RpcHooks,
|
||||
},
|
||||
BuilderContext, Node,
|
||||
};
|
||||
use reth_node_ethereum::{
|
||||
EthEngineTypes, EthereumEngineValidatorBuilder, EthereumEthApiBuilder, EthereumNetworkBuilder,
|
||||
EthereumNode, EthereumPayloadBuilder, EthereumPoolBuilder,
|
||||
};
|
||||
use reth_payload_primitives::ExecutionPayload;
|
||||
use reth_primitives_traits::SealedBlock;
|
||||
use reth_provider::EthStorage;
|
||||
use reth_rpc_api::{RethNewPayloadInput, RethPayloadStatus};
|
||||
use reth_rpc_engine_api::EngineApiError;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::{info, trace};
|
||||
|
||||
/// Shared map for big block data, keyed by payload hash.
|
||||
pub type BigBlockMap = Arc<Mutex<HashMap<B256, BigBlockData<ExecutionData>>>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom RPC trait for big-block payloads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Big-block extension of the `reth_` engine API.
|
||||
#[jsonrpsee::proc_macros::rpc(server, namespace = "reth")]
|
||||
pub trait BbRethEngineApi {
|
||||
/// `reth_newPayload` with optional big-block data.
|
||||
#[method(name = "newPayload")]
|
||||
async fn reth_new_payload(
|
||||
&self,
|
||||
payload: RethNewPayloadInput<ExecutionData>,
|
||||
wait_for_persistence: Option<bool>,
|
||||
wait_for_caches: Option<bool>,
|
||||
big_block_data: Option<BigBlockData<ExecutionData>>,
|
||||
) -> RpcResult<RethPayloadStatus>;
|
||||
|
||||
/// `reth_forkchoiceUpdated` – pass-through.
|
||||
#[method(name = "forkchoiceUpdated")]
|
||||
async fn reth_forkchoice_updated(
|
||||
&self,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
) -> RpcResult<ForkchoiceUpdated>;
|
||||
}
|
||||
|
||||
/// Server-side implementation of `BbRethEngineApi`.
|
||||
#[derive(Debug)]
|
||||
struct BbRethEngineApiHandler {
|
||||
pending: BigBlockMap,
|
||||
engine: ConsensusEngineHandle<EthEngineTypes>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BbRethEngineApiServer for BbRethEngineApiHandler {
|
||||
async fn reth_new_payload(
|
||||
&self,
|
||||
input: RethNewPayloadInput<ExecutionData>,
|
||||
wait_for_persistence: Option<bool>,
|
||||
wait_for_caches: Option<bool>,
|
||||
big_block_data: Option<BigBlockData<ExecutionData>>,
|
||||
) -> RpcResult<RethPayloadStatus> {
|
||||
let wait_for_persistence = wait_for_persistence.unwrap_or(true);
|
||||
let wait_for_caches = wait_for_caches.unwrap_or(true);
|
||||
trace!(
|
||||
target: "rpc::engine",
|
||||
wait_for_persistence,
|
||||
wait_for_caches,
|
||||
has_big_block_data = big_block_data.is_some(),
|
||||
"Serving bb reth_newPayload"
|
||||
);
|
||||
|
||||
let payload = match input {
|
||||
RethNewPayloadInput::ExecutionData(data) => data,
|
||||
RethNewPayloadInput::BlockRlp(rlp) => {
|
||||
let block = alloy_rlp::Decodable::decode(&mut rlp.as_ref())
|
||||
.map_err(|err| EngineApiError::Internal(Box::new(err)))?;
|
||||
<EthEngineTypes as PayloadTypes>::block_to_payload(SealedBlock::new_unhashed(block))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(data) = big_block_data {
|
||||
let hash = ExecutionPayload::block_hash(&payload);
|
||||
self.pending.lock().unwrap().insert(hash, data);
|
||||
}
|
||||
|
||||
let (status, timings) = self
|
||||
.engine
|
||||
.reth_new_payload(payload, wait_for_persistence, wait_for_caches)
|
||||
.await
|
||||
.map_err(EngineApiError::from)?;
|
||||
|
||||
Ok(RethPayloadStatus {
|
||||
status,
|
||||
latency_us: timings.latency.as_micros() as u64,
|
||||
persistence_wait_us: timings.persistence_wait.map(|d| d.as_micros() as u64),
|
||||
execution_cache_wait_us: timings.execution_cache_wait.map(|d| d.as_micros() as u64),
|
||||
sparse_trie_wait_us: timings.sparse_trie_wait.map(|d| d.as_micros() as u64),
|
||||
})
|
||||
}
|
||||
|
||||
async fn reth_forkchoice_updated(
|
||||
&self,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
) -> RpcResult<ForkchoiceUpdated> {
|
||||
trace!(target: "rpc::engine", "Serving reth_forkchoiceUpdated");
|
||||
self.engine
|
||||
.fork_choice_updated(forkchoice_state, None)
|
||||
.await
|
||||
.map_err(|e| EngineApiError::from(e).into())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node add-ons wrapper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Add-ons for the big-block node.
|
||||
#[derive(Debug)]
|
||||
pub struct BbAddOns {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl BbAddOns {
|
||||
const fn new(pending: BigBlockMap) -> Self {
|
||||
Self { pending }
|
||||
}
|
||||
|
||||
fn make_rpc_add_ons<N: FullNodeComponents>(
|
||||
&self,
|
||||
) -> RpcAddOns<
|
||||
N,
|
||||
EthereumEthApiBuilder,
|
||||
EthereumEngineValidatorBuilder,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>,
|
||||
>
|
||||
where
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
{
|
||||
RpcAddOns::new(
|
||||
EthereumEthApiBuilder::default(),
|
||||
EthereumEngineValidatorBuilder::default(),
|
||||
BasicEngineApiBuilder::default(),
|
||||
BasicEngineValidatorBuilder::default(),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> reth_node_api::NodeAddOns<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
|
||||
Payload = EthEngineTypes,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type Handle =
|
||||
RpcHandle<N, <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi>;
|
||||
|
||||
async fn launch_add_ons(self, ctx: AddOnsContext<'_, N>) -> eyre::Result<Self::Handle> {
|
||||
let engine_handle = ctx.beacon_engine_handle.clone();
|
||||
let pending = self.pending.clone();
|
||||
let rpc_add_ons = self.make_rpc_add_ons::<N>();
|
||||
|
||||
rpc_add_ons
|
||||
.launch_add_ons_with(ctx, move |container| {
|
||||
let handler = BbRethEngineApiHandler { pending, engine: engine_handle };
|
||||
let bb_module = BbRethEngineApiServer::into_rpc(handler);
|
||||
container.auth_module.replace_auth_methods(bb_module.remove_context())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> RethRpcAddOns<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
|
||||
Payload = EthEngineTypes,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type EthApi = <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi;
|
||||
|
||||
fn hooks_mut(&mut self) -> &mut RpcHooks<N, Self::EthApi> {
|
||||
unimplemented!("BbAddOns does not support dynamic hook mutation")
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> EngineValidatorAddOn<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type ValidatorBuilder = BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>;
|
||||
|
||||
fn engine_validator_builder(&self) -> Self::ValidatorBuilder {
|
||||
BasicEngineValidatorBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom executor builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Executor builder that creates a [`BbEvmConfig`].
|
||||
#[derive(Debug)]
|
||||
pub struct BbExecutorBuilder {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl<Node> ExecutorBuilder<Node> for BbExecutorBuilder
|
||||
where
|
||||
Node: FullNodeTypes<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: reth_ethereum_forks::Hardforks
|
||||
+ alloy_evm::eth::spec::EthExecutorSpec
|
||||
+ EthereumHardforks,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
type EVM = BbEvmConfig<<Node::Types as NodeTypes>::ChainSpec>;
|
||||
|
||||
async fn build_evm(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::EVM> {
|
||||
Ok(BbEvmConfig::new(EthEvmConfig::new(ctx.chain_spec()), self.pending))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Node type for big block execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbNode {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl BbNode {
|
||||
const fn new(pending: BigBlockMap) -> Self {
|
||||
Self { pending }
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTypes for BbNode {
|
||||
type Primitives = EthPrimitives;
|
||||
type ChainSpec = ChainSpec;
|
||||
type Storage = EthStorage;
|
||||
type Payload = EthEngineTypes;
|
||||
}
|
||||
|
||||
impl<N> Node<N> for BbNode
|
||||
where
|
||||
N: FullNodeTypes<Types = Self>,
|
||||
{
|
||||
type ComponentsBuilder = ComponentsBuilder<
|
||||
N,
|
||||
EthereumPoolBuilder,
|
||||
BasicPayloadServiceBuilder<EthereumPayloadBuilder>,
|
||||
EthereumNetworkBuilder,
|
||||
BbExecutorBuilder,
|
||||
BbConsensusBuilder,
|
||||
>;
|
||||
|
||||
type AddOns = BbAddOns;
|
||||
|
||||
fn components_builder(&self) -> Self::ComponentsBuilder {
|
||||
EthereumNode::components()
|
||||
.executor(BbExecutorBuilder { pending: self.pending.clone() })
|
||||
.consensus(BbConsensusBuilder)
|
||||
}
|
||||
|
||||
fn add_ons(&self) -> Self::AddOns {
|
||||
BbAddOns::new(self.pending.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Consensus builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Consensus builder for big block execution.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct BbConsensusBuilder;
|
||||
|
||||
impl<Node> ConsensusBuilder<Node> for BbConsensusBuilder
|
||||
where
|
||||
Node: FullNodeTypes<
|
||||
Types: NodeTypes<ChainSpec: EthChainSpec + EthereumHardforks, Primitives = EthPrimitives>,
|
||||
>,
|
||||
{
|
||||
type Consensus = Arc<EthBeaconConsensus<<Node::Types as NodeTypes>::ChainSpec>>;
|
||||
|
||||
async fn build_consensus(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Consensus> {
|
||||
Ok(Arc::new(
|
||||
EthBeaconConsensus::new(ctx.chain_spec())
|
||||
.with_skip_gas_limit_ramp_check(true)
|
||||
.with_skip_blob_gas_used_check(true)
|
||||
.with_skip_requests_hash_check(true),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn main() {
|
||||
reth_cli_util::sigsegv_handler::install();
|
||||
|
||||
if std::env::var_os("RUST_BACKTRACE").is_none() {
|
||||
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
|
||||
}
|
||||
|
||||
let pending: BigBlockMap = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
if let Err(err) = Cli::<EthereumChainSpecParser>::parse().run(async move |builder, _| {
|
||||
info!(target: "reth::cli", "Launching big block node");
|
||||
let handle = builder.launch_node(BbNode::new(pending.clone())).await?;
|
||||
|
||||
handle.wait_for_node_exit().await
|
||||
}) {
|
||||
eprintln!("Error: {err:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
99
bin/reth-bench-compare/Cargo.toml
Normal file
99
bin/reth-bench-compare/Cargo.toml
Normal file
@@ -0,0 +1,99 @@
|
||||
[package]
|
||||
name = "reth-bench-compare"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Automated reth benchmark comparison between git references"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "reth-bench-compare"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-cli-runner.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-provider = { workspace = true, features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
|
||||
alloy-rpc-types-eth.workspace = true
|
||||
alloy-transport-ws.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
|
||||
# CLI and argument parsing
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
eyre.workspace = true
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
# Time handling
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
|
||||
# CSV handling
|
||||
csv.workspace = true
|
||||
|
||||
# Process management
|
||||
ctrlc.workspace = true
|
||||
shlex.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.31", features = ["signal", "process"] }
|
||||
|
||||
[features]
|
||||
default = ["jemalloc"]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
jemalloc = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-node-core/jemalloc",
|
||||
]
|
||||
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
|
||||
tracy = [
|
||||
"reth-node-core/tracy",
|
||||
"reth-tracing/tracy",
|
||||
]
|
||||
|
||||
min-error-logs = [
|
||||
"tracing/release_max_level_error",
|
||||
"reth-node-core/min-error-logs",
|
||||
]
|
||||
min-warn-logs = [
|
||||
"tracing/release_max_level_warn",
|
||||
"reth-node-core/min-warn-logs",
|
||||
]
|
||||
min-info-logs = [
|
||||
"tracing/release_max_level_info",
|
||||
"reth-node-core/min-info-logs",
|
||||
]
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-node-core/min-debug-logs",
|
||||
]
|
||||
min-trace-logs = [
|
||||
"tracing/release_max_level_trace",
|
||||
"reth-node-core/min-trace-logs",
|
||||
]
|
||||
|
||||
# no-op feature flag for CI matrices
|
||||
ethereum = []
|
||||
50
bin/reth-bench-compare/README.md
Normal file
50
bin/reth-bench-compare/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# reth-bench-compare
|
||||
|
||||
Compare reth performance between two git references.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
reth-bench-compare \
|
||||
--baseline-ref main \
|
||||
--feature-ref my-feature \
|
||||
--blocks 100 \
|
||||
--wait-for-persistence
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `--baseline-ref <REF>` | Git reference for baseline | - | Yes |
|
||||
| `--feature-ref <REF>` | Git reference to compare | - | Yes |
|
||||
| `--blocks <N>` | Number of blocks to benchmark | `100` | No |
|
||||
| `--chain <CHAIN>` | Chain to benchmark | `mainnet` | No |
|
||||
| `--datadir <PATH>` | Data directory path | OS-specific | No |
|
||||
| `--rpc-url <URL>` | RPC endpoint for block data | Chain default | No |
|
||||
| `--output-dir <PATH>` | Output directory | `./reth-bench-compare` | No |
|
||||
| `--wait-for-persistence` | Wait for block persistence | `false` | No |
|
||||
| `--persistence-threshold <N>` | Wait after every N+1 blocks | `2` | No |
|
||||
| `--wait-time <DURATION>` | Fixed delay (legacy) | - | No |
|
||||
| `--warmup-blocks <N>` | Cache warmup blocks | Same as `--blocks` | No |
|
||||
| `--draw` | Generate charts (needs Python/uv) | `false` | No |
|
||||
| `--profile` | Enable CPU profiling (needs samply) | `false` | No |
|
||||
| `-vvvv` | Debug logging | Info | No |
|
||||
| `--features <FEATURES>` | Extra Rust features for both builds | - | No |
|
||||
| `--rustflags <FLAGS>` | RUSTFLAGS for both builds | `-C target-cpu=native` | No |
|
||||
| `--baseline-features <FEATURES>` | Features for baseline only | Inherits `--features` | No |
|
||||
| `--feature-features <FEATURES>` | Features for feature only | Inherits `--features` | No |
|
||||
| `--baseline-rustflags <FLAGS>` | RUSTFLAGS for baseline only | Inherits `--rustflags` | No |
|
||||
| `--feature-rustflags <FLAGS>` | RUSTFLAGS for feature only | Inherits `--rustflags` | No |
|
||||
| `--baseline-args <ARGS>` | Extra args for baseline node | - | No |
|
||||
| `--feature-args <ARGS>` | Extra args for feature node | - | No |
|
||||
| `--metrics-port <PORT>` | Metrics endpoint port | `5005` | No |
|
||||
| `--sudo` | Run with elevated privileges | `false` | No |
|
||||
|
||||
## Output
|
||||
|
||||
Results in `./reth-bench-compare/results/<timestamp>/`:
|
||||
- `comparison_report.json` - Metrics comparison
|
||||
- `per_block_comparison.csv` - Per-block statistics
|
||||
- `baseline/` and `feature/` - Individual run results
|
||||
- `latency_comparison.png` - Chart (if `--draw` used)
|
||||
307
bin/reth-bench-compare/src/benchmark.rs
Normal file
307
bin/reth-bench-compare/src/benchmark.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
//! Benchmark execution using reth-bench.
|
||||
|
||||
use crate::cli::Args;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::{
|
||||
fs::File as AsyncFile,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Manages benchmark execution using reth-bench
|
||||
pub(crate) struct BenchmarkRunner {
|
||||
rpc_url: String,
|
||||
jwt_secret: String,
|
||||
wait_time: Option<String>,
|
||||
wait_for_persistence: bool,
|
||||
persistence_threshold: Option<u64>,
|
||||
warmup_blocks: u64,
|
||||
}
|
||||
|
||||
impl BenchmarkRunner {
|
||||
/// Create a new `BenchmarkRunner` from CLI arguments
|
||||
pub(crate) fn new(args: &Args) -> Self {
|
||||
Self {
|
||||
rpc_url: args.get_rpc_url(),
|
||||
jwt_secret: args.jwt_secret_path().to_string_lossy().to_string(),
|
||||
wait_time: args.wait_time.clone(),
|
||||
wait_for_persistence: args.wait_for_persistence,
|
||||
persistence_threshold: args.persistence_threshold,
|
||||
warmup_blocks: args.get_warmup_blocks(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear filesystem caches (page cache, dentries, and inodes)
|
||||
pub(crate) async fn clear_fs_caches() -> Result<()> {
|
||||
info!("Clearing filesystem caches...");
|
||||
|
||||
// First sync to ensure all pending writes are flushed
|
||||
let sync_output =
|
||||
Command::new("sync").output().await.wrap_err("Failed to execute sync command")?;
|
||||
|
||||
if !sync_output.status.success() {
|
||||
return Err(eyre!("sync command failed"));
|
||||
}
|
||||
|
||||
// Drop caches - requires sudo/root permissions
|
||||
// 3 = drop pagecache, dentries, and inodes
|
||||
let drop_caches_cmd = Command::new("sudo")
|
||||
.args(["-n", "sh", "-c", "echo 3 > /proc/sys/vm/drop_caches"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match drop_caches_cmd {
|
||||
Ok(output) if output.status.success() => {
|
||||
info!("Successfully cleared filesystem caches");
|
||||
Ok(())
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains("sudo: a password is required") {
|
||||
warn!("Unable to clear filesystem caches: sudo password required");
|
||||
warn!(
|
||||
"For optimal benchmarking, configure passwordless sudo for cache clearing:"
|
||||
);
|
||||
warn!(" echo '$USER ALL=(ALL) NOPASSWD: /bin/sh -c echo\\\\ [0-9]\\\\ \\\\>\\\\ /proc/sys/vm/drop_caches' | sudo tee /etc/sudoers.d/drop_caches");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(eyre!("Failed to clear filesystem caches: {}", stderr))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Unable to clear filesystem caches: {}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a warmup benchmark for cache warming
|
||||
pub(crate) async fn run_warmup(&self, from_block: u64) -> Result<()> {
|
||||
let to_block = from_block + self.warmup_blocks;
|
||||
info!(
|
||||
"Running warmup benchmark from block {} to {} ({} blocks)",
|
||||
from_block, to_block, self.warmup_blocks
|
||||
);
|
||||
|
||||
// Build the reth-bench command for warmup (no output flag)
|
||||
let mut cmd = Command::new("reth-bench");
|
||||
cmd.args([
|
||||
"new-payload-fcu",
|
||||
"--rpc-url",
|
||||
&self.rpc_url,
|
||||
"--jwt-secret",
|
||||
&self.jwt_secret,
|
||||
"--from",
|
||||
&from_block.to_string(),
|
||||
"--to",
|
||||
&to_block.to_string(),
|
||||
"--wait-time=0ms", // Warmup should avoid persistence waits.
|
||||
]);
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
debug!("Executing warmup reth-bench command: {:?}", cmd);
|
||||
|
||||
// Execute the warmup benchmark
|
||||
let mut child = cmd.spawn().wrap_err("Failed to start warmup reth-bench process")?;
|
||||
|
||||
// Stream output at debug level
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[WARMUP] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[WARMUP] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let status = child.wait().await.wrap_err("Failed to wait for warmup reth-bench")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(eyre!("Warmup reth-bench failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
info!("Warmup completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a benchmark for the specified block range
|
||||
pub(crate) async fn run_benchmark(
|
||||
&self,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
output_dir: &Path,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
"Running benchmark from block {} to {} (output: {:?})",
|
||||
from_block, to_block, output_dir
|
||||
);
|
||||
|
||||
// Ensure output directory exists
|
||||
std::fs::create_dir_all(output_dir)
|
||||
.wrap_err_with(|| format!("Failed to create output directory: {output_dir:?}"))?;
|
||||
|
||||
// Create log file path for reth-bench output
|
||||
let log_file_path = output_dir.join("reth_bench.log");
|
||||
info!("reth-bench logs will be saved to: {:?}", log_file_path);
|
||||
|
||||
// Build the reth-bench command
|
||||
let mut cmd = Command::new("reth-bench");
|
||||
cmd.args([
|
||||
"new-payload-fcu",
|
||||
"--rpc-url",
|
||||
&self.rpc_url,
|
||||
"--jwt-secret",
|
||||
&self.jwt_secret,
|
||||
"--from",
|
||||
&from_block.to_string(),
|
||||
"--to",
|
||||
&to_block.to_string(),
|
||||
"--output",
|
||||
&output_dir.to_string_lossy(),
|
||||
]);
|
||||
|
||||
// Configure wait mode: both can be used together
|
||||
// When both are set: wait at least wait_time, and also wait for persistence if needed
|
||||
if let Some(ref wait_time) = self.wait_time {
|
||||
cmd.args(["--wait-time", wait_time]);
|
||||
}
|
||||
if self.wait_for_persistence {
|
||||
cmd.arg("--wait-for-persistence");
|
||||
|
||||
// Add persistence threshold if specified
|
||||
if let Some(threshold) = self.persistence_threshold {
|
||||
cmd.args(["--persistence-threshold", &threshold.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing reth-bench command: {:?}", cmd);
|
||||
|
||||
// Execute the benchmark
|
||||
let mut child = cmd.spawn().wrap_err("Failed to start reth-bench process")?;
|
||||
|
||||
// Capture stdout and stderr for error reporting
|
||||
let stdout_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Stream stdout with prefix at debug level, capture for error reporting, and write to log
|
||||
// file
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let stdout_lines_clone = stdout_lines.clone();
|
||||
let log_file = AsyncFile::create(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to create log file: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-BENCH] {}", line);
|
||||
if let Ok(mut captured) = stdout_lines_clone.lock() {
|
||||
captured.push(line.clone());
|
||||
}
|
||||
// Write to log file (reth-bench output already has timestamps if needed)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Stream stderr with prefix at debug level, capture for error reporting, and write to log
|
||||
// file
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let stderr_lines_clone = stderr_lines.clone();
|
||||
let log_file = AsyncFile::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to open log file for stderr: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-BENCH] {}", line);
|
||||
if let Ok(mut captured) = stderr_lines_clone.lock() {
|
||||
captured.push(line.clone());
|
||||
}
|
||||
// Write to log file (reth-bench output already has timestamps if needed)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let status = child.wait().await.wrap_err("Failed to wait for reth-bench")?;
|
||||
|
||||
if !status.success() {
|
||||
// Print all captured output when command fails
|
||||
error!("reth-bench failed with exit code: {:?}", status.code());
|
||||
|
||||
if let Ok(stdout) = stdout_lines.lock() &&
|
||||
!stdout.is_empty()
|
||||
{
|
||||
error!("reth-bench stdout:");
|
||||
for line in stdout.iter() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(stderr) = stderr_lines.lock() &&
|
||||
!stderr.is_empty()
|
||||
{
|
||||
error!("reth-bench stderr:");
|
||||
for line in stderr.iter() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(eyre!("reth-bench failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
info!("Benchmark completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
1028
bin/reth-bench-compare/src/cli.rs
Normal file
1028
bin/reth-bench-compare/src/cli.rs
Normal file
File diff suppressed because it is too large
Load Diff
723
bin/reth-bench-compare/src/comparison.rs
Normal file
723
bin/reth-bench-compare/src/comparison.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
//! Results comparison and report generation.
|
||||
|
||||
use crate::cli::Args;
|
||||
use chrono::{DateTime, Utc};
|
||||
use csv::Reader;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Manages comparison between baseline and feature reference results
|
||||
pub(crate) struct ComparisonGenerator {
|
||||
output_dir: PathBuf,
|
||||
timestamp: String,
|
||||
baseline_ref_name: String,
|
||||
feature_ref_name: String,
|
||||
baseline_results: Option<BenchmarkResults>,
|
||||
feature_results: Option<BenchmarkResults>,
|
||||
baseline_command: Option<String>,
|
||||
feature_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents the results from a single benchmark run
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BenchmarkResults {
|
||||
pub ref_name: String,
|
||||
pub combined_latency_data: Vec<CombinedLatencyRow>,
|
||||
pub summary: BenchmarkSummary,
|
||||
pub start_timestamp: Option<DateTime<Utc>>,
|
||||
pub end_timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Combined latency CSV row structure
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct CombinedLatencyRow {
|
||||
pub block_number: u64,
|
||||
#[serde(default)]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub new_payload_latency: u128,
|
||||
}
|
||||
|
||||
/// Total gas CSV row structure
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct TotalGasRow {
|
||||
pub block_number: u64,
|
||||
#[serde(default)]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub time: u128,
|
||||
}
|
||||
|
||||
/// Summary statistics for a benchmark run.
|
||||
///
|
||||
/// Latencies are derived from per-block `engine_newPayload` timings (converted from µs to ms):
|
||||
/// - `mean_new_payload_latency_ms`: arithmetic mean latency across blocks.
|
||||
/// - `median_new_payload_latency_ms`: p50 latency across blocks.
|
||||
/// - `p90_new_payload_latency_ms` / `p99_new_payload_latency_ms`: tail latencies across blocks.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct BenchmarkSummary {
|
||||
pub total_blocks: u64,
|
||||
pub total_gas_used: u64,
|
||||
pub total_duration_ms: u128,
|
||||
pub mean_new_payload_latency_ms: f64,
|
||||
pub median_new_payload_latency_ms: f64,
|
||||
pub p90_new_payload_latency_ms: f64,
|
||||
pub p99_new_payload_latency_ms: f64,
|
||||
pub gas_per_second: f64,
|
||||
pub blocks_per_second: f64,
|
||||
pub min_block_number: u64,
|
||||
pub max_block_number: u64,
|
||||
}
|
||||
|
||||
/// Comparison report between two benchmark runs
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ComparisonReport {
|
||||
pub timestamp: String,
|
||||
pub baseline: RefInfo,
|
||||
pub feature: RefInfo,
|
||||
pub comparison_summary: ComparisonSummary,
|
||||
pub per_block_comparisons: Vec<BlockComparison>,
|
||||
}
|
||||
|
||||
/// Information about a reference in the comparison
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct RefInfo {
|
||||
pub ref_name: String,
|
||||
pub summary: BenchmarkSummary,
|
||||
pub start_timestamp: Option<DateTime<Utc>>,
|
||||
pub end_timestamp: Option<DateTime<Utc>>,
|
||||
pub reth_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Summary of the comparison between references.
|
||||
///
|
||||
/// Percent deltas are `(feature - baseline) / baseline * 100`:
|
||||
/// - `new_payload_latency_mean_change_percent`: percent changes of the per-block means.
|
||||
/// - `new_payload_latency_p50_change_percent` / p90 / p99: percent changes of the respective
|
||||
/// per-block percentiles.
|
||||
/// - `per_block_latency_change_mean_percent` / `per_block_latency_change_median_percent` are the
|
||||
/// mean and median of per-block percent deltas (feature vs baseline), capturing block-level
|
||||
/// drift.
|
||||
/// - `per_block_latency_change_std_dev_percent`: standard deviation of per-block percent changes,
|
||||
/// measuring consistency of performance changes across blocks.
|
||||
/// - `new_payload_total_latency_change_percent` is the percent change of the total newPayload time
|
||||
/// across the run.
|
||||
///
|
||||
/// Positive means slower/higher; negative means faster/lower.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ComparisonSummary {
|
||||
pub per_block_latency_change_mean_percent: f64,
|
||||
pub per_block_latency_change_median_percent: f64,
|
||||
pub per_block_latency_change_std_dev_percent: f64,
|
||||
pub new_payload_total_latency_change_percent: f64,
|
||||
pub new_payload_latency_mean_change_percent: f64,
|
||||
pub new_payload_latency_p50_change_percent: f64,
|
||||
pub new_payload_latency_p90_change_percent: f64,
|
||||
pub new_payload_latency_p99_change_percent: f64,
|
||||
pub gas_per_second_change_percent: f64,
|
||||
pub blocks_per_second_change_percent: f64,
|
||||
}
|
||||
|
||||
/// Per-block comparison data
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct BlockComparison {
|
||||
pub block_number: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub baseline_new_payload_latency: u128,
|
||||
pub feature_new_payload_latency: u128,
|
||||
pub new_payload_latency_change_percent: f64,
|
||||
}
|
||||
|
||||
impl ComparisonGenerator {
|
||||
/// Create a new comparison generator
|
||||
pub(crate) fn new(args: &Args) -> Self {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||
|
||||
Self {
|
||||
output_dir: args.output_dir_path(),
|
||||
timestamp,
|
||||
baseline_ref_name: args.baseline_ref.clone(),
|
||||
feature_ref_name: args.feature_ref.clone(),
|
||||
baseline_results: None,
|
||||
feature_results: None,
|
||||
baseline_command: None,
|
||||
feature_command: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the output directory for a specific reference
|
||||
pub(crate) fn get_ref_output_dir(&self, ref_type: &str) -> PathBuf {
|
||||
self.output_dir.join("results").join(&self.timestamp).join(ref_type)
|
||||
}
|
||||
|
||||
/// Get the main output directory for this comparison run
|
||||
pub(crate) fn get_output_dir(&self) -> PathBuf {
|
||||
self.output_dir.join("results").join(&self.timestamp)
|
||||
}
|
||||
|
||||
/// Add benchmark results for a reference
|
||||
pub(crate) fn add_ref_results(&mut self, ref_type: &str, output_path: &Path) -> Result<()> {
|
||||
let ref_name = match ref_type {
|
||||
"baseline" => &self.baseline_ref_name,
|
||||
"feature" => &self.feature_ref_name,
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
};
|
||||
|
||||
let results = self.load_benchmark_results(ref_name, output_path)?;
|
||||
|
||||
match ref_type {
|
||||
"baseline" => self.baseline_results = Some(results),
|
||||
"feature" => self.feature_results = Some(results),
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
info!("Loaded benchmark results for {} reference", ref_type);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the benchmark run timestamps for a reference
|
||||
pub(crate) fn set_ref_timestamps(
|
||||
&mut self,
|
||||
ref_type: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
) -> Result<()> {
|
||||
match ref_type {
|
||||
"baseline" => {
|
||||
if let Some(ref mut results) = self.baseline_results {
|
||||
results.start_timestamp = Some(start);
|
||||
results.end_timestamp = Some(end);
|
||||
} else {
|
||||
return Err(eyre!("Baseline results not loaded yet"));
|
||||
}
|
||||
}
|
||||
"feature" => {
|
||||
if let Some(ref mut results) = self.feature_results {
|
||||
results.start_timestamp = Some(start);
|
||||
results.end_timestamp = Some(end);
|
||||
} else {
|
||||
return Err(eyre!("Feature results not loaded yet"));
|
||||
}
|
||||
}
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the reth command for a reference
|
||||
pub(crate) fn set_ref_command(&mut self, ref_type: &str, command: String) -> Result<()> {
|
||||
match ref_type {
|
||||
"baseline" => {
|
||||
self.baseline_command = Some(command);
|
||||
}
|
||||
"feature" => {
|
||||
self.feature_command = Some(command);
|
||||
}
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate the final comparison report
|
||||
pub(crate) async fn generate_comparison_report(&self) -> Result<()> {
|
||||
info!("Generating comparison report...");
|
||||
|
||||
let baseline =
|
||||
self.baseline_results.as_ref().ok_or_else(|| eyre!("Baseline results not loaded"))?;
|
||||
|
||||
let feature =
|
||||
self.feature_results.as_ref().ok_or_else(|| eyre!("Feature results not loaded"))?;
|
||||
|
||||
let per_block_comparisons = self.calculate_per_block_comparisons(baseline, feature)?;
|
||||
let comparison_summary = self.calculate_comparison_summary(
|
||||
&baseline.summary,
|
||||
&feature.summary,
|
||||
&per_block_comparisons,
|
||||
)?;
|
||||
|
||||
let report = ComparisonReport {
|
||||
timestamp: self.timestamp.clone(),
|
||||
baseline: RefInfo {
|
||||
ref_name: baseline.ref_name.clone(),
|
||||
summary: baseline.summary.clone(),
|
||||
start_timestamp: baseline.start_timestamp,
|
||||
end_timestamp: baseline.end_timestamp,
|
||||
reth_command: self.baseline_command.clone(),
|
||||
},
|
||||
feature: RefInfo {
|
||||
ref_name: feature.ref_name.clone(),
|
||||
summary: feature.summary.clone(),
|
||||
start_timestamp: feature.start_timestamp,
|
||||
end_timestamp: feature.end_timestamp,
|
||||
reth_command: self.feature_command.clone(),
|
||||
},
|
||||
comparison_summary,
|
||||
per_block_comparisons,
|
||||
};
|
||||
|
||||
// Write reports
|
||||
self.write_comparison_reports(&report).await?;
|
||||
|
||||
// Print summary to console
|
||||
self.print_comparison_summary(&report);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load benchmark results from CSV files
|
||||
fn load_benchmark_results(
|
||||
&self,
|
||||
ref_name: &str,
|
||||
output_path: &Path,
|
||||
) -> Result<BenchmarkResults> {
|
||||
let combined_latency_path = output_path.join("combined_latency.csv");
|
||||
let total_gas_path = output_path.join("total_gas.csv");
|
||||
|
||||
let combined_latency_data = self.load_combined_latency_csv(&combined_latency_path)?;
|
||||
let total_gas_data = self.load_total_gas_csv(&total_gas_path)?;
|
||||
|
||||
let summary = self.calculate_summary(&combined_latency_data, &total_gas_data)?;
|
||||
|
||||
Ok(BenchmarkResults {
|
||||
ref_name: ref_name.to_string(),
|
||||
combined_latency_data,
|
||||
summary,
|
||||
start_timestamp: None,
|
||||
end_timestamp: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load combined latency CSV data
|
||||
fn load_combined_latency_csv(&self, path: &Path) -> Result<Vec<CombinedLatencyRow>> {
|
||||
let mut reader = Reader::from_path(path)
|
||||
.wrap_err_with(|| format!("Failed to open combined latency CSV: {path:?}"))?;
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
let row: CombinedLatencyRow = result
|
||||
.wrap_err_with(|| format!("Failed to parse combined latency row in {path:?}"))?;
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(eyre!("No data found in combined latency CSV: {:?}", path));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Load total gas CSV data
|
||||
fn load_total_gas_csv(&self, path: &Path) -> Result<Vec<TotalGasRow>> {
|
||||
let mut reader = Reader::from_path(path)
|
||||
.wrap_err_with(|| format!("Failed to open total gas CSV: {path:?}"))?;
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
let row: TotalGasRow =
|
||||
result.wrap_err_with(|| format!("Failed to parse total gas row in {path:?}"))?;
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(eyre!("No data found in total gas CSV: {:?}", path));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Calculate summary statistics for a benchmark run.
|
||||
///
|
||||
/// Computes latency statistics from per-block `new_payload_latency` values in `combined_data`
|
||||
/// (converting from µs to ms), and throughput metrics using the total run duration from
|
||||
/// `total_gas_data`. Percentiles (p50/p90/p99) use linear interpolation on sorted latencies.
|
||||
fn calculate_summary(
|
||||
&self,
|
||||
combined_data: &[CombinedLatencyRow],
|
||||
total_gas_data: &[TotalGasRow],
|
||||
) -> Result<BenchmarkSummary> {
|
||||
if combined_data.is_empty() || total_gas_data.is_empty() {
|
||||
return Err(eyre!("Cannot calculate summary for empty data"));
|
||||
}
|
||||
|
||||
let total_blocks = combined_data.len() as u64;
|
||||
let total_gas_used: u64 = combined_data.iter().map(|r| r.gas_used).sum();
|
||||
|
||||
let total_duration_ms = total_gas_data.last().unwrap().time / 1000; // Convert microseconds to milliseconds
|
||||
|
||||
let latencies_ms: Vec<f64> =
|
||||
combined_data.iter().map(|r| r.new_payload_latency as f64 / 1000.0).collect();
|
||||
let mean_new_payload_latency_ms: f64 =
|
||||
latencies_ms.iter().sum::<f64>() / total_blocks as f64;
|
||||
|
||||
let mut sorted_latencies_ms = latencies_ms;
|
||||
sorted_latencies_ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
let median_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.5);
|
||||
let p90_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.9);
|
||||
let p99_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.99);
|
||||
|
||||
let total_duration_seconds = total_duration_ms as f64 / 1000.0;
|
||||
let gas_per_second = if total_duration_seconds > f64::EPSILON {
|
||||
total_gas_used as f64 / total_duration_seconds
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let blocks_per_second = if total_duration_seconds > f64::EPSILON {
|
||||
total_blocks as f64 / total_duration_seconds
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let min_block_number = combined_data.first().unwrap().block_number;
|
||||
let max_block_number = combined_data.last().unwrap().block_number;
|
||||
|
||||
Ok(BenchmarkSummary {
|
||||
total_blocks,
|
||||
total_gas_used,
|
||||
total_duration_ms,
|
||||
mean_new_payload_latency_ms,
|
||||
median_new_payload_latency_ms,
|
||||
p90_new_payload_latency_ms,
|
||||
p99_new_payload_latency_ms,
|
||||
gas_per_second,
|
||||
blocks_per_second,
|
||||
min_block_number,
|
||||
max_block_number,
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate comparison summary between baseline and feature
|
||||
fn calculate_comparison_summary(
|
||||
&self,
|
||||
baseline: &BenchmarkSummary,
|
||||
feature: &BenchmarkSummary,
|
||||
per_block_comparisons: &[BlockComparison],
|
||||
) -> Result<ComparisonSummary> {
|
||||
let calc_percent_change = |baseline: f64, feature: f64| -> f64 {
|
||||
if baseline.abs() > f64::EPSILON {
|
||||
((feature - baseline) / baseline) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate per-block statistics. "Per-block" means: for each block, compute the percent
|
||||
// change (feature - baseline) / baseline * 100, then calculate statistics across those
|
||||
// per-block percent changes. This captures how consistently the feature performs relative
|
||||
// to baseline across all blocks.
|
||||
let per_block_percent_changes: Vec<f64> =
|
||||
per_block_comparisons.iter().map(|c| c.new_payload_latency_change_percent).collect();
|
||||
let per_block_latency_change_mean_percent = if per_block_percent_changes.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
per_block_percent_changes.iter().sum::<f64>() / per_block_percent_changes.len() as f64
|
||||
};
|
||||
let per_block_latency_change_median_percent = if per_block_percent_changes.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
let mut sorted = per_block_percent_changes.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
percentile(&sorted, 0.5)
|
||||
};
|
||||
let per_block_latency_change_std_dev_percent =
|
||||
calculate_std_dev(&per_block_percent_changes, per_block_latency_change_mean_percent);
|
||||
|
||||
let baseline_total_latency_ms =
|
||||
baseline.mean_new_payload_latency_ms * baseline.total_blocks as f64;
|
||||
let feature_total_latency_ms =
|
||||
feature.mean_new_payload_latency_ms * feature.total_blocks as f64;
|
||||
let new_payload_total_latency_change_percent =
|
||||
calc_percent_change(baseline_total_latency_ms, feature_total_latency_ms);
|
||||
|
||||
Ok(ComparisonSummary {
|
||||
per_block_latency_change_mean_percent,
|
||||
per_block_latency_change_median_percent,
|
||||
per_block_latency_change_std_dev_percent,
|
||||
new_payload_total_latency_change_percent,
|
||||
new_payload_latency_mean_change_percent: calc_percent_change(
|
||||
baseline.mean_new_payload_latency_ms,
|
||||
feature.mean_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p50_change_percent: calc_percent_change(
|
||||
baseline.median_new_payload_latency_ms,
|
||||
feature.median_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p90_change_percent: calc_percent_change(
|
||||
baseline.p90_new_payload_latency_ms,
|
||||
feature.p90_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p99_change_percent: calc_percent_change(
|
||||
baseline.p99_new_payload_latency_ms,
|
||||
feature.p99_new_payload_latency_ms,
|
||||
),
|
||||
gas_per_second_change_percent: calc_percent_change(
|
||||
baseline.gas_per_second,
|
||||
feature.gas_per_second,
|
||||
),
|
||||
blocks_per_second_change_percent: calc_percent_change(
|
||||
baseline.blocks_per_second,
|
||||
feature.blocks_per_second,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate per-block comparisons
|
||||
fn calculate_per_block_comparisons(
|
||||
&self,
|
||||
baseline: &BenchmarkResults,
|
||||
feature: &BenchmarkResults,
|
||||
) -> Result<Vec<BlockComparison>> {
|
||||
let mut baseline_map: HashMap<u64, &CombinedLatencyRow> = HashMap::new();
|
||||
for row in &baseline.combined_latency_data {
|
||||
baseline_map.insert(row.block_number, row);
|
||||
}
|
||||
|
||||
let mut comparisons = Vec::new();
|
||||
for feature_row in &feature.combined_latency_data {
|
||||
if let Some(baseline_row) = baseline_map.get(&feature_row.block_number) {
|
||||
let calc_percent_change = |baseline: u128, feature: u128| -> f64 {
|
||||
if baseline > 0 {
|
||||
((feature as f64 - baseline as f64) / baseline as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
let comparison = BlockComparison {
|
||||
block_number: feature_row.block_number,
|
||||
transaction_count: feature_row.transaction_count,
|
||||
gas_used: feature_row.gas_used,
|
||||
baseline_new_payload_latency: baseline_row.new_payload_latency,
|
||||
feature_new_payload_latency: feature_row.new_payload_latency,
|
||||
new_payload_latency_change_percent: calc_percent_change(
|
||||
baseline_row.new_payload_latency,
|
||||
feature_row.new_payload_latency,
|
||||
),
|
||||
};
|
||||
comparisons.push(comparison);
|
||||
} else {
|
||||
warn!("Block {} not found in baseline data", feature_row.block_number);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(comparisons)
|
||||
}
|
||||
|
||||
/// Write comparison reports to files
|
||||
async fn write_comparison_reports(&self, report: &ComparisonReport) -> Result<()> {
|
||||
let report_dir = self.output_dir.join("results").join(&self.timestamp);
|
||||
fs::create_dir_all(&report_dir)
|
||||
.wrap_err_with(|| format!("Failed to create report directory: {report_dir:?}"))?;
|
||||
|
||||
// Write JSON report
|
||||
let json_path = report_dir.join("comparison_report.json");
|
||||
let json_content = serde_json::to_string_pretty(report)
|
||||
.wrap_err("Failed to serialize comparison report to JSON")?;
|
||||
fs::write(&json_path, json_content)
|
||||
.wrap_err_with(|| format!("Failed to write JSON report: {json_path:?}"))?;
|
||||
|
||||
// Write CSV report for per-block comparisons
|
||||
let csv_path = report_dir.join("per_block_comparison.csv");
|
||||
let mut writer = csv::Writer::from_path(&csv_path)
|
||||
.wrap_err_with(|| format!("Failed to create CSV writer: {csv_path:?}"))?;
|
||||
|
||||
for comparison in &report.per_block_comparisons {
|
||||
writer.serialize(comparison).wrap_err("Failed to write comparison row to CSV")?;
|
||||
}
|
||||
writer.flush().wrap_err("Failed to flush CSV writer")?;
|
||||
|
||||
info!("Comparison reports written to: {:?}", report_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print comparison summary to console
|
||||
fn print_comparison_summary(&self, report: &ComparisonReport) {
|
||||
// Parse and format timestamp nicely
|
||||
let formatted_timestamp = if let Ok(dt) = chrono::DateTime::parse_from_str(
|
||||
&format!("{} +0000", report.timestamp.replace('_', " ")),
|
||||
"%Y%m%d %H%M%S %z",
|
||||
) {
|
||||
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
} else {
|
||||
// Fallback to original if parsing fails
|
||||
report.timestamp.clone()
|
||||
};
|
||||
|
||||
println!("\n=== BENCHMARK COMPARISON SUMMARY ===");
|
||||
println!("Timestamp: {formatted_timestamp}");
|
||||
println!("Baseline: {}", report.baseline.ref_name);
|
||||
println!("Feature: {}", report.feature.ref_name);
|
||||
println!();
|
||||
|
||||
let summary = &report.comparison_summary;
|
||||
|
||||
println!("Performance Changes:");
|
||||
println!(
|
||||
" NewPayload Latency per-block mean change: {:+.2}%",
|
||||
summary.per_block_latency_change_mean_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency per-block median change: {:+.2}%",
|
||||
summary.per_block_latency_change_median_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency per-block std dev: {:.2}%",
|
||||
summary.per_block_latency_change_std_dev_percent
|
||||
);
|
||||
println!(
|
||||
" Total newPayload time change: {:+.2}%",
|
||||
summary.new_payload_total_latency_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency mean: {:+.2}%",
|
||||
summary.new_payload_latency_mean_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p50: {:+.2}%",
|
||||
summary.new_payload_latency_p50_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p90: {:+.2}%",
|
||||
summary.new_payload_latency_p90_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p99: {:+.2}%",
|
||||
summary.new_payload_latency_p99_change_percent
|
||||
);
|
||||
println!(
|
||||
" Gas/Second: {:+.2}%",
|
||||
summary.gas_per_second_change_percent
|
||||
);
|
||||
println!(
|
||||
" Blocks/Second: {:+.2}%",
|
||||
summary.blocks_per_second_change_percent
|
||||
);
|
||||
println!();
|
||||
|
||||
println!("Baseline Summary:");
|
||||
let baseline = &report.baseline.summary;
|
||||
println!(
|
||||
" Blocks: {} (blocks {} to {}), Gas: {}, Duration: {:.2}s",
|
||||
baseline.total_blocks,
|
||||
baseline.min_block_number,
|
||||
baseline.max_block_number,
|
||||
baseline.total_gas_used,
|
||||
baseline.total_duration_ms as f64 / 1000.0
|
||||
);
|
||||
println!(" NewPayload latency (ms):");
|
||||
println!(
|
||||
" mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}",
|
||||
baseline.mean_new_payload_latency_ms,
|
||||
baseline.median_new_payload_latency_ms,
|
||||
baseline.p90_new_payload_latency_ms,
|
||||
baseline.p99_new_payload_latency_ms
|
||||
);
|
||||
if let (Some(start), Some(end)) =
|
||||
(&report.baseline.start_timestamp, &report.baseline.end_timestamp)
|
||||
{
|
||||
println!(
|
||||
" Started: {}, Ended: {}",
|
||||
start.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
end.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
if let Some(ref cmd) = report.baseline.reth_command {
|
||||
println!(" Command: {}", cmd);
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("Feature Summary:");
|
||||
let feature = &report.feature.summary;
|
||||
println!(
|
||||
" Blocks: {} (blocks {} to {}), Gas: {}, Duration: {:.2}s",
|
||||
feature.total_blocks,
|
||||
feature.min_block_number,
|
||||
feature.max_block_number,
|
||||
feature.total_gas_used,
|
||||
feature.total_duration_ms as f64 / 1000.0
|
||||
);
|
||||
println!(" NewPayload latency (ms):");
|
||||
println!(
|
||||
" mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}",
|
||||
feature.mean_new_payload_latency_ms,
|
||||
feature.median_new_payload_latency_ms,
|
||||
feature.p90_new_payload_latency_ms,
|
||||
feature.p99_new_payload_latency_ms
|
||||
);
|
||||
if let (Some(start), Some(end)) =
|
||||
(&report.feature.start_timestamp, &report.feature.end_timestamp)
|
||||
{
|
||||
println!(
|
||||
" Started: {}, Ended: {}",
|
||||
start.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
end.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
if let Some(ref cmd) = report.feature.reth_command {
|
||||
println!(" Command: {}", cmd);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate standard deviation from a set of values and their mean.
|
||||
///
|
||||
/// Computes the population standard deviation using the formula:
|
||||
/// `sqrt(sum((x - mean)²) / n)`
|
||||
///
|
||||
/// Returns 0.0 for empty input.
|
||||
fn calculate_std_dev(values: &[f64], mean: f64) -> f64 {
|
||||
if values.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let variance = values
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let diff = x - mean;
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f64>() /
|
||||
values.len() as f64;
|
||||
|
||||
variance.sqrt()
|
||||
}
|
||||
|
||||
/// Calculate percentile using linear interpolation on a sorted slice.
|
||||
///
|
||||
/// Computes `rank = percentile × (n - 1)` where n is the array length. If the rank falls
|
||||
/// between two indices, linearly interpolates between those values. For example, with 100 values,
|
||||
/// p90 computes rank = 0.9 × 99 = 89.1, then returns `values[89] × 0.9 + values[90] × 0.1`.
|
||||
///
|
||||
/// Returns 0.0 for empty input.
|
||||
fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
|
||||
if sorted_values.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let clamped = percentile.clamp(0.0, 1.0);
|
||||
let max_index = sorted_values.len() - 1;
|
||||
let rank = clamped * max_index as f64;
|
||||
let lower = rank.floor() as usize;
|
||||
let upper = rank.ceil() as usize;
|
||||
|
||||
if lower == upper {
|
||||
sorted_values[lower]
|
||||
} else {
|
||||
let weight = rank - lower as f64;
|
||||
sorted_values[lower].mul_add(1.0 - weight, sorted_values[upper] * weight)
|
||||
}
|
||||
}
|
||||
305
bin/reth-bench-compare/src/compilation.rs
Normal file
305
bin/reth-bench-compare/src/compilation.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
//! Compilation operations for reth and reth-bench.
|
||||
|
||||
use crate::git::GitManager;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use std::{fs, path::PathBuf, process::Command};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Manages compilation operations for reth components
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CompilationManager {
|
||||
repo_root: String,
|
||||
output_dir: PathBuf,
|
||||
git_manager: GitManager,
|
||||
}
|
||||
|
||||
impl CompilationManager {
|
||||
/// Create a new `CompilationManager`
|
||||
pub(crate) const fn new(
|
||||
repo_root: String,
|
||||
output_dir: PathBuf,
|
||||
git_manager: GitManager,
|
||||
) -> Result<Self> {
|
||||
Ok(Self { repo_root, output_dir, git_manager })
|
||||
}
|
||||
|
||||
/// Get the path to the cached binary using explicit commit hash
|
||||
pub(crate) fn get_cached_binary_path_for_commit(&self, commit: &str) -> PathBuf {
|
||||
let identifier = &commit[..8]; // Use first 8 chars of commit
|
||||
self.output_dir.join("bin").join(format!("reth_{identifier}"))
|
||||
}
|
||||
|
||||
/// Compile reth using cargo build and cache the binary
|
||||
pub(crate) fn compile_reth(&self, commit: &str, features: &str, rustflags: &str) -> Result<()> {
|
||||
// Validate that current git commit matches the expected commit
|
||||
let current_commit = self.git_manager.get_current_commit()?;
|
||||
if current_commit != commit {
|
||||
return Err(eyre!(
|
||||
"Git commit mismatch! Expected: {}, but currently at: {}",
|
||||
&commit[..8],
|
||||
¤t_commit[..8]
|
||||
));
|
||||
}
|
||||
|
||||
let cached_path = self.get_cached_binary_path_for_commit(commit);
|
||||
|
||||
// Check if cached binary already exists (since path contains commit hash, it's valid)
|
||||
if cached_path.exists() {
|
||||
info!("Using cached binary (commit: {})", &commit[..8]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("No cached binary found, compiling (commit: {})...", &commit[..8]);
|
||||
|
||||
let binary_name = "reth";
|
||||
|
||||
info!(
|
||||
"Compiling {} with profiling configuration (commit: {})...",
|
||||
binary_name,
|
||||
&commit[..8]
|
||||
);
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("build").arg("--profile").arg("profiling");
|
||||
|
||||
cmd.arg("--features").arg(features);
|
||||
info!("Using features: {features}");
|
||||
|
||||
cmd.current_dir(&self.repo_root);
|
||||
|
||||
// Set RUSTFLAGS
|
||||
cmd.env("RUSTFLAGS", rustflags);
|
||||
info!("Using RUSTFLAGS: {rustflags}");
|
||||
|
||||
info!("Compiling {binary_name} with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute cargo build command")?;
|
||||
|
||||
// Print stdout and stderr with prefixes at debug level
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
for line in stdout.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[CARGO] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in stderr.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[CARGO] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
// Print all output when compilation fails
|
||||
error!("Cargo build failed with exit code: {:?}", output.status.code());
|
||||
|
||||
if !stdout.trim().is_empty() {
|
||||
error!("Cargo stdout:");
|
||||
for line in stdout.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !stderr.trim().is_empty() {
|
||||
error!("Cargo stderr:");
|
||||
for line in stderr.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(eyre!("Compilation failed with exit code: {:?}", output.status.code()));
|
||||
}
|
||||
|
||||
info!("{} compilation completed", binary_name);
|
||||
|
||||
// Copy the compiled binary to cache
|
||||
let source_path =
|
||||
PathBuf::from(&self.repo_root).join(format!("target/profiling/{}", binary_name));
|
||||
if !source_path.exists() {
|
||||
return Err(eyre!("Compiled binary not found at {:?}", source_path));
|
||||
}
|
||||
|
||||
// Create bin directory if it doesn't exist
|
||||
let bin_dir = self.output_dir.join("bin");
|
||||
fs::create_dir_all(&bin_dir).wrap_err("Failed to create bin directory")?;
|
||||
|
||||
// Copy binary to cache
|
||||
fs::copy(&source_path, &cached_path).wrap_err("Failed to copy binary to cache")?;
|
||||
|
||||
// Make the cached binary executable
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&cached_path)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&cached_path, perms)?;
|
||||
}
|
||||
|
||||
info!("Cached compiled binary at: {:?}", cached_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if reth-bench is available in PATH
|
||||
pub(crate) fn is_reth_bench_available(&self) -> bool {
|
||||
match Command::new("which").arg("reth-bench").output() {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
info!("Found reth-bench: {}", path.trim());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if samply is available in PATH
|
||||
pub(crate) fn is_samply_available(&self) -> bool {
|
||||
match Command::new("which").arg("samply").output() {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
info!("Found samply: {}", path.trim());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Install samply using cargo
|
||||
pub(crate) fn install_samply(&self) -> Result<()> {
|
||||
info!("Installing samply via cargo...");
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(["install", "--locked", "samply"]);
|
||||
|
||||
info!("Installing samply with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute cargo install samply command")?;
|
||||
|
||||
// Print stdout and stderr with prefixes at debug level
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
for line in stdout.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[CARGO-SAMPLY] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in stderr.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[CARGO-SAMPLY] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
// Print all output when installation fails
|
||||
error!("Cargo install samply failed with exit code: {:?}", output.status.code());
|
||||
|
||||
if !stdout.trim().is_empty() {
|
||||
error!("Cargo stdout:");
|
||||
for line in stdout.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !stderr.trim().is_empty() {
|
||||
error!("Cargo stderr:");
|
||||
for line in stderr.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(eyre!(
|
||||
"samply installation failed with exit code: {:?}",
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
info!("Samply installation completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure samply is available, installing if necessary
|
||||
pub(crate) fn ensure_samply_available(&self) -> Result<()> {
|
||||
if self.is_samply_available() {
|
||||
Ok(())
|
||||
} else {
|
||||
warn!("samply not found in PATH, installing...");
|
||||
self.install_samply()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure reth-bench is available, compiling if necessary
|
||||
pub(crate) fn ensure_reth_bench_available(&self) -> Result<()> {
|
||||
if self.is_reth_bench_available() {
|
||||
Ok(())
|
||||
} else {
|
||||
warn!("reth-bench not found in PATH, compiling and installing...");
|
||||
self.compile_reth_bench()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile and install reth-bench using `make install-reth-bench`
|
||||
pub(crate) fn compile_reth_bench(&self) -> Result<()> {
|
||||
info!("Compiling and installing reth-bench...");
|
||||
|
||||
let mut cmd = Command::new("make");
|
||||
cmd.arg("install-reth-bench").current_dir(&self.repo_root);
|
||||
|
||||
info!("Compiling reth-bench with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute make install-reth-bench command")?;
|
||||
|
||||
// Print stdout and stderr with prefixes at debug level
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
for line in stdout.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[MAKE-BENCH] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in stderr.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
debug!("[MAKE-BENCH] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
// Print all output when compilation fails
|
||||
error!("Make install-reth-bench failed with exit code: {:?}", output.status.code());
|
||||
|
||||
if !stdout.trim().is_empty() {
|
||||
error!("Make stdout:");
|
||||
for line in stdout.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !stderr.trim().is_empty() {
|
||||
error!("Make stderr:");
|
||||
for line in stderr.lines() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(eyre!(
|
||||
"reth-bench compilation failed with exit code: {:?}",
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
info!("Reth-bench compilation completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
328
bin/reth-bench-compare/src/git.rs
Normal file
328
bin/reth-bench-compare/src/git.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Git operations for branch management.
|
||||
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use std::process::Command;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Manages git operations for branch switching
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct GitManager {
|
||||
repo_root: String,
|
||||
}
|
||||
|
||||
impl GitManager {
|
||||
/// Create a new `GitManager`, detecting the repository root
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--show-toplevel"])
|
||||
.output()
|
||||
.wrap_err("Failed to execute git command - is git installed?")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("Not in a git repository or git command failed"));
|
||||
}
|
||||
|
||||
let repo_root = String::from_utf8(output.stdout)
|
||||
.wrap_err("Git output is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let manager = Self { repo_root };
|
||||
info!(
|
||||
"Detected git repository at: {}, current reference: {}",
|
||||
manager.repo_root(),
|
||||
manager.get_current_ref()?
|
||||
);
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// Get the current git branch name
|
||||
pub(crate) fn get_current_branch(&self) -> Result<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["branch", "--show-current"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to get current branch")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("Failed to determine current branch"));
|
||||
}
|
||||
|
||||
let branch = String::from_utf8(output.stdout)
|
||||
.wrap_err("Branch name is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if branch.is_empty() {
|
||||
return Err(eyre!("Not on a named branch (detached HEAD?)"));
|
||||
}
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
/// Get the current git reference (branch name, tag, or commit hash)
|
||||
pub(crate) fn get_current_ref(&self) -> Result<String> {
|
||||
// First try to get branch name
|
||||
if let Ok(branch) = self.get_current_branch() {
|
||||
return Ok(branch);
|
||||
}
|
||||
|
||||
// If not on a branch, check if we're on a tag
|
||||
let tag_output = Command::new("git")
|
||||
.args(["describe", "--exact-match", "--tags", "HEAD"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to check for tag")?;
|
||||
|
||||
if tag_output.status.success() {
|
||||
let tag = String::from_utf8(tag_output.stdout)
|
||||
.wrap_err("Tag name is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
return Ok(tag);
|
||||
}
|
||||
|
||||
// If not on a branch or tag, return the commit hash
|
||||
let commit_output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to get current commit")?;
|
||||
|
||||
if !commit_output.status.success() {
|
||||
return Err(eyre!("Failed to get current commit hash"));
|
||||
}
|
||||
|
||||
let commit_hash = String::from_utf8(commit_output.stdout)
|
||||
.wrap_err("Commit hash is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(commit_hash)
|
||||
}
|
||||
|
||||
/// Check if the git working directory has uncommitted changes to tracked files
|
||||
pub(crate) fn validate_clean_state(&self) -> Result<()> {
|
||||
let output = Command::new("git")
|
||||
.args(["status", "--porcelain"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to check git status")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("Git status command failed"));
|
||||
}
|
||||
|
||||
let status_output =
|
||||
String::from_utf8(output.stdout).wrap_err("Git status output is not valid UTF-8")?;
|
||||
|
||||
// Check for uncommitted changes to tracked files
|
||||
// Status codes: M = modified, A = added, D = deleted, R = renamed, C = copied, U = updated
|
||||
// ?? = untracked files (we want to ignore these)
|
||||
let has_uncommitted_changes = status_output.lines().any(|line| {
|
||||
if line.len() >= 2 {
|
||||
let status = &line[0..2];
|
||||
// Ignore untracked files (??) and ignored files (!!)
|
||||
!matches!(status, "??" | "!!")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if has_uncommitted_changes {
|
||||
warn!("Git working directory has uncommitted changes to tracked files:");
|
||||
for line in status_output.lines() {
|
||||
if line.len() >= 2 && !matches!(&line[0..2], "??" | "!!") {
|
||||
warn!(" {}", line);
|
||||
}
|
||||
}
|
||||
return Err(eyre!(
|
||||
"Git working directory has uncommitted changes to tracked files. Please commit or stash changes before running benchmark comparison."
|
||||
));
|
||||
}
|
||||
|
||||
// Check if there are untracked files and log them as info
|
||||
let untracked_files: Vec<&str> =
|
||||
status_output.lines().filter(|line| line.starts_with("??")).collect();
|
||||
|
||||
if !untracked_files.is_empty() {
|
||||
info!(
|
||||
"Git working directory has {} untracked files (this is OK)",
|
||||
untracked_files.len()
|
||||
);
|
||||
}
|
||||
|
||||
info!("Git working directory is clean (no uncommitted changes to tracked files)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch all refs from remote to ensure we have latest branches and tags
|
||||
pub(crate) fn fetch_all(&self) -> Result<()> {
|
||||
let output = Command::new("git")
|
||||
.args(["fetch", "--all", "--tags", "--quiet", "--force"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to fetch latest refs")?;
|
||||
|
||||
if output.status.success() {
|
||||
info!("Fetched latest refs");
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
// Only warn if there's actual error content, not just fetch progress
|
||||
if !stderr.trim().is_empty() && !stderr.contains("-> origin/") {
|
||||
warn!("Git fetch encountered issues (continuing anyway): {}", stderr);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that the specified git references exist (branches, tags, or commits)
|
||||
pub(crate) fn validate_refs(&self, refs: &[&str]) -> Result<()> {
|
||||
for &git_ref in refs {
|
||||
// Try to resolve the ref similar to `git checkout` by peeling to a commit.
|
||||
// First try the ref as-is with ^{commit}, then fall back to origin/{ref}^{commit}.
|
||||
let as_is = format!("{git_ref}^{{commit}}");
|
||||
let ref_check = Command::new("git")
|
||||
.args(["rev-parse", "--verify", &as_is])
|
||||
.current_dir(&self.repo_root)
|
||||
.output();
|
||||
|
||||
let found = if let Ok(output) = ref_check &&
|
||||
output.status.success()
|
||||
{
|
||||
info!("Validated reference exists: {}", git_ref);
|
||||
true
|
||||
} else {
|
||||
// Try remote-only branches via origin/{ref}
|
||||
let origin_ref = format!("origin/{git_ref}^{{commit}}");
|
||||
let origin_check = Command::new("git")
|
||||
.args(["rev-parse", "--verify", &origin_ref])
|
||||
.current_dir(&self.repo_root)
|
||||
.output();
|
||||
|
||||
if let Ok(output) = origin_check &&
|
||||
output.status.success()
|
||||
{
|
||||
info!("Validated remote reference exists: origin/{}", git_ref);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !found {
|
||||
return Err(eyre!(
|
||||
"Git reference '{}' does not exist as branch, tag, or commit (tried '{}' and 'origin/{}^{{commit}}')",
|
||||
git_ref,
|
||||
format!("{git_ref}^{{commit}}"),
|
||||
git_ref,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Switch to the specified git reference (branch, tag, or commit)
|
||||
pub(crate) fn switch_ref(&self, git_ref: &str) -> Result<()> {
|
||||
// First checkout the reference
|
||||
let output = Command::new("git")
|
||||
.args(["checkout", git_ref])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err_with(|| format!("Failed to switch to reference '{git_ref}'"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(eyre!("Failed to switch to reference '{}': {}", git_ref, stderr));
|
||||
}
|
||||
|
||||
// Check if this is a branch that tracks a remote and pull latest changes
|
||||
let is_branch = Command::new("git")
|
||||
.args(["show-ref", "--verify", "--quiet", &format!("refs/heads/{git_ref}")])
|
||||
.current_dir(&self.repo_root)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_branch {
|
||||
// Check if the branch tracks a remote
|
||||
let tracking_output = Command::new("git")
|
||||
.args([
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"--symbolic-full-name",
|
||||
&format!("{git_ref}@{{upstream}}"),
|
||||
])
|
||||
.current_dir(&self.repo_root)
|
||||
.output();
|
||||
|
||||
if let Ok(output) = tracking_output &&
|
||||
output.status.success()
|
||||
{
|
||||
let upstream = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !upstream.is_empty() && upstream != format!("{git_ref}@{{upstream}}") {
|
||||
// Branch tracks a remote, pull latest changes
|
||||
info!("Pulling latest changes for branch: {}", git_ref);
|
||||
|
||||
let pull_output = Command::new("git")
|
||||
.args(["pull", "--ff-only"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to pull latest changes for branch '{git_ref}'")
|
||||
})?;
|
||||
|
||||
if pull_output.status.success() {
|
||||
info!("Successfully pulled latest changes for branch: {}", git_ref);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&pull_output.stderr);
|
||||
warn!("Failed to pull latest changes for branch '{}': {}", git_ref, stderr);
|
||||
// Continue anyway, we'll use whatever version we have
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the checkout succeeded by checking the current commit
|
||||
let current_commit_output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to get current commit")?;
|
||||
|
||||
if !current_commit_output.status.success() {
|
||||
return Err(eyre!("Failed to verify git checkout"));
|
||||
}
|
||||
|
||||
info!("Switched to reference: {}", git_ref);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current commit hash
|
||||
pub(crate) fn get_current_commit(&self) -> Result<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(&self.repo_root)
|
||||
.output()
|
||||
.wrap_err("Failed to get current commit")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("Failed to get current commit hash"));
|
||||
}
|
||||
|
||||
let commit_hash = String::from_utf8(output.stdout)
|
||||
.wrap_err("Commit hash is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(commit_hash)
|
||||
}
|
||||
|
||||
/// Get the repository root path
|
||||
pub(crate) fn repo_root(&self) -> &str {
|
||||
&self.repo_root
|
||||
}
|
||||
}
|
||||
47
bin/reth-bench-compare/src/main.rs
Normal file
47
bin/reth-bench-compare/src/main.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! # reth-bench-compare
|
||||
//!
|
||||
//! Automated tool for comparing reth performance between two git branches.
|
||||
//! This tool automates the complete workflow of compiling, running, and benchmarking
|
||||
//! reth on different branches to provide meaningful performance comparisons.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
use alloy_primitives as _;
|
||||
|
||||
mod benchmark;
|
||||
mod cli;
|
||||
mod comparison;
|
||||
mod compilation;
|
||||
mod git;
|
||||
mod node;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{run_comparison, Args};
|
||||
use eyre::Result;
|
||||
use reth_cli_runner::CliRunner;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided.
|
||||
if std::env::var_os("RUST_BACKTRACE").is_none() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize tracing
|
||||
let _guard = args.init_tracing()?;
|
||||
|
||||
// Run until either exit or sigint or sigterm
|
||||
let runner = CliRunner::try_default_runtime()?;
|
||||
runner.run_command_until_exit(|ctx| run_comparison(args, ctx))
|
||||
}
|
||||
695
bin/reth-bench-compare/src/node.rs
Normal file
695
bin/reth-bench-compare/src/node.rs
Normal file
@@ -0,0 +1,695 @@
|
||||
//! Node management for starting, stopping, and controlling reth instances.
|
||||
|
||||
use crate::cli::Args;
|
||||
use alloy_provider::{Provider, ProviderBuilder};
|
||||
use alloy_rpc_client::RpcClient;
|
||||
use alloy_rpc_types_eth::SyncStatus;
|
||||
use alloy_transport_ws::WsConnect;
|
||||
use eyre::{eyre, OptionExt, Result, WrapErr};
|
||||
#[cfg(unix)]
|
||||
use nix::sys::signal::{killpg, Signal};
|
||||
#[cfg(unix)]
|
||||
use nix::unistd::Pid;
|
||||
use reth_chainspec::Chain;
|
||||
use std::{fs, path::PathBuf, time::Duration};
|
||||
use tokio::{
|
||||
fs::File as AsyncFile,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader as AsyncBufReader},
|
||||
process::Command,
|
||||
time::{sleep, timeout},
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Default websocket RPC port used by reth
|
||||
const DEFAULT_WS_RPC_PORT: u16 = 8546;
|
||||
|
||||
/// Manages reth node lifecycle and operations
|
||||
pub(crate) struct NodeManager {
|
||||
datadir: Option<String>,
|
||||
metrics_port: u16,
|
||||
chain: Chain,
|
||||
use_sudo: bool,
|
||||
binary_path: Option<std::path::PathBuf>,
|
||||
enable_profiling: bool,
|
||||
output_dir: PathBuf,
|
||||
additional_reth_args: Vec<String>,
|
||||
comparison_dir: Option<PathBuf>,
|
||||
tracing_endpoint: Option<String>,
|
||||
otlp_max_queue_size: usize,
|
||||
}
|
||||
|
||||
impl NodeManager {
|
||||
/// Create a new `NodeManager` with configuration from CLI args
|
||||
pub(crate) fn new(args: &Args) -> Self {
|
||||
Self {
|
||||
datadir: Some(args.datadir_path().to_string_lossy().to_string()),
|
||||
metrics_port: args.metrics_port,
|
||||
chain: args.chain,
|
||||
use_sudo: args.sudo,
|
||||
binary_path: None,
|
||||
enable_profiling: args.profile,
|
||||
output_dir: args.output_dir_path(),
|
||||
// Filter out empty strings to prevent invalid arguments being passed to reth node
|
||||
additional_reth_args: args
|
||||
.reth_args
|
||||
.iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.cloned()
|
||||
.collect(),
|
||||
comparison_dir: None,
|
||||
tracing_endpoint: args.traces.otlp.as_ref().map(|u| u.to_string()),
|
||||
otlp_max_queue_size: args.otlp_max_queue_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the comparison directory path for logging
|
||||
pub(crate) fn set_comparison_dir(&mut self, dir: PathBuf) {
|
||||
self.comparison_dir = Some(dir);
|
||||
}
|
||||
|
||||
/// Get the log file path for a given reference type
|
||||
fn get_log_file_path(&self, ref_type: &str) -> Result<PathBuf> {
|
||||
let comparison_dir = self
|
||||
.comparison_dir
|
||||
.as_ref()
|
||||
.ok_or_eyre("Comparison directory not set. Call set_comparison_dir first.")?;
|
||||
|
||||
// The comparison directory already contains the full path to results/<timestamp>
|
||||
let log_dir = comparison_dir.join(ref_type);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
fs::create_dir_all(&log_dir)
|
||||
.wrap_err(format!("Failed to create log directory: {:?}", log_dir))?;
|
||||
|
||||
let log_file = log_dir.join("reth_node.log");
|
||||
Ok(log_file)
|
||||
}
|
||||
|
||||
/// Get the perf event max sample rate from the system, capped at 10000
|
||||
fn get_perf_sample_rate(&self) -> Option<String> {
|
||||
let perf_rate_file = "/proc/sys/kernel/perf_event_max_sample_rate";
|
||||
if let Ok(content) = fs::read_to_string(perf_rate_file) {
|
||||
let rate_str = content.trim();
|
||||
if !rate_str.is_empty() {
|
||||
if let Ok(system_rate) = rate_str.parse::<u32>() {
|
||||
let capped_rate = std::cmp::min(system_rate, 10000);
|
||||
info!(
|
||||
"Detected perf_event_max_sample_rate: {}, using: {}",
|
||||
system_rate, capped_rate
|
||||
);
|
||||
return Some(capped_rate.to_string());
|
||||
}
|
||||
warn!("Failed to parse perf_event_max_sample_rate: {}", rate_str);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the absolute path to samply using 'which' command
|
||||
async fn get_samply_path(&self) -> Result<String> {
|
||||
let output = Command::new("which")
|
||||
.arg("samply")
|
||||
.output()
|
||||
.await
|
||||
.wrap_err("Failed to execute 'which samply' command")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("samply not found in PATH"));
|
||||
}
|
||||
|
||||
let samply_path = String::from_utf8(output.stdout)
|
||||
.wrap_err("samply path is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if samply_path.is_empty() {
|
||||
return Err(eyre!("which samply returned empty path"));
|
||||
}
|
||||
|
||||
Ok(samply_path)
|
||||
}
|
||||
|
||||
/// Build reth arguments as a vector of strings
|
||||
fn build_reth_args(
|
||||
&self,
|
||||
binary_path_str: &str,
|
||||
additional_args: &[String],
|
||||
ref_type: &str,
|
||||
) -> (Vec<String>, String) {
|
||||
let mut reth_args = vec![binary_path_str.to_string(), "node".to_string()];
|
||||
|
||||
// Add chain argument (skip for mainnet as it's the default)
|
||||
let chain_str = self.chain.to_string();
|
||||
if chain_str != "mainnet" {
|
||||
reth_args.extend_from_slice(&["--chain".to_string(), chain_str.clone()]);
|
||||
}
|
||||
|
||||
// Add datadir if specified
|
||||
if let Some(ref datadir) = self.datadir {
|
||||
reth_args.extend_from_slice(&["--datadir".to_string(), datadir.clone()]);
|
||||
}
|
||||
|
||||
// Add reth-specific arguments
|
||||
let metrics_arg = format!("0.0.0.0:{}", self.metrics_port);
|
||||
reth_args.extend_from_slice(&[
|
||||
"--engine.accept-execution-requests-hash".to_string(),
|
||||
"--metrics".to_string(),
|
||||
metrics_arg,
|
||||
"--http".to_string(),
|
||||
"--http.api".to_string(),
|
||||
"eth,reth".to_string(),
|
||||
"--ws".to_string(),
|
||||
"--ws.api".to_string(),
|
||||
"eth,reth".to_string(),
|
||||
"--disable-discovery".to_string(),
|
||||
"--trusted-only".to_string(),
|
||||
"--disable-tx-gossip".to_string(),
|
||||
]);
|
||||
|
||||
// Add tracing arguments if OTLP endpoint is configured
|
||||
if let Some(ref endpoint) = self.tracing_endpoint {
|
||||
info!("Enabling OTLP tracing export to: {} (service: reth-{})", endpoint, ref_type);
|
||||
// Endpoint requires equals per clap settings in reth
|
||||
reth_args.push(format!("--tracing-otlp={}", endpoint));
|
||||
}
|
||||
|
||||
// Add any additional arguments passed via command line (common to both baseline and
|
||||
// feature)
|
||||
reth_args.extend_from_slice(&self.additional_reth_args);
|
||||
|
||||
// Add reference-specific additional arguments
|
||||
reth_args.extend_from_slice(additional_args);
|
||||
|
||||
(reth_args, chain_str)
|
||||
}
|
||||
|
||||
/// Create a command for profiling mode
|
||||
async fn create_profiling_command(
|
||||
&self,
|
||||
ref_type: &str,
|
||||
reth_args: &[String],
|
||||
) -> Result<Command> {
|
||||
// Create profiles directory if it doesn't exist
|
||||
let profile_dir = self.output_dir.join("profiles");
|
||||
fs::create_dir_all(&profile_dir).wrap_err("Failed to create profiles directory")?;
|
||||
|
||||
let profile_path = profile_dir.join(format!("{}.json.gz", ref_type));
|
||||
info!("Starting reth node with samply profiling...");
|
||||
info!("Profile output: {:?}", profile_path);
|
||||
|
||||
// Get absolute path to samply
|
||||
let samply_path = self.get_samply_path().await?;
|
||||
|
||||
let mut cmd = if self.use_sudo {
|
||||
let mut sudo_cmd = Command::new("sudo");
|
||||
sudo_cmd.arg(&samply_path);
|
||||
sudo_cmd
|
||||
} else {
|
||||
Command::new(&samply_path)
|
||||
};
|
||||
|
||||
// Add samply arguments
|
||||
cmd.args(["record", "--save-only", "-o", &profile_path.to_string_lossy()]);
|
||||
|
||||
// Add rate argument if available
|
||||
if let Some(rate) = self.get_perf_sample_rate() {
|
||||
cmd.args(["--rate", &rate]);
|
||||
}
|
||||
|
||||
// Add separator and complete reth command
|
||||
cmd.arg("--");
|
||||
cmd.args(reth_args);
|
||||
|
||||
// Enable tracing-samply
|
||||
if supports_samply_flags(&reth_args[0]) {
|
||||
cmd.arg("--log.samply");
|
||||
}
|
||||
|
||||
// Set environment variable to disable log styling
|
||||
cmd.env("RUST_LOG_STYLE", "never");
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Create a command for direct reth execution
|
||||
fn create_direct_command(&self, reth_args: &[String]) -> Command {
|
||||
let binary_path = &reth_args[0];
|
||||
|
||||
let mut cmd = if self.use_sudo {
|
||||
info!("Starting reth node with sudo...");
|
||||
let mut sudo_cmd = Command::new("sudo");
|
||||
sudo_cmd.args(reth_args);
|
||||
sudo_cmd
|
||||
} else {
|
||||
info!("Starting reth node...");
|
||||
let mut reth_cmd = Command::new(binary_path);
|
||||
reth_cmd.args(&reth_args[1..]); // Skip the binary path since it's the command
|
||||
reth_cmd
|
||||
};
|
||||
|
||||
// Set environment variable to disable log styling
|
||||
cmd.env("RUST_LOG_STYLE", "never");
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Start a reth node using the specified binary path and return the process handle
|
||||
/// along with the formatted reth command string for reporting.
|
||||
pub(crate) async fn start_node(
|
||||
&mut self,
|
||||
binary_path: &std::path::Path,
|
||||
_git_ref: &str,
|
||||
ref_type: &str,
|
||||
additional_args: &[String],
|
||||
) -> Result<(tokio::process::Child, String)> {
|
||||
// Store the binary path for later use (e.g., in unwind_to_block)
|
||||
self.binary_path = Some(binary_path.to_path_buf());
|
||||
|
||||
let binary_path_str = binary_path.to_string_lossy();
|
||||
let (reth_args, _) = self.build_reth_args(&binary_path_str, additional_args, ref_type);
|
||||
|
||||
// Format the reth command string for reporting
|
||||
let reth_command = shlex::try_join(reth_args.iter().map(|s| s.as_str()))
|
||||
.wrap_err("Failed to format reth command string")?;
|
||||
|
||||
// Log additional arguments if any
|
||||
if !self.additional_reth_args.is_empty() {
|
||||
info!("Using common additional reth arguments: {:?}", self.additional_reth_args);
|
||||
}
|
||||
if !additional_args.is_empty() {
|
||||
info!("Using reference-specific additional reth arguments: {:?}", additional_args);
|
||||
}
|
||||
|
||||
let mut cmd = if self.enable_profiling {
|
||||
self.create_profiling_command(ref_type, &reth_args).await?
|
||||
} else {
|
||||
self.create_direct_command(&reth_args)
|
||||
};
|
||||
|
||||
// Set process group for better signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Set high queue size to prevent trace dropping during benchmarks
|
||||
if self.tracing_endpoint.is_some() {
|
||||
cmd.env("OTEL_BSP_MAX_QUEUE_SIZE", self.otlp_max_queue_size.to_string()); // Traces
|
||||
cmd.env("OTEL_BLRP_MAX_QUEUE_SIZE", "10000"); // Logs
|
||||
|
||||
// Set service name to differentiate baseline vs feature runs in Jaeger
|
||||
cmd.env("OTEL_SERVICE_NAME", format!("reth-{}", ref_type));
|
||||
}
|
||||
|
||||
debug!("Executing reth command: {cmd:?}");
|
||||
|
||||
let mut child = cmd
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true) // Kill on drop so that on Ctrl-C for parent process we stop all child processes
|
||||
.spawn()
|
||||
.wrap_err("Failed to start reth node")?;
|
||||
|
||||
info!(
|
||||
"Reth node started with PID: {:?} (binary: {})",
|
||||
child.id().ok_or_eyre("Reth node is not running")?,
|
||||
binary_path_str
|
||||
);
|
||||
|
||||
// Prepare log file path
|
||||
let log_file_path = self.get_log_file_path(ref_type)?;
|
||||
info!("Reth node logs will be saved to: {:?}", log_file_path);
|
||||
|
||||
// Stream stdout and stderr with prefixes at debug level and to log file
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let log_file = AsyncFile::create(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to create log file: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = AsyncBufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH] {}", line);
|
||||
// Write to log file (reth already includes timestamps)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let log_file = AsyncFile::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to open log file for stderr: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = AsyncBufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH] {}", line);
|
||||
// Write to log file (reth already includes timestamps)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Give the node a moment to start up
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
|
||||
Ok((child, reth_command))
|
||||
}
|
||||
|
||||
/// Wait for the node to be ready and return its current tip.
|
||||
///
|
||||
/// Fails early if the node process exits before becoming ready.
|
||||
pub(crate) async fn wait_for_node_ready_and_get_tip(
|
||||
&self,
|
||||
child: &mut tokio::process::Child,
|
||||
) -> Result<u64> {
|
||||
info!("Waiting for node to be ready and synced...");
|
||||
|
||||
let max_wait = Duration::from_secs(120); // 2 minutes to allow for sync
|
||||
let check_interval = Duration::from_secs(2);
|
||||
let rpc_url = "http://localhost:8545";
|
||||
|
||||
// Create Alloy provider
|
||||
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
|
||||
let provider = ProviderBuilder::new().connect_http(url);
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
let mut iteration = 0;
|
||||
|
||||
timeout(max_wait, async {
|
||||
loop {
|
||||
iteration += 1;
|
||||
debug!(
|
||||
"Readiness check iteration {} (elapsed: {:?})",
|
||||
iteration,
|
||||
start_time.elapsed()
|
||||
);
|
||||
|
||||
// Check if the node process has exited.
|
||||
if let Some(status) = child.try_wait()? {
|
||||
return Err(eyre!("Node process exited unexpectedly with {status}"));
|
||||
}
|
||||
|
||||
// First check if RPC is up and node is not syncing
|
||||
match provider.syncing().await {
|
||||
Ok(sync_result) => {
|
||||
match sync_result {
|
||||
SyncStatus::Info(sync_info) => {
|
||||
debug!("Node is still syncing {sync_info:?}, waiting...");
|
||||
}
|
||||
_ => {
|
||||
debug!("HTTP RPC is up and node is not syncing, checking block number...");
|
||||
// Node is not syncing, now get the tip
|
||||
match provider.get_block_number().await {
|
||||
Ok(tip) => {
|
||||
debug!("HTTP RPC ready at block: {}, checking WebSocket...", tip);
|
||||
|
||||
// Verify WebSocket RPC is ready (public endpoint, no JWT required)
|
||||
let ws_url = format!("ws://localhost:{}", DEFAULT_WS_RPC_PORT);
|
||||
debug!("Attempting WebSocket connection to {} (public endpoint)", ws_url);
|
||||
let ws_connect = WsConnect::new(&ws_url);
|
||||
|
||||
match RpcClient::connect_pubsub(ws_connect).await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Node is ready (HTTP and WebSocket) at block: {} (took {:?}, {} iterations)",
|
||||
tip, start_time.elapsed(), iteration
|
||||
);
|
||||
return Ok(tip);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"HTTP RPC ready but WebSocket not ready yet (iteration {}): {:?}",
|
||||
iteration, e
|
||||
);
|
||||
debug!("WebSocket error details: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to get block number (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Node RPC not ready yet or failed to check sync status (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Sleeping for {:?} before next check", check_interval);
|
||||
sleep(check_interval).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.wrap_err("Timed out waiting for node to be ready and synced")?
|
||||
}
|
||||
|
||||
/// Wait for the node RPC to be ready and return its current tip, without waiting for sync.
|
||||
///
|
||||
/// This is faster than `wait_for_node_ready_and_get_tip` but may return a tip while
|
||||
/// the node is still syncing.
|
||||
pub(crate) async fn wait_for_rpc_and_get_tip(
|
||||
&self,
|
||||
child: &mut tokio::process::Child,
|
||||
) -> Result<u64> {
|
||||
info!("Waiting for node RPC to be ready (skipping sync wait)...");
|
||||
|
||||
let max_wait = Duration::from_secs(60);
|
||||
let check_interval = Duration::from_secs(2);
|
||||
let rpc_url = "http://localhost:8545";
|
||||
|
||||
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
|
||||
let provider = ProviderBuilder::new().connect_http(url);
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
let mut iteration = 0;
|
||||
|
||||
timeout(max_wait, async {
|
||||
loop {
|
||||
iteration += 1;
|
||||
debug!(
|
||||
"RPC readiness check iteration {} (elapsed: {:?})",
|
||||
iteration,
|
||||
start_time.elapsed()
|
||||
);
|
||||
|
||||
if let Some(status) = child.try_wait()? {
|
||||
return Err(eyre!("Node process exited unexpectedly with {status}"));
|
||||
}
|
||||
|
||||
match provider.get_block_number().await {
|
||||
Ok(tip) => {
|
||||
debug!("HTTP RPC ready at block: {}, checking WebSocket...", tip);
|
||||
|
||||
let ws_url = format!("ws://localhost:{}", DEFAULT_WS_RPC_PORT);
|
||||
let ws_connect = WsConnect::new(&ws_url);
|
||||
|
||||
match RpcClient::connect_pubsub(ws_connect).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Node RPC is ready at block: {} (took {:?}, {} iterations)",
|
||||
tip,
|
||||
start_time.elapsed(),
|
||||
iteration
|
||||
);
|
||||
return Ok(tip);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"HTTP RPC ready but WebSocket not ready yet (iteration {}): {:?}",
|
||||
iteration, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("RPC not ready yet (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
|
||||
sleep(check_interval).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.wrap_err("Timed out waiting for node RPC to be ready")?
|
||||
}
|
||||
|
||||
/// Stop the reth node gracefully
|
||||
pub(crate) async fn stop_node(&self, child: &mut tokio::process::Child) -> Result<()> {
|
||||
let pid = child.id().ok_or_eyre("Child process ID should be available")?;
|
||||
|
||||
// Check if the process has already exited
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
info!("Reth node (PID: {}) has already exited with status: {:?}", pid, status);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(None) => {
|
||||
// Process is still running, proceed to stop it
|
||||
info!("Stopping process gracefully with SIGINT (PID: {})...", pid);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(eyre!("Failed to check process status: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Send SIGINT to process group to mimic Ctrl-C behavior
|
||||
let nix_pgid = Pid::from_raw(pid as i32);
|
||||
|
||||
match killpg(nix_pgid, Signal::SIGINT) {
|
||||
Ok(()) => {}
|
||||
Err(nix::errno::Errno::ESRCH) => {
|
||||
info!("Process group {} has already exited", pid);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(eyre!("Failed to send SIGINT to process group {}: {}", pid, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix systems, fall back to using external kill command
|
||||
let output = Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/F"])
|
||||
.output()
|
||||
.await
|
||||
.wrap_err("Failed to execute taskkill command")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
// Check if the error is because the process doesn't exist
|
||||
if stderr.contains("not found") || stderr.contains("not exist") {
|
||||
info!("Process {} has already exited", pid);
|
||||
} else {
|
||||
return Err(eyre!("Failed to kill process {}: {}", pid, stderr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the process to exit
|
||||
match child.wait().await {
|
||||
Ok(status) => {
|
||||
info!("Reth node (PID: {}) exited with status: {:?}", pid, status);
|
||||
}
|
||||
Err(e) => {
|
||||
// If we get an error here, it might be because the process already exited
|
||||
debug!("Error waiting for process exit (may have already exited): {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unwind the node to a specific block
|
||||
pub(crate) async fn unwind_to_block(&self, block_number: u64) -> Result<()> {
|
||||
if self.use_sudo {
|
||||
info!("Unwinding node to block: {} (with sudo)", block_number);
|
||||
} else {
|
||||
info!("Unwinding node to block: {}", block_number);
|
||||
}
|
||||
|
||||
// Use the binary path from the last start_node call, or fallback to default
|
||||
let binary_path = self
|
||||
.binary_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "./target/profiling/reth".to_string());
|
||||
|
||||
let mut cmd = if self.use_sudo {
|
||||
let mut sudo_cmd = Command::new("sudo");
|
||||
sudo_cmd.args([&binary_path, "stage", "unwind"]);
|
||||
sudo_cmd
|
||||
} else {
|
||||
let mut reth_cmd = Command::new(&binary_path);
|
||||
reth_cmd.args(["stage", "unwind"]);
|
||||
reth_cmd
|
||||
};
|
||||
|
||||
// Add chain argument (skip for mainnet as it's the default)
|
||||
let chain_str = self.chain.to_string();
|
||||
if chain_str != "mainnet" {
|
||||
cmd.args(["--chain", &chain_str]);
|
||||
}
|
||||
|
||||
// Add datadir if specified
|
||||
if let Some(ref datadir) = self.datadir {
|
||||
cmd.args(["--datadir", datadir]);
|
||||
}
|
||||
|
||||
cmd.args(["to-block", &block_number.to_string()]);
|
||||
|
||||
// Set environment variable to disable log styling
|
||||
cmd.env("RUST_LOG_STYLE", "never");
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing reth unwind command: {:?}", cmd);
|
||||
|
||||
let mut child = cmd
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.wrap_err("Failed to start unwind command")?;
|
||||
|
||||
// Stream stdout and stderr with prefixes in real-time
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = AsyncBufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-UNWIND] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = AsyncBufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-UNWIND] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for the command to complete
|
||||
let status = child.wait().await.wrap_err("Failed to wait for unwind command")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(eyre!("Unwind command failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
info!("Unwound to block: {}", block_number);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_samply_flags(bin: &str) -> bool {
|
||||
let mut cmd = std::process::Command::new(bin);
|
||||
// NOTE: The flag to check must come before --help.
|
||||
// We pass --help as a shortcut to not execute any command.
|
||||
cmd.args(["--log.samply", "--help"]);
|
||||
debug!(?cmd, "Checking samply flags support");
|
||||
let Ok(output) = cmd.output() else {
|
||||
return false;
|
||||
};
|
||||
debug!(?output, "Samply flags support check");
|
||||
output.status.success()
|
||||
}
|
||||
@@ -14,12 +14,9 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-chainspec.workspace = true
|
||||
reth-cli.workspace = true
|
||||
reth-cli-runner.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-ethereum-cli.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-fs-util.workspace = true
|
||||
reth-node-api.workspace = true
|
||||
@@ -28,12 +25,13 @@ reth-primitives-traits.workspace = true
|
||||
reth-rpc-api.workspace = true
|
||||
|
||||
reth-tracing.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eips.workspace = true
|
||||
alloy-json-rpc.workspace = true
|
||||
|
||||
alloy-consensus.workspace = true
|
||||
alloy-network.workspace = true
|
||||
alloy-primitives = { workspace = true, features = ["rand"] }
|
||||
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }
|
||||
alloy-pubsub.workspace = true
|
||||
@@ -84,54 +82,39 @@ default = ["jemalloc"]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"reth-ethereum-cli/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
jemalloc = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-node-core/jemalloc",
|
||||
"reth-ethereum-cli/jemalloc",
|
||||
]
|
||||
jemalloc-prof = [
|
||||
"reth-cli-util/jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-prof",
|
||||
]
|
||||
tracy-allocator = [
|
||||
"reth-cli-util/tracy-allocator",
|
||||
"tracy",
|
||||
"reth-ethereum-cli/tracy-allocator",
|
||||
]
|
||||
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
|
||||
tracy = [
|
||||
"reth-node-core/tracy",
|
||||
"reth-tracing/tracy",
|
||||
"reth-ethereum-cli/tracy",
|
||||
]
|
||||
|
||||
min-error-logs = [
|
||||
"tracing/release_max_level_error",
|
||||
"reth-node-core/min-error-logs",
|
||||
"reth-ethereum-cli/min-error-logs",
|
||||
]
|
||||
min-warn-logs = [
|
||||
"tracing/release_max_level_warn",
|
||||
"reth-node-core/min-warn-logs",
|
||||
"reth-ethereum-cli/min-warn-logs",
|
||||
]
|
||||
min-info-logs = [
|
||||
"tracing/release_max_level_info",
|
||||
"reth-node-core/min-info-logs",
|
||||
"reth-ethereum-cli/min-info-logs",
|
||||
]
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-node-core/min-debug-logs",
|
||||
"reth-ethereum-cli/min-debug-logs",
|
||||
]
|
||||
min-trace-logs = [
|
||||
"tracing/release_max_level_trace",
|
||||
"reth-node-core/min-trace-logs",
|
||||
"reth-ethereum-cli/min-trace-logs",
|
||||
]
|
||||
|
||||
# no-op feature flag for CI matrices
|
||||
|
||||
@@ -35,10 +35,6 @@ The `new-payload-fcu` command supports two optional waiting modes that can be us
|
||||
- `--wait-time <duration>`: Fixed sleep interval between blocks (e.g., `--wait-time 100ms` or `--wait-time 400` for 400ms)
|
||||
- `--wait-for-persistence`: Waits for blocks to be persisted using the `reth_subscribePersistedBlock` subscription
|
||||
|
||||
Both `new-payload-fcu` and `new-payload-only` support `--rpc-block-fetch-retries <RETRIES>`
|
||||
to control how many times block fetches are retried after an RPC failure. The default is `10`.
|
||||
Use `--rpc-block-fetch-retries forever` to keep retrying indefinitely.
|
||||
|
||||
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
|
||||
|
||||
By default, the WebSocket URL for persistence subscriptions is derived from `--engine-rpc-url` (converting to ws:// on port 8546). Use `--ws-rpc-url` to override this.
|
||||
|
||||
@@ -52,7 +52,8 @@ impl InnerTransport {
|
||||
url: Url,
|
||||
jwt: JwtSecret,
|
||||
) -> Result<(Self, Claims), AuthenticatedTransportError> {
|
||||
let mut client_builder = reqwest::Client::builder();
|
||||
let mut client_builder =
|
||||
reqwest::Client::builder().tls_built_in_root_certs(url.scheme() == "https");
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
||||
// Add the JWT to the headers if we can decode it.
|
||||
|
||||
@@ -9,7 +9,7 @@ use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::JwtSecret;
|
||||
use alloy_transport::layers::{RateLimitRetryPolicy, RetryBackoffLayer};
|
||||
use reqwest::Url;
|
||||
use reth_node_core::args::{BenchmarkArgs, WaitForPersistence};
|
||||
use reth_node_core::args::BenchmarkArgs;
|
||||
use tracing::info;
|
||||
|
||||
/// This is intended to be used by benchmarks that replay blocks from an RPC.
|
||||
@@ -33,10 +33,6 @@ pub(crate) struct BenchContext {
|
||||
pub(crate) use_reth_namespace: bool,
|
||||
/// Whether to fetch and replay RLP-encoded blocks.
|
||||
pub(crate) rlp_blocks: bool,
|
||||
/// Controls when `reth_newPayload` waits for persistence.
|
||||
pub(crate) wait_for_persistence: WaitForPersistence,
|
||||
/// Whether to skip waiting for caches (pass `wait_for_caches: false`).
|
||||
pub(crate) no_wait_for_caches: bool,
|
||||
}
|
||||
|
||||
impl BenchContext {
|
||||
@@ -64,9 +60,8 @@ impl BenchContext {
|
||||
.and_then(|t| t.as_http_error())
|
||||
.is_some_and(|e| e.status == 502)
|
||||
});
|
||||
let max_retries = bench_args.rpc_block_fetch_retries.as_max_retries();
|
||||
let client = ClientBuilder::default()
|
||||
.layer(RetryBackoffLayer::new_with_policy(max_retries, 800, u64::MAX, retry_policy))
|
||||
.layer(RetryBackoffLayer::new_with_policy(10, 800, u64::MAX, retry_policy))
|
||||
.http(rpc_url.parse()?);
|
||||
let block_provider = RootProvider::<AnyNetwork>::new(client);
|
||||
|
||||
@@ -166,10 +161,7 @@ impl BenchContext {
|
||||
|
||||
let next_block = first_block.header.number + 1;
|
||||
let rlp_blocks = bench_args.rlp_blocks;
|
||||
let wait_for_persistence =
|
||||
bench_args.wait_for_persistence.unwrap_or(WaitForPersistence::Never);
|
||||
let use_reth_namespace = bench_args.reth_new_payload || rlp_blocks;
|
||||
let no_wait_for_caches = bench_args.no_wait_for_caches;
|
||||
Ok(Self {
|
||||
auth_provider,
|
||||
block_provider,
|
||||
@@ -178,8 +170,6 @@ impl BenchContext {
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
251
bin/reth-bench/src/bench/gas_limit_ramp.rs
Normal file
251
bin/reth-bench/src/bench/gas_limit_ramp.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
//! Benchmarks empty block processing by ramping the block gas limit.
|
||||
|
||||
use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::{
|
||||
helpers::{build_payload, parse_gas_limit, prepare_payload_request, rpc_block_to_header},
|
||||
output::GasRampPayloadFile,
|
||||
},
|
||||
valid_payload::{
|
||||
call_forkchoice_updated_with_reth, call_new_payload_with_reth, payload_to_new_payload,
|
||||
},
|
||||
};
|
||||
use alloy_eips::BlockNumberOrTag;
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{ExecutionPayload, ForkchoiceState, JwtSecret};
|
||||
|
||||
use clap::Parser;
|
||||
use reqwest::Url;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_ethereum_primitives::TransactionSigned;
|
||||
use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK};
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use tracing::info;
|
||||
|
||||
/// `reth benchmark gas-limit-ramp` command.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// Number of blocks to generate. Mutually exclusive with --target-gas-limit.
|
||||
#[arg(long, value_name = "BLOCKS", conflicts_with = "target_gas_limit")]
|
||||
blocks: Option<u64>,
|
||||
|
||||
/// Target gas limit to ramp up to. The benchmark will generate blocks until the gas limit
|
||||
/// reaches or exceeds this value. Mutually exclusive with --blocks.
|
||||
/// Accepts short notation: K for thousand, M for million, G for billion (e.g., 2G = 2
|
||||
/// billion).
|
||||
#[arg(long, value_name = "TARGET_GAS_LIMIT", conflicts_with = "blocks", value_parser = parse_gas_limit)]
|
||||
target_gas_limit: Option<u64>,
|
||||
|
||||
/// The Engine API RPC URL.
|
||||
#[arg(long = "engine-rpc-url", value_name = "ENGINE_RPC_URL")]
|
||||
engine_rpc_url: String,
|
||||
|
||||
/// Path to the JWT secret for Engine API authentication.
|
||||
#[arg(long = "jwt-secret", value_name = "JWT_SECRET")]
|
||||
jwt_secret: PathBuf,
|
||||
|
||||
/// Output directory for benchmark results and generated payloads.
|
||||
#[arg(long, value_name = "OUTPUT")]
|
||||
output: PathBuf,
|
||||
|
||||
/// Use `reth_newPayload` endpoint instead of `engine_newPayload*`.
|
||||
///
|
||||
/// The `reth_newPayload` endpoint is a reth-specific extension that takes `ExecutionData`
|
||||
/// directly, waits for persistence and cache updates to complete before processing,
|
||||
/// and returns server-side timing breakdowns (latency, persistence wait, cache wait).
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
reth_new_payload: bool,
|
||||
}
|
||||
|
||||
/// Mode for determining when to stop ramping.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RampMode {
|
||||
/// Ramp for a fixed number of blocks.
|
||||
Blocks(u64),
|
||||
/// Ramp until reaching or exceeding target gas limit.
|
||||
TargetGasLimit(u64),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute `benchmark gas-limit-ramp` command.
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
let mode = match (self.blocks, self.target_gas_limit) {
|
||||
(Some(blocks), None) => {
|
||||
if blocks == 0 {
|
||||
return Err(eyre::eyre!("--blocks must be greater than 0"));
|
||||
}
|
||||
RampMode::Blocks(blocks)
|
||||
}
|
||||
(None, Some(target)) => {
|
||||
if target == 0 {
|
||||
return Err(eyre::eyre!("--target-gas-limit must be greater than 0"));
|
||||
}
|
||||
RampMode::TargetGasLimit(target)
|
||||
}
|
||||
_ => {
|
||||
return Err(eyre::eyre!(
|
||||
"Exactly one of --blocks or --target-gas-limit must be specified"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure output directory exists
|
||||
if self.output.is_file() {
|
||||
return Err(eyre::eyre!("Output path must be a directory"));
|
||||
}
|
||||
if !self.output.exists() {
|
||||
std::fs::create_dir_all(&self.output)?;
|
||||
info!(target: "reth-bench", "Created output directory: {:?}", self.output);
|
||||
}
|
||||
|
||||
// Set up authenticated provider (used for both Engine API and eth_ methods)
|
||||
let jwt = std::fs::read_to_string(&self.jwt_secret)?;
|
||||
let jwt = JwtSecret::from_hex(jwt)?;
|
||||
let auth_url = Url::parse(&self.engine_rpc_url)?;
|
||||
|
||||
info!(target: "reth-bench", "Connecting to Engine RPC at {}", auth_url);
|
||||
let auth_transport = AuthenticatedTransportConnect::new(auth_url, jwt);
|
||||
let client = ClientBuilder::default().connect_with(auth_transport).await?;
|
||||
let provider = RootProvider::<AnyNetwork>::new(client);
|
||||
|
||||
// Get chain spec - required for fork detection
|
||||
let chain_id = provider.get_chain_id().await?;
|
||||
let chain_spec = ChainSpec::from_chain_id(chain_id)
|
||||
.ok_or_else(|| eyre::eyre!("Unsupported chain id: {chain_id}"))?;
|
||||
|
||||
// Fetch the current head block as parent
|
||||
let parent_block = provider
|
||||
.get_block_by_number(BlockNumberOrTag::Latest)
|
||||
.full()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?;
|
||||
|
||||
let (mut parent_header, mut parent_hash) = rpc_block_to_header(parent_block);
|
||||
|
||||
let canonical_parent = parent_header.number;
|
||||
let start_block = canonical_parent + 1;
|
||||
|
||||
match mode {
|
||||
RampMode::Blocks(blocks) => {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
canonical_parent,
|
||||
start_block,
|
||||
end_block = start_block + blocks - 1,
|
||||
"Starting gas limit ramp benchmark (block count mode)"
|
||||
);
|
||||
}
|
||||
RampMode::TargetGasLimit(target) => {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
canonical_parent,
|
||||
start_block,
|
||||
current_gas_limit = parent_header.gas_limit,
|
||||
target_gas_limit = target,
|
||||
"Starting gas limit ramp benchmark (target gas limit mode)"
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.reth_new_payload {
|
||||
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
|
||||
}
|
||||
|
||||
let mut blocks_processed = 0u64;
|
||||
let total_benchmark_duration = Instant::now();
|
||||
|
||||
while !should_stop(mode, blocks_processed, parent_header.gas_limit) {
|
||||
let timestamp = parent_header.timestamp.saturating_add(1);
|
||||
|
||||
let request = prepare_payload_request(&chain_spec, timestamp, parent_hash);
|
||||
let new_payload_version = request.new_payload_version;
|
||||
|
||||
let (payload, sidecar) = build_payload(&provider, request).await?;
|
||||
|
||||
let mut block =
|
||||
payload.clone().try_into_block_with_sidecar::<TransactionSigned>(&sidecar)?;
|
||||
|
||||
let max_increase = max_gas_limit_increase(parent_header.gas_limit);
|
||||
let gas_limit =
|
||||
parent_header.gas_limit.saturating_add(max_increase).min(MAXIMUM_GAS_LIMIT_BLOCK);
|
||||
|
||||
block.header.gas_limit = gas_limit;
|
||||
|
||||
let block_hash = block.header.hash_slow();
|
||||
// Regenerate the payload from the modified block, but keep the original sidecar
|
||||
// which contains the actual execution requests data (not just the hash)
|
||||
let (payload, _) = ExecutionPayload::from_block_unchecked(block_hash, &block);
|
||||
let (version, params, execution_data) = payload_to_new_payload(
|
||||
payload,
|
||||
sidecar,
|
||||
false,
|
||||
block.header.withdrawals_root,
|
||||
Some(new_payload_version),
|
||||
)?;
|
||||
|
||||
let (version, params) = if self.reth_new_payload {
|
||||
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?)
|
||||
} else {
|
||||
(Some(version), params)
|
||||
};
|
||||
|
||||
// Save payload to file with version info for replay
|
||||
let payload_path =
|
||||
self.output.join(format!("payload_block_{}.json", block.header.number));
|
||||
let file = GasRampPayloadFile {
|
||||
version: version.map(|v| v as u8),
|
||||
block_hash,
|
||||
params: params.clone(),
|
||||
};
|
||||
let payload_json = serde_json::to_string_pretty(&file)?;
|
||||
std::fs::write(&payload_path, &payload_json)?;
|
||||
info!(target: "reth-bench", block_number = block.header.number, path = %payload_path.display(), "Saved payload");
|
||||
|
||||
let _ = call_new_payload_with_reth(&provider, version, params).await?;
|
||||
|
||||
let forkchoice_state = ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash: block_hash,
|
||||
finalized_block_hash: block_hash,
|
||||
};
|
||||
call_forkchoice_updated_with_reth(&provider, version, forkchoice_state).await?;
|
||||
|
||||
parent_header = block.header;
|
||||
parent_hash = block_hash;
|
||||
blocks_processed += 1;
|
||||
|
||||
let progress = match mode {
|
||||
RampMode::Blocks(total) => format!("{blocks_processed}/{total}"),
|
||||
RampMode::TargetGasLimit(target) => {
|
||||
let pct = (parent_header.gas_limit as f64 / target as f64 * 100.0).min(100.0);
|
||||
format!("{pct:.1}%")
|
||||
}
|
||||
};
|
||||
info!(target: "reth-bench", progress, block_number = parent_header.number, gas_limit = parent_header.gas_limit, "Block processed");
|
||||
}
|
||||
|
||||
let final_gas_limit = parent_header.gas_limit;
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
total_duration=?total_benchmark_duration.elapsed(),
|
||||
blocks_processed,
|
||||
final_gas_limit,
|
||||
"Benchmark complete"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const fn max_gas_limit_increase(parent_gas_limit: u64) -> u64 {
|
||||
(parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR).saturating_sub(1)
|
||||
}
|
||||
|
||||
const fn should_stop(mode: RampMode, blocks_processed: u64, current_gas_limit: u64) -> bool {
|
||||
match mode {
|
||||
RampMode::Blocks(target_blocks) => blocks_processed >= target_blocks,
|
||||
RampMode::TargetGasLimit(target) => current_gas_limit >= target,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
//! Common helpers for reth-bench commands.
|
||||
|
||||
use crate::valid_payload::call_forkchoice_updated;
|
||||
use eyre::Result;
|
||||
use std::{
|
||||
io::{BufReader, Read},
|
||||
@@ -69,6 +70,180 @@ pub(crate) fn parse_duration(s: &str) -> eyre::Result<Duration> {
|
||||
}
|
||||
}
|
||||
|
||||
use alloy_consensus::Header;
|
||||
use alloy_eips::eip4844::kzg_to_versioned_hash;
|
||||
use alloy_primitives::{Address, B256};
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, RootProvider};
|
||||
use alloy_rpc_types_engine::{
|
||||
CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ForkchoiceState,
|
||||
PayloadAttributes, PayloadId,
|
||||
};
|
||||
use eyre::OptionExt;
|
||||
use reth_chainspec::{ChainSpec, EthereumHardforks};
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use tracing::debug;
|
||||
|
||||
/// Prepared payload request data for triggering block building.
|
||||
pub(crate) struct PayloadRequest {
|
||||
/// The payload attributes for the new block.
|
||||
pub(crate) attributes: PayloadAttributes,
|
||||
/// The forkchoice state pointing to the parent block.
|
||||
pub(crate) forkchoice_state: ForkchoiceState,
|
||||
/// The engine API version for FCU calls.
|
||||
pub(crate) fcu_version: EngineApiMessageVersion,
|
||||
/// The getPayload version to use (1-5).
|
||||
pub(crate) get_payload_version: u8,
|
||||
/// The newPayload version to use.
|
||||
pub(crate) new_payload_version: EngineApiMessageVersion,
|
||||
}
|
||||
|
||||
/// Prepare payload attributes and forkchoice state for a new block.
|
||||
pub(crate) fn prepare_payload_request(
|
||||
chain_spec: &ChainSpec,
|
||||
timestamp: u64,
|
||||
parent_hash: B256,
|
||||
) -> PayloadRequest {
|
||||
let shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
|
||||
let cancun_active = chain_spec.is_cancun_active_at_timestamp(timestamp);
|
||||
let prague_active = chain_spec.is_prague_active_at_timestamp(timestamp);
|
||||
let osaka_active = chain_spec.is_osaka_active_at_timestamp(timestamp);
|
||||
|
||||
// FCU version: V3 for Cancun+Prague+Osaka, V2 for Shanghai, V1 otherwise
|
||||
let fcu_version = if cancun_active {
|
||||
EngineApiMessageVersion::V3
|
||||
} else if shanghai_active {
|
||||
EngineApiMessageVersion::V2
|
||||
} else {
|
||||
EngineApiMessageVersion::V1
|
||||
};
|
||||
|
||||
// getPayload version: 5 for Osaka, 4 for Prague, 3 for Cancun, 2 for Shanghai, 1 otherwise
|
||||
// newPayload version: 4 for Prague+Osaka (no V5), 3 for Cancun, 2 for Shanghai, 1 otherwise
|
||||
let (get_payload_version, new_payload_version) = if osaka_active {
|
||||
(5, EngineApiMessageVersion::V4) // Osaka uses getPayloadV5 but newPayloadV4
|
||||
} else if prague_active {
|
||||
(4, EngineApiMessageVersion::V4)
|
||||
} else if cancun_active {
|
||||
(3, EngineApiMessageVersion::V3)
|
||||
} else if shanghai_active {
|
||||
(2, EngineApiMessageVersion::V2)
|
||||
} else {
|
||||
(1, EngineApiMessageVersion::V1)
|
||||
};
|
||||
|
||||
PayloadRequest {
|
||||
attributes: PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: Address::ZERO,
|
||||
withdrawals: shanghai_active.then(Vec::new),
|
||||
parent_beacon_block_root: cancun_active.then_some(B256::ZERO),
|
||||
},
|
||||
forkchoice_state: ForkchoiceState {
|
||||
head_block_hash: parent_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
},
|
||||
fcu_version,
|
||||
get_payload_version,
|
||||
new_payload_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger payload building via FCU and retrieve the built payload.
|
||||
///
|
||||
/// This sends a forkchoiceUpdated with payload attributes to start building,
|
||||
/// then calls getPayload to retrieve the result.
|
||||
pub(crate) async fn build_payload(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
request: PayloadRequest,
|
||||
) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
|
||||
let fcu_result = call_forkchoice_updated(
|
||||
provider,
|
||||
request.fcu_version,
|
||||
request.forkchoice_state,
|
||||
Some(request.attributes.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let payload_id =
|
||||
fcu_result.payload_id.ok_or_eyre("Payload builder did not return a payload id")?;
|
||||
|
||||
get_payload_with_sidecar(
|
||||
provider,
|
||||
request.get_payload_version,
|
||||
payload_id,
|
||||
request.attributes.parent_beacon_block_root,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Convert an RPC block to a consensus header and block hash.
|
||||
pub(crate) fn rpc_block_to_header(block: alloy_provider::network::AnyRpcBlock) -> (Header, B256) {
|
||||
let block_hash = block.header.hash;
|
||||
let header = block.header.inner.clone().into_header_with_defaults();
|
||||
(header, block_hash)
|
||||
}
|
||||
|
||||
/// Compute versioned hashes from KZG commitments.
|
||||
fn versioned_hashes_from_commitments(
|
||||
commitments: &[alloy_primitives::FixedBytes<48>],
|
||||
) -> Vec<B256> {
|
||||
commitments.iter().map(|c| kzg_to_versioned_hash(c.as_ref())).collect()
|
||||
}
|
||||
|
||||
/// Fetch an execution payload using the appropriate engine API version.
|
||||
pub(crate) async fn get_payload_with_sidecar(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
version: u8,
|
||||
payload_id: PayloadId,
|
||||
parent_beacon_block_root: Option<B256>,
|
||||
) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
|
||||
debug!(target: "reth-bench", get_payload_version = ?version, ?payload_id, "Sending getPayload");
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
let payload = provider.get_payload_v1(payload_id).await?;
|
||||
Ok((ExecutionPayload::V1(payload), ExecutionPayloadSidecar::none()))
|
||||
}
|
||||
2 => {
|
||||
let envelope = provider.get_payload_v2(payload_id).await?;
|
||||
let payload = match envelope.execution_payload {
|
||||
alloy_rpc_types_engine::ExecutionPayloadFieldV2::V1(p) => ExecutionPayload::V1(p),
|
||||
alloy_rpc_types_engine::ExecutionPayloadFieldV2::V2(p) => ExecutionPayload::V2(p),
|
||||
};
|
||||
Ok((payload, ExecutionPayloadSidecar::none()))
|
||||
}
|
||||
3 => {
|
||||
let envelope = provider.get_payload_v3(payload_id).await?;
|
||||
let versioned_hashes =
|
||||
versioned_hashes_from_commitments(&envelope.blobs_bundle.commitments);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V3")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.execution_payload),
|
||||
ExecutionPayloadSidecar::v3(cancun_fields),
|
||||
))
|
||||
}
|
||||
4 => {
|
||||
let envelope = provider.get_payload_v4(payload_id).await?;
|
||||
Ok(envelope.into_payload_and_sidecar(
|
||||
parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V4")?,
|
||||
))
|
||||
}
|
||||
5 => {
|
||||
let envelope = provider.get_payload_v5(payload_id).await?;
|
||||
Ok(envelope.into_payload_and_sidecar(
|
||||
parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V5")?,
|
||||
))
|
||||
}
|
||||
_ => panic!("This tool does not support getPayload versions past v5"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -6,16 +6,17 @@ use reth_node_core::args::LogArgs;
|
||||
use reth_tracing::FileWorkerGuard;
|
||||
|
||||
mod context;
|
||||
mod gas_limit_ramp;
|
||||
mod generate_big_block;
|
||||
pub(crate) mod helpers;
|
||||
pub use generate_big_block::{
|
||||
compute_payload_block_hash, BigBlockPayload, RawTransaction, RpcTransactionSource,
|
||||
TransactionCollector, TransactionSource,
|
||||
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
|
||||
};
|
||||
pub(crate) mod metrics_scraper;
|
||||
mod new_payload_fcu;
|
||||
mod new_payload_only;
|
||||
mod output;
|
||||
mod persistence_waiter;
|
||||
mod replay_payloads;
|
||||
mod send_invalid_payload;
|
||||
mod send_payload;
|
||||
@@ -36,6 +37,9 @@ pub enum Subcommands {
|
||||
/// Benchmark which calls `newPayload`, then `forkchoiceUpdated`.
|
||||
NewPayloadFcu(new_payload_fcu::Command),
|
||||
|
||||
/// Benchmark which builds empty blocks with a ramped gas limit.
|
||||
GasLimitRamp(gas_limit_ramp::Command),
|
||||
|
||||
/// Benchmark which only calls subsequent `newPayload` calls.
|
||||
NewPayloadOnly(new_payload_only::Command),
|
||||
|
||||
@@ -51,16 +55,16 @@ pub enum Subcommands {
|
||||
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
|
||||
SendPayload(send_payload::Command),
|
||||
|
||||
/// Generate a large block by merging consecutive blocks from an RPC.
|
||||
/// Generate a large block by packing transactions from existing blocks.
|
||||
///
|
||||
/// Fetches N consecutive blocks, takes block 0 as the base payload, concatenates
|
||||
/// transactions from blocks 1..N-1, and saves the result to disk as a JSON file
|
||||
/// containing the merged execution data and environment switches at block boundaries.
|
||||
/// This command fetches transactions from real blocks and packs them into a single
|
||||
/// block using the `testing_buildBlockV1` RPC endpoint.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --from-block 20000000
|
||||
/// --count 10 --output-dir ./payloads`
|
||||
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --engine-rpc-url
|
||||
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex --target-gas
|
||||
/// 30000000`
|
||||
GenerateBigBlock(generate_big_block::Command),
|
||||
|
||||
/// Replay pre-generated payloads from a directory.
|
||||
@@ -95,6 +99,7 @@ impl BenchmarkCommand {
|
||||
|
||||
match self.command {
|
||||
Subcommands::NewPayloadFcu(command) => command.execute(ctx).await,
|
||||
Subcommands::GasLimitRamp(command) => command.execute(ctx).await,
|
||||
Subcommands::NewPayloadOnly(command) => command.execute(ctx).await,
|
||||
Subcommands::SendPayload(command) => command.execute(ctx).await,
|
||||
Subcommands::GenerateBigBlock(command) => command.execute(ctx).await,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
//! Runs the `reth bench` command, calling first newPayload for each block, then calling
|
||||
//! forkchoiceUpdated.
|
||||
//!
|
||||
//! Supports configurable waiting behavior:
|
||||
//! - **`--wait-time`**: Fixed sleep interval between blocks.
|
||||
//! - **`--wait-for-persistence`**: Waits for every Nth block to be persisted using the
|
||||
//! `reth_subscribePersistedBlock` subscription, where N matches the engine's persistence
|
||||
//! threshold. This ensures the benchmark doesn't outpace persistence.
|
||||
//!
|
||||
//! Both options can be used together or independently.
|
||||
|
||||
use crate::{
|
||||
bench::{
|
||||
@@ -9,6 +17,9 @@ use crate::{
|
||||
output::{
|
||||
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
|
||||
},
|
||||
persistence_waiter::{
|
||||
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
|
||||
},
|
||||
},
|
||||
valid_payload::{
|
||||
block_to_new_payload, call_forkchoice_updated_with_reth, call_new_payload_with_reth,
|
||||
@@ -18,7 +29,6 @@ use alloy_provider::{ext::DebugApi, Provider};
|
||||
use alloy_rpc_types_engine::ForkchoiceState;
|
||||
use clap::Parser;
|
||||
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;
|
||||
@@ -39,6 +49,16 @@ pub struct Command {
|
||||
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
|
||||
wait_time: Option<Duration>,
|
||||
|
||||
/// Wait for blocks to be persisted before sending the next batch.
|
||||
///
|
||||
/// When enabled, waits for every Nth block to be persisted using the
|
||||
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
|
||||
/// doesn't outpace persistence.
|
||||
///
|
||||
/// The subscription uses the regular RPC websocket endpoint (no JWT required).
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
wait_for_persistence: bool,
|
||||
|
||||
/// Engine persistence threshold used for deciding when to wait for persistence.
|
||||
///
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
|
||||
@@ -86,17 +106,55 @@ impl Command {
|
||||
if let Some(duration) = self.wait_time {
|
||||
info!(target: "reth-bench", "Using wait-time mode with {}ms delay between blocks", duration.as_millis());
|
||||
}
|
||||
if self.wait_for_persistence {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
"Persistence waiting enabled (waits after every {} blocks to match engine gap > {} behavior)",
|
||||
self.persistence_threshold + 1,
|
||||
self.persistence_threshold
|
||||
);
|
||||
}
|
||||
|
||||
// Set up waiter based on configured options
|
||||
// When both are set: wait at least wait_time, and also wait for persistence if needed
|
||||
let mut waiter = match (self.wait_time, self.wait_for_persistence) {
|
||||
(Some(duration), true) => {
|
||||
let ws_url = derive_ws_rpc_url(
|
||||
self.benchmark.ws_rpc_url.as_deref(),
|
||||
&self.benchmark.engine_rpc_url,
|
||||
)?;
|
||||
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
|
||||
Some(PersistenceWaiter::with_duration_and_subscription(
|
||||
duration,
|
||||
sub,
|
||||
self.persistence_threshold,
|
||||
self.persistence_timeout,
|
||||
))
|
||||
}
|
||||
(Some(duration), false) => Some(PersistenceWaiter::with_duration(duration)),
|
||||
(None, true) => {
|
||||
let ws_url = derive_ws_rpc_url(
|
||||
self.benchmark.ws_rpc_url.as_deref(),
|
||||
&self.benchmark.engine_rpc_url,
|
||||
)?;
|
||||
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
|
||||
Some(PersistenceWaiter::with_subscription(
|
||||
sub,
|
||||
self.persistence_threshold,
|
||||
self.persistence_timeout,
|
||||
))
|
||||
}
|
||||
(None, false) => None,
|
||||
};
|
||||
|
||||
let BenchContext {
|
||||
benchmark_mode,
|
||||
block_provider,
|
||||
auth_provider,
|
||||
next_block,
|
||||
mut next_block,
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let total_blocks = benchmark_mode.total_blocks();
|
||||
@@ -109,71 +167,70 @@ impl Command {
|
||||
|
||||
let buffer_size = self.rpc_block_buffer_size;
|
||||
|
||||
let mut blocks = Box::pin(
|
||||
stream::iter((next_block..)
|
||||
.take_while(|next_block| {
|
||||
benchmark_mode.contains(*next_block)
|
||||
}))
|
||||
.map(|next_block| {
|
||||
let block_provider = block_provider.clone();
|
||||
async move {
|
||||
let block_res = block_provider
|
||||
.get_block_by_number(next_block.into())
|
||||
.full()
|
||||
.await
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to fetch block by number {next_block}")
|
||||
});
|
||||
let block =
|
||||
match block_res.and_then(|opt| opt.ok_or_eyre("Block not found")) {
|
||||
Ok(block) => block,
|
||||
Err(e) => {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch block {next_block}: {e}");
|
||||
return Err(e)
|
||||
}
|
||||
};
|
||||
// Use a oneshot channel to propagate errors from the spawned task
|
||||
let (error_sender, mut error_receiver) = tokio::sync::oneshot::channel();
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel(buffer_size);
|
||||
|
||||
let rlp = if rlp_blocks {
|
||||
let rlp = match block_provider
|
||||
.debug_get_raw_block(next_block.into())
|
||||
.await
|
||||
{
|
||||
Ok(rlp) => rlp,
|
||||
Err(e) => {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}: {e}");
|
||||
return Err(e.into())
|
||||
}
|
||||
};
|
||||
Some(rlp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let head_block_hash = block.header.hash;
|
||||
let safe_block_hash = block_provider
|
||||
.get_block_by_number(block.header.number.saturating_sub(32).into());
|
||||
|
||||
let finalized_block_hash = block_provider
|
||||
.get_block_by_number(block.header.number.saturating_sub(64).into());
|
||||
|
||||
let (safe, finalized) =
|
||||
tokio::join!(safe_block_hash, finalized_block_hash);
|
||||
|
||||
let safe_block_hash = match safe {
|
||||
Ok(Some(block)) => block.header.hash,
|
||||
Ok(None) | Err(_) => head_block_hash,
|
||||
};
|
||||
|
||||
let finalized_block_hash = match finalized {
|
||||
Ok(Some(block)) => block.header.hash,
|
||||
Ok(None) | Err(_) => head_block_hash,
|
||||
};
|
||||
|
||||
Ok((block, head_block_hash, safe_block_hash, finalized_block_hash, rlp))
|
||||
tokio::task::spawn(async move {
|
||||
while benchmark_mode.contains(next_block) {
|
||||
let block_res = block_provider
|
||||
.get_block_by_number(next_block.into())
|
||||
.full()
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to fetch block by number {next_block}"));
|
||||
let block = match block_res.and_then(|opt| opt.ok_or_eyre("Block not found")) {
|
||||
Ok(block) => block,
|
||||
Err(e) => {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch block {next_block}: {e}");
|
||||
let _ = error_sender.send(e);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.buffered(buffer_size),
|
||||
);
|
||||
};
|
||||
|
||||
let rlp = if rlp_blocks {
|
||||
let rlp = match block_provider.debug_get_raw_block(next_block.into()).await {
|
||||
Ok(rlp) => rlp,
|
||||
Err(e) => {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}: {e}");
|
||||
let _ = error_sender
|
||||
.send(eyre::eyre!("Failed to fetch raw block {next_block}: {e}"));
|
||||
break;
|
||||
}
|
||||
};
|
||||
Some(rlp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let head_block_hash = block.header.hash;
|
||||
let safe_block_hash = block_provider
|
||||
.get_block_by_number(block.header.number.saturating_sub(32).into());
|
||||
|
||||
let finalized_block_hash = block_provider
|
||||
.get_block_by_number(block.header.number.saturating_sub(64).into());
|
||||
|
||||
let (safe, finalized) = tokio::join!(safe_block_hash, finalized_block_hash,);
|
||||
|
||||
let safe_block_hash = match safe {
|
||||
Ok(Some(block)) => block.header.hash,
|
||||
Ok(None) | Err(_) => head_block_hash,
|
||||
};
|
||||
|
||||
let finalized_block_hash = match finalized {
|
||||
Ok(Some(block)) => block.header.hash,
|
||||
Ok(None) | Err(_) => head_block_hash,
|
||||
};
|
||||
|
||||
next_block += 1;
|
||||
if let Err(e) = sender
|
||||
.send((block, head_block_hash, safe_block_hash, finalized_block_hash, rlp))
|
||||
.await
|
||||
{
|
||||
tracing::error!(target: "reth-bench", "Failed to send block data: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut blocks_processed = 0u64;
|
||||
@@ -182,7 +239,7 @@ impl Command {
|
||||
|
||||
while let Some((block, head, safe, finalized, rlp)) = {
|
||||
let wait_start = Instant::now();
|
||||
let result = blocks.try_next().await?;
|
||||
let result = receiver.recv().await;
|
||||
total_wait_time += wait_start.elapsed();
|
||||
result
|
||||
} {
|
||||
@@ -199,14 +256,8 @@ impl Command {
|
||||
finalized_block_hash: finalized,
|
||||
};
|
||||
|
||||
let (version, params) = block_to_new_payload(
|
||||
block,
|
||||
is_optimism,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
)?;
|
||||
let (version, params) =
|
||||
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
|
||||
let start = Instant::now();
|
||||
let server_timings =
|
||||
call_new_payload_with_reth(&auth_provider, version, params).await?;
|
||||
@@ -264,8 +315,8 @@ impl Command {
|
||||
warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
|
||||
}
|
||||
|
||||
if let Some(wait_time) = self.wait_time {
|
||||
tokio::time::sleep(wait_time).await;
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(block_number).await?;
|
||||
}
|
||||
|
||||
let gas_row =
|
||||
@@ -273,6 +324,15 @@ impl Command {
|
||||
results.push((gas_row, combined_result));
|
||||
}
|
||||
|
||||
// Check if the spawned task encountered an error
|
||||
if let Ok(error) = error_receiver.try_recv() {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Drop waiter - we don't need to wait for final blocks to persist
|
||||
// since the benchmark goal is measuring Ggas/s of newPayload/FCU, not persistence.
|
||||
drop(waiter);
|
||||
|
||||
let (gas_output_results, combined_results): (Vec<TotalGasRow>, Vec<CombinedResult>) =
|
||||
results.into_iter().unzip();
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ impl Command {
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let total_blocks = benchmark_mode.total_blocks();
|
||||
@@ -124,14 +122,8 @@ impl Command {
|
||||
|
||||
debug!(target: "reth-bench", number=?block.header.number, "Sending payload to engine");
|
||||
|
||||
let (version, params) = block_to_new_payload(
|
||||
block,
|
||||
is_optimism,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
)?;
|
||||
let (version, params) =
|
||||
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
|
||||
|
||||
let start = Instant::now();
|
||||
let server_timings =
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Contains various benchmark output formats, either for logging or for
|
||||
//! serialization to / from files.
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use csv::Writer;
|
||||
use eyre::OptionExt;
|
||||
use reth_primitives_traits::constants::GIGAGAS;
|
||||
use serde::{ser::SerializeStruct, Serialize};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use std::{fs, path::Path, time::Duration};
|
||||
use tracing::info;
|
||||
|
||||
@@ -17,6 +18,20 @@ pub(crate) const COMBINED_OUTPUT_SUFFIX: &str = "combined_latency.csv";
|
||||
/// This is the suffix for new payload output csv files.
|
||||
pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
|
||||
|
||||
/// Serialized format for gas ramp payloads on disk.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct GasRampPayloadFile {
|
||||
/// Engine API version (1-5).
|
||||
///
|
||||
/// `None` indicates that `reth_newPayload` should be used.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) version: Option<u8>,
|
||||
/// The block hash for FCU.
|
||||
pub(crate) block_hash: B256,
|
||||
/// The params to pass to newPayload.
|
||||
pub(crate) params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// This represents the results of a single `newPayload` call in the benchmark, containing the gas
|
||||
/// used and the `newPayload` latency.
|
||||
#[derive(Debug)]
|
||||
|
||||
334
bin/reth-bench/src/bench/persistence_waiter.rs
Normal file
334
bin/reth-bench/src/bench/persistence_waiter.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! Persistence waiting utilities for benchmarks.
|
||||
//!
|
||||
//! Provides waiting behavior to control benchmark pacing:
|
||||
//! - **Fixed duration waits**: Sleep for a fixed time between blocks
|
||||
//! - **Persistence-based waits**: Wait for blocks to be persisted using
|
||||
//! `reth_subscribePersistedBlock` subscription
|
||||
//! - **Combined mode**: Wait at least the fixed duration, and also wait for persistence if the
|
||||
//! block hasn't been persisted yet (whichever takes longer)
|
||||
|
||||
use alloy_eips::BlockNumHash;
|
||||
use alloy_network::Ethereum;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
use alloy_pubsub::SubscriptionStream;
|
||||
use alloy_rpc_client::RpcClient;
|
||||
use alloy_transport_ws::WsConnect;
|
||||
use eyre::Context;
|
||||
use futures::StreamExt;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Default `WebSocket` RPC port for reth.
|
||||
const DEFAULT_WS_RPC_PORT: u16 = 8546;
|
||||
use url::Url;
|
||||
|
||||
/// Returns the websocket RPC URL used for the persistence subscription.
|
||||
///
|
||||
/// Preference:
|
||||
/// - If `ws_rpc_url` is provided, use it directly.
|
||||
/// - Otherwise, derive a WS RPC URL from `engine_rpc_url`.
|
||||
///
|
||||
/// The persistence subscription endpoint (`reth_subscribePersistedBlock`) is exposed on
|
||||
/// the regular RPC server (WS port, usually 8546), not on the engine API port (usually 8551).
|
||||
/// Since we may only have the engine URL by default, we convert the scheme
|
||||
/// (http→ws, https→wss) and force the port to 8546.
|
||||
pub(crate) fn derive_ws_rpc_url(
|
||||
ws_rpc_url: Option<&str>,
|
||||
engine_rpc_url: &str,
|
||||
) -> eyre::Result<Url> {
|
||||
if let Some(ws_url) = ws_rpc_url {
|
||||
let parsed: Url = ws_url
|
||||
.parse()
|
||||
.wrap_err_with(|| format!("Failed to parse WebSocket RPC URL: {ws_url}"))?;
|
||||
info!(target: "reth-bench", ws_url = %parsed, "Using provided WebSocket RPC URL");
|
||||
Ok(parsed)
|
||||
} else {
|
||||
let derived = engine_url_to_ws_url(engine_rpc_url)?;
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
engine_url = %engine_rpc_url,
|
||||
%derived,
|
||||
"Derived WebSocket RPC URL from engine RPC URL"
|
||||
);
|
||||
Ok(derived)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an engine API URL to the default RPC websocket URL.
|
||||
///
|
||||
/// Transformations:
|
||||
/// - `http` → `ws`
|
||||
/// - `https` → `wss`
|
||||
/// - `ws` / `wss` keep their scheme
|
||||
/// - Port is always set to `8546`, reth's default RPC websocket port.
|
||||
///
|
||||
/// This is used when we only know the engine API URL (typically `:8551`) but
|
||||
/// need to connect to the node's WS RPC endpoint for persistence events.
|
||||
fn engine_url_to_ws_url(engine_url: &str) -> eyre::Result<Url> {
|
||||
let url: Url = engine_url
|
||||
.parse()
|
||||
.wrap_err_with(|| format!("Failed to parse engine RPC URL: {engine_url}"))?;
|
||||
|
||||
let mut ws_url = url.clone();
|
||||
|
||||
match ws_url.scheme() {
|
||||
"http" => ws_url
|
||||
.set_scheme("ws")
|
||||
.map_err(|_| eyre::eyre!("Failed to set WS scheme for URL: {url}"))?,
|
||||
"https" => ws_url
|
||||
.set_scheme("wss")
|
||||
.map_err(|_| eyre::eyre!("Failed to set WSS scheme for URL: {url}"))?,
|
||||
"ws" | "wss" => {}
|
||||
scheme => {
|
||||
return Err(eyre::eyre!(
|
||||
"Unsupported URL scheme '{scheme}' for URL: {url}. Expected http, https, ws, or wss."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
ws_url
|
||||
.set_port(Some(DEFAULT_WS_RPC_PORT))
|
||||
.map_err(|_| eyre::eyre!("Failed to set port for URL: {url}"))?;
|
||||
|
||||
Ok(ws_url)
|
||||
}
|
||||
|
||||
/// Waits until the persistence subscription reports that `target` has been persisted.
|
||||
///
|
||||
/// Consumes subscription events until `last_persisted >= target`, or returns an error if:
|
||||
/// - the subscription stream ends unexpectedly, or
|
||||
/// - `timeout` elapses before `target` is observed.
|
||||
async fn wait_for_persistence(
|
||||
stream: &mut SubscriptionStream<BlockNumHash>,
|
||||
target: u64,
|
||||
last_persisted: &mut u64,
|
||||
timeout: Duration,
|
||||
) -> eyre::Result<()> {
|
||||
tokio::time::timeout(timeout, async {
|
||||
while *last_persisted < target {
|
||||
match stream.next().await {
|
||||
Some(persisted) => {
|
||||
*last_persisted = persisted.number;
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
persisted_block = ?last_persisted,
|
||||
"Received persistence notification"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(eyre::eyre!("Persistence subscription closed unexpectedly"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
eyre::eyre!(
|
||||
"Persistence timeout: target block {} not persisted within {:?}. Last persisted: {}",
|
||||
target,
|
||||
timeout,
|
||||
last_persisted
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
/// Wrapper that keeps both the subscription stream and the underlying provider alive.
|
||||
/// The provider must be kept alive for the subscription to continue receiving events.
|
||||
pub(crate) struct PersistenceSubscription {
|
||||
_provider: RootProvider<Ethereum>,
|
||||
stream: SubscriptionStream<BlockNumHash>,
|
||||
}
|
||||
|
||||
impl PersistenceSubscription {
|
||||
const fn new(
|
||||
provider: RootProvider<Ethereum>,
|
||||
stream: SubscriptionStream<BlockNumHash>,
|
||||
) -> Self {
|
||||
Self { _provider: provider, stream }
|
||||
}
|
||||
|
||||
const fn stream_mut(&mut self) -> &mut SubscriptionStream<BlockNumHash> {
|
||||
&mut self.stream
|
||||
}
|
||||
}
|
||||
|
||||
/// Establishes a websocket connection and subscribes to `reth_subscribePersistedBlock`.
|
||||
///
|
||||
/// The `keepalive_interval` is set to match `persistence_timeout` so that the `WebSocket`
|
||||
/// connection is not dropped during long MDBX commits that block the server from responding
|
||||
/// to pings.
|
||||
pub(crate) async fn setup_persistence_subscription(
|
||||
ws_url: Url,
|
||||
persistence_timeout: Duration,
|
||||
) -> eyre::Result<PersistenceSubscription> {
|
||||
info!(target: "reth-bench", "Connecting to WebSocket at {} for persistence subscription", ws_url);
|
||||
|
||||
let ws_connect =
|
||||
WsConnect::new(ws_url.to_string()).with_keepalive_interval(persistence_timeout);
|
||||
let client = RpcClient::connect_pubsub(ws_connect)
|
||||
.await
|
||||
.wrap_err("Failed to connect to WebSocket RPC endpoint")?;
|
||||
let provider: RootProvider<Ethereum> = RootProvider::new(client);
|
||||
|
||||
let subscription = provider
|
||||
.subscribe_to::<BlockNumHash>("reth_subscribePersistedBlock")
|
||||
.await
|
||||
.wrap_err("Failed to subscribe to persistence notifications")?;
|
||||
|
||||
info!(target: "reth-bench", "Subscribed to persistence notifications");
|
||||
|
||||
Ok(PersistenceSubscription::new(provider, subscription.into_stream()))
|
||||
}
|
||||
|
||||
/// Encapsulates the block waiting logic.
|
||||
///
|
||||
/// Provides a simple `on_block()` interface that handles both:
|
||||
/// - Fixed duration waits (when `wait_time` is set)
|
||||
/// - Persistence-based waits (when `subscription` is set)
|
||||
///
|
||||
/// For persistence mode, waits after every `(threshold + 1)` blocks.
|
||||
pub(crate) struct PersistenceWaiter {
|
||||
wait_time: Option<Duration>,
|
||||
subscription: Option<PersistenceSubscription>,
|
||||
blocks_sent: u64,
|
||||
last_persisted: u64,
|
||||
threshold: u64,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl PersistenceWaiter {
|
||||
pub(crate) const fn with_duration(wait_time: Duration) -> Self {
|
||||
Self {
|
||||
wait_time: Some(wait_time),
|
||||
subscription: None,
|
||||
blocks_sent: 0,
|
||||
last_persisted: 0,
|
||||
threshold: 0,
|
||||
timeout: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn with_subscription(
|
||||
subscription: PersistenceSubscription,
|
||||
threshold: u64,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
wait_time: None,
|
||||
subscription: Some(subscription),
|
||||
blocks_sent: 0,
|
||||
last_persisted: 0,
|
||||
threshold,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a waiter that combines both duration and persistence waiting.
|
||||
///
|
||||
/// Waits at least `wait_time` between blocks, and also waits for persistence
|
||||
/// if the block hasn't been persisted yet (whichever takes longer).
|
||||
pub(crate) const fn with_duration_and_subscription(
|
||||
wait_time: Duration,
|
||||
subscription: PersistenceSubscription,
|
||||
threshold: u64,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
wait_time: Some(wait_time),
|
||||
subscription: Some(subscription),
|
||||
blocks_sent: 0,
|
||||
last_persisted: 0,
|
||||
threshold,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once per block. Waits based on the configured mode.
|
||||
///
|
||||
/// When both `wait_time` and `subscription` are set (combined mode):
|
||||
/// - Always waits at least `wait_time`
|
||||
/// - Additionally waits for persistence if we're at a persistence checkpoint
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
pub(crate) async fn on_block(&mut self, block_number: u64) -> eyre::Result<()> {
|
||||
// Always wait for the fixed duration if configured
|
||||
if let Some(wait_time) = self.wait_time {
|
||||
tokio::time::sleep(wait_time).await;
|
||||
}
|
||||
|
||||
// Check persistence if subscription is configured
|
||||
let Some(ref mut subscription) = self.subscription else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.blocks_sent += 1;
|
||||
|
||||
if self.blocks_sent % (self.threshold + 1) == 0 {
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
target_block = ?block_number,
|
||||
last_persisted = self.last_persisted,
|
||||
blocks_sent = self.blocks_sent,
|
||||
"Waiting for persistence"
|
||||
);
|
||||
|
||||
wait_for_persistence(
|
||||
subscription.stream_mut(),
|
||||
block_number,
|
||||
&mut self.last_persisted,
|
||||
self.timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
persisted = self.last_persisted,
|
||||
"Persistence caught up"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn test_engine_url_to_ws_url() {
|
||||
// http -> ws, always uses port 8546
|
||||
let result = engine_url_to_ws_url("http://localhost:8551").unwrap();
|
||||
assert_eq!(result.as_str(), "ws://localhost:8546/");
|
||||
|
||||
// https -> wss
|
||||
let result = engine_url_to_ws_url("https://localhost:8551").unwrap();
|
||||
assert_eq!(result.as_str(), "wss://localhost:8546/");
|
||||
|
||||
// Custom engine port still maps to 8546
|
||||
let result = engine_url_to_ws_url("http://localhost:9551").unwrap();
|
||||
assert_eq!(result.port(), Some(8546));
|
||||
|
||||
// Already ws passthrough
|
||||
let result = engine_url_to_ws_url("ws://localhost:8546").unwrap();
|
||||
assert_eq!(result.scheme(), "ws");
|
||||
|
||||
// Invalid inputs
|
||||
assert!(engine_url_to_ws_url("ftp://localhost:8551").is_err());
|
||||
assert!(engine_url_to_ws_url("not a valid url").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_waiter_with_duration() {
|
||||
let mut waiter = PersistenceWaiter::with_duration(Duration::from_millis(1));
|
||||
|
||||
let start = Instant::now();
|
||||
waiter.on_block(1).await.unwrap();
|
||||
waiter.on_block(2).await.unwrap();
|
||||
waiter.on_block(3).await.unwrap();
|
||||
|
||||
// Should have waited ~3ms total
|
||||
assert!(start.elapsed() >= Duration::from_millis(3));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
//! Command for replaying pre-generated payloads from disk.
|
||||
//!
|
||||
//! This command reads `ExecutionPayloadEnvelopeV4` files from a directory and replays them
|
||||
//! in sequence using `newPayload` followed by `forkchoiceUpdated`.
|
||||
//!
|
||||
//! Supports configurable waiting behavior:
|
||||
//! - **`--wait-time`**: Fixed sleep interval between blocks.
|
||||
//! - **`--wait-for-persistence`**: Waits for every Nth block to be persisted using the
|
||||
//! `reth_subscribePersistedBlock` subscription, where N matches the engine's persistence
|
||||
//! threshold. This ensures the benchmark doesn't outpace persistence.
|
||||
//!
|
||||
//! Both options can be used together or independently.
|
||||
|
||||
use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::{
|
||||
generate_big_block::BigBlockPayload,
|
||||
helpers::parse_duration,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
|
||||
write_benchmark_results, CombinedResult, GasRampPayloadFile, NewPayloadResult,
|
||||
TotalGasOutput, TotalGasRow,
|
||||
},
|
||||
persistence_waiter::{
|
||||
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
|
||||
},
|
||||
},
|
||||
valid_payload::{call_forkchoice_updated_with_reth, call_new_payload_with_reth},
|
||||
@@ -22,15 +36,14 @@ use alloy_rpc_types_engine::{
|
||||
use clap::Parser;
|
||||
use eyre::Context;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::BigBlockData;
|
||||
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use reth_node_core::args::WaitForPersistence;
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
/// `reth bench replay-payloads` command
|
||||
@@ -60,8 +73,9 @@ pub struct Command {
|
||||
#[arg(long, value_name = "SKIP", default_value = "0")]
|
||||
skip: usize,
|
||||
|
||||
/// Deprecated: gas ramp is no longer needed. This flag is accepted but ignored.
|
||||
#[arg(long, value_name = "GAS_RAMP_DIR", hide = true)]
|
||||
/// Optional directory containing gas ramp payloads to replay first.
|
||||
/// These are replayed before the main payloads to warm up the gas limit.
|
||||
#[arg(long, value_name = "GAS_RAMP_DIR")]
|
||||
gas_ramp_dir: Option<PathBuf>,
|
||||
|
||||
/// Optional output directory for benchmark results (CSV files).
|
||||
@@ -75,6 +89,47 @@ pub struct Command {
|
||||
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
|
||||
wait_time: Option<Duration>,
|
||||
|
||||
/// Wait for blocks to be persisted before sending the next batch.
|
||||
///
|
||||
/// When enabled, waits for every Nth block to be persisted using the
|
||||
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
|
||||
/// doesn't outpace persistence.
|
||||
///
|
||||
/// The subscription uses the regular RPC websocket endpoint (no JWT required).
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
wait_for_persistence: bool,
|
||||
|
||||
/// Engine persistence threshold used for deciding when to wait for persistence.
|
||||
///
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
|
||||
/// matches the engine's `DEFAULT_PERSISTENCE_THRESHOLD` (2), so waits occur
|
||||
/// at blocks 3, 6, 9, etc.
|
||||
#[arg(
|
||||
long = "persistence-threshold",
|
||||
value_name = "PERSISTENCE_THRESHOLD",
|
||||
default_value_t = DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
persistence_threshold: u64,
|
||||
|
||||
/// Timeout for waiting on persistence at each checkpoint.
|
||||
///
|
||||
/// Must be long enough to account for the persistence thread being blocked
|
||||
/// by pruning after the previous save.
|
||||
#[arg(
|
||||
long = "persistence-timeout",
|
||||
value_name = "PERSISTENCE_TIMEOUT",
|
||||
value_parser = parse_duration,
|
||||
default_value = "120s",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
persistence_timeout: Duration,
|
||||
|
||||
/// Optional `WebSocket` RPC URL for persistence subscription.
|
||||
/// If not provided, derives from engine RPC URL by changing scheme to ws and port to 8546.
|
||||
#[arg(long, value_name = "WS_RPC_URL", verbatim_doc_comment)]
|
||||
ws_rpc_url: Option<String>,
|
||||
|
||||
/// Use `reth_newPayload` endpoint instead of `engine_newPayload*`.
|
||||
///
|
||||
/// The `reth_newPayload` endpoint is a reth-specific extension that takes `ExecutionData`
|
||||
@@ -83,30 +138,6 @@ pub struct Command {
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
reth_new_payload: bool,
|
||||
|
||||
/// Control when `reth_newPayload` waits for in-flight persistence.
|
||||
///
|
||||
/// Accepts `always` (default — wait on every block), `never`, or a number N
|
||||
/// to wait every N blocks and skip the rest.
|
||||
///
|
||||
/// Requires `--reth-new-payload`.
|
||||
#[arg(
|
||||
long = "wait-for-persistence",
|
||||
value_name = "MODE",
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "always",
|
||||
value_parser = clap::value_parser!(WaitForPersistence),
|
||||
requires = "reth_new_payload",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
wait_for_persistence: Option<WaitForPersistence>,
|
||||
|
||||
/// Skip waiting for execution cache and sparse trie locks before processing.
|
||||
///
|
||||
/// Only works with `--reth-new-payload`. When set, passes `wait_for_caches: false`
|
||||
/// to the `reth_newPayload` endpoint.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment, requires = "reth_new_payload")]
|
||||
no_wait_for_caches: bool,
|
||||
|
||||
/// Optional Prometheus metrics endpoint to scrape after each block.
|
||||
///
|
||||
/// When provided, reth-bench will fetch metrics from this URL after each
|
||||
@@ -120,12 +151,22 @@ pub struct Command {
|
||||
struct LoadedPayload {
|
||||
/// The index (from filename).
|
||||
index: u64,
|
||||
/// The execution data for the block.
|
||||
execution_data: ExecutionData,
|
||||
/// The payload envelope.
|
||||
envelope: ExecutionPayloadEnvelopeV4,
|
||||
/// The block hash.
|
||||
block_hash: B256,
|
||||
/// Big block data containing environment switches and prior block hashes.
|
||||
big_block_data: BigBlockData<ExecutionData>,
|
||||
}
|
||||
|
||||
/// A gas ramp payload loaded from disk.
|
||||
struct GasRampPayload {
|
||||
/// Block number from filename.
|
||||
block_number: u64,
|
||||
/// Engine API version for newPayload.
|
||||
///
|
||||
/// `None` indicates that `reth_newPayload` should be used.
|
||||
version: Option<EngineApiMessageVersion>,
|
||||
/// The file contents.
|
||||
file: GasRampPayloadFile,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -137,10 +178,44 @@ impl Command {
|
||||
if let Some(duration) = self.wait_time {
|
||||
info!(target: "reth-bench", "Using wait-time mode with {}ms delay between blocks", duration.as_millis());
|
||||
}
|
||||
if self.wait_for_persistence {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
"Persistence waiting enabled (waits after every {} blocks to match engine gap > {} behavior)",
|
||||
self.persistence_threshold + 1,
|
||||
self.persistence_threshold
|
||||
);
|
||||
}
|
||||
if self.reth_new_payload {
|
||||
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
|
||||
}
|
||||
|
||||
// Set up waiter based on configured options
|
||||
// When both are set: wait at least wait_time, and also wait for persistence if needed
|
||||
let mut waiter = match (self.wait_time, self.wait_for_persistence) {
|
||||
(Some(duration), true) => {
|
||||
let ws_url = derive_ws_rpc_url(self.ws_rpc_url.as_deref(), &self.engine_rpc_url)?;
|
||||
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
|
||||
Some(PersistenceWaiter::with_duration_and_subscription(
|
||||
duration,
|
||||
sub,
|
||||
self.persistence_threshold,
|
||||
self.persistence_timeout,
|
||||
))
|
||||
}
|
||||
(Some(duration), false) => Some(PersistenceWaiter::with_duration(duration)),
|
||||
(None, true) => {
|
||||
let ws_url = derive_ws_rpc_url(self.ws_rpc_url.as_deref(), &self.engine_rpc_url)?;
|
||||
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
|
||||
Some(PersistenceWaiter::with_subscription(
|
||||
sub,
|
||||
self.persistence_threshold,
|
||||
self.persistence_timeout,
|
||||
))
|
||||
}
|
||||
(None, false) => None,
|
||||
};
|
||||
|
||||
let mut metrics_scraper = MetricsScraper::maybe_new(self.metrics_url.clone());
|
||||
|
||||
// Set up authenticated engine provider
|
||||
@@ -170,48 +245,74 @@ impl Command {
|
||||
"Using initial parent block"
|
||||
);
|
||||
|
||||
// Warn if deprecated --gas-ramp-dir is passed
|
||||
if self.gas_ramp_dir.is_some() {
|
||||
warn!(
|
||||
target: "reth-bench",
|
||||
"--gas-ramp-dir is deprecated and ignored."
|
||||
);
|
||||
}
|
||||
|
||||
// Load all payloads upfront to avoid I/O delays between phases
|
||||
let gas_ramp_payloads = if let Some(ref gas_ramp_dir) = self.gas_ramp_dir {
|
||||
let payloads = self.load_gas_ramp_payloads(gas_ramp_dir)?;
|
||||
if payloads.is_empty() {
|
||||
return Err(eyre::eyre!("No gas ramp payload files found in {:?}", gas_ramp_dir));
|
||||
}
|
||||
info!(target: "reth-bench", count = payloads.len(), "Loaded gas ramp payloads from disk");
|
||||
payloads
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let payloads = self.load_payloads()?;
|
||||
if payloads.is_empty() {
|
||||
return Err(eyre::eyre!("No payload files found in {:?}", self.payload_dir));
|
||||
}
|
||||
info!(target: "reth-bench", count = payloads.len(), "Loaded main payloads from disk");
|
||||
|
||||
// If any payload has env_switches but we're not using reth_newPayload, warn the user
|
||||
if !self.reth_new_payload {
|
||||
let has_env_switches =
|
||||
payloads.iter().any(|p| !p.big_block_data.env_switches.is_empty());
|
||||
if has_env_switches {
|
||||
warn!(
|
||||
target: "reth-bench",
|
||||
"Payloads contain env_switches but --reth-new-payload is not set. \
|
||||
env_switches are only supported with reth_newPayload and will be ignored."
|
||||
);
|
||||
}
|
||||
let mut parent_hash = initial_parent_hash;
|
||||
|
||||
// Replay gas ramp payloads first
|
||||
for (i, payload) in gas_ramp_payloads.iter().enumerate() {
|
||||
info!(
|
||||
target: "reth-bench",
|
||||
gas_ramp_payload = i + 1,
|
||||
total = gas_ramp_payloads.len(),
|
||||
block_number = payload.block_number,
|
||||
block_hash = %payload.file.block_hash,
|
||||
"Executing gas ramp payload (newPayload + FCU)"
|
||||
);
|
||||
|
||||
let _ = call_new_payload_with_reth(
|
||||
&auth_provider,
|
||||
payload.version,
|
||||
payload.file.params.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let fcu_state = ForkchoiceState {
|
||||
head_block_hash: payload.file.block_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
call_forkchoice_updated_with_reth(&auth_provider, payload.version, fcu_state).await?;
|
||||
|
||||
info!(target: "reth-bench", gas_ramp_payload = i + 1, "Gas ramp payload executed successfully");
|
||||
|
||||
parent_hash = payload.file.block_hash;
|
||||
}
|
||||
|
||||
let mut parent_hash = initial_parent_hash;
|
||||
if !gas_ramp_payloads.is_empty() {
|
||||
info!(target: "reth-bench", count = gas_ramp_payloads.len(), "All gas ramp payloads replayed");
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let total_benchmark_duration = Instant::now();
|
||||
|
||||
for (i, payload) in payloads.iter().enumerate() {
|
||||
let execution_data = &payload.execution_data;
|
||||
let envelope = &payload.envelope;
|
||||
let block_hash = payload.block_hash;
|
||||
let v1 = execution_data.payload.as_v1();
|
||||
let execution_payload = &envelope.envelope_inner.execution_payload;
|
||||
let inner_payload = &execution_payload.payload_inner.payload_inner;
|
||||
|
||||
let gas_used = v1.gas_used;
|
||||
let gas_limit = v1.gas_limit;
|
||||
let block_number = v1.block_number;
|
||||
let transaction_count = v1.transactions.len() as u64;
|
||||
let gas_used = inner_payload.gas_used;
|
||||
let gas_limit = inner_payload.gas_limit;
|
||||
let block_number = inner_payload.block_number;
|
||||
let transaction_count =
|
||||
execution_payload.payload_inner.payload_inner.transactions.len() as u64;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
@@ -232,36 +333,27 @@ impl Command {
|
||||
);
|
||||
|
||||
let (version, params) = if self.reth_new_payload {
|
||||
let big_block_data_param = if payload.big_block_data.env_switches.is_empty() &&
|
||||
payload.big_block_data.prior_block_hashes.is_empty()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(payload.big_block_data.clone())
|
||||
let reth_data = ExecutionData {
|
||||
payload: execution_payload.clone().into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
parent_beacon_block_root: B256::ZERO,
|
||||
},
|
||||
PraguePayloadFields {
|
||||
requests: envelope.execution_requests.clone().into(),
|
||||
},
|
||||
),
|
||||
};
|
||||
let wait_for_persistence = self
|
||||
.wait_for_persistence
|
||||
.unwrap_or(WaitForPersistence::Never)
|
||||
.rpc_value(block_number);
|
||||
(
|
||||
None,
|
||||
serde_json::to_value((
|
||||
RethNewPayloadInput::ExecutionData(execution_data.clone()),
|
||||
wait_for_persistence,
|
||||
self.no_wait_for_caches.then_some(false),
|
||||
big_block_data_param,
|
||||
))?,
|
||||
)
|
||||
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(reth_data),))?)
|
||||
} else {
|
||||
let requests =
|
||||
execution_data.sidecar.requests().cloned().unwrap_or_default().to_vec();
|
||||
(
|
||||
Some(EngineApiMessageVersion::V4),
|
||||
serde_json::to_value((
|
||||
execution_data.payload.clone(),
|
||||
execution_payload.clone(),
|
||||
Vec::<B256>::new(),
|
||||
B256::ZERO,
|
||||
requests,
|
||||
envelope.execution_requests.to_vec(),
|
||||
))?,
|
||||
)
|
||||
};
|
||||
@@ -317,8 +409,8 @@ impl Command {
|
||||
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
|
||||
}
|
||||
|
||||
if let Some(wait_time) = self.wait_time {
|
||||
tokio::time::sleep(wait_time).await;
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(block_number).await?;
|
||||
}
|
||||
|
||||
let gas_row =
|
||||
@@ -328,6 +420,10 @@ impl Command {
|
||||
parent_hash = block_hash;
|
||||
}
|
||||
|
||||
// Drop waiter - we don't need to wait for final blocks to persist
|
||||
// since the benchmark goal is measuring Ggas/s of newPayload/FCU, not persistence.
|
||||
drop(waiter);
|
||||
|
||||
let (gas_output_results, combined_results): (Vec<TotalGasRow>, Vec<CombinedResult>) =
|
||||
results.into_iter().unzip();
|
||||
|
||||
@@ -356,42 +452,28 @@ impl Command {
|
||||
}
|
||||
|
||||
/// Load and parse all payload files from the directory.
|
||||
///
|
||||
/// Tries to load each file as a [`BigBlockPayload`] first (which includes `env_switches`),
|
||||
/// falling back to [`ExecutionPayloadEnvelopeV4`] for backwards compatibility.
|
||||
fn load_payloads(&self) -> eyre::Result<Vec<LoadedPayload>> {
|
||||
let mut payloads = Vec::new();
|
||||
|
||||
// Read directory entries — match both legacy "payload_block_*.json" and new
|
||||
// "big_block_*.json" formats
|
||||
// Read directory entries
|
||||
let entries: Vec<_> = std::fs::read_dir(&self.payload_dir)
|
||||
.wrap_err_with(|| format!("Failed to read directory {:?}", self.payload_dir))?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
let name = e.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
e.path().extension().and_then(|s| s.to_str()) == Some("json") &&
|
||||
(name_str.starts_with("payload_block_") ||
|
||||
name_str.starts_with("big_block_"))
|
||||
e.file_name().to_string_lossy().starts_with("payload_block_")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse filenames to get indices and sort.
|
||||
// Supports "payload_block_N.json" and "big_block_FROM_to_TO.json" naming.
|
||||
// Parse filenames to get indices and sort
|
||||
let mut indexed_paths: Vec<(u64, PathBuf)> = entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
let index = if let Some(rest) = name_str.strip_prefix("payload_block_") {
|
||||
rest.strip_suffix(".json")?.parse::<u64>().ok()?
|
||||
} else if let Some(rest) = name_str.strip_prefix("big_block_") {
|
||||
// "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;
|
||||
};
|
||||
// Extract index from "payload_NNN.json"
|
||||
let index_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
|
||||
let index: u64 = index_str.parse().ok()?;
|
||||
Some((index, e.path()))
|
||||
})
|
||||
.collect();
|
||||
@@ -409,42 +491,82 @@ impl Command {
|
||||
for (index, path) in indexed_paths {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
|
||||
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
|
||||
// Try BigBlockPayload first, then fall back to legacy ExecutionPayloadEnvelopeV4
|
||||
let (execution_data, big_block_data) =
|
||||
if let Ok(big_block) = serde_json::from_str::<BigBlockPayload>(&content) {
|
||||
(big_block.execution_data, big_block.big_block_data)
|
||||
} else {
|
||||
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
let execution_data = ExecutionData {
|
||||
payload: envelope.envelope_inner.execution_payload.clone().into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
parent_beacon_block_root: B256::ZERO,
|
||||
},
|
||||
PraguePayloadFields {
|
||||
requests: envelope.execution_requests.clone().into(),
|
||||
},
|
||||
),
|
||||
};
|
||||
(execution_data, BigBlockData::default())
|
||||
};
|
||||
|
||||
let block_hash = execution_data.payload.as_v1().block_hash;
|
||||
let block_hash =
|
||||
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
index = index,
|
||||
block_hash = %block_hash,
|
||||
env_switches = big_block_data.env_switches.len(),
|
||||
prior_block_hashes = big_block_data.prior_block_hashes.len(),
|
||||
path = %path.display(),
|
||||
"Loaded payload"
|
||||
);
|
||||
|
||||
payloads.push(LoadedPayload { index, execution_data, block_hash, big_block_data });
|
||||
payloads.push(LoadedPayload { index, envelope, block_hash });
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
/// Load and parse gas ramp payload files from a directory.
|
||||
fn load_gas_ramp_payloads(&self, dir: &PathBuf) -> eyre::Result<Vec<GasRampPayload>> {
|
||||
let mut payloads = Vec::new();
|
||||
|
||||
let entries: Vec<_> = std::fs::read_dir(dir)
|
||||
.wrap_err_with(|| format!("Failed to read directory {:?}", dir))?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.path().extension().and_then(|s| s.to_str()) == Some("json") &&
|
||||
e.file_name().to_string_lossy().starts_with("payload_block_")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse filenames to get block numbers and sort
|
||||
let mut indexed_paths: Vec<(u64, PathBuf)> = entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Extract block number from "payload_block_NNN.json"
|
||||
let block_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
|
||||
let block_number: u64 = block_str.parse().ok()?;
|
||||
Some((block_number, e.path()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
indexed_paths.sort_by_key(|(num, _)| *num);
|
||||
|
||||
for (block_number, path) in indexed_paths {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
|
||||
let file: GasRampPayloadFile = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
|
||||
let version = if let Some(version) = file.version {
|
||||
match version {
|
||||
1 => EngineApiMessageVersion::V1,
|
||||
2 => EngineApiMessageVersion::V2,
|
||||
3 => EngineApiMessageVersion::V3,
|
||||
4 => EngineApiMessageVersion::V4,
|
||||
5 => EngineApiMessageVersion::V5,
|
||||
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
info!(
|
||||
block_number,
|
||||
block_hash = %file.block_hash,
|
||||
path = %path.display(),
|
||||
"Loaded gas ramp payload"
|
||||
);
|
||||
|
||||
payloads.push(GasRampPayload { block_number, version, file });
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
#[cfg(all(feature = "jemalloc", unix))]
|
||||
use reth_cli_util::allocator::tikv_jemalloc_sys as _;
|
||||
|
||||
pub mod authenticated_transport;
|
||||
pub mod bench;
|
||||
pub mod bench_mode;
|
||||
|
||||
@@ -12,7 +12,6 @@ use alloy_rpc_types_engine::{
|
||||
use alloy_transport::TransportResult;
|
||||
use op_alloy_rpc_types_engine::OpExecutionPayloadV4;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use reth_node_core::args::WaitForPersistence;
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
@@ -168,28 +167,16 @@ where
|
||||
/// Converts an RPC block into versioned engine API params and an [`ExecutionData`].
|
||||
///
|
||||
/// Returns `(version, versioned_params, execution_data)`.
|
||||
///
|
||||
/// `wait_for_persistence` controls how `wait_for_persistence` is passed to
|
||||
/// `reth_newPayload` on a per-block basis.
|
||||
pub(crate) fn block_to_new_payload(
|
||||
block: AnyRpcBlock,
|
||||
is_optimism: bool,
|
||||
rlp: Option<Bytes>,
|
||||
reth_new_payload: bool,
|
||||
wait_for_persistence: WaitForPersistence,
|
||||
no_wait_for_caches: bool,
|
||||
) -> eyre::Result<(Option<EngineApiMessageVersion>, serde_json::Value)> {
|
||||
let block_number = block.header.number;
|
||||
let wait_for_persistence = wait_for_persistence.rpc_value(block_number);
|
||||
|
||||
if let Some(rlp) = rlp {
|
||||
return Ok((
|
||||
None,
|
||||
serde_json::to_value((
|
||||
RethNewPayloadInput::<ExecutionData>::BlockRlp(rlp),
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches.then_some(false),
|
||||
))?,
|
||||
serde_json::to_value((RethNewPayloadInput::<ExecutionData>::BlockRlp(rlp),))?,
|
||||
));
|
||||
}
|
||||
let block = block
|
||||
@@ -207,14 +194,7 @@ pub(crate) fn block_to_new_payload(
|
||||
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)?;
|
||||
|
||||
if reth_new_payload {
|
||||
Ok((
|
||||
None,
|
||||
serde_json::to_value((
|
||||
RethNewPayloadInput::ExecutionData(execution_data),
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches.then_some(false),
|
||||
))?,
|
||||
))
|
||||
Ok((None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?))
|
||||
} else {
|
||||
Ok((Some(version), params))
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ jemalloc = [
|
||||
"reth-provider/jemalloc",
|
||||
]
|
||||
jemalloc-prof = [
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-cli-util/jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-prof",
|
||||
"reth-node-metrics/jemalloc-prof",
|
||||
@@ -136,6 +136,12 @@ jemalloc-symbols = [
|
||||
"jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-symbols",
|
||||
]
|
||||
jemalloc-unprefixed = [
|
||||
"reth-cli-util/jemalloc-unprefixed",
|
||||
"reth-node-core/jemalloc",
|
||||
"reth-node-metrics/jemalloc",
|
||||
"reth-ethereum-cli/jemalloc",
|
||||
]
|
||||
tracy-allocator = [
|
||||
"reth-cli-util/tracy-allocator",
|
||||
"reth-ethereum-cli/tracy-allocator",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof)
|
||||
//! documentation for usage details. This is **not recommended on Windows**.
|
||||
//! - `jemalloc-symbols`: Enables jemalloc symbols for profiling. Includes `jemalloc-prof`.
|
||||
//! - `jemalloc-unprefixed`: Uses unprefixed jemalloc symbols.
|
||||
//! - `tracy-allocator`: Enables [Tracy](https://github.com/wolfpld/tracy) profiler allocator
|
||||
//! integration for memory profiling.
|
||||
//! - `snmalloc`: Uses [snmalloc](https://github.com/microsoft/snmalloc) as the global allocator.
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
// Required for "override_allocator_on_supported_platforms".
|
||||
#[cfg(all(feature = "jemalloc", unix))]
|
||||
use reth_cli_util::allocator::tikv_jemalloc_sys as _;
|
||||
|
||||
#[cfg(all(feature = "jemalloc-prof", unix))]
|
||||
#[unsafe(export_name = "malloc_conf")]
|
||||
#[unsafe(export_name = "_rjem_malloc_conf")]
|
||||
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
@@ -84,7 +84,7 @@ tracing.workspace = true
|
||||
backon.workspace = true
|
||||
secp256k1 = { workspace = true, features = ["global-context", "std", "recovery"] }
|
||||
tokio-stream.workspace = true
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
reqwest.workspace = true
|
||||
url.workspace = true
|
||||
metrics.workspace = true
|
||||
blake3.workspace = true
|
||||
|
||||
@@ -75,15 +75,10 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
|
||||
impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
/// Returns the storage settings for new database initialization.
|
||||
///
|
||||
/// Determined by the `--storage.v2` flag (defaults to `true`).
|
||||
/// Existing databases retain whatever settings are persisted in their
|
||||
/// metadata (checked during genesis init).
|
||||
/// Always returns [`StorageSettings::v2()`] — v2 is the default for all new
|
||||
/// databases. Existing databases use the settings persisted in their metadata.
|
||||
pub fn storage_settings(&self) -> StorageSettings {
|
||||
if self.storage.v2 {
|
||||
StorageSettings::v2()
|
||||
} else {
|
||||
StorageSettings::v1()
|
||||
}
|
||||
StorageSettings::v2()
|
||||
}
|
||||
|
||||
/// Initializes environment according to [`AccessRights`] and returns an instance of
|
||||
|
||||
@@ -8,9 +8,8 @@ use reth_db::{tables, DatabaseEnv};
|
||||
use reth_db_api::table::Table;
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::NodeTypesWithDBAdapter;
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use reth_provider::RocksDBProviderFactory;
|
||||
use std::hash::Hasher;
|
||||
use std::{hash::Hasher, time::Instant};
|
||||
use tracing::info;
|
||||
|
||||
/// RocksDB tables that can be checksummed.
|
||||
|
||||
@@ -22,7 +22,6 @@ mod settings;
|
||||
mod stage_checkpoints;
|
||||
mod state;
|
||||
mod static_file_header;
|
||||
mod static_files;
|
||||
mod stats;
|
||||
/// DB List TUI
|
||||
mod tui;
|
||||
@@ -64,8 +63,6 @@ pub enum Subcommands {
|
||||
RepairTrie(repair_trie::Command),
|
||||
/// Reads and displays the static file segment header
|
||||
StaticFileHeader(static_file_header::Command),
|
||||
/// Static file operations (split, etc.)
|
||||
StaticFiles(static_files::Command),
|
||||
/// Lists current and local database versions
|
||||
Version,
|
||||
/// Returns the full database path
|
||||
@@ -191,11 +188,6 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::StaticFiles(command) => {
|
||||
db_exec!(self.env, tool, N, AccessRights::RO, {
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::Version => {
|
||||
let local_db_version = match get_db_version(&db_path) {
|
||||
Ok(version) => Some(version),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//! Static file related CLI commands
|
||||
|
||||
mod split;
|
||||
|
||||
pub use split::SplitCommand;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_provider::providers::ProviderNodeTypes;
|
||||
|
||||
/// Static files subcommands
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
#[command(subcommand)]
|
||||
command: Subcommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Subcommands {
|
||||
/// Split static files into new files with different blocks-per-file setting
|
||||
Split(SplitCommand),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute the static files command
|
||||
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
|
||||
match self.command {
|
||||
Subcommands::Split(cmd) => cmd.execute(tool),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,878 +0,0 @@
|
||||
use clap::Parser;
|
||||
use reth_codecs::Compact;
|
||||
use reth_db::{
|
||||
cursor::DbCursorRO,
|
||||
static_file::{
|
||||
AccountChangesetMask, BlockHashMask, HeaderMask, ReceiptMask, StorageChangesetMask,
|
||||
TotalDifficultyMask, TransactionMask, TransactionSenderMask,
|
||||
},
|
||||
tables,
|
||||
transaction::DbTx,
|
||||
};
|
||||
use reth_db_api::models::{CompactU256, StoredBlockBodyIndices};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use reth_provider::{
|
||||
providers::{ProviderNodeTypes, StaticFileProvider},
|
||||
DBProvider, StaticFileProviderBuilder, StaticFileProviderFactory, StaticFileWriter,
|
||||
};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tracing::info;
|
||||
|
||||
/// Split static files into new files with different blocks-per-file setting
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SplitCommand {
|
||||
/// Source static files directory.
|
||||
/// If not specified, uses the datadir's static_files directory.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
static_files_dir: Option<PathBuf>,
|
||||
|
||||
/// Output directory for the new static files.
|
||||
/// Required unless --in-place is specified.
|
||||
#[arg(long, value_name = "PATH", required_unless_present = "in_place")]
|
||||
output_dir: Option<PathBuf>,
|
||||
|
||||
/// Number of blocks per output file
|
||||
#[arg(long, value_name = "NUM")]
|
||||
blocks_per_file: u64,
|
||||
|
||||
/// Segments to split (default: all)
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
segments: Option<Vec<StaticFileSegment>>,
|
||||
|
||||
/// Start block number (default: 0)
|
||||
#[arg(long)]
|
||||
from_block: Option<u64>,
|
||||
|
||||
/// End block number (default: highest available)
|
||||
#[arg(long)]
|
||||
to_block: Option<u64>,
|
||||
|
||||
/// Print what would be done without writing
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Split in-place: write to temp dir, verify, then atomically swap.
|
||||
/// Original files are preserved in static_files.bak
|
||||
#[arg(long, conflicts_with = "output_dir")]
|
||||
in_place: bool,
|
||||
|
||||
/// Skip verification step when using --in-place
|
||||
#[arg(long, requires = "in_place")]
|
||||
skip_verify: bool,
|
||||
}
|
||||
|
||||
impl SplitCommand {
|
||||
/// Execute the split command
|
||||
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()>
|
||||
where
|
||||
N::Primitives: NodePrimitives<BlockHeader: Compact, SignedTx: Compact, Receipt: Compact>,
|
||||
{
|
||||
let segments = self.segments.clone().unwrap_or_else(|| StaticFileSegment::iter().collect());
|
||||
|
||||
// Use custom static files dir if provided, otherwise use datadir's static files
|
||||
let (source_provider, source_dir) =
|
||||
if let Some(ref static_files_dir) = self.static_files_dir {
|
||||
let provider = StaticFileProviderBuilder::read_only(static_files_dir)
|
||||
.build::<N::Primitives>()?;
|
||||
let dir = static_files_dir.clone();
|
||||
(provider, dir)
|
||||
} else {
|
||||
let provider = tool.provider_factory.static_file_provider();
|
||||
let dir = provider.directory().to_path_buf();
|
||||
(provider, dir)
|
||||
};
|
||||
|
||||
// Determine output directory
|
||||
let (output_dir, is_in_place) = if self.in_place {
|
||||
let temp_dir = source_dir.with_file_name("static_files.tmp");
|
||||
(temp_dir, true)
|
||||
} else {
|
||||
(self.output_dir.clone().expect("output_dir required when not in_place"), false)
|
||||
};
|
||||
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
output_dir = %output_dir.display(),
|
||||
blocks_per_file = self.blocks_per_file,
|
||||
?segments,
|
||||
from_block = ?self.from_block,
|
||||
to_block = ?self.to_block,
|
||||
dry_run = self.dry_run,
|
||||
in_place = is_in_place,
|
||||
"Splitting static files"
|
||||
);
|
||||
|
||||
if self.dry_run {
|
||||
println!("Dry run mode - no files will be written");
|
||||
if is_in_place {
|
||||
println!("In-place mode:");
|
||||
println!(" 1. Write to: {}", output_dir.display());
|
||||
println!(" 2. Verify output integrity");
|
||||
println!(" 3. Rename {} -> {}.bak", source_dir.display(), source_dir.display());
|
||||
println!(" 4. Rename {} -> {}", output_dir.display(), source_dir.display());
|
||||
}
|
||||
for segment in &segments {
|
||||
let min_block = source_provider.get_lowest_range_start(*segment);
|
||||
let max_block = source_provider.get_highest_static_file_block(*segment);
|
||||
if let (Some(min_block), Some(max_block)) = (min_block, max_block) {
|
||||
let from_block = self.from_block.unwrap_or(min_block).max(min_block);
|
||||
let to_block = self.to_block.unwrap_or(max_block).min(max_block);
|
||||
let num_blocks = to_block.saturating_sub(from_block) + 1;
|
||||
let num_files = num_blocks.div_ceil(self.blocks_per_file);
|
||||
println!(
|
||||
" {segment}: blocks {from_block}..={to_block} ({num_blocks} blocks) -> {num_files} files"
|
||||
);
|
||||
} else {
|
||||
println!(" {segment}: no data available");
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clean up output directory if it exists
|
||||
// For in-place mode: remove previous incomplete temp directory
|
||||
// For regular mode: ensure we start fresh to avoid block number mismatches
|
||||
if output_dir.exists() {
|
||||
info!(target: "reth::cli", output_dir = %output_dir.display(), "Removing existing output directory");
|
||||
reth_fs_util::remove_dir_all(&output_dir)?;
|
||||
}
|
||||
|
||||
reth_fs_util::create_dir_all(&output_dir)?;
|
||||
|
||||
// Calculate segment ranges first to determine the global starting block
|
||||
let mut segment_ranges = Vec::new();
|
||||
for &segment in &segments {
|
||||
let Some(min_block) = source_provider.get_lowest_range_start(segment) else {
|
||||
continue;
|
||||
};
|
||||
let Some(max_block) = source_provider.get_highest_static_file_block(segment) else {
|
||||
continue;
|
||||
};
|
||||
let from_block = self.from_block.unwrap_or(min_block).max(min_block);
|
||||
let to_block = self.to_block.unwrap_or(max_block).min(max_block);
|
||||
if from_block <= to_block {
|
||||
segment_ranges.push((segment, from_block, to_block));
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-load block body indices for segments that need them (transactions, receipts,
|
||||
// transaction senders). This avoids holding a long-lived DB read transaction open and
|
||||
// is much faster than seeking per-block since the entire table is small.
|
||||
let needs_indices = segment_ranges.iter().any(|(seg, _, _)| {
|
||||
matches!(
|
||||
seg,
|
||||
StaticFileSegment::Transactions |
|
||||
StaticFileSegment::Receipts |
|
||||
StaticFileSegment::TransactionSenders
|
||||
)
|
||||
});
|
||||
let block_body_indices = if needs_indices {
|
||||
let global_from = segment_ranges
|
||||
.iter()
|
||||
.filter(|(seg, _, _)| {
|
||||
matches!(
|
||||
seg,
|
||||
StaticFileSegment::Transactions |
|
||||
StaticFileSegment::Receipts |
|
||||
StaticFileSegment::TransactionSenders
|
||||
)
|
||||
})
|
||||
.map(|(_, from, _)| *from)
|
||||
.min()
|
||||
.unwrap();
|
||||
let global_to = segment_ranges
|
||||
.iter()
|
||||
.filter(|(seg, _, _)| {
|
||||
matches!(
|
||||
seg,
|
||||
StaticFileSegment::Transactions |
|
||||
StaticFileSegment::Receipts |
|
||||
StaticFileSegment::TransactionSenders
|
||||
)
|
||||
})
|
||||
.map(|(_, _, to)| *to)
|
||||
.max()
|
||||
.unwrap();
|
||||
|
||||
info!(target: "reth::cli", from_block = global_from, to_block = global_to, "Loading block body indices");
|
||||
Self::load_block_body_indices(tool, global_from, global_to)?
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
for (segment, from_block, to_block) in segment_ranges {
|
||||
info!(target: "reth::cli", ?segment, from_block, to_block, "Processing segment");
|
||||
|
||||
// Build output provider per-segment with genesis_block_number set to this segment's
|
||||
// starting block. This prevents the writer from trying to load non-existent previous
|
||||
// files when segments have different starting blocks (e.g., pruned transactions).
|
||||
let output_provider = StaticFileProviderBuilder::read_write(&output_dir)
|
||||
.with_blocks_per_file(self.blocks_per_file)
|
||||
.with_genesis_block_number(from_block)
|
||||
.build::<N::Primitives>()?;
|
||||
|
||||
match segment {
|
||||
StaticFileSegment::Headers => {
|
||||
self.split_headers::<N>(
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
StaticFileSegment::Transactions => {
|
||||
self.split_transactions::<N>(
|
||||
&block_body_indices,
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
StaticFileSegment::Receipts => {
|
||||
self.split_receipts::<N>(
|
||||
&block_body_indices,
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
StaticFileSegment::TransactionSenders => {
|
||||
self.split_transaction_senders::<N>(
|
||||
&block_body_indices,
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
StaticFileSegment::AccountChangeSets => {
|
||||
self.split_account_changesets::<N>(
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
StaticFileSegment::StorageChangeSets => {
|
||||
self.split_storage_changesets::<N>(
|
||||
&source_provider,
|
||||
&output_provider,
|
||||
from_block,
|
||||
to_block,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", ?segment, "Segment complete");
|
||||
|
||||
// Drop the output provider to release file handles before processing next segment
|
||||
drop(output_provider);
|
||||
}
|
||||
|
||||
// In-place mode: verify and swap directories
|
||||
if is_in_place {
|
||||
// Verification step
|
||||
if !self.skip_verify {
|
||||
info!(target: "reth::cli", "Verifying output integrity");
|
||||
self.verify_output::<N>(&output_dir, &segments)?;
|
||||
}
|
||||
|
||||
// Atomic swap
|
||||
let backup_dir = source_dir.with_file_name("static_files.bak");
|
||||
|
||||
// Remove old backup if exists
|
||||
if backup_dir.exists() {
|
||||
info!(target: "reth::cli", backup_dir = %backup_dir.display(), "Removing old backup");
|
||||
reth_fs_util::remove_dir_all(&backup_dir)?;
|
||||
}
|
||||
|
||||
// Drop source provider to release file handles
|
||||
drop(source_provider);
|
||||
|
||||
// Rename: source -> backup
|
||||
info!(target: "reth::cli",
|
||||
from = %source_dir.display(),
|
||||
to = %backup_dir.display(),
|
||||
"Moving original to backup"
|
||||
);
|
||||
reth_fs_util::rename(&source_dir, &backup_dir)?;
|
||||
|
||||
// Rename: temp -> source
|
||||
info!(target: "reth::cli",
|
||||
from = %output_dir.display(),
|
||||
to = %source_dir.display(),
|
||||
"Moving new files into place"
|
||||
);
|
||||
reth_fs_util::rename(&output_dir, &source_dir)?;
|
||||
|
||||
info!(target: "reth::cli",
|
||||
backup = %backup_dir.display(),
|
||||
"In-place split complete. Original files preserved in backup directory"
|
||||
);
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "Static file split complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the output static files have valid data
|
||||
fn verify_output<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
output_dir: &PathBuf,
|
||||
segments: &[StaticFileSegment],
|
||||
) -> eyre::Result<()> {
|
||||
let provider = StaticFileProviderBuilder::read_only(output_dir).build::<N::Primitives>()?;
|
||||
|
||||
for &segment in segments {
|
||||
let Some(lowest) = provider.get_lowest_range_start(segment) else {
|
||||
return Err(eyre::eyre!("Verification failed: no data for segment {segment}"));
|
||||
};
|
||||
let Some(highest) = provider.get_highest_static_file_block(segment) else {
|
||||
return Err(eyre::eyre!("Verification failed: no data for segment {segment}"));
|
||||
};
|
||||
|
||||
// Verify we can read the first and last blocks
|
||||
provider.get_segment_provider(segment, lowest)?;
|
||||
provider.get_segment_provider(segment, highest)?;
|
||||
|
||||
info!(target: "reth::cli", ?segment, from_block = lowest, to_block = highest, "Verified");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_headers<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
<N::Primitives as NodePrimitives>::BlockHeader: Compact,
|
||||
{
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::Headers)?;
|
||||
|
||||
for block in from_block..=to_block {
|
||||
let jar = source.get_segment_provider(StaticFileSegment::Headers, block)?;
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
let header: <N::Primitives as NodePrimitives>::BlockHeader = cursor
|
||||
.get_one::<HeaderMask<_>>(block.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing header for block {block}"))?;
|
||||
|
||||
let td: CompactU256 = cursor
|
||||
.get_one::<TotalDifficultyMask>(block.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing TD for block {block}"))?;
|
||||
|
||||
let hash = cursor
|
||||
.get_one::<BlockHashMask>(block.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing hash for block {block}"))?;
|
||||
|
||||
writer.append_header_with_td(&header, td.into(), &hash)?;
|
||||
|
||||
if block % 100_000 == 0 {
|
||||
info!(target: "reth::cli", block, to_block, "Headers progress");
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_block_body_indices<N: ProviderNodeTypes>(
|
||||
tool: &DbTool<N>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<HashMap<u64, StoredBlockBodyIndices>> {
|
||||
let provider = tool.provider_factory.provider()?.disable_long_read_transaction_safety();
|
||||
let tx = provider.tx_ref();
|
||||
let mut cursor = tx.cursor_read::<tables::BlockBodyIndices>()?;
|
||||
let mut indices = HashMap::with_capacity((to_block - from_block + 1) as usize);
|
||||
for entry in cursor.walk_range(from_block..=to_block)? {
|
||||
let (block, body_indices) = entry?;
|
||||
indices.insert(block, body_indices);
|
||||
}
|
||||
info!(target: "reth::cli", count = indices.len(), "Loaded block body indices");
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
fn split_transactions<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
<N::Primitives as NodePrimitives>::SignedTx: Compact,
|
||||
{
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::Transactions)?;
|
||||
let mut block = from_block;
|
||||
let mut block_incremented = false;
|
||||
|
||||
while block <= to_block {
|
||||
if !block_incremented {
|
||||
writer.increment_block(block)?;
|
||||
}
|
||||
block_incremented = false;
|
||||
|
||||
// Skip blocks with no transactions until we find one that needs a jar
|
||||
let Some(indices) =
|
||||
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
|
||||
else {
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Transactions progress");
|
||||
}
|
||||
block += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
// Open jar + cursor, reuse for all subsequent blocks within this jar's range
|
||||
let jar =
|
||||
source.get_segment_provider(StaticFileSegment::Transactions, indices.first_tx_num)?;
|
||||
let jar_tx_end =
|
||||
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
loop {
|
||||
if let Some(indices) = block_body_indices.get(&block) {
|
||||
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
|
||||
let transaction: <N::Primitives as NodePrimitives>::SignedTx = cursor
|
||||
.get_one::<TransactionMask<_>>(tx_num.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing transaction {tx_num}"))?;
|
||||
writer.append_transaction(tx_num, &transaction)?;
|
||||
}
|
||||
}
|
||||
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Transactions progress");
|
||||
}
|
||||
block += 1;
|
||||
if block > to_block {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.increment_block(block)?;
|
||||
block_incremented = true;
|
||||
|
||||
// Check if next block's txs need a different jar
|
||||
if let Some(next_indices) = block_body_indices.get(&block) &&
|
||||
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_receipts<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
<N::Primitives as NodePrimitives>::Receipt: Compact,
|
||||
{
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::Receipts)?;
|
||||
let mut block = from_block;
|
||||
let mut block_incremented = false;
|
||||
|
||||
while block <= to_block {
|
||||
if !block_incremented {
|
||||
writer.increment_block(block)?;
|
||||
}
|
||||
block_incremented = false;
|
||||
|
||||
let Some(indices) =
|
||||
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
|
||||
else {
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Receipts progress");
|
||||
}
|
||||
block += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let jar =
|
||||
source.get_segment_provider(StaticFileSegment::Receipts, indices.first_tx_num)?;
|
||||
let jar_tx_end =
|
||||
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
loop {
|
||||
if let Some(indices) = block_body_indices.get(&block) {
|
||||
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
|
||||
let receipt: <N::Primitives as NodePrimitives>::Receipt = cursor
|
||||
.get_one::<ReceiptMask<_>>(tx_num.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing receipt {tx_num}"))?;
|
||||
writer.append_receipt(tx_num, &receipt)?;
|
||||
}
|
||||
}
|
||||
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Receipts progress");
|
||||
}
|
||||
block += 1;
|
||||
if block > to_block {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.increment_block(block)?;
|
||||
block_incremented = true;
|
||||
|
||||
if let Some(next_indices) = block_body_indices.get(&block) &&
|
||||
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_transaction_senders<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()> {
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::TransactionSenders)?;
|
||||
let mut block = from_block;
|
||||
let mut block_incremented = false;
|
||||
|
||||
while block <= to_block {
|
||||
if !block_incremented {
|
||||
writer.increment_block(block)?;
|
||||
}
|
||||
block_incremented = false;
|
||||
|
||||
let Some(indices) =
|
||||
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
|
||||
else {
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Transaction senders progress");
|
||||
}
|
||||
block += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let jar = source
|
||||
.get_segment_provider(StaticFileSegment::TransactionSenders, indices.first_tx_num)?;
|
||||
let jar_tx_end =
|
||||
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
loop {
|
||||
if let Some(indices) = block_body_indices.get(&block) {
|
||||
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
|
||||
let sender = cursor
|
||||
.get_one::<TransactionSenderMask>(tx_num.into())?
|
||||
.ok_or_else(|| eyre::eyre!("Missing sender {tx_num}"))?;
|
||||
writer.append_transaction_sender(tx_num, &sender)?;
|
||||
}
|
||||
}
|
||||
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Transaction senders progress");
|
||||
}
|
||||
block += 1;
|
||||
if block > to_block {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.increment_block(block)?;
|
||||
block_incremented = true;
|
||||
|
||||
if let Some(next_indices) = block_body_indices.get(&block) &&
|
||||
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_account_changesets<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()> {
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::AccountChangeSets)?;
|
||||
let mut block = from_block;
|
||||
|
||||
while block <= to_block {
|
||||
// Open jar + cursor, reuse for all blocks within this jar's range
|
||||
let jar =
|
||||
source.get_segment_provider(StaticFileSegment::AccountChangeSets, block)?;
|
||||
let jar_block_end = jar
|
||||
.user_header()
|
||||
.block_range()
|
||||
.map(|r| r.end())
|
||||
.unwrap_or(u64::MAX);
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
loop {
|
||||
let mut changes = Vec::new();
|
||||
if let Some(offset) = jar.read_changeset_offset(block)? {
|
||||
for i in offset.changeset_range() {
|
||||
if let Some(change) =
|
||||
cursor.get_one::<AccountChangesetMask>(i.into())?
|
||||
{
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.append_account_changeset(changes, block)?;
|
||||
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Account changesets progress");
|
||||
}
|
||||
block += 1;
|
||||
if block > to_block || block > jar_block_end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_storage_changesets<N: ProviderNodeTypes>(
|
||||
&self,
|
||||
source: &StaticFileProvider<N::Primitives>,
|
||||
output: &StaticFileProvider<N::Primitives>,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> eyre::Result<()> {
|
||||
let mut writer = output.get_writer(from_block, StaticFileSegment::StorageChangeSets)?;
|
||||
let mut block = from_block;
|
||||
|
||||
while block <= to_block {
|
||||
let jar =
|
||||
source.get_segment_provider(StaticFileSegment::StorageChangeSets, block)?;
|
||||
let jar_block_end = jar
|
||||
.user_header()
|
||||
.block_range()
|
||||
.map(|r| r.end())
|
||||
.unwrap_or(u64::MAX);
|
||||
let mut cursor = jar.cursor()?;
|
||||
|
||||
loop {
|
||||
let mut changes = Vec::new();
|
||||
if let Some(offset) = jar.read_changeset_offset(block)? {
|
||||
for i in offset.changeset_range() {
|
||||
if let Some(change) =
|
||||
cursor.get_one::<StorageChangesetMask>(i.into())?
|
||||
{
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.append_storage_changeset(changes, block)?;
|
||||
|
||||
if block.is_multiple_of(100_000) {
|
||||
info!(target: "reth::cli", block, to_block, "Storage changesets progress");
|
||||
}
|
||||
block += 1;
|
||||
if block > to_block || block > jar_block_end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct TestCli {
|
||||
#[command(subcommand)]
|
||||
command: TestCommand,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum TestCommand {
|
||||
Split(SplitCommand),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_minimal() {
|
||||
let args = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--output-dir",
|
||||
"/tmp/output",
|
||||
"--blocks-per-file",
|
||||
"100000",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
match args.command {
|
||||
TestCommand::Split(cmd) => {
|
||||
assert_eq!(cmd.output_dir, Some(PathBuf::from("/tmp/output")));
|
||||
assert_eq!(cmd.blocks_per_file, 100000);
|
||||
assert!(cmd.segments.is_none());
|
||||
assert!(cmd.from_block.is_none());
|
||||
assert!(cmd.to_block.is_none());
|
||||
assert!(!cmd.dry_run);
|
||||
assert!(!cmd.in_place);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_full() {
|
||||
let args = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--output-dir",
|
||||
"/tmp/output",
|
||||
"--blocks-per-file",
|
||||
"50000",
|
||||
"--segments",
|
||||
"headers,receipts",
|
||||
"--from-block",
|
||||
"1000",
|
||||
"--to-block",
|
||||
"500000",
|
||||
"--dry-run",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
match args.command {
|
||||
TestCommand::Split(cmd) => {
|
||||
assert_eq!(cmd.output_dir, Some(PathBuf::from("/tmp/output")));
|
||||
assert_eq!(cmd.blocks_per_file, 50000);
|
||||
assert_eq!(
|
||||
cmd.segments,
|
||||
Some(vec![StaticFileSegment::Headers, StaticFileSegment::Receipts])
|
||||
);
|
||||
assert_eq!(cmd.from_block, Some(1000));
|
||||
assert_eq!(cmd.to_block, Some(500000));
|
||||
assert!(cmd.dry_run);
|
||||
assert!(!cmd.in_place);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_in_place() {
|
||||
let args =
|
||||
TestCli::try_parse_from(["test", "split", "--in-place", "--blocks-per-file", "100000"])
|
||||
.unwrap();
|
||||
|
||||
match args.command {
|
||||
TestCommand::Split(cmd) => {
|
||||
assert!(cmd.output_dir.is_none());
|
||||
assert_eq!(cmd.blocks_per_file, 100000);
|
||||
assert!(cmd.in_place);
|
||||
assert!(!cmd.skip_verify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_in_place_skip_verify() {
|
||||
let args = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--in-place",
|
||||
"--skip-verify",
|
||||
"--blocks-per-file",
|
||||
"100000",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
match args.command {
|
||||
TestCommand::Split(cmd) => {
|
||||
assert!(cmd.in_place);
|
||||
assert!(cmd.skip_verify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_output_dir_conflicts_with_in_place() {
|
||||
let result = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--output-dir",
|
||||
"/tmp/out",
|
||||
"--in-place",
|
||||
"--blocks-per-file",
|
||||
"100000",
|
||||
]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_skip_verify_requires_in_place() {
|
||||
// --skip-verify without --in-place should fail
|
||||
let result = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--skip-verify",
|
||||
"--blocks-per-file",
|
||||
"100000",
|
||||
]);
|
||||
assert!(result.is_err(), "--skip-verify should require --in-place");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_split_command_all_segments() {
|
||||
let args = TestCli::try_parse_from([
|
||||
"test",
|
||||
"split",
|
||||
"--output-dir",
|
||||
"/tmp/out",
|
||||
"--blocks-per-file",
|
||||
"10",
|
||||
"--segments",
|
||||
"headers,transactions,receipts,transaction-senders,account-change-sets,storage-change-sets",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
match args.command {
|
||||
TestCommand::Split(cmd) => {
|
||||
let segments = cmd.segments.unwrap();
|
||||
assert_eq!(segments.len(), 6);
|
||||
assert!(segments.contains(&StaticFileSegment::Headers));
|
||||
assert!(segments.contains(&StaticFileSegment::Transactions));
|
||||
assert!(segments.contains(&StaticFileSegment::Receipts));
|
||||
assert!(segments.contains(&StaticFileSegment::TransactionSenders));
|
||||
assert!(segments.contains(&StaticFileSegment::AccountChangeSets));
|
||||
assert!(segments.contains(&StaticFileSegment::StorageChangeSets));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@ use clap::Parser;
|
||||
use eyre::{Result, WrapErr};
|
||||
use reth_db::{mdbx::DatabaseArguments, open_db_read_only, tables, Database};
|
||||
use reth_db_api::transaction::DbTx;
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use reth_stages_types::StageId;
|
||||
use reth_static_file_types::DEFAULT_BLOCKS_PER_STATIC_FILE;
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Generate modular chunk archives and a snapshot manifest from a source datadir.
|
||||
|
||||
@@ -84,10 +84,6 @@ pub struct DownloadDefaults {
|
||||
///
|
||||
/// Falls back to [`default_base_url`](Self::default_base_url) when `None`.
|
||||
pub default_chain_aware_base_url: Option<Cow<'static, str>>,
|
||||
/// URL for the snapshot discovery API that lists available snapshots.
|
||||
///
|
||||
/// Defaults to `https://snapshots.reth.rs/api/snapshots`.
|
||||
pub snapshot_api_url: Cow<'static, str>,
|
||||
/// Optional custom long help text that overrides the generated help
|
||||
pub long_help: Option<String>,
|
||||
}
|
||||
@@ -112,7 +108,6 @@ impl DownloadDefaults {
|
||||
],
|
||||
default_base_url: Cow::Borrowed(RETH_SNAPSHOTS_BASE_URL),
|
||||
default_chain_aware_base_url: None,
|
||||
snapshot_api_url: Cow::Borrowed(RETH_SNAPSHOTS_API_URL),
|
||||
long_help: None,
|
||||
}
|
||||
}
|
||||
@@ -126,11 +121,10 @@ impl DownloadDefaults {
|
||||
return custom_help.clone();
|
||||
}
|
||||
|
||||
let mut help = format!(
|
||||
let mut help = String::from(
|
||||
"Specify a snapshot URL or let the command propose a default one.\n\n\
|
||||
Browse available snapshots at {}\n\
|
||||
Browse available snapshots at https://snapshots.reth.rs\n\
|
||||
or use --list-snapshots to see them from the CLI.\n\nAvailable snapshot sources:\n",
|
||||
self.snapshot_api_url.trim_end_matches("/api/snapshots"),
|
||||
);
|
||||
|
||||
for source in &self.available_snapshots {
|
||||
@@ -175,12 +169,6 @@ impl DownloadDefaults {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the snapshot discovery API URL.
|
||||
pub fn with_snapshot_api_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.snapshot_api_url = url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set custom long help text, overriding the generated help
|
||||
pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
|
||||
self.long_help = Some(help.into());
|
||||
@@ -203,7 +191,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
|
||||
/// Custom URL to download a single snapshot archive (legacy mode).
|
||||
///
|
||||
/// When provided, downloads and extracts a single archive without component selection.
|
||||
/// Browse available snapshots with --list-snapshots.
|
||||
/// Browse available snapshots at <https://snapshots.reth.rs> or use --list-snapshots.
|
||||
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
|
||||
url: Option<String>,
|
||||
|
||||
@@ -273,7 +261,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
|
||||
#[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
|
||||
download_concurrency: usize,
|
||||
|
||||
/// List available snapshots and exit.
|
||||
/// List available snapshots from snapshots.reth.rs and exit.
|
||||
///
|
||||
/// Queries the snapshots API and prints all available snapshots for the selected chain,
|
||||
/// including block number, size, and manifest URL.
|
||||
@@ -1340,17 +1328,7 @@ fn streaming_download_and_extract(
|
||||
let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let err = eyre::Error::from(e);
|
||||
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);
|
||||
last_error = Some(e.into());
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
@@ -1377,15 +1355,6 @@ fn streaming_download_and_extract(
|
||||
match result {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
warn!(target: "reth::cli",
|
||||
url = %url,
|
||||
attempt,
|
||||
max = MAX_DOWNLOAD_RETRIES,
|
||||
err = %e,
|
||||
"Streaming extraction failed, retrying"
|
||||
);
|
||||
}
|
||||
last_error = Some(e);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
@@ -1551,7 +1520,6 @@ fn blocking_process_modular_archive(
|
||||
}
|
||||
|
||||
let format = CompressionFormat::from_url(&archive.file_name)?;
|
||||
let mut last_error: Option<eyre::Error> = None;
|
||||
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
|
||||
cleanup_output_files(target_dir, &archive.output_files);
|
||||
|
||||
@@ -1559,31 +1527,13 @@ fn blocking_process_modular_archive(
|
||||
let cache_dir = cache_dir.ok_or_else(|| eyre::eyre!("Missing cache directory"))?;
|
||||
let archive_path = cache_dir.join(&archive.file_name);
|
||||
let part_path = cache_dir.join(format!("{}.part", archive.file_name));
|
||||
let result =
|
||||
resumable_download(&archive.url, cache_dir, shared.as_ref(), cancel_token.clone())
|
||||
.and_then(|(downloaded_path, _)| {
|
||||
let file = fs::open(&downloaded_path)?;
|
||||
extract_archive_raw(file, format, target_dir)
|
||||
});
|
||||
let (downloaded_path, _downloaded_size) =
|
||||
resumable_download(&archive.url, cache_dir, shared.as_ref(), cancel_token.clone())?;
|
||||
let file = fs::open(&downloaded_path)?;
|
||||
extract_archive_raw(file, format, target_dir)?;
|
||||
let _ = fs::remove_file(&archive_path);
|
||||
let _ = fs::remove_file(&part_path);
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!(target: "reth::cli",
|
||||
file = %archive.file_name,
|
||||
component = %planned.component,
|
||||
attempt,
|
||||
err = %e,
|
||||
"Download or extraction failed, retrying"
|
||||
);
|
||||
last_error = Some(e);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// streaming_download_and_extract already has its own internal retry loop
|
||||
streaming_download_and_extract(
|
||||
&archive.url,
|
||||
format,
|
||||
@@ -1603,13 +1553,6 @@ fn blocking_process_modular_archive(
|
||||
warn!(target: "reth::cli", file = %archive.file_name, component = %planned.component, attempt, "Extracted files failed integrity checks, retrying");
|
||||
}
|
||||
|
||||
if let Some(e) = last_error {
|
||||
return Err(e.wrap_err(format!(
|
||||
"Failed after {} attempts for {}",
|
||||
MAX_DOWNLOAD_RETRIES, archive.file_name
|
||||
)));
|
||||
}
|
||||
|
||||
eyre::bail!(
|
||||
"Failed integrity validation after {} attempts for {}",
|
||||
MAX_DOWNLOAD_RETRIES,
|
||||
@@ -1665,11 +1608,10 @@ fn file_blake3_hex(path: &Path) -> Result<String> {
|
||||
|
||||
/// 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
|
||||
/// Queries `snapshots.reth.rs/api/snapshots` and returns the manifest URL for the most
|
||||
/// recent modular snapshot matching the requested chain.
|
||||
async fn discover_manifest_url(chain_id: u64) -> Result<String> {
|
||||
let defaults = DownloadDefaults::get_global();
|
||||
let api_url = &*defaults.snapshot_api_url;
|
||||
let api_url = RETH_SNAPSHOTS_API_URL;
|
||||
|
||||
info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
|
||||
|
||||
@@ -1682,9 +1624,8 @@ async fn discover_manifest_url(chain_id: u64) -> Result<String> {
|
||||
{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"),
|
||||
\t- https://snapshots.reth.rs\n\n\
|
||||
Use --list to see all available snapshots."
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -1715,7 +1656,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry from the snapshot discovery API listing.
|
||||
/// An entry from the `snapshots.reth.rs/api/snapshots` listing.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SnapshotApiEntry {
|
||||
@@ -1740,7 +1681,7 @@ impl SnapshotApiEntry {
|
||||
|
||||
/// Fetches the full snapshot listing from the snapshots API, filtered by chain ID.
|
||||
async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntry>> {
|
||||
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
|
||||
let api_url = RETH_SNAPSHOTS_API_URL;
|
||||
|
||||
let entries: Vec<SnapshotApiEntry> = Client::new()
|
||||
.get(api_url)
|
||||
@@ -1758,11 +1699,7 @@ async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntr
|
||||
fn print_snapshot_listing(entries: &[SnapshotApiEntry], chain_id: u64) {
|
||||
let modular: Vec<_> = entries.iter().filter(|e| e.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!("Available snapshots for chain {chain_id} (https://snapshots.reth.rs):\n");
|
||||
println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
|
||||
println!("{}", "-".repeat(100));
|
||||
|
||||
@@ -1801,18 +1738,14 @@ async fn fetch_manifest_from_source(source: &str) -> Result<SnapshotManifest> {
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.wrap_err_with(|| {
|
||||
let sources = DownloadDefaults::get_global()
|
||||
.available_snapshots
|
||||
.iter()
|
||||
.map(|s| format!("\t- {s}"))
|
||||
.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}"
|
||||
Available snapshot sources:\n\
|
||||
\t- https://snapshots.reth.rs\n\
|
||||
\t- https://publicnode.com/snapshots"
|
||||
)
|
||||
})?;
|
||||
Ok(response.json().await?)
|
||||
|
||||
@@ -178,8 +178,6 @@ where
|
||||
ext,
|
||||
} = self;
|
||||
|
||||
engine.validate()?;
|
||||
|
||||
// set up node config
|
||||
let mut node_config = NodeConfig {
|
||||
datadir,
|
||||
|
||||
@@ -6,7 +6,7 @@ use reth_db_api::{
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_node_api::HeaderTy;
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_node_core::dirs::{ChainPath, DataDirPath};
|
||||
use reth_provider::{
|
||||
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
|
||||
@@ -58,7 +58,7 @@ where
|
||||
}
|
||||
|
||||
/// Imports all the tables that can be copied over a range.
|
||||
fn import_tables_with_range<N: ProviderNodeTypes>(
|
||||
fn import_tables_with_range<N: NodeTypesWithDB>(
|
||||
output_db: &DatabaseEnv,
|
||||
db_tool: &DbTool<N>,
|
||||
from: u64,
|
||||
@@ -74,7 +74,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
|
||||
)
|
||||
})??;
|
||||
output_db.update(|tx| {
|
||||
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
|
||||
tx.import_table_with_range::<tables::Headers, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from),
|
||||
to,
|
||||
|
||||
@@ -10,7 +10,6 @@ use reth_db_api::{database::Database, models::BlockNumberAddress, table::TableIm
|
||||
use reth_db_common::DbTool;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_exex::ExExManagerHandle;
|
||||
use reth_node_api::HeaderTy;
|
||||
use reth_node_core::dirs::{ChainPath, DataDirPath};
|
||||
use reth_provider::{
|
||||
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
|
||||
@@ -42,7 +41,7 @@ where
|
||||
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
|
||||
|
||||
output_db.update(|tx| {
|
||||
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
|
||||
tx.import_table_with_range::<tables::Headers, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from),
|
||||
to,
|
||||
|
||||
@@ -28,7 +28,6 @@ use reth_node_metrics::{
|
||||
server::{MetricServer, MetricServerConfig},
|
||||
version::VersionInfo,
|
||||
};
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use reth_provider::{
|
||||
ChainSpecProvider, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
|
||||
StageCheckpointWriter,
|
||||
@@ -41,7 +40,7 @@ use reth_stages::{
|
||||
},
|
||||
ExecInput, ExecOutput, ExecutionStageThresholds, Stage, StageExt, UnwindInput, UnwindOutput,
|
||||
};
|
||||
use std::{any::Any, net::SocketAddr, sync::Arc};
|
||||
use std::{any::Any, net::SocketAddr, sync::Arc, time::Instant};
|
||||
use tokio::sync::watch;
|
||||
use tracing::*;
|
||||
|
||||
|
||||
@@ -33,21 +33,19 @@ reth-tracing = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
tikv-jemalloc-sys = { workspace = true, optional = true }
|
||||
tikv-jemallocator = { workspace = true, optional = true }
|
||||
snmalloc-rs = { workspace = true, optional = true }
|
||||
libc = "0.2"
|
||||
|
||||
[features]
|
||||
jemalloc = [
|
||||
"dep:tikv-jemallocator",
|
||||
"dep:tikv-jemalloc-sys",
|
||||
"tikv-jemallocator?/override_allocator_on_supported_platforms",
|
||||
]
|
||||
jemalloc = ["dep:tikv-jemallocator"]
|
||||
|
||||
# Enables jemalloc profiling features
|
||||
jemalloc-prof = ["jemalloc", "tikv-jemallocator?/profiling"]
|
||||
|
||||
# Enables unprefixed malloc (reproducible builds support)
|
||||
jemalloc-unprefixed = ["jemalloc", "tikv-jemallocator?/unprefixed_malloc_on_supported_platforms"]
|
||||
|
||||
# Wraps the selected allocator in the tracy profiling allocator
|
||||
tracy-allocator = ["dep:tracy-client", "dep:reth-tracing"]
|
||||
|
||||
|
||||
@@ -15,11 +15,6 @@ cfg_if::cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export jemalloc-sys so that binaries can `use` it in main.rs to make it
|
||||
// visible to the linker, which is required for `override_allocator_on_supported_platforms`.
|
||||
#[cfg(all(feature = "jemalloc", unix))]
|
||||
pub use tikv_jemalloc_sys;
|
||||
|
||||
// This is to prevent clippy unused warnings when we do `--all-features`
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(feature = "snmalloc", feature = "jemalloc", unix))] {
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
#[cfg(feature = "tracy-allocator")]
|
||||
use reth_tracing as _;
|
||||
#[cfg(feature = "tracy-allocator")]
|
||||
use tracy_client as _;
|
||||
|
||||
pub mod allocator;
|
||||
pub mod cancellation;
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
//! Consensus protocol functions
|
||||
//!
|
||||
//! # Trait hierarchy
|
||||
//!
|
||||
//! Consensus validation is split across three traits, each adding a layer:
|
||||
//!
|
||||
//! - [`HeaderValidator`] — validates a header in isolation and against its parent. Used early in
|
||||
//! the validation pipeline before block execution.
|
||||
//!
|
||||
//! - [`Consensus`] — extends `HeaderValidator` with block body validation. Checks that the body
|
||||
//! matches the header (tx root, ommer hash, withdrawals) and runs pre-execution checks. Used
|
||||
//! before a block is executed.
|
||||
//!
|
||||
//! - [`FullConsensus`] — extends `Consensus` with post-execution validation. Checks execution
|
||||
//! results against the header (gas used, receipt root, logs bloom). Used after block execution to
|
||||
//! verify the outcome.
|
||||
//!
|
||||
//! In the engine, these are applied in order during payload validation (`engine_newPayload`).
|
||||
//! Payload attribute validation for block building (`engine_forkchoiceUpdated`) is handled
|
||||
//! separately at the engine API layer and does not use these traits.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
|
||||
@@ -29,7 +29,7 @@ auto_impl.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
eyre.workspace = true
|
||||
reqwest = { workspace = true, features = ["query"] }
|
||||
reqwest.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use alloy_consensus::Sealable;
|
||||
use alloy_primitives::B256;
|
||||
use reth_node_api::{
|
||||
BuiltPayload, ConsensusEngineHandle, ExecutionPayload, NodePrimitives, PayloadTypes,
|
||||
BuiltPayload, ConsensusEngineHandle, EngineApiMessageVersion, ExecutionPayload, NodePrimitives,
|
||||
PayloadTypes,
|
||||
};
|
||||
use reth_primitives_traits::{Block, SealedBlock};
|
||||
use reth_tracing::tracing::warn;
|
||||
@@ -130,7 +131,10 @@ where
|
||||
safe_block_hash,
|
||||
finalized_block_hash,
|
||||
};
|
||||
let _ = self.engine_handle.fork_choice_updated(state, None).await;
|
||||
let _ = self
|
||||
.engine_handle
|
||||
.fork_choice_updated(state, None, EngineApiMessageVersion::V3)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,16 +79,10 @@ where
|
||||
target: "consensus::debug-client",
|
||||
%err,
|
||||
url=%self.url,
|
||||
"Failed to subscribe to blocks, retrying in 5s",
|
||||
"Failed to subscribe to blocks",
|
||||
);
|
||||
}) else {
|
||||
// Exit if the receiver has been dropped (e.g. during shutdown) so we
|
||||
// don't keep retrying after the consumer is gone.
|
||||
if tx.is_closed() {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
continue
|
||||
return
|
||||
};
|
||||
|
||||
while let Some(res) = stream.next().await {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Utilities for end-to-end tests.
|
||||
|
||||
use alloy_rpc_types_engine::PayloadAttributes;
|
||||
use node::NodeTestContext;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_db::{test_utils::TempDatabase, DatabaseEnv};
|
||||
@@ -49,11 +48,7 @@ pub async fn setup<N>(
|
||||
num_nodes: usize,
|
||||
chain_spec: Arc<N::ChainSpec>,
|
||||
is_dev: bool,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)>
|
||||
where
|
||||
N: NodeBuilderHelper,
|
||||
@@ -70,11 +65,7 @@ pub async fn setup_engine<N>(
|
||||
chain_spec: Arc<N::ChainSpec>,
|
||||
is_dev: bool,
|
||||
tree_config: reth_node_api::TreeConfig,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
) -> eyre::Result<(
|
||||
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
|
||||
Wallet,
|
||||
@@ -99,11 +90,7 @@ pub async fn setup_engine_with_connection<N>(
|
||||
chain_spec: Arc<N::ChainSpec>,
|
||||
is_dev: bool,
|
||||
tree_config: reth_node_api::TreeConfig,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
connect_nodes: bool,
|
||||
) -> eyre::Result<(
|
||||
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
|
||||
@@ -146,8 +133,11 @@ pub type NodeHelperType<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<
|
||||
pub trait NodeBuilderHelper
|
||||
where
|
||||
Self: Default
|
||||
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
|
||||
+ Node<
|
||||
+ NodeTypesForProvider<
|
||||
Payload: PayloadTypes<
|
||||
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
|
||||
>,
|
||||
> + Node<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
ComponentsBuilder: NodeComponentsBuilder<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
@@ -168,8 +158,11 @@ where
|
||||
|
||||
impl<T> NodeBuilderHelper for T where
|
||||
Self: Default
|
||||
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
|
||||
+ Node<
|
||||
+ NodeTypesForProvider<
|
||||
Payload: PayloadTypes<
|
||||
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
|
||||
>,
|
||||
> + Node<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
ComponentsBuilder: NodeComponentsBuilder<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
|
||||
@@ -9,10 +9,13 @@ use futures_util::Future;
|
||||
use jsonrpsee::{core::client::ClientT, http_client::HttpClient};
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_network_api::test_utils::PeersHandleProvider;
|
||||
use reth_node_api::{Block, BlockBody, BlockTy, FullNodeComponents, PayloadTypes, PrimitivesTy};
|
||||
use reth_node_api::{
|
||||
Block, BlockBody, BlockTy, EngineApiMessageVersion, FullNodeComponents, PayloadTypes,
|
||||
PrimitivesTy,
|
||||
};
|
||||
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
|
||||
|
||||
use reth_payload_primitives::BuiltPayload;
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
|
||||
use reth_provider::{
|
||||
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
|
||||
HeaderProvider, StageCheckpointReader,
|
||||
@@ -55,7 +58,7 @@ where
|
||||
/// Creates a new test node
|
||||
pub async fn new(
|
||||
node: FullNode<Node, AddOns>,
|
||||
attributes_generator: impl Fn(u64) -> Payload::PayloadAttributes + Send + Sync + 'static,
|
||||
attributes_generator: impl Fn(u64) -> Payload::PayloadBuilderAttributes + Send + Sync + 'static,
|
||||
) -> eyre::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: node.clone(),
|
||||
@@ -103,50 +106,17 @@ where
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Returns the current forkchoice state of the node.
|
||||
pub fn current_forkchoice_state(&self) -> eyre::Result<ForkchoiceState> {
|
||||
let latest_header =
|
||||
self.inner.provider.sealed_header_by_number_or_tag(BlockNumberOrTag::Latest)?.unwrap();
|
||||
|
||||
if latest_header.number() == 0 {
|
||||
return Ok(ForkchoiceState::same_hash(latest_header.hash()));
|
||||
}
|
||||
|
||||
Ok(ForkchoiceState {
|
||||
head_block_hash: latest_header.hash(),
|
||||
safe_block_hash: self
|
||||
.inner
|
||||
.provider
|
||||
.sealed_header_by_number_or_tag(BlockNumberOrTag::Safe)?
|
||||
.unwrap()
|
||||
.hash(),
|
||||
finalized_block_hash: self
|
||||
.inner
|
||||
.provider
|
||||
.sealed_header_by_number_or_tag(BlockNumberOrTag::Finalized)?
|
||||
.unwrap()
|
||||
.hash(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new payload from given attributes generator
|
||||
/// expects a payload attribute event and waits until the payload is built.
|
||||
///
|
||||
/// It triggers the resolve payload via engine api and expects the built payload event.
|
||||
pub async fn new_payload(&mut self) -> eyre::Result<Payload::BuiltPayload> {
|
||||
let eth_attr = self.payload.next_attributes();
|
||||
let payload_id = self
|
||||
.inner
|
||||
.add_ons_handle
|
||||
.beacon_engine_handle
|
||||
.fork_choice_updated(self.current_forkchoice_state()?, Some(eth_attr.clone()))
|
||||
.await?
|
||||
.payload_id
|
||||
.unwrap();
|
||||
// trigger new payload building draining the pool
|
||||
let eth_attr = self.payload.new_payload().await.unwrap();
|
||||
// first event is the payload attributes
|
||||
self.payload.expect_attr_event(eth_attr).await?;
|
||||
self.payload.expect_attr_event(eth_attr.clone()).await?;
|
||||
// wait for the payload builder to have finished building
|
||||
self.payload.wait_for_built_payload(payload_id).await;
|
||||
self.payload.wait_for_built_payload(eth_attr.payload_id()).await;
|
||||
// ensure we're also receiving the built payload as event
|
||||
Ok(self.payload.expect_built_payload().await?)
|
||||
}
|
||||
@@ -295,6 +265,7 @@ where
|
||||
finalized_block_hash: current_head,
|
||||
},
|
||||
None,
|
||||
EngineApiMessageVersion::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use futures_util::StreamExt;
|
||||
use reth_node_api::{BlockBody, PayloadAttributes, PayloadKind};
|
||||
use reth_node_api::{BlockBody, PayloadKind};
|
||||
use reth_payload_builder::{PayloadBuilderHandle, PayloadId};
|
||||
use reth_payload_builder_primitives::Events;
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, PayloadTypes};
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
|
||||
/// Helper for payload operations
|
||||
@@ -12,14 +12,14 @@ pub struct PayloadTestContext<T: PayloadTypes> {
|
||||
payload_builder: PayloadBuilderHandle<T>,
|
||||
pub timestamp: u64,
|
||||
#[debug(skip)]
|
||||
attributes_generator: Box<dyn Fn(u64) -> T::PayloadAttributes + Send + Sync>,
|
||||
attributes_generator: Box<dyn Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T: PayloadTypes> PayloadTestContext<T> {
|
||||
/// Creates a new payload helper
|
||||
pub async fn new(
|
||||
payload_builder: PayloadBuilderHandle<T>,
|
||||
attributes_generator: impl Fn(u64) -> T::PayloadAttributes + Send + Sync + 'static,
|
||||
attributes_generator: impl Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync + 'static,
|
||||
) -> eyre::Result<Self> {
|
||||
let payload_events = payload_builder.subscribe().await?;
|
||||
let payload_event_stream = payload_events.into_stream();
|
||||
@@ -32,14 +32,19 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates the next payload attributes
|
||||
pub fn next_attributes(&mut self) -> T::PayloadAttributes {
|
||||
/// Creates a new payload job from static attributes
|
||||
pub async fn new_payload(&mut self) -> eyre::Result<T::PayloadBuilderAttributes> {
|
||||
self.timestamp += 1;
|
||||
(self.attributes_generator)(self.timestamp)
|
||||
let attributes = (self.attributes_generator)(self.timestamp);
|
||||
self.payload_builder.send_new_payload(attributes.clone()).await.unwrap()?;
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Asserts that the next event is a payload attributes event
|
||||
pub async fn expect_attr_event(&mut self, attrs: T::PayloadAttributes) -> eyre::Result<()> {
|
||||
pub async fn expect_attr_event(
|
||||
&mut self,
|
||||
attrs: T::PayloadBuilderAttributes,
|
||||
) -> eyre::Result<()> {
|
||||
let first_event = self.payload_event_stream.next().await.unwrap()?;
|
||||
if let Events::Attributes(attr) = first_event {
|
||||
assert_eq!(attrs.timestamp(), attr.timestamp());
|
||||
|
||||
@@ -33,7 +33,7 @@ type NodeConfigModifier<C> = Box<dyn Fn(NodeConfig<C>) -> NodeConfig<C> + Send +
|
||||
pub struct E2ETestSetupBuilder<N, F>
|
||||
where
|
||||
N: NodeBuilderHelper,
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
@@ -50,7 +50,7 @@ where
|
||||
impl<N, F> E2ETestSetupBuilder<N, F>
|
||||
where
|
||||
N: NodeBuilderHelper,
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
@@ -207,7 +207,7 @@ where
|
||||
impl<N, F> std::fmt::Debug for E2ETestSetupBuilder<N, F>
|
||||
where
|
||||
N: NodeBuilderHelper,
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Setup utilities for importing RLP chain data before starting nodes.
|
||||
|
||||
use crate::{node::NodeTestContext, NodeHelperType, Wallet};
|
||||
use alloy_rpc_types_engine::PayloadAttributes;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_cli_commands::import_core::{import_blocks_from_file, ImportConfig};
|
||||
use reth_config::Config;
|
||||
@@ -60,7 +59,11 @@ pub async fn setup_engine_with_chain_import(
|
||||
is_dev: bool,
|
||||
tree_config: TreeConfig,
|
||||
rlp_path: &Path,
|
||||
attributes_generator: impl Fn(u64) -> PayloadAttributes + Send + Sync + Copy + 'static,
|
||||
attributes_generator: impl Fn(u64) -> reth_payload_builder::EthPayloadBuilderAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
) -> eyre::Result<ChainImportResult> {
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
|
||||
@@ -270,10 +273,10 @@ pub fn load_forkchoice_state(path: &Path) -> eyre::Result<alloy_rpc_types_engine
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp};
|
||||
use alloy_rpc_types_engine::PayloadAttributes;
|
||||
use reth_chainspec::{ChainSpecBuilder, MAINNET};
|
||||
use reth_db::mdbx::DatabaseArguments;
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_payload_builder::EthPayloadBuilderAttributes;
|
||||
use reth_primitives_traits::SealedBlock;
|
||||
use reth_provider::{
|
||||
test_utils::MockNodeTypesWithDB, BlockHashReader, BlockNumReader, BlockReaderIdExt,
|
||||
@@ -566,7 +569,7 @@ mod tests {
|
||||
false,
|
||||
TreeConfig::default(),
|
||||
&rlp_path,
|
||||
|_| PayloadAttributes::default(),
|
||||
|_| EthPayloadBuilderAttributes::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to setup nodes with chain import");
|
||||
|
||||
@@ -10,6 +10,7 @@ use reth_ethereum_primitives::Block;
|
||||
use reth_network_p2p::sync::{NetworkSyncUpdater, SyncState};
|
||||
use reth_node_api::{EngineTypes, NodeTypes, PayloadTypes, TreeConfig};
|
||||
use reth_node_core::primitives::RecoveredBlock;
|
||||
use reth_payload_builder::EthPayloadBuilderAttributes;
|
||||
use revm::state::EvmState;
|
||||
use std::{marker::PhantomData, path::Path, sync::Arc};
|
||||
use tokio::{
|
||||
@@ -263,12 +264,15 @@ where
|
||||
let chain_spec =
|
||||
self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
|
||||
|
||||
let attributes_generator = move |timestamp| PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
let attributes_generator = move |timestamp| {
|
||||
let attributes = PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
};
|
||||
|
||||
crate::setup_import::setup_engine_with_chain_import(
|
||||
@@ -284,19 +288,23 @@ where
|
||||
|
||||
/// Create a static attributes generator that doesn't capture any instance data
|
||||
fn create_static_attributes_generator<N>(
|
||||
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes + Copy + use<N, I>
|
||||
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
|
||||
+ Copy
|
||||
+ use<N, I>
|
||||
where
|
||||
N: NodeBuilderHelper<Payload = I>,
|
||||
{
|
||||
move |timestamp| {
|
||||
PayloadAttributes {
|
||||
let attributes = PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
}
|
||||
.into()
|
||||
};
|
||||
<<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes::from(
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use reth_e2e_test_utils::{
|
||||
};
|
||||
use reth_node_api::TreeConfig;
|
||||
use reth_node_ethereum::{EthEngineTypes, EthereumNode};
|
||||
use reth_payload_builder::EthPayloadBuilderAttributes;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tracing::debug;
|
||||
@@ -370,7 +371,7 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
|
||||
);
|
||||
|
||||
let (nodes, _wallet) = E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
|
||||
PayloadAttributes::default()
|
||||
EthPayloadBuilderAttributes::default()
|
||||
})
|
||||
.with_tree_config_modifier(|config| {
|
||||
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
//! E2E tests for `RocksDB` provider functionality.
|
||||
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_eips::eip2718::Encodable2718;
|
||||
use alloy_primitives::{Address, Bytes, TxKind, B256, U256};
|
||||
use alloy_rpc_types_engine::PayloadAttributes;
|
||||
use alloy_rpc_types_eth::{Transaction, TransactionInput, TransactionReceipt, TransactionRequest};
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rpc_types_eth::{Transaction, TransactionReceipt};
|
||||
use eyre::Result;
|
||||
use jsonrpsee::core::client::ClientT;
|
||||
use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
|
||||
use reth_db::tables;
|
||||
use reth_e2e_test_utils::{transaction::TransactionTestContext, wallet, E2ETestSetupBuilder};
|
||||
use reth_node_ethereum::EthereumNode;
|
||||
use reth_payload_builder::EthPayloadBuilderAttributes;
|
||||
use reth_provider::RocksDBProviderFactory;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
@@ -83,14 +82,15 @@ fn test_chain_spec() -> Arc<ChainSpec> {
|
||||
}
|
||||
|
||||
/// Returns test payload attributes for the given timestamp.
|
||||
const fn test_attributes_generator(timestamp: u64) -> PayloadAttributes {
|
||||
PayloadAttributes {
|
||||
fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
|
||||
let attributes = alloy_rpc_types_engine::PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
}
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
}
|
||||
|
||||
/// Smoke test: node boots with `RocksDB` routing enabled.
|
||||
@@ -573,426 +573,3 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Historical account queries: verifies that `eth_getBalance` and `eth_getTransactionCount`
|
||||
/// return correct values at past block numbers after the account state has changed.
|
||||
///
|
||||
/// This test exercises the database-backed historical state lookup path. After mining the
|
||||
/// blocks we care about (1-3), we mine additional blocks to advance the canonical head
|
||||
/// far enough that the engine tree's persistence + in-memory eviction cycle guarantees
|
||||
/// blocks 1-3 are no longer in the in-memory overlay. Historical queries for those blocks
|
||||
/// must then be served from `RocksDB` changesets.
|
||||
#[tokio::test]
|
||||
async fn test_rocksdb_historical_account_queries() -> Result<()> {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let chain_spec = test_chain_spec();
|
||||
let chain_id = chain_spec.chain().id();
|
||||
|
||||
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
|
||||
1,
|
||||
chain_spec.clone(),
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(nodes.len(), 1);
|
||||
|
||||
let wallets = wallet::Wallet::new(1).with_chain_id(chain_id).wallet_gen();
|
||||
let signer = wallets[0].clone();
|
||||
let sender: Address = signer.address();
|
||||
let client = nodes[0].rpc_client().expect("RPC client");
|
||||
|
||||
// Query the sender's balance and nonce at genesis (block 0)
|
||||
let genesis_balance: U256 = client.request("eth_getBalance", (sender, "0x0")).await?;
|
||||
let genesis_nonce: U256 = client.request("eth_getTransactionCount", (sender, "0x0")).await?;
|
||||
assert!(genesis_balance > U256::ZERO, "Sender should have genesis balance");
|
||||
assert_eq!(genesis_nonce, U256::ZERO, "Sender nonce should be 0 at genesis");
|
||||
|
||||
// Mine block 1 with a transfer (nonce 0)
|
||||
let raw_tx1 =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 0).await;
|
||||
let tx_hash1 = nodes[0].rpc.inject_tx(raw_tx1).await?;
|
||||
wait_for_pending_tx(&client, tx_hash1).await;
|
||||
|
||||
let payload1 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload1.block().number(), 1);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash1).await;
|
||||
|
||||
// Record state after block 1
|
||||
let balance_at_1: U256 = client.request("eth_getBalance", (sender, "0x1")).await?;
|
||||
let nonce_at_1: U256 = client.request("eth_getTransactionCount", (sender, "0x1")).await?;
|
||||
assert!(balance_at_1 < genesis_balance, "Balance should decrease after transfer + gas");
|
||||
assert_eq!(nonce_at_1, U256::from(1), "Nonce should be 1 after first tx");
|
||||
|
||||
// Mine block 2 with another transfer (nonce 1)
|
||||
let raw_tx2 =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 1).await;
|
||||
let tx_hash2 = nodes[0].rpc.inject_tx(raw_tx2).await?;
|
||||
wait_for_pending_tx(&client, tx_hash2).await;
|
||||
|
||||
let payload2 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload2.block().number(), 2);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash2).await;
|
||||
|
||||
let balance_at_2: U256 = client.request("eth_getBalance", (sender, "0x2")).await?;
|
||||
let nonce_at_2: U256 = client.request("eth_getTransactionCount", (sender, "0x2")).await?;
|
||||
assert!(balance_at_2 < balance_at_1, "Balance should decrease further after second tx");
|
||||
assert_eq!(nonce_at_2, U256::from(2), "Nonce should be 2 after second tx");
|
||||
|
||||
// Mine block 3 with a third transfer (nonce 2)
|
||||
let raw_tx3 =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), 2).await;
|
||||
let tx_hash3 = nodes[0].rpc.inject_tx(raw_tx3).await?;
|
||||
wait_for_pending_tx(&client, tx_hash3).await;
|
||||
|
||||
let payload3 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload3.block().number(), 3);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, tx_hash3).await;
|
||||
|
||||
let balance_at_3: U256 = client.request("eth_getBalance", (sender, "0x3")).await?;
|
||||
let nonce_at_3: U256 = client.request("eth_getTransactionCount", (sender, "0x3")).await?;
|
||||
assert!(balance_at_3 < balance_at_2, "Balance should decrease further after third tx");
|
||||
assert_eq!(nonce_at_3, U256::from(3), "Nonce should be 3 after third tx");
|
||||
|
||||
// Mine additional blocks to push blocks 1-3 out of the in-memory overlay.
|
||||
// With persistence_threshold=0 and memory_block_buffer_target=0, each new block
|
||||
// triggers persistence up to `head` followed by in-memory eviction. Mining several
|
||||
// more blocks ensures the engine loop has completed at least one full
|
||||
// persist-then-evict cycle covering blocks 1-3.
|
||||
// Each block needs a transaction because the payload builder requires non-empty payloads.
|
||||
for nonce in 3..8u64 {
|
||||
let raw_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), nonce)
|
||||
.await;
|
||||
let tx_hash = nodes[0].rpc.inject_tx(raw_tx).await?;
|
||||
wait_for_pending_tx(&client, tx_hash).await;
|
||||
nodes[0].advance_block().await?;
|
||||
}
|
||||
// Allow the engine loop to process the persistence completions
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Historical queries — blocks 1-3 should now be served from the database.
|
||||
let hist_balance_0: U256 = client.request("eth_getBalance", (sender, "0x0")).await?;
|
||||
let hist_nonce_0: U256 = client.request("eth_getTransactionCount", (sender, "0x0")).await?;
|
||||
assert_eq!(
|
||||
hist_balance_0, genesis_balance,
|
||||
"Historical balance at block 0 should match genesis"
|
||||
);
|
||||
assert_eq!(hist_nonce_0, U256::ZERO, "Historical nonce at block 0 should be 0");
|
||||
|
||||
let hist_balance_1: U256 = client.request("eth_getBalance", (sender, "0x1")).await?;
|
||||
let hist_nonce_1: U256 = client.request("eth_getTransactionCount", (sender, "0x1")).await?;
|
||||
assert_eq!(hist_balance_1, balance_at_1, "Historical balance at block 1 should match");
|
||||
assert_eq!(hist_nonce_1, U256::from(1), "Historical nonce at block 1 should be 1");
|
||||
|
||||
let hist_balance_2: U256 = client.request("eth_getBalance", (sender, "0x2")).await?;
|
||||
let hist_nonce_2: U256 = client.request("eth_getTransactionCount", (sender, "0x2")).await?;
|
||||
assert_eq!(hist_balance_2, balance_at_2, "Historical balance at block 2 should match");
|
||||
assert_eq!(hist_nonce_2, U256::from(2), "Historical nonce at block 2 should be 2");
|
||||
|
||||
let hist_balance_3: U256 = client.request("eth_getBalance", (sender, "0x3")).await?;
|
||||
let hist_nonce_3: U256 = client.request("eth_getTransactionCount", (sender, "0x3")).await?;
|
||||
assert_eq!(hist_balance_3, balance_at_3, "Historical balance at block 3 should match");
|
||||
assert_eq!(hist_nonce_3, U256::from(3), "Historical nonce at block 3 should be 3");
|
||||
|
||||
// "latest" should still match head
|
||||
let latest_balance: U256 = client.request("eth_getBalance", (sender, "latest")).await?;
|
||||
let latest_nonce: U256 = client.request("eth_getTransactionCount", (sender, "latest")).await?;
|
||||
assert_eq!(
|
||||
latest_nonce,
|
||||
U256::from(8),
|
||||
"Latest nonce should be 8 (3 original + 5 extra blocks)"
|
||||
);
|
||||
assert!(latest_balance < balance_at_3, "Latest balance should be less than block 3 balance");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reproduces the race condition between `save_blocks` and `RocksDB` pruning described in
|
||||
/// <https://github.com/paradigmxyz/reth/pull/23081>.
|
||||
///
|
||||
/// Both `save_blocks` and the pruner push to `pending_rocksdb_batches` before a single
|
||||
/// `commit()`. The pruner reads committed (stale) state that doesn't include `save_blocks`'
|
||||
/// new entry, filters it, and pushes its own batch. On commit the pruner's batch overwrites
|
||||
/// `save_blocks`' batch for the same `ShardedKey(addr, u64::MAX)`. Every cycle the new
|
||||
/// block's history entry is silently lost. After enough cycles the shard is completely empty.
|
||||
///
|
||||
/// This test mines blocks with account-history pruning (`block_interval=1`,
|
||||
/// `minimum_distance=5`), waits for persistence, then reads the `AccountsHistory` shard
|
||||
/// directly from `RocksDB`. Without the fix the shard is empty — all entries were
|
||||
/// overwritten by the pruner's stale batches. With the fix, entries for the most recent
|
||||
/// blocks survive.
|
||||
#[tokio::test]
|
||||
async fn test_rocksdb_account_history_pruning() -> Result<()> {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let chain_spec = test_chain_spec();
|
||||
let chain_id = chain_spec.chain().id();
|
||||
|
||||
const PRUNE_DISTANCE: u64 = 5;
|
||||
const TOTAL_BLOCKS: u64 = 20;
|
||||
|
||||
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
|
||||
1,
|
||||
chain_spec.clone(),
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_node_config_modifier(|mut config| {
|
||||
config.pruning.account_history_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.minimum_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.block_interval = Some(1);
|
||||
config
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(nodes.len(), 1);
|
||||
|
||||
let wallets = wallet::Wallet::new(1).with_chain_id(chain_id).wallet_gen();
|
||||
let signer = wallets[0].clone();
|
||||
let sender: Address = signer.address();
|
||||
let client = nodes[0].rpc_client().expect("RPC client");
|
||||
|
||||
// Mine blocks one at a time with a delay so each save_blocks + pruner cycle
|
||||
// completes independently. The race fires every cycle (the pruner reads stale
|
||||
// committed state that doesn't include save_blocks' pending batch), but processing
|
||||
// one block at a time makes the outcome deterministic.
|
||||
let mut last_tx_hash = B256::ZERO;
|
||||
for nonce in 0..TOTAL_BLOCKS {
|
||||
let raw_tx =
|
||||
TransactionTestContext::transfer_tx_bytes_with_nonce(chain_id, signer.clone(), nonce)
|
||||
.await;
|
||||
let tx_hash = nodes[0].rpc.inject_tx(raw_tx).await?;
|
||||
wait_for_pending_tx(&client, tx_hash).await;
|
||||
|
||||
let payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload.block().number(), nonce + 1);
|
||||
last_tx_hash = tx_hash;
|
||||
|
||||
// Let the persistence cycle (save_blocks → pruner → commit) complete before
|
||||
// producing the next block, so each cycle processes exactly one block.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
}
|
||||
|
||||
// Wait for the last block to be fully persisted to RocksDB.
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, last_tx_hash).await;
|
||||
|
||||
// Read the AccountsHistory shard for `sender` directly from RocksDB.
|
||||
// This is the data structure corrupted by the race.
|
||||
let rocksdb = nodes[0].inner.provider.rocksdb_provider();
|
||||
let shards = rocksdb.account_history_shards(sender).unwrap();
|
||||
let all_entries: Vec<u64> = shards.iter().flat_map(|(_, list)| list.iter()).collect();
|
||||
|
||||
// The sender has a transfer in every block, so the shard should contain an entry
|
||||
// for every block in the retention window: (TOTAL_BLOCKS - PRUNE_DISTANCE, TOTAL_BLOCKS].
|
||||
//
|
||||
// Without the fix: the pruner reads stale committed state each cycle, overwrites
|
||||
// save_blocks' entry, and only the very last block survives (no subsequent pruner
|
||||
// cycle to overwrite it). The shard ends up as just [TOTAL_BLOCKS].
|
||||
//
|
||||
// With the fix: save_blocks' batch is committed before the pruner reads, so the
|
||||
// pruner sees the new entry and preserves it. All retained blocks are present.
|
||||
let expected: Vec<u64> = ((TOTAL_BLOCKS - PRUNE_DISTANCE + 1)..=TOTAL_BLOCKS).collect();
|
||||
assert_eq!(
|
||||
all_entries, expected,
|
||||
"AccountsHistory shard for sender doesn't match expected retention window. \
|
||||
Expected {expected:?}, got {all_entries:?}. \
|
||||
The pruner's stale batch overwrote save_blocks' entries \
|
||||
(save_blocks/pruner race, see PR #23081)."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mirrors [`test_rocksdb_account_history_pruning`] for the `StoragesHistory` table.
|
||||
///
|
||||
/// The same race condition between `save_blocks` and the pruner that affects
|
||||
/// `AccountsHistory` also affects `StoragesHistory`:
|
||||
/// - `write_storage_history` reads committed `RocksDB` state and pushes a batch.
|
||||
/// - `prune_storage_history_batch` also reads stale committed state and pushes its own batch.
|
||||
/// - On a single `commit()`, the pruner's batch overwrites `save_blocks`' batch for the same
|
||||
/// `StorageShardedKey(addr, slot, u64::MAX)`.
|
||||
///
|
||||
/// This test deploys a minimal contract that writes to storage slot 0 each block,
|
||||
/// then verifies that `StoragesHistory` contains entries for the full retention
|
||||
/// window. Without the fix the shard would be truncated — new entries silently
|
||||
/// lost every cycle.
|
||||
#[tokio::test]
|
||||
async fn test_rocksdb_storage_history_pruning() -> Result<()> {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let chain_spec = test_chain_spec();
|
||||
let chain_id = chain_spec.chain().id();
|
||||
|
||||
const PRUNE_DISTANCE: u64 = 5;
|
||||
const TOTAL_BLOCKS: u64 = 20;
|
||||
|
||||
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
|
||||
1,
|
||||
chain_spec.clone(),
|
||||
test_attributes_generator,
|
||||
)
|
||||
.with_storage_v2()
|
||||
.with_tree_config_modifier(|config| config.with_persistence_threshold(0))
|
||||
.with_node_config_modifier(|mut config| {
|
||||
config.pruning.storage_history_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.minimum_distance = Some(PRUNE_DISTANCE);
|
||||
config.pruning.block_interval = Some(1);
|
||||
config
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(nodes.len(), 1);
|
||||
|
||||
let wallets = wallet::Wallet::new(1).with_chain_id(chain_id).wallet_gen();
|
||||
let signer = wallets[0].clone();
|
||||
let client = nodes[0].rpc_client().expect("RPC client");
|
||||
|
||||
// Deploy a minimal contract that stores CALLDATA[0..32] into slot 0:
|
||||
// PUSH0 ; [0]
|
||||
// CALLDATALOAD ; [calldata[0..32]]
|
||||
// PUSH0 ; [0, calldata[0..32]]
|
||||
// SSTORE ; sstore(0, calldata[0..32])
|
||||
// STOP
|
||||
// Bytecode: 5f355f5500
|
||||
//
|
||||
// Init code that deploys this runtime:
|
||||
// PUSH5 5f355f5500 ; push 5-byte runtime
|
||||
// PUSH0 ; offset 0 in memory
|
||||
// MSTORE ; store at mem[0..32] (right-padded in 32 bytes)
|
||||
// PUSH1 0x05 ; size = 5
|
||||
// PUSH1 0x1b ; offset = 27 (32 - 5)
|
||||
// PUSH0 ; destOffset = 0
|
||||
// CODECOPY ; copy runtime to mem[0..5]
|
||||
// PUSH1 0x05 ; size = 5
|
||||
// PUSH0 ; offset = 0
|
||||
// RETURN ; return mem[0..5]
|
||||
//
|
||||
// We can simplify: just use PUSH + MSTORE + RETURN pattern.
|
||||
// Init code (hex):
|
||||
// 645f355f5500 PUSH5 runtime_bytecode
|
||||
// 5f PUSH0 (memory offset for MSTORE, stores at 27..32)
|
||||
// 52 MSTORE
|
||||
// 6005 PUSH1 5 (size)
|
||||
// 601b PUSH1 27 (offset = 32-5)
|
||||
// f3 RETURN
|
||||
let init_code = Bytes::from_static(&[
|
||||
0x64, 0x5f, 0x35, 0x5f, 0x55, 0x00, // PUSH5 runtime
|
||||
0x5f, // PUSH0
|
||||
0x52, // MSTORE
|
||||
0x60, 0x05, // PUSH1 5
|
||||
0x60, 0x1b, // PUSH1 27
|
||||
0xf3, // RETURN
|
||||
]);
|
||||
|
||||
// Deploy in block 1 (nonce 0)
|
||||
let deploy_tx = TransactionRequest {
|
||||
nonce: Some(0),
|
||||
value: Some(U256::ZERO),
|
||||
to: Some(TxKind::Create),
|
||||
gas: Some(100_000),
|
||||
max_fee_per_gas: Some(1000e9 as u128),
|
||||
max_priority_fee_per_gas: Some(20e9 as u128),
|
||||
chain_id: Some(chain_id),
|
||||
input: TransactionInput { input: None, data: Some(init_code) },
|
||||
..Default::default()
|
||||
};
|
||||
let signed_deploy = TransactionTestContext::sign_tx(signer.clone(), deploy_tx).await;
|
||||
let deploy_bytes: Bytes = signed_deploy.encoded_2718().into();
|
||||
let deploy_hash = nodes[0].rpc.inject_tx(deploy_bytes).await?;
|
||||
wait_for_pending_tx(&client, deploy_hash).await;
|
||||
|
||||
let payload1 = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload1.block().number(), 1);
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, deploy_hash).await;
|
||||
|
||||
// Let the persistence cycle complete before the next block (same cadence as the loop below)
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Get the deployed contract address from the receipt
|
||||
let receipt: Option<TransactionReceipt> =
|
||||
client.request("eth_getTransactionReceipt", [deploy_hash]).await?;
|
||||
let contract_address = receipt
|
||||
.expect("deploy receipt should exist")
|
||||
.contract_address
|
||||
.expect("deploy should create a contract");
|
||||
|
||||
// Sanity check: verify the runtime bytecode is what we expect
|
||||
let code: Bytes = client.request("eth_getCode", (contract_address, "latest")).await?;
|
||||
assert_eq!(
|
||||
code,
|
||||
Bytes::from_static(&[0x5f, 0x35, 0x5f, 0x55, 0x00]),
|
||||
"Deployed runtime should be PUSH0 CALLDATALOAD PUSH0 SSTORE STOP"
|
||||
);
|
||||
|
||||
// The storage slot we track: slot 0, encoded as B256
|
||||
let storage_slot = B256::ZERO;
|
||||
|
||||
// Mine TOTAL_BLOCKS - 1 more blocks (block 2..=TOTAL_BLOCKS), each calling the
|
||||
// contract to write a new value to slot 0. This creates a storage changeset entry
|
||||
// per block for (contract_address, slot 0).
|
||||
let mut last_tx_hash = deploy_hash;
|
||||
for nonce in 1..TOTAL_BLOCKS {
|
||||
// calldata = abi encode the block number so each write is unique
|
||||
let block_num = nonce + 1;
|
||||
let calldata = B256::from(U256::from(block_num));
|
||||
|
||||
let call_tx = TransactionRequest {
|
||||
nonce: Some(nonce),
|
||||
value: Some(U256::ZERO),
|
||||
to: Some(TxKind::Call(contract_address)),
|
||||
gas: Some(50_000),
|
||||
max_fee_per_gas: Some(1000e9 as u128),
|
||||
max_priority_fee_per_gas: Some(20e9 as u128),
|
||||
chain_id: Some(chain_id),
|
||||
input: TransactionInput { input: None, data: Some(Bytes::from(calldata.0)) },
|
||||
..Default::default()
|
||||
};
|
||||
let signed_call = TransactionTestContext::sign_tx(signer.clone(), call_tx).await;
|
||||
let call_bytes: Bytes = signed_call.encoded_2718().into();
|
||||
let tx_hash = nodes[0].rpc.inject_tx(call_bytes).await?;
|
||||
wait_for_pending_tx(&client, tx_hash).await;
|
||||
|
||||
let payload = nodes[0].advance_block().await?;
|
||||
assert_eq!(payload.block().number(), block_num);
|
||||
last_tx_hash = tx_hash;
|
||||
|
||||
// Let the persistence cycle complete before the next block
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
}
|
||||
|
||||
// Wait for the last block to be fully persisted to RocksDB
|
||||
poll_tx_in_rocksdb(&nodes[0].inner.provider, last_tx_hash).await;
|
||||
|
||||
// Read StoragesHistory shard for (contract_address, slot 0) directly from RocksDB
|
||||
let rocksdb = nodes[0].inner.provider.rocksdb_provider();
|
||||
let shards = rocksdb.storage_history_shards(contract_address, storage_slot).unwrap();
|
||||
let all_entries: Vec<u64> = shards.iter().flat_map(|(_, list)| list.iter()).collect();
|
||||
|
||||
// The contract has a storage write in blocks 2..=TOTAL_BLOCKS (the deploy in block 1
|
||||
// only executes init code — no SSTORE — so block 1 has no StoragesHistory entry for
|
||||
// slot 0). With pruning distance=5, the retention window should be
|
||||
// (TOTAL_BLOCKS - PRUNE_DISTANCE, TOTAL_BLOCKS] = blocks 16..=20.
|
||||
//
|
||||
// Without the fix: the pruner overwrites save_blocks' entries each cycle,
|
||||
// leaving only the very last block (or empty).
|
||||
//
|
||||
// With the fix: all blocks in the retention window are present.
|
||||
let expected: Vec<u64> = ((TOTAL_BLOCKS - PRUNE_DISTANCE + 1)..=TOTAL_BLOCKS).collect();
|
||||
assert_eq!(
|
||||
all_entries, expected,
|
||||
"StoragesHistory shard for contract slot 0 doesn't match expected retention window. \
|
||||
Expected {expected:?}, got {all_entries:?}. \
|
||||
The pruner's stale batch overwrote save_blocks' entries \
|
||||
(save_blocks/pruner race for StorageHistory, see PR #23081)."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "reth-execution-cache"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Cross-block execution cache for payload processing"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy-primitives.workspace = true
|
||||
fixed-cache = { workspace = true, features = ["stats"] }
|
||||
metrics.workspace = true
|
||||
parking_lot.workspace = true
|
||||
reth-errors.workspace = true
|
||||
reth-metrics = { workspace = true, features = ["common"] }
|
||||
reth-primitives-traits = { workspace = true, features = ["std"] }
|
||||
reth-provider.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-trie.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-primitives = { workspace = true, features = ["rand"] }
|
||||
reth-provider = { workspace = true, features = ["test-utils"] }
|
||||
reth-revm = { workspace = true, features = ["test-utils"] }
|
||||
revm-state.workspace = true
|
||||
|
||||
[features]
|
||||
test-utils = [
|
||||
"reth-primitives-traits/test-utils",
|
||||
"reth-revm/test-utils",
|
||||
"reth-provider/test-utils",
|
||||
"reth-trie/test-utils",
|
||||
]
|
||||
@@ -1,227 +0,0 @@
|
||||
//! Cross-block execution cache for payload processing.
|
||||
//!
|
||||
//! This crate provides the core caching infrastructure used during block execution:
|
||||
//! - [`ExecutionCache`]: Fixed-size concurrent caches for accounts, storage, and bytecode
|
||||
//! - [`SavedCache`]: An execution cache snapshot associated with a specific block hash
|
||||
//! - [`PayloadExecutionCache`]: Thread-safe wrapper for sharing cached state across payload
|
||||
//! processing tasks
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
|
||||
mod cached_state;
|
||||
pub use cached_state::*;
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use metrics::{Counter, Histogram};
|
||||
use parking_lot::RwLock;
|
||||
use reth_metrics::Metrics;
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
/// A guarded, thread-safe cache of execution state that tracks the most recent block's caches.
|
||||
///
|
||||
/// This is the cross-block cache used to accelerate sequential payload processing.
|
||||
/// When a new block arrives, its parent's cached state can be reused to avoid
|
||||
/// redundant database lookups.
|
||||
///
|
||||
/// This process assumes that payloads are received sequentially.
|
||||
///
|
||||
/// ## Cache Safety
|
||||
///
|
||||
/// **CRITICAL**: Cache update operations require exclusive access. All concurrent cache users
|
||||
/// (such as prewarming tasks) must be terminated before calling
|
||||
/// [`PayloadExecutionCache::update_with_guard`], otherwise the cache may be corrupted or cleared.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PayloadExecutionCache {
|
||||
/// Guarded cloneable cache identified by a block hash.
|
||||
inner: Arc<RwLock<Option<SavedCache>>>,
|
||||
/// Metrics for cache operations.
|
||||
metrics: PayloadExecutionCacheMetrics,
|
||||
}
|
||||
|
||||
impl PayloadExecutionCache {
|
||||
/// Returns the cache for `parent_hash` if it's available for use.
|
||||
///
|
||||
/// A cache is considered available when:
|
||||
/// - It exists and matches the requested parent hash
|
||||
/// - No other tasks are currently using it (checked via Arc reference count)
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
|
||||
pub fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
|
||||
let start = Instant::now();
|
||||
let mut cache = self.inner.write();
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
|
||||
if elapsed.as_millis() > 5 {
|
||||
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
|
||||
}
|
||||
|
||||
if let Some(c) = cache.as_mut() {
|
||||
let cached_hash = c.executed_block_hash();
|
||||
// Check that the cache hash matches the parent hash of the current block. It won't
|
||||
// match in case it's a fork block.
|
||||
let hash_matches = cached_hash == parent_hash;
|
||||
// Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
|
||||
// a reference to this cache. We can only reuse it when we have exclusive access.
|
||||
let available = c.is_available();
|
||||
let usage_count = c.usage_count();
|
||||
|
||||
debug!(
|
||||
target: "engine::caching",
|
||||
%cached_hash,
|
||||
%parent_hash,
|
||||
hash_matches,
|
||||
available,
|
||||
usage_count,
|
||||
"Existing cache found"
|
||||
);
|
||||
|
||||
if available {
|
||||
if !hash_matches {
|
||||
// Fork block: clear and update the hash on the ORIGINAL before cloning.
|
||||
// This prevents the canonical chain from matching on the stale hash
|
||||
// and picking up polluted data if the fork block fails.
|
||||
c.clear_with_hash(parent_hash);
|
||||
}
|
||||
return Some(c.clone())
|
||||
} else if hash_matches {
|
||||
self.metrics.execution_cache_in_use.increment(1);
|
||||
}
|
||||
} else {
|
||||
debug!(target: "engine::caching", %parent_hash, "No cache found");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Waits until the execution cache becomes available for use.
|
||||
///
|
||||
/// This acquires a write lock to ensure exclusive access, then immediately releases it.
|
||||
/// This is useful for synchronization before starting payload processing.
|
||||
///
|
||||
/// Returns the time spent waiting for the lock.
|
||||
pub fn wait_for_availability(&self) -> Duration {
|
||||
let start = Instant::now();
|
||||
// Acquire write lock to wait for any current holders to finish
|
||||
let _guard = self.inner.write();
|
||||
let elapsed = start.elapsed();
|
||||
if elapsed.as_millis() > 5 {
|
||||
debug!(
|
||||
target: "engine::tree::payload_processor",
|
||||
blocked_for=?elapsed,
|
||||
"Waited for execution cache to become available"
|
||||
);
|
||||
}
|
||||
elapsed
|
||||
}
|
||||
|
||||
/// Updates the cache with a closure that has exclusive access to the guard.
|
||||
/// This ensures that all cache operations happen atomically.
|
||||
///
|
||||
/// ## CRITICAL SAFETY REQUIREMENT
|
||||
///
|
||||
/// **Before calling this method, you MUST ensure there are no other active cache users.**
|
||||
/// This includes:
|
||||
/// - No running prewarming task instances that could write to the cache
|
||||
/// - No concurrent transactions that might access the cached state
|
||||
/// - All prewarming operations must be completed or cancelled
|
||||
///
|
||||
/// Violating this requirement can result in cache corruption, incorrect state data,
|
||||
/// and potential consensus failures.
|
||||
pub fn update_with_guard<F>(&self, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut Option<SavedCache>),
|
||||
{
|
||||
let mut guard = self.inner.write();
|
||||
update_fn(&mut guard);
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for [`PayloadExecutionCache`] operations.
|
||||
#[derive(Metrics, Clone)]
|
||||
#[metrics(scope = "consensus.engine.beacon")]
|
||||
struct PayloadExecutionCacheMetrics {
|
||||
/// Counter for when the execution cache was unavailable because other threads
|
||||
/// (e.g., prewarming) are still using it.
|
||||
execution_cache_in_use: Counter,
|
||||
/// Time spent waiting for execution cache mutex to become available.
|
||||
execution_cache_wait_duration: Histogram,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_checkout_blocks_second() {
|
||||
let cache = PayloadExecutionCache::default();
|
||||
let hash = B256::from([1u8; 32]);
|
||||
|
||||
cache.update_with_guard(|slot| {
|
||||
*slot = Some(SavedCache::new(
|
||||
hash,
|
||||
ExecutionCache::new(1_000),
|
||||
CachedStateMetrics::zeroed(),
|
||||
))
|
||||
});
|
||||
|
||||
let first = cache.get_cache_for(hash);
|
||||
assert!(first.is_some());
|
||||
|
||||
let second = cache.get_cache_for(hash);
|
||||
assert!(second.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkout_available_after_drop() {
|
||||
let cache = PayloadExecutionCache::default();
|
||||
let hash = B256::from([2u8; 32]);
|
||||
|
||||
cache.update_with_guard(|slot| {
|
||||
*slot = Some(SavedCache::new(
|
||||
hash,
|
||||
ExecutionCache::new(1_000),
|
||||
CachedStateMetrics::zeroed(),
|
||||
))
|
||||
});
|
||||
|
||||
let checked_out = cache.get_cache_for(hash);
|
||||
assert!(checked_out.is_some());
|
||||
drop(checked_out);
|
||||
|
||||
let second = cache.get_cache_for(hash);
|
||||
assert!(second.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_mismatch_clears_and_retags() {
|
||||
let cache = PayloadExecutionCache::default();
|
||||
let hash_a = B256::from([0xAA; 32]);
|
||||
let hash_b = B256::from([0xBB; 32]);
|
||||
|
||||
cache.update_with_guard(|slot| {
|
||||
*slot = Some(SavedCache::new(
|
||||
hash_a,
|
||||
ExecutionCache::new(1_000),
|
||||
CachedStateMetrics::zeroed(),
|
||||
))
|
||||
});
|
||||
|
||||
let checked_out = cache.get_cache_for(hash_b);
|
||||
assert!(checked_out.is_some());
|
||||
assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_cache_returns_none() {
|
||||
let cache = PayloadExecutionCache::default();
|
||||
assert!(cache.get_cache_for(B256::ZERO).is_none());
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,14 @@ futures-util.workspace = true
|
||||
# misc
|
||||
eyre.workspace = true
|
||||
tracing.workspace = true
|
||||
op-alloy-rpc-types-engine = { workspace = true, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
op = [
|
||||
"dep:op-alloy-rpc-types-engine",
|
||||
"reth-payload-primitives/op",
|
||||
"reth-primitives-traits/op",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,9 @@ use eyre::OptionExt;
|
||||
use futures_util::{stream::Fuse, Stream, StreamExt};
|
||||
use reth_engine_primitives::ConsensusEngineHandle;
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadAttributesBuilder, PayloadKind, PayloadTypes};
|
||||
use reth_payload_primitives::{
|
||||
BuiltPayload, EngineApiMessageVersion, PayloadAttributesBuilder, PayloadKind, PayloadTypes,
|
||||
};
|
||||
use reth_primitives_traits::{HeaderTy, SealedHeaderFor};
|
||||
use reth_storage_api::BlockReader;
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
@@ -212,7 +214,10 @@ where
|
||||
/// Sends a FCU to the engine.
|
||||
async fn update_forkchoice_state(&self) -> eyre::Result<()> {
|
||||
let state = self.forkchoice_state();
|
||||
let res = self.to_engine.fork_choice_updated(state, None).await?;
|
||||
let res = self
|
||||
.to_engine
|
||||
.fork_choice_updated(state, None, EngineApiMessageVersion::default())
|
||||
.await?;
|
||||
|
||||
if !res.is_valid() {
|
||||
eyre::bail!("Invalid fork choice update {state:?}: {res:?}")
|
||||
@@ -229,6 +234,7 @@ where
|
||||
.fork_choice_updated(
|
||||
self.forkchoice_state(),
|
||||
Some(self.payload_attributes_builder.build(&self.last_header)),
|
||||
EngineApiMessageVersion::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -60,3 +60,56 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "op")]
|
||||
impl<ChainSpec>
|
||||
PayloadAttributesBuilder<op_alloy_rpc_types_engine::OpPayloadAttributes, ChainSpec::Header>
|
||||
for LocalPayloadAttributesBuilder<ChainSpec>
|
||||
where
|
||||
ChainSpec: EthChainSpec + EthereumHardforks + 'static,
|
||||
{
|
||||
fn build(
|
||||
&self,
|
||||
parent: &SealedHeader<ChainSpec::Header>,
|
||||
) -> op_alloy_rpc_types_engine::OpPayloadAttributes {
|
||||
use alloy_primitives::B64;
|
||||
use reth_chainspec::BaseFeeParams;
|
||||
use std::env;
|
||||
/// Dummy system transaction for dev mode.
|
||||
/// OP Mainnet transaction at index 0 in block 124665056.
|
||||
///
|
||||
/// <https://optimistic.etherscan.io/tx/0x312e290cf36df704a2217b015d6455396830b0ce678b860ebfcc30f41403d7b1>
|
||||
const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = alloy_primitives::hex!(
|
||||
"7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
|
||||
);
|
||||
|
||||
// Configure EIP-1559 parameters for dev mode. These can be overridden via environment
|
||||
// variables (OP_DEV_EIP1559_DENOMINATOR, OP_DEV_EIP1559_ELASTICITY, OP_DEV_GAS_LIMIT),
|
||||
// otherwise defaults from Optimism's BaseFeeParams are used. The parameters are encoded
|
||||
// as an 8-byte value (denominator + elasticity) required by Optimism's Jovian fork.
|
||||
let default_eip_1559_params = BaseFeeParams::optimism();
|
||||
let denominator = env::var("OP_DEV_EIP1559_DENOMINATOR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(default_eip_1559_params.max_change_denominator as u32);
|
||||
let elasticity = env::var("OP_DEV_EIP1559_ELASTICITY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(default_eip_1559_params.elasticity_multiplier as u32);
|
||||
let gas_limit = env::var("OP_DEV_GAS_LIMIT").ok().and_then(|v| v.parse::<u64>().ok());
|
||||
|
||||
let mut eip1559_bytes = [0u8; 8];
|
||||
eip1559_bytes[0..4].copy_from_slice(&denominator.to_be_bytes());
|
||||
eip1559_bytes[4..8].copy_from_slice(&elasticity.to_be_bytes());
|
||||
let eip_1559_params = Some(B64::from(eip1559_bytes));
|
||||
|
||||
op_alloy_rpc_types_engine::OpPayloadAttributes {
|
||||
payload_attributes: self.build(parent),
|
||||
transactions: Some(vec![TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.into()]),
|
||||
no_tx_pool: None,
|
||||
gas_limit,
|
||||
eip_1559_params,
|
||||
min_base_fee: Some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ use core::time::Duration;
|
||||
/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
|
||||
pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
|
||||
|
||||
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
|
||||
pub const DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD: u64 = 16;
|
||||
|
||||
/// How close to the canonical head we persist blocks.
|
||||
pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
|
||||
|
||||
@@ -47,16 +44,6 @@ const DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH: u32 = 256;
|
||||
const DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE: usize = 4;
|
||||
const DEFAULT_CROSS_BLOCK_CACHE_SIZE: usize = default_cross_block_cache_size();
|
||||
|
||||
const fn assert_backpressure_threshold_invariant(
|
||||
persistence_threshold: u64,
|
||||
persistence_backpressure_threshold: u64,
|
||||
) {
|
||||
debug_assert!(
|
||||
persistence_backpressure_threshold > persistence_threshold,
|
||||
"persistence_backpressure_threshold must be greater than persistence_threshold",
|
||||
);
|
||||
}
|
||||
|
||||
const fn default_cross_block_cache_size() -> usize {
|
||||
if cfg!(test) {
|
||||
1024 * 1024 // 1 MB in tests
|
||||
@@ -95,8 +82,6 @@ pub struct TreeConfig {
|
||||
///
|
||||
/// Note: this should be less than or equal to `persistence_threshold`.
|
||||
memory_block_buffer_target: u64,
|
||||
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
|
||||
persistence_backpressure_threshold: u64,
|
||||
/// Number of pending blocks that cannot be executed due to missing parent and
|
||||
/// are kept in cache.
|
||||
block_buffer_limit: u32,
|
||||
@@ -161,15 +146,13 @@ pub struct TreeConfig {
|
||||
slow_block_threshold: Option<Duration>,
|
||||
/// Whether to fully disable sparse trie cache pruning between blocks.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to use the arena-based sparse trie implementation.
|
||||
enable_arena_sparse_trie: bool,
|
||||
/// Timeout for the state root task before spawning a sequential fallback computation.
|
||||
/// If `Some`, after waiting this duration for the state root task, a sequential state root
|
||||
/// computation is spawned in parallel and whichever finishes first is used.
|
||||
/// If `None`, the timeout fallback is disabled.
|
||||
state_root_task_timeout: Option<Duration>,
|
||||
/// Whether to share execution cache with the payload builder.
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
/// Whether to share sparse trie with the payload builder.
|
||||
share_sparse_trie_with_payload_builder: 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.
|
||||
@@ -179,14 +162,9 @@ pub struct TreeConfig {
|
||||
|
||||
impl Default for TreeConfig {
|
||||
fn default() -> Self {
|
||||
assert_backpressure_threshold_invariant(
|
||||
DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
|
||||
);
|
||||
Self {
|
||||
persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
memory_block_buffer_target: DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
|
||||
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,
|
||||
max_execute_block_batch_size: DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE,
|
||||
@@ -209,9 +187,8 @@ impl Default for TreeConfig {
|
||||
sparse_trie_max_hot_accounts: DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS,
|
||||
slow_block_threshold: None,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
enable_arena_sparse_trie: false,
|
||||
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
|
||||
share_execution_cache_with_payload_builder: false,
|
||||
share_sparse_trie_with_payload_builder: false,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -224,7 +201,6 @@ impl TreeConfig {
|
||||
pub const fn new(
|
||||
persistence_threshold: u64,
|
||||
memory_block_buffer_target: u64,
|
||||
persistence_backpressure_threshold: u64,
|
||||
block_buffer_limit: u32,
|
||||
max_invalid_header_cache_length: u32,
|
||||
max_execute_block_batch_size: usize,
|
||||
@@ -247,17 +223,10 @@ impl TreeConfig {
|
||||
sparse_trie_max_hot_accounts: usize,
|
||||
slow_block_threshold: Option<Duration>,
|
||||
state_root_task_timeout: Option<Duration>,
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
share_sparse_trie_with_payload_builder: bool,
|
||||
) -> Self {
|
||||
assert_backpressure_threshold_invariant(
|
||||
persistence_threshold,
|
||||
persistence_backpressure_threshold,
|
||||
);
|
||||
Self {
|
||||
persistence_threshold,
|
||||
memory_block_buffer_target,
|
||||
persistence_backpressure_threshold,
|
||||
block_buffer_limit,
|
||||
max_invalid_header_cache_length,
|
||||
max_execute_block_batch_size,
|
||||
@@ -280,9 +249,8 @@ impl TreeConfig {
|
||||
sparse_trie_max_hot_accounts,
|
||||
slow_block_threshold,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
enable_arena_sparse_trie: false,
|
||||
state_root_task_timeout,
|
||||
share_execution_cache_with_payload_builder,
|
||||
share_sparse_trie_with_payload_builder,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -298,11 +266,6 @@ impl TreeConfig {
|
||||
self.memory_block_buffer_target
|
||||
}
|
||||
|
||||
/// Return the persistence backpressure threshold.
|
||||
pub const fn persistence_backpressure_threshold(&self) -> u64 {
|
||||
self.persistence_backpressure_threshold
|
||||
}
|
||||
|
||||
/// Return the block buffer limit.
|
||||
pub const fn block_buffer_limit(&self) -> u32 {
|
||||
self.block_buffer_limit
|
||||
@@ -399,10 +362,6 @@ impl TreeConfig {
|
||||
/// Setter for persistence threshold.
|
||||
pub const fn with_persistence_threshold(mut self, persistence_threshold: u64) -> Self {
|
||||
self.persistence_threshold = persistence_threshold;
|
||||
assert_backpressure_threshold_invariant(
|
||||
self.persistence_threshold,
|
||||
self.persistence_backpressure_threshold,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -415,19 +374,6 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for persistence backpressure threshold.
|
||||
pub const fn with_persistence_backpressure_threshold(
|
||||
mut self,
|
||||
persistence_backpressure_threshold: u64,
|
||||
) -> Self {
|
||||
self.persistence_backpressure_threshold = persistence_backpressure_threshold;
|
||||
assert_backpressure_threshold_invariant(
|
||||
self.persistence_threshold,
|
||||
self.persistence_backpressure_threshold,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for block buffer limit.
|
||||
pub const fn with_block_buffer_limit(mut self, block_buffer_limit: u32) -> Self {
|
||||
self.block_buffer_limit = block_buffer_limit;
|
||||
@@ -606,6 +552,17 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether the arena-based sparse trie is enabled.
|
||||
pub const fn enable_arena_sparse_trie(&self) -> bool {
|
||||
self.enable_arena_sparse_trie
|
||||
}
|
||||
|
||||
/// Setter for whether to enable the arena-based sparse trie.
|
||||
pub const fn with_enable_arena_sparse_trie(mut self, value: bool) -> Self {
|
||||
self.enable_arena_sparse_trie = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the state root task timeout.
|
||||
pub const fn state_root_task_timeout(&self) -> Option<Duration> {
|
||||
self.state_root_task_timeout
|
||||
@@ -617,35 +574,6 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether to share execution cache with the payload builder.
|
||||
pub const fn share_execution_cache_with_payload_builder(&self) -> bool {
|
||||
self.share_execution_cache_with_payload_builder
|
||||
}
|
||||
|
||||
/// Returns whether to share sparse trie with the payload builder.
|
||||
pub const fn share_sparse_trie_with_payload_builder(&self) -> bool {
|
||||
self.share_sparse_trie_with_payload_builder
|
||||
}
|
||||
|
||||
/// Setter for whether to share execution cache with the payload builder.
|
||||
pub const fn with_share_execution_cache_with_payload_builder(
|
||||
mut self,
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
) -> Self {
|
||||
self.share_execution_cache_with_payload_builder =
|
||||
share_execution_cache_with_payload_builder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for whether to share sparse trie with the payload builder.
|
||||
pub const fn with_share_sparse_trie_with_payload_builder(
|
||||
mut self,
|
||||
share_sparse_trie_with_payload_builder: bool,
|
||||
) -> Self {
|
||||
self.share_sparse_trie_with_payload_builder = share_sparse_trie_with_payload_builder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the proof jitter duration, if configured (trie-debug only).
|
||||
#[cfg(feature = "trie-debug")]
|
||||
pub const fn proof_jitter(&self) -> Option<Duration> {
|
||||
@@ -659,18 +587,3 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TreeConfig;
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "persistence_backpressure_threshold must be greater than persistence_threshold"
|
||||
)]
|
||||
fn rejects_backpressure_threshold_at_or_below_persistence_threshold() {
|
||||
let _ = TreeConfig::default()
|
||||
.with_persistence_threshold(4)
|
||||
.with_persistence_backpressure_threshold(4);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user