diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000000..d308228d379
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,209 @@
+version: 2.1
+
+# Runs this fork's validation workflows through pinned act because GitHub Actions is disabled.
+# Pull requests and branch pushes run the PR checks; merge queues add stage.yml. Event metadata is
+# generated once per CircleCI revision and shared by all shards.
+#
+# The read-only GitHub token is exposed to PR code, so its CircleCI context must remain
+# restricted to trusted contributors. Keep CircleCI auto-cancellation enabled.
+#
+# The act binary, checksum, and runner image are pinned. Compiler caches are no-ops because
+# act's local cache is not reusable across these machines; Pages plumbing is skipped after the
+# validation build. Lint and unit jobs are distributed; other workflows run up to four jobs.
+#
+# Excluded: pr-title.yml, label-pr.yml, and the privileged cyclops-audit.yml integration.
+
+jobs:
+ prepare-act-event:
+ machine:
+ image: ubuntu-2404:2025.09.1
+ resource_class: medium
+ steps:
+ - checkout
+ - run:
+ name: Snapshot GitHub event
+ command: |
+ mkdir -p /tmp/act-event
+ if ! .circleci/prepare-act-event.sh; then
+ echo "GitHub event preparation failed; act shards will report the failure" |
+ tee /tmp/act-event/failed
+ fi
+ - persist_to_workspace:
+ root: /tmp/act-event
+ paths:
+ - .
+
+ act:
+ parameters:
+ wf:
+ type: string
+ job:
+ type: string
+ default: ""
+ concurrent_jobs:
+ type: integer
+ default: 4
+ machine:
+ image: ubuntu-2404:2025.09.1
+ resource_class: xlarge
+ steps:
+ - checkout
+ - attach_workspace:
+ at: /tmp/act-event
+ - run:
+ name: Check event snapshot
+ command: |
+ if [ -f /tmp/act-event/failed ]; then
+ cat /tmp/act-event/failed >&2
+ exit 1
+ fi
+ - run:
+ name: Install pinned act
+ command: |
+ set -euo pipefail
+ readonly ACT_VERSION="0.2.89-action-cache-fix.1"
+ readonly ACT_RELEASE="v0.2.89-action-cache-fix.4"
+ readonly ACT_BINARY="act_Linux_x86_64"
+ readonly ACT_SHA256="28bfcf1502f59a834b14f2fb23c39e5b253c2234d875592b5aa0a175f9674dce"
+ curl --fail --silent --show-error --location \
+ "https://github.com/ethereum-optimism/act/releases/download/${ACT_RELEASE}/${ACT_BINARY}" \
+ --output "/tmp/${ACT_BINARY}"
+ echo "${ACT_SHA256} /tmp/${ACT_BINARY}" | sha256sum --check
+ sudo install -m 0755 "/tmp/${ACT_BINARY}" /usr/local/bin/act
+ test "$(act --version)" = "act version ${ACT_VERSION}"
+ - run:
+ name: Stub unsupported non-check actions
+ command: |
+ mkdir -p /tmp/noop-action
+ printf '%s\n' \
+ 'name: noop' \
+ 'description: disabled service plumbing under act' \
+ 'inputs:' \
+ ' cache-on-failure: {required: false}' \
+ ' cache-targets: {required: false}' \
+ 'runs:' \
+ ' using: composite' \
+ ' steps:' \
+ ' - run: echo "Service plumbing disabled under act"' \
+ ' shell: bash' \
+ > /tmp/noop-action/action.yml
+ - run:
+ name: Run << parameters.wf >> via act
+ no_output_timeout: 45m
+ command: |
+ set -euo pipefail
+ event_path=/tmp/act-event/github-event.json
+ event_name="$(jq -er 'if .pull_request then "pull_request" elif .merge_group then "merge_group" elif .ref and .after then "push" else error("unsupported event") end' "$event_path")"
+ base_ref="$(jq -r '(.pull_request.base.ref // .merge_group.base_ref // "") | sub("^refs/heads/"; "")' "$event_path")"
+ target_job="<< parameters.job >>"
+ # Validate documentation without entering Pages deployment on branch pushes.
+ if [ "<< parameters.wf >>" = "book.yml" ]; then
+ target_job=build
+ fi
+ act_job=()
+ if [ -n "$target_job" ]; then
+ case "$target_job" in
+ crate-checks-*)
+ partition="${target_job#crate-checks-}"
+ if [[ ! "$partition" =~ ^[1-9][0-9]*$ ]]; then
+ echo "invalid crate-checks partition: $partition" >&2
+ exit 1
+ fi
+ act_job=(-j crate-checks --matrix "partition:${partition}")
+ ;;
+ *)
+ act_job=(-j "$target_job")
+ ;;
+ esac
+ fi
+
+ act_env=(--env "RUSTC_WRAPPER=" --env "GITHUB_BASE_REF=${base_ref}")
+ if [ "<< parameters.wf >>" = "unit.yml" ] && [ "$target_job" = "state" ]; then
+ # EF tests use Rayon internally, so unrestricted nextest processes multiply
+ # runnable threads and starve valid_blocks past its upstream timeout.
+ act_env+=(--env "NEXTEST_TEST_THREADS=2")
+ fi
+ if [ "$event_name" != "pull_request" ]; then
+ act_env+=(
+ --env "GITHUB_REF=refs/heads/${CIRCLE_BRANCH}"
+ --env "GITHUB_SHA=${CIRCLE_SHA1}"
+ )
+ fi
+
+ act "$event_name" -W ".github/workflows/<< parameters.wf >>" \
+ "${act_job[@]}" \
+ --eventpath "$event_path" \
+ -P "ubuntu-latest=catthehacker/ubuntu@sha256:b839c14c4410998529ec18f951262bdf87a2b23bc1467304d07b491b9455e074" \
+ --concurrent-jobs << parameters.concurrent_jobs >> \
+ "${act_env[@]}" \
+ -s GITHUB_TOKEN="${MISE_GITHUB_TOKEN:?missing restricted read-only GitHub token}" \
+ --local-repository "mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696=/tmp/noop-action" \
+ --local-repository "Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4=/tmp/noop-action" \
+ --local-repository "actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d=/tmp/noop-action" \
+ --local-repository "actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9=/tmp/noop-action"
+
+workflows:
+ reth-act:
+ jobs:
+ - prepare-act-event:
+ context:
+ - circleci-repo-readonly-authenticated-github-token
+ - act:
+ context:
+ - circleci-repo-readonly-authenticated-github-token
+ name: "act-<< matrix.wf >>"
+ requires:
+ - prepare-act-event
+ matrix:
+ parameters:
+ wf:
+ - book.yml
+ - compact.yml
+ - e2e.yml
+ - grafana.yml
+ - integration.yml
+ - lint-actions.yml
+ - stage.yml
+ - act:
+ wf: lint.yml
+ concurrent_jobs: 1
+ context:
+ - circleci-repo-readonly-authenticated-github-token
+ name: "act-lint-<< matrix.job >>"
+ requires:
+ - prepare-act-event
+ matrix:
+ parameters:
+ job:
+ - clippy-binaries
+ - clippy
+ - wasm
+ - riscv
+ - crate-checks-1
+ - crate-checks-2
+ - crate-checks-3
+ - msrv
+ - docs
+ - fmt
+ - udeps
+ - book
+ - typos
+ - check-toml
+ - grafana
+ - no-test-deps
+ - feature-propagation
+ - deny
+ - act:
+ wf: unit.yml
+ concurrent_jobs: 1
+ context:
+ - circleci-repo-readonly-authenticated-github-token
+ name: "act-unit-<< matrix.job >>"
+ requires:
+ - prepare-act-event
+ matrix:
+ parameters:
+ job:
+ - test
+ - state
+ - doc
diff --git a/.circleci/prepare-act-event.sh b/.circleci/prepare-act-event.sh
new file mode 100755
index 00000000000..99cccb27f3c
--- /dev/null
+++ b/.circleci/prepare-act-event.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly OUTPUT_DIR="${ACT_EVENT_OUTPUT_DIR:-/tmp/act-event}"
+readonly EVENT_PATH="${OUTPUT_DIR}/github-event.json"
+
+mkdir -p "$OUTPUT_DIR"
+rm -f "$EVENT_PATH"
+
+: "${CIRCLE_SHA1:?missing CircleCI revision}"
+: "${CIRCLE_PROJECT_USERNAME:?missing CircleCI project owner}"
+: "${CIRCLE_PROJECT_REPONAME:?missing CircleCI project repository}"
+
+repository="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}"
+
+if [[ "${CIRCLE_BRANCH:-}" == gh-readonly-queue/* ]]; then
+ event_name="merge_group"
+elif [[ -n "${CIRCLE_PULL_REQUEST:-}" ]]; then
+ event_name="pull_request"
+else
+ event_name="push"
+fi
+
+case "$event_name" in
+ pull_request)
+ : "${CIRCLE_PULL_REQUEST:?pull-request trigger must provide CIRCLE_PULL_REQUEST}"
+ pr_number="${CIRCLE_PULL_REQUEST##*/}"
+ if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]]; then
+ echo "invalid pull-request URL: ${CIRCLE_PULL_REQUEST}" >&2
+ exit 1
+ fi
+
+ pull_request_path="${OUTPUT_DIR}/pull-request.json"
+ curl --fail --silent --show-error --location \
+ -H "Accept: application/vnd.github+json" \
+ -H "Authorization: Bearer ${MISE_GITHUB_TOKEN:?missing restricted read-only GitHub token}" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "https://api.github.com/repos/${repository}/pulls/${pr_number}" > "$pull_request_path"
+
+ event_head_sha="$(jq -er '.head.sha' "$pull_request_path")"
+ if [[ "$event_head_sha" != "$CIRCLE_SHA1" ]]; then
+ echo "PR head does not match CircleCI revision: ${CIRCLE_SHA1} != ${event_head_sha}" >&2
+ exit 1
+ fi
+
+ jq '{
+ action: "synchronize",
+ number: .number,
+ pull_request: .,
+ repository: .base.repo,
+ sender: .user
+ }' "$pull_request_path" > "$EVENT_PATH"
+ base_ref="$(jq -er '.base.ref | select(length > 0)' "$pull_request_path")"
+ head_ref="$(jq -er '.head.ref | select(length > 0)' "$pull_request_path")"
+ echo "act event: pull_request #${pr_number}, ${head_ref} -> ${base_ref}"
+ ;;
+ push)
+ : "${CIRCLE_BRANCH:?push trigger must provide CIRCLE_BRANCH}"
+ jq -n \
+ --arg ref "refs/heads/${CIRCLE_BRANCH}" \
+ --arg after "$CIRCLE_SHA1" \
+ '{ref: $ref, after: $after}' > "$EVENT_PATH"
+ echo "act event: push ${CIRCLE_BRANCH} at ${CIRCLE_SHA1}"
+ ;;
+ merge_group)
+ : "${CIRCLE_BRANCH:?merge-queue trigger must provide CIRCLE_BRANCH}"
+ if [[ ! "$CIRCLE_BRANCH" =~ ^gh-readonly-queue/(.+)/pr-([1-9][0-9]*)-[^/]+$ ]]; then
+ echo "merge_group requires a gh-readonly-queue//pr-- branch, got: ${CIRCLE_BRANCH}" >&2
+ exit 1
+ fi
+ base_ref="${BASH_REMATCH[1]}"
+
+ if ! base_sha="$(git rev-parse "${CIRCLE_SHA1}^1" 2>/dev/null)"; then
+ git fetch --no-tags --depth=2 origin "$CIRCLE_SHA1"
+ base_sha="$(git rev-parse "${CIRCLE_SHA1}^1")"
+ fi
+
+ jq -n \
+ --arg head_sha "$CIRCLE_SHA1" \
+ --arg head_ref "refs/heads/${CIRCLE_BRANCH}" \
+ --arg base_sha "$base_sha" \
+ --arg base_ref "refs/heads/${base_ref}" \
+ --arg repository "$repository" \
+ --arg repository_name "$CIRCLE_PROJECT_REPONAME" \
+ --arg repository_owner "$CIRCLE_PROJECT_USERNAME" \
+ '{
+ action: "checks_requested",
+ merge_group: {
+ head_sha: $head_sha,
+ head_ref: $head_ref,
+ base_sha: $base_sha,
+ base_ref: $base_ref
+ },
+ repository: {
+ full_name: $repository,
+ name: $repository_name,
+ owner: {login: $repository_owner}
+ }
+ }' > "$EVENT_PATH"
+ echo "act event: merge_group ${CIRCLE_BRANCH} -> ${base_ref} at ${base_sha}"
+ ;;
+esac
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 65d6fc7e40d..6887f88baf4 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -38,6 +38,7 @@ crates/storage/libmdbx-rs/ @shekhirin
crates/storage/nippy-jar/ @joshieDo @shekhirin
crates/storage/provider/ @joshieDo @shekhirin
crates/storage/storage-api/ @joshieDo
+crates/storage/storage-overlay/ @joshieDo @shekhirin @mediocregopher
crates/tasks/ @mattsse @DaniPopes
crates/tokio-util/ @mattsse
crates/tracing/ @mattsse @shekhirin
diff --git a/.github/config/bench-metrics-targets.json b/.github/config/bench-metrics-targets.json
index 4df96b969b5..aa9c839221b 100644
--- a/.github/config/bench-metrics-targets.json
+++ b/.github/config/bench-metrics-targets.json
@@ -14,11 +14,60 @@
}
],
"histograms": [
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_user_cpu_seconds",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_system_cpu_seconds",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_minor_page_faults",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_major_page_faults",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_voluntary_context_switches",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_involuntary_context_switches",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_block_input_operations",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
+ {
+ "query": "reth_consensus_engine_beacon_new_payload_thread_block_output_operations",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0
+ },
{
"query": "reth_sync_block_validation_state_root_histogram",
"target": "decrease",
"floor_pct": 5.0,
- "min_value_pct_total_latency": 5.0
+ "min_value_pct_total_latency": 5.0,
+ "unit": "seconds"
},
{
"query": "reth_trie_proof_task_account_worker_idle_time_seconds",
@@ -38,6 +87,13 @@
"floor_pct": 7.5,
"min_value_pct_total_latency": 15.0
},
+ {
+ "query": "reth_tree_root_sparse_trie_prune_duration_histogram",
+ "target": "decrease",
+ "floor_pct": 5.0,
+ "min_value_pct_total_latency": 0.0,
+ "unit": "seconds"
+ },
{
"query": "reth_tree_root_hashing_task_idle_time_seconds",
"target": "increase",
@@ -60,13 +116,15 @@
"query": "reth_storage_providers_database_save_blocks_commit_mdbx",
"target": "decrease",
"floor_pct": 5.0,
- "min_value_pct_total_latency": 13.6
+ "min_value_pct_total_latency": 13.6,
+ "unit": "seconds"
},
{
"query": "reth_storage_providers_database_save_blocks_commit_rocksdb",
"target": "decrease",
"floor_pct": 2.5,
- "min_value_pct_total_latency": 0.8
+ "min_value_pct_total_latency": 0.8,
+ "unit": "seconds"
},
{
"query": "reth_storage_providers_database_save_blocks_mdbx",
@@ -79,19 +137,22 @@
"query": "reth_storage_providers_database_save_blocks_rocksdb",
"target": "decrease",
"floor_pct": 2.5,
- "min_value_pct_total_latency": 14.2
+ "min_value_pct_total_latency": 14.2,
+ "unit": "seconds"
},
{
"query": "reth_storage_providers_database_save_blocks_sf",
"target": "decrease",
"floor_pct": 2.5,
- "min_value_pct_total_latency": 5.4
+ "min_value_pct_total_latency": 5.4,
+ "unit": "seconds"
},
{
"query": "reth_sync_block_validation_deferred_trie_compute_duration",
"target": "decrease",
"floor_pct": 2.5,
- "min_value_pct_total_latency": 1.4
+ "min_value_pct_total_latency": 1.4,
+ "unit": "seconds"
},
{
"query": "reth_executor_named_worker_queue_wait_duration_seconds",
diff --git a/.github/scripts/bench-job-summary.js b/.github/scripts/bench-job-summary.js
index 6187aad008f..e79085eb2fd 100644
--- a/.github/scripts/bench-job-summary.js
+++ b/.github/scripts/bench-job-summary.js
@@ -157,5 +157,5 @@ module.exports = async function ({ core, context, chartSha, grafanaUrl, logsUrl,
if (errors.trim()) md += '\n' + errors + '\n';
} catch {}
- await core.summary.addRaw(md).write();
+ await core.summary.addRaw(md).write({ overwrite: true });
};
diff --git a/.github/scripts/bench-reth-summary.py b/.github/scripts/bench-reth-summary.py
index 9b942a183af..7d1ad07a2f4 100755
--- a/.github/scripts/bench-reth-summary.py
+++ b/.github/scripts/bench-reth-summary.py
@@ -1659,7 +1659,9 @@ def generate_comparison_table(
big_blocks: bool = False,
warmup_blocks: str | None = None,
wait_time: str | None = None,
+ block_time: str | None = None,
bal_mode: str | None = None,
+ mode: str = "engine",
run_pairs: int | None = None,
) -> str:
"""Generate a markdown comparison table between baseline and feature."""
@@ -1715,13 +1717,15 @@ def pct(base: float, feat: float) -> float:
f"| Persist Wait | {fmt_ms(run1['mean_persist_ms'])} | {fmt_ms(run2['mean_persist_ms'])} | {change_str(persist_pct, persist_ci_pct, persist_floor, lower_is_better=True, informational=persist_informational)} |",
"",
]
- meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}"]
+ meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}", f"mode: {mode}"]
if warmup_blocks:
meta_parts.append(f"{warmup_blocks} warmup")
if run_pairs:
meta_parts.append(f"{run_pairs} run pairs")
if wait_time:
meta_parts.append(f"wait time: {wait_time}")
+ if block_time:
+ meta_parts.append(f"block time: {block_time}")
display_mode = display_bal_mode(bal_mode)
if big_blocks and display_mode:
meta_parts.append(f"BAL: {display_mode}")
@@ -1969,7 +1973,9 @@ def main():
parser.add_argument("--big-blocks", action="store_true", default=False, help="Big blocks mode")
parser.add_argument("--warmup-blocks", default=None, help="Number of warmup blocks")
parser.add_argument("--wait-time", default=None, help="Wait time interval used between blocks")
+ parser.add_argument("--block-time", default=None, help="RPC mode local block interval")
parser.add_argument("--bal-mode", default=None, help="BAL mode (true, feature, baseline)")
+ parser.add_argument("--mode", choices=("engine", "rpc"), default="engine", help="Benchmark execution mode")
parser.add_argument("--benchmark-id", default=os.environ.get("BENCH_ID"), help="Benchmark ID used for OTLP labels")
parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run")
parser.add_argument("--logs-url", default=None, help="Grafana Explore URL for benchmark logs")
@@ -2033,7 +2039,9 @@ def main():
big_blocks=args.big_blocks,
warmup_blocks=args.warmup_blocks,
wait_time=args.wait_time,
+ block_time=args.block_time,
bal_mode=bal_mode,
+ mode=args.mode,
run_pairs=args.run_pairs,
)
print(
@@ -2084,7 +2092,9 @@ def main():
"warmup_blocks": args.warmup_blocks,
"run_pairs": args.run_pairs,
"wait_time": args.wait_time,
+ "block_time": args.block_time,
"bal_mode": bal_mode,
+ "mode": args.mode,
"baseline": {
"name": baseline_name,
"ref": baseline_ref,
diff --git a/.github/scripts/bench-txgen-extract.sh b/.github/scripts/bench-txgen-extract.sh
index 39131424021..a513dcfd1d4 100755
--- a/.github/scripts/bench-txgen-extract.sh
+++ b/.github/scripts/bench-txgen-extract.sh
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
#
-# Pre-extracts txgen payloads (normal or big blocks) so they can be reused
+# Pre-extracts txgen payloads or transactions so they can be reused
# across multiple benchmark runs instead of re-fetching from the remote RPC
# every time.
#
# Starts a throwaway reth node to discover the snapshot's chain tip, extracts
-# payloads from BENCH_RPC_URL, then stops the node and recovers the snapshot.
+# data from BENCH_RPC_URL, then stops the node and recovers the snapshot.
#
# Usage: bench-txgen-extract.sh
#
@@ -13,15 +13,17 @@
# /all-blocks.ndjson (or all-big-blocks.ndjson)
# /warmup-blocks.ndjson
# /benchmark-blocks.ndjson
+# In RPC mode, the corresponding names end in `-transactions.ndjson`.
#
# Required env: SCHELK_MOUNT, BENCH_RPC_URL, BENCH_BLOCKS, BENCH_WARMUP_BLOCKS
-# Optional env: BENCH_BIG_BLOCKS, BENCH_BIG_BLOCKS_TARGET_GAS, BENCH_BAL
+# Optional env: BENCH_EXECUTION_MODE, BENCH_BIG_BLOCKS, BENCH_BIG_BLOCKS_TARGET_GAS, BENCH_BAL
set -euxo pipefail
BINARY="$1"
OUTPUT_DIR="$2"
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
+EXECUTION_MODE="${BENCH_EXECUTION_MODE:-engine}"
BAL_MODE="${BENCH_BAL:-false}"
INCLUDE_BAL=false
if [ "$BAL_MODE" != "false" ] && [ -n "$BAL_MODE" ]; then
@@ -123,19 +125,44 @@ fi
ALL_BLOCKS="$OUTPUT_DIR/all-blocks.ndjson"
WARMUP_FILE="$OUTPUT_DIR/warmup-blocks.ndjson"
BENCHMARK_FILE="$OUTPUT_DIR/benchmark-blocks.ndjson"
-
-if [ "$BIG_BLOCKS" = "true" ]; then
- ALL_BLOCKS="$OUTPUT_DIR/all-big-blocks.ndjson"
- WARMUP_FILE="$OUTPUT_DIR/warmup-big-blocks.ndjson"
- BENCHMARK_FILE="$OUTPUT_DIR/measured-big-blocks.ndjson"
-fi
-
EXTRACT_FROM=$(( HEAD_DEC + 1 ))
TXGEN_EXTRACT_ARGS=()
if [ "$INCLUDE_BAL" = "true" ]; then
TXGEN_EXTRACT_ARGS+=(--bal)
fi
-if [ "$BIG_BLOCKS" = "true" ]; then
+
+if [ "$EXECUTION_MODE" = "rpc" ]; then
+ ALL_BLOCKS="$OUTPUT_DIR/all-transactions.ndjson"
+ WARMUP_FILE="$OUTPUT_DIR/warmup-transactions.ndjson"
+ BENCHMARK_FILE="$OUTPUT_DIR/benchmark-transactions.ndjson"
+ if [ "$BIG_BLOCKS" = "true" ] || [ "$INCLUDE_BAL" = "true" ]; then
+ echo "::error::RPC mode does not support big blocks or BAL"
+ exit 1
+ fi
+
+ EXTRACT_TO=$(( HEAD_DEC + TOTAL ))
+ echo "Extracting transactions from blocks ${EXTRACT_FROM}..${EXTRACT_TO} for RPC benchmark (${WARMUP} warmup blocks, ${BLOCKS} measured blocks)"
+ if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
+ "$TXGEN_ETHEREUM" extract \
+ --rpc "$BENCH_RPC_URL" \
+ --from "$EXTRACT_FROM" \
+ --to "$(( HEAD_DEC + WARMUP ))" \
+ --format transactions \
+ -o "$WARMUP_FILE"
+ else
+ : > "$WARMUP_FILE"
+ fi
+ "$TXGEN_ETHEREUM" extract \
+ --rpc "$BENCH_RPC_URL" \
+ --from "$(( HEAD_DEC + WARMUP + 1 ))" \
+ --to "$EXTRACT_TO" \
+ --format transactions \
+ -o "$BENCHMARK_FILE"
+ cat "$WARMUP_FILE" "$BENCHMARK_FILE" > "$ALL_BLOCKS"
+elif [ "$BIG_BLOCKS" = "true" ]; then
+ ALL_BLOCKS="$OUTPUT_DIR/all-big-blocks.ndjson"
+ WARMUP_FILE="$OUTPUT_DIR/warmup-big-blocks.ndjson"
+ BENCHMARK_FILE="$OUTPUT_DIR/measured-big-blocks.ndjson"
echo "Extracting ${TOTAL} big blocks from ${EXTRACT_FROM} for txgen benchmark (${WARMUP} warmup, ${BLOCKS} measured, bal=${INCLUDE_BAL})"
"$TXGEN_ETHEREUM" extract-big-blocks \
--rpc "$BENCH_RPC_URL" \
@@ -155,13 +182,16 @@ else
-o "$ALL_BLOCKS"
fi
-# Split into warmup and measured files
-if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
- head -n "$WARMUP" "$ALL_BLOCKS" > "$WARMUP_FILE"
-else
- : > "$WARMUP_FILE"
+# Block output has one line per source block. Transaction output is extracted
+# separately above because it has one line per transaction.
+if [ "$EXECUTION_MODE" = "engine" ]; then
+ if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
+ head -n "$WARMUP" "$ALL_BLOCKS" > "$WARMUP_FILE"
+ else
+ : > "$WARMUP_FILE"
+ fi
+ awk -v warmup="$WARMUP" 'NR > warmup { print }' "$ALL_BLOCKS" > "$BENCHMARK_FILE"
fi
-awk -v warmup="$WARMUP" 'NR > warmup { print }' "$ALL_BLOCKS" > "$BENCHMARK_FILE"
if [ "$INCLUDE_BAL" = "true" ] && [ "$BAL_MODE" != "true" ]; then
echo "Writing no-BAL payload variants for selective BAL mode (${BAL_MODE})"
@@ -170,4 +200,4 @@ if [ "$INCLUDE_BAL" = "true" ] && [ "$BAL_MODE" != "true" ]; then
done
fi
-echo "Extraction complete: $(wc -l < "$ALL_BLOCKS") payloads in ${OUTPUT_DIR}"
+echo "Extraction complete: $(wc -l < "$ALL_BLOCKS") ${EXECUTION_MODE} records in ${OUTPUT_DIR}"
diff --git a/.github/scripts/bench-txgen-report-to-reth-csv.py b/.github/scripts/bench-txgen-report-to-reth-csv.py
index 6501abe617a..a9847014e45 100755
--- a/.github/scripts/bench-txgen-report-to-reth-csv.py
+++ b/.github/scripts/bench-txgen-report-to-reth-csv.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Convert txgen `bench send-blocks` JSON into legacy benchmark CSVs.
+"""Convert txgen `bench send-blocks` or `bench send` JSON into legacy benchmark CSVs.
The benchmark rendering pipeline still consumes `combined_latency.csv` and
`total_gas.csv`. This adapter lets the txgen runner reuse the existing
@@ -18,12 +18,17 @@ def opt_int(value, default=None):
return int(value)
-def block_latency_us(block: dict) -> tuple[int, int, int]:
+def block_latency_us(block: dict, fallback_block_time_ms: int = 0) -> tuple[int, int, int]:
# txgen currently records server newPayload latency in microseconds but
# client-side forkchoiceUpdated latency in milliseconds.
new_payload_us = opt_int(block.get("new_payload_server_latency_us"))
if new_payload_us is None:
- new_payload_us = opt_int(block.get("new_payload_ms"), 0) * 1000
+ new_payload_ms = opt_int(block.get("new_payload_ms"))
+ if new_payload_ms is None:
+ # `bench send` has no Engine API timings. Use locally mined block
+ # time so the existing gas/latency summaries remain meaningful.
+ new_payload_ms = opt_int(block.get("block_time_ms"), fallback_block_time_ms)
+ new_payload_us = new_payload_ms * 1000
fcu_us = opt_int(block.get("forkchoice_updated_ms"), 0) * 1000
return new_payload_us, fcu_us, new_payload_us + fcu_us
@@ -45,6 +50,11 @@ def main() -> None:
if not blocks:
raise SystemExit(f"txgen report {report_path} does not contain any blocks")
+ fallback_block_time_ms = next(
+ (int(block["block_time_ms"]) for block in blocks if block.get("block_time_ms") is not None),
+ 0,
+ )
+
combined_path = output_dir / "combined_latency.csv"
with combined_path.open("w", newline="") as f:
writer = csv.DictWriter(
@@ -64,7 +74,7 @@ def main() -> None:
)
writer.writeheader()
for block in blocks:
- new_payload_us, fcu_us, total_us = block_latency_us(block)
+ new_payload_us, fcu_us, total_us = block_latency_us(block, fallback_block_time_ms)
writer.writerow(
{
"block_number": block["number"],
@@ -89,7 +99,7 @@ def main() -> None:
)
writer.writeheader()
for block in blocks:
- _, _, total_us = block_latency_us(block)
+ _, _, total_us = block_latency_us(block, fallback_block_time_ms)
elapsed_us += total_us
writer.writerow(
{
diff --git a/.github/scripts/bench-txgen-run.sh b/.github/scripts/bench-txgen-run.sh
index 98ea31f04c3..b709cf7c544 100755
--- a/.github/scripts/bench-txgen-run.sh
+++ b/.github/scripts/bench-txgen-run.sh
@@ -1,14 +1,15 @@
#!/usr/bin/env bash
#
-# Runs a single txgen-backed Engine API benchmark cycle:
-# mount snapshot → start node → extract source blocks → warmup → send-blocks →
+# Runs a single txgen-backed benchmark cycle:
+# mount snapshot → start node → extract source blocks → warmup → replay →
# convert txgen JSON report into legacy benchmark CSVs.
#
# Usage: bench-txgen-run.sh