mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
94 Commits
bal-devnet
...
dan/static
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956a31c149 | ||
|
|
f8efc76880 | ||
|
|
ef3cda7b66 | ||
|
|
23b68fcc38 | ||
|
|
3802a31991 | ||
|
|
eab36bd18f | ||
|
|
afbb3986d7 | ||
|
|
0f7cd0fd98 | ||
|
|
f0d07c38be | ||
|
|
930f2a6eb2 | ||
|
|
69bde3a5cc | ||
|
|
949fe33066 | ||
|
|
fc6462b5ba | ||
|
|
dae2485b04 | ||
|
|
dc22ece4d2 | ||
|
|
540f513a88 | ||
|
|
43dfe6ed84 | ||
|
|
0f89525111 | ||
|
|
49339780c0 | ||
|
|
27781443a6 | ||
|
|
afdf905295 | ||
|
|
d2d2f34409 | ||
|
|
4f34ac7e10 | ||
|
|
29bab063b7 | ||
|
|
9d360728f3 | ||
|
|
9db411efce | ||
|
|
3208a4a615 | ||
|
|
7096d6ce1a | ||
|
|
e3dbdbb115 | ||
|
|
0cbd0aa4cf | ||
|
|
dba8b21aa7 | ||
|
|
ef0095b565 | ||
|
|
b3dd2e246d | ||
|
|
eb663aeaac | ||
|
|
7f4a9a05ef | ||
|
|
d3c3466c44 | ||
|
|
fb62487148 | ||
|
|
401e751088 | ||
|
|
7d31bb176c | ||
|
|
50ce26f719 | ||
|
|
4094d677e4 | ||
|
|
a37f91e6c0 | ||
|
|
78b97e81b7 | ||
|
|
aedda7f6ad | ||
|
|
acc7b56e31 | ||
|
|
87077ddcde | ||
|
|
5a66d0064c | ||
|
|
6183361f83 | ||
|
|
e91a900dd7 | ||
|
|
33ec89994e | ||
|
|
80094e1bda | ||
|
|
2e5730b6b5 | ||
|
|
677d07041e | ||
|
|
8606df3075 | ||
|
|
cf83b198d3 | ||
|
|
52ab4223a0 | ||
|
|
15338b8113 | ||
|
|
b3f5e62494 | ||
|
|
7b4c07338e | ||
|
|
bbed2e9ebf | ||
|
|
8c6e67bbaa | ||
|
|
8bb96ace64 | ||
|
|
81dc5e2136 | ||
|
|
ad00546081 | ||
|
|
cc0c29e449 | ||
|
|
bdcc262bbb | ||
|
|
0c90359be6 | ||
|
|
b8aca9586a | ||
|
|
fbbadab3be | ||
|
|
e0d40df3df | ||
|
|
ff217592bc | ||
|
|
a9b6969e77 | ||
|
|
4338fb2631 | ||
|
|
cc6d14a2ca | ||
|
|
cfab0c6371 | ||
|
|
bc7d585506 | ||
|
|
4bfc0083c9 | ||
|
|
3d1dc4d9e2 | ||
|
|
c9e9db184e | ||
|
|
2d2778fa24 | ||
|
|
7551d9c5dd | ||
|
|
182f39db67 | ||
|
|
e738bd34b3 | ||
|
|
6fb5337786 | ||
|
|
f1c71d0c2e | ||
|
|
76e45117da | ||
|
|
40eb2d63e8 | ||
|
|
832ac79b8e | ||
|
|
47681f785d | ||
|
|
cb47b195fd | ||
|
|
5a9bbbe72b | ||
|
|
749719c053 | ||
|
|
cf12b2cb7f | ||
|
|
8122fdf0af |
5
.changelog/fix-download-api-url.md
Normal file
5
.changelog/fix-download-api-url.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
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.
|
||||
6
.changelog/kind-eagles-hum.md
Normal file
6
.changelog/kind-eagles-hum.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
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.
|
||||
13
.changelog/kind-frogs-meow.md
Normal file
13
.changelog/kind-frogs-meow.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
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.
|
||||
6
.changelog/odd-birds-meow.md
Normal file
6
.changelog/odd-birds-meow.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
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`.
|
||||
6
.changelog/rich-pigs-shake.md
Normal file
6
.changelog/rich-pigs-shake.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
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.
|
||||
5
.changelog/tidy-ducks-bake.md
Normal file
5
.changelog/tidy-ducks-bake.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
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.
|
||||
5
.changelog/warm-slugs-crawl.md
Normal file
5
.changelog/warm-slugs-crawl.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
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.
|
||||
8
.changelog/zesty-bees-drink.md
Normal file
8
.changelog/zesty-bees-drink.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
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",
|
||||
"--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",
|
||||
# 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,10 +11,11 @@
|
||||
# optional branch-sha is the PR head commit for cache key
|
||||
#
|
||||
# Outputs:
|
||||
# baseline: <source-dir>/target/profiling/reth
|
||||
# feature: <source-dir>/target/profiling/reth, reth-bench installed to cargo bin
|
||||
# baseline: <source-dir>/target/profiling/reth (or reth-bb if BENCH_BIG_BLOCKS=true)
|
||||
# feature: <source-dir>/target/profiling/reth (or reth-bb), reth-bench installed to cargo bin
|
||||
#
|
||||
# Required: mc (MinIO client) with a configured alias
|
||||
# Optional env: BENCH_BIG_BLOCKS (true/false) — build reth-bb instead of reth
|
||||
set -euo pipefail
|
||||
|
||||
MC="mc"
|
||||
@@ -22,6 +23,16 @@ 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=""
|
||||
@@ -62,18 +73,18 @@ case "$MODE" in
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
|
||||
CACHE_VALID=false
|
||||
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
|
||||
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
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached baseline binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building baseline (${COMMIT}) from source..."
|
||||
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
FEATURES_ARG=""
|
||||
WORKSPACE_ARG=""
|
||||
@@ -84,8 +95,8 @@ case "$MODE" in
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling --bin reth $WORKSPACE_ARG $FEATURES_ARG
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
cargo build --profile profiling $NODE_PKG $WORKSPACE_ARG $FEATURES_ARG
|
||||
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -94,32 +105,34 @@ case "$MODE" in
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
if $MC stat "${BUCKET}/${NODE_BIN}" &>/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}/reth" "${SOURCE_DIR}/target/profiling/reth"
|
||||
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
|
||||
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
|
||||
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
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached feature binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building feature (${COMMIT}) from source..."
|
||||
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
rustup show active-toolchain || rustup default stable
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# Can't use `make profiling` when adding features; build explicitly
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling --workspace --bin reth --features "${EXTRA_FEATURES}"
|
||||
cargo build --profile profiling --workspace $NODE_PKG --features "${EXTRA_FEATURES}"
|
||||
else
|
||||
make profiling
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling $NODE_PKG
|
||||
fi
|
||||
make install-reth-bench
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
|
||||
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
|
||||
fi
|
||||
;;
|
||||
|
||||
27
.github/scripts/bench-reth-run.sh
vendored
27
.github/scripts/bench-reth-run.sh
vendored
@@ -18,7 +18,11 @@ set -euo pipefail
|
||||
LABEL="$1"
|
||||
BINARY="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
DATADIR_NAME="datadir"
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
LOG="${OUTPUT_DIR}/node.log"
|
||||
|
||||
@@ -128,12 +132,6 @@ if "$BINARY" node --help 2>/dev/null | grep -qF -- '--debug.startup-sync-state-i
|
||||
SYNC_STATE_IDLE=true
|
||||
fi
|
||||
|
||||
# Big blocks mode requires the testing API, skip-invalid-transactions, and
|
||||
# skip-gas-limit-ramp-check + gas-limit override to avoid the 6800-block ramp.
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
RETH_ARGS+=(--http.api eth,net,web3,reth,testing --rpc.max-request-size max --testing.skip-invalid-transactions --testing.skip-gas-limit-ramp-check --testing.gas-limit 1000000000)
|
||||
fi
|
||||
|
||||
# Append per-label extra node args (baseline or feature)
|
||||
EXTRA_NODE_ARGS=""
|
||||
case "$LABEL" in
|
||||
@@ -244,7 +242,7 @@ 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)
|
||||
EXTRA_BENCH_ARGS+=(--reth-new-payload --wait-for-persistence)
|
||||
fi
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
@@ -252,7 +250,7 @@ fi
|
||||
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
# Big blocks mode: replay pre-generated payloads
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
|
||||
|
||||
# Start tracy-capture so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
@@ -262,9 +260,18 @@ 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 \
|
||||
"${EXTRA_BENCH_ARGS[@]}" \
|
||||
"${BB_BENCH_ARGS[@]}" \
|
||||
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
|
||||
17
.github/scripts/bench-reth-snapshot.sh
vendored
17
.github/scripts/bench-reth-snapshot.sh
vendored
@@ -22,9 +22,18 @@ set -euo pipefail
|
||||
|
||||
MC="mc"
|
||||
BUCKET="minio/reth-snapshots"
|
||||
MANIFEST_PATH="reth-1-minimal-stable/manifest.json"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
HASH_FILE="$HOME/.reth-bench-snapshot-hash"
|
||||
# Allow overriding the snapshot name (e.g. for big-blocks mode where the
|
||||
# big-blocks manifest specifies which base snapshot to use).
|
||||
SNAPSHOT_NAME="${BENCH_SNAPSHOT_NAME:-reth-1-minimal-stable}"
|
||||
MANIFEST_PATH="${SNAPSHOT_NAME}/manifest.json"
|
||||
DATADIR_NAME="datadir"
|
||||
HASH_MODE_SUFFIX=""
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
HASH_MODE_SUFFIX="-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
HASH_FILE="$HOME/.reth-bench-snapshot-hash${HASH_MODE_SUFFIX}"
|
||||
|
||||
# Fetch manifest and compute content hash for reliable freshness check
|
||||
MANIFEST_CONTENT=$($MC cat "${BUCKET}/${MANIFEST_PATH}" 2>/dev/null) || {
|
||||
@@ -60,7 +69,7 @@ if [ -z "$MINIO_ENDPOINT" ]; then
|
||||
echo "::error::Failed to resolve MinIO endpoint from mc alias 'minio'"
|
||||
exit 1
|
||||
fi
|
||||
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/reth-1-minimal-stable"
|
||||
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/${SNAPSHOT_NAME}"
|
||||
|
||||
# Rewrite manifest's base_url with the runner-reachable endpoint
|
||||
MANIFEST_TMP=$(mktemp --suffix=.json)
|
||||
|
||||
125
.github/scripts/bench-scheduled-refs.sh
vendored
125
.github/scripts/bench-scheduled-refs.sh
vendored
@@ -1,31 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Resolves baseline and feature refs for nightly regression benchmark runs.
|
||||
# Resolves baseline and feature refs for scheduled benchmark runs.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# Usage: bench-nightly-refs.sh [--force]
|
||||
# Usage: bench-scheduled-refs.sh <force> <mode>
|
||||
# force — "true" to run even if no new commit (bypass skip logic)
|
||||
# mode — "nightly" or "hourly"
|
||||
#
|
||||
# Outputs (via GITHUB_OUTPUT):
|
||||
# baseline-ref — commit SHA for baseline
|
||||
# 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
|
||||
# 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)
|
||||
#
|
||||
# Reads:
|
||||
# .nightly-state/last-feature-ref (from GH Actions cache, may not exist)
|
||||
# .nightly-state/last-feature-ref (nightly, from GH Actions cache)
|
||||
# .hourly-state/last-feature-ref (hourly, from GH Actions cache)
|
||||
#
|
||||
# Requires: gh (GitHub CLI), jq, date
|
||||
# Requires: gh (GitHub CLI), jq, date, git (hourly mode)
|
||||
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,5 +20,6 @@
|
||||
"SuperFluffy": "U095BKHB2Q4",
|
||||
"kamsz": "U0A2563UBRD",
|
||||
"zerosnacks": "U09FARPMN74",
|
||||
"samczsun": "U096R14E4H3"
|
||||
"samczsun": "U096R14E4H3",
|
||||
"laibe": "U09FARE0B9Q"
|
||||
}
|
||||
|
||||
3
.github/scripts/check_rv32imac.sh
vendored
3
.github/scripts/check_rv32imac.sh
vendored
@@ -2,9 +2,6 @@
|
||||
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,6 +22,7 @@ exclude_crates=(
|
||||
reth-downloaders
|
||||
reth-e2e-test-utils
|
||||
reth-engine-service
|
||||
reth-execution-cache
|
||||
reth-engine-tree
|
||||
reth-engine-util
|
||||
reth-eth-wire
|
||||
@@ -55,6 +56,7 @@ 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
|
||||
|
||||
4
.github/scripts/hive/Dockerfile
vendored
4
.github/scripts/hive/Dockerfile
vendored
@@ -3,11 +3,11 @@
|
||||
#
|
||||
# We'll use cargo-chef to speed up the build
|
||||
#
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-trixie AS chef
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
|
||||
RUN apt-get update && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
#
|
||||
# We prepare the build plan
|
||||
|
||||
11
.github/scripts/hive/build_simulators.sh
vendored
11
.github/scripts/hive/build_simulators.sh
vendored
@@ -9,16 +9,15 @@ go build .
|
||||
|
||||
./hive -client reth # first builds and caches the client
|
||||
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
echo "Building images"
|
||||
# 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/consume-engine" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.1.0/fixtures_bal.tar.gz \
|
||||
--sim.buildarg branch=err-map-3 \
|
||||
--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/bal@v5.1.0/fixtures_bal.tar.gz \
|
||||
--sim.buildarg branch=err-map-3 \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
|
||||
|
||||
85
.github/scripts/hive/expected_failures.yaml
vendored
85
.github/scripts/hive/expected_failures.yaml
vendored
@@ -1,9 +1,6 @@
|
||||
# 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)
|
||||
@@ -16,19 +13,15 @@ rpc-compat:
|
||||
# syncing mode, the test expects syncing to be false on start
|
||||
- eth_syncing/check-syncing (reth)
|
||||
|
||||
engine-withdrawals: []
|
||||
engine-withdrawals: [ ]
|
||||
|
||||
engine-api: []
|
||||
engine-api: [ ]
|
||||
|
||||
engine-cancun:
|
||||
- Invalid PayloadAttributes, Missing BeaconRoot, Syncing=True (Cancun) (reth)
|
||||
# the test fails with older versions of the code for which it passed before, probably related to changes
|
||||
# in hive or its dependencies
|
||||
- Blob Transaction Ordering, Multiple Clients (Cancun) (reth)
|
||||
engine-cancun: [ ]
|
||||
|
||||
sync: []
|
||||
sync: [ ]
|
||||
|
||||
engine-auth: []
|
||||
engine-auth: [ ]
|
||||
|
||||
# EIP-7610 related tests (Revert creation in case of non-empty storage):
|
||||
#
|
||||
@@ -46,10 +39,6 @@ engine-auth: []
|
||||
#
|
||||
# System contract tests (already fixed and deployed):
|
||||
#
|
||||
# tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout and test_invalid_log_length
|
||||
# System contract is already fixed and deployed; tests cover scenarios where contract is
|
||||
# malformed which can't happen retroactively. No point in adding checks.
|
||||
#
|
||||
# tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment
|
||||
# tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment
|
||||
# Post-fork system contract deployment tests. Should fix for spec compliance but not realistic
|
||||
@@ -58,44 +47,8 @@ eels/consume-engine:
|
||||
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test_engine-zero_nonce]-reth
|
||||
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
|
||||
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
|
||||
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
|
||||
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_False]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_True]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_False]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_True]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_True]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_False]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Osaka-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Paris-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
@@ -146,20 +99,6 @@ eels/consume-engine:
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_engine_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
|
||||
|
||||
# Blob limit tests:
|
||||
#
|
||||
@@ -254,17 +193,3 @@ eels/consume-rlp:
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
|
||||
162
.github/workflows/bench-scheduled.yml
vendored
162
.github/workflows/bench-scheduled.yml
vendored
@@ -1,19 +1,25 @@
|
||||
# Nightly regression benchmark.
|
||||
# Scheduled regression benchmarks (nightly + hourly).
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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:
|
||||
- cron: "30 5 * * *" # 06:30 UTC daily
|
||||
# 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 * * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: "Force run even if no new nightly (bypass skip logic)"
|
||||
description: "Force run even if no new commit (bypass skip logic)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -22,6 +28,14 @@ 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
|
||||
@@ -35,44 +49,105 @@ permissions:
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: Resolve nightly refs, check staleness, manage state
|
||||
# Job 1: Resolve 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: Restore nightly state
|
||||
- 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
|
||||
id: state-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .nightly-state
|
||||
key: bench-scheduled-state-dummy
|
||||
path: .${{ steps.mode.outputs.mode == 'hourly' && 'hourly' || 'nightly' }}-state
|
||||
key: bench-${{ steps.mode.outputs.mode }}-state-dummy
|
||||
restore-keys: |
|
||||
bench-scheduled-state-
|
||||
bench-${{ steps.mode.outputs.mode }}-state-
|
||||
|
||||
- name: Resolve nightly refs
|
||||
- name: Resolve refs
|
||||
id: refs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
FORCE="${{ inputs.force || 'false' }}"
|
||||
.github/scripts/bench-scheduled-refs.sh "$FORCE"
|
||||
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 }),
|
||||
});
|
||||
|
||||
- name: Alert on stale nightly
|
||||
if: steps.refs.outputs.is-stale == 'true'
|
||||
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
@@ -146,7 +221,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Fail on stale nightly
|
||||
if: steps.refs.outputs.is-stale == 'true'
|
||||
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
|
||||
run: |
|
||||
echo "::error::Nightly build is stale (>24h old). Aborting."
|
||||
exit 1
|
||||
@@ -167,7 +242,8 @@ jobs:
|
||||
SCHELK_MOUNT: /reth-bench
|
||||
BENCH_WORK_DIR: ${{ github.workspace }}/bench-work
|
||||
BENCH_PR: ""
|
||||
BENCH_ACTOR: "nightly-regression"
|
||||
BENCH_MODE: ${{ needs.resolve-refs.outputs.mode }}
|
||||
BENCH_ACTOR: "${{ needs.resolve-refs.outputs.mode }}-regression"
|
||||
BENCH_BLOCKS: "2000"
|
||||
BENCH_WARMUP_BLOCKS: "500"
|
||||
BENCH_SAMPLY: "false"
|
||||
@@ -273,8 +349,8 @@ jobs:
|
||||
run: |
|
||||
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
|
||||
FEATURE_SHORT=$(echo "$FEATURE_REF" | cut -c1-8)
|
||||
echo "baseline-name=nightly-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-name=nightly-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -390,7 +466,7 @@ jobs:
|
||||
|
||||
- name: Start metrics proxy
|
||||
run: |
|
||||
BENCH_ID="nightly-${{ github.run_id }}"
|
||||
BENCH_ID="${BENCH_MODE}-${{ github.run_id }}"
|
||||
BENCH_REFERENCE_EPOCH=$(date +%s)
|
||||
echo "BENCH_ID=${BENCH_ID}" >> "$GITHUB_ENV"
|
||||
echo "BENCH_REFERENCE_EPOCH=${BENCH_REFERENCE_EPOCH}" >> "$GITHUB_ENV"
|
||||
@@ -519,7 +595,7 @@ jobs:
|
||||
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
|
||||
|
||||
- name: Generate charts
|
||||
if: success()
|
||||
if: success() && env.BENCH_MODE != 'hourly'
|
||||
env:
|
||||
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
|
||||
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
|
||||
@@ -545,7 +621,7 @@ jobs:
|
||||
|
||||
- name: Push charts
|
||||
id: push-charts
|
||||
if: success()
|
||||
if: success() && env.BENCH_MODE != 'hourly'
|
||||
run: |
|
||||
RUN_ID=${{ github.run_id }}
|
||||
CHART_DIR="nightly/${RUN_ID}"
|
||||
@@ -591,7 +667,9 @@ jobs:
|
||||
const featureLink = `[\`${summary.feature.name}\`](${commitUrl}/${summary.feature.ref})`;
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
|
||||
let md = `# ${emoji} Nightly Regression: ${label}\n\n`;
|
||||
const mode = process.env.BENCH_MODE || 'nightly';
|
||||
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
|
||||
let md = `# ${emoji} ${modeLabel}: ${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';
|
||||
@@ -671,9 +749,19 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Only notify on significant changes (regression OR improvement)
|
||||
// Filter notifications based on mode
|
||||
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;
|
||||
@@ -697,8 +785,9 @@ jobs:
|
||||
|
||||
function cell(text) { return { type: 'raw_text', text: String(text) || ' ' }; }
|
||||
|
||||
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
|
||||
const sectionText = [
|
||||
'*Nightly Regression*',
|
||||
`*${modeLabel}*`,
|
||||
'',
|
||||
`*Baseline:* ${baselineLink}`,
|
||||
`*Feature:* ${featureLink}`,
|
||||
@@ -714,7 +803,7 @@ jobs:
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: `${headerEmoji} Nightly: ${label}`, emoji: true },
|
||||
text: { type: 'plain_text', text: `${headerEmoji} ${modeLabel}: ${label}`, emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -744,7 +833,7 @@ jobs:
|
||||
},
|
||||
];
|
||||
|
||||
const text = `Nightly Regression: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
const text = `${modeLabel}: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
const resp = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -812,14 +901,17 @@ 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: Nightly Bench Failed', emoji: true },
|
||||
text: { type: 'plain_text', text: `:rotating_light: ${modeLabel} Bench Failed`, emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: `*Nightly regression* failed while *${failedStep}*` },
|
||||
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>` },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
@@ -841,7 +933,7 @@ jobs:
|
||||
body: JSON.stringify({
|
||||
channel,
|
||||
blocks,
|
||||
text: `Nightly bench failed while ${failedStep}`,
|
||||
text: `${modeLabel} bench failed while ${failedStep}`,
|
||||
unfurl_links: false,
|
||||
}),
|
||||
});
|
||||
@@ -862,11 +954,13 @@ jobs:
|
||||
steps:
|
||||
- name: Write state file
|
||||
run: |
|
||||
mkdir -p .nightly-state
|
||||
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > .nightly-state/last-feature-ref
|
||||
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"
|
||||
|
||||
- name: Save nightly state
|
||||
- name: Save state
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: .nightly-state
|
||||
key: bench-scheduled-state-${{ needs.resolve-refs.outputs.feature-ref }}
|
||||
path: .${{ needs.resolve-refs.outputs.mode }}-state
|
||||
key: bench-${{ needs.resolve-refs.outputs.mode }}-state-${{ needs.resolve-refs.outputs.feature-ref }}
|
||||
|
||||
101
.github/workflows/bench.yml
vendored
101
.github/workflows/bench.yml
vendored
@@ -12,10 +12,15 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
blocks:
|
||||
description: "Number of blocks to benchmark (or 'big' for big blocks mode)"
|
||||
description: "Number of blocks to benchmark"
|
||||
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
|
||||
@@ -152,7 +157,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 = blocks === 'big' ? 'true' : 'false';
|
||||
bigBlocks = '${{ github.event.inputs.big_blocks }}' === 'true' ? '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';
|
||||
@@ -178,14 +183,13 @@ jobs:
|
||||
actor = context.payload.comment.user.login;
|
||||
|
||||
const body = context.payload.comment.body.trim();
|
||||
const intArgs = new Set(['warmup', 'cores']);
|
||||
const intOrKeywordArgs = new Map([['blocks', new Set(['big'])]]);
|
||||
const intArgs = new Set(['warmup', 'cores', 'blocks']);
|
||||
const refArgs = new Set(['baseline', 'feature']);
|
||||
const boolArgs = new Set(['samply', 'no-slack']);
|
||||
const boolArgs = new Set(['samply', 'no-slack', 'big-blocks']);
|
||||
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', 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', 'big-blocks': '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*/, '');
|
||||
@@ -230,15 +234,6 @@ 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)`);
|
||||
@@ -255,7 +250,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] [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-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="..."]\``;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
@@ -272,7 +267,7 @@ jobs:
|
||||
samply = defaults.samply;
|
||||
var noSlack = defaults['no-slack'];
|
||||
cores = defaults.cores;
|
||||
bigBlocks = blocks === 'big' ? 'true' : 'false';
|
||||
bigBlocks = defaults['big-blocks'];
|
||||
var rethNewPayload = defaults.reth_newPayload;
|
||||
var abba = defaults.abba;
|
||||
var otlp = defaults.otlp;
|
||||
@@ -508,6 +503,7 @@ 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 || '' }}
|
||||
@@ -723,6 +719,39 @@ 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: |
|
||||
@@ -792,7 +821,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BENCH_REPO: ${{ github.repository }}
|
||||
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/reth
|
||||
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
|
||||
@@ -850,13 +879,31 @@ 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"
|
||||
BUCKET="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.tar.zst"
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
|
||||
rm -rf "$BIG_BLOCKS_DIR"; mkdir -p "$BIG_BLOCKS_DIR"
|
||||
echo "Downloading big blocks from $BUCKET..."
|
||||
$MC cat "$BUCKET" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
|
||||
|
||||
# Download and parse manifest
|
||||
echo "Downloading manifest from $MANIFEST..."
|
||||
$MC cat "$MANIFEST" > "$BIG_BLOCKS_DIR/manifest.json"
|
||||
UPLOAD_PATH=$(jq -r '.upload_path' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
COUNT=$(jq -r '.count' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
TARGET_GAS=$(jq -r '.target_gas' "$BIG_BLOCKS_DIR/manifest.json")
|
||||
echo "Manifest: count=$COUNT, target_gas=$TARGET_GAS, archive=$UPLOAD_PATH"
|
||||
|
||||
# Download and extract archive
|
||||
ARCHIVE="minio/$UPLOAD_PATH"
|
||||
echo "Downloading big blocks from $ARCHIVE..."
|
||||
$MC cat "$ARCHIVE" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
|
||||
echo "Big blocks downloaded to $BIG_BLOCKS_DIR"
|
||||
|
||||
# Verify expected directory structure
|
||||
if [ ! -d "$BIG_BLOCKS_DIR/payloads" ]; then
|
||||
echo "::error::Big blocks archive missing expected payloads/ directory"
|
||||
@@ -864,6 +911,8 @@ jobs:
|
||||
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: |
|
||||
@@ -905,7 +954,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/reth "$BENCH_WORK_DIR/baseline-1"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
|
||||
|
||||
- name: "Run benchmark: feature (1/2)"
|
||||
id: run-feature-1
|
||||
@@ -916,7 +965,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/reth "$BENCH_WORK_DIR/feature-1"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
|
||||
|
||||
- name: "Run benchmark: feature (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
@@ -928,7 +977,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/reth "$BENCH_WORK_DIR/feature-2"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
- name: "Run benchmark: baseline (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
@@ -942,7 +991,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/reth "$BENCH_WORK_DIR/baseline-2"
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
- name: Stop metrics proxy & generate Grafana URL
|
||||
id: metrics
|
||||
|
||||
2
.github/workflows/docker-test.yml
vendored
2
.github/workflows/docker-test.yml
vendored
@@ -79,4 +79,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
path: ./artifacts
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -183,6 +183,8 @@ 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 }}
|
||||
|
||||
71
.github/workflows/hive.yml
vendored
71
.github/workflows/hive.yml
vendored
@@ -5,10 +5,7 @@ name: hive
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -18,26 +15,23 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-reth-stable:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
build-reth:
|
||||
uses: ./.github/workflows/docker-test.yml
|
||||
with:
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
cargo_features: "asm-keccak"
|
||||
artifact_name: "reth-stable"
|
||||
hive_target: hive
|
||||
artifact_name: "reth"
|
||||
secrets: inherit
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on:
|
||||
group: Reth
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: Soubhik-10/hive # rm later and use ethereum/hive
|
||||
ref: master
|
||||
repository: ethereum/hive
|
||||
path: hivetests
|
||||
|
||||
- name: Get hive commit hash
|
||||
@@ -83,7 +77,6 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
storage: [stable]
|
||||
# ethereum/rpc to be deprecated:
|
||||
# https://github.com/ethereum/hive/pull/1117
|
||||
scenario:
|
||||
@@ -124,28 +117,26 @@ jobs:
|
||||
- sim: ethereum/rpc-compat
|
||||
include:
|
||||
- eth_blockNumber
|
||||
# - eth_call
|
||||
# - eth_chainId
|
||||
# - eth_createAccessList
|
||||
# - eth_estimateGas
|
||||
# - eth_feeHistory
|
||||
# - eth_getBalance
|
||||
# - eth_getBlockBy
|
||||
# - eth_getBlockTransactionCountBy
|
||||
# - eth_getCode
|
||||
# - eth_getProof
|
||||
# - eth_getStorage
|
||||
# - eth_getTransactionBy
|
||||
# - eth_getTransactionCount
|
||||
# - eth_getTransactionReceipt
|
||||
# - eth_sendRawTransaction
|
||||
# - eth_syncing
|
||||
# # debug_ rpc methods
|
||||
# - debug_
|
||||
- eth_call
|
||||
- eth_chainId
|
||||
- eth_createAccessList
|
||||
- eth_estimateGas
|
||||
- eth_feeHistory
|
||||
- eth_getBalance
|
||||
- eth_getBlockBy
|
||||
- eth_getBlockTransactionCountBy
|
||||
- eth_getCode
|
||||
- eth_getProof
|
||||
- eth_getStorage
|
||||
- eth_getTransactionBy
|
||||
- eth_getTransactionCount
|
||||
- eth_getTransactionReceipt
|
||||
- eth_sendRawTransaction
|
||||
- eth_syncing
|
||||
# debug_ rpc methods
|
||||
- debug_
|
||||
|
||||
# consume-engine
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
@@ -166,8 +157,6 @@ jobs:
|
||||
limit: .*tests/paris.*
|
||||
|
||||
# consume-rlp
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
@@ -187,9 +176,9 @@ jobs:
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/paris.*
|
||||
needs:
|
||||
- prepare-reth-stable
|
||||
- build-reth
|
||||
- prepare-hive
|
||||
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
@@ -208,7 +197,7 @@ jobs:
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: reth-${{ matrix.storage }}
|
||||
name: reth
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker images
|
||||
@@ -222,7 +211,7 @@ jobs:
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: Soubhik-10/hive
|
||||
repository: ethereum/hive
|
||||
ref: master
|
||||
path: hivetests
|
||||
|
||||
|
||||
26
.github/workflows/lint.yml
vendored
26
.github/workflows/lint.yml
vendored
@@ -237,31 +237,6 @@ 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' }}
|
||||
@@ -297,7 +272,6 @@ jobs:
|
||||
- typos
|
||||
- grafana
|
||||
- no-test-deps
|
||||
- features
|
||||
- feature-propagation
|
||||
- deny
|
||||
timeout-minutes: 30
|
||||
|
||||
61
.github/workflows/prepare-reth.yml
vendored
61
.github/workflows/prepare-reth.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Prepare Reth Image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
required: true
|
||||
type: string
|
||||
description: "Docker image tag to use"
|
||||
binary_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "reth"
|
||||
description: "Binary name to build (reth or op-reth)"
|
||||
cargo_features:
|
||||
required: false
|
||||
type: string
|
||||
default: "asm-keccak"
|
||||
description: "Cargo features to enable"
|
||||
cargo_package:
|
||||
required: false
|
||||
type: string
|
||||
description: "Optional cargo package path"
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "artifacts"
|
||||
description: "Name for the uploaded artifact"
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: mkdir artifacts
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and export reth image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: .github/scripts/hive/Dockerfile
|
||||
tags: ${{ inputs.image_tag }}
|
||||
outputs: type=docker,dest=./artifacts/reth_image.tar
|
||||
build-args: |
|
||||
CARGO_BIN=${{ inputs.binary_name }}
|
||||
MANIFEST_PATH=${{ inputs.cargo_package }}
|
||||
FEATURES=${{ inputs.cargo_features }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Upload reth image
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
549
AGENTS.md
Normal file
549
AGENTS.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
|
||||
```
|
||||
549
CLAUDE.md
549
CLAUDE.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
|
||||
```
|
||||
1169
Cargo.lock
generated
1169
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
169
Cargo.toml
169
Cargo.toml
@@ -9,6 +9,7 @@ exclude = [".github/"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"bin/reth-bb/",
|
||||
"bin/reth-bench/",
|
||||
"bin/reth/",
|
||||
"crates/storage/rpc-provider/",
|
||||
@@ -26,6 +27,7 @@ members = [
|
||||
"crates/engine/invalid-block-hooks/",
|
||||
"crates/engine/local",
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/execution-cache/",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
@@ -76,8 +78,6 @@ 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,8 +99,6 @@ 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/",
|
||||
@@ -111,7 +109,6 @@ members = [
|
||||
"crates/storage/nippy-jar/",
|
||||
"crates/storage/provider/",
|
||||
"crates/storage/storage-api/",
|
||||
"crates/storage/zstd-compressors/",
|
||||
"crates/tasks/",
|
||||
"crates/tokio-util/",
|
||||
"crates/tracing/",
|
||||
@@ -137,6 +134,7 @@ members = [
|
||||
"examples/exex-subscription",
|
||||
"examples/exex-test",
|
||||
"examples/full-contract-state",
|
||||
"examples/migrate-trie-to-packed",
|
||||
"examples/manual-p2p/",
|
||||
"examples/network-txpool/",
|
||||
"examples/network/",
|
||||
@@ -328,8 +326,8 @@ reth-cli = { path = "crates/cli/cli" }
|
||||
reth-cli-commands = { path = "crates/cli/commands" }
|
||||
reth-cli-runner = { path = "crates/cli/runner" }
|
||||
reth-cli-util = { path = "crates/cli/util" }
|
||||
reth-codecs = { path = "crates/storage/codecs" }
|
||||
reth-codecs-derive = { path = "crates/storage/codecs/derive" }
|
||||
reth-codecs = { version = "0.1.0", default-features = false }
|
||||
reth-codecs-derive = "0.1.0"
|
||||
reth-config = { path = "crates/config", default-features = false }
|
||||
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
|
||||
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
|
||||
@@ -345,6 +343,7 @@ 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" }
|
||||
@@ -396,8 +395,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
|
||||
reth-payload-primitives = { path = "crates/payload/primitives" }
|
||||
reth-payload-validator = { path = "crates/payload/validator" }
|
||||
reth-payload-util = { path = "crates/payload/util" }
|
||||
reth-primitives = { path = "crates/primitives", default-features = false, features = ["__internal"] }
|
||||
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
|
||||
reth-primitives-traits = { version = "0.1.0", default-features = false }
|
||||
reth-provider = { path = "crates/storage/provider" }
|
||||
reth-prune = { path = "crates/prune/prune" }
|
||||
reth-prune-types = { path = "crates/prune/types", default-features = false }
|
||||
@@ -413,6 +411,7 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal
|
||||
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
|
||||
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
|
||||
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
|
||||
reth-rpc-traits = { version = "0.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 }
|
||||
@@ -431,7 +430,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 = { path = "crates/storage/zstd-compressors", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.1.0", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "36.0.0", default-features = false }
|
||||
@@ -444,55 +443,51 @@ revm-database-interface = { version = "10.0.0", default-features = false }
|
||||
revm-inspectors = "0.36.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.0"
|
||||
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-sol-types = { version = "1.5.0", default-features = false }
|
||||
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.5", default-features = false }
|
||||
alloy-chains = { version = "0.2.33", default-features = false }
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.3", default-features = false, features = ["rlp"] }
|
||||
alloy-evm = { version = "0.29.2", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.0", default-features = false }
|
||||
alloy-evm = { version = "0.30.0", default-features = false }
|
||||
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
|
||||
alloy-trie = { version = "0.9.4", default-features = false }
|
||||
|
||||
alloy-hardforks = "0.4.5"
|
||||
|
||||
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 }
|
||||
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 }
|
||||
|
||||
# op
|
||||
alloy-op-hardforks = "0.4.4"
|
||||
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
|
||||
op-alloy-network = { version = "0.23.1", default-features = false }
|
||||
op-alloy-consensus = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-jsonrpsee = { version = "0.23.1", default-features = false }
|
||||
op-alloy-flz = { version = "0.13.1", default-features = false }
|
||||
op-alloy-rpc-types = { version = "0.24.0", default-features = false }
|
||||
op-alloy-rpc-types-engine = { version = "0.24.0", default-features = false }
|
||||
op-alloy-consensus = { version = "0.24.0", default-features = false }
|
||||
|
||||
# misc
|
||||
either = { version = "1.15.0", default-features = false }
|
||||
@@ -531,13 +526,13 @@ quanta = "0.12"
|
||||
paste = "1.0"
|
||||
rand = "0.9"
|
||||
rayon = "1.7"
|
||||
thread-priority = "3.0.0"
|
||||
rustc-hash = { version = "2.0", default-features = false }
|
||||
schnellru = "0.2"
|
||||
serde = { version = "1.0", default-features = false }
|
||||
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 }
|
||||
shellexpand = "3.0.0"
|
||||
shlex = "1.3"
|
||||
slotmap = "1"
|
||||
smallvec = "1"
|
||||
@@ -545,7 +540,6 @@ strum = { version = "0.27", default-features = false }
|
||||
strum_macros = "0.27"
|
||||
syn = "2.0"
|
||||
thiserror = { version = "2.0.0", default-features = false }
|
||||
thread-priority = "3.0.0"
|
||||
tar = "0.4.44"
|
||||
tracing = { version = "0.1.0", default-features = false, features = ["attributes"] }
|
||||
tracing-appender = "0.2"
|
||||
@@ -585,7 +579,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.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "stream"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream"] }
|
||||
tracing-futures = "0.2"
|
||||
tower = "0.5"
|
||||
tower-http = "0.6"
|
||||
@@ -651,6 +645,7 @@ 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"] }
|
||||
@@ -685,7 +680,6 @@ 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 }
|
||||
@@ -698,7 +692,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.5"
|
||||
tracing-logfmt = "0.3.7"
|
||||
tracing-samply = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
tracing-tracy = "0.11"
|
||||
@@ -756,68 +750,3 @@ ipnet = "2.11"
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "9bc2dba" }
|
||||
|
||||
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "3020ea8" }
|
||||
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
|
||||
# =============================================================================
|
||||
# BAL devnet patches (EIP-7778, EIP-7928 block access lists)
|
||||
# =============================================================================
|
||||
|
||||
# revm staging patches
|
||||
revm = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
op-revm = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
|
||||
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", branch = "staging" }
|
||||
|
||||
# alloy-evm bal-devnet2 patches
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet2" }
|
||||
|
||||
# alloy bal-devnet2 patches
|
||||
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-consensus-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-contract = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-eips = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-genesis = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-network = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-provider = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-serde = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-signer = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-tx-macros = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
|
||||
# op-alloy bal-devnet2 patches
|
||||
op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
|
||||
4
Makefile
4
Makefile
@@ -29,7 +29,7 @@ EF_TESTS_URL := https://github.com/ethereum/tests/archive/refs/tags/$(EF_TESTS_T
|
||||
EF_TESTS_DIR := ./testing/ef-tests/ethereum-tests
|
||||
|
||||
# The release tag of https://github.com/ethereum/execution-spec-tests to use for EEST tests
|
||||
EEST_TESTS_TAG := bal@v5.0.0
|
||||
EEST_TESTS_TAG := v4.5.0
|
||||
EEST_TESTS_URL := https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_TESTS_TAG)/fixtures_stable.tar.gz
|
||||
EEST_TESTS_DIR := ./testing/ef-tests/execution-spec-tests
|
||||
|
||||
@@ -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) jemalloc-unprefixed" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
cargo build --bin reth --features "$(FEATURES)" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
|
||||
.PHONY: build-debug
|
||||
build-debug: ## Build the reth binary into `target/debug` directory.
|
||||
|
||||
100
bin/reth-bb/Cargo.toml
Normal file
100
bin/reth-bb/Cargo.toml
Normal file
@@ -0,0 +1,100 @@
|
||||
[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"
|
||||
67
bin/reth-bb/README.md
Normal file
67
bin/reth-bb/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
570
bin/reth-bb/src/evm.rs
Normal file
570
bin/reth-bb/src/evm.rs
Normal file
@@ -0,0 +1,570 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
291
bin/reth-bb/src/evm_config.rs
Normal file
291
bin/reth-bb/src/evm_config.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
374
bin/reth-bb/src/main.rs
Normal file
374
bin/reth-bb/src/main.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,13 @@ 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
|
||||
reth-node-core.workspace = true
|
||||
@@ -26,6 +30,7 @@ reth-rpc-api.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eips.workspace = true
|
||||
alloy-json-rpc.workspace = true
|
||||
|
||||
@@ -79,39 +84,54 @@ 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
|
||||
|
||||
@@ -52,8 +52,7 @@ impl InnerTransport {
|
||||
url: Url,
|
||||
jwt: JwtSecret,
|
||||
) -> Result<(Self, Claims), AuthenticatedTransportError> {
|
||||
let mut client_builder =
|
||||
reqwest::Client::builder().tls_built_in_root_certs(url.scheme() == "https");
|
||||
let mut client_builder = reqwest::Client::builder();
|
||||
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;
|
||||
use reth_node_core::args::{BenchmarkArgs, WaitForPersistence};
|
||||
use tracing::info;
|
||||
|
||||
/// This is intended to be used by benchmarks that replay blocks from an RPC.
|
||||
@@ -33,8 +33,8 @@ pub(crate) struct BenchContext {
|
||||
pub(crate) use_reth_namespace: bool,
|
||||
/// Whether to fetch and replay RLP-encoded blocks.
|
||||
pub(crate) rlp_blocks: bool,
|
||||
/// Whether to skip waiting for persistence (pass `wait_for_persistence: false`).
|
||||
pub(crate) no_wait_for_persistence: 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,
|
||||
}
|
||||
@@ -166,8 +166,9 @@ 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_persistence = bench_args.no_wait_for_persistence;
|
||||
let no_wait_for_caches = bench_args.no_wait_for_caches;
|
||||
Ok(Self {
|
||||
auth_provider,
|
||||
@@ -177,7 +178,7 @@ impl BenchContext {
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
no_wait_for_persistence,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@ mod context;
|
||||
mod generate_big_block;
|
||||
pub(crate) mod helpers;
|
||||
pub use generate_big_block::{
|
||||
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
|
||||
compute_payload_block_hash, BigBlockPayload, RawTransaction, RpcTransactionSource,
|
||||
TransactionCollector, TransactionSource,
|
||||
};
|
||||
pub(crate) mod metrics_scraper;
|
||||
mod new_payload_fcu;
|
||||
@@ -50,16 +51,16 @@ pub enum Subcommands {
|
||||
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
|
||||
SendPayload(send_payload::Command),
|
||||
|
||||
/// Generate a large block by packing transactions from existing blocks.
|
||||
/// Generate a large block by merging consecutive blocks from an RPC.
|
||||
///
|
||||
/// This command fetches transactions from real blocks and packs them into a single
|
||||
/// block using the `testing_buildBlockV1` RPC endpoint.
|
||||
/// 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.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// `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`
|
||||
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --from-block 20000000
|
||||
/// --count 10 --output-dir ./payloads`
|
||||
GenerateBigBlock(generate_big_block::Command),
|
||||
|
||||
/// Replay pre-generated payloads from a directory.
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Command {
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
no_wait_for_persistence,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
@@ -204,7 +204,7 @@ impl Command {
|
||||
is_optimism,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
no_wait_for_persistence,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
)?;
|
||||
let start = Instant::now();
|
||||
|
||||
@@ -52,7 +52,7 @@ impl Command {
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
no_wait_for_persistence,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl Command {
|
||||
is_optimism,
|
||||
rlp,
|
||||
use_reth_namespace,
|
||||
no_wait_for_persistence,
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::{
|
||||
generate_big_block::BigBlockPayload,
|
||||
helpers::parse_duration,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
@@ -21,7 +22,9 @@ use alloy_rpc_types_engine::{
|
||||
use clap::Parser;
|
||||
use eyre::Context;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::BigBlockData;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use reth_node_core::args::WaitForPersistence;
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
@@ -57,8 +60,7 @@ pub struct Command {
|
||||
#[arg(long, value_name = "SKIP", default_value = "0")]
|
||||
skip: usize,
|
||||
|
||||
/// Deprecated: gas ramp is no longer needed. Use `--testing.skip-gas-limit-ramp-check`
|
||||
/// and `--testing.gas-limit` on the reth node instead. This flag is accepted but ignored.
|
||||
/// Deprecated: gas ramp is no longer needed. This flag is accepted but ignored.
|
||||
#[arg(long, value_name = "GAS_RAMP_DIR", hide = true)]
|
||||
gas_ramp_dir: Option<PathBuf>,
|
||||
|
||||
@@ -81,12 +83,22 @@ pub struct Command {
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
reth_new_payload: bool,
|
||||
|
||||
/// Skip waiting for in-flight persistence before processing.
|
||||
/// Control when `reth_newPayload` waits for in-flight persistence.
|
||||
///
|
||||
/// Only works with `--reth-new-payload`. When set, passes `wait_for_persistence: false`
|
||||
/// to the `reth_newPayload` endpoint.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment, requires = "reth_new_payload")]
|
||||
no_wait_for_persistence: bool,
|
||||
/// 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.
|
||||
///
|
||||
@@ -108,10 +120,12 @@ pub struct Command {
|
||||
struct LoadedPayload {
|
||||
/// The index (from filename).
|
||||
index: u64,
|
||||
/// The payload envelope.
|
||||
envelope: ExecutionPayloadEnvelopeV4,
|
||||
/// The execution data for the block.
|
||||
execution_data: ExecutionData,
|
||||
/// The block hash.
|
||||
block_hash: B256,
|
||||
/// Big block data containing environment switches and prior block hashes.
|
||||
big_block_data: BigBlockData<ExecutionData>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -160,8 +174,7 @@ impl Command {
|
||||
if self.gas_ramp_dir.is_some() {
|
||||
warn!(
|
||||
target: "reth-bench",
|
||||
"--gas-ramp-dir is deprecated and ignored. Use --testing.skip-gas-limit-ramp-check \
|
||||
and --testing.gas-limit on the reth node instead."
|
||||
"--gas-ramp-dir is deprecated and ignored."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,22 +185,33 @@ impl Command {
|
||||
}
|
||||
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;
|
||||
|
||||
let mut results = Vec::new();
|
||||
let total_benchmark_duration = Instant::now();
|
||||
|
||||
for (i, payload) in payloads.iter().enumerate() {
|
||||
let envelope = &payload.envelope;
|
||||
let execution_data = &payload.execution_data;
|
||||
let block_hash = payload.block_hash;
|
||||
let execution_payload = &envelope.envelope_inner.execution_payload;
|
||||
let inner_payload = &execution_payload.payload_inner.payload_inner;
|
||||
let v1 = execution_data.payload.as_v1();
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
@@ -208,34 +232,36 @@ impl Command {
|
||||
);
|
||||
|
||||
let (version, params) = if self.reth_new_payload {
|
||||
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 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 wait_for_persistence = self
|
||||
.wait_for_persistence
|
||||
.unwrap_or(WaitForPersistence::Never)
|
||||
.rpc_value(block_number);
|
||||
(
|
||||
None,
|
||||
serde_json::to_value((
|
||||
RethNewPayloadInput::ExecutionData(reth_data),
|
||||
self.no_wait_for_persistence.then_some(false),
|
||||
RethNewPayloadInput::ExecutionData(execution_data.clone()),
|
||||
wait_for_persistence,
|
||||
self.no_wait_for_caches.then_some(false),
|
||||
big_block_data_param,
|
||||
))?,
|
||||
)
|
||||
} else {
|
||||
let requests =
|
||||
execution_data.sidecar.requests().cloned().unwrap_or_default().to_vec();
|
||||
(
|
||||
Some(EngineApiMessageVersion::V4),
|
||||
serde_json::to_value((
|
||||
execution_payload.clone(),
|
||||
execution_data.payload.clone(),
|
||||
Vec::<B256>::new(),
|
||||
B256::ZERO,
|
||||
envelope.execution_requests.to_vec(),
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
};
|
||||
@@ -291,6 +317,10 @@ 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;
|
||||
}
|
||||
|
||||
let gas_row =
|
||||
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
|
||||
results.push((gas_row, combined_result));
|
||||
@@ -326,28 +356,42 @@ 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
|
||||
// Read directory entries — match both legacy "payload_block_*.json" and new
|
||||
// "big_block_*.json" formats
|
||||
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") &&
|
||||
e.file_name().to_string_lossy().starts_with("payload_block_")
|
||||
(name_str.starts_with("payload_block_") ||
|
||||
name_str.starts_with("big_block_"))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse filenames to get indices and sort
|
||||
// Parse filenames to get indices and sort.
|
||||
// Supports "payload_block_N.json" and "big_block_FROM_to_TO.json" naming.
|
||||
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 index from "payload_NNN.json"
|
||||
let index_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
|
||||
let index: u64 = index_str.parse().ok()?;
|
||||
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;
|
||||
};
|
||||
Some((index, e.path()))
|
||||
})
|
||||
.collect();
|
||||
@@ -365,21 +409,42 @@ 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))?;
|
||||
|
||||
let block_hash =
|
||||
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
|
||||
// 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;
|
||||
|
||||
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, envelope, block_hash });
|
||||
payloads.push(LoadedPayload { index, execution_data, block_hash, big_block_data });
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
|
||||
@@ -240,7 +240,6 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
|
||||
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
|
||||
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
|
||||
ExecutionPayload::V4(p) => config.apply_to_payload_v3(&mut p.payload_inner),
|
||||
};
|
||||
|
||||
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
|
||||
@@ -255,9 +254,6 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => p.block_hash,
|
||||
ExecutionPayload::V2(p) => p.payload_inner.block_hash,
|
||||
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
|
||||
ExecutionPayload::V4(p) => {
|
||||
p.payload_inner.payload_inner.payload_inner.block_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -266,9 +262,6 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => p.block_hash = new_hash,
|
||||
ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
|
||||
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
|
||||
ExecutionPayload::V4(p) => {
|
||||
p.payload_inner.payload_inner.payload_inner.block_hash = new_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#[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,6 +12,7 @@ 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,22 +169,25 @@ where
|
||||
///
|
||||
/// Returns `(version, versioned_params, execution_data)`.
|
||||
///
|
||||
/// When `no_wait_for_persistence` or `no_wait_for_caches` is `true` and using `reth_newPayload`,
|
||||
/// passes the corresponding `wait_for_*: false` to skip that wait.
|
||||
/// `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,
|
||||
no_wait_for_persistence: 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),
|
||||
no_wait_for_persistence.then_some(false),
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches.then_some(false),
|
||||
))?,
|
||||
));
|
||||
@@ -207,7 +211,7 @@ pub(crate) fn block_to_new_payload(
|
||||
None,
|
||||
serde_json::to_value((
|
||||
RethNewPayloadInput::ExecutionData(execution_data),
|
||||
no_wait_for_persistence.then_some(false),
|
||||
wait_for_persistence,
|
||||
no_wait_for_caches.then_some(false),
|
||||
))?,
|
||||
))
|
||||
@@ -230,20 +234,6 @@ pub(crate) fn payload_to_new_payload(
|
||||
let execution_data = ExecutionData { payload: payload.clone(), sidecar: sidecar.clone() };
|
||||
|
||||
let (version, params) = match payload {
|
||||
ExecutionPayload::V4(payload) => {
|
||||
let cancun = sidecar.cancun().unwrap();
|
||||
let prague = sidecar.prague().unwrap();
|
||||
let requests = prague.requests.requests_hash();
|
||||
(
|
||||
EngineApiMessageVersion::V5,
|
||||
serde_json::to_value((
|
||||
payload,
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
}
|
||||
ExecutionPayload::V3(payload) => {
|
||||
let cancun = sidecar.cancun().unwrap();
|
||||
|
||||
@@ -401,12 +391,9 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
|
||||
forkchoice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
// FCU V3 is used for Cancun, Prague, and Amsterdam (there is no FCU V4-V6)
|
||||
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
|
||||
match message_version {
|
||||
EngineApiMessageVersion::V3 |
|
||||
EngineApiMessageVersion::V4 |
|
||||
EngineApiMessageVersion::V5 |
|
||||
EngineApiMessageVersion::V6 => {
|
||||
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
|
||||
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
|
||||
}
|
||||
EngineApiMessageVersion::V2 => {
|
||||
|
||||
@@ -70,7 +70,7 @@ aquamarine.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-node-bindings = "1.5.2"
|
||||
alloy-node-bindings = "1.6.3"
|
||||
alloy-provider = { workspace = true, features = ["reqwest"] }
|
||||
alloy-rpc-types-eth.workspace = true
|
||||
backon.workspace = true
|
||||
@@ -127,7 +127,7 @@ jemalloc = [
|
||||
"reth-provider/jemalloc",
|
||||
]
|
||||
jemalloc-prof = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-prof",
|
||||
"reth-node-metrics/jemalloc-prof",
|
||||
@@ -136,12 +136,6 @@ 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,7 +22,6 @@
|
||||
//! 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,8 +3,12 @@
|
||||
#[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 = "_rjem_malloc_conf")]
|
||||
#[unsafe(export_name = "malloc_conf")]
|
||||
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
@@ -28,7 +28,7 @@ use alloy_consensus::{
|
||||
};
|
||||
use alloy_eips::{
|
||||
eip1559::INITIAL_BASE_FEE, eip7685::EMPTY_REQUESTS_HASH, eip7840::BlobParams,
|
||||
eip7892::BlobScheduleBlobParams, eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
|
||||
eip7892::BlobScheduleBlobParams,
|
||||
};
|
||||
use alloy_genesis::{ChainConfig, Genesis};
|
||||
use alloy_primitives::{address, b256, Address, BlockNumber, B256, U256};
|
||||
@@ -76,18 +76,6 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(EMPTY_REQUESTS_HASH);
|
||||
|
||||
// If Amsterdam is activated at genesis we set block access list hash to empty hash.
|
||||
let block_access_list_hash = hardforks
|
||||
.fork(EthereumHardfork::Amsterdam)
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(EMPTY_BLOCK_ACCESS_LIST_HASH);
|
||||
|
||||
// If Amsterdam is activated at genesis we set slot number to 0.
|
||||
let slot_number = hardforks
|
||||
.fork(EthereumHardfork::Amsterdam)
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(0);
|
||||
|
||||
Header {
|
||||
number: genesis.number.unwrap_or_default(),
|
||||
parent_hash: genesis.parent_hash.unwrap_or_default(),
|
||||
@@ -105,8 +93,6 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
|
||||
blob_gas_used,
|
||||
excess_blob_gas,
|
||||
requests_hash,
|
||||
block_access_list_hash,
|
||||
slot_number,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -312,7 +298,6 @@ pub fn create_chain_config(
|
||||
cancun_time: timestamp(EthereumHardfork::Cancun),
|
||||
prague_time: timestamp(EthereumHardfork::Prague),
|
||||
osaka_time: timestamp(EthereumHardfork::Osaka),
|
||||
amsterdam_time: timestamp(EthereumHardfork::Amsterdam),
|
||||
bpo1_time: timestamp(EthereumHardfork::Bpo1),
|
||||
bpo2_time: timestamp(EthereumHardfork::Bpo2),
|
||||
bpo3_time: timestamp(EthereumHardfork::Bpo3),
|
||||
@@ -895,7 +880,6 @@ impl From<Genesis> for ChainSpec {
|
||||
(EthereumHardfork::Cancun.boxed(), genesis.config.cancun_time),
|
||||
(EthereumHardfork::Prague.boxed(), genesis.config.prague_time),
|
||||
(EthereumHardfork::Osaka.boxed(), genesis.config.osaka_time),
|
||||
(EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
|
||||
(EthereumHardfork::Bpo1.boxed(), genesis.config.bpo1_time),
|
||||
(EthereumHardfork::Bpo2.boxed(), genesis.config.bpo2_time),
|
||||
(EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
|
||||
@@ -1207,19 +1191,6 @@ impl ChainSpecBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable Amsterdam at genesis.
|
||||
pub fn amsterdam_activated(mut self) -> Self {
|
||||
self = self.osaka_activated();
|
||||
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(0));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable Amsterdam at the given timestamp.
|
||||
pub fn with_amsterdam_at(mut self, timestamp: u64) -> Self {
|
||||
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(timestamp));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the resulting [`ChainSpec`].
|
||||
///
|
||||
/// # Panics
|
||||
|
||||
@@ -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
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
url.workspace = true
|
||||
metrics.workspace = true
|
||||
blake3.workspace = true
|
||||
|
||||
@@ -75,10 +75,15 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
|
||||
impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
/// Returns the storage settings for new database initialization.
|
||||
///
|
||||
/// Always returns [`StorageSettings::v2()`] — v2 is the default for all new
|
||||
/// databases. Existing databases use the settings persisted in their metadata.
|
||||
/// Determined by the `--storage.v2` flag (defaults to `true`).
|
||||
/// Existing databases retain whatever settings are persisted in their
|
||||
/// metadata (checked during genesis init).
|
||||
pub fn storage_settings(&self) -> StorageSettings {
|
||||
StorageSettings::v2()
|
||||
if self.storage.v2 {
|
||||
StorageSettings::v2()
|
||||
} else {
|
||||
StorageSettings::v1()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes environment according to [`AccessRights`] and returns an instance of
|
||||
|
||||
@@ -8,8 +8,9 @@ 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, time::Instant};
|
||||
use std::hash::Hasher;
|
||||
use tracing::info;
|
||||
|
||||
/// RocksDB tables that can be checksummed.
|
||||
|
||||
@@ -22,6 +22,7 @@ mod settings;
|
||||
mod stage_checkpoints;
|
||||
mod state;
|
||||
mod static_file_header;
|
||||
mod static_files;
|
||||
mod stats;
|
||||
/// DB List TUI
|
||||
mod tui;
|
||||
@@ -63,6 +64,8 @@ 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
|
||||
@@ -188,6 +191,11 @@ 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),
|
||||
|
||||
31
crates/cli/commands/src/db/static_files/mod.rs
Normal file
31
crates/cli/commands/src/db/static_files/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
878
crates/cli/commands/src/db/static_files/split.rs
Normal file
878
crates/cli/commands/src/db/static_files/split.rs
Normal file
@@ -0,0 +1,878 @@
|
||||
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,9 +3,10 @@ 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, time::Instant};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Generate modular chunk archives and a snapshot manifest from a source datadir.
|
||||
|
||||
@@ -84,6 +84,10 @@ 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>,
|
||||
}
|
||||
@@ -108,6 +112,7 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -121,10 +126,11 @@ impl DownloadDefaults {
|
||||
return custom_help.clone();
|
||||
}
|
||||
|
||||
let mut help = String::from(
|
||||
let mut help = format!(
|
||||
"Specify a snapshot URL or let the command propose a default one.\n\n\
|
||||
Browse available snapshots at https://snapshots.reth.rs\n\
|
||||
Browse available snapshots at {}\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 {
|
||||
@@ -169,6 +175,12 @@ 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());
|
||||
@@ -191,7 +203,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 at <https://snapshots.reth.rs> or use --list-snapshots.
|
||||
/// Browse available snapshots with --list-snapshots.
|
||||
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
|
||||
url: Option<String>,
|
||||
|
||||
@@ -261,7 +273,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
|
||||
#[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
|
||||
download_concurrency: usize,
|
||||
|
||||
/// List available snapshots from snapshots.reth.rs and exit.
|
||||
/// List available snapshots and exit.
|
||||
///
|
||||
/// Queries the snapshots API and prints all available snapshots for the selected chain,
|
||||
/// including block number, size, and manifest URL.
|
||||
@@ -1328,7 +1340,17 @@ fn streaming_download_and_extract(
|
||||
let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
last_error = Some(e.into());
|
||||
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);
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
@@ -1355,6 +1377,15 @@ 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));
|
||||
@@ -1520,6 +1551,7 @@ 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);
|
||||
|
||||
@@ -1527,13 +1559,31 @@ 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 (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 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 _ = 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,
|
||||
@@ -1553,6 +1603,13 @@ 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,
|
||||
@@ -1608,10 +1665,11 @@ fn file_blake3_hex(path: &Path) -> Result<String> {
|
||||
|
||||
/// Discovers the latest snapshot manifest URL for the given chain from the snapshots API.
|
||||
///
|
||||
/// Queries `snapshots.reth.rs/api/snapshots` and returns the manifest URL for the most
|
||||
/// Queries the configured snapshot API 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 api_url = RETH_SNAPSHOTS_API_URL;
|
||||
let defaults = DownloadDefaults::get_global();
|
||||
let api_url = &*defaults.snapshot_api_url;
|
||||
|
||||
info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
|
||||
|
||||
@@ -1624,8 +1682,9 @@ 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- https://snapshots.reth.rs\n\n\
|
||||
Use --list to see all available snapshots."
|
||||
\t- {}\n\n\
|
||||
Use --list to see all available snapshots.",
|
||||
api_url.trim_end_matches("/api/snapshots"),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -1656,7 +1715,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry from the `snapshots.reth.rs/api/snapshots` listing.
|
||||
/// An entry from the snapshot discovery API listing.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SnapshotApiEntry {
|
||||
@@ -1681,7 +1740,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 = RETH_SNAPSHOTS_API_URL;
|
||||
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
|
||||
|
||||
let entries: Vec<SnapshotApiEntry> = Client::new()
|
||||
.get(api_url)
|
||||
@@ -1699,7 +1758,11 @@ 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();
|
||||
|
||||
println!("Available snapshots for chain {chain_id} (https://snapshots.reth.rs):\n");
|
||||
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
|
||||
println!(
|
||||
"Available snapshots for chain {chain_id} ({}):\n",
|
||||
api_url.trim_end_matches("/api/snapshots"),
|
||||
);
|
||||
println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
|
||||
println!("{}", "-".repeat(100));
|
||||
|
||||
@@ -1738,14 +1801,18 @@ 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\
|
||||
\t- https://snapshots.reth.rs\n\
|
||||
\t- https://publicnode.com/snapshots"
|
||||
Available snapshot sources:\n{sources}"
|
||||
)
|
||||
})?;
|
||||
Ok(response.json().await?)
|
||||
|
||||
@@ -178,6 +178,8 @@ where
|
||||
ext,
|
||||
} = self;
|
||||
|
||||
engine.validate()?;
|
||||
|
||||
// set up node config
|
||||
let mut node_config = NodeConfig {
|
||||
datadir,
|
||||
|
||||
@@ -179,10 +179,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
}
|
||||
};
|
||||
|
||||
let bal= executor.take_bal();
|
||||
|
||||
if let Err(err) = consensus
|
||||
.validate_block_post_execution(&block, &result, None, bal, true)
|
||||
.validate_block_post_execution(&block, &result, None)
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to validate block {} {}",
|
||||
|
||||
@@ -6,7 +6,7 @@ use reth_db_api::{
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_node_api::HeaderTy;
|
||||
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: NodeTypesWithDB>(
|
||||
fn import_tables_with_range<N: ProviderNodeTypes>(
|
||||
output_db: &DatabaseEnv,
|
||||
db_tool: &DbTool<N>,
|
||||
from: u64,
|
||||
@@ -74,7 +74,7 @@ fn import_tables_with_range<N: NodeTypesWithDB>(
|
||||
)
|
||||
})??;
|
||||
output_db.update(|tx| {
|
||||
tx.import_table_with_range::<tables::Headers, _>(
|
||||
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from),
|
||||
to,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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},
|
||||
@@ -41,7 +42,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, _>(
|
||||
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
|
||||
&db_tool.provider_factory.db_ref().tx()?,
|
||||
Some(from),
|
||||
to,
|
||||
|
||||
@@ -28,6 +28,7 @@ use reth_node_metrics::{
|
||||
server::{MetricServer, MetricServerConfig},
|
||||
version::VersionInfo,
|
||||
};
|
||||
use reth_primitives_traits::FastInstant as Instant;
|
||||
use reth_provider::{
|
||||
ChainSpecProvider, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
|
||||
StageCheckpointWriter,
|
||||
@@ -40,7 +41,7 @@ use reth_stages::{
|
||||
},
|
||||
ExecInput, ExecOutput, ExecutionStageThresholds, Stage, StageExt, UnwindInput, UnwindOutput,
|
||||
};
|
||||
use std::{any::Any, net::SocketAddr, sync::Arc, time::Instant};
|
||||
use std::{any::Any, net::SocketAddr, sync::Arc};
|
||||
use tokio::sync::watch;
|
||||
use tracing::*;
|
||||
|
||||
|
||||
@@ -33,19 +33,21 @@ 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"]
|
||||
jemalloc = [
|
||||
"dep:tikv-jemallocator",
|
||||
"dep:tikv-jemalloc-sys",
|
||||
"tikv-jemallocator?/override_allocator_on_supported_platforms",
|
||||
]
|
||||
|
||||
# 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,6 +15,11 @@ 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,6 +10,8 @@
|
||||
|
||||
#[cfg(feature = "tracy-allocator")]
|
||||
use reth_tracing as _;
|
||||
#[cfg(feature = "tracy-allocator")]
|
||||
use tracy_client as _;
|
||||
|
||||
pub mod allocator;
|
||||
pub mod cancellation;
|
||||
|
||||
@@ -87,27 +87,6 @@ pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), Conse
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that Amsterdam header fields are present in the block.
|
||||
///
|
||||
/// This checks that the `block_access_list_hash` and `slot_number` are set in the header,
|
||||
/// as required post-Amsterdam.
|
||||
///
|
||||
/// See [EIP-7928]: Block-level Access Lists
|
||||
/// See [EIP-7778]: Slot Number in Block Header
|
||||
///
|
||||
/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
|
||||
/// [EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778
|
||||
#[inline]
|
||||
pub fn validate_amsterdam_header_fields<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
|
||||
if header.block_access_list_hash().is_none() {
|
||||
return Err(ConsensusError::BlockAccessListHashMissing);
|
||||
}
|
||||
if header.slot_number().is_none() {
|
||||
return Err(ConsensusError::SlotNumberMissing);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures the block response data matches the header.
|
||||
///
|
||||
/// This ensures the body response items match the header's hashes:
|
||||
|
||||
@@ -18,7 +18,6 @@ reth-primitives-traits.workspace = true
|
||||
# ethereum
|
||||
alloy-primitives.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eip7928.workspace = true
|
||||
|
||||
# misc
|
||||
auto_impl.workspace = true
|
||||
@@ -30,8 +29,10 @@ std = [
|
||||
"reth-primitives-traits/std",
|
||||
"alloy-primitives/std",
|
||||
"alloy-consensus/std",
|
||||
"alloy-eip7928/std",
|
||||
"reth-primitives-traits/std",
|
||||
"reth-execution-types/std",
|
||||
"thiserror/std",
|
||||
]
|
||||
test-utils = ["reth-primitives-traits/test-utils"]
|
||||
test-utils = [
|
||||
"reth-primitives-traits/test-utils",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
//! 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",
|
||||
@@ -13,7 +32,6 @@ extern crate alloc;
|
||||
|
||||
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
|
||||
use core::error::Error;
|
||||
|
||||
@@ -55,17 +73,12 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
|
||||
/// If `receipt_root_bloom` is provided, the implementation should use the pre-computed
|
||||
/// receipt root and logs bloom instead of computing them from the receipts.
|
||||
///
|
||||
/// If `allow_bal_check` is enabled, we calculate the bal hash and match with the header bal
|
||||
/// hash. We don't do by default because for payload validation, we do the same bal check
|
||||
///
|
||||
/// Note: validating blocks does not include other validations of the Consensus
|
||||
fn validate_block_post_execution(
|
||||
&self,
|
||||
block: &RecoveredBlock<N::Block>,
|
||||
result: &BlockExecutionResult<N::Receipt>,
|
||||
receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
block_access_list: Option<BlockAccessList>,
|
||||
allow_bal_check: bool,
|
||||
) -> Result<(), ConsensusError>;
|
||||
}
|
||||
|
||||
@@ -337,30 +350,6 @@ pub enum ConsensusError {
|
||||
#[error("unexpected parent beacon block root")]
|
||||
ParentBeaconBlockRootUnexpected,
|
||||
|
||||
/// Error when the block access list hash is missing.
|
||||
#[error("missing block access list hash")]
|
||||
BlockAccessListHashMissing,
|
||||
|
||||
/// Error when an unexpected block access list hash is encountered.
|
||||
#[error("unexpected block access list hash")]
|
||||
BlockAccessListHashUnexpected,
|
||||
|
||||
/// Error when an unexpected block access list cost is encountered.
|
||||
#[error("block access list cost exceeds gas limit")]
|
||||
BlockAccessListCostMoreThanGasLimit,
|
||||
|
||||
/// Error when the block access list hash doesn't match the expected value.
|
||||
#[error("block access list hash mismatch: {0}")]
|
||||
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
|
||||
|
||||
/// Error when the slot number is missing.
|
||||
#[error("missing slot number")]
|
||||
SlotNumberMissing,
|
||||
|
||||
/// Error when an unexpected slot number is encountered.
|
||||
#[error("unexpected slot number")]
|
||||
SlotNumberUnexpected,
|
||||
|
||||
/// Error when blob gas used exceeds the maximum allowed.
|
||||
#[error("blob gas used {blob_gas_used} exceeds maximum allowance {max_blob_gas_per_block}")]
|
||||
BlobGasUsedExceedsMaxBlobGasPerBlock {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
|
||||
use alloc::sync::Arc;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
|
||||
|
||||
@@ -78,8 +77,6 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
|
||||
_block: &RecoveredBlock<N::Block>,
|
||||
_result: &BlockExecutionResult<N::Receipt>,
|
||||
_receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
_block_access_list: Option<BlockAccessList>,
|
||||
_allow_bal_check: bool,
|
||||
) -> Result<(), ConsensusError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
|
||||
@@ -53,8 +52,6 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
|
||||
_block: &RecoveredBlock<N::Block>,
|
||||
_result: &BlockExecutionResult<N::Receipt>,
|
||||
_receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
_block_access_list: Option<BlockAccessList>,
|
||||
_allow_bal_check: bool,
|
||||
) -> Result<(), ConsensusError> {
|
||||
if self.fail_validation() {
|
||||
Err(ConsensusError::BaseFeeMissing)
|
||||
|
||||
@@ -29,7 +29,7 @@ auto_impl.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
eyre.workspace = true
|
||||
reqwest.workspace = true
|
||||
reqwest = { workspace = true, features = ["query"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use alloy_consensus::Sealable;
|
||||
use alloy_primitives::B256;
|
||||
use reth_node_api::{
|
||||
BuiltPayload, ConsensusEngineHandle, EngineApiMessageVersion, ExecutionPayload, NodePrimitives,
|
||||
PayloadTypes,
|
||||
BuiltPayload, ConsensusEngineHandle, ExecutionPayload, NodePrimitives, PayloadTypes,
|
||||
};
|
||||
use reth_primitives_traits::{Block, SealedBlock};
|
||||
use reth_tracing::tracing::warn;
|
||||
@@ -131,10 +130,7 @@ where
|
||||
safe_block_hash,
|
||||
finalized_block_hash,
|
||||
};
|
||||
let _ = self
|
||||
.engine_handle
|
||||
.fork_choice_updated(state, None, EngineApiMessageVersion::V3)
|
||||
.await;
|
||||
let _ = self.engine_handle.fork_choice_updated(state, None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,16 @@ where
|
||||
target: "consensus::debug-client",
|
||||
%err,
|
||||
url=%self.url,
|
||||
"Failed to subscribe to blocks",
|
||||
"Failed to subscribe to blocks, retrying in 5s",
|
||||
);
|
||||
}) else {
|
||||
return
|
||||
// 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
|
||||
};
|
||||
|
||||
while let Some(res) = stream.next().await {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! 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};
|
||||
@@ -48,7 +49,11 @@ 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>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)>
|
||||
where
|
||||
N: NodeBuilderHelper,
|
||||
@@ -65,7 +70,11 @@ 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>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
) -> eyre::Result<(
|
||||
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
|
||||
Wallet,
|
||||
@@ -90,7 +99,11 @@ 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>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
|
||||
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
connect_nodes: bool,
|
||||
) -> eyre::Result<(
|
||||
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
|
||||
@@ -133,11 +146,8 @@ pub type NodeHelperType<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<
|
||||
pub trait NodeBuilderHelper
|
||||
where
|
||||
Self: Default
|
||||
+ NodeTypesForProvider<
|
||||
Payload: PayloadTypes<
|
||||
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
|
||||
>,
|
||||
> + Node<
|
||||
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
|
||||
+ Node<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
ComponentsBuilder: NodeComponentsBuilder<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
@@ -158,11 +168,8 @@ where
|
||||
|
||||
impl<T> NodeBuilderHelper for T where
|
||||
Self: Default
|
||||
+ NodeTypesForProvider<
|
||||
Payload: PayloadTypes<
|
||||
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
|
||||
>,
|
||||
> + Node<
|
||||
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
|
||||
+ Node<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
ComponentsBuilder: NodeComponentsBuilder<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
|
||||
@@ -9,13 +9,10 @@ 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, EngineApiMessageVersion, FullNodeComponents, PayloadTypes,
|
||||
PrimitivesTy,
|
||||
};
|
||||
use reth_node_api::{Block, BlockBody, BlockTy, FullNodeComponents, PayloadTypes, PrimitivesTy};
|
||||
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
|
||||
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
|
||||
use reth_payload_primitives::BuiltPayload;
|
||||
use reth_provider::{
|
||||
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
|
||||
HeaderProvider, StageCheckpointReader,
|
||||
@@ -58,7 +55,7 @@ where
|
||||
/// Creates a new test node
|
||||
pub async fn new(
|
||||
node: FullNode<Node, AddOns>,
|
||||
attributes_generator: impl Fn(u64) -> Payload::PayloadBuilderAttributes + Send + Sync + 'static,
|
||||
attributes_generator: impl Fn(u64) -> Payload::PayloadAttributes + Send + Sync + 'static,
|
||||
) -> eyre::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: node.clone(),
|
||||
@@ -106,17 +103,50 @@ 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> {
|
||||
// trigger new payload building draining the pool
|
||||
let eth_attr = self.payload.new_payload().await.unwrap();
|
||||
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();
|
||||
// first event is the payload attributes
|
||||
self.payload.expect_attr_event(eth_attr.clone()).await?;
|
||||
self.payload.expect_attr_event(eth_attr).await?;
|
||||
// wait for the payload builder to have finished building
|
||||
self.payload.wait_for_built_payload(eth_attr.payload_id()).await;
|
||||
self.payload.wait_for_built_payload(payload_id).await;
|
||||
// ensure we're also receiving the built payload as event
|
||||
Ok(self.payload.expect_built_payload().await?)
|
||||
}
|
||||
@@ -265,7 +295,6 @@ where
|
||||
finalized_block_hash: current_head,
|
||||
},
|
||||
None,
|
||||
EngineApiMessageVersion::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use futures_util::StreamExt;
|
||||
use reth_node_api::{BlockBody, PayloadKind};
|
||||
use reth_node_api::{BlockBody, PayloadAttributes, PayloadKind};
|
||||
use reth_payload_builder::{PayloadBuilderHandle, PayloadId};
|
||||
use reth_payload_builder_primitives::Events;
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, PayloadTypes};
|
||||
use reth_payload_primitives::{BuiltPayload, 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::PayloadBuilderAttributes + Send + Sync>,
|
||||
attributes_generator: Box<dyn Fn(u64) -> T::PayloadAttributes + 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::PayloadBuilderAttributes + Send + Sync + 'static,
|
||||
attributes_generator: impl Fn(u64) -> T::PayloadAttributes + Send + Sync + 'static,
|
||||
) -> eyre::Result<Self> {
|
||||
let payload_events = payload_builder.subscribe().await?;
|
||||
let payload_event_stream = payload_events.into_stream();
|
||||
@@ -32,19 +32,14 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new payload job from static attributes
|
||||
pub async fn new_payload(&mut self) -> eyre::Result<T::PayloadBuilderAttributes> {
|
||||
/// Generates the next payload attributes
|
||||
pub fn next_attributes(&mut self) -> T::PayloadAttributes {
|
||||
self.timestamp += 1;
|
||||
let attributes = (self.attributes_generator)(self.timestamp);
|
||||
self.payload_builder.send_new_payload(attributes.clone()).await.unwrap()?;
|
||||
Ok(attributes)
|
||||
(self.attributes_generator)(self.timestamp)
|
||||
}
|
||||
|
||||
/// Asserts that the next event is a payload attributes event
|
||||
pub async fn expect_attr_event(
|
||||
&mut self,
|
||||
attrs: T::PayloadBuilderAttributes,
|
||||
) -> eyre::Result<()> {
|
||||
pub async fn expect_attr_event(&mut self, attrs: T::PayloadAttributes) -> 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>::PayloadBuilderAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ 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>::PayloadBuilderAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ 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>::PayloadBuilderAttributes
|
||||
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 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;
|
||||
@@ -59,11 +60,7 @@ pub async fn setup_engine_with_chain_import(
|
||||
is_dev: bool,
|
||||
tree_config: TreeConfig,
|
||||
rlp_path: &Path,
|
||||
attributes_generator: impl Fn(u64) -> reth_payload_builder::EthPayloadBuilderAttributes
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
+ 'static,
|
||||
attributes_generator: impl Fn(u64) -> PayloadAttributes + Send + Sync + Copy + 'static,
|
||||
) -> eyre::Result<ChainImportResult> {
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
|
||||
@@ -273,10 +270,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,
|
||||
@@ -569,7 +566,7 @@ mod tests {
|
||||
false,
|
||||
TreeConfig::default(),
|
||||
&rlp_path,
|
||||
|_| EthPayloadBuilderAttributes::default(),
|
||||
|_| PayloadAttributes::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to setup nodes with chain import");
|
||||
|
||||
@@ -55,8 +55,6 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
|
||||
excess_blob_gas: None,
|
||||
parent_beacon_block_root: None,
|
||||
requests_hash: None,
|
||||
block_access_list_hash: None,
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
// Set required fields based on chain spec
|
||||
|
||||
@@ -227,7 +227,6 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::random(),
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
env.active_node_state_mut()?
|
||||
@@ -300,7 +299,6 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::random(),
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
let fresh_fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
|
||||
|
||||
@@ -10,7 +10,6 @@ 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::{
|
||||
@@ -264,16 +263,12 @@ where
|
||||
let chain_spec =
|
||||
self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
|
||||
|
||||
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),
|
||||
slot_number: None,
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
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),
|
||||
};
|
||||
|
||||
crate::setup_import::setup_engine_with_chain_import(
|
||||
@@ -289,24 +284,19 @@ 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>::PayloadBuilderAttributes
|
||||
+ Copy
|
||||
+ use<N, I>
|
||||
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes + Copy + use<N, I>
|
||||
where
|
||||
N: NodeBuilderHelper<Payload = I>,
|
||||
{
|
||||
move |timestamp| {
|
||||
let attributes = PayloadAttributes {
|
||||
PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
<<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes::from(
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
|
||||
)
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -161,7 +160,6 @@ async fn test_testsuite_assert_mine_block() -> Result<()> {
|
||||
suggested_fee_recipient: Address::random(),
|
||||
withdrawals: None,
|
||||
parent_beacon_block_root: None,
|
||||
slot_number: None,
|
||||
},
|
||||
));
|
||||
|
||||
@@ -372,7 +370,7 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
|
||||
);
|
||||
|
||||
let (nodes, _wallet) = E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
|
||||
EthPayloadBuilderAttributes::default()
|
||||
PayloadAttributes::default()
|
||||
})
|
||||
.with_tree_config_modifier(|config| {
|
||||
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 eyre::Result;
|
||||
use jsonrpsee::core::client::ClientT;
|
||||
@@ -10,7 +11,6 @@ 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,16 +83,14 @@ fn test_chain_spec() -> Arc<ChainSpec> {
|
||||
}
|
||||
|
||||
/// Returns test payload attributes for the given timestamp.
|
||||
fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
|
||||
let attributes = alloy_rpc_types_engine::PayloadAttributes {
|
||||
const fn test_attributes_generator(timestamp: u64) -> PayloadAttributes {
|
||||
PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Smoke test: node boots with `RocksDB` routing enabled.
|
||||
|
||||
39
crates/engine/execution-cache/Cargo.toml
Normal file
39
crates/engine/execution-cache/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[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",
|
||||
]
|
||||
@@ -6,7 +6,7 @@ use alloy_primitives::{
|
||||
use fixed_cache::{AnyRef, CacheConfig, Stats, StatsHandler};
|
||||
use metrics::{Counter, Gauge, Histogram};
|
||||
use parking_lot::Once;
|
||||
use reth_errors::{ProviderError, ProviderResult};
|
||||
use reth_errors::ProviderResult;
|
||||
use reth_metrics::Metrics;
|
||||
use reth_primitives_traits::{Account, Bytecode};
|
||||
use reth_provider::{
|
||||
@@ -87,7 +87,7 @@ type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, Epoc
|
||||
/// The const generic `PREWARM` controls whether every cache miss is populated. This is only
|
||||
/// relevant for pre-warm transaction execution with the intention to pre-populate the cache with
|
||||
/// data for regular block execution. During regular block execution the cache doesn't need to be
|
||||
/// populated because the actual EVM database [`State`](revm::database::State) also caches
|
||||
/// populated because the actual EVM database `State` also caches
|
||||
/// internally during block execution and the cache is then updated after the block with the entire
|
||||
/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
|
||||
/// also [`ExecutionCache::insert_state`].
|
||||
@@ -230,7 +230,7 @@ impl CachedStateMetrics {
|
||||
}
|
||||
|
||||
/// Records a new execution cache creation with its duration.
|
||||
pub(crate) fn record_cache_creation(&self, duration: Duration) {
|
||||
pub fn record_cache_creation(&self, duration: Duration) {
|
||||
self.execution_cache_created_total.increment(1);
|
||||
self.execution_cache_creation_duration_seconds.record(duration.as_secs_f64());
|
||||
}
|
||||
@@ -254,51 +254,63 @@ pub struct CacheStats {
|
||||
}
|
||||
|
||||
impl CacheStats {
|
||||
pub(crate) fn record_account_hit(&self) {
|
||||
/// Records an account cache hit.
|
||||
pub fn record_account_hit(&self) {
|
||||
self.account_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_account_miss(&self) {
|
||||
/// Records an account cache miss.
|
||||
pub fn record_account_miss(&self) {
|
||||
self.account_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn account_hits(&self) -> usize {
|
||||
/// Returns the number of account cache hits.
|
||||
pub fn account_hits(&self) -> usize {
|
||||
self.account_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn account_misses(&self) -> usize {
|
||||
/// Returns the number of account cache misses.
|
||||
pub fn account_misses(&self) -> usize {
|
||||
self.account_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn record_storage_hit(&self) {
|
||||
/// Records a storage cache hit.
|
||||
pub fn record_storage_hit(&self) {
|
||||
self.storage_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_storage_miss(&self) {
|
||||
/// Records a storage cache miss.
|
||||
pub fn record_storage_miss(&self) {
|
||||
self.storage_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn storage_hits(&self) -> usize {
|
||||
/// Returns the number of storage cache hits.
|
||||
pub fn storage_hits(&self) -> usize {
|
||||
self.storage_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn storage_misses(&self) -> usize {
|
||||
/// Returns the number of storage cache misses.
|
||||
pub fn storage_misses(&self) -> usize {
|
||||
self.storage_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn record_code_hit(&self) {
|
||||
/// Records a code cache hit.
|
||||
pub fn record_code_hit(&self) {
|
||||
self.code_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_code_miss(&self) {
|
||||
/// Records a code cache miss.
|
||||
pub fn record_code_miss(&self) {
|
||||
self.code_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn code_hits(&self) -> usize {
|
||||
/// Returns the number of code cache hits.
|
||||
pub fn code_hits(&self) -> usize {
|
||||
self.code_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn code_misses(&self) -> usize {
|
||||
/// Returns the number of code cache misses.
|
||||
pub fn code_misses(&self) -> usize {
|
||||
self.code_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -318,7 +330,7 @@ impl CacheStats {
|
||||
///
|
||||
/// Collisions (evicting a different key) don't change size since they replace an existing entry.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CacheStatsHandler {
|
||||
pub struct CacheStatsHandler {
|
||||
collisions: AtomicU64,
|
||||
size: AtomicUsize,
|
||||
capacity: usize,
|
||||
@@ -326,42 +338,42 @@ pub(crate) struct CacheStatsHandler {
|
||||
|
||||
impl CacheStatsHandler {
|
||||
/// Creates a new stats handler with all counters initialized to zero.
|
||||
pub(crate) const fn new(capacity: usize) -> Self {
|
||||
pub const fn new(capacity: usize) -> Self {
|
||||
Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
|
||||
}
|
||||
|
||||
/// Returns the number of cache collisions.
|
||||
pub(crate) fn collisions(&self) -> u64 {
|
||||
pub fn collisions(&self) -> u64 {
|
||||
self.collisions.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns the current size (number of entries).
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
pub fn size(&self) -> usize {
|
||||
self.size.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns the capacity (maximum number of entries).
|
||||
pub(crate) const fn capacity(&self) -> usize {
|
||||
pub const fn capacity(&self) -> usize {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
/// Increments the size counter. Called on cache insert.
|
||||
pub(crate) fn increment_size(&self) {
|
||||
pub fn increment_size(&self) {
|
||||
let _ = self.size.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Decrements the size counter. Called on cache remove.
|
||||
pub(crate) fn decrement_size(&self) {
|
||||
pub fn decrement_size(&self) {
|
||||
let _ = self.size.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Resets size to zero. Called on cache clear.
|
||||
pub(crate) fn reset_size(&self) {
|
||||
pub fn reset_size(&self) {
|
||||
self.size.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Resets collision counter to zero (but not size).
|
||||
pub(crate) fn reset_stats(&self) {
|
||||
pub fn reset_stats(&self) {
|
||||
self.collisions.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -466,55 +478,6 @@ impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvide
|
||||
self.state_provider.storage(account, storage_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn storage_range(
|
||||
&self,
|
||||
account: Address,
|
||||
keys: &[StorageKey],
|
||||
) -> ProviderResult<Vec<(StorageKey, StorageValue)>> {
|
||||
let mut uncached_keys = Vec::new();
|
||||
let mut result = Vec::with_capacity(keys.len());
|
||||
|
||||
for &key in keys {
|
||||
if let Some(value) = self.caches.get_storage(account, key) {
|
||||
if !value.is_zero() {
|
||||
result.push((key, value));
|
||||
}
|
||||
} else {
|
||||
uncached_keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-fetch all uncached keys from the inner provider
|
||||
if !uncached_keys.is_empty() {
|
||||
let mut fetched = self.state_provider.storage_range(account, &uncached_keys)?;
|
||||
// Sort by raw key to align with uncached_keys for the merge-join below.
|
||||
// The inner provider may return results in a different order (e.g. hashed state
|
||||
// iterates by hashed slot).
|
||||
fetched.sort_unstable_by_key(|(k, _)| *k);
|
||||
// Merge-join to find zero slots without allocating a HashSet.
|
||||
let mut fetched_iter = fetched.iter();
|
||||
let mut next = fetched_iter.next();
|
||||
for &key in &uncached_keys {
|
||||
if let Some(&(fk, fv)) = next &&
|
||||
fk == key
|
||||
{
|
||||
let _ = self.caches.get_or_try_insert_storage_with(account, key, || {
|
||||
Ok::<_, ProviderError>(fv)
|
||||
});
|
||||
result.push((key, fv));
|
||||
next = fetched_iter.next();
|
||||
continue;
|
||||
}
|
||||
// key not returned by inner provider → zero slot
|
||||
let _ = self.caches.get_or_try_insert_storage_with(account, key, || {
|
||||
Ok::<_, ProviderError>(StorageValue::ZERO)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {
|
||||
@@ -805,23 +768,18 @@ impl ExecutionCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a cached storage value if present, without inserting.
|
||||
pub fn get_storage(&self, address: Address, key: StorageKey) -> Option<StorageValue> {
|
||||
self.0.storage_cache.get(&(address, key))
|
||||
}
|
||||
|
||||
/// Insert storage value into cache.
|
||||
pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
|
||||
self.0.storage_cache.insert((address, key), value.unwrap_or_default());
|
||||
}
|
||||
|
||||
/// Insert code into cache.
|
||||
fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
|
||||
pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
|
||||
self.0.code_cache.insert(hash, code);
|
||||
}
|
||||
|
||||
/// Insert account into cache.
|
||||
fn insert_account(&self, address: Address, account: Option<Account>) {
|
||||
pub fn insert_account(&self, address: Address, account: Option<Account>) {
|
||||
self.0.account_cache.insert(address, account);
|
||||
}
|
||||
|
||||
@@ -894,7 +852,6 @@ impl ExecutionCache {
|
||||
}
|
||||
|
||||
self.0.account_cache.remove(addr);
|
||||
self.0.account_stats.decrement_size();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -923,7 +880,7 @@ impl ExecutionCache {
|
||||
///
|
||||
/// We do not clear the bytecodes cache, because its mapping can never change, as it's
|
||||
/// `keccak256(bytecode) => bytecode`.
|
||||
pub(crate) fn clear(&self) {
|
||||
pub fn clear(&self) {
|
||||
self.0.storage_cache.clear();
|
||||
self.0.account_cache.clear();
|
||||
|
||||
@@ -933,7 +890,7 @@ impl ExecutionCache {
|
||||
|
||||
/// Updates the provided metrics with the current stats from the cache's stats handlers,
|
||||
/// and resets the hit/miss/collision counters.
|
||||
pub(crate) fn update_metrics(&self, metrics: &CachedStateMetrics) {
|
||||
pub fn update_metrics(&self, metrics: &CachedStateMetrics) {
|
||||
metrics.code_cache_size.set(self.0.code_stats.size() as f64);
|
||||
metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
|
||||
metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
|
||||
@@ -1018,7 +975,7 @@ impl SavedCache {
|
||||
///
|
||||
/// Note: This can be expensive with large cached state. Use
|
||||
/// `with_disable_cache_metrics(true)` to skip.
|
||||
pub(crate) fn update_metrics(&self) {
|
||||
pub fn update_metrics(&self) {
|
||||
if self.disable_cache_metrics {
|
||||
return
|
||||
}
|
||||
@@ -1027,15 +984,16 @@ impl SavedCache {
|
||||
|
||||
/// Clears all caches, resetting them to empty state,
|
||||
/// and updates the hash of the block this cache belongs to.
|
||||
pub(crate) fn clear_with_hash(&mut self, hash: B256) {
|
||||
pub fn clear_with_hash(&mut self, hash: B256) {
|
||||
self.hash = hash;
|
||||
self.caches.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
impl SavedCache {
|
||||
fn clone_guard_for_test(&self) -> Arc<()> {
|
||||
/// Clones the usage guard for testing availability tracking.
|
||||
pub fn clone_guard_for_test(&self) -> Arc<()> {
|
||||
self.usage_guard.clone()
|
||||
}
|
||||
}
|
||||
@@ -1276,6 +1234,33 @@ mod tests {
|
||||
assert!(caches.0.account_cache.get(&addr2).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_state_destroyed_uncached_account_keeps_size_zero() {
|
||||
let caches = ExecutionCache::new(1000);
|
||||
assert_eq!(caches.0.account_stats.size(), 0);
|
||||
|
||||
let addr = Address::random();
|
||||
let bundle = BundleState {
|
||||
state: HashMap::from_iter([(
|
||||
addr,
|
||||
BundleAccount::new(
|
||||
None, // No original info
|
||||
None, // Destroyed
|
||||
Default::default(),
|
||||
AccountStatus::Destroyed,
|
||||
),
|
||||
)]),
|
||||
contracts: Default::default(),
|
||||
reverts: Default::default(),
|
||||
state_size: 0,
|
||||
reverts_size: 0,
|
||||
};
|
||||
|
||||
assert!(caches.insert_state(&bundle).is_ok());
|
||||
assert_eq!(caches.0.account_stats.size(), 0);
|
||||
assert!(caches.0.account_cache.get(&addr).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_cache_capacity_with_default_budget() {
|
||||
// Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.
|
||||
227
crates/engine/execution-cache/src/lib.rs
Normal file
227
crates/engine/execution-cache/src/lib.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
//! 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,14 +32,6 @@ 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,9 +6,7 @@ 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, EngineApiMessageVersion, PayloadAttributesBuilder, PayloadKind, PayloadTypes,
|
||||
};
|
||||
use reth_payload_primitives::{BuiltPayload, PayloadAttributesBuilder, PayloadKind, PayloadTypes};
|
||||
use reth_primitives_traits::{HeaderTy, SealedHeaderFor};
|
||||
use reth_storage_api::BlockReader;
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
@@ -214,10 +212,7 @@ 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, EngineApiMessageVersion::default())
|
||||
.await?;
|
||||
let res = self.to_engine.fork_choice_updated(state, None).await?;
|
||||
|
||||
if !res.is_valid() {
|
||||
eyre::bail!("Invalid fork choice update {state:?}: {res:?}")
|
||||
@@ -234,7 +229,6 @@ where
|
||||
.fork_choice_updated(
|
||||
self.forkchoice_state(),
|
||||
Some(self.payload_attributes_builder.build(&self.last_header)),
|
||||
EngineApiMessageVersion::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -57,60 +57,6 @@ where
|
||||
.chain_spec
|
||||
.is_cancun_active_at_timestamp(timestamp)
|
||||
.then(B256::random),
|
||||
slot_number: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,6 +6,9 @@ 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;
|
||||
|
||||
@@ -44,6 +47,16 @@ 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
|
||||
@@ -82,6 +95,8 @@ 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,
|
||||
@@ -151,17 +166,10 @@ pub struct TreeConfig {
|
||||
/// 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 disable BAL (Block Access List, EIP-7928) based parallel execution.
|
||||
/// When disabled, falls back to transaction-based prewarming even when a BAL is available.
|
||||
disable_bal_parallel_execution: bool,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
/// When disabled, the BAL hashed post state is not sent to the multiproof task for
|
||||
/// early parallel state root computation.
|
||||
disable_bal_parallel_state_root: bool,
|
||||
/// Whether to disable BAL (Block Access List) batched IO during prewarming.
|
||||
/// When disabled, falls back to individual per-slot storage reads instead of
|
||||
/// batched cursor reads via `storage_range`.
|
||||
disable_bal_batch_io: bool,
|
||||
/// 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.
|
||||
@@ -171,9 +179,14 @@ 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,
|
||||
@@ -197,9 +210,8 @@ impl Default for TreeConfig {
|
||||
slow_block_threshold: None,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
|
||||
disable_bal_parallel_execution: false,
|
||||
disable_bal_parallel_state_root: false,
|
||||
disable_bal_batch_io: false,
|
||||
share_execution_cache_with_payload_builder: false,
|
||||
share_sparse_trie_with_payload_builder: false,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -212,6 +224,7 @@ 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,
|
||||
@@ -234,10 +247,17 @@ 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,
|
||||
@@ -261,9 +281,8 @@ impl TreeConfig {
|
||||
slow_block_threshold,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
state_root_task_timeout,
|
||||
disable_bal_parallel_execution: false,
|
||||
disable_bal_parallel_state_root: false,
|
||||
disable_bal_batch_io: false,
|
||||
share_execution_cache_with_payload_builder,
|
||||
share_sparse_trie_with_payload_builder,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
@@ -279,6 +298,11 @@ 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
|
||||
@@ -375,6 +399,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -387,6 +415,19 @@ 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;
|
||||
@@ -576,31 +617,32 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether BAL-based parallel execution is disabled.
|
||||
pub const fn disable_bal_parallel_execution(&self) -> bool {
|
||||
self.disable_bal_parallel_execution
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Setter for whether to disable BAL-based parallel execution.
|
||||
pub const fn without_bal_parallel_execution(
|
||||
/// 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,
|
||||
disable_bal_parallel_execution: bool,
|
||||
share_execution_cache_with_payload_builder: bool,
|
||||
) -> Self {
|
||||
self.disable_bal_parallel_execution = disable_bal_parallel_execution;
|
||||
self.share_execution_cache_with_payload_builder =
|
||||
share_execution_cache_with_payload_builder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether BAL-driven parallel state root computation is disabled.
|
||||
pub const fn disable_bal_parallel_state_root(&self) -> bool {
|
||||
self.disable_bal_parallel_state_root
|
||||
}
|
||||
|
||||
/// Setter for whether to disable BAL-driven parallel state root computation.
|
||||
pub const fn without_bal_parallel_state_root(
|
||||
/// Setter for whether to share sparse trie with the payload builder.
|
||||
pub const fn with_share_sparse_trie_with_payload_builder(
|
||||
mut self,
|
||||
disable_bal_parallel_state_root: bool,
|
||||
share_sparse_trie_with_payload_builder: bool,
|
||||
) -> Self {
|
||||
self.disable_bal_parallel_state_root = disable_bal_parallel_state_root;
|
||||
self.share_sparse_trie_with_payload_builder = share_sparse_trie_with_payload_builder;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -616,15 +658,19 @@ impl TreeConfig {
|
||||
self.proof_jitter = proof_jitter;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether BAL batched IO is disabled.
|
||||
pub const fn disable_bal_batch_io(&self) -> bool {
|
||||
self.disable_bal_batch_io
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TreeConfig;
|
||||
|
||||
/// Setter for whether to disable BAL batched IO.
|
||||
pub const fn without_bal_batch_io(mut self, disable_bal_batch_io: bool) -> Self {
|
||||
self.disable_bal_batch_io = disable_bal_batch_io;
|
||||
self
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,13 +86,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a slow block detected after persistence.
|
||||
/// Information about a slow block detected after execution or persistence.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SlowBlockInfo {
|
||||
/// The timing statistics for the slow block.
|
||||
pub stats: Box<ExecutionTimingStats>,
|
||||
/// The commit duration for the batch containing this block.
|
||||
pub commit_duration: Duration,
|
||||
/// `None` when emitted immediately after execution (before persistence).
|
||||
pub commit_duration: Option<Duration>,
|
||||
/// The total duration (execution + `state_root` + commit).
|
||||
/// Note: `state_read` is a subset of execution and is not added separately.
|
||||
pub total_duration: Duration,
|
||||
|
||||
@@ -117,7 +117,19 @@ pub trait EngineTypes:
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
/// Type that validates the payloads processed by the engine API.
|
||||
/// Validates engine API requests at the RPC layer, before payloads and attributes
|
||||
/// are forwarded to the engine for processing.
|
||||
///
|
||||
/// - [`validate_version_specific_fields`](Self::validate_version_specific_fields): Enforced in each
|
||||
/// `engine_newPayloadVN` RPC handler to verify the payload contains the correct fields for the
|
||||
/// engine API version (e.g., blob fields in V3+, requests in V4+).
|
||||
///
|
||||
/// - [`ensure_well_formed_attributes`](Self::ensure_well_formed_attributes): Enforced in
|
||||
/// `engine_forkchoiceUpdatedVN` RPC handlers to validate payload attributes are well-formed for
|
||||
/// the given version before forwarding to the engine.
|
||||
///
|
||||
/// After this validation passes, the engine performs the full consensus validation
|
||||
/// pipeline (header, pre-execution, execution, post-execution).
|
||||
pub trait EngineApiValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
|
||||
/// Validates the presence or exclusion of fork-specific fields based on the payload attributes
|
||||
/// and the message version.
|
||||
@@ -136,6 +148,34 @@ pub trait EngineApiValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static
|
||||
}
|
||||
|
||||
/// Type that validates an [`ExecutionPayload`].
|
||||
///
|
||||
/// This trait handles validation at the engine API boundary — converting payloads
|
||||
/// into blocks and validating payload attributes for block building.
|
||||
///
|
||||
/// # Methods and when they're used
|
||||
///
|
||||
/// - [`convert_payload_to_block`](Self::convert_payload_to_block): Used during `engine_newPayload`
|
||||
/// processing to decode the payload into a [`SealedBlock`]. Also used to validate payload
|
||||
/// structure during backfill buffering. In the engine tree, this runs concurrently with state
|
||||
/// setup on a background thread.
|
||||
///
|
||||
/// - [`ensure_well_formed_payload`](Self::ensure_well_formed_payload): Converts payload and
|
||||
/// recovers transaction signatures. Used when recovered senders are needed immediately.
|
||||
///
|
||||
/// - [`validate_payload_attributes_against_header`](Self::validate_payload_attributes_against_header):
|
||||
/// Enforced as part of the engine's `forkchoiceUpdated` handling when payload attributes
|
||||
/// are provided. Gates whether a payload build job is started.
|
||||
///
|
||||
/// - [`validate_block_post_execution_with_hashed_state`](Self::validate_block_post_execution_with_hashed_state):
|
||||
/// Called after block execution in the engine's payload validation pipeline.
|
||||
/// No-op on L1, used by L2s (e.g., OP Stack) for additional post-execution checks.
|
||||
///
|
||||
/// # Relationship to consensus traits
|
||||
///
|
||||
/// This trait does NOT replace the consensus traits (`Consensus`, `FullConsensus` from
|
||||
/// `reth-consensus`). Those handle the actual consensus rule
|
||||
/// validation (header checks, pre/post-execution). This trait handles engine API-specific
|
||||
/// concerns: payload encoding/decoding and attribute validation.
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait PayloadValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
|
||||
/// The block type used by the engine.
|
||||
@@ -191,6 +231,11 @@ pub trait PayloadValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
|
||||
/// > of a block referenced by forkchoiceState.headBlockHash.
|
||||
///
|
||||
/// See also: <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#specification-1>
|
||||
///
|
||||
/// Enforced as part of the engine's `forkchoiceUpdated` handling when the consensus layer
|
||||
/// provides payload attributes. If this returns an error, the forkchoice state update itself
|
||||
/// is NOT rolled back, but no payload build job is started — the response includes
|
||||
/// `INVALID_PAYLOAD_ATTRIBUTES`.
|
||||
fn validate_payload_attributes_against_header(
|
||||
&self,
|
||||
attr: &Types::PayloadAttributes,
|
||||
|
||||
@@ -14,7 +14,7 @@ use core::{
|
||||
use futures::{future::Either, FutureExt, TryFutureExt};
|
||||
use reth_errors::RethResult;
|
||||
use reth_payload_builder_primitives::PayloadBuilderError;
|
||||
use reth_payload_primitives::{EngineApiMessageVersion, PayloadTypes};
|
||||
use reth_payload_primitives::PayloadTypes;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||
|
||||
@@ -162,6 +162,30 @@ pub struct NewPayloadTimings {
|
||||
pub sparse_trie_wait: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Additional data for big block payloads that merge multiple real blocks.
|
||||
///
|
||||
/// This is used by the `reth_newPayload` endpoint to pass environment switches
|
||||
/// and prior block hashes needed for correct multi-segment execution.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BigBlockData<ExecutionData> {
|
||||
/// Environment switches at block boundaries.
|
||||
/// Each entry is `(cumulative_tx_count, execution_data_of_next_block)`.
|
||||
///
|
||||
/// The first entry at index 0 represents the **original unmutated** base block's
|
||||
/// `ExecutionData`, which must be used to derive the initial EVM environment.
|
||||
pub env_switches: Vec<(usize, ExecutionData)>,
|
||||
/// Block number → real block hash for blocks covered by previous big blocks in a sequence.
|
||||
/// When replaying chained big blocks, the BLOCKHASH opcode needs real hashes for blocks
|
||||
/// that were merged into earlier big blocks (and thus not individually persisted).
|
||||
pub prior_block_hashes: Vec<(u64, alloy_primitives::B256)>,
|
||||
}
|
||||
|
||||
impl<T> Default for BigBlockData<T> {
|
||||
fn default() -> Self {
|
||||
Self { env_switches: Vec::new(), prior_block_hashes: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A message for the beacon engine from other components of the node (engine RPC API invoked by the
|
||||
/// consensus layer).
|
||||
#[derive(Debug)]
|
||||
@@ -195,8 +219,6 @@ pub enum BeaconEngineMessage<Payload: PayloadTypes> {
|
||||
state: ForkchoiceState,
|
||||
/// The payload attributes for block building.
|
||||
payload_attrs: Option<Payload::PayloadAttributes>,
|
||||
/// The Engine API Version.
|
||||
version: EngineApiMessageVersion,
|
||||
/// The sender for returning forkchoice updated result.
|
||||
tx: oneshot::Sender<RethResult<OnForkChoiceUpdated>>,
|
||||
},
|
||||
@@ -297,10 +319,9 @@ where
|
||||
&self,
|
||||
state: ForkchoiceState,
|
||||
payload_attrs: Option<Payload::PayloadAttributes>,
|
||||
version: EngineApiMessageVersion,
|
||||
) -> Result<ForkchoiceUpdated, BeaconForkChoiceUpdateError> {
|
||||
Ok(self
|
||||
.send_fork_choice_updated(state, payload_attrs, version)
|
||||
.send_fork_choice_updated(state, payload_attrs)
|
||||
.map_err(|_| BeaconForkChoiceUpdateError::EngineUnavailable)
|
||||
.await?
|
||||
.map_err(BeaconForkChoiceUpdateError::internal)?
|
||||
@@ -313,14 +334,12 @@ where
|
||||
&self,
|
||||
state: ForkchoiceState,
|
||||
payload_attrs: Option<Payload::PayloadAttributes>,
|
||||
version: EngineApiMessageVersion,
|
||||
) -> oneshot::Receiver<RethResult<OnForkChoiceUpdated>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.to_engine.send(BeaconEngineMessage::ForkchoiceUpdated {
|
||||
state,
|
||||
payload_attrs,
|
||||
tx,
|
||||
version,
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ reth-chainspec = { workspace = true, optional = true }
|
||||
reth-consensus.workspace = true
|
||||
reth-db.workspace = true
|
||||
reth-engine-primitives = { workspace = true, features = ["std"] }
|
||||
reth-execution-cache.workspace = true
|
||||
reth-errors.workspace = true
|
||||
reth-execution-types.workspace = true
|
||||
reth-evm = { workspace = true, features = ["metrics"] }
|
||||
@@ -52,7 +53,6 @@ revm-primitives.workspace = true
|
||||
futures.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "macros"] }
|
||||
fixed-cache.workspace = true
|
||||
moka = { workspace = true, features = ["sync"] }
|
||||
|
||||
# metrics
|
||||
@@ -60,7 +60,6 @@ metrics.workspace = true
|
||||
reth-metrics = { workspace = true, features = ["common"] }
|
||||
|
||||
# misc
|
||||
itertools.workspace = true
|
||||
schnellru.workspace = true
|
||||
rayon.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -133,6 +132,7 @@ test-utils = [
|
||||
"reth-node-ethereum/test-utils",
|
||||
"reth-evm-ethereum/test-utils",
|
||||
"reth-tasks/test-utils",
|
||||
"reth-execution-cache/test-utils",
|
||||
]
|
||||
trie-debug = [
|
||||
"reth-trie-sparse/trie-debug",
|
||||
|
||||
@@ -21,7 +21,7 @@ use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, ProviderNodeTypes},
|
||||
ProviderFactory, StorageSettingsCache,
|
||||
ProviderFactory,
|
||||
};
|
||||
use reth_prune::PrunerWithFactory;
|
||||
use reth_stages_api::{MetricEventsSender, Pipeline};
|
||||
@@ -81,7 +81,6 @@ where
|
||||
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
let downloader = BasicBlockDownloader::new(client, consensus.clone());
|
||||
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
|
||||
|
||||
let persistence_handle =
|
||||
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
|
||||
@@ -99,7 +98,6 @@ where
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
);
|
||||
|
||||
|
||||
@@ -171,6 +171,10 @@ pub struct EngineMetrics {
|
||||
pub(crate) executed_new_block_cache_miss: Counter,
|
||||
/// Histogram of persistence operation durations (in seconds)
|
||||
pub(crate) persistence_duration: Histogram,
|
||||
/// Whether the engine loop is currently stalled on persistence backpressure.
|
||||
pub(crate) backpressure_active: Gauge,
|
||||
/// Time spent blocked waiting on persistence because backpressure was active.
|
||||
pub(crate) backpressure_stall_duration: Histogram,
|
||||
/// Tracks the how often we failed to deliver a newPayload response.
|
||||
///
|
||||
/// This effectively tracks how often the message sender dropped the channel and indicates a CL
|
||||
@@ -448,11 +452,17 @@ impl NewPayloadStatusMetrics {
|
||||
Ok(outcome) => match outcome.outcome.status {
|
||||
PayloadStatusEnum::Valid => {
|
||||
self.new_payload_valid.increment(1);
|
||||
self.new_payload_total_gas.record(gas_used as f64);
|
||||
self.new_payload_total_gas_last.set(gas_used as f64);
|
||||
let gas_per_second = gas_used as f64 / elapsed.as_secs_f64();
|
||||
self.new_payload_gas_per_second.record(gas_per_second);
|
||||
self.new_payload_gas_per_second_last.set(gas_per_second);
|
||||
if !outcome.already_seen {
|
||||
self.new_payload_total_gas.record(gas_used as f64);
|
||||
self.new_payload_total_gas_last.set(gas_used as f64);
|
||||
let gas_per_second = gas_used as f64 / elapsed.as_secs_f64();
|
||||
self.new_payload_gas_per_second.record(gas_per_second);
|
||||
self.new_payload_gas_per_second_last.set(gas_per_second);
|
||||
|
||||
self.new_payload_latency.record(elapsed);
|
||||
self.new_payload_last.set(elapsed);
|
||||
self.gas_bucket.record(gas_used, elapsed);
|
||||
}
|
||||
}
|
||||
PayloadStatusEnum::Syncing => self.new_payload_syncing.increment(1),
|
||||
PayloadStatusEnum::Accepted => self.new_payload_accepted.increment(1),
|
||||
@@ -461,9 +471,6 @@ impl NewPayloadStatusMetrics {
|
||||
Err(_) => self.new_payload_error.increment(1),
|
||||
}
|
||||
self.new_payload_messages.increment(1);
|
||||
self.new_payload_latency.record(elapsed);
|
||||
self.new_payload_last.set(elapsed);
|
||||
self.gas_bucket.record(gas_used, elapsed);
|
||||
if let Some(latest_forkchoice_updated_at) = latest_forkchoice_updated_at.take() {
|
||||
self.forkchoice_updated_new_payload_time_diff
|
||||
.record(start - latest_forkchoice_updated_at);
|
||||
|
||||
@@ -23,10 +23,8 @@ use reth_engine_primitives::{
|
||||
};
|
||||
use reth_errors::{ConsensusError, ProviderResult};
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_payload_primitives::{
|
||||
BuiltPayload, EngineApiMessageVersion, NewPayloadError, PayloadBuilderAttributes, PayloadTypes,
|
||||
};
|
||||
use reth_payload_builder::{BuildNewPayload, PayloadBuilderHandle};
|
||||
use reth_payload_primitives::{BuiltPayload, NewPayloadError, PayloadTypes};
|
||||
use reth_primitives_traits::{
|
||||
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
|
||||
};
|
||||
@@ -52,7 +50,6 @@ use tokio::sync::{
|
||||
use tracing::*;
|
||||
|
||||
mod block_buffer;
|
||||
mod cached_state;
|
||||
pub mod error;
|
||||
pub mod instrumented_state;
|
||||
mod invalid_headers;
|
||||
@@ -67,13 +64,15 @@ mod trie_updates;
|
||||
|
||||
use crate::{persistence::PersistenceResult, tree::error::AdvancePersistenceError};
|
||||
pub use block_buffer::BlockBuffer;
|
||||
pub use cached_state::{CachedStateMetrics, CachedStateProvider, ExecutionCache, SavedCache};
|
||||
pub use invalid_headers::InvalidHeaderCache;
|
||||
pub use metrics::EngineApiMetrics;
|
||||
pub use payload_processor::*;
|
||||
pub use payload_validator::{BasicEngineValidator, EngineValidator};
|
||||
pub use persistence_state::PersistenceState;
|
||||
pub use reth_engine_primitives::TreeConfig;
|
||||
pub use reth_execution_cache::{
|
||||
CachedStateMetrics, CachedStateProvider, ExecutionCache, PayloadExecutionCache, SavedCache,
|
||||
};
|
||||
|
||||
pub mod state;
|
||||
|
||||
@@ -180,12 +179,15 @@ pub struct TreeOutcome<T> {
|
||||
pub outcome: T,
|
||||
/// An optional event to tell the caller to do something.
|
||||
pub event: Option<TreeEvent>,
|
||||
/// Whether the block was already seen, meaning no real execution happened during this
|
||||
/// `newPayload` call.
|
||||
pub already_seen: bool,
|
||||
}
|
||||
|
||||
impl<T> TreeOutcome<T> {
|
||||
/// Create new tree outcome.
|
||||
pub const fn new(outcome: T) -> Self {
|
||||
Self { outcome, event: None }
|
||||
Self { outcome, event: None, already_seen: false }
|
||||
}
|
||||
|
||||
/// Set event on the outcome.
|
||||
@@ -193,6 +195,31 @@ impl<T> TreeOutcome<T> {
|
||||
self.event = Some(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `already_seen` flag on the outcome.
|
||||
pub const fn with_already_seen(mut self, value: bool) -> Self {
|
||||
self.already_seen = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of trying to insert a new payload in [`EngineApiTreeHandler`].
|
||||
#[derive(Debug)]
|
||||
pub struct TryInsertPayloadResult {
|
||||
/// - `Valid`: Payload successfully validated and inserted
|
||||
/// - `Syncing`: Parent missing, payload buffered for later
|
||||
/// - Error status: Payload is invalid
|
||||
pub status: PayloadStatus,
|
||||
/// Whether the block was already seen
|
||||
pub already_seen: bool,
|
||||
}
|
||||
|
||||
impl TryInsertPayloadResult {
|
||||
/// Convert the result into a [`TreeOutcome`].
|
||||
#[inline]
|
||||
pub fn into_outcome(self) -> TreeOutcome<PayloadStatus> {
|
||||
TreeOutcome::new(self.status).with_already_seen(self.already_seen)
|
||||
}
|
||||
}
|
||||
|
||||
/// Events that are triggered by Tree Chain
|
||||
@@ -277,9 +304,6 @@ where
|
||||
/// Stored here (not in `ExecutedBlock`) to avoid leaking observability concerns into the block
|
||||
/// type. Entries are removed when blocks are persisted or invalidated.
|
||||
execution_timing_stats: HashMap<B256, Box<ExecutionTimingStats>>,
|
||||
/// Whether the node uses hashed state as canonical storage (v2 mode).
|
||||
/// Cached at construction to avoid threading `StorageSettingsCache` bounds everywhere.
|
||||
use_hashed_state: bool,
|
||||
/// Task runtime for spawning blocking work on named, reusable threads.
|
||||
runtime: reth_tasks::Runtime,
|
||||
}
|
||||
@@ -308,7 +332,6 @@ where
|
||||
.field("evm_config", &self.evm_config)
|
||||
.field("changeset_cache", &self.changeset_cache)
|
||||
.field("execution_timing_stats", &self.execution_timing_stats.len())
|
||||
.field("use_hashed_state", &self.use_hashed_state)
|
||||
.field("runtime", &self.runtime)
|
||||
.finish()
|
||||
}
|
||||
@@ -349,7 +372,6 @@ where
|
||||
engine_kind: EngineApiKind,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
use_hashed_state: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> Self {
|
||||
let (incoming_tx, incoming) = crossbeam_channel::unbounded();
|
||||
@@ -373,7 +395,6 @@ where
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
execution_timing_stats: HashMap::new(),
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
@@ -395,7 +416,6 @@ where
|
||||
kind: EngineApiKind,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
use_hashed_state: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> (Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>, UnboundedReceiver<EngineApiEvent<N>>)
|
||||
{
|
||||
@@ -429,7 +449,6 @@ where
|
||||
kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
);
|
||||
let incoming = task.incoming_tx.clone();
|
||||
@@ -453,12 +472,79 @@ where
|
||||
self.incoming_tx.clone()
|
||||
}
|
||||
|
||||
/// How many blocks the canonical tip is ahead of the last persisted block. A large gap means
|
||||
/// persistence is falling behind execution.
|
||||
const fn persistence_gap(&self) -> u64 {
|
||||
self.state
|
||||
.tree_state
|
||||
.canonical_block_number()
|
||||
.saturating_sub(self.persistence_state.last_persisted_block.number)
|
||||
}
|
||||
|
||||
/// Returns `true` when the main loop should stop draining the tree input channel.
|
||||
///
|
||||
/// This is the case when persistence is already running and the gap between the canonical tip
|
||||
/// and the last persisted block has reached the configured threshold.
|
||||
const fn should_backpressure(&self) -> bool {
|
||||
self.persistence_state.in_progress() &&
|
||||
self.persistence_gap() >= self.config.persistence_backpressure_threshold()
|
||||
}
|
||||
|
||||
/// Run the engine API handler.
|
||||
///
|
||||
/// This will block the current thread and process incoming messages.
|
||||
pub fn run(mut self) {
|
||||
loop {
|
||||
match self.wait_for_event() {
|
||||
// Each iteration has three phases:
|
||||
//
|
||||
// 1. Non-blocking poll for persistence completion. If the background flush already
|
||||
// landed, absorb the result now so the gap calculation below is fresh.
|
||||
// 2. Decide how to wait for the next event. When the canonical-to-persisted gap exceeds
|
||||
// the backpressure threshold we only block on the persistence receiver, leaving new
|
||||
// engine requests sitting in the unbounded upstream channel.
|
||||
// 3. Handle the event (engine message or persistence completion) and kick off a new
|
||||
// persistence cycle if the threshold is met again.
|
||||
//
|
||||
// The net effect: when the persistence gap exceeds the threshold, we stop
|
||||
// processing incoming messages and let them queue in the channel. This is only a
|
||||
// soft form of backpressure: it delays replies and, more importantly, prevents
|
||||
// executing further blocks that would pile up in the persistence queue - where each
|
||||
// block carries heavier state (eg. trie updates) than the raw payload sitting in the
|
||||
// engine channel.
|
||||
//
|
||||
// Standard Ethereum CLs won't truly back off - the engine API has no
|
||||
// backpressure semantics, and CLs typically timeout after ≈8s and resend - so
|
||||
// this cannot prevent the incoming channel from growing under sustained load.
|
||||
// But it shifts the bottleneck to the lighter-weight incoming queue rather than
|
||||
// the costlier persistence pipeline. Other clients that respect reply latency
|
||||
// can treat the delayed responses as a signal to chill out.
|
||||
match self.try_poll_persistence() {
|
||||
Ok(true) => {
|
||||
if let Err(err) = self.advance_persistence() {
|
||||
error!(target: "engine::tree", %err, "Advancing persistence failed");
|
||||
return
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
error!(target: "engine::tree", %err, "Polling persistence failed");
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let event = if self.should_backpressure() {
|
||||
self.metrics.engine.backpressure_active.set(1.0);
|
||||
let stall_start = Instant::now();
|
||||
let event = self.wait_for_persistence_event();
|
||||
self.metrics.engine.backpressure_stall_duration.record(stall_start.elapsed());
|
||||
event
|
||||
} else {
|
||||
self.metrics.engine.backpressure_active.set(0.0);
|
||||
self.wait_for_event()
|
||||
};
|
||||
|
||||
match event {
|
||||
LoopEvent::EngineMessage(msg) => {
|
||||
debug!(target: "engine::tree", %msg, "received new engine message");
|
||||
match self.on_engine_message(msg) {
|
||||
@@ -493,6 +579,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocks until the in-flight persistence task completes, used when we are under
|
||||
/// backpressure.
|
||||
///
|
||||
/// Unlike `wait_for_event`, this deliberately does not read from the tree input channel. Any
|
||||
/// requests sent to the tree remain queued upstream until persistence catches up.
|
||||
fn wait_for_persistence_event(&mut self) -> LoopEvent<T, N> {
|
||||
let maybe_persistence = self.persistence_state.rx.take();
|
||||
|
||||
if let Some((persistence_rx, start_time, _action)) = maybe_persistence {
|
||||
match persistence_rx.recv() {
|
||||
Ok(result) => LoopEvent::PersistenceComplete { result, start_time },
|
||||
Err(_) => LoopEvent::Disconnected,
|
||||
}
|
||||
} else {
|
||||
self.wait_for_event()
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocks until the next event is ready: either an incoming engine message or a persistence
|
||||
/// completion (if one is in progress).
|
||||
///
|
||||
@@ -639,13 +743,12 @@ where
|
||||
// record pre-execution phase duration
|
||||
self.metrics.block_validation.record_payload_validation(start.elapsed().as_secs_f64());
|
||||
|
||||
let status = if self.backfill_sync_state.is_idle() {
|
||||
self.try_insert_payload(payload)?
|
||||
let mut outcome = if self.backfill_sync_state.is_idle() {
|
||||
self.try_insert_payload(payload)?.into_outcome()
|
||||
} else {
|
||||
self.try_buffer_payload(payload)?
|
||||
TreeOutcome::new(self.try_buffer_payload(payload)?)
|
||||
};
|
||||
|
||||
let mut outcome = TreeOutcome::new(status);
|
||||
// if the block is valid and it is the current sync target head, make it canonical
|
||||
if outcome.outcome.is_valid() && self.is_sync_target_head(block_hash) {
|
||||
// Only create the canonical event if this block isn't already the canonical head
|
||||
@@ -663,16 +766,11 @@ where
|
||||
}
|
||||
|
||||
/// Processes a payload during normal sync operation.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Valid`: Payload successfully validated and inserted
|
||||
/// - `Syncing`: Parent missing, payload buffered for later
|
||||
/// - Error status: Payload is invalid
|
||||
#[instrument(level = "debug", target = "engine::tree", skip_all)]
|
||||
fn try_insert_payload(
|
||||
&mut self,
|
||||
payload: T::ExecutionData,
|
||||
) -> Result<PayloadStatus, InsertBlockFatalError> {
|
||||
) -> Result<TryInsertPayloadResult, InsertBlockFatalError> {
|
||||
let block_hash = payload.block_hash();
|
||||
let num_hash = payload.num_hash();
|
||||
let parent_hash = payload.parent_hash();
|
||||
@@ -680,31 +778,40 @@ where
|
||||
|
||||
match self.insert_payload(payload) {
|
||||
Ok(status) => {
|
||||
let status = match status {
|
||||
let (status, already_seen) = match status {
|
||||
InsertPayloadOk::Inserted(BlockStatus::Valid) => {
|
||||
latest_valid_hash = Some(block_hash);
|
||||
self.try_connect_buffered_blocks(num_hash)?;
|
||||
PayloadStatusEnum::Valid
|
||||
(PayloadStatusEnum::Valid, false)
|
||||
}
|
||||
InsertPayloadOk::AlreadySeen(BlockStatus::Valid) => {
|
||||
latest_valid_hash = Some(block_hash);
|
||||
PayloadStatusEnum::Valid
|
||||
(PayloadStatusEnum::Valid, true)
|
||||
}
|
||||
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) => {
|
||||
(PayloadStatusEnum::Syncing, false)
|
||||
}
|
||||
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) |
|
||||
InsertPayloadOk::AlreadySeen(BlockStatus::Disconnected { .. }) => {
|
||||
// not known to be invalid, but we don't know anything else
|
||||
PayloadStatusEnum::Syncing
|
||||
(PayloadStatusEnum::Syncing, true)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(PayloadStatus::new(status, latest_valid_hash))
|
||||
Ok(TryInsertPayloadResult {
|
||||
status: PayloadStatus::new(status, latest_valid_hash),
|
||||
already_seen,
|
||||
})
|
||||
}
|
||||
Err(error) => {
|
||||
let status = match error {
|
||||
InsertPayloadError::Block(error) => self.on_insert_block_error(error)?,
|
||||
InsertPayloadError::Payload(error) => {
|
||||
self.on_new_payload_error(error, num_hash, parent_hash)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TryInsertPayloadResult { status, already_seen: false })
|
||||
}
|
||||
Err(error) => match error {
|
||||
InsertPayloadError::Block(error) => Ok(self.on_insert_block_error(error)?),
|
||||
InsertPayloadError::Payload(error) => {
|
||||
Ok(self.on_new_payload_error(error, num_hash, parent_hash)?)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,7 +1108,6 @@ where
|
||||
&mut self,
|
||||
state: ForkchoiceState,
|
||||
attrs: Option<T::PayloadAttributes>,
|
||||
version: EngineApiMessageVersion,
|
||||
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
|
||||
trace!(target: "engine::tree", ?attrs, "invoked forkchoice update");
|
||||
|
||||
@@ -1014,13 +1120,13 @@ where
|
||||
}
|
||||
|
||||
// Return early if we are on the correct fork
|
||||
if let Some(result) = self.handle_canonical_head(state, &attrs, version)? {
|
||||
if let Some(result) = self.handle_canonical_head(state, &attrs)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Attempt to apply a chain update when the head differs from our canonical chain.
|
||||
// This handles reorgs and chain extensions by making the specified head canonical.
|
||||
if let Some(result) = self.apply_chain_update(state, &attrs, version)? {
|
||||
if let Some(result) = self.apply_chain_update(state, &attrs)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -1071,7 +1177,6 @@ where
|
||||
&self,
|
||||
state: ForkchoiceState,
|
||||
attrs: &Option<T::PayloadAttributes>, // Changed to reference
|
||||
version: EngineApiMessageVersion,
|
||||
) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
|
||||
// Process the forkchoice update by trying to make the head block canonical
|
||||
//
|
||||
@@ -1109,7 +1214,7 @@ where
|
||||
ProviderError::HeaderNotFound(state.head_block_hash.into())
|
||||
})?;
|
||||
// Clone only when we actually need to process the attributes
|
||||
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
|
||||
let updated = self.process_payload_attributes(attr.clone(), &tip, state);
|
||||
return Ok(Some(TreeOutcome::new(updated)));
|
||||
}
|
||||
|
||||
@@ -1132,7 +1237,6 @@ where
|
||||
&mut self,
|
||||
state: ForkchoiceState,
|
||||
attrs: &Option<T::PayloadAttributes>,
|
||||
version: EngineApiMessageVersion,
|
||||
) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
|
||||
// Check if the head is already part of the canonical chain
|
||||
if let Ok(Some(canonical_header)) = self.find_canonical_header(state.head_block_hash) {
|
||||
@@ -1155,12 +1259,8 @@ where
|
||||
if let Some(attr) = attrs {
|
||||
debug!(target: "engine::tree", head = canonical_header.number(), "handling payload attributes for canonical head");
|
||||
// Clone only when we actually need to process the attributes
|
||||
let updated = self.process_payload_attributes(
|
||||
attr.clone(),
|
||||
&canonical_header,
|
||||
state,
|
||||
version,
|
||||
);
|
||||
let updated =
|
||||
self.process_payload_attributes(attr.clone(), &canonical_header, state);
|
||||
return Ok(Some(TreeOutcome::new(updated)));
|
||||
}
|
||||
}
|
||||
@@ -1191,7 +1291,7 @@ where
|
||||
|
||||
if let Some(attr) = attrs {
|
||||
// Clone only when we actually need to process the attributes
|
||||
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
|
||||
let updated = self.process_payload_attributes(attr.clone(), &tip, state);
|
||||
return Ok(Some(TreeOutcome::new(updated)));
|
||||
}
|
||||
|
||||
@@ -1305,7 +1405,8 @@ where
|
||||
fn persist_until_complete(&mut self) -> Result<(), AdvancePersistenceError> {
|
||||
loop {
|
||||
// Wait for any in-progress persistence to complete (blocking)
|
||||
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
|
||||
if let Some((rx, start_time, action)) = self.persistence_state.rx.take() {
|
||||
debug!(target: "engine::tree", ?action, "waiting for in-flight persistence");
|
||||
let result = rx.recv().map_err(|_| AdvancePersistenceError::ChannelClosed)?;
|
||||
self.on_persistence_complete(result, start_time)?;
|
||||
}
|
||||
@@ -1325,8 +1426,7 @@ where
|
||||
/// Tries to poll for a completed persistence task (non-blocking).
|
||||
///
|
||||
/// Returns `true` if a persistence task was completed, `false` otherwise.
|
||||
#[cfg(test)]
|
||||
pub fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
|
||||
fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
|
||||
let Some((rx, start_time, action)) = self.persistence_state.rx.take() else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -1464,17 +1564,11 @@ where
|
||||
}
|
||||
EngineApiRequest::Beacon(request) => {
|
||||
match request {
|
||||
BeaconEngineMessage::ForkchoiceUpdated {
|
||||
state,
|
||||
payload_attrs,
|
||||
tx,
|
||||
version,
|
||||
} => {
|
||||
BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx } => {
|
||||
let has_attrs = payload_attrs.is_some();
|
||||
|
||||
let start = Instant::now();
|
||||
let mut output =
|
||||
self.on_forkchoice_updated(state, payload_attrs, version);
|
||||
let mut output = self.on_forkchoice_updated(state, payload_attrs);
|
||||
|
||||
if let Ok(res) = &mut output {
|
||||
// track last received forkchoice state
|
||||
@@ -1870,7 +1964,7 @@ where
|
||||
if total_duration > threshold.expect("checked above") {
|
||||
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
|
||||
stats,
|
||||
commit_duration: commit_dur,
|
||||
commit_duration: Some(commit_dur),
|
||||
total_duration,
|
||||
}));
|
||||
}
|
||||
@@ -2519,12 +2613,7 @@ where
|
||||
|
||||
self.update_reorg_metrics(old.len(), old_first);
|
||||
self.reinsert_reorged_blocks(new.clone());
|
||||
|
||||
// When use_hashed_state is enabled, skip reinserting the old chain — the
|
||||
// bundle state references plain state reverts which don't exist.
|
||||
if !self.use_hashed_state {
|
||||
self.reinsert_reorged_blocks(old.clone());
|
||||
}
|
||||
self.reinsert_reorged_blocks(old.clone());
|
||||
}
|
||||
|
||||
// update the tracked in-memory state with the new chain
|
||||
@@ -2688,10 +2777,7 @@ where
|
||||
|
||||
// try to append the block
|
||||
match self.insert_block(block) {
|
||||
Ok(
|
||||
InsertPayloadOk::Inserted(BlockStatus::Valid) |
|
||||
InsertPayloadOk::AlreadySeen(BlockStatus::Valid),
|
||||
) => {
|
||||
Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
|
||||
return self.on_valid_downloaded_block(block_num_hash);
|
||||
}
|
||||
Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
|
||||
@@ -2849,8 +2935,19 @@ where
|
||||
|
||||
let (executed, timing_stats) = execute(&mut self.payload_validator, input, ctx)?;
|
||||
|
||||
// Store timing stats for detailed block logging after persistence
|
||||
// Emit slow block event immediately after execution so it appears even when
|
||||
// persistence hasn't completed yet (e.g. blocks arriving faster than persistence).
|
||||
if let Some(stats) = timing_stats {
|
||||
if let Some(threshold) = self.config.slow_block_threshold() {
|
||||
let total_duration = stats.execution_duration + stats.state_hash_duration;
|
||||
if total_duration > threshold {
|
||||
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
|
||||
stats: stats.clone(),
|
||||
commit_duration: None,
|
||||
total_duration,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.execution_timing_stats.insert(executed.recovered_block().hash(), stats);
|
||||
}
|
||||
|
||||
@@ -3052,17 +3149,26 @@ where
|
||||
|
||||
/// Validates the payload attributes with respect to the header and fork choice state.
|
||||
///
|
||||
/// This is called during `engine_forkchoiceUpdated` when the CL provides payload attributes,
|
||||
/// indicating it wants the EL to start building a new block.
|
||||
///
|
||||
/// Runs [`PayloadValidator::validate_payload_attributes_against_header`](reth_engine_primitives::PayloadValidator::validate_payload_attributes_against_header) to ensure
|
||||
/// `payloadAttributes.timestamp > headBlock.timestamp` per the Engine API spec.
|
||||
///
|
||||
/// If validation passes, sends the attributes to the payload builder to start a new
|
||||
/// payload job. If it fails, returns `INVALID_PAYLOAD_ATTRIBUTES` without rolling back
|
||||
/// the forkchoice update.
|
||||
///
|
||||
/// Note: At this point, the fork choice update is considered to be VALID, however, we can still
|
||||
/// return an error if the payload attributes are invalid.
|
||||
fn process_payload_attributes(
|
||||
&self,
|
||||
attrs: T::PayloadAttributes,
|
||||
attributes: T::PayloadAttributes,
|
||||
head: &N::BlockHeader,
|
||||
state: ForkchoiceState,
|
||||
version: EngineApiMessageVersion,
|
||||
) -> OnForkChoiceUpdated {
|
||||
if let Err(err) =
|
||||
self.payload_validator.validate_payload_attributes_against_header(&attrs, head)
|
||||
self.payload_validator.validate_payload_attributes_against_header(&attributes, head)
|
||||
{
|
||||
warn!(target: "engine::tree", %err, ?head, "Invalid payload attributes");
|
||||
return OnForkChoiceUpdated::invalid_payload_attributes()
|
||||
@@ -3072,34 +3178,47 @@ where
|
||||
// forkchoiceState.headBlockHash and identified via buildProcessId value if
|
||||
// payloadAttributes is not null and the forkchoice state has been updated successfully.
|
||||
// The build process is specified in the Payload building section.
|
||||
match <T::PayloadBuilderAttributes as PayloadBuilderAttributes>::try_new(
|
||||
state.head_block_hash,
|
||||
attrs,
|
||||
version as u8,
|
||||
) {
|
||||
Ok(attributes) => {
|
||||
// send the payload to the builder and return the receiver for the pending payload
|
||||
// id, initiating payload job is handled asynchronously
|
||||
let pending_payload_id = self.payload_builder.send_new_payload(attributes);
|
||||
|
||||
// Client software MUST respond to this method call in the following way:
|
||||
// {
|
||||
// payloadStatus: {
|
||||
// status: VALID,
|
||||
// latestValidHash: forkchoiceState.headBlockHash,
|
||||
// validationError: null
|
||||
// },
|
||||
// payloadId: buildProcessId
|
||||
// }
|
||||
//
|
||||
// if the payload is deemed VALID and the build process has begun.
|
||||
OnForkChoiceUpdated::updated_with_pending_payload_id(
|
||||
PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)),
|
||||
pending_payload_id,
|
||||
)
|
||||
}
|
||||
Err(_) => OnForkChoiceUpdated::invalid_payload_attributes(),
|
||||
}
|
||||
let cache = if self.config.share_execution_cache_with_payload_builder() {
|
||||
self.payload_validator.cache_for(state.head_block_hash)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let trie_handle = if self.config.share_sparse_trie_with_payload_builder() {
|
||||
self.payload_validator.sparse_trie_handle_for(
|
||||
state.head_block_hash,
|
||||
head.state_root(),
|
||||
&self.state,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// send the payload to the builder and return the receiver for the pending payload
|
||||
// id, initiating payload job is handled asynchronously
|
||||
let pending_payload_id = self.payload_builder.send_new_payload(BuildNewPayload {
|
||||
parent_hash: state.head_block_hash,
|
||||
attributes,
|
||||
cache,
|
||||
trie_handle,
|
||||
});
|
||||
|
||||
// Client software MUST respond to this method call in the following way:
|
||||
// {
|
||||
// payloadStatus: {
|
||||
// status: VALID,
|
||||
// latestValidHash: forkchoiceState.headBlockHash,
|
||||
// validationError: null
|
||||
// },
|
||||
// payloadId: buildProcessId
|
||||
// }
|
||||
//
|
||||
// if the payload is deemed VALID and the build process has begun.
|
||||
OnForkChoiceUpdated::updated_with_pending_payload_id(
|
||||
PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)),
|
||||
pending_payload_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove all blocks up to __and including__ the given block number.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user