Compare commits

..

60 Commits

Author SHA1 Message Date
rakita
d3771d341a add reth-core t4 patch (6b12498) 2026-04-16 10:58:25 +02:00
rakita
9936061e5f bump revm, revm-inspectors, and alloy-evm to t4 branches
revm: 89ecb25dbe49e1c3a10d99529e42f027d0bd2386
revm-inspectors: c6f88bbe7186d863f4667dd43c42608eb7a8ba5c
alloy-evm: ff0bbec9ccaa818155e25003a77f4d73d350bbd7
2026-04-16 10:26:28 +02:00
Alexey Shekhirin
199b7460a9 refactor: decouple CachedStateMetrics from SavedCache (#23552) 2026-04-15 21:30:15 +00:00
Derek Cofausper
41592ef1f8 fix(download): respect --datadir.static-files during extraction (#23445)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
Co-authored-by: Emma Jamieson-Hoare <21029500+emmajam@users.noreply.github.com>
Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com>
2026-04-15 21:19:42 +00:00
Arsenii Kulikov
bdbb8df17e fix: validate against executor output gas used (#23569) 2026-04-15 20:37:14 +00:00
Dan Cline
f451ad5380 feat(cli): add reth download config options (#23513) 2026-04-15 20:23:08 +00:00
AJStonewee
6e4009eed4 fix(rpc): prevent panic in log subscription on broadcast lag (#23561) 2026-04-15 19:23:33 +00:00
Brian Picciano
cf29b3fffe perf(engine): include backpressure in newPayload latency metric (#23541)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-15 17:14:35 +00:00
Matthias Seitz
7fe76a83d1 fix(net): encode block access lists as raw BAL RLP (#23536)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-04-15 12:42:16 +00:00
Ishika Choudhury
b1cff500ad chore(BAL): remove debug_get_block_access_list (#23534) 2026-04-15 12:33:37 +00:00
figtracer
0b33057414 fix(init-state): write accounts directly with chunked commits (#23469) 2026-04-15 10:52:53 +00:00
Soubhik Singha Mahapatra
3891092ee9 chore: add amsterdam time to chainspec (#23526) 2026-04-15 10:29:14 +00:00
Emma Jamieson-Hoare
8784aa45fc chore: bump revm to v37 (EIP-8037 state gas) (#23191)
Co-authored-by: Federico Gimenez <federico.gimenez@gmail.com>
Co-authored-by: Federico Gimenez <fgimenez@users.noreply.github.com>
Co-authored-by: klkvr <klkvrr@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-15 10:08:12 +00:00
Brian Picciano
f1d90612e3 feat(ci): add slack=on-win mode to bench workflows (#23522)
Co-authored-by: Amp <amp@ampcode.com>
2026-04-15 09:45:37 +00:00
Derek Cofausper
03d69f59a5 chore(ci): add @ai investigate to bench failure alerts (#23520)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2026-04-15 09:19:14 +00:00
Ishika Choudhury
d372c8f5a9 chore(BAL): added gas limit fn to ExecutionPayload (#23518) 2026-04-15 09:01:35 +00:00
Arsenii Kulikov
dbb8495be1 fix: allow adding peers without overriding kind (#23516) 2026-04-14 21:00:39 +00:00
Ishika Choudhury
044db3ec95 feat: implement try into v6 (#23497)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
2026-04-14 20:04:21 +00:00
Matthias Seitz
13217d5517 feat(discv4): add AddBootNode command (#23515) 2026-04-14 19:32:38 +00:00
Matthias Seitz
0165569bc1 feat(net): add discv4/discv5 getters to NetworkHandle (#23514) 2026-04-14 19:25:54 +00:00
Brian Picciano
84c14fe0a8 ci(bench): replace no_slack boolean with slack dropdown (always/on-error/never) (#23501)
Co-authored-by: Amp <amp@ampcode.com>
2026-04-14 15:57:20 +00:00
Tim
5b4af55017 feat: add reqwest-rustls to support otlp endpoints with https (#23495) 2026-04-14 14:09:09 +00:00
Dan Cline
b8ab2c628e chore(cli): add binary name and chain detection in tempo download log (#23356)
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: Emma Jamieson-Hoare <21029500+emmajam@users.noreply.github.com>
2026-04-14 13:23:29 +00:00
Brian Picciano
766f4317a6 chore(bench): reduce default blocks to 200, warmup to 20 for big-blocks (#23494)
Co-authored-by: mediocregopher <mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-14 13:13:21 +00:00
Derek Cofausper
c20d897efe fix(node): downgrade prune config log from warn to info (#23493)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
2026-04-14 11:43:43 +00:00
Brian Picciano
ad1e8f2cea feat(bench): BAL capture, replay, and inline payload decoding (#23434)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: steven <corderosteven6@gmail.com>
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
2026-04-14 11:23:13 +00:00
Derek Cofausper
51309ff55c fix(bench): retry on all transport errors (#23491)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-04-14 10:42:05 +00:00
Soubhik Singha Mahapatra
e0aac5015f chore(BAL): added newPayloadV5 and getPayloadV6 (#23486)
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
2026-04-14 08:57:42 +00:00
Ishika Choudhury
3b8290439a chore(BAL): added helper functions for building (#23490) 2026-04-14 08:49:49 +00:00
yottaes
1a2836ff53 feat(rpc): support transactionReceipts subscription in eth_subscribe (#23485)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-14 08:42:01 +00:00
MagicJoshh
bce7368a82 fix(engine): use IndexSet for deterministic block buffer child ordering (#22676) 2026-04-13 19:18:23 +00:00
MagicJoshh
1e461ef281 fix(trie): terminate depth-first iterator on database error (#22709) 2026-04-13 17:48:07 +00:00
Ishika Choudhury
a5113622fd chore(BAL): added fcuv4 and EngineApiMessageVersion6 (#23480)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
2026-04-13 14:47:28 +00:00
stevencartavia
bfb7ab72f7 chore: bump alloy to 2.0.0 (#23407)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-13 13:51:19 +00:00
Matthias Seitz
3d5c29c179 feat(net): add enforce_enr_fork_id to DefaultNetworkArgs (#23477) 2026-04-13 13:00:59 +00:00
Ignacio Hagopian
a05960ab07 feat(stateless): make witness generation conform to the draft specs (#22289)
Signed-off-by: jsign <jsign.uy@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2026-04-13 13:34:00 +02:00
John Chase
6b499151d8 perf(txpool): use FxHashMap/FxHashSet for TxHash-heavy containers (#23037) 2026-04-13 11:22:21 +02:00
Matthias Seitz
a9bd38a43e perf(trie): parallelize merge_ancestors_into_overlay (#21473)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-04-12 15:27:30 +02:00
github-actions[bot]
a544d244d8 chore(deps): weekly cargo update (#23464)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-12 12:25:36 +02:00
Derek Cofausper
a550b7a7d3 perf(tracing): also disable target attribute in OTLP spans (#23462)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-04-11 16:54:06 +00:00
Derek Cofausper
7035bbcf3a refactor(examples): replace mev-share-sse with reqwest bytes_stream in beacon-api-sse (#23458)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
2026-04-11 10:33:34 +00:00
Tamjid Ahmed
0c278f5fab feat(eth-wire): introduce configurable maximum ETH message size acros… (#22668)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-11 06:23:12 +00:00
Arsh
03dd1c3ae2 fix(txpool): do not mark ExceedsFeeCap as a bad transaction (#23450) 2026-04-11 05:13:05 +00:00
Dan Cline
6aa2234d9a chore(cli): make --resumable default (#23451) 2026-04-11 04:49:16 +00:00
Derek Cofausper
5ae8f0bc54 perf(engine): downgrade sparse trie spans to trace (#23448)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
2026-04-11 00:24:25 +00:00
Derek Cofausper
e3536f768e perf(tracing): disable location and inactivity tracking in OTLP span export (#23447)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
2026-04-10 19:23:34 +00:00
Derek Cofausper
ff1a78e1ce ci: remove PGO from CI workflows (#23405)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2026-04-10 19:17:42 +00:00
Matthias Seitz
fc3f465321 fix(net): seed peer range from handshake status (#23446)
Co-authored-by: Weixie Cui <cuiweixie@gmail.com>
2026-04-10 14:06:10 +00:00
cui
b0956b12ae fix(rpc): paginate ots_getBlockTransactions in block order (#23442)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-10 13:32:28 +00:00
cui
a774920b78 fix(provider): size block_range buffer for inclusive span (#23443) 2026-04-10 13:07:59 +00:00
cui
77d5f86b42 fix(consensus): always validate minimum gas limit (#23441) 2026-04-10 12:55:11 +00:00
Hwangjae Lee
e118963b8f fix(rpc): preserve nested bundle structure in mev_simBundle logs (#20565)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-10 12:54:27 +00:00
Derek Cofausper
64f6117dc0 docs(trie): replace stale MultiProofTask references with SparseTrieCacheTask (#22780)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-10 11:30:42 +00:00
Emma Jamieson-Hoare
53fe0a077a bench: add release regression mode (#23416)
Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 11:13:24 +00:00
Brian Picciano
828965c39d perf(engine): improve BAL prewarm sparse-trie streaming (#23423)
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 10:11:16 +00:00
Emma Jamieson-Hoare
53e1ec81b3 docs: update README for Reth 2.0 (#23424)
Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 09:25:31 +00:00
dependabot[bot]
608c96791f chore(deps): bump tokio from 1.51.0 to 1.51.1 in the cargo-weekly group (#23410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 07:22:23 +00:00
Derek Cofausper
13ae241a0d ci: bump MSRV job runner to depot-ubuntu-latest-8 (#23432)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2026-04-10 06:45:30 +00:00
Alexey Shekhirin
cecbb4cc8c fix(cli): use recent speed instead of all-time average for download ETA (#23425) 2026-04-10 00:29:54 +00:00
Alexey Shekhirin
0ed4739482 fix(download): show error on retry and reset counter on progress (#23426) 2026-04-10 00:29:45 +00:00
169 changed files with 4698 additions and 2995 deletions

View File

@@ -20,11 +20,6 @@
# include dist directory, where the reth binary is located after compilation
!/dist
# include PGO build helper used by Dockerfile.depot
!/.github
!/.github/scripts
!/.github/scripts/build_pgo_bolt.sh
# include licenses
!LICENSE-*

View File

@@ -7,6 +7,7 @@
#
# Required env: SCHELK_MOUNT, BENCH_RPC_URL, BENCH_BLOCKS, BENCH_WARMUP_BLOCKS
# Optional env: BENCH_BIG_BLOCKS (true/false), BENCH_WORK_DIR (for big blocks path)
# BENCH_BAL (false/true/feature/baseline; only used with big blocks)
# BENCH_WAIT_TIME (duration like 500ms, default empty)
# BENCH_BASELINE_ARGS (extra reth node args for baseline runs)
# BENCH_FEATURE_ARGS (extra reth node args for feature runs)
@@ -249,11 +250,33 @@ fi
if [ "$BIG_BLOCKS" = "true" ]; then
# Big blocks mode: replay pre-generated payloads
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
BENCH_BAL_MODE="${BENCH_BAL:-false}"
BB_BENCH_ARGS=(--reth-new-payload)
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
BB_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
fi
case "$BENCH_BAL_MODE" in
false)
;;
true)
BB_BENCH_ARGS+=(--bal)
;;
baseline)
if [[ "$LABEL" == baseline* ]]; then
BB_BENCH_ARGS+=(--bal)
fi
;;
feature)
if [[ "$LABEL" == feature* ]]; then
BB_BENCH_ARGS+=(--bal)
fi
;;
*)
echo "::error::Unknown BENCH_BAL value: $BENCH_BAL_MODE"
exit 1
;;
esac
# Warmup
WARMUP="${BENCH_WARMUP_BLOCKS:-50}"

View File

@@ -111,6 +111,14 @@ def compute_stats(combined: list[dict]) -> dict:
wall_clock_s = sum(total_latencies_ms) / 1_000
mean_total_lat_ms = sum(total_latencies_ms) / n
# Persistence wait mean (for main table)
persist_values_ms = []
for r in combined:
v = r.get("persistence_wait_us")
if v is not None:
persist_values_ms.append(v / 1_000)
mean_persist_ms = sum(persist_values_ms) / len(persist_values_ms) if persist_values_ms else 0.0
return {
"n": n,
"mean_ms": mean_lat,
@@ -121,6 +129,7 @@ def compute_stats(combined: list[dict]) -> dict:
"mean_mgas_s": mean_mgas_s,
"wall_clock_s": wall_clock_s,
"mean_total_lat_ms": mean_total_lat_ms,
"mean_persist_ms": mean_persist_ms,
}
@@ -145,7 +154,7 @@ def compute_wait_stats(combined: list[dict], field: str) -> dict:
def _paired_data(
baseline: list[dict], feature: list[dict]
) -> tuple[list[tuple[float, float]], list[float], list[float], list[float]]:
) -> tuple[list[tuple[float, float]], list[float], list[float], list[float], list[float]]:
"""Match blocks and return paired latencies and per-block diffs.
Returns:
@@ -153,6 +162,7 @@ def _paired_data(
lat_diffs_ms: list of feature baseline latency diffs in ms
mgas_diffs: list of feature baseline Mgas/s diffs
total_lat_diffs_ms: list of feature baseline total latency diffs in ms
persist_diffs_ms: list of feature baseline persistence wait diffs in ms
"""
baseline_by_block = {r["block_number"]: r for r in baseline}
feature_by_block = {r["block_number"]: r for r in feature}
@@ -162,6 +172,7 @@ def _paired_data(
lat_diffs_ms = []
mgas_diffs = []
total_lat_diffs_ms = []
persist_diffs_ms = []
for bn in common_blocks:
b = baseline_by_block[bn]
f = feature_by_block[bn]
@@ -179,7 +190,10 @@ def _paired_data(
total_lat_diffs_ms.append(
f["total_latency_us"] / 1_000 - b["total_latency_us"] / 1_000
)
return pairs, lat_diffs_ms, mgas_diffs, total_lat_diffs_ms
b_persist = (b.get("persistence_wait_us") or 0) / 1_000
f_persist = (f.get("persistence_wait_us") or 0) / 1_000
persist_diffs_ms.append(f_persist - b_persist)
return pairs, lat_diffs_ms, mgas_diffs, total_lat_diffs_ms, persist_diffs_ms
def compute_paired_stats(
@@ -195,13 +209,15 @@ def compute_paired_stats(
all_lat_diffs = []
all_mgas_diffs = []
all_total_lat_diffs = []
all_persist_diffs = []
blocks_per_pair = []
for baseline, feature in zip(baseline_runs, feature_runs):
pairs, lat_diffs, mgas_diffs, total_lat_diffs = _paired_data(baseline, feature)
pairs, lat_diffs, mgas_diffs, total_lat_diffs, persist_diffs = _paired_data(baseline, feature)
all_pairs.extend(pairs)
all_lat_diffs.extend(lat_diffs)
all_mgas_diffs.extend(mgas_diffs)
all_total_lat_diffs.extend(total_lat_diffs)
all_persist_diffs.extend(persist_diffs)
blocks_per_pair.append(len(pairs))
if not all_lat_diffs:
@@ -245,6 +261,11 @@ def compute_paired_stats(
total_se = std_total_diff / math.sqrt(len(all_total_lat_diffs)) if all_total_lat_diffs else 0.0
wall_clock_ci_ms = T_CRITICAL * total_se
mean_persist_diff = sum(all_persist_diffs) / len(all_persist_diffs) if all_persist_diffs else 0.0
std_persist_diff = stddev(all_persist_diffs, mean_persist_diff) if len(all_persist_diffs) > 1 else 0.0
persist_se = std_persist_diff / math.sqrt(len(all_persist_diffs)) if all_persist_diffs else 0.0
persist_ci_ms = T_CRITICAL * persist_se
return {
"n": n,
"mean_diff_ms": mean_diff,
@@ -258,6 +279,7 @@ def compute_paired_stats(
"mean_mgas_diff": mean_mgas_diff,
"mgas_ci": mgas_ci,
"wall_clock_ci_ms": wall_clock_ci_ms,
"persist_ci_ms": persist_ci_ms,
"blocks": max(blocks_per_pair),
}
@@ -290,6 +312,14 @@ def fmt_s(v: float) -> str:
return f"{v:.2f}s"
def display_bal_mode(bal_mode: str | None) -> str | None:
if not bal_mode or bal_mode == "false":
return None
if bal_mode == "both":
return "true"
return bal_mode
def significance(pct: float, ci_pct: float, lower_is_better: bool) -> str:
"""Return significance label: 'good', 'bad', or 'neutral'."""
significant = abs(pct) > ci_pct
@@ -328,6 +358,7 @@ def compute_changes(
("p99", "p99_ms", "p99_ci_ms", "p99_ms", True),
("mgas_s", "mean_mgas_s", "mgas_ci", "mean_mgas_s", False),
("wall_clock", "wall_clock_s", "wall_clock_ci_ms", "mean_total_lat_ms", True),
("persist_wait", "mean_persist_ms", "persist_ci_ms", "mean_persist_ms", True),
]
changes = {}
for name, stat_key, ci_key, base_key, lower_is_better in metrics:
@@ -353,6 +384,7 @@ def generate_comparison_table(
big_blocks: bool = False,
warmup_blocks: str | None = None,
wait_time: str | None = None,
bal_mode: str | None = None,
) -> str:
"""Generate a markdown comparison table between baseline and feature."""
n = paired["blocks"]
@@ -368,6 +400,8 @@ def generate_comparison_table(
p90_pct = pct(run1["p90_ms"], run2["p90_ms"])
p99_pct = pct(run1["p99_ms"], run2["p99_ms"])
persist_pct = pct(run1["mean_persist_ms"], run2["mean_persist_ms"])
# Bootstrap CIs as % of baseline percentile
p50_ci_pct = paired["p50_ci_ms"] / run1["p50_ms"] * 100.0 if run1["p50_ms"] > 0 else 0.0
p90_ci_pct = paired["p90_ci_ms"] / run1["p90_ms"] * 100.0 if run1["p90_ms"] > 0 else 0.0
@@ -377,6 +411,7 @@ def generate_comparison_table(
lat_ci_pct = paired["ci_ms"] / run1["mean_ms"] * 100.0 if run1["mean_ms"] > 0 else 0.0
mgas_ci_pct = paired["mgas_ci"] / run1["mean_mgas_s"] * 100.0 if run1["mean_mgas_s"] > 0 else 0.0
wall_ci_pct = paired["wall_clock_ci_ms"] / run1["mean_total_lat_ms"] * 100.0 if run1["mean_total_lat_ms"] > 0 else 0.0
persist_ci_pct = paired["persist_ci_ms"] / run1["mean_persist_ms"] * 100.0 if run1["mean_persist_ms"] > 0 else 0.0
base_url = f"https://github.com/{repo}/commit"
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
@@ -392,6 +427,7 @@ def generate_comparison_table(
f"| P99 | {fmt_ms(run1['p99_ms'])} | {fmt_ms(run2['p99_ms'])} | {change_str(p99_pct, p99_ci_pct, lower_is_better=True)} |",
f"| Mgas/s | {fmt_mgas(run1['mean_mgas_s'])} | {fmt_mgas(run2['mean_mgas_s'])} | {change_str(gas_pct, mgas_ci_pct, lower_is_better=False)} |",
f"| Wall Clock | {fmt_s(run1['wall_clock_s'])} | {fmt_s(run2['wall_clock_s'])} | {change_str(wall_pct, wall_ci_pct, lower_is_better=True)} |",
f"| Persist Wait | {fmt_ms(run1['mean_persist_ms'])} | {fmt_ms(run2['mean_persist_ms'])} | {change_str(persist_pct, persist_ci_pct, lower_is_better=True)} |",
"",
]
meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}"]
@@ -399,6 +435,9 @@ def generate_comparison_table(
meta_parts.append(f"{warmup_blocks} warmup")
if wait_time:
meta_parts.append(f"wait time: {wait_time}")
display_mode = display_bal_mode(bal_mode)
if big_blocks and display_mode:
meta_parts.append(f"BAL: {display_mode}")
lines.append(f"*{', '.join(meta_parts)}*")
return "\n".join(lines)
@@ -481,6 +520,7 @@ 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("--bal-mode", default=None, help="BAL mode (true, feature, baseline)")
parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run")
args = parser.parse_args()
@@ -520,6 +560,7 @@ def main():
baseline_name = args.baseline_name or "baseline"
feature_name = args.feature_name or "feature"
feature_sha = args.feature_ref or "unknown"
bal_mode = display_bal_mode(args.bal_mode)
comparison_table = generate_comparison_table(
baseline_stats,
@@ -533,6 +574,7 @@ def main():
big_blocks=args.big_blocks,
warmup_blocks=args.warmup_blocks,
wait_time=args.wait_time,
bal_mode=bal_mode,
)
print(f"Generated comparison ({paired_stats['n']} paired blocks, "
f"mean diff {paired_stats['mean_diff_ms']:+.3f}ms ± {paired_stats['ci_ms']:.3f}ms)")
@@ -566,6 +608,7 @@ def main():
"big_blocks": args.big_blocks,
"warmup_blocks": args.warmup_blocks,
"wait_time": args.wait_time,
"bal_mode": bal_mode,
"baseline": {
"name": baseline_name,
"ref": baseline_ref,

View File

@@ -2,17 +2,20 @@
#
# Resolves baseline and feature refs for scheduled benchmark runs.
#
# Supports two modes:
# Supports three 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.
# release — Compares the latest GitHub release tag against the current
# nightly Docker build. Baseline is the release tag commit,
# feature is the nightly commit.
#
# Usage: bench-scheduled-refs.sh <force> <mode>
# force — "true" to run even if no new commit (bypass skip logic)
# mode — "nightly" or "hourly"
# mode — "nightly", "hourly", or "release"
#
# Outputs (via GITHUB_OUTPUT):
# baseline-ref — commit SHA for baseline
@@ -21,10 +24,12 @@
# 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)
# release-tag — release tag name (release mode only, e.g. "v2.0.0")
#
# Reads:
# state/nightly-last-feature-ref (nightly, from decofe/reth-bench-charts repo)
# state/hourly-last-feature-ref (hourly, from decofe/reth-bench-charts repo)
# state/release-last-feature-ref (release, from decofe/reth-bench-charts repo)
#
# Requires: gh (GitHub CLI), jq, date, git (hourly mode), curl, DEREK_TOKEN env
set -euxo pipefail
@@ -121,6 +126,106 @@ if [ "$MODE" = "hourly" ]; then
exit 0
fi
# ==========================================================================
# Release mode: compare latest GitHub release tag vs current nightly build
# ==========================================================================
if [ "$MODE" = "release" ]; then
# --- Step 1: Resolve feature ref from latest nightly Docker build ---
echo "::group::Querying latest nightly docker build"
RUNS_JSON=$(gh run list \
-R "$REPO" \
--workflow=docker.yml \
--event=schedule \
--status=completed \
--limit 5 \
--json headSha,createdAt,conclusion)
LATEST=$(echo "$RUNS_JSON" | jq -r '[.[] | select(.conclusion == "success")] | first // empty')
if [ -z "$LATEST" ]; then
echo "::error::No successful scheduled docker.yml run found in the last 5 runs"
exit 1
fi
FEATURE_REF=$(echo "$LATEST" | jq -r '.headSha')
echo "Nightly commit (feature): $FEATURE_REF"
echo "::endgroup::"
# --- Step 2: Resolve baseline ref from latest GitHub release ---
echo "::group::Resolving latest release tag"
RELEASE_JSON=$(gh release view --repo "$REPO" --json tagName,targetCommitish,publishedAt 2>/dev/null || echo "{}")
RELEASE_TAG=$(echo "$RELEASE_JSON" | jq -r '.tagName // empty')
if [ -z "$RELEASE_TAG" ]; then
echo "::error::No release found on $REPO"
exit 1
fi
# Resolve the tag to a commit SHA
BASELINE_REF=$(gh api "repos/$REPO/git/ref/tags/$RELEASE_TAG" --jq '.object.sha' 2>/dev/null || true)
# If tag points to an annotated tag object, dereference to the commit
if [ -n "$BASELINE_REF" ]; then
OBJ_TYPE=$(gh api "repos/$REPO/git/tags/$BASELINE_REF" --jq '.object.type' 2>/dev/null || echo "commit")
if [ "$OBJ_TYPE" = "commit" ]; then
BASELINE_REF=$(gh api "repos/$REPO/git/tags/$BASELINE_REF" --jq '.object.sha' 2>/dev/null || echo "$BASELINE_REF")
fi
fi
if [ -z "$BASELINE_REF" ]; then
echo "::error::Could not resolve release tag $RELEASE_TAG to a commit"
exit 1
fi
echo "Release tag: $RELEASE_TAG"
echo "Release commit (baseline): $BASELINE_REF"
echo "::endgroup::"
# --- Step 3: Read last successful feature ref from charts repo ---
echo "::group::Reading persisted state"
LAST_FEATURE_REF=""
STATE_URL="https://raw.githubusercontent.com/decofe/reth-bench-charts/state/state/release-last-feature-ref"
if RAW=$(curl -sfL -H "Authorization: token ${DEREK_TOKEN}" "$STATE_URL"); then
LAST_FEATURE_REF=$(echo "$RAW" | tr -d '[:space:]')
echo "Previous feature ref: $LAST_FEATURE_REF"
else
echo "No persisted state found (first run)"
fi
echo "::endgroup::"
# --- Step 4: Skip logic ---
echo "::group::Resolving skip logic"
SHOULD_SKIP="false"
if [ -n "$LAST_FEATURE_REF" ] && [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
echo "No new nightly, but force=true — running anyway"
else
SHOULD_SKIP="true"
echo "No new nightly since last release regression run — will skip"
fi
else
echo "New nightly detected or first run"
fi
echo "Baseline: $BASELINE_REF ($RELEASE_TAG)"
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="
echo "long-running=false"
echo "release-tag=$RELEASE_TAG"
} >> "$GITHUB_OUTPUT"
exit 0
fi
# ==========================================================================
# Nightly mode: query latest Docker nightly build (original logic)
# ==========================================================================

View File

@@ -250,6 +250,8 @@ async function success({ core, context }) {
}
}
const slackMode = process.env.BENCH_SLACK || 'always';
// Post to public channel if any metric shows significant improvement or regression
const channel = process.env.SLACK_BENCH_CHANNEL;
let postedToChannel = false;
@@ -264,6 +266,14 @@ async function success({ core, context }) {
}
}
// In on-win mode, only notify on improvement — skip DM fallback entirely
if (slackMode === 'on-win') {
if (!postedToChannel) {
core.info('on-win mode: no improvement detected, skipping all notifications');
}
return;
}
// DM the actor only when results were not posted to the public channel
if (!postedToChannel) {
if (actorSlackId) {

View File

@@ -39,10 +39,25 @@ function loadSamplyUrls(workDir) {
return urls;
}
function balModeLabel(mode) {
switch (mode) {
case 'true':
case 'feature':
case 'baseline':
return mode;
case 'both':
return 'true';
default:
return '';
}
}
function blocksLabel(summary) {
const parts = [];
if (summary.big_blocks) {
parts.push({ key: 'Big Blocks', value: summary.blocks });
const balMode = balModeLabel(summary.bal_mode || summary.bal || process.env.BENCH_BAL || 'false');
if (balMode) parts.push({ key: 'BAL', value: balMode });
} else {
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
if (warmup) parts.push({ key: 'Warmup', value: warmup });
@@ -68,6 +83,7 @@ function metricRows(summary) {
{ label: 'P99', baseline: fmtMs(b.p99_ms), feature: fmtMs(f.p99_ms), change: fmtChange(c.p99) },
{ label: 'Mgas/s', baseline: fmtMgas(b.mean_mgas_s), feature: fmtMgas(f.mean_mgas_s), change: fmtChange(c.mgas_s) },
{ label: 'Wall Clock', baseline: fmtS(b.wall_clock_s), feature: fmtS(f.wall_clock_s), change: fmtChange(c.wall_clock) },
{ label: 'Persist Wait', baseline: fmtMs(b.mean_persist_ms || 0), feature: fmtMs(f.mean_persist_ms || 0), change: fmtChange(c.persist_wait) },
];
}

View File

@@ -1,414 +0,0 @@
#!/usr/bin/env bash
#
# Full PGO+BOLT optimized build for reth using real reth-bench workloads.
#
# Phases:
# 1. Build PGO-instrumented reth, run reth-bench → collect PGO profiles
# 2. Build BOLT-instrumented reth (with PGO), run reth-bench → collect BOLT profiles
# 3. Build final PGO+BOLT optimized binary
#
# Required environment variables:
# DATADIR - Path to reth datadir (must already contain chain data)
# RPC_URL - Source RPC URL for reth-bench to fetch payloads from
#
# Optional environment variables:
# PGO_BLOCKS - Number of blocks for PGO profiling (default: 20)
# BOLT_BLOCKS - Number of blocks for BOLT profiling (default: 20)
# SKIP_BOLT - Temporarily skip BOLT phases (default: false)
# STRIP_SYMBOLS - Strip debug symbols from output binary (default: true)
# COLLECT_PGO_ONLY - Stop after producing merged.profdata (default: false)
# PGO_PROFDATA - Path to pre-collected merged.profdata (optional)
# PROFILE - Cargo profile (default: maxperf-symbols)
# FEATURES - Cargo features (default: jemalloc,asm-keccak,min-debug-logs)
# TARGET - Target triple (default: auto-detected)
# EXTRA_RUSTFLAGS - Additional RUSTFLAGS (e.g. -C target-cpu=x86-64-v3)
#
# Output:
# target/$PROFILE_DIR/reth — final optimized binary
set -euxo pipefail
gha_section_start() {
local title="$1"
if [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "::group::$title"
else
echo ""
echo "=== $title ==="
fi
}
gha_section_end() {
if [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "::endgroup::"
fi
}
cd "$(dirname "$0")/../.."
# ── Configuration ──────────────────────────────────────────────────────────────
PGO_BLOCKS="${PGO_BLOCKS:-20}"
BOLT_BLOCKS="${BOLT_BLOCKS:-20}"
SKIP_BOLT="${SKIP_BOLT:-false}"
STRIP_SYMBOLS="${STRIP_SYMBOLS:-true}"
COLLECT_PGO_ONLY="${COLLECT_PGO_ONLY:-false}"
PROFILE="${PROFILE:-maxperf-symbols}"
FEATURES="${FEATURES:-jemalloc,asm-keccak,min-debug-logs}"
TARGET="${TARGET:-$(rustc -Vv | grep host | cut -d' ' -f2)}"
BASE_RUSTFLAGS="${RUSTFLAGS:-}"
EXTRA_RUSTFLAGS="${EXTRA_RUSTFLAGS:-}"
COMBINED_RUSTFLAGS="$BASE_RUSTFLAGS $EXTRA_RUSTFLAGS"
PGO_PROFDATA="${PGO_PROFDATA:-}"
DATADIR="${DATADIR:-}"
RPC_URL="${RPC_URL:-}"
SKIP_BOLT_BOOL=false
if [[ "${SKIP_BOLT,,}" == "true" || "$SKIP_BOLT" == "1" ]]; then
SKIP_BOLT_BOOL=true
fi
STRIP_SYMBOLS_BOOL=false
if [[ "${STRIP_SYMBOLS,,}" == "true" || "$STRIP_SYMBOLS" == "1" ]]; then
STRIP_SYMBOLS_BOOL=true
fi
COLLECT_PGO_ONLY_BOOL=false
if [[ "${COLLECT_PGO_ONLY,,}" == "true" || "$COLLECT_PGO_ONLY" == "1" ]]; then
COLLECT_PGO_ONLY_BOOL=true
fi
USE_PRECOLLECTED_PGO=false
if [ -n "$PGO_PROFDATA" ]; then
if [ ! -f "$PGO_PROFDATA" ]; then
echo "error: PGO_PROFDATA points to a missing file: $PGO_PROFDATA"
exit 1
fi
USE_PRECOLLECTED_PGO=true
fi
NEEDS_BENCH_WORKLOAD=true
if [ "$USE_PRECOLLECTED_PGO" = true ] && [ "$SKIP_BOLT_BOOL" = true ]; then
NEEDS_BENCH_WORKLOAD=false
fi
if [ "$NEEDS_BENCH_WORKLOAD" = true ]; then
: "${DATADIR:?DATADIR must be set to the reth data directory}"
: "${RPC_URL:?RPC_URL must be set}"
fi
if [[ "$PROFILE" == dev ]]; then
PROFILE_DIR=debug
else
PROFILE_DIR=$PROFILE
fi
MANIFEST_PATH="bin/reth"
LLVM_VERSION=$(rustc -Vv | grep -oP 'LLVM version: \K\d+')
PGO_DIR="$PWD/target/pgo-profiles"
BOLT_DIR="$PWD/target/bolt-profiles"
CARGO_ARGS=(--profile "$PROFILE" --features "$FEATURES" --manifest-path "$MANIFEST_PATH/Cargo.toml" --bin "reth" --locked)
# Enable debug symbols for BOLT (requires symbols to reorder code).
# Strip them at the end.
PROFILE_UPPER=$(echo "$PROFILE" | tr '[:lower:]-' '[:upper:]_')
export "CARGO_PROFILE_${PROFILE_UPPER}_STRIP=debuginfo"
gha_section_start "Full PGO+BOLT Build"
echo "Binary: reth"
echo "Manifest: $MANIFEST_PATH"
echo "Target: $TARGET"
echo "Profile: $PROFILE"
echo "Features: $FEATURES"
echo "LLVM: $LLVM_VERSION"
echo "PGO blocks: $PGO_BLOCKS"
echo "BOLT blocks: $BOLT_BLOCKS"
echo "Skip BOLT: $SKIP_BOLT"
echo "Strip symbols: $STRIP_SYMBOLS"
echo "Collect only: $COLLECT_PGO_ONLY"
echo "PGO profdata: ${PGO_PROFDATA:-<collect with reth-bench>}"
echo "RUSTFLAGS: ${BASE_RUSTFLAGS:-<unset>}"
echo "EXTRA_RUSTFLAGS: ${EXTRA_RUSTFLAGS:-<unset>}"
if [ "$NEEDS_BENCH_WORKLOAD" = true ]; then
echo "Datadir: $DATADIR"
echo "RPC URL: $RPC_URL"
else
echo "Datadir: <not required>"
echo "RPC URL: <not required>"
fi
gha_section_end
# ── Prerequisites ──────────────────────────────────────────────────────────────
gha_section_start "Installing prerequisites"
rustup component add llvm-tools-preview
LLVM_PROFDATA=$(find "$(rustc --print sysroot)" -name llvm-profdata -type f | head -1)
if [ -z "$LLVM_PROFDATA" ]; then
echo "error: llvm-profdata not found"
exit 1
fi
install_bolt() {
if command -v llvm-bolt &>/dev/null; then
echo "BOLT already installed"
return
fi
echo "Installing BOLT from apt.llvm.org..."
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null
CODENAME=$(lsb_release -cs)
echo "deb http://apt.llvm.org/$CODENAME/ llvm-toolchain-$CODENAME-$LLVM_VERSION main" | sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null
sudo apt-get update -qq
sudo apt-get install -y -qq "bolt-$LLVM_VERSION"
sudo ln -sf "/usr/bin/llvm-bolt-$LLVM_VERSION" /usr/local/bin/llvm-bolt
sudo ln -sf "/usr/bin/merge-fdata-$LLVM_VERSION" /usr/local/bin/merge-fdata
}
if [ "$SKIP_BOLT_BOOL" = true ]; then
echo "Skipping BOLT installation (SKIP_BOLT=$SKIP_BOLT)"
else
install_bolt
fi
gha_section_end
if [ "$NEEDS_BENCH_WORKLOAD" = true ]; then
# Build reth-bench once (non-instrumented) — reused for both phases.
gha_section_start "Building reth-bench"
RUSTFLAGS="$COMBINED_RUSTFLAGS" \
cargo build --profile "$PROFILE" --features "$FEATURES" \
--manifest-path bin/reth-bench/Cargo.toml --bin reth-bench --locked
RETH_BENCH_BIN="$(find target -name reth-bench -type f -executable | head -1)"
echo "reth-bench: $RETH_BENCH_BIN"
gha_section_end
else
gha_section_start "Building reth-bench"
echo "Skipping reth-bench build (pre-collected PGO with SKIP_BOLT=true)"
gha_section_end
fi
# ── Helpers ────────────────────────────────────────────────────────────────────
RETH_PID=
cleanup() {
if [ -n "${RETH_PID:-}" ] && kill -0 "$RETH_PID" 2>/dev/null; then
echo "Stopping reth (pid $RETH_PID)..."
sudo kill "$RETH_PID" 2>/dev/null || true
for i in $(seq 1 60); do
sudo kill -0 "$RETH_PID" 2>/dev/null || break
if [ $((i % 10)) -eq 0 ]; then
echo " waiting... (${i}s)"
fi
sleep 1
done
sudo kill -9 "$RETH_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
# Start reth, wait for RPC, run reth-bench, then stop reth.
# Arguments: $1 = reth binary path, $2 = number of blocks, $3 = log label
run_bench_workload() {
local reth_bin="$1" blocks="$2" label="$3"
local http_port=8545 authrpc_port=8551
echo "--- Starting reth ($label) ---"
sudo "$reth_bin" node \
--datadir "$DATADIR" \
--log.file.directory "/tmp/reth-${label}-logs" \
--engine.accept-execution-requests-hash \
--http --http.port "$http_port" \
--authrpc.port "$authrpc_port" \
--disable-discovery --no-persist-peers \
> "/tmp/reth-${label}.log" 2>&1 &
RETH_PID=$!
echo "Waiting for reth RPC..."
for i in $(seq 1 120); do
if curl -sf "http://127.0.0.1:$http_port" -X POST \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
> /dev/null 2>&1; then
echo "reth is ready after ${i}s"
break
fi
if [ "$i" -eq 120 ]; then
echo "error: reth failed to start within 120s"
cat "/tmp/reth-${label}.log"
exit 1
fi
sleep 1
done
echo "Running reth-bench ($blocks blocks)..."
"$RETH_BENCH_BIN" new-payload-fcu \
--rpc-url "$RPC_URL" \
--engine-rpc-url "http://127.0.0.1:$authrpc_port" \
--jwt-secret "$DATADIR/jwt.hex" \
--advance "$blocks" \
--reth-new-payload 2>&1 | sed -u "s/^/[$label] /"
echo "Stopping reth ($label)..."
sudo kill "$RETH_PID" 2>/dev/null || true
for i in $(seq 1 60); do
sudo kill -0 "$RETH_PID" 2>/dev/null || break
sleep 1
done
sudo kill -9 "$RETH_PID" 2>/dev/null || true
RETH_PID=
}
publish_binary() {
local source_bin="$1"
for out in "target/$TARGET/$PROFILE_DIR" "target/$PROFILE_DIR"; do
local destination="$out/reth"
mkdir -p "$out"
# Skip copying when source and destination resolve to the same inode.
if [ -e "$destination" ] && [ "$source_bin" -ef "$destination" ]; then
continue
fi
cp "$source_bin" "$destination"
done
}
if [ "$USE_PRECOLLECTED_PGO" = true ]; then
gha_section_start "Phase 1: Using Pre-Collected PGO Profile"
rm -rf "$PGO_DIR"
mkdir -p "$PGO_DIR"
cp "$PGO_PROFDATA" "$PGO_DIR/merged.profdata"
echo "Using pre-collected profile: $PGO_PROFDATA"
echo "PGO profile: $PGO_DIR/merged.profdata ($(ls -lh "$PGO_DIR/merged.profdata" | awk '{print $5}'))"
gha_section_end
else
# ── Phase 1: PGO profile collection ───────────────────────────────────────
gha_section_start "Phase 1: PGO Profile Collection"
rm -rf "$PGO_DIR"
mkdir -p "$PGO_DIR"
echo "Building PGO-instrumented binary..."
RUSTFLAGS="-Cprofile-generate=$PGO_DIR -Crelocation-model=pic $COMBINED_RUSTFLAGS" \
cargo build "${CARGO_ARGS[@]}" --target "$TARGET"
PGO_RETH_BIN="$PWD/target/$TARGET/$PROFILE_DIR/reth"
echo "Instrumented binary: $PGO_RETH_BIN ($(ls -lh "$PGO_RETH_BIN" | awk '{print $5}'))"
run_bench_workload "$PGO_RETH_BIN" "$PGO_BLOCKS" "pgo"
# Fix ownership if reth ran as root.
sudo chown -R "$(id -un):$(id -gn)" "$PGO_DIR" 2>/dev/null || true
# Merge PGO profiles.
echo "Merging PGO profiles..."
PROFRAW_COUNT=$(find "$PGO_DIR" -name '*.profraw' | wc -l)
echo "Found $PROFRAW_COUNT .profraw files"
if [ "$PROFRAW_COUNT" -eq 0 ]; then
echo "error: no .profraw files — instrumented binary did not produce profiles"
exit 1
fi
"$LLVM_PROFDATA" merge -o "$PGO_DIR/merged.profdata" "$PGO_DIR"/*.profraw
echo "PGO profile: $PGO_DIR/merged.profdata ($(ls -lh "$PGO_DIR/merged.profdata" | awk '{print $5}'))"
gha_section_end
fi
if [ "$COLLECT_PGO_ONLY_BOOL" = true ]; then
gha_section_start "PGO Collection Complete"
echo "COLLECT_PGO_ONLY=true, skipping PGO/BOLT optimized binary build"
echo "Profile: $PGO_DIR/merged.profdata"
gha_section_end
exit 0
fi
if [ "$SKIP_BOLT_BOOL" = true ]; then
gha_section_start "BOLT Phase Skipped"
echo "SKIP_BOLT=$SKIP_BOLT, building PGO-only binary"
echo "Building PGO-optimized binary..."
RUSTFLAGS="-Cprofile-use=$PGO_DIR/merged.profdata $COMBINED_RUSTFLAGS" \
cargo build "${CARGO_ARGS[@]}" --target "$TARGET"
BUILT_BIN="$PWD/target/$TARGET/$PROFILE_DIR/reth"
if [ "$STRIP_SYMBOLS_BOOL" = true ]; then
echo "Stripping debug symbols..."
strip "$BUILT_BIN"
else
echo "Skipping strip (STRIP_SYMBOLS=$STRIP_SYMBOLS)"
fi
publish_binary "$BUILT_BIN"
gha_section_end
else
# ── Phase 2: BOLT profile collection (with PGO) ──────────────────────────
gha_section_start "Phase 2: BOLT Profile Collection (with PGO)"
rm -rf "$BOLT_DIR"
mkdir -p "$BOLT_DIR"
echo "Building BOLT-instrumented binary with PGO..."
# --emit-relocs preserves relocation entries in the binary, required by llvm-bolt -instrument
RUSTFLAGS="-Cprofile-use=$PGO_DIR/merged.profdata -Clink-arg=-Wl,--emit-relocs $COMBINED_RUSTFLAGS" \
cargo build "${CARGO_ARGS[@]}" --target "$TARGET"
# Instrument with BOLT
BUILT_BIN="$PWD/target/$TARGET/$PROFILE_DIR/reth"
BOLT_INSTRUMENTED_BIN="$BUILT_BIN-bolt-instrumented"
echo "Instrumenting binary with BOLT..."
# --skip-funcs: skip compiler-generated drop_in_place functions that BOLT can't handle
# as split functions in relocation mode (triggered by --emit-relocs)
llvm-bolt "$BUILT_BIN" \
-instrument \
--instrumentation-file-append-pid \
--instrumentation-file="$BOLT_DIR/prof" \
--skip-funcs='.*drop_in_place.*' \
-o "$BOLT_INSTRUMENTED_BIN"
echo "BOLT-instrumented binary: $BOLT_INSTRUMENTED_BIN ($(ls -lh "$BOLT_INSTRUMENTED_BIN" | awk '{print $5}'))"
run_bench_workload "$BOLT_INSTRUMENTED_BIN" "$BOLT_BLOCKS" "bolt"
# Fix ownership for BOLT profiles
sudo chown -R "$(id -un):$(id -gn)" "$BOLT_DIR" 2>/dev/null || true
# Merge BOLT profiles
echo "Merging BOLT profiles..."
FDATA_COUNT=$(find "$BOLT_DIR" -name '*.fdata' | wc -l)
echo "Found $FDATA_COUNT .fdata files"
if [ "$FDATA_COUNT" -eq 0 ]; then
echo "error: no .fdata files — BOLT-instrumented binary did not produce profiles"
exit 1
fi
merge-fdata "$BOLT_DIR"/*.fdata > "$BOLT_DIR/merged.fdata"
echo "BOLT profile: $BOLT_DIR/merged.fdata ($(ls -lh "$BOLT_DIR/merged.fdata" | awk '{print $5}'))"
gha_section_end
# ── Phase 3: Final optimized build ───────────────────────────────────────
gha_section_start "Phase 3: Final PGO+BOLT Optimized Build"
echo "Building PGO-optimized binary..."
# --emit-relocs preserves relocation entries in the binary, required by llvm-bolt for code reordering
RUSTFLAGS="-Cprofile-use=$PGO_DIR/merged.profdata -Clink-arg=-Wl,--emit-relocs $COMBINED_RUSTFLAGS" \
cargo build "${CARGO_ARGS[@]}" --target "$TARGET"
BUILT_BIN="$PWD/target/$TARGET/$PROFILE_DIR/reth"
OPTIMIZED_BIN="$BUILT_BIN-bolt-optimized"
echo "Optimizing with BOLT..."
llvm-bolt "$BUILT_BIN" \
-o "$OPTIMIZED_BIN" \
--data "$BOLT_DIR/merged.fdata" \
-reorder-blocks=ext-tsp \
-reorder-functions=cdsort \
-split-functions \
-split-all-cold \
-dyno-stats \
-icf=1 \
-use-gnu-stack \
--skip-funcs='.*drop_in_place.*'
if [ "$STRIP_SYMBOLS_BOOL" = true ]; then
echo "Stripping debug symbols..."
strip "$OPTIMIZED_BIN"
else
echo "Skipping strip (STRIP_SYMBOLS=$STRIP_SYMBOLS)"
fi
publish_binary "$OPTIMIZED_BIN"
gha_section_end
fi
gha_section_start "Build Complete"
ls -lh "target/$PROFILE_DIR/reth"
echo "Output: target/$PROFILE_DIR/reth"
gha_section_end

View File

@@ -21,7 +21,6 @@ engine-cancun:
# Affects all clients, not just reth. Tracked: https://github.com/ethereum/hive/issues/1382
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=False, Invalid P8 (Cancun) (reth)
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=True, Invalid P8 (Cancun) (reth)
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Cancun) (reth)
engine-api:
- Transaction Re-Org, Re-Org Out (Paris) (reth)
- Transaction Re-Org, Re-Org to Different Block (Paris) (reth)
@@ -31,5 +30,3 @@ engine-api:
- Invalid Missing Ancestor Syncing ReOrg, Transaction Signature, EmptyTxs=False, CanonicalReOrg=True, Invalid P9 (Paris) (reth)
- Invalid Missing Ancestor Syncing ReOrg, Transaction Signature, EmptyTxs=False, CanonicalReOrg=False, Invalid P9 (Paris) (reth)
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=True, Invalid P10 (Paris) (reth)
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Paris) (reth)
- Multiple New Payloads Extending Canonical Chain, Set Head to First Payload Received (Paris) (reth)

View File

@@ -1,11 +1,13 @@
# Scheduled regression benchmarks (nightly + hourly).
# Scheduled regression benchmarks (nightly + hourly + release).
#
# Two modes:
# Three 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.
# release — Compares the latest GitHub release tag against the current nightly
# Docker build. Runs daily to track nightly vs release performance.
#
# State is persisted between runs via the decofe/reth-bench-charts repo: each
# successful run saves the feature commit SHA so the next run knows what to
@@ -17,6 +19,8 @@ on:
- cron: "30 5 * * *"
# Hourly: compares main HEAD vs last benchmarked commit, skips if no new commits
- cron: "0 * * * *"
# Release: compares latest GitHub release tag vs current nightly Docker build
- cron: "0 9 * * *"
workflow_dispatch:
inputs:
force:
@@ -24,11 +28,16 @@ on:
required: false
default: false
type: boolean
no_slack:
description: "Suppress Slack notifications"
slack:
description: "Slack notification policy"
required: false
default: true
type: boolean
default: "never"
type: choice
options:
- always
- on-win
- on-error
- never
mode:
description: "Benchmark mode"
required: false
@@ -37,6 +46,7 @@ on:
options:
- nightly
- hourly
- release
env:
CARGO_TERM_COLOR: always
@@ -64,6 +74,7 @@ jobs:
stale-age-hours: ${{ steps.refs.outputs.stale-age-hours }}
nightly-created: ${{ steps.refs.outputs.nightly-created }}
long-running: ${{ steps.refs.outputs.long-running }}
release-tag: ${{ steps.refs.outputs.release-tag }}
steps:
- uses: actions/checkout@v6
with:
@@ -79,6 +90,8 @@ jobs:
MODE="${{ inputs.mode || 'nightly' }}"
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
MODE="nightly"
elif [ "${{ github.event.schedule }}" = "0 9 * * *" ]; then
MODE="release"
else
MODE="hourly"
fi
@@ -98,7 +111,7 @@ jobs:
.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'
if: steps.mode.outputs.mode == 'hourly' && steps.refs.outputs.long-running == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -140,7 +153,7 @@ jobs:
});
- name: Alert on stale nightly
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -248,7 +261,7 @@ jobs:
BENCH_FEATURE_ARGS: ""
BENCH_ABBA: "true"
BENCH_COMMENT_ID: ""
BENCH_NO_SLACK: ${{ github.event_name == 'workflow_dispatch' && inputs.no_slack == true && 'true' || 'false' }}
BENCH_SLACK: ${{ github.event_name == 'workflow_dispatch' && inputs.slack || 'always' }}
BENCH_METRICS_ADDR: "127.0.0.1:9100"
BENCH_OTLP_DISABLED: "true"
BASELINE_REF: ${{ needs.resolve-refs.outputs.baseline-ref }}
@@ -338,12 +351,19 @@ jobs:
- name: Resolve display names
id: refs
env:
RELEASE_TAG: ${{ needs.resolve-refs.outputs.release-tag }}
run: |
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
FEATURE_SHORT=$(echo "$FEATURE_REF" | cut -c1-8)
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
if [ "$BENCH_MODE" = "release" ] && [ -n "$RELEASE_TAG" ]; then
echo "baseline-name=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
echo "baseline-ref=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
else
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
fi
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
- name: Check if snapshot needs update
@@ -559,11 +579,12 @@ jobs:
env:
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
BASELINE_REF_DISPLAY: ${{ steps.refs.outputs.baseline-ref }}
run: |
SUMMARY_ARGS="--output-summary $BENCH_WORK_DIR/summary.json"
SUMMARY_ARGS="$SUMMARY_ARGS --output-markdown $BENCH_WORK_DIR/comment.md"
SUMMARY_ARGS="$SUMMARY_ARGS --repo ${{ github.repository }}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-ref ${BASELINE_REF}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-ref ${BASELINE_REF_DISPLAY}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-name ${BASELINE_NAME}"
SUMMARY_ARGS="$SUMMARY_ARGS --feature-name ${FEATURE_NAME}"
SUMMARY_ARGS="$SUMMARY_ARGS --feature-ref ${FEATURE_REF}"
@@ -590,7 +611,11 @@ jobs:
CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_USER }}
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }}
run: |
WORKFLOW_NAME="workflows-nightly-regression-${{ github.run_id }}"
if [ "$BENCH_MODE" = "release" ]; then
WORKFLOW_NAME="workflows-release-regression-${{ github.run_id }}"
else
WORKFLOW_NAME="workflows-nightly-regression-${{ github.run_id }}"
fi
DIFF_URL="https://github.com/${{ github.repository }}/compare/${BASELINE_REF}...${FEATURE_REF}"
GRAFANA_URL='${{ steps.metrics.outputs.grafana-url }}'
JOB_URL="${BENCH_JOB_URL:-${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}}"
@@ -633,7 +658,7 @@ jobs:
if: success() && env.BENCH_MODE != 'hourly'
run: |
RUN_ID=${{ github.run_id }}
CHART_DIR="nightly/${RUN_ID}"
CHART_DIR="${BENCH_MODE}/${RUN_ID}"
CHARTS_REPO="https://x-access-token:${{ secrets.DEREK_TOKEN }}@github.com/decofe/reth-bench-charts.git"
TMP_DIR=$(mktemp -d)
@@ -733,7 +758,7 @@ jobs:
await core.summary.addRaw(md).write();
- name: Send Slack notification (success)
if: success() && env.BENCH_NO_SLACK != 'true'
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -761,7 +786,15 @@ jobs:
// Filter notifications based on mode
const changes = summary.changes || {};
const mode = process.env.BENCH_MODE || 'nightly';
const slackMode = process.env.BENCH_SLACK || 'always';
const hasRegression = Object.values(changes).some(c => c.sig === 'bad');
const hasImprovement = Object.values(changes).some(c => c.sig === 'good');
// on-win mode: only notify on improvements
if (slackMode === 'on-win' && !hasImprovement) {
core.info('on-win mode: no improvement detected, skipping Slack notification');
return;
}
// Hourly mode: only notify on regressions
if (mode === 'hourly' && !hasRegression) {
@@ -789,7 +822,7 @@ jobs:
function cell(text) { return { type: 'raw_text', text: String(text) || ' ' }; }
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
const modeLabel = mode === 'release' ? 'Release Regression' : mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
const sectionText = [
`*${modeLabel}*`,
'',
@@ -880,7 +913,7 @@ jobs:
}
- name: Send Slack notification (failure)
if: failure()
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -906,7 +939,7 @@ jobs:
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 modeLabel = mode === 'release' ? 'Release' : mode === 'hourly' ? 'Hourly' : 'Nightly';
const blocks = [
{
@@ -915,7 +948,7 @@ jobs:
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>` },
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>\n@ai investigate this` },
},
{
type: 'actions',

View File

@@ -14,13 +14,23 @@ on:
blocks:
description: "Number of blocks to benchmark"
required: false
default: "500"
default: "200"
type: string
big_blocks:
description: "Use big blocks mode (pre-generated merged payloads with reth-bb)"
required: false
default: "false"
type: boolean
bal:
description: "Replay block access lists during big-block benchmarks"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
- "feature"
- "baseline"
warmup:
description: "Number of warmup blocks"
required: false
@@ -61,11 +71,16 @@ on:
required: false
default: "0"
type: string
no_slack:
description: "Suppress Slack notifications for benchmark results"
slack:
description: "Slack notification policy"
required: false
default: "true"
type: boolean
default: "never"
type: choice
options:
- always
- on-win
- on-error
- never
abba:
description: "Run ABBA (BFFB) interleaved order; false = single AB pass"
required: false
@@ -105,9 +120,10 @@ jobs:
baseline-name: ${{ steps.args.outputs.baseline-name }}
feature-name: ${{ steps.args.outputs.feature-name }}
samply: ${{ steps.args.outputs.samply }}
no-slack: ${{ steps.args.outputs.no-slack }}
slack: ${{ steps.args.outputs.slack }}
cores: ${{ steps.args.outputs.cores }}
big-blocks: ${{ steps.args.outputs.big-blocks }}
bal: ${{ steps.args.outputs.bal }}
wait-time: ${{ steps.args.outputs.wait-time }}
baseline-args: ${{ steps.args.outputs.baseline-args }}
feature-args: ${{ steps.args.outputs.feature-args }}
@@ -140,18 +156,24 @@ jobs:
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
let pr, actor, blocks, warmup, baseline, feature, samply, cores, bigBlocks;
const validBalModes = new Set(['false', 'true', 'feature', 'baseline']);
const validSlackModes = new Set(['always', 'on-win', 'on-error', 'never']);
const usage = '`@decofe bench [blocks=N] [big-blocks[=true|false]] [bal=true|false|feature|baseline] [warmup=N] [baseline=REF] [feature=REF] [samply] [slack=always|on-win|on-error|never] [cores=N] [abba=true|false] [otlp=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]`';
let pr, actor, blocks, warmup, baseline, feature, samply, cores, bigBlocks, bal;
let explicitWarmup = false;
if (context.eventName === 'workflow_dispatch') {
actor = '${{ github.actor }}';
blocks = '${{ github.event.inputs.blocks }}' || '500';
blocks = '${{ github.event.inputs.blocks }}' || '200';
warmup = '${{ github.event.inputs.warmup }}' || '100';
if (warmup !== '100') explicitWarmup = true;
baseline = '${{ github.event.inputs.baseline }}';
feature = '${{ github.event.inputs.feature }}';
samply = '${{ github.event.inputs.samply }}' === 'true' ? 'true' : 'false';
var noSlack = '${{ github.event.inputs.no_slack }}' !== 'false' ? 'true' : 'false';
var slack = '${{ github.event.inputs.slack }}' || 'never';
cores = '${{ github.event.inputs.cores }}' || '0';
bigBlocks = '${{ github.event.inputs.big_blocks }}' === 'true' ? 'true' : 'false';
bal = '${{ github.event.inputs.bal }}' || 'false';
var abba = '${{ github.event.inputs.abba }}' !== 'false' ? 'true' : 'false';
var otlp = '${{ github.event.inputs.otlp }}' !== 'false' ? 'true' : 'false';
var waitTime = '${{ github.event.inputs.wait_time }}' || '';
@@ -178,11 +200,12 @@ jobs:
const body = context.payload.comment.body.trim();
const intArgs = new Set(['warmup', 'cores', 'blocks']);
const refArgs = new Set(['baseline', 'feature']);
const boolArgs = new Set(['samply', 'no-slack', 'big-blocks']);
const boolArgs = new Set(['samply', 'big-blocks']);
const boolDefaultTrue = new Set(['abba', 'otlp']);
const enumArgs = new Map([['bal', validBalModes], ['slack', validSlackModes]]);
const durationArgs = new Set(['wait-time']);
const stringArgs = new Set(['baseline-args', 'feature-args']);
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', 'big-blocks': 'false', cores: '0', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
const defaults = { blocks: '200', warmup: '100', baseline: '', feature: '', samply: 'false', slack: 'always', 'big-blocks': 'false', bal: 'false', cores: '0', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
const unknown = [];
const invalid = [];
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
@@ -209,7 +232,7 @@ jobs:
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (boolDefaultTrue.has(key)) {
if (boolArgs.has(key) || boolDefaultTrue.has(key)) {
if (value === 'true' || value === 'false') {
defaults[key] = value;
} else {
@@ -221,11 +244,18 @@ jobs:
} else {
invalid.push(`\`${key}=${value}\` (must be a duration like 500ms, 1s, 2m)`);
}
} else if (enumArgs.has(key)) {
if (enumArgs.get(key).has(value)) {
defaults[key] = value;
} else {
invalid.push(`\`${key}=${value}\` (must be true, false, feature, or baseline)`);
}
} else if (intArgs.has(key)) {
if (!/^\d+$/.test(value)) {
invalid.push(`\`${key}=${value}\` (must be a positive integer)`);
} else {
defaults[key] = value;
if (key === 'warmup') explicitWarmup = true;
}
} else if (refArgs.has(key)) {
if (!value) {
@@ -243,7 +273,7 @@ jobs:
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
if (errors.length) {
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N] [big-blocks] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [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:** ${usage}`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -258,9 +288,10 @@ jobs:
baseline = defaults.baseline;
feature = defaults.feature;
samply = defaults.samply;
var noSlack = defaults['no-slack'];
var slack = defaults.slack;
cores = defaults.cores;
bigBlocks = defaults['big-blocks'];
bal = defaults.bal;
var abba = defaults.abba;
var otlp = defaults.otlp;
var waitTime = defaults['wait-time'];
@@ -268,6 +299,29 @@ jobs:
var featureNodeArgs = defaults['feature-args'];
}
// Default warmup to 20 for big-blocks mode unless explicitly set
if (bigBlocks === 'true' && !explicitWarmup) {
warmup = '20';
}
if (!validBalModes.has(bal)) {
core.setFailed(`Invalid bal mode: ${bal}`);
return;
}
if (bal !== 'false' && bigBlocks !== 'true') {
const msg = `❌ **Invalid bench command**\n\n\`bal\` requires \`big-blocks=true\`.\n\n**Usage:** ${usage}`;
if (context.eventName === 'issue_comment') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg,
});
}
core.setFailed(msg);
return;
}
// Resolve display names for baseline/feature
let baselineName = baseline || 'main';
let featureName = feature;
@@ -293,9 +347,10 @@ jobs:
core.setOutput('baseline-name', baselineName);
core.setOutput('feature-name', featureName);
core.setOutput('samply', samply);
core.setOutput('no-slack', noSlack);
core.setOutput('slack', slack);
core.setOutput('cores', cores);
core.setOutput('big-blocks', bigBlocks);
core.setOutput('bal', bal);
core.setOutput('wait-time', waitTime);
core.setOutput('baseline-args', baselineNodeArgs);
core.setOutput('feature-args', featureNodeArgs);
@@ -358,10 +413,12 @@ jobs:
const baseline = '${{ steps.args.outputs.baseline-name }}';
const feature = '${{ steps.args.outputs.feature-name }}';
const samply = '${{ steps.args.outputs.samply }}' === 'true';
const noSlack = '${{ steps.args.outputs.no-slack }}' === 'true';
const slack = '${{ steps.args.outputs.slack }}' || 'always';
const bigBlocks = '${{ steps.args.outputs.big-blocks }}' === 'true';
const bal = '${{ steps.args.outputs.bal }}' || 'false';
const samplyNote = samply ? ', samply: `enabled`' : '';
const noSlackNote = noSlack ? ', no-slack' : '';
const slackNote = slack !== 'always' ? `, slack: \`${slack}\`` : '';
const balNote = bigBlocks && bal !== 'false' ? `, BAL: \`${bal}\`` : '';
const cores = '${{ steps.args.outputs.cores }}';
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
const abbaEnabled = '${{ steps.args.outputs.abba }}' !== 'false';
@@ -375,7 +432,7 @@ jobs:
const featureArgsVal = '${{ steps.args.outputs.feature-args }}';
const featureArgsNote = featureArgsVal ? `, feature-args: \`${featureArgsVal}\`` : '';
const blocksDesc = bigBlocks ? 'blocks: `big`' : `${blocks} blocks, ${warmup} warmup blocks`;
const config = `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${noSlackNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`;
const config = `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${slackNote}${balNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`;
const { data: comment } = await github.rest.issues.createComment({
owner: context.repo.owner,
@@ -400,10 +457,12 @@ jobs:
const baseline = '${{ steps.args.outputs.baseline-name }}';
const feature = '${{ steps.args.outputs.feature-name }}';
const samply = '${{ steps.args.outputs.samply }}' === 'true';
const noSlack = '${{ steps.args.outputs.no-slack }}' === 'true';
const slack = '${{ steps.args.outputs.slack }}' || 'always';
const bigBlocks = '${{ steps.args.outputs.big-blocks }}' === 'true';
const bal = '${{ steps.args.outputs.bal }}' || 'false';
const samplyNote = samply ? ', samply: `enabled`' : '';
const noSlackNote = noSlack ? ', no-slack' : '';
const slackNote = slack !== 'always' ? `, slack: \`${slack}\`` : '';
const balNote = bigBlocks && bal !== 'false' ? `, BAL: \`${bal}\`` : '';
const cores = '${{ steps.args.outputs.cores }}';
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
const abbaEnabled = '${{ steps.args.outputs.abba }}' !== 'false';
@@ -417,7 +476,7 @@ jobs:
const featureArgsVal = '${{ steps.args.outputs.feature-args }}';
const featureArgsNote = featureArgsVal ? `, feature-args: \`${featureArgsVal}\`` : '';
const blocksDesc = bigBlocks ? 'blocks: `big`' : `${blocks} blocks, ${warmup} warmup blocks`;
const config = `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${noSlackNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`;
const config = `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${slackNote}${balNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const numRunners = parseInt(process.env.BENCH_RUNNERS) || 1;
@@ -483,13 +542,14 @@ jobs:
BENCH_SAMPLY: ${{ needs.reth-bench-ack.outputs.samply }}
BENCH_CORES: ${{ needs.reth-bench-ack.outputs.cores }}
BENCH_BIG_BLOCKS: ${{ needs.reth-bench-ack.outputs.big-blocks }}
BENCH_BAL: ${{ needs.reth-bench-ack.outputs.bal }}
BENCH_WAIT_TIME: ${{ needs.reth-bench-ack.outputs.wait-time }}
BENCH_BASELINE_ARGS: ${{ needs.reth-bench-ack.outputs.baseline-args }}
BENCH_FEATURE_ARGS: ${{ needs.reth-bench-ack.outputs.feature-args }}
BENCH_ABBA: ${{ needs.reth-bench-ack.outputs.abba }}
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_SLACK: ${{ needs.reth-bench-ack.outputs.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 || '' }}
@@ -544,10 +604,12 @@ jobs:
const baseline = '${{ needs.reth-bench-ack.outputs.baseline-name }}';
const feature = '${{ needs.reth-bench-ack.outputs.feature-name }}';
const samply = process.env.BENCH_SAMPLY === 'true';
const noSlack = process.env.BENCH_NO_SLACK === 'true';
const slack = process.env.BENCH_SLACK || 'always';
const bigBlocks = process.env.BENCH_BIG_BLOCKS === 'true';
const bal = process.env.BENCH_BAL || 'false';
const samplyNote = samply ? ', samply: `enabled`' : '';
const noSlackNote = noSlack ? ', no-slack' : '';
const slackNote = slack !== 'always' ? `, slack: \`${slack}\`` : '';
const balNote = bigBlocks && bal !== 'false' ? `, BAL: \`${bal}\`` : '';
const cores = process.env.BENCH_CORES || '0';
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
const abbaEnabled = (process.env.BENCH_ABBA || 'true') !== 'false';
@@ -561,7 +623,7 @@ jobs:
const featureArgsVal = process.env.BENCH_FEATURE_ARGS || '';
const featureArgsNote = featureArgsVal ? `, feature-args: \`${featureArgsVal}\`` : '';
const blocksDesc = bigBlocks ? 'blocks: `big`' : `${blocks} blocks, ${warmup} warmup blocks`;
core.exportVariable('BENCH_CONFIG', `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${noSlackNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`);
core.exportVariable('BENCH_CONFIG', `**Config:** ${blocksDesc}, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${slackNote}${balNote}${coresNote}${abbaNote}${otlpNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`);
const { buildBody } = require('./.github/scripts/bench-update-status.js');
await github.rest.issues.updateComment({
@@ -1134,6 +1196,9 @@ jobs:
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
SUMMARY_ARGS="$SUMMARY_ARGS --wait-time $BENCH_WAIT_TIME"
fi
if [ -n "${BENCH_BAL:-}" ] && [ "${BENCH_BAL}" != "false" ]; then
SUMMARY_ARGS="$SUMMARY_ARGS --bal-mode $BENCH_BAL"
fi
GRAFANA_URL='${{ steps.metrics.outputs.grafana-url }}'
if [ -n "$GRAFANA_URL" ]; then
SUMMARY_ARGS="$SUMMARY_ARGS --grafana-url $GRAFANA_URL"
@@ -1289,7 +1354,7 @@ jobs:
});
- name: Send Slack notification (success)
if: success() && env.BENCH_NO_SLACK != 'true'
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -1334,7 +1399,7 @@ jobs:
});
- name: Send Slack notification (failure)
if: failure()
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}

View File

@@ -28,30 +28,12 @@ on:
required: false
type: boolean
default: false
pgo:
description: "Enable PGO profiling"
required: false
type: boolean
default: false
pgo_blocks:
description: "Number of blocks to execute for PGO profiling"
required: false
type: string
default: "20"
jobs:
collect-pgo-profile:
if: github.repository == 'paradigmxyz/reth' && github.event_name == 'workflow_dispatch' && inputs.pgo
uses: ./.github/workflows/pgo-profile.yml
with:
pgo_blocks: ${{ inputs.pgo_blocks || '20' }}
secrets: inherit
build:
if: github.repository == 'paradigmxyz/reth' && !failure() && !cancelled()
if: github.repository == 'paradigmxyz/reth'
name: Build Docker images
runs-on: ubuntu-24.04
needs: collect-pgo-profile
permissions:
packages: write
contents: read
@@ -76,30 +58,6 @@ jobs:
echo "describe=$(git describe --always --tags)" >> "$GITHUB_OUTPUT"
echo "dirty=false" >> "$GITHUB_OUTPUT"
- name: Download pre-collected PGO profile
if: ${{ github.event_name == 'workflow_dispatch' && inputs.pgo }}
uses: actions/download-artifact@v7
with:
name: pgo-profdata
path: dist
- name: Configure PGO build args
id: pgo
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ "${{ inputs.pgo }}" == "true" ]]; then
if [ ! -f dist/merged.profdata ]; then
echo "::error::Expected dist/merged.profdata from collect-pgo-profile job"
exit 1
fi
echo "use_pgo_bolt=true" >> "$GITHUB_OUTPUT"
echo "pgo_profdata=dist/merged.profdata" >> "$GITHUB_OUTPUT"
echo "Using pre-collected PGO profile from collect-pgo-profile job"
else
echo "use_pgo_bolt=false" >> "$GITHUB_OUTPUT"
echo "pgo_profdata=" >> "$GITHUB_OUTPUT"
echo "PGO disabled"
fi
- name: Determine build parameters
id: params
run: |
@@ -149,9 +107,6 @@ jobs:
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
set: |
${{ steps.params.outputs.ethereum_set }}
*.args.USE_PGO_BOLT=${{ steps.pgo.outputs.use_pgo_bolt }}
*.args.PGO_PROFDATA=${{ steps.pgo.outputs.pgo_profdata }}
*.args.STRIP_SYMBOLS=false
- name: Verify image architectures
env:

View File

@@ -117,7 +117,7 @@ jobs:
msrv:
name: MSRV
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v6

View File

@@ -1,99 +0,0 @@
name: pgo-profile
on:
workflow_call:
inputs:
pgo_blocks:
description: "Number of blocks to execute for PGO profiling"
required: false
type: string
default: "20"
workflow_dispatch:
inputs:
pgo_blocks:
description: "Number of blocks to execute for PGO profiling"
required: false
type: string
default: "20"
jobs:
collect:
name: collect PGO profiles
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 180
env:
SCHELK_MOUNT: /reth-bench
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
with:
submodules: true
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-linux-gnu
- uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
dmsetup lsb-release wget linux-tools-"$(uname -r)" || \
sudo apt-get install -y --no-install-recommends linux-tools-generic
- name: Download snapshot if needed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BENCH_REPO: ${{ github.repository }}
run: |
if ! .github/scripts/bench-reth-snapshot.sh --check; then
echo "Snapshot outdated or missing, downloading..."
.github/scripts/bench-reth-snapshot.sh
fi
- name: Mount snapshot
run: |
sudo schelk recover -y --kill || true
sudo schelk mount -y
sync
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
- name: Collect PGO profile
run: |
DATADIR="$SCHELK_MOUNT/datadir" \
RPC_URL="$BENCH_RPC_URL" \
PGO_BLOCKS="${{ inputs.pgo_blocks || '20' }}" \
BOLT_BLOCKS="${{ inputs.pgo_blocks || '20' }}" \
COLLECT_PGO_ONLY=true \
SKIP_BOLT=true \
PROFILE=maxperf-symbols \
FEATURES="jemalloc,asm-keccak,min-debug-logs" \
TARGET=x86_64-unknown-linux-gnu \
EXTRA_RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq" \
.github/scripts/build_pgo_bolt.sh
- name: Show PGO profile stats
run: |
LLVM_PROFDATA=$(find "$(rustc --print sysroot)" -name llvm-profdata -type f | head -1)
if [ -z "$LLVM_PROFDATA" ]; then
echo "::error::llvm-profdata not found in rust toolchain"
exit 1
fi
"$LLVM_PROFDATA" show --detailed-summary --topn=20 target/pgo-profiles/merged.profdata
- name: Upload PGO profile
uses: actions/upload-artifact@v7
with:
name: pgo-profdata
path: target/pgo-profiles/merged.profdata
retention-days: 1
- name: Recover snapshot
if: always()
run: |
sudo schelk recover -y --kill || true

View File

@@ -13,14 +13,6 @@ on:
description: "Enable dry run mode (builds artifacts but skips uploads and release creation)"
type: boolean
default: false
pgo:
description: "Enable PGO profiling"
type: boolean
default: false
pgo_blocks:
description: "Number of blocks to execute for PGO profiling on self-hosted runner"
type: string
default: "20"
env:
REPO_NAME: ${{ github.repository_owner }}/reth
@@ -77,6 +69,12 @@ jobs:
fail-fast: true
matrix:
configs:
- target: x86_64-unknown-linux-gnu
os: ubuntu-24.04
profile: maxperf
allow_fail: false
rustflags: "-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"
native: true
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
profile: maxperf
@@ -119,6 +117,8 @@ jobs:
echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV
- name: Build Reth
env:
CC: clang
run: |
if [ "${{ matrix.configs.native }}" = "true" ]; then
make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-native-${{ matrix.configs.target }}
@@ -157,93 +157,12 @@ jobs:
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
collect-pgo-profile:
if: github.event_name == 'workflow_dispatch' && inputs.pgo
uses: ./.github/workflows/pgo-profile.yml
with:
pgo_blocks: ${{ inputs.pgo_blocks || '20' }}
secrets: inherit
build-pgo:
if: github.event_name == 'workflow_dispatch' && inputs.pgo
name: build release (x86_64-linux PGO+BOLT)
runs-on: [self-hosted, Linux, X64]
needs: [extract-version, collect-pgo-profile]
timeout-minutes: 120
env:
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
with:
submodules: true
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-linux-gnu
- uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Download pre-collected PGO profile
uses: actions/download-artifact@v7
with:
name: pgo-profdata
path: dist
- name: Verify PGO profile artifact
run: |
test -f dist/merged.profdata
ls -lh dist/merged.profdata
- name: Build Reth with PGO+BOLT
run: |
SKIP_BOLT=true \
PGO_PROFDATA="$PWD/dist/merged.profdata" \
PROFILE=maxperf-symbols \
FEATURES="jemalloc,asm-keccak,min-debug-logs" \
TARGET=x86_64-unknown-linux-gnu \
EXTRA_RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq" \
.github/scripts/build_pgo_bolt.sh
- name: Move binary
run: |
mkdir artifacts
mv target/maxperf-symbols/reth ./artifacts
- name: Configure GPG and create artifacts
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
export GPG_TTY=$(tty)
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
cd artifacts
tar -czf reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz reth*
echo "$GPG_PASSPHRASE" | gpg --passphrase-fd 0 --pinentry-mode loopback --batch -ab reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
mv *tar.gz* ..
shell: bash
- name: Upload artifact
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@v7
with:
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
- name: Upload signature
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@v7
with:
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
draft-release:
name: draft release
runs-on: ubuntu-latest
needs: [build, build-pgo, extract-version]
if: ${{ !failure() && !cancelled() && github.event.inputs.dry_run != 'true' }}
needs: [build, extract-version]
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
VERSION: ${{ needs.extract-version.outputs.VERSION }}
permissions:

849
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -325,8 +325,8 @@ reth-cli = { path = "crates/cli/cli" }
reth-cli-commands = { path = "crates/cli/commands" }
reth-cli-runner = { path = "crates/cli/runner" }
reth-cli-util = { path = "crates/cli/util" }
reth-codecs = { version = "0.1.0", default-features = false }
reth-codecs-derive = "0.1.0"
reth-codecs = { version = "0.3.0", default-features = false }
reth-codecs-derive = "0.3.0"
reth-config = { path = "crates/config", default-features = false }
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
@@ -394,7 +394,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
reth-payload-primitives = { path = "crates/payload/primitives" }
reth-payload-validator = { path = "crates/payload/validator" }
reth-payload-util = { path = "crates/payload/util" }
reth-primitives-traits = { version = "0.1.0", default-features = false }
reth-primitives-traits = { version = "0.3.0", default-features = false }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune/prune" }
reth-prune-types = { path = "crates/prune/types", default-features = false }
@@ -410,7 +410,7 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
reth-rpc-traits = { version = "0.1.0", default-features = false }
reth-rpc-traits = { version = "0.3.0", default-features = false }
reth-stages = { path = "crates/stages/stages" }
reth-stages-api = { path = "crates/stages/api" }
reth-stages-types = { path = "crates/stages/types", default-features = false }
@@ -429,17 +429,17 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
reth-trie-db = { path = "crates/trie/db" }
reth-trie-parallel = { path = "crates/trie/parallel" }
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-zstd-compressors = { version = "0.1.0", default-features = false }
reth-zstd-compressors = { version = "0.3.0", default-features = false }
# revm
revm = { version = "36.0.0", default-features = false }
revm-bytecode = { version = "9.0.0", default-features = false }
revm-database = { version = "12.0.0", default-features = false }
revm-state = { version = "10.0.0", default-features = false }
revm-primitives = { version = "22.1.0", default-features = false }
revm-interpreter = { version = "34.0.0", default-features = false }
revm-database-interface = { version = "10.0.0", default-features = false }
revm-inspectors = "0.36.0"
revm = { version = "37.0.0", default-features = false }
revm-bytecode = { version = "10.0.0", default-features = false }
revm-database = { version = "13.0.0", default-features = false }
revm-state = { version = "11.0.0", default-features = false }
revm-primitives = { version = "23.0.0", default-features = false }
revm-interpreter = { version = "35.0.0", default-features = false }
revm-database-interface = { version = "11.0.0", default-features = false }
revm-inspectors = "0.38.0"
# eth
alloy-dyn-abi = "1.5.6"
@@ -449,39 +449,39 @@ alloy-sol-types = { version = "1.5.6", default-features = false }
alloy-chains = { version = "0.2.33", default-features = false }
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.3.0", default-features = false }
alloy-evm = { version = "0.30.0", default-features = false }
alloy-evm = { version = "0.32.0", default-features = false }
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
alloy-trie = { version = "0.9.4", default-features = false }
alloy-hardforks = "0.4.5"
alloy-hardforks = "0.4.7"
alloy-consensus = { version = "1.8.2", default-features = false }
alloy-contract = { version = "1.8.2", default-features = false }
alloy-eips = { version = "1.8.2", default-features = false }
alloy-genesis = { version = "1.8.2", default-features = false }
alloy-json-rpc = { version = "1.8.2", default-features = false }
alloy-network = { version = "1.8.2", default-features = false }
alloy-network-primitives = { version = "1.8.2", default-features = false }
alloy-provider = { version = "1.8.2", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.8.2", default-features = false }
alloy-rpc-client = { version = "1.8.2", default-features = false }
alloy-rpc-types = { version = "1.8.2", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.8.2", default-features = false }
alloy-rpc-types-anvil = { version = "1.8.2", default-features = false }
alloy-rpc-types-beacon = { version = "1.8.2", default-features = false }
alloy-rpc-types-debug = { version = "1.8.2", default-features = false }
alloy-rpc-types-engine = { version = "1.8.2", default-features = false }
alloy-rpc-types-eth = { version = "1.8.2", default-features = false }
alloy-rpc-types-mev = { version = "1.8.2", default-features = false }
alloy-rpc-types-trace = { version = "1.8.2", default-features = false }
alloy-rpc-types-txpool = { version = "1.8.2", default-features = false }
alloy-serde = { version = "1.8.2", default-features = false }
alloy-signer = { version = "1.8.2", default-features = false }
alloy-signer-local = { version = "1.8.2", default-features = false }
alloy-transport = { version = "1.8.2" }
alloy-transport-http = { version = "1.8.2", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.8.2", default-features = false }
alloy-transport-ws = { version = "1.8.2", default-features = false }
alloy-consensus = { version = "2.0.0", default-features = false }
alloy-contract = { version = "2.0.0", default-features = false }
alloy-eips = { version = "2.0.0", default-features = false }
alloy-genesis = { version = "2.0.0", default-features = false }
alloy-json-rpc = { version = "2.0.0", default-features = false }
alloy-network = { version = "2.0.0", default-features = false }
alloy-network-primitives = { version = "2.0.0", default-features = false }
alloy-provider = { version = "2.0.0", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "2.0.0", default-features = false }
alloy-rpc-client = { version = "2.0.0", default-features = false }
alloy-rpc-types = { version = "2.0.0", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "2.0.0", default-features = false }
alloy-rpc-types-anvil = { version = "2.0.0", default-features = false }
alloy-rpc-types-beacon = { version = "2.0.0", default-features = false }
alloy-rpc-types-debug = { version = "2.0.0", default-features = false }
alloy-rpc-types-engine = { version = "2.0.0", default-features = false }
alloy-rpc-types-eth = { version = "2.0.0", default-features = false }
alloy-rpc-types-mev = { version = "2.0.0", default-features = false }
alloy-rpc-types-trace = { version = "2.0.0", default-features = false }
alloy-rpc-types-txpool = { version = "2.0.0", default-features = false }
alloy-serde = { version = "2.0.0", default-features = false }
alloy-signer = { version = "2.0.0", default-features = false }
alloy-signer-local = { version = "2.0.0", default-features = false }
alloy-transport = { version = "2.0.0" }
alloy-transport-http = { version = "2.0.0", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "2.0.0", default-features = false }
alloy-transport-ws = { version = "2.0.0", default-features = false }
# misc
either = { version = "1.15.0", default-features = false }
@@ -558,7 +558,7 @@ proc-macro2 = "1.0"
quote = "1.0"
# tokio
tokio = { version = "1.44.2", default-features = false }
tokio = { version = "1.51.1", default-features = false }
tokio-stream = "0.1.11"
tokio-tungstenite = "0.28.0"
tokio-util = { version = "0.7.4", features = ["codec"] }
@@ -671,7 +671,6 @@ indexmap = "2"
interprocess = "2.2.0"
lz4_flex = { version = "0.12", default-features = false }
memmap2 = "0.9.4"
mev-share-sse = { version = "0.5.0", default-features = false }
num-traits = "0.2.15"
page_size = "0.6.0"
plain_hasher = "0.2"
@@ -701,42 +700,17 @@ vergen-git2 = "9.1.0"
ipnet = "2.11"
[patch.crates-io]
# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "1207e33" }
#
# jsonrpsee = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-core = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-server = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-http-client = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-types = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "9bc2dba" }
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "3020ea8" }
revm = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "89ecb25dbe49e1c3a10d99529e42f027d0bd2386" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "c6f88bbe7186d863f4667dd43c42608eb7a8ba5c" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "ff0bbec9ccaa818155e25003a77f4d73d350bbd7" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498871bc1b1d42c6dcf28968c271660de8c0" }

View File

@@ -1,10 +1,8 @@
# syntax=docker/dockerfile:1
# Dockerfile for reth, optimized for Depot builds
# Supports PGO+BOLT optimization for maximum performance
# Usage:
# reth: --build-arg BINARY=reth
# PGO+BOLT: --build-arg USE_PGO_BOLT=true (Linux x86_64/aarch64 only)
FROM rust:1.93 AS builder
WORKDIR /app
@@ -45,18 +43,6 @@ ENV VERGEN_GIT_SHA=$VERGEN_GIT_SHA
ENV VERGEN_GIT_DESCRIBE=$VERGEN_GIT_DESCRIBE
ENV VERGEN_GIT_DIRTY=$VERGEN_GIT_DIRTY
# Enable PGO+BOLT optimization (Linux only)
ARG USE_PGO_BOLT=false
ENV USE_PGO_BOLT=$USE_PGO_BOLT
# Optional path to a pre-collected merged.profdata file in build context.
ARG PGO_PROFDATA=""
ENV PGO_PROFDATA=$PGO_PROFDATA
# Whether to strip debug symbols from PGO-built binaries.
ARG STRIP_SYMBOLS=true
ENV STRIP_SYMBOLS=$STRIP_SYMBOLS
# Build application
# Platform-specific RUSTFLAGS: amd64 uses x86-64-v3 (Haswell+) with pclmulqdq for rocksdb
ARG TARGETPLATFORM
@@ -67,21 +53,12 @@ RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
--mount=type=cache,target=$SCCACHE_DIR,sharing=shared \
export RUSTC_WRAPPER=sccache SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev SCCACHE_DIR=/sccache && \
sccache --start-server && \
if [ "$USE_PGO_BOLT" = "true" ] && [ "$TARGETPLATFORM" = "linux/amd64" ] && [ -n "$PGO_PROFDATA" ] && [ -f "$PGO_PROFDATA" ]; then \
apt-get update && apt-get install -y -qq lsb-release wget sudo && \
BINARY="$BINARY" PROFILE="$BUILD_PROFILE" FEATURES="$FEATURES" SKIP_BOLT=true STRIP_SYMBOLS="$STRIP_SYMBOLS" PGO_PROFDATA="$PGO_PROFDATA" \
./.github/scripts/build_pgo_bolt.sh; \
else \
if [ "$USE_PGO_BOLT" = "true" ]; then \
echo "PGO requested but pre-collected profile missing at '${PGO_PROFDATA:-<unset>}' - falling back to non-PGO build"; \
fi; \
if [ -n "$RUSTFLAGS" ]; then \
export RUSTFLAGS="$RUSTFLAGS"; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml; \
if [ -n "$RUSTFLAGS" ]; then \
export RUSTFLAGS="$RUSTFLAGS"; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml && \
sccache --show-stats
# Copy binary to a known location (ARG not resolved in COPY)

View File

@@ -9,7 +9,7 @@
![](./assets/reth-2.png)
**[Install](https://paradigmxyz.github.io/reth/installation/installation.html)**
**[Install](https://reth.rs/installation/installation)**
| [User Docs](https://reth.rs)
| [Developer Docs](./docs)
| [Crate Docs](https://reth.rs/docs)
@@ -18,51 +18,43 @@
[gh-lint]: https://github.com/paradigmxyz/reth/actions/workflows/lint.yml
[tg-badge]: https://img.shields.io/endpoint?color=neon&logo=telegram&label=chat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fparadigm%5Freth
> **Note: OP-Reth has moved**
>
> The Optimism (op-reth) crates have been moved to [ethereum-optimism/optimism](https://github.com/ethereum-optimism/optimism).
> Git contribution history has been preserved. If you are looking for op-reth, please see the new repository.
## What is Reth?
Reth (short for Rust Ethereum, [pronunciation](https://x.com/kelvinfichter/status/1597653609411268608)) is a new Ethereum full node implementation that is focused on being user-friendly, highly modular, as well as being fast and efficient. Reth is an Execution Layer (EL) and is compatible with all Ethereum Consensus Layer (CL) implementations that support the [Engine API](https://github.com/ethereum/execution-apis/tree/a0d03086564ab1838b462befbc083f873dcf0c0f/src/engine). It is originally built and driven forward by [Paradigm](https://paradigm.xyz/), and is licensed under the Apache and MIT licenses.
Reth (short for Rust Ethereum, [pronunciation](https://x.com/kelvinfichter/status/1597653609411268608)) is a production-ready Ethereum execution layer client focused on modularity, performance, and user-friendliness. Reth is compatible with all Ethereum Consensus Layer (CL) implementations that support the [Engine API](https://github.com/ethereum/execution-apis/tree/a0d03086564ab1838b462befbc083f873dcf0c0f/src/engine). It is built and driven forward by [Paradigm](https://paradigm.xyz/), and is licensed under the Apache and MIT licenses.
> **Note:** OP-Reth has moved to [ethereum-optimism/optimism](https://github.com/ethereum-optimism/optimism). Git history has been preserved.
## Goals
As a full Ethereum node, Reth allows users to connect to the Ethereum network and interact with the Ethereum blockchain. This includes sending and receiving transactions/logs/traces, as well as accessing and interacting with smart contracts. Building a successful Ethereum node requires creating a high-quality implementation that is both secure and efficient, as well as being easy to use on consumer hardware. It also requires building a strong community of contributors who can help support and improve the software.
1. **Modularity**: Every component is built to be used as a library: well-tested, documented and benchmarked. Import crates, mix and match, and innovate on top of them. Learn more about the project's components [here](./docs/repo/layout.md).
2. **Performance**: Built with Rust, [Alloy](https://github.com/alloy-rs/alloy/), [revm](https://github.com/bluealloy/revm/), and [Foundry](https://github.com/foundry-rs/foundry/) — battle-tested and optimized for speed. Check the [ethPandaOps Lab Dashboard](https://lab.ethpandaops.io/ethereum/execution/timings) for a third-party comparison against other Ethereum clients.
Here's what that looks like in practice, measured with [reth-bench](https://github.com/paradigmxyz/reth/tree/main/bin/reth-bench) on Ethereum Mainnet:
More concretely, our goals are:
![](./assets/reth-perf.png)
1. **Modularity**: Every component of Reth is built to be used as a library: well-tested, heavily documented and benchmarked. We envision that developers will import the node's crates, mix and match, and innovate on top of them. Examples of such usage include but are not limited to spinning up standalone P2P networks, talking directly to a node's database, or "unbundling" the node into the components you need. To achieve that, we are licensing Reth under the Apache/MIT permissive license. You can learn more about the project's components [here](./docs/repo/layout.md).
2. **Performance**: Reth aims to be fast, so we use Rust and the [Erigon staged-sync](https://erigon.substack.com/p/erigon-stage-sync-and-control-flows) node architecture. We also use our Ethereum libraries (including [Alloy](https://github.com/alloy-rs/alloy/) and [revm](https://github.com/bluealloy/revm/)) which we've battle-tested and optimized via [Foundry](https://github.com/foundry-rs/foundry/).
3. **Free for anyone to use any way they want**: Reth is free open source software, built for the community, by the community. By licensing the software under the Apache/MIT license, we want developers to use it without being bound by business licenses, or having to think about the implications of GPL-like licenses.
4. **Client Diversity**: The Ethereum protocol becomes more antifragile when no node implementation dominates. This ensures that if there's a software bug, the network does not finalize a bad block. By building a new client, we hope to contribute to Ethereum's antifragility.
5. **Support as many EVM chains as possible**: We aspire that Reth can full-sync not only Ethereum, but also other chains like Optimism, Polygon, BNB Smart Chain, and more. If you're working on any of these projects, please reach out. Note: OP-Reth has moved to [ethereum-optimism/optimism](https://github.com/ethereum-optimism/optimism).
6. **Configurability**: We want to solve for node operators that care about fast historical queries, but also for hobbyists who cannot operate on large hardware. We also want to support teams and individuals who want both sync from genesis and via "fast sync". We envision that Reth will be configurable enough and provide configurable "profiles" for the tradeoffs that each team faces.
3. **Free for anyone to use any way they want**: Apache/MIT licensed, no business license restrictions.
4. **Client Diversity**: More client implementations make Ethereum more antifragile.
5. **Support as many EVM chains as possible**: Reth can sync Ethereum and other EVM chains. If you're building one, reach out.
6. **Configurability**: Profiles for different use cases — from high-performance RPC operators to hobbyists on consumer hardware.
## Status
Reth is production ready, and suitable for usage in mission-critical environments such as staking or high-uptime services. We also actively recommend professional node operators to switch to Reth in production for performance and cost reasons in use cases where high performance with great margins is required such as RPC, MEV, Indexing, Simulations, and P2P activities.
More historical context below:
- We released **Reth 2.0** in April 2026. See the [release notes](https://github.com/paradigmxyz/reth/releases/tag/v2.0.0) and [blog post](https://www.paradigm.xyz/2026/04/releasing-reth-2-0).
- We released 1.0 "production-ready" stable Reth in June 2024.
- Reth completed an audit with [Sigma Prime](https://sigmaprime.io/), the developers of [Lighthouse](https://github.com/sigp/lighthouse), the Rust Consensus Layer implementation. Find it [here](./audit/sigma_prime_audit_v2.pdf).
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)). We will publish the results soon.
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)).
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024, the last beta release.
- We released [beta](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) on Monday March 4, 2024, our first breaking change to the database model, providing faster query speed, smaller database footprint, and allowing "history" to be mounted on separate drives.
- We shipped iterative improvements until the last alpha release on February 28, 2024, [0.1.0-alpha.21](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.21).
- We [initially announced](https://www.paradigm.xyz/2023/06/reth-alpha) [0.1.0-alpha.1](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.1) on June 20, 2023.
### Database compatibility
### Storage compatibility
We do not have any breaking database changes since beta.1, and we do not plan any in the near future.
Storage V2 is the default for new nodes in Reth 2.0. Existing V1 nodes continue to work, but V1 support will be removed in a future release — all users are encouraged to migrate. V2 snapshots are available at [snapshots.reth.rs](https://snapshots.reth.rs/).
Reth [v0.2.0-beta.1](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) includes
a [set of breaking database changes](https://github.com/paradigmxyz/reth/pull/5191) that makes it impossible to use database files produced by earlier versions.
If you had a database produced by alpha versions of Reth, you need to drop it with `reth db drop`
(using the same arguments such as `--config` or `--datadir` that you passed to `reth node`), and resync using the same `reth node` command you've used before.
![](./assets/reth-storage.png)
## For Users

BIN
assets/reth-perf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/reth-storage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -11,7 +11,7 @@ use alloy_eips::eip7685::Requests;
use alloy_evm::{
block::{
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
BlockExecutorFor, ExecutableTx, OnStateHook, StateChangeSource, StateDB,
BlockExecutorFor, ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
precompiles::PrecompilesMap,
@@ -386,7 +386,10 @@ where
self.inner_mut().execute_transaction_without_commit(tx)
}
fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
fn commit_transaction(
&mut self,
output: Self::Result,
) -> Result<GasOutput, BlockExecutionError> {
let gas_used = self.inner_mut().commit_transaction(output)?;
// Fix up cumulative_gas_used on the just-committed receipt so that

View File

@@ -31,8 +31,10 @@ reth-tracing.workspace = true
# alloy
alloy-consensus.workspace = true
alloy-eip7928 = { workspace = true, features = ["rlp"] }
alloy-eips.workspace = true
alloy-json-rpc.workspace = true
alloy-rlp.workspace = true
alloy-primitives = { workspace = true, features = ["rand"] }
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }

View File

@@ -54,13 +54,8 @@ impl BenchContext {
}
}
// set up alloy client for blocks, retrying on 429/503 (default) and 502
let retry_policy =
RateLimitRetryPolicy::default().or(|err: &alloy_transport::TransportError| -> bool {
err.as_transport_err()
.and_then(|t| t.as_http_error())
.is_some_and(|e| e.status == 502)
});
// set up alloy client for blocks, retrying on any errors, whether HTTP or OS
let retry_policy = RateLimitRetryPolicy::default().or(|_| true);
let max_retries = bench_args.rpc_block_fetch_retries.as_max_retries();
let client = ClientBuilder::default()
.layer(RetryBackoffLayer::new_with_policy(max_retries, 800, u64::MAX, retry_policy))

View File

@@ -6,7 +6,12 @@
//! [`ExecutionData`] and environment switches at each block boundary.
use alloy_consensus::{TxEnvelope, TxReceipt};
use alloy_eips::{eip1559::BaseFeeParams, eip7840::BlobParams, Typed2718};
use alloy_eips::{
eip1559::BaseFeeParams,
eip7840::BlobParams,
eip7928::{AccountChanges, BlockAccessList, SlotChanges},
BlockNumberOrTag, Typed2718,
};
use alloy_primitives::{Bloom, Bytes, B256};
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
use alloy_rpc_client::ClientBuilder;
@@ -24,7 +29,7 @@ use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
use reth_ethereum_primitives::Receipt;
use reth_primitives_traits::proofs;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::{collections::HashMap, future::Future};
use tracing::{info, warn};
/// A single transaction with its gas used and raw encoded bytes.
@@ -215,6 +220,9 @@ pub struct BigBlockPayload {
/// Big block data containing environment switches and prior block hashes.
#[serde(default)]
pub big_block_data: BigBlockData<ExecutionData>,
/// Flattened BAL across all constituent blocks, if requested during generation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_access_list: Option<BlockAccessList>,
}
/// `reth bench generate-big-block` command
@@ -252,6 +260,11 @@ pub struct Command {
/// Output directory for generated payloads.
#[arg(long, value_name = "OUTPUT_DIR")]
output_dir: std::path::PathBuf,
/// Query `eth_getBlockAccessListByBlockNumber` for each fetched block and persist
/// the flattened BAL on the stored payload.
#[arg(long, default_value_t = false)]
bal: bool,
}
impl Command {
@@ -273,6 +286,7 @@ impl Command {
from_block = self.from_block,
target_gas = self.target_gas,
num_big_blocks = self.num_big_blocks,
include_bal = self.bal,
chain = %chain_spec.chain(),
output_dir = %self.output_dir.display(),
"Generating big block payloads"
@@ -312,6 +326,7 @@ impl Command {
// Fetch consecutive blocks until the gas target is reached.
let mut blocks = Vec::new();
let mut block_receipts: Vec<Vec<Receipt>> = Vec::new();
let mut block_access_lists: Vec<Option<BlockAccessList>> = Vec::new();
let mut accumulated_block_gas: u64 = 0;
let mut reached_chain_tip = false;
@@ -338,6 +353,14 @@ impl Command {
Err(e) => return Err(e.into()),
};
let block_access_list = if self.bal {
Some(fetch_block_access_list(&provider, block_number).await.wrap_err_with(
|| format!("Failed to fetch BAL for block {block_number}"),
)?)
} else {
None
};
// Convert RPC receipts to consensus receipts
let consensus_receipts: Vec<Receipt> = receipts
.iter()
@@ -375,10 +398,14 @@ impl Command {
let execution_data = ExecutionData { payload, sidecar };
let block_gas = execution_data.payload.as_v1().gas_used;
let block_blob_gas =
execution_data.payload.as_v3().map(|v3| v3.blob_gas_used).unwrap_or(0);
info!(
target: "reth-bench",
block_number,
gas_used = block_gas,
blob_gas_used = block_blob_gas,
tx_count = execution_data.payload.transactions().len(),
receipts = consensus_receipts.len(),
"Fetched block"
@@ -387,6 +414,7 @@ impl Command {
accumulated_block_gas += block_gas;
blocks.push(execution_data);
block_receipts.push(consensus_receipts);
block_access_lists.push(block_access_list);
next_block += 1;
}
@@ -404,6 +432,7 @@ impl Command {
// Block 0 is the base
let mut base = blocks.remove(0);
let base_receipts = block_receipts.remove(0);
let mut merged_block_access_list = block_access_lists.remove(0);
let mut env_switches = Vec::new();
// Accumulate all receipts with corrected cumulative_gas_used.
@@ -439,12 +468,22 @@ impl Command {
let mut total_gas_limit = base.payload.as_v1().gas_limit;
// Concatenate transactions from subsequent blocks and build env_switches
for (block_data, receipts) in blocks.into_iter().zip(block_receipts) {
for ((block_data, receipts), block_access_list) in
blocks.into_iter().zip(block_receipts).zip(block_access_lists)
{
let block_v1 = block_data.payload.as_v1();
let block_gas = block_v1.gas_used;
total_gas_used += block_gas;
total_gas_limit += block_v1.gas_limit;
if let Some(block_access_list) = block_access_list {
merge_block_access_list(
merged_block_access_list.get_or_insert_with(Default::default),
block_access_list,
cumulative_tx_count as u64,
);
}
// Accumulate receipts with corrected cumulative_gas_used
all_receipts.extend(receipts.into_iter().map(|mut r| {
r.cumulative_gas_used += cumulative_gas_offset;
@@ -579,6 +618,7 @@ impl Command {
env_switches,
prior_block_hashes: accumulated_block_hashes.clone(),
},
block_access_list: merged_block_access_list,
};
// Accumulate real block hashes from this big block's env_switches for
@@ -610,6 +650,7 @@ impl Command {
total_gas_used = big_block.execution_data.payload.as_v1().gas_used,
env_switches = big_block.big_block_data.env_switches.len(),
prior_block_hashes = big_block.big_block_data.prior_block_hashes.len(),
bal_accounts = big_block.block_access_list.as_ref().map_or(0, Vec::len),
"Big block payload saved"
);
@@ -628,6 +669,85 @@ impl Command {
}
}
async fn fetch_block_access_list(
provider: &RootProvider<AnyNetwork>,
block_number: u64,
) -> eyre::Result<BlockAccessList> {
provider
.client()
.request("eth_getBlockAccessListByBlockNumber", (BlockNumberOrTag::Number(block_number),))
.await
.map_err(Into::into)
.and_then(|block_access_list: Option<BlockAccessList>| {
block_access_list.ok_or_else(|| eyre::eyre!("BAL not found for block {block_number}"))
})
}
fn merge_block_access_list(
merged: &mut BlockAccessList,
incoming: BlockAccessList,
tx_index_offset: u64,
) {
let mut account_positions = merged
.iter()
.enumerate()
.map(|(idx, account)| (account.address, idx))
.collect::<HashMap<_, _>>();
for mut account_changes in incoming {
shift_account_changes(&mut account_changes, tx_index_offset);
if let Some(&idx) = account_positions.get(&account_changes.address) {
merge_account_changes(&mut merged[idx], account_changes);
} else {
account_positions.insert(account_changes.address, merged.len());
merged.push(account_changes);
}
}
}
fn shift_account_changes(account_changes: &mut AccountChanges, tx_index_offset: u64) {
for slot_changes in &mut account_changes.storage_changes {
for change in &mut slot_changes.changes {
change.block_access_index += tx_index_offset;
}
}
for change in &mut account_changes.balance_changes {
change.block_access_index += tx_index_offset;
}
for change in &mut account_changes.nonce_changes {
change.block_access_index += tx_index_offset;
}
for change in &mut account_changes.code_changes {
change.block_access_index += tx_index_offset;
}
}
fn merge_account_changes(existing: &mut AccountChanges, incoming: AccountChanges) {
merge_slot_changes(&mut existing.storage_changes, incoming.storage_changes);
existing.storage_reads.extend(incoming.storage_reads);
existing.balance_changes.extend(incoming.balance_changes);
existing.nonce_changes.extend(incoming.nonce_changes);
existing.code_changes.extend(incoming.code_changes);
}
fn merge_slot_changes(existing: &mut Vec<SlotChanges>, incoming: Vec<SlotChanges>) {
let mut slot_positions = existing
.iter()
.enumerate()
.map(|(idx, slot_changes)| (slot_changes.slot, idx))
.collect::<HashMap<_, _>>();
for slot_changes in incoming {
if let Some(&idx) = slot_positions.get(&slot_changes.slot) {
existing[idx].changes.extend(slot_changes.changes);
} else {
slot_positions.insert(slot_changes.slot, existing.len());
existing.push(slot_changes);
}
}
}
/// Computes the block hash for an [`ExecutionData`] by converting it to a raw block
/// and hashing the header.
pub fn compute_payload_block_hash(data: &ExecutionData) -> eyre::Result<B256> {
@@ -638,3 +758,94 @@ pub fn compute_payload_block_hash(data: &ExecutionData) -> eyre::Result<B256> {
.wrap_err("failed to convert payload to block for hash computation")?;
Ok(block.header.hash_slow())
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eips::eip7928::{BalanceChange, CodeChange, NonceChange, StorageChange};
use alloy_primitives::{Address, U256};
#[test]
fn merge_block_access_list_offsets_and_merges_accounts() {
let shared = Address::repeat_byte(0x11);
let other = Address::repeat_byte(0x22);
let mut merged = vec![AccountChanges {
address: shared,
storage_changes: vec![SlotChanges::new(
U256::from(1),
vec![StorageChange::new(0, U256::from(10))],
)],
storage_reads: vec![U256::from(3)],
balance_changes: vec![BalanceChange::new(1, U256::from(100))],
nonce_changes: vec![NonceChange::new(2, 7)],
code_changes: vec![],
}];
let incoming = vec![
AccountChanges {
address: shared,
storage_changes: vec![
SlotChanges::new(U256::from(1), vec![StorageChange::new(1, U256::from(20))]),
SlotChanges::new(U256::from(2), vec![StorageChange::new(2, U256::from(30))]),
],
storage_reads: vec![U256::from(4)],
balance_changes: vec![BalanceChange::new(0, U256::from(150))],
nonce_changes: vec![NonceChange::new(2, 8)],
code_changes: vec![CodeChange::new(1, Bytes::from_static(&[0xaa]))],
},
AccountChanges {
address: other,
storage_changes: vec![SlotChanges::new(
U256::from(9),
vec![StorageChange::new(0, U256::from(90))],
)],
storage_reads: vec![],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
},
];
merge_block_access_list(&mut merged, incoming, 3);
assert_eq!(merged.len(), 2);
let shared = &merged[0];
assert_eq!(shared.storage_reads, vec![U256::from(3), U256::from(4)]);
assert_eq!(
shared
.balance_changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![1, 3]
);
assert_eq!(
shared.nonce_changes.iter().map(|change| change.block_access_index).collect::<Vec<_>>(),
vec![2, 5]
);
assert_eq!(shared.code_changes[0].block_access_index, 4);
let slot_one = shared
.storage_changes
.iter()
.find(|slot_changes| slot_changes.slot == U256::from(1))
.unwrap();
assert_eq!(
slot_one.changes.iter().map(|change| change.block_access_index).collect::<Vec<_>>(),
vec![0, 4]
);
let slot_two = shared
.storage_changes
.iter()
.find(|slot_changes| slot_changes.slot == U256::from(2))
.unwrap();
assert_eq!(slot_two.changes[0].block_access_index, 5);
let other = &merged[1];
assert_eq!(other.address, Address::repeat_byte(0x22));
assert_eq!(other.storage_changes[0].changes[0].block_access_index, 3);
}
}

View File

@@ -3,7 +3,7 @@
use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
generate_big_block::BigBlockPayload,
generate_big_block::{compute_payload_block_hash, BigBlockPayload},
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
@@ -12,12 +12,14 @@ use crate::{
},
valid_payload::{call_forkchoice_updated_with_reth, call_new_payload_with_reth},
};
use alloy_eip7928::bal::Bal;
use alloy_eips::eip7928::BlockAccessList;
use alloy_primitives::B256;
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
use alloy_rpc_client::ClientBuilder;
use alloy_rpc_types_engine::{
CancunPayloadFields, ExecutionData, ExecutionPayloadEnvelopeV4, ExecutionPayloadSidecar,
ForkchoiceState, JwtSecret, PraguePayloadFields,
CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV4,
ExecutionPayloadSidecar, ExecutionPayloadV4, ForkchoiceState, JwtSecret, PraguePayloadFields,
};
use clap::Parser;
use eyre::Context;
@@ -83,6 +85,14 @@ pub struct Command {
#[arg(long, default_value = "false", verbatim_doc_comment)]
reth_new_payload: bool,
/// Forward embedded block access lists to `reth_newPayload` when payload files contain them.
///
/// Disabled by default so the same payload set can be replayed with or without BALs.
///
/// Requires `--reth-new-payload`.
#[arg(long, default_value = "false", verbatim_doc_comment, requires = "reth_new_payload")]
bal: bool,
/// Control when `reth_newPayload` waits for in-flight persistence.
///
/// Accepts `always` (default — wait on every block), `never`, or a number N
@@ -126,6 +136,8 @@ struct LoadedPayload {
block_hash: B256,
/// Big block data containing environment switches and prior block hashes.
big_block_data: BigBlockData<ExecutionData>,
/// Optional BAL flattened into the payload file.
block_access_list: Option<BlockAccessList>,
}
impl Command {
@@ -139,6 +151,9 @@ impl Command {
}
if self.reth_new_payload {
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
if self.bal {
info!(target: "reth-bench", "Forwarding embedded block_access_list data");
}
}
let mut metrics_scraper = MetricsScraper::maybe_new(self.metrics_url.clone());
@@ -185,10 +200,13 @@ impl Command {
}
info!(target: "reth-bench", count = payloads.len(), "Loaded main payloads from disk");
let has_env_switches = payloads.iter().any(|p| !p.big_block_data.env_switches.is_empty());
let has_block_access_lists = payloads.iter().any(|p| {
p.block_access_list.as_ref().is_some_and(|bal: &BlockAccessList| !bal.is_empty())
});
// 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",
@@ -196,6 +214,18 @@ impl Command {
env_switches are only supported with reth_newPayload and will be ignored."
);
}
if has_block_access_lists {
warn!(
target: "reth-bench",
"Payloads contain block_access_list data but --reth-new-payload is not set. \
BALs are only forwarded with reth_newPayload and will be ignored."
);
}
} else if has_block_access_lists && !self.bal {
info!(
target: "reth-bench",
"Payloads contain block_access_list data but --bal is not set. BALs will be ignored."
);
}
let mut parent_hash = initial_parent_hash;
@@ -205,7 +235,7 @@ impl Command {
for (i, payload) in payloads.iter().enumerate() {
let execution_data = &payload.execution_data;
let block_hash = payload.block_hash;
let mut block_hash = payload.block_hash;
let v1 = execution_data.payload.as_v1();
let gas_used = v1.gas_used;
@@ -243,10 +273,39 @@ impl Command {
.wait_for_persistence
.unwrap_or(WaitForPersistence::Never)
.rpc_value(block_number);
// Inject sidecar BAL into the inline V4 payload field when --bal is set.
// If the payload is not already V4 we upgrade it (V3→V4) so the BAL
// can be carried inline. This changes the block hash, so we recompute
// it and patch parent_hash to maintain the chain.
let mut execution_data = execution_data.clone();
if self.bal &&
let Some(bal) = &payload.block_access_list
{
let encoded_bal: alloy_primitives::Bytes =
alloy_rlp::encode(Bal::from(bal.clone())).into();
// Upgrade to V4 if necessary, then set the BAL field.
if execution_data.payload.as_v4().is_none() {
execution_data.payload = upgrade_to_v4(execution_data.payload, encoded_bal);
} else {
execution_data.payload.as_v4_mut().unwrap().block_access_list = encoded_bal;
}
// Patch parent_hash so this block chains off the (possibly
// rehashed) previous block.
execution_data.payload.as_v1_mut().parent_hash = parent_hash;
// Recompute block hash after payload modification and update
// the hash stored in the payload itself.
block_hash = compute_payload_block_hash(&execution_data)?;
execution_data.payload.as_v1_mut().block_hash = block_hash;
}
(
None,
serde_json::to_value((
RethNewPayloadInput::ExecutionData(execution_data.clone()),
RethNewPayloadInput::ExecutionData(execution_data),
wait_for_persistence,
self.no_wait_for_caches.then_some(false),
big_block_data_param,
@@ -417,26 +476,27 @@ impl Command {
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
// Try BigBlockPayload first, then fall back to legacy ExecutionPayloadEnvelopeV4
let (execution_data, big_block_data) =
if let Ok(big_block) = serde_json::from_str::<BigBlockPayload>(&content) {
(big_block.execution_data, big_block.big_block_data)
} else {
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
let execution_data = ExecutionData {
payload: envelope.envelope_inner.execution_payload.clone().into(),
sidecar: ExecutionPayloadSidecar::v4(
CancunPayloadFields {
versioned_hashes: Vec::new(),
parent_beacon_block_root: B256::ZERO,
},
PraguePayloadFields {
requests: envelope.execution_requests.clone().into(),
},
),
};
(execution_data, BigBlockData::default())
let (execution_data, big_block_data, block_access_list) = if let Ok(big_block) =
serde_json::from_str::<BigBlockPayload>(&content)
{
(big_block.execution_data, big_block.big_block_data, big_block.block_access_list)
} 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(), None)
};
let block_hash = execution_data.payload.as_v1().block_hash;
@@ -446,13 +506,48 @@ impl Command {
block_hash = %block_hash,
env_switches = big_block_data.env_switches.len(),
prior_block_hashes = big_block_data.prior_block_hashes.len(),
bal_accounts = block_access_list.as_ref().map_or(0, Vec::len),
path = %path.display(),
"Loaded payload"
);
payloads.push(LoadedPayload { index, execution_data, block_hash, big_block_data });
payloads.push(LoadedPayload {
index,
execution_data,
block_hash,
big_block_data,
block_access_list,
});
}
Ok(payloads)
}
}
/// Upgrades an [`ExecutionPayload`] to V4 by wrapping the inner V3 payload (constructing
/// default V2/V3 layers for V1 payloads if needed) and setting the provided BAL bytes.
fn upgrade_to_v4(
payload: ExecutionPayload,
block_access_list: alloy_primitives::Bytes,
) -> ExecutionPayload {
use alloy_rpc_types_engine::{ExecutionPayloadV2, ExecutionPayloadV3};
let v3 = match payload {
ExecutionPayload::V4(_) => unreachable!("caller checks as_v4().is_none()"),
ExecutionPayload::V3(v3) => v3,
ExecutionPayload::V2(v2) => {
ExecutionPayloadV3 { payload_inner: v2, blob_gas_used: 0, excess_blob_gas: 0 }
}
ExecutionPayload::V1(v1) => ExecutionPayloadV3 {
payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals: Vec::new() },
blob_gas_used: 0,
excess_blob_gas: 0,
},
};
ExecutionPayload::V4(ExecutionPayloadV4 {
payload_inner: v3,
block_access_list,
slot_number: 0,
})
}

View File

@@ -242,6 +242,7 @@ 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();
@@ -256,6 +257,9 @@ 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
}
}
}
};
@@ -264,6 +268,9 @@ 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
}
}
}

View File

@@ -266,6 +266,22 @@ pub(crate) fn payload_to_new_payload(
ExecutionPayload::V1(payload) => {
(EngineApiMessageVersion::V1, serde_json::to_value((payload,))?)
}
ExecutionPayload::V4(payload) => {
let cancun = sidecar
.cancun()
.ok_or_else(|| eyre::eyre!("missing cancun sidecar for V4 payload"))?;
let version = target_version.unwrap_or(EngineApiMessageVersion::V4);
let requests = sidecar.prague().map(|p| p.requests.clone()).unwrap_or_default();
(
version,
serde_json::to_value((
payload,
cancun.versioned_hashes.clone(),
cancun.parent_beacon_block_root,
requests,
))?,
)
}
};
Ok((version, params, execution_data))
@@ -370,7 +386,10 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
) -> TransportResult<ForkchoiceUpdated> {
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
match message_version {
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 |
EngineApiMessageVersion::V6 => {
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
}
EngineApiMessageVersion::V2 => {

View File

@@ -70,7 +70,7 @@ aquamarine.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
[dev-dependencies]
alloy-node-bindings = "1.6.3"
alloy-node-bindings = "2.0.0"
alloy-provider = { workspace = true, features = ["reqwest"] }
alloy-rpc-types-eth.workspace = true
backon.workspace = true

View File

@@ -274,8 +274,54 @@ impl DeferredTrieData {
/// In normal operation, the parent always has a cached overlay and this
/// function is never called.
///
/// Iterates ancestors oldest -> newest, then extends with current block's data,
/// so later state takes precedence.
/// When the `rayon` feature is enabled:
/// 1. Collects ancestor data (states and updates)
/// 2. Merges states and trie updates in parallel using k-way merge
#[cfg(feature = "rayon")]
fn merge_ancestors_into_overlay(
ancestors: &[Self],
sorted_hashed_state: &HashedPostStateSorted,
sorted_trie_updates: &TrieUpdatesSorted,
) -> TrieInputSorted {
// Early exit: no ancestors means just wrap current block's data
if ancestors.is_empty() {
return TrieInputSorted::new(
Arc::new(sorted_trie_updates.clone()),
Arc::new(sorted_hashed_state.clone()),
Default::default(),
);
}
// Collect ancestor data in reverse (newest to oldest) for merge_slice
let (states, updates): (Vec<_>, Vec<_>) = ancestors
.iter()
.rev()
.map(|a| {
// Note: we can assume that this trie data has already been computed
let data = a.wait_cloned();
(data.hashed_state, data.trie_updates)
})
.unzip();
// Merge state and nodes in parallel using k-way merge
let (state, nodes) = rayon::join(
|| {
let mut merged = HashedPostStateSorted::merge_slice(&states);
merged.extend_ref_and_sort(sorted_hashed_state);
merged
},
|| {
let mut merged = TrieUpdatesSorted::merge_slice(&updates);
merged.extend_ref_and_sort(sorted_trie_updates);
merged
},
);
TrieInputSorted::new(Arc::new(nodes), Arc::new(state), Default::default())
}
/// Sequential fallback when rayon is not available.
#[cfg(not(feature = "rayon"))]
fn merge_ancestors_into_overlay(
ancestors: &[Self],
sorted_hashed_state: &HashedPostStateSorted,
@@ -293,18 +339,8 @@ impl DeferredTrieData {
nodes_mut.extend_ref_and_sort(ancestor_data.trie_updates.as_ref());
}
// Extend with current block's sorted data last (takes precedence)
#[cfg(feature = "rayon")]
rayon::join(
|| state_mut.extend_ref_and_sort(sorted_hashed_state),
|| nodes_mut.extend_ref_and_sort(sorted_trie_updates),
);
#[cfg(not(feature = "rayon"))]
{
state_mut.extend_ref_and_sort(sorted_hashed_state);
nodes_mut.extend_ref_and_sort(sorted_trie_updates);
}
state_mut.extend_ref_and_sort(sorted_hashed_state);
nodes_mut.extend_ref_and_sort(sorted_trie_updates);
overlay
}

View File

@@ -1169,6 +1169,7 @@ mod tests {
&self,
_input: TrieInput,
_target: HashedPostState,
_mode: reth_trie::ExecutionWitnessMode,
) -> ProviderResult<Vec<Bytes>> {
Ok(Vec::default())
}

View File

@@ -197,9 +197,14 @@ impl<N: NodePrimitives> StateProofProvider for MemoryOverlayStateProviderRef<'_,
self.historical.multiproof(input, targets)
}
fn witness(&self, mut input: TrieInput, target: HashedPostState) -> ProviderResult<Vec<Bytes>> {
fn witness(
&self,
mut input: TrieInput,
target: HashedPostState,
mode: reth_trie::ExecutionWitnessMode,
) -> ProviderResult<Vec<Bytes>> {
input.prepend_self(self.trie_input().clone());
self.historical.witness(input, target)
self.historical.witness(input, target, mode)
}
}

View File

@@ -28,7 +28,7 @@ use alloy_consensus::{
};
use alloy_eips::{
eip1559::INITIAL_BASE_FEE, eip7685::EMPTY_REQUESTS_HASH, eip7840::BlobParams,
eip7892::BlobScheduleBlobParams,
eip7892::BlobScheduleBlobParams, eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
};
use alloy_genesis::{ChainConfig, Genesis};
use alloy_primitives::{address, b256, Address, BlockNumber, B256, U256};
@@ -76,6 +76,18 @@ 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 an empty bal 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(),
@@ -93,6 +105,8 @@ 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()
}
}
@@ -275,7 +289,6 @@ pub fn create_chain_config(
// Check if DAO fork is supported (it has an activation block)
let dao_fork_support = hardforks.fork(EthereumHardfork::Dao) != ForkCondition::Never;
#[expect(clippy::needless_update)]
ChainConfig {
chain_id: chain.map(|c| c.id()).unwrap_or(0),
homestead_block: block_num(EthereumHardfork::Homestead),
@@ -298,6 +311,7 @@ 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),
@@ -305,10 +319,6 @@ pub fn create_chain_config(
bpo5_time: timestamp(EthereumHardfork::Bpo5),
terminal_total_difficulty,
terminal_total_difficulty_passed,
ethash: None,
clique: None,
parlia: None,
extra_fields: Default::default(),
deposit_contract_address,
blob_schedule,
..Default::default()
@@ -885,6 +895,7 @@ impl From<Genesis> for ChainSpec {
(EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
(EthereumHardfork::Bpo4.boxed(), genesis.config.bpo4_time),
(EthereumHardfork::Bpo5.boxed(), genesis.config.bpo5_time),
(EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
];
let mut time_hardforks = time_hardfork_opts
@@ -1191,6 +1202,19 @@ 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

View File

@@ -248,6 +248,7 @@ fn selection_to_prune_mode(
ComponentSelection::Distance(d) => {
Some(PruneMode::Distance(min_distance.map_or(d, |min| d.max(min))))
}
ComponentSelection::Since(block) => Some(PruneMode::Before(block)),
ComponentSelection::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
}
}
@@ -453,6 +454,36 @@ mod tests {
assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
}
#[test]
fn selections_since_maps_to_before_prune_mode() {
let mut selections = BTreeMap::new();
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
selections
.insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
selections.insert(
SnapshotComponentType::AccountChangesets,
ComponentSelection::Since(15_537_394),
);
selections.insert(
SnapshotComponentType::StorageChangesets,
ComponentSelection::Since(15_537_394),
);
let config = config_for_selections(
&selections,
&empty_manifest(),
None,
None::<&reth_chainspec::ChainSpec>,
);
assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
}
#[test]
fn full_preset_matches_default_full_prune_config() {
let mut selections = BTreeMap::new();

View File

@@ -119,6 +119,9 @@ pub enum ComponentSelection {
/// Download only the most recent chunks covering at least `distance` blocks.
/// Maps to `PruneMode::Distance(distance)` in the generated config.
Distance(u64),
/// Download chunks starting at the specified block number.
/// Maps to `PruneMode::Before(block)` in the generated config.
Since(u64),
/// Don't download this component at all.
/// Maps to `PruneMode::Full` for tx-based segments, or a minimal distance for others.
None,
@@ -129,6 +132,7 @@ impl std::fmt::Display for ComponentSelection {
match self {
Self::All => write!(f, "All"),
Self::Distance(d) => write!(f, "Last {d} blocks"),
Self::Since(block) => write!(f, "Since block {block}"),
Self::None => write!(f, "None"),
}
}
@@ -936,6 +940,7 @@ mod tests {
fn component_selection_display() {
assert_eq!(ComponentSelection::All.to_string(), "All");
assert_eq!(ComponentSelection::Distance(10_064).to_string(), "Last 10064 blocks");
assert_eq!(ComponentSelection::Since(15_537_394).to_string(), "Since block 15537394");
assert_eq!(ComponentSelection::None.to_string(), "None");
}

File diff suppressed because it is too large Load Diff

View File

@@ -262,6 +262,7 @@ impl SelectorApp {
ComponentSelection::None => return 0,
ComponentSelection::All => None,
ComponentSelection::Distance(d) => Some(d),
ComponentSelection::Since(block) => Some(self.manifest.block - block + 1),
};
self.groups[group_idx]
.types
@@ -344,6 +345,7 @@ fn format_selection(sel: &ComponentSelection) -> String {
match sel {
ComponentSelection::All => "All".to_string(),
ComponentSelection::Distance(d) => format!("Last {d} blocks"),
ComponentSelection::Since(block) => format!("Since block {block}"),
ComponentSelection::None => "None".to_string(),
}
}

View File

@@ -78,9 +78,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateC
self.env.init::<N>(AccessRights::RW, runtime)?;
let static_file_provider = provider_factory.static_file_provider();
let provider_rw = provider_factory.database_provider_rw()?;
if self.without_evm {
let provider_rw = provider_factory.database_provider_rw()?;
// ensure header, total difficulty and header hash are provided
let header = self.header.ok_or_else(|| eyre::eyre!("Header file must be provided"))?;
let header = without_evm::read_header_from_file::<
@@ -106,23 +107,22 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateC
// SAFETY: it's safe to commit static files, since in the event of a crash, they
// will be unwound according to database checkpoints.
//
// Necessary to commit, so the header is accessible to provider_rw and
// init_state_dump
// Necessary to commit, so the header is accessible to init_from_state_dump
static_file_provider.commit()?;
} else if last_block_number > 0 && last_block_number < header.number() {
return Err(eyre::eyre!(
"Data directory should be empty when calling init-state with --without-evm."
));
}
provider_rw.commit()?;
}
info!(target: "reth::cli", "Initiating state dump");
let reader = BufReader::new(reth_fs_util::open(self.state)?);
let hash = init_from_state_dump(reader, &provider_rw, config.stages.etl)?;
provider_rw.commit()?;
let hash = init_from_state_dump(reader, &provider_factory, config.stages.etl)?;
info!(target: "reth::cli", hash = ?hash, "Genesis block written");
Ok(())

View File

@@ -403,7 +403,7 @@ pub fn validate_against_parent_gas_limit<
})
}
// Check if the self gas limit is below the minimum required limit.
else if header.gas_limit() < MINIMUM_GAS_LIMIT {
if header.gas_limit() < MINIMUM_GAS_LIMIT {
return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() })
}

View File

@@ -55,6 +55,8 @@ 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

View File

@@ -227,6 +227,7 @@ 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()?
@@ -299,6 +300,7 @@ 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(

View File

@@ -269,6 +269,7 @@ where
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
};
crate::setup_import::setup_engine_with_chain_import(
@@ -295,6 +296,7 @@ where
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
}
.into()
}

View File

@@ -3,7 +3,8 @@ use alloy_consensus::{
};
use alloy_eips::{eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization};
use alloy_network::{
eip2718::Encodable2718, Ethereum, EthereumWallet, TransactionBuilder, TransactionBuilder4844,
eip2718::Encodable2718, Ethereum, EthereumWallet, NetworkTransactionBuilder,
TransactionBuilder4844,
};
use alloy_primitives::{hex, Address, Bytes, TxKind, B256, U256};
use alloy_rpc_types_eth::{Authorization, TransactionInput, TransactionRequest};
@@ -117,7 +118,8 @@ impl TransactionTestContext {
let mut builder = SidecarBuilder::<SimpleCoder>::new();
builder.ingest(b"dummy blob");
tx.set_blob_sidecar(builder.build()?);
let sidecar: alloy_consensus::BlobTransactionSidecar = builder.build()?;
tx.set_blob_sidecar(alloy_eips::eip7594::BlobTransactionSidecarVariant::Eip4844(sidecar));
tx.set_max_fee_per_blob_gas(15e9 as u128);
let signed = Self::sign_tx(wallet, tx).await;
@@ -127,7 +129,9 @@ impl TransactionTestContext {
/// Signs an arbitrary [`TransactionRequest`] using the provided wallet
pub async fn sign_tx(wallet: PrivateKeySigner, tx: TransactionRequest) -> TxEnvelope {
let signer = EthereumWallet::from(wallet);
<TransactionRequest as TransactionBuilder<Ethereum>>::build(tx, &signer).await.unwrap()
<TransactionRequest as NetworkTransactionBuilder<Ethereum>>::build(tx, &signer)
.await
.unwrap()
}
/// Creates a tx with blob sidecar and sign it, returning bytes
@@ -151,7 +155,7 @@ impl TransactionTestContext {
));
let tx = tx(chain_id, 210000, Some(l1_block_info), None, nonce, Some(20e9 as u128));
let signer = EthereumWallet::from(wallet);
<TransactionRequest as TransactionBuilder<Ethereum>>::build(tx, &signer)
<TransactionRequest as NetworkTransactionBuilder<Ethereum>>::build(tx, &signer)
.await
.unwrap()
.encoded_2718()

View File

@@ -160,6 +160,7 @@ async fn test_testsuite_assert_mine_block() -> Result<()> {
suggested_fee_recipient: Address::random(),
withdrawals: None,
parent_beacon_block_root: None,
slot_number: None,
},
));

View File

@@ -90,6 +90,7 @@ const fn test_attributes_generator(timestamp: u64) -> PayloadAttributes {
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
}
}

View File

@@ -19,6 +19,7 @@ use reth_trie::{
MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
};
use std::{
fmt,
sync::{
atomic::{AtomicU64, AtomicUsize, Ordering},
Arc,
@@ -147,6 +148,29 @@ pub enum CachedStatus<T> {
Cached(T),
}
/// The source that is using the execution cache.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachedStateMetricsSource {
/// Engine (validation).
Engine,
/// Payload builder.
Builder,
/// Tests.
#[cfg(any(test, feature = "test-utils"))]
Test,
}
impl fmt::Display for CachedStateMetricsSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Engine => f.write_str("engine"),
Self::Builder => f.write_str("builder"),
#[cfg(any(test, feature = "test-utils"))]
Self::Test => f.write_str("test"),
}
}
}
/// Metrics for the cached state provider, showing hits / misses for each cache
#[derive(Metrics, Clone)]
#[metrics(scope = "sync.caching")]
@@ -222,9 +246,10 @@ impl CachedStateMetrics {
self.account_cache_collisions.set(0);
}
/// Returns a new zeroed-out instance of [`CachedStateMetrics`].
pub fn zeroed() -> Self {
let zeroed = Self::default();
/// Returns a new zeroed-out instance of [`CachedStateMetrics`] with a `source` label
/// to distinguish between different callers (e.g., engine vs builder).
pub fn zeroed(source: CachedStateMetricsSource) -> Self {
let zeroed = Self::new_with_labels(&[("source", source.to_string())]);
zeroed.reset();
zeroed
}
@@ -566,8 +591,9 @@ impl<S: StateProofProvider, const PREWARM: bool> StateProofProvider
&self,
input: TrieInput,
target: HashedPostState,
mode: reth_trie::ExecutionWitnessMode,
) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
self.state_provider.witness(input, target)
self.state_provider.witness(input, target, mode)
}
}
@@ -918,27 +944,15 @@ pub struct SavedCache {
/// The caches used for the provider.
caches: ExecutionCache,
/// Metrics for the cached state provider (includes size/capacity/collisions from fixed-cache)
metrics: CachedStateMetrics,
/// A guard to track in-flight usage of this cache.
/// The cache is considered available if the strong count is 1.
usage_guard: Arc<()>,
/// Whether to skip cache metrics recording (can be expensive with large cached state).
disable_cache_metrics: bool,
}
impl SavedCache {
/// Creates a new instance with the internals
pub fn new(hash: B256, caches: ExecutionCache, metrics: CachedStateMetrics) -> Self {
Self { hash, caches, metrics, usage_guard: Arc::new(()), disable_cache_metrics: false }
}
/// Sets whether to disable cache metrics recording.
pub const fn with_disable_cache_metrics(mut self, disable: bool) -> Self {
self.disable_cache_metrics = disable;
self
pub fn new(hash: B256, caches: ExecutionCache) -> Self {
Self { hash, caches, usage_guard: Arc::new(()) }
}
/// Returns the hash for this cache
@@ -946,11 +960,6 @@ impl SavedCache {
self.hash
}
/// Splits the cache into its caches, metrics, and `disable_cache_metrics` flag, consuming it.
pub fn split(self) -> (ExecutionCache, CachedStateMetrics, bool) {
(self.caches, self.metrics, self.disable_cache_metrics)
}
/// Returns true if the cache is available for use (no other tasks are currently using it).
pub fn is_available(&self) -> bool {
Arc::strong_count(&self.usage_guard) == 1
@@ -966,20 +975,11 @@ impl SavedCache {
&self.caches
}
/// Returns the metrics associated with this cache.
pub const fn metrics(&self) -> &CachedStateMetrics {
&self.metrics
}
/// Updates the cache metrics (size/capacity/collisions) from the stats handlers.
///
/// Note: This can be expensive with large cached state. Use
/// `with_disable_cache_metrics(true)` to skip.
pub fn update_metrics(&self) {
if self.disable_cache_metrics {
return
pub fn update_metrics(&self, metrics: Option<&CachedStateMetrics>) {
if let Some(metrics) = metrics {
self.caches.update_metrics(metrics);
}
self.caches.update_metrics(&self.metrics);
}
/// Clears all caches, resetting them to empty state,
@@ -1016,8 +1016,11 @@ mod tests {
provider.extend_accounts(vec![(address, account)]);
let caches = ExecutionCache::new(1000);
let state_provider =
CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
let state_provider = CachedStateProvider::new(
provider,
caches,
CachedStateMetrics::zeroed(CachedStateMetricsSource::Test),
);
let res = state_provider.storage(address, storage_key);
assert!(res.is_ok());
@@ -1036,8 +1039,11 @@ mod tests {
provider.extend_accounts(vec![(address, account)]);
let caches = ExecutionCache::new(1000);
let state_provider =
CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
let state_provider = CachedStateProvider::new(
provider,
caches,
CachedStateMetrics::zeroed(CachedStateMetricsSource::Test),
);
let res = state_provider.storage(address, storage_key);
assert!(res.is_ok());
@@ -1074,7 +1080,7 @@ mod tests {
#[test]
fn test_saved_cache_is_available() {
let execution_cache = ExecutionCache::new(1000);
let cache = SavedCache::new(B256::ZERO, execution_cache, CachedStateMetrics::zeroed());
let cache = SavedCache::new(B256::ZERO, execution_cache);
assert!(cache.is_available(), "Cache should be available initially");
@@ -1086,8 +1092,7 @@ mod tests {
#[test]
fn test_saved_cache_multiple_references() {
let execution_cache = ExecutionCache::new(1000);
let cache =
SavedCache::new(B256::from([2u8; 32]), execution_cache, CachedStateMetrics::zeroed());
let cache = SavedCache::new(B256::from([2u8; 32]), execution_cache);
let guard1 = cache.clone_guard_for_test();
let guard2 = cache.clone_guard_for_test();

View File

@@ -165,11 +165,7 @@ mod tests {
let hash = B256::from([1u8; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
*slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
});
let first = cache.get_cache_for(hash);
@@ -185,11 +181,7 @@ mod tests {
let hash = B256::from([2u8; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
*slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
});
let checked_out = cache.get_cache_for(hash);
@@ -207,11 +199,7 @@ mod tests {
let hash_b = B256::from([0xBB; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash_a,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
*slot = Some(SavedCache::new(hash_a, ExecutionCache::new(1_000)))
});
let checked_out = cache.get_cache_for(hash_b);

View File

@@ -160,7 +160,11 @@ fn generate(
hashed_state: reth_trie::HashedPostState,
state_provider: Box<dyn StateProvider>,
) -> eyre::Result<ExecutionWitness> {
let state = state_provider.witness(Default::default(), hashed_state)?;
let state = state_provider.witness(
Default::default(),
hashed_state,
reth_trie::ExecutionWitnessMode::Legacy,
)?;
Ok(ExecutionWitness {
state,
codes: codes.into_values().collect(),
@@ -239,6 +243,7 @@ where
DebugApiClient::<()>::debug_execution_witness(
healthy_node_client,
block_number.into(),
None,
)
.await
})?;

View File

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

View File

@@ -41,7 +41,7 @@ reth-trie-db.workspace = true
alloy-evm.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-eip7928.workspace = true
alloy-eip7928 = { workspace = true, features = ["rlp"] }
alloy-primitives.workspace = true
alloy-rlp.workspace = true
alloy-rpc-types-engine.workspace = true
@@ -60,6 +60,7 @@ metrics.workspace = true
reth-metrics = { workspace = true, features = ["common"] }
# misc
indexmap.workspace = true
schnellru.workspace = true
rayon.workspace = true
tracing.workspace = true

View File

@@ -1,6 +1,7 @@
use crate::tree::metrics::BlockBufferMetrics;
use alloy_consensus::BlockHeader;
use alloy_primitives::{BlockHash, BlockNumber};
use indexmap::IndexSet;
use reth_primitives_traits::{Block, SealedBlock};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
@@ -22,7 +23,7 @@ pub struct BlockBuffer<B: Block> {
/// Map of any parent block hash (even the ones not currently in the buffer)
/// to the buffered children.
/// Allows connecting buffered blocks by parent.
pub(crate) parent_to_child: HashMap<BlockHash, HashSet<BlockHash>>,
pub(crate) parent_to_child: HashMap<BlockHash, IndexSet<BlockHash>>,
/// `BTreeMap` tracking the earliest blocks by block number.
/// Used for removal of old blocks that precede finalization.
pub(crate) earliest_blocks: BTreeMap<BlockNumber, HashSet<BlockHash>>,
@@ -139,7 +140,7 @@ impl<B: Block> BlockBuffer<B> {
fn remove_from_parent(&mut self, parent_hash: BlockHash, hash: &BlockHash) {
// remove from parent to child connection, but only for this block parent.
if let Some(entry) = self.parent_to_child.get_mut(&parent_hash) {
entry.remove(hash);
entry.swap_remove(hash);
// if set is empty remove block entry.
if entry.is_empty() {
self.parent_to_child.remove(&parent_hash);

View File

@@ -237,8 +237,9 @@ impl<S: StateProofProvider> StateProofProvider for InstrumentedStateProvider<S>
&self,
input: TrieInput,
target: HashedPostState,
mode: reth_trie::ExecutionWitnessMode,
) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
self.state_provider.witness(input, target)
self.state_provider.witness(input, target, mode)
}
}

View File

@@ -71,7 +71,8 @@ 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,
CachedStateMetrics, CachedStateMetricsSource, CachedStateProvider, ExecutionCache,
PayloadExecutionCache, SavedCache,
};
pub mod state;
@@ -1692,7 +1693,6 @@ where
let gas_used = payload.gas_used();
let num_hash = payload.num_hash();
let mut output = self.on_new_payload(payload);
let latency = start.elapsed();
self.metrics.engine.new_payload.update_response_metrics(
start,
&mut self.metrics.engine.forkchoice_updated.latest_finish_at,
@@ -1700,6 +1700,12 @@ where
gas_used,
);
// Latency measures time from enqueue to completion, excluding
// only the explicit persistence wait. This means backpressure
// (time spent queued due to the engine being busy) is included,
// reflecting real-world engine responsiveness.
let latency = enqueued_at.elapsed() - explicit_persistence_wait;
let maybe_event =
output.as_mut().ok().and_then(|out| out.event.take());

View File

@@ -1,539 +0,0 @@
//! BAL (Block Access List, EIP-7928) related functionality.
use alloy_consensus::constants::KECCAK_EMPTY;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{keccak256, Address, StorageKey, U256};
use reth_primitives_traits::Account;
use reth_provider::{AccountReader, ProviderError};
use reth_trie::{HashedPostState, HashedStorage};
use std::ops::Range;
/// Returns the total number of storage slots (both changed and read-only) across all accounts in
/// the BAL.
pub fn total_slots(bal: &BlockAccessList) -> usize {
bal.iter().map(|account| account.storage_changes.len() + account.storage_reads.len()).sum()
}
/// Iterator over storage slots in a [`BlockAccessList`], with range-based filtering.
///
/// Iterates over all `(Address, StorageKey)` pairs representing both changed and read-only
/// storage slots across all accounts in the BAL. For each account, changed slots are iterated
/// first, followed by read-only slots. The iterator intelligently skips accounts and slots
/// outside the specified range for efficient traversal.
#[derive(Debug)]
pub struct BALSlotIter<'a> {
bal: &'a BlockAccessList,
range: Range<usize>,
current_index: usize,
account_idx: usize,
/// Index within the current account's combined slots (changed + read-only).
/// If `slot_idx < storage_changes.len()`, we're in changed slots.
/// Otherwise, we're in read-only slots at index `slot_idx - storage_changes.len()`.
slot_idx: usize,
}
impl<'a> BALSlotIter<'a> {
/// Creates a new iterator over storage slots within the specified range.
pub fn new(bal: &'a BlockAccessList, range: Range<usize>) -> Self {
let mut iter = Self { bal, range, current_index: 0, account_idx: 0, slot_idx: 0 };
iter.skip_to_range_start();
iter
}
/// Skips to the first item within the range.
fn skip_to_range_start(&mut self) {
while self.account_idx < self.bal.len() {
let account = &self.bal[self.account_idx];
let slots_in_account = account.storage_changes.len() + account.storage_reads.len();
// Check if this account contains items in our range
let account_end = self.current_index + slots_in_account;
if account_end <= self.range.start {
// Entire account is before range, skip it
self.current_index = account_end;
self.account_idx += 1;
self.slot_idx = 0;
} else if self.current_index < self.range.start {
// Range starts somewhere in this account
let skip_slots = self.range.start - self.current_index;
self.slot_idx = skip_slots;
self.current_index = self.range.start;
break;
} else {
// We're at or past range start
break;
}
}
}
}
impl<'a> Iterator for BALSlotIter<'a> {
type Item = (Address, StorageKey);
fn next(&mut self) -> Option<Self::Item> {
// Check if we've exceeded the range
if self.current_index >= self.range.end {
return None;
}
// Find the next valid slot
while self.account_idx < self.bal.len() {
let account = &self.bal[self.account_idx];
let changed_len = account.storage_changes.len();
let total_len = changed_len + account.storage_reads.len();
if self.slot_idx < total_len {
let address = account.address;
let slot = if self.slot_idx < changed_len {
// We're in changed slots
account.storage_changes[self.slot_idx].slot
} else {
// We're in read-only slots
account.storage_reads[self.slot_idx - changed_len]
};
self.slot_idx += 1;
self.current_index += 1;
// Check if we've reached the end of range
if self.current_index > self.range.end {
return None;
}
return Some((address, StorageKey::from(slot)));
}
// Move to next account
self.account_idx += 1;
self.slot_idx = 0;
}
None
}
}
/// Converts a Block Access List into a [`HashedPostState`] by extracting the final state
/// of modified accounts and storage slots.
pub(crate) fn bal_to_hashed_post_state<P>(
bal: &BlockAccessList,
provider: P,
) -> Result<HashedPostState, ProviderError>
where
P: AccountReader,
{
let mut hashed_state = HashedPostState::with_capacity(bal.len());
for account_changes in bal {
let address = account_changes.address;
// Always fetch the account; even if we don't need the db account to construct the final
// `Account`, doing this fills the cache.
let existing_account = provider.basic_account(&address)?;
// Get the latest balance (last balance change if any)
let balance = account_changes.balance_changes.last().map(|change| change.post_balance);
// Get the latest nonce (last nonce change if any)
let nonce = account_changes.nonce_changes.last().map(|change| change.new_nonce);
// Get the latest code (last code change if any)
let code_hash = if let Some(code_change) = account_changes.code_changes.last() {
if code_change.new_code.is_empty() {
Some(Some(KECCAK_EMPTY))
} else {
Some(Some(keccak256(&code_change.new_code)))
}
} else {
None
};
// If the account was only read then don't add it to the HashedPostState
if balance.is_none() &&
nonce.is_none() &&
code_hash.is_none() &&
account_changes.storage_changes.is_empty()
{
continue
}
// Build the final account state
let account = Account {
balance: balance.unwrap_or_else(|| {
existing_account.as_ref().map(|acc| acc.balance).unwrap_or(U256::ZERO)
}),
nonce: nonce
.unwrap_or_else(|| existing_account.as_ref().map(|acc| acc.nonce).unwrap_or(0)),
bytecode_hash: code_hash.unwrap_or_else(|| {
existing_account.as_ref().and_then(|acc| acc.bytecode_hash).or(Some(KECCAK_EMPTY))
}),
};
let hashed_address = keccak256(address);
hashed_state.accounts.insert(hashed_address, Some(account));
// Process storage changes
if !account_changes.storage_changes.is_empty() {
let mut storage_map = HashedStorage::new(false);
for slot_changes in &account_changes.storage_changes {
let hashed_slot = keccak256(slot_changes.slot.to_be_bytes::<32>());
// Get the last change for this slot
if let Some(last_change) = slot_changes.changes.last() {
storage_map.storage.insert(hashed_slot, last_change.new_value);
}
}
hashed_state.storages.insert(hashed_address, storage_map);
}
}
Ok(hashed_state)
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{
AccountChanges, BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange,
};
use alloy_primitives::{Address, Bytes, StorageKey, B256};
use reth_revm::test_utils::StateProviderTest;
#[test]
fn test_bal_to_hashed_post_state_basic() {
let provider = StateProviderTest::default();
let address = Address::random();
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
assert_eq!(result.accounts.len(), 1);
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
assert!(account_opt.is_some());
let account = account_opt.as_ref().unwrap();
assert_eq!(account.balance, U256::from(100));
assert_eq!(account.nonce, 1);
assert_eq!(account.bytecode_hash, Some(KECCAK_EMPTY));
}
#[test]
fn test_bal_with_storage_changes() {
let provider = StateProviderTest::default();
let address = Address::random();
let slot = U256::random();
let value = U256::random();
let slot_changes = SlotChanges { slot, changes: vec![StorageChange::new(0, value)] };
let account_changes = AccountChanges {
address,
storage_changes: vec![slot_changes],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(500))],
nonce_changes: vec![NonceChange::new(0, 2)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
assert!(result.storages.contains_key(&hashed_address));
let storage = result.storages.get(&hashed_address).unwrap();
let hashed_slot = keccak256(slot.to_be_bytes::<32>());
let stored_value = storage.storage.get(&hashed_slot).unwrap();
assert_eq!(*stored_value, value);
}
#[test]
fn test_bal_with_code_change() {
let provider = StateProviderTest::default();
let address = Address::random();
let code = Bytes::from(vec![0x60, 0x80, 0x60, 0x40]); // Some bytecode
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1000))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![CodeChange::new(0, code.clone())],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
let expected_code_hash = keccak256(&code);
assert_eq!(account.bytecode_hash, Some(expected_code_hash));
}
#[test]
fn test_bal_with_empty_code() {
let provider = StateProviderTest::default();
let address = Address::random();
let empty_code = Bytes::default();
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1000))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![CodeChange::new(0, empty_code)],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
assert_eq!(account.bytecode_hash, Some(KECCAK_EMPTY));
}
#[test]
fn test_bal_multiple_changes_takes_last() {
let provider = StateProviderTest::default();
let address = Address::random();
// Multiple balance changes - should take the last one
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![
BalanceChange::new(0, U256::from(100)),
BalanceChange::new(1, U256::from(200)),
BalanceChange::new(2, U256::from(300)),
],
nonce_changes: vec![
NonceChange::new(0, 1),
NonceChange::new(1, 2),
NonceChange::new(2, 3),
],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
// Should have the last values
assert_eq!(account.balance, U256::from(300));
assert_eq!(account.nonce, 3);
}
#[test]
fn test_bal_uses_provider_for_missing_fields() {
let mut provider = StateProviderTest::default();
let address = Address::random();
let code_hash = B256::random();
let existing_account =
Account { balance: U256::from(999), nonce: 42, bytecode_hash: Some(code_hash) };
provider.insert_account(address, existing_account, None, Default::default());
// Only change balance, nonce and code should come from provider
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1500))],
nonce_changes: vec![],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
// Balance should be updated
assert_eq!(account.balance, U256::from(1500));
// Nonce and bytecode_hash should come from provider
assert_eq!(account.nonce, 42);
assert_eq!(account.bytecode_hash, Some(code_hash));
}
#[test]
fn test_bal_multiple_storage_changes_per_slot() {
let provider = StateProviderTest::default();
let address = Address::random();
let slot = U256::random();
// Multiple changes to the same slot - should take the last one
let slot_changes = SlotChanges {
slot,
changes: vec![
StorageChange::new(0, U256::from(100)),
StorageChange::new(1, U256::from(200)),
StorageChange::new(2, U256::from(300)),
],
};
let account_changes = AccountChanges {
address,
storage_changes: vec![slot_changes],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let storage = result.storages.get(&hashed_address).unwrap();
let hashed_slot = keccak256(slot.to_be_bytes::<32>());
let stored_value = storage.storage.get(&hashed_slot).unwrap();
// Should have the last value
assert_eq!(*stored_value, U256::from(300));
}
#[test]
fn test_bal_slot_iter() {
// Create test data with multiple accounts and slots (both changed and read-only)
let addr1 = Address::repeat_byte(0x01);
let addr2 = Address::repeat_byte(0x02);
let addr3 = Address::repeat_byte(0x03);
// Account 1: 2 changed slots + 1 read-only = 3 total slots (indices 0, 1, 2)
let account1 = AccountChanges {
address: addr1,
storage_changes: vec![
SlotChanges {
slot: U256::from(100),
changes: vec![StorageChange::new(0, U256::ZERO)],
},
SlotChanges {
slot: U256::from(101),
changes: vec![StorageChange::new(0, U256::ZERO)],
},
],
storage_reads: vec![U256::from(102)],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// Account 2: 1 changed slot + 1 read-only = 2 total slots (indices 3, 4)
let account2 = AccountChanges {
address: addr2,
storage_changes: vec![SlotChanges {
slot: U256::from(200),
changes: vec![StorageChange::new(0, U256::ZERO)],
}],
storage_reads: vec![U256::from(201)],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// Account 3: 2 changed slots + 1 read-only = 3 total slots (indices 5, 6, 7)
let account3 = AccountChanges {
address: addr3,
storage_changes: vec![
SlotChanges {
slot: U256::from(300),
changes: vec![StorageChange::new(0, U256::ZERO)],
},
SlotChanges {
slot: U256::from(301),
changes: vec![StorageChange::new(0, U256::ZERO)],
},
],
storage_reads: vec![U256::from(302)],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
let bal = vec![account1, account2, account3];
// Test 1: Iterate over all slots (range 0..8)
let items: Vec<_> = BALSlotIter::new(&bal, 0..8).collect();
assert_eq!(items.len(), 8);
// Account 1: changed slots first (100, 101), then read-only (102)
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(100))));
assert_eq!(items[1], (addr1, StorageKey::from(U256::from(101))));
assert_eq!(items[2], (addr1, StorageKey::from(U256::from(102))));
// Account 2: changed slot (200), then read-only (201)
assert_eq!(items[3], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[4], (addr2, StorageKey::from(U256::from(201))));
// Account 3: changed slots (300, 301), then read-only (302)
assert_eq!(items[5], (addr3, StorageKey::from(U256::from(300))));
assert_eq!(items[6], (addr3, StorageKey::from(U256::from(301))));
assert_eq!(items[7], (addr3, StorageKey::from(U256::from(302))));
// Test 2: Range that skips first account (range 3..6)
let items: Vec<_> = BALSlotIter::new(&bal, 3..6).collect();
assert_eq!(items.len(), 3);
assert_eq!(items[0], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(201))));
assert_eq!(items[2], (addr3, StorageKey::from(U256::from(300))));
// Test 3: Range within first account (range 1..2)
let items: Vec<_> = BALSlotIter::new(&bal, 1..2).collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(101))));
// Test 4: Range spanning multiple accounts (range 2..5)
let items: Vec<_> = BALSlotIter::new(&bal, 2..5).collect();
assert_eq!(items.len(), 3);
// Last slot from account 1 (read-only)
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(102))));
// Account 2 (changed + read-only)
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[2], (addr2, StorageKey::from(U256::from(201))));
// Test 5: Empty range
let items: Vec<_> = BALSlotIter::new(&bal, 5..5).collect();
assert_eq!(items.len(), 0);
// Test 6: Range beyond end (starts at index 6)
let items: Vec<_> = BALSlotIter::new(&bal, 6..100).collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (addr3, StorageKey::from(U256::from(301))));
assert_eq!(items[1], (addr3, StorageKey::from(U256::from(302))));
// Test 7: Range that starts in read-only slots (index 2 is the read-only slot of account 1)
let items: Vec<_> = BALSlotIter::new(&bal, 2..4).collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(102))));
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(200))));
}
}

View File

@@ -4,8 +4,8 @@ use super::precompile_cache::PrecompileCacheMap;
use crate::tree::{
payload_processor::prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
sparse_trie::SparseTrieCacheTask,
CacheWaitDurations, CachedStateMetrics, ExecutionCache, PayloadExecutionCache, SavedCache,
StateProviderBuilder, TreeConfig, WaitForCaches,
CacheWaitDurations, CachedStateMetrics, CachedStateMetricsSource, ExecutionCache,
PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig, WaitForCaches,
};
use alloy_eip7928::BlockAccessList;
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal};
@@ -44,7 +44,6 @@ use std::{
};
use tracing::{debug, debug_span, instrument, trace, warn, Span};
pub mod bal;
pub mod multiproof;
mod preserved_sparse_trie;
pub mod prewarm;
@@ -97,6 +96,8 @@ where
executor: Runtime,
/// The most recent cache used for execution.
execution_cache: PayloadExecutionCache,
/// Metrics for the execution cache.
cache_metrics: Option<CachedStateMetrics>,
/// Metrics for trie operations
trie_metrics: MultiProofTaskMetrics,
/// Cross-block cache size in bytes.
@@ -121,8 +122,6 @@ where
sparse_trie_max_hot_accounts: usize,
/// Whether sparse trie cache pruning is fully disabled.
disable_sparse_trie_cache_pruning: bool,
/// Whether to disable cache metrics recording.
disable_cache_metrics: bool,
}
impl<N, Evm> PayloadProcessor<Evm>
@@ -156,7 +155,8 @@ where
sparse_trie_max_hot_slots: config.sparse_trie_max_hot_slots(),
sparse_trie_max_hot_accounts: config.sparse_trie_max_hot_accounts(),
disable_sparse_trie_cache_pruning: config.disable_sparse_trie_cache_pruning(),
disable_cache_metrics: config.disable_cache_metrics(),
cache_metrics: (!config.disable_cache_metrics())
.then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
}
}
}
@@ -483,6 +483,7 @@ where
saved_cache: saved_cache.clone(),
provider: provider_builder,
metrics: PrewarmMetrics::default(),
cache_metrics: self.cache_metrics.clone(),
terminate_execution: Arc::new(AtomicBool::new(false)),
executed_tx_index: Arc::clone(&executed_tx_index),
precompile_cache_disabled: self.precompile_cache_disabled,
@@ -510,7 +511,12 @@ where
});
}
CacheTaskHandle { saved_cache, to_prewarm_task: Some(to_prewarm_task), executed_tx_index }
CacheTaskHandle {
saved_cache,
to_prewarm_task: Some(to_prewarm_task),
executed_tx_index,
cache_metrics: self.cache_metrics.clone(),
}
}
/// Returns the cache for the given parent hash.
@@ -526,10 +532,10 @@ where
debug!("creating new execution cache on cache miss");
let start = Instant::now();
let cache = ExecutionCache::new(self.cross_block_cache_size);
let metrics = CachedStateMetrics::zeroed();
metrics.record_cache_creation(start.elapsed());
SavedCache::new(parent_hash, cache, metrics)
.with_disable_cache_metrics(self.disable_cache_metrics)
if let Some(metrics) = &self.cache_metrics {
metrics.record_cache_creation(start.elapsed());
}
SavedCache::new(parent_hash, cache)
}
}
@@ -676,7 +682,7 @@ where
block_with_parent: BlockWithParent,
bundle_state: &BundleState,
) {
let disable_cache_metrics = self.disable_cache_metrics;
let cache_metrics = self.cache_metrics.clone();
self.execution_cache.update_with_guard(|cached| {
if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
debug!(
@@ -688,25 +694,19 @@ where
}
// Take existing cache (if any) or create fresh caches
let (caches, cache_metrics, _) = match cached.take() {
Some(existing) => existing.split(),
None => (
ExecutionCache::new(self.cross_block_cache_size),
CachedStateMetrics::zeroed(),
false,
),
let caches = match cached.take() {
Some(existing) => existing.cache().clone(),
None => ExecutionCache::new(self.cross_block_cache_size),
};
// Insert the block's bundle state into cache
let new_cache =
SavedCache::new(block_with_parent.block.hash, caches, cache_metrics)
.with_disable_cache_metrics(disable_cache_metrics);
let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
if new_cache.cache().insert_state(bundle_state).is_err() {
*cached = None;
debug!(target: "engine::caching", "cleared execution cache on update error");
return
}
new_cache.update_metrics();
new_cache.update_metrics(cache_metrics.as_ref());
// Replace with the updated cache
*cached = Some(new_cache);
@@ -800,9 +800,9 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
}
/// Returns a clone of the cache metrics used by prewarming
/// Returns engine cache metrics if a cache exists for prewarming.
pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.metrics().clone())
self.prewarm_handle.cache_metrics.clone()
}
/// Returns a reference to the shared executed transaction index counter.
@@ -853,6 +853,8 @@ pub struct CacheTaskHandle<R> {
/// Shared counter tracking the next transaction index to be executed by the main execution
/// loop. Prewarm workers skip transactions below this index.
executed_tx_index: Arc<AtomicUsize>,
/// Metrics for the execution cache.
cache_metrics: Option<CachedStateMetrics>,
}
impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
@@ -947,8 +949,7 @@ mod tests {
use crate::tree::{
payload_processor::{evm_state_to_hashed_post_state, ExecutionEnv, PayloadProcessor},
precompile_cache::PrecompileCacheMap,
CachedStateMetrics, ExecutionCache, PayloadExecutionCache, SavedCache,
StateProviderBuilder, TreeConfig,
ExecutionCache, PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig,
};
use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
use alloy_evm::block::StateChangeSource;
@@ -974,7 +975,7 @@ mod tests {
fn make_saved_cache(hash: B256) -> SavedCache {
let execution_cache = ExecutionCache::new(1_000);
SavedCache::new(hash, execution_cache, CachedStateMetrics::zeroed())
SavedCache::new(hash, execution_cache)
}
#[test]

View File

@@ -12,9 +12,10 @@
//! 3. When actual block execution happens, it benefits from the warmed cache
use crate::tree::{
payload_processor::{bal, multiproof::StateRootMessage},
payload_processor::multiproof::StateRootMessage,
precompile_cache::{CachedPrecompile, PrecompileCacheMap},
CachedStateProvider, ExecutionEnv, PayloadExecutionCache, SavedCache, StateProviderBuilder,
CachedStateMetrics, CachedStateProvider, ExecutionEnv, PayloadExecutionCache, SavedCache,
StateProviderBuilder,
};
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::BlockAccessList;
@@ -38,6 +39,7 @@ use std::sync::{
mpsc::{self, channel, Receiver, Sender},
Arc,
};
use tokio::sync::oneshot;
use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
/// Determines the prewarming mode: transaction-based, BAL-based, or skipped.
@@ -132,7 +134,7 @@ where
self.executor.spawn_blocking_named("prewarm-txs", move || {
let _enter = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: span,
parent: &span,
"prewarm_txs"
)
.entered();
@@ -275,19 +277,20 @@ where
) {
let start = Instant::now();
let Self { execution_cache, ctx: PrewarmContext { env, metrics, saved_cache, .. }, .. } =
self;
let Self {
execution_cache,
ctx: PrewarmContext { env, metrics, cache_metrics, saved_cache, .. },
..
} = self;
let hash = env.hash;
if let Some(saved_cache) = saved_cache {
debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
// Perform all cache operations atomically under the lock
execution_cache.update_with_guard(|cached| {
// consumes the `SavedCache` held by the prewarming task, which releases its usage
// guard
let (caches, cache_metrics, disable_cache_metrics) = saved_cache.split();
let new_cache = SavedCache::new(hash, caches, cache_metrics)
.with_disable_cache_metrics(disable_cache_metrics);
let caches = saved_cache.cache().clone();
let new_cache = SavedCache::new(hash, caches);
// Insert state into cache while holding the lock
// Access the BundleState through the shared ExecutionOutcome
@@ -298,7 +301,7 @@ where
return;
}
new_cache.update_metrics();
new_cache.update_metrics(cache_metrics.as_ref());
if valid_block_rx.recv().is_ok() {
// Replace the shared cache with the new one; the previous cache (if any) is
@@ -319,28 +322,22 @@ where
}
}
/// Runs BAL-based prewarming by using the prewarming pool's parallel iterator to prefetch
/// accounts and storage slots.
/// Runs BAL-based prewarming and sparse-trie work inline.
///
/// Spawns two halves concurrently on separate pools, then waits for both to complete:
/// 1. Storage prefetch on the prewarming pool to populate the execution cache.
/// 2. Hashed state streaming on the BAL streaming pool so storage updates can reach the sparse
/// trie before account reads finish.
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn run_bal_prewarm(
&self,
bal: Arc<BlockAccessList>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
) {
// Only prefetch if we have a cache to populate
if self.ctx.saved_cache.is_none() {
trace!(
target: "engine::tree::payload_processor::prewarm",
"Skipping BAL prewarm - no cache available"
);
self.send_bal_hashed_state(&bal);
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
}
if bal.is_empty() {
self.send_bal_hashed_state(&bal);
if let Some(to_sparse_trie_task) = self.to_sparse_trie_task.as_ref() {
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
}
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
@@ -353,66 +350,88 @@ where
);
let ctx = self.ctx.clone();
self.executor.prewarming_pool().install_fn(|| {
bal.par_iter().for_each_init(
|| (ctx.clone(), None::<CachedStateProvider<reth_provider::StateProviderBox>>),
|(ctx, provider), account| {
if ctx.should_stop() {
return;
}
ctx.prefetch_bal_account(provider, account);
},
);
});
let to_sparse_trie_task = self.to_sparse_trie_task.clone();
let executor = self.executor.clone();
let parent_span = Span::current();
let prefetch_parent_span = parent_span.clone();
let stream_parent_span = parent_span;
let prefetch_bal = Arc::clone(&bal);
let stream_bal = Arc::clone(&bal);
let (prefetch_tx, prefetch_rx) = oneshot::channel();
let (stream_tx, stream_rx) = oneshot::channel();
trace!(
target: "engine::tree::payload_processor::prewarm",
"All BAL prewarm accounts completed"
);
// Convert BAL to HashedPostState and send to sparse trie task
self.send_bal_hashed_state(&bal);
// Signal that execution has finished
let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
}
/// Converts the BAL to [`HashedPostState`](reth_trie::HashedPostState) and sends it to the
/// sparse trie task.
fn send_bal_hashed_state(&self, bal: &BlockAccessList) {
let Some(to_sparse_trie_task) = &self.to_sparse_trie_task else { return };
let provider = match self.ctx.provider.build() {
Ok(provider) => provider,
Err(err) => {
warn!(
if ctx.saved_cache.is_some() {
let prefetch_ctx = ctx.clone();
executor.prewarming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
?err,
"Failed to build provider for BAL hashed state conversion"
parent: &prefetch_parent_span,
"bal_prefetch_storage",
bal_accounts = prefetch_bal.len(),
);
return;
}
};
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
match bal::bal_to_hashed_post_state(bal, &provider) {
Ok(hashed_state) => {
debug!(
target: "engine::tree::payload_processor::prewarm",
accounts = hashed_state.accounts.len(),
storages = hashed_state.storages.len(),
"Converted BAL to hashed post state"
prefetch_bal.par_iter().for_each_init(
|| {
(
prefetch_ctx.clone(),
None::<CachedStateProvider<reth_provider::StateProviderBox, true>>,
provider_parent_span.clone(),
)
},
|(ctx, provider, parent_span), account| {
if ctx.should_stop() {
return;
}
ctx.prefetch_bal_storage(parent_span, provider, account);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::HashedStateUpdate(hashed_state));
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
}
Err(err) => {
warn!(
target: "engine::tree::payload_processor::prewarm",
?err,
"Failed to convert BAL to hashed state"
);
}
let _ = prefetch_tx.send(());
});
} else {
let _ = prefetch_tx.send(());
}
if let Some(to_sparse_trie_task) = to_sparse_trie_task {
executor.bal_streaming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &stream_parent_span,
"bal_hashed_state_stream",
bal_accounts = stream_bal.len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
stream_bal.par_iter().for_each_init(
|| (ctx.clone(), None::<Box<dyn AccountReader>>, provider_parent_span.clone()),
|(ctx, provider, parent_span), account_changes| {
ctx.send_bal_hashed_state(
parent_span,
provider,
account_changes,
&to_sparse_trie_task,
);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
let _ = stream_tx.send(());
});
} else {
let _ = stream_tx.send(());
}
prefetch_rx
.blocking_recv()
.expect("BAL prefetch task dropped without signaling completion");
stream_rx
.blocking_recv()
.expect("BAL hashed-state streaming task dropped without signaling completion");
let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
}
/// Executes the task.
@@ -504,6 +523,9 @@ where
pub provider: StateProviderBuilder<N, P>,
/// The metrics for the prewarm task.
pub metrics: PrewarmMetrics,
/// Metrics for the execution cache.
/// Metrics for the execution cache. `None` disables metrics recording.
pub cache_metrics: Option<CachedStateMetrics>,
/// An atomic bool that tells prewarm tasks to not start any more execution.
pub terminate_execution: Arc<AtomicBool>,
/// Shared counter tracking the next transaction index to be executed by the main execution
@@ -545,9 +567,11 @@ where
// Use the caches to create a new provider with caching
if let Some(saved_cache) = &self.saved_cache {
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
state_provider =
Box::new(CachedStateProvider::new_prewarm(state_provider, caches, cache_metrics));
state_provider = Box::new(CachedStateProvider::new_prewarm(
state_provider,
caches,
self.cache_metrics.clone().unwrap_or_default(),
));
}
let state_provider = StateProviderDatabase::new(state_provider);
@@ -593,18 +617,146 @@ where
self.terminate_execution.store(true, Ordering::Relaxed);
}
/// Prefetches a single account and all its storage slots from the BAL into the cache.
/// Hashes and streams a single BAL account's state to the sparse trie task.
///
/// For each account, storage slots are hashed and sent immediately, then the account is read
/// from the database and sent as a separate update.
///
/// The `provider` is lazily initialized on first call and reused across accounts on the same
/// thread.
fn prefetch_bal_account(
fn send_bal_hashed_state(
&self,
provider: &mut Option<CachedStateProvider<reth_provider::StateProviderBox>>,
parent_span: &Span,
provider: &mut Option<Box<dyn AccountReader>>,
account_changes: &alloy_eip7928::AccountChanges,
to_sparse_trie_task: &CrossbeamSender<StateRootMessage>,
) {
let address = account_changes.address;
let mut hashed_address = None;
if !account_changes.storage_changes.is_empty() {
let hashed_address = *hashed_address.get_or_insert_with(|| keccak256(address));
let mut storage_map = reth_trie::HashedStorage::new(false);
for slot_changes in &account_changes.storage_changes {
let hashed_slot = keccak256(slot_changes.slot.to_be_bytes::<32>());
if let Some(last_change) = slot_changes.changes.last() {
storage_map.storage.insert(hashed_slot, last_change.new_value);
}
}
let mut hashed_state = reth_trie::HashedPostState::default();
hashed_state.storages.insert(hashed_address, storage_map);
let _ = to_sparse_trie_task.send(StateRootMessage::HashedStateUpdate(hashed_state));
}
if provider.is_none() {
let _span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: parent_span,
"bal_hashed_state_provider_init",
has_saved_cache = self.saved_cache.is_some(),
)
.entered();
let inner = match self.provider.build() {
Ok(p) => p,
Err(err) => {
warn!(
target: "engine::tree::payload_processor::prewarm",
?err,
"Failed to build provider for BAL account reads"
);
return;
}
};
let boxed: Box<dyn AccountReader> = if let Some(saved) = &self.saved_cache {
let caches = saved.cache().clone();
Box::new(CachedStateProvider::new_prewarm(
inner,
caches,
self.cache_metrics.clone().unwrap_or_default(),
))
} else {
Box::new(inner)
};
*provider = Some(boxed);
}
let account_reader = provider.as_ref().expect("provider just initialized");
let existing_account = account_reader.basic_account(&address).ok().flatten();
let balance = account_changes.balance_changes.last().map(|change| change.post_balance);
let nonce = account_changes.nonce_changes.last().map(|change| change.new_nonce);
let code_hash = account_changes.code_changes.last().map(|code_change| {
if code_change.new_code.is_empty() {
alloy_consensus::constants::KECCAK_EMPTY
} else {
keccak256(&code_change.new_code)
}
});
if balance.is_none() &&
nonce.is_none() &&
code_hash.is_none() &&
account_changes.storage_changes.is_empty()
{
return;
}
let account = reth_primitives_traits::Account {
balance: balance.unwrap_or_else(|| {
existing_account
.as_ref()
.map(|account| account.balance)
.unwrap_or(alloy_primitives::U256::ZERO)
}),
nonce: nonce.unwrap_or_else(|| {
existing_account.as_ref().map(|account| account.nonce).unwrap_or(0)
}),
bytecode_hash: code_hash.or_else(|| {
existing_account
.as_ref()
.and_then(|account| account.bytecode_hash)
.or(Some(alloy_consensus::constants::KECCAK_EMPTY))
}),
};
let hashed_address = hashed_address.unwrap_or_else(|| keccak256(address));
let mut hashed_state = reth_trie::HashedPostState::default();
hashed_state.accounts.insert(hashed_address, Some(account));
let _ = to_sparse_trie_task.send(StateRootMessage::HashedStateUpdate(hashed_state));
}
/// Prefetches storage slots for a single BAL account into the cache.
///
/// Account reads are handled separately by [`Self::send_bal_hashed_state`], so this method
/// only
/// warms storage.
///
/// The `provider` is lazily initialized on first call and reused across accounts on the same
/// thread.
fn prefetch_bal_storage(
&self,
parent_span: &Span,
provider: &mut Option<CachedStateProvider<reth_provider::StateProviderBox, true>>,
account: &alloy_eip7928::AccountChanges,
) {
if account.storage_changes.is_empty() && account.storage_reads.is_empty() {
return;
}
let state_provider = match provider {
Some(p) => p,
slot @ None => {
let _span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: parent_span,
"bal_prefetch_provider_init",
)
.entered();
let built = match self.provider.build() {
Ok(p) => p,
Err(err) => {
@@ -619,15 +771,16 @@ where
let saved_cache =
self.saved_cache.as_ref().expect("BAL prewarm should only run with cache");
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
slot.insert(CachedStateProvider::new(built, caches, cache_metrics))
slot.insert(CachedStateProvider::new_prewarm(
built,
caches,
self.cache_metrics.clone().unwrap_or_default(),
))
}
};
let start = Instant::now();
let _ = state_provider.basic_account(&account.address);
for slot in &account.storage_changes {
let _ = state_provider.storage(account.address, StorageKey::from(slot.slot));
}

View File

@@ -128,7 +128,7 @@ where
let parent_span = tracing::Span::current();
let hashing_metrics = metrics.clone();
executor.spawn_blocking_named("trie-hashing", move || {
let _span = debug_span!(parent: parent_span, "run_hashing_task").entered();
let _span = trace_span!(parent: parent_span, "run_hashing_task").entered();
Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
});
@@ -177,7 +177,7 @@ where
SparseTrieTaskMessage::PrefetchProofs(targets)
}
StateRootMessage::StateUpdate(_, state) => {
let _span = debug_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
let hashed = evm_state_to_hashed_post_state(state);
SparseTrieTaskMessage::HashedState(hashed)
}
@@ -542,7 +542,7 @@ where
/// Applies all account and storage leaf updates to corresponding tries and collects any new
/// multiproof targets.
#[instrument(
level = "debug",
level = "trace",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
@@ -551,7 +551,7 @@ where
if new { &mut self.new_storage_updates } else { &mut self.storage_updates };
// Process all storage updates, skipping tries with no pending updates.
let span = debug_span!("process_storage_leaf_updates").entered();
let span = trace_span!("process_storage_leaf_updates").entered();
for (address, updates) in storage_updates {
if updates.is_empty() {
continue;
@@ -596,7 +596,7 @@ where
///
/// Returns whether any updates were drained (applied to the trie).
#[instrument(
level = "debug",
level = "trace",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
@@ -638,11 +638,6 @@ where
/// 3. but the storage root hasn't been updated yet,
///
/// we trigger state root computation on a rayon pool.
#[instrument(
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
fn compute_drained_storage_roots(&mut self) {
let addresses_to_compute_roots: Vec<_> = self
.storage_updates
@@ -665,15 +660,28 @@ where
}
}
let parent_span = tracing::Span::current();
if tries_to_compute_roots.is_empty() {
return;
}
let parent_span =
debug_span!("compute_drained_storage_roots", n = tries_to_compute_roots.len());
tries_to_compute_roots.into_par_iter().for_each(|(address, SendStorageTriePtr(trie))| {
let _enter = debug_span!(
target: "engine::tree::payload_processor::sparse_trie",
parent: &parent_span,
"storage_root",
?address
)
.entered();
let span = if tracing::enabled!(tracing::Level::TRACE) {
debug_span!(
target: "engine::tree::payload_processor::sparse_trie",
parent: &parent_span,
"storage_root",
?address
)
} else {
debug_span!(
target: "engine::tree::payload_processor::sparse_trie",
parent: &parent_span,
"storage_root",
)
};
let _enter = span.entered();
// SAFETY:
// - pointers are created from `storage_tries_mut().get_mut(address)` above;
// - `addresses_to_compute_roots` comes from map iteration, so addresses are unique;
@@ -688,7 +696,7 @@ where
/// storage roots, and promotes corresponding pending account updates into proper leaf updates
/// for accounts trie.
#[instrument(
level = "debug",
level = "trace",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
@@ -702,7 +710,7 @@ where
self.compute_drained_storage_roots();
loop {
let span = debug_span!("promote_updates", promoted = tracing::field::Empty).entered();
let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
// Now handle pending account updates that can be upgraded to a proper update.
let account_rlp_buf = &mut self.account_rlp_buf;
let mut num_promoted = 0;
@@ -770,7 +778,7 @@ where
return;
}
let _span = debug_span!("dispatch_pending_targets").entered();
let _span = trace_span!("dispatch_pending_targets").entered();
let (targets, chunking_length) = self.pending_targets.take();
dispatch_with_chunking(
targets,

View File

@@ -48,7 +48,7 @@ use crate::tree::{
PayloadHandle, StateProviderBuilder, StateProviderDatabase, TreeConfig, WaitForCaches,
};
use alloy_consensus::transaction::{Either, TxHashRef};
use alloy_eip7928::BlockAccessList;
use alloy_eip7928::{bal::Bal, BlockAccessList};
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
use alloy_evm::Evm;
use alloy_primitives::{map::B256Set, B256};
@@ -2100,10 +2100,14 @@ impl<T: PayloadTypes> BlockOrPayload<T> {
}
}
/// Returns the block access list if available.
pub const fn block_access_list(&self) -> Option<Result<BlockAccessList, alloy_rlp::Error>> {
// TODO decode and return `BlockAccessList`
None
/// Returns the block access list embedded in a payload, if present.
pub fn block_access_list(&self) -> Option<Result<BlockAccessList, alloy_rlp::Error>> {
match self {
Self::Payload(payload) => payload.block_access_list().map(|block_access_list| {
alloy_rlp::decode_exact::<Bal>(block_access_list.as_ref()).map(Bal::into_inner)
}),
Self::Block(_) => None,
}
}
/// Returns the number of transactions in the payload or block.

View File

@@ -73,7 +73,9 @@ where
}
}
/// Cache entry, precompile successful output.
/// Cache entry for a successful precompile output.
///
/// We intentionally do not cache non-successful statuses or errors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheEntry<S> {
output: PrecompileOutput,
@@ -180,7 +182,9 @@ where
let result = self.precompile.call(input);
match &result {
Ok(output) => {
// Only successful outputs are cacheable. Non-success statuses and errors must execute
// again instead of poisoning the cache for subsequent calls.
Ok(output) if output.is_success() => {
let size = self.cache.insert(
Bytes::copy_from_slice(calldata),
CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
@@ -228,17 +232,21 @@ mod tests {
use super::*;
use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
use reth_revm::db::EmptyDB;
use revm::{context::TxEnv, precompile::PrecompileOutput};
use revm::{
context::TxEnv,
precompile::{PrecompileOutput, PrecompileStatus},
};
use revm_primitives::hardfork::SpecId;
#[test]
fn test_precompile_cache_basic() {
let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult {
Ok(PrecompileOutput {
status: PrecompileStatus::Success,
gas_used: 0,
gas_refunded: 0,
state_gas_used: 0,
reservoir: 0,
bytes: Bytes::default(),
reverted: false,
})
})
.into();
@@ -247,10 +255,11 @@ mod tests {
CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
let output = PrecompileOutput {
status: PrecompileStatus::Success,
gas_used: 50,
gas_refunded: 0,
state_gas_used: 0,
reservoir: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
reverted: false,
};
let input = b"test_input";
@@ -279,10 +288,11 @@ mod tests {
assert_eq!(input.data, input_data);
Ok(PrecompileOutput {
status: PrecompileStatus::Success,
gas_used: 5000,
gas_refunded: 0,
state_gas_used: 0,
reservoir: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
reverted: false,
})
}
})
@@ -294,10 +304,11 @@ mod tests {
assert_eq!(input.data, input_data);
Ok(PrecompileOutput {
status: PrecompileStatus::Success,
gas_used: 7000,
gas_refunded: 0,
state_gas_used: 0,
reservoir: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
reverted: false,
})
}
})

View File

@@ -34,6 +34,8 @@ pub(crate) fn create_header() -> Header {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
}
}
@@ -138,6 +140,8 @@ pub(crate) fn create_test_block_with_compressed_data(number: BlockNumber) -> Blo
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
};
// Create test body

View File

@@ -109,13 +109,8 @@ where
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
) -> Result<(), ConsensusError> {
let res = validate_block_post_execution(
block,
&self.chain_spec,
&result.receipts,
&result.requests,
receipt_root_bloom,
);
let res =
validate_block_post_execution(block, &self.chain_spec, result, receipt_root_bloom);
if self.skip_requests_hash_check &&
let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res

View File

@@ -1,9 +1,10 @@
use alloc::vec::Vec;
use alloy_consensus::{proofs::calculate_receipt_root, BlockHeader, TxReceipt};
use alloy_eips::{eip7685::Requests, Encodable2718};
use alloy_eips::Encodable2718;
use alloy_primitives::{Bloom, Bytes, B256};
use reth_chainspec::EthereumHardforks;
use reth_consensus::ConsensusError;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{
receipt::gas_spent_by_transactions, Block, GotExpected, Receipt, RecoveredBlock,
};
@@ -18,8 +19,7 @@ use reth_primitives_traits::{
pub fn validate_block_post_execution<B, R, ChainSpec>(
block: &RecoveredBlock<B>,
chain_spec: &ChainSpec,
receipts: &[R],
requests: &Requests,
result: &BlockExecutionResult<R>,
receipt_root_bloom: Option<(B256, Bloom)>,
) -> Result<(), ConsensusError>
where
@@ -28,12 +28,10 @@ where
ChainSpec: EthereumHardforks,
{
// Check if gas used matches the value set in header.
let cumulative_gas_used =
receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
if block.header().gas_used() != cumulative_gas_used {
if block.header().gas_used() != result.gas_used {
return Err(ConsensusError::BlockGasUsed {
gas: GotExpected { got: cumulative_gas_used, expected: block.header().gas_used() },
gas_spent_by_tx: gas_spent_by_transactions(receipts),
gas: GotExpected { got: result.gas_used, expected: block.header().gas_used() },
gas_spent_by_tx: gas_spent_by_transactions(&result.receipts),
})
}
@@ -42,7 +40,7 @@ where
// transaction This was replaced with is_success flag.
// See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
if chain_spec.is_byzantium_active_at_block(block.header().number()) {
let result = if let Some((receipts_root, logs_bloom)) = receipt_root_bloom {
let res = if let Some((receipts_root, logs_bloom)) = receipt_root_bloom {
compare_receipts_root_and_logs_bloom(
receipts_root,
logs_bloom,
@@ -50,11 +48,16 @@ where
block.header().logs_bloom(),
)
} else {
verify_receipts(block.header().receipts_root(), block.header().logs_bloom(), receipts)
verify_receipts(
block.header().receipts_root(),
block.header().logs_bloom(),
&result.receipts,
)
};
if let Err(error) = result {
let receipts = receipts
if let Err(error) = res {
let receipts = result
.receipts
.iter()
.map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
.collect::<Vec<_>>();
@@ -68,7 +71,7 @@ where
let Some(header_requests_hash) = block.header().requests_hash() else {
return Err(ConsensusError::RequestsHashMissing)
};
let requests_hash = requests.requests_hash();
let requests_hash = result.requests.requests_hash();
if requests_hash != header_requests_hash {
return Err(ConsensusError::BodyRequestsHashDiff(
GotExpected::new(requests_hash, header_requests_hash).into(),

View File

@@ -10,4 +10,7 @@ pub enum BuiltPayloadConversionError {
/// Unexpected EIP-7594 sidecars in the built payload.
#[error("unexpected EIP-7594 sidecars")]
UnexpectedEip7594Sidecars,
/// Missing block access list (required for V6 envelope).
#[error("missing block access list")]
MissingBlockAccessList,
}

View File

@@ -6,11 +6,11 @@ use alloy_eips::{
eip7594::{BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant},
eip7685::Requests,
};
use alloy_primitives::U256;
use alloy_primitives::{Bytes, U256};
use alloy_rpc_types_engine::{
BlobsBundleV1, BlobsBundleV2, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3,
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6,
ExecutionPayloadFieldV2, ExecutionPayloadV1, ExecutionPayloadV3,
ExecutionPayloadFieldV2, ExecutionPayloadV1, ExecutionPayloadV3, ExecutionPayloadV4,
};
use reth_ethereum_primitives::EthPrimitives;
use reth_payload_primitives::BuiltPayload;
@@ -36,6 +36,8 @@ pub struct EthBuiltPayload<N: NodePrimitives = EthPrimitives> {
pub(crate) sidecars: BlobSidecars,
/// The requests of the payload
pub(crate) requests: Option<Requests>,
/// The block access list of the payload
pub(crate) block_access_list: Option<Bytes>,
}
// === impl BuiltPayload ===
@@ -48,8 +50,9 @@ impl<N: NodePrimitives> EthBuiltPayload<N> {
block: Arc<SealedBlock<N::Block>>,
fees: U256,
requests: Option<Requests>,
block_access_list: Option<Bytes>,
) -> Self {
Self { block, fees, requests, sidecars: BlobSidecars::Empty }
Self { block, fees, requests, sidecars: BlobSidecars::Empty, block_access_list }
}
/// Returns the built block(sealed)
@@ -152,9 +155,39 @@ impl EthBuiltPayload {
/// Try converting built payload into [`ExecutionPayloadEnvelopeV6`].
///
/// Note: Amsterdam fork is not yet implemented, so this conversion is not yet supported.
/// Returns an error if the block access list is missing, as it's required for V6 envelopes.
pub fn try_into_v6(self) -> Result<ExecutionPayloadEnvelopeV6, BuiltPayloadConversionError> {
unimplemented!("ExecutionPayloadEnvelopeV6 not yet supported")
let Self { block, fees, sidecars, requests, block_access_list, .. } = self;
let block_access_list =
block_access_list.ok_or(BuiltPayloadConversionError::MissingBlockAccessList)?;
let blobs_bundle = match sidecars {
BlobSidecars::Empty => BlobsBundleV2::empty(),
BlobSidecars::Eip7594(sidecars) => BlobsBundleV2::from(sidecars),
BlobSidecars::Eip4844(_) => {
return Err(BuiltPayloadConversionError::UnexpectedEip4844Sidecars)
}
};
Ok(ExecutionPayloadEnvelopeV6 {
execution_payload: ExecutionPayloadV4::from_block_unchecked_with_bal(
block.hash(),
&Arc::unwrap_or_clone(block).into_block(),
block_access_list,
),
block_value: fees,
// From the engine API spec:
//
// > Client software **MAY** use any heuristics to decide whether to set
// `shouldOverrideBuilder` flag or not. If client software does not implement any
// heuristic this flag **SHOULD** be set to `false`.
//
// Spec:
// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
should_override_builder: false,
blobs_bundle,
execution_requests: requests.unwrap_or_default(),
})
}
}

View File

@@ -112,6 +112,8 @@ where
blob_gas_used: block_blob_gas_used,
excess_blob_gas,
requests_hash,
block_access_list_hash: None,
slot_number: None,
};
Ok(Block {

View File

@@ -175,6 +175,7 @@ where
suggested_fee_recipient: attributes.suggested_fee_recipient,
prev_randao: attributes.prev_randao,
gas_limit: attributes.gas_limit,
slot_number: attributes.slot_number,
},
self.chain_spec().next_block_base_fee(parent, attributes.timestamp).unwrap_or_default(),
self.chain_spec(),

View File

@@ -221,6 +221,7 @@ async fn test_testing_build_block_v1_osaka() -> eyre::Result<()> {
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
};
let request = TestingBuildBlockRequestV1 {

View File

@@ -24,6 +24,7 @@ pub(crate) const fn eth_payload_attributes(timestamp: u64) -> PayloadAttributes
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
}
}
@@ -36,6 +37,7 @@ pub(crate) const fn eth_payload_attributes_shanghai(timestamp: u64) -> PayloadAt
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: None,
slot_number: None,
}
}

View File

@@ -56,6 +56,7 @@ async fn testing_rpc_build_block_works() -> eyre::Result<()> {
suggested_fee_recipient: Address::ZERO,
withdrawals: None,
parent_beacon_block_root: None,
slot_number: None,
};
let request = TestingBuildBlockRequestV1 {

View File

@@ -25,7 +25,7 @@ use reth_evm::{
ConfigureEvm, Evm, NextBlockEnvAttributes,
};
use reth_evm_ethereum::EthEvmConfig;
use reth_execution_cache::CachedStateProvider;
use reth_execution_cache::{CachedStateMetrics, CachedStateMetricsSource, CachedStateProvider};
use reth_payload_builder::{BlobSidecars, EthBuiltPayload};
use reth_payload_builder_primitives::PayloadBuilderError;
use reth_payload_primitives::PayloadAttributes;
@@ -172,7 +172,9 @@ where
state_provider = Box::new(CachedStateProvider::new(
state_provider,
execution_cache.cache().clone(),
execution_cache.metrics().clone(),
// It's ok to recreate the cache every time, because it's cheap to do so for a vanilla
// Ethereum builder every 12s.
CachedStateMetrics::zeroed(CachedStateMetricsSource::Builder),
));
}
let state = StateProviderDatabase::new(state_provider.as_ref());
@@ -191,6 +193,7 @@ where
parent_beacon_block_root: attributes.parent_beacon_block_root(),
withdrawals: attributes.withdrawals.clone().map(Into::into),
extra_data: builder_config.extra_data,
slot_number: attributes.slot_number(),
},
)
.map_err(PayloadBuilderError::other)?;
@@ -431,7 +434,7 @@ where
}));
}
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests)
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
// add blob sidecars from the executed txs
.with_sidecars(blob_sidecars);

View File

@@ -21,10 +21,7 @@ use reth_primitives_traits::{
use reth_storage_api::StateProvider;
pub use reth_storage_errors::provider::ProviderError;
use reth_trie_common::{updates::TrieUpdates, HashedPostState};
use revm::{
context::result::ExecutionResult,
database::{states::bundle_state::BundleRetention, BundleState, State},
};
use revm::database::{states::bundle_state::BundleRetention, BundleState, State};
/// A type that knows how to execute a block. It is assumed to operate on a
/// [`crate::Evm`] internally and use [`State`] as database.
@@ -329,9 +326,7 @@ pub trait BlockBuilder {
fn execute_transaction_with_commit_condition(
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(
&ExecutionResult<<<Self::Executor as BlockExecutor>::Evm as Evm>::HaltReason>,
) -> CommitChanges,
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
) -> Result<Option<u64>, BlockExecutionError>;
/// Invokes [`BlockExecutor::execute_transaction_with_result_closure`] and saves the
@@ -339,7 +334,7 @@ pub trait BlockBuilder {
fn execute_transaction_with_result_closure(
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(&ExecutionResult<<<Self::Executor as BlockExecutor>::Evm as Evm>::HaltReason>),
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result),
) -> Result<u64, BlockExecutionError> {
self.execute_transaction_with_commit_condition(tx, |res| {
f(res);
@@ -464,16 +459,14 @@ where
fn execute_transaction_with_commit_condition(
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(
&ExecutionResult<<<Self::Executor as BlockExecutor>::Evm as Evm>::HaltReason>,
) -> CommitChanges,
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
) -> Result<Option<u64>, BlockExecutionError> {
let (tx_env, tx) = tx.into_parts();
if let Some(gas_used) =
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
{
self.transactions.push(tx);
Ok(Some(gas_used))
Ok(Some(gas_used.tx_gas_used()))
} else {
Ok(None)
}

View File

@@ -117,6 +117,7 @@ pub use alloy_evm::{
/// gas_limit: 30_000_000,
/// withdrawals: Some(withdrawals),
/// parent_beacon_block_root: Some(beacon_root),
/// slot_number: None,
/// };
///
/// // Build a new block on top of parent
@@ -501,4 +502,6 @@ pub struct NextBlockEnvAttributes {
pub withdrawals: Option<Withdrawals>,
/// Optional extra data.
pub extra_data: Bytes,
/// Optional slot number for post-Amsterdam payloads.
pub slot_number: Option<u64>,
}

View File

@@ -346,6 +346,15 @@ impl Discv4 {
self.send_to_service(cmd);
}
/// Adds the node as a bootnode.
///
/// This registers the node in the configured bootstrap set and inserts it into the routing
/// table, pinging it to establish the endpoint proof, same as the nodes provided at startup.
pub fn add_boot_node(&self, node_record: NodeRecord) {
let cmd = Discv4Command::AddBootNode(node_record);
self.send_to_service(cmd);
}
/// Adds the peer and id to the ban list.
///
/// This will prevent any future inclusion in the table
@@ -719,6 +728,15 @@ impl Discv4Service {
}
}
/// Adds the node to the bootstrap set and to the routing table.
///
/// Behaves like [`Self::add_node`] but also registers the node in the configured bootstrap
/// set so it is used for subsequent bootstrap attempts.
pub fn add_boot_node(&mut self, record: NodeRecord) -> bool {
self.config.bootstrap_nodes.insert(record);
self.add_node(record)
}
/// Spawns this services onto a new task
///
/// Note: requires a running tokio runtime
@@ -1750,6 +1768,9 @@ impl Discv4Service {
Discv4Command::Add(enr) => {
self.add_node(enr);
}
Discv4Command::AddBootNode(record) => {
self.add_boot_node(record);
}
Discv4Command::Lookup { node_id, tx } => {
let node_id = node_id.unwrap_or(self.local_node_record.id);
self.lookup_with(node_id, tx);
@@ -2066,6 +2087,7 @@ impl Default for ReceiveCache {
/// The commands sent from the frontend [Discv4] to the service [`Discv4Service`].
enum Discv4Command {
Add(NodeRecord),
AddBootNode(NodeRecord),
SetTcpPort(u16),
SetEIP868RLPPair { key: Vec<u8>, rlp: Bytes },
Ban(PeerId, IpAddr),

View File

@@ -20,6 +20,7 @@ reth-primitives-traits.workspace = true
# ethereum
alloy-chains = { workspace = true, features = ["rlp"] }
alloy-eip7928 = { workspace = true, features = ["rlp"], optional = true }
alloy-eips.workspace = true
alloy-primitives = { workspace = true, features = ["map"] }
alloy-rlp = { workspace = true, features = ["derive"] }
@@ -40,6 +41,7 @@ alloy-hardforks.workspace = true
reth-ethereum-primitives = { workspace = true, features = ["arbitrary"] }
alloy-primitives = { workspace = true, features = ["arbitrary", "rand"] }
alloy-consensus = { workspace = true, features = ["arbitrary"] }
alloy-eip7928 = { workspace = true, features = ["arbitrary", "rlp"] }
alloy-eips = { workspace = true, features = ["arbitrary"] }
alloy-genesis.workspace = true
alloy-chains = { workspace = true, features = ["arbitrary"] }
@@ -53,6 +55,7 @@ default = ["std"]
std = [
"alloy-chains/std",
"alloy-consensus/std",
"alloy-eip7928?/std",
"alloy-eips/std",
"alloy-genesis/std",
"alloy-primitives/std",
@@ -68,6 +71,8 @@ std = [
arbitrary = [
"reth-ethereum-primitives/arbitrary",
"alloy-chains/arbitrary",
"dep:alloy-eip7928",
"alloy-eip7928/arbitrary",
"dep:arbitrary",
"dep:proptest",
"dep:proptest-arbitrary-interop",
@@ -88,4 +93,5 @@ serde = [
"reth-primitives-traits/serde",
"reth-ethereum-primitives/serde",
"alloy-hardforks/serde",
"alloy-eip7928?/serde",
]

View File

@@ -2,7 +2,7 @@
use alloc::vec::Vec;
use alloy_primitives::{Bytes, B256};
use alloy_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodableWrapper, RlpEncodableWrapper};
use reth_codecs_derive::add_arbitrary_tests;
/// A request for block access lists from the given block hashes.
@@ -16,12 +16,209 @@ pub struct GetBlockAccessLists(
);
/// Response for [`GetBlockAccessLists`] containing one BAL per requested block hash.
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
///
/// The inner `Bytes` values store raw BAL RLP payloads and are encoded as nested RLP items, not
/// as RLP byte strings.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct BlockAccessLists(
/// The requested block access lists as opaque bytes. Unavailable entries are represented by
/// empty byte slices.
/// The requested block access lists as raw RLP blobs. Per EIP-8159, unavailable entries are
/// represented by an RLP-encoded empty list (`0xc0`).
pub Vec<Bytes>,
);
impl Encodable for BlockAccessLists {
fn encode(&self, out: &mut dyn BufMut) {
let payload_length = self.0.iter().map(|bytes| bytes.len()).sum();
Header { list: true, payload_length }.encode(out);
for bal in &self.0 {
out.put_slice(bal);
}
}
fn length(&self) -> usize {
let payload_length = self.0.iter().map(|bytes| bytes.len()).sum();
Header { list: true, payload_length }.length_with_payload()
}
}
impl Decodable for BlockAccessLists {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
let header = Header::decode(buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let (mut payload, rest) = buf.split_at(header.payload_length);
*buf = rest;
let mut bals = Vec::new();
while !payload.is_empty() {
let item_start = payload;
let item_header = Header::decode(&mut payload)?;
if !item_header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let item_length = item_header.length_with_payload();
bals.push(Bytes::copy_from_slice(&item_start[..item_length]));
payload = &payload[item_header.payload_length..];
}
Ok(Self(bals))
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a> arbitrary::Arbitrary<'a> for BlockAccessLists {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let entries = Vec::<Vec<alloy_eip7928::AccountChanges>>::arbitrary(u)?
.into_iter()
.map(|entry| {
let mut out = Vec::new();
alloy_rlp::encode_list(&entry, &mut out);
Bytes::from(out)
})
.collect();
Ok(Self(entries))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{
AccountChanges, BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange,
};
use alloy_primitives::{Address, U256};
use alloy_rlp::EMPTY_LIST_CODE;
fn elaborate_account_changes(seed: u8) -> Vec<AccountChanges> {
vec![
AccountChanges {
address: Address::from([seed; 20]),
storage_changes: vec![SlotChanges::new(
U256::from_be_bytes([seed.wrapping_add(1); 32]),
vec![
StorageChange::new(1, U256::from_be_bytes([seed.wrapping_add(2); 32])),
StorageChange::new(2, U256::from_be_bytes([seed.wrapping_add(3); 32])),
],
)],
storage_reads: vec![
U256::from_be_bytes([seed.wrapping_add(4); 32]),
U256::from_be_bytes([seed.wrapping_add(5); 32]),
],
balance_changes: vec![
BalanceChange::new(1, U256::from(1_000 + seed as u64)),
BalanceChange::new(2, U256::from(2_000 + seed as u64)),
],
nonce_changes: vec![
NonceChange::new(1, seed as u64),
NonceChange::new(2, seed as u64 + 1),
],
code_changes: vec![CodeChange::new(
1,
Bytes::from(vec![0x60, seed, 0x61, seed.wrapping_add(1), 0x56]),
)],
},
AccountChanges {
address: Address::from([seed.wrapping_add(9); 20]),
storage_changes: Vec::new(),
storage_reads: vec![U256::from_be_bytes([seed.wrapping_add(10); 32])],
balance_changes: vec![BalanceChange::new(3, U256::from(3_000 + seed as u64))],
nonce_changes: vec![NonceChange::new(3, seed as u64 + 2)],
code_changes: vec![CodeChange::new(2, Bytes::from(vec![0x5f, 0x5f, 0xf3]))],
},
]
}
fn elaborate_bal_entry(seed: u8) -> Bytes {
let account_changes = elaborate_account_changes(seed);
let mut out = Vec::new();
alloy_rlp::encode_list(&account_changes, &mut out);
Bytes::from(out)
}
#[test]
fn empty_bal_entry_encodes_as_empty_list() {
let encoded =
alloy_rlp::encode(BlockAccessLists(vec![Bytes::from_static(&[EMPTY_LIST_CODE])]));
assert_eq!(encoded, vec![0xc1, EMPTY_LIST_CODE]);
}
#[test]
fn block_access_lists_roundtrip_preserves_raw_bal_items() {
let original = BlockAccessLists(vec![
Bytes::from_static(&[EMPTY_LIST_CODE]),
Bytes::from_static(&[0xc1, EMPTY_LIST_CODE]),
Bytes::from_static(&[0xc2, EMPTY_LIST_CODE, EMPTY_LIST_CODE]),
]);
let encoded = alloy_rlp::encode(&original);
let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn empty_response_roundtrips() {
let original = BlockAccessLists(Vec::new());
let encoded = alloy_rlp::encode(&original);
let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn rejects_non_list_bal_entries() {
let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, 0x01]).unwrap_err();
assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
}
#[test]
fn elaborate_bal_entry_roundtrips_into_account_changes() {
let expected = elaborate_account_changes(0x11);
let decoded =
alloy_rlp::decode_exact::<Vec<AccountChanges>>(&elaborate_bal_entry(0x11)).unwrap();
assert_eq!(decoded, expected);
}
#[test]
fn elaborate_block_access_lists_roundtrip_preserves_complex_bal_contents() {
let original = BlockAccessLists(vec![
elaborate_bal_entry(0x11),
Bytes::from_static(&[EMPTY_LIST_CODE]),
elaborate_bal_entry(0x77),
]);
let encoded = alloy_rlp::encode(&original);
let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
assert_eq!(decoded, original);
assert_eq!(
alloy_rlp::decode_exact::<Vec<AccountChanges>>(&decoded.0[0]).unwrap(),
elaborate_account_changes(0x11)
);
assert_eq!(alloy_rlp::decode_exact::<Vec<AccountChanges>>(&decoded.0[1]).unwrap(), vec![]);
assert_eq!(
alloy_rlp::decode_exact::<Vec<AccountChanges>>(&decoded.0[2]).unwrap(),
elaborate_account_changes(0x77)
);
}
#[test]
fn elaborate_block_access_lists_embed_raw_bal_payloads_without_reencoding() {
let first = elaborate_bal_entry(0x21);
let second = elaborate_bal_entry(0x42);
let encoded = alloy_rlp::encode(BlockAccessLists(vec![first.clone(), second.clone()]));
let header = alloy_rlp::Header::decode(&mut &encoded[..]).unwrap();
let payload = &encoded[header.length()..];
let expected_payload = [first.as_ref(), second.as_ref()].concat();
assert!(header.list);
assert_eq!(payload, expected_payload.as_slice());
}
}

View File

@@ -296,6 +296,8 @@ mod tests {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
},
]),
}.encode(&mut data);
@@ -333,6 +335,8 @@ mod tests {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
},
]),
};
@@ -439,6 +443,8 @@ mod tests {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
},
],
withdrawals: None,
@@ -516,6 +522,8 @@ mod tests {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
},
],
withdrawals: None,

View File

@@ -152,6 +152,8 @@ mod tests {
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
};
assert_eq!(header.hash_slow(), expected_hash);
}
@@ -268,6 +270,8 @@ mod tests {
excess_blob_gas: Some(0),
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
};
let header = Header::decode(&mut data.as_slice()).unwrap();
@@ -310,6 +314,8 @@ mod tests {
blob_gas_used: Some(0),
excess_blob_gas: Some(0x1600000),
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
};
let header = Header::decode(&mut data.as_slice()).unwrap();

View File

@@ -1,4 +1,4 @@
use crate::EthVersion;
use crate::{BlockRangeUpdate, EthVersion};
use alloy_chains::{Chain, NamedChain};
use alloy_hardforks::{EthereumHardfork, ForkId, Head};
use alloy_primitives::{hex, B256, U256};
@@ -78,6 +78,19 @@ impl UnifiedStatus {
self.latest_block = Some(latest);
}
/// Returns the peer's advertised `BlockRangeUpdate` if this status came from an `eth/69+`
/// handshake.
pub fn block_range_update(&self) -> Option<BlockRangeUpdate> {
(self.version >= EthVersion::Eth69)
.then_some(())
.and_then(|_| self.earliest_block.zip(self.latest_block))
.map(|(earliest, latest)| BlockRangeUpdate {
earliest,
latest,
latest_hash: self.blockhash,
})
}
/// Sets the [`EthVersion`] for the status.
pub const fn set_eth_version(&mut self, v: EthVersion) {
self.version = v;
@@ -457,7 +470,7 @@ impl Display for StatusMessage {
}
#[cfg(test)]
mod tests {
use crate::{EthVersion, Status, StatusEth69, StatusMessage, UnifiedStatus};
use crate::{BlockRangeUpdate, EthVersion, Status, StatusEth69, StatusMessage, UnifiedStatus};
use alloy_consensus::constants::MAINNET_GENESIS_HASH;
use alloy_genesis::Genesis;
use alloy_hardforks::{EthereumHardfork, ForkHash, ForkId, Head};
@@ -564,6 +577,30 @@ mod tests {
assert_eq!(unified_status, roundtripped_unified_status);
}
#[test]
fn block_range_update_for_eth69_status() {
let latest_hash =
b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d");
let status = UnifiedStatus::builder()
.version(EthVersion::Eth69)
.earliest_block(Some(10))
.latest_block(Some(20))
.blockhash(latest_hash)
.build();
assert_eq!(
status.block_range_update(),
Some(BlockRangeUpdate { earliest: 10, latest: 20, latest_hash })
);
}
#[test]
fn block_range_update_is_none_for_legacy_status() {
let status = UnifiedStatus::builder().version(EthVersion::Eth68).build();
assert!(status.block_range_update().is_none());
}
#[test]
fn encode_eth69_status_message() {
let expected = hex!("f8544501a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3c684b715077d8083ed14f2840112a880a0feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d");

View File

@@ -74,6 +74,18 @@ where
Self { eth_snap: EthSnapStreamInner::new(eth_version), inner: stream }
}
/// Create a new eth and snap protocol stream with a custom max message size.
pub const fn with_max_message_size(
stream: S,
eth_version: EthVersion,
max_message_size: usize,
) -> Self {
Self {
eth_snap: EthSnapStreamInner::with_max_message_size(eth_version, max_message_size),
inner: stream,
}
}
/// Returns the eth version
#[inline]
pub const fn eth_version(&self) -> EthVersion {
@@ -188,6 +200,8 @@ where
struct EthSnapStreamInner<N> {
/// Eth protocol version
eth_version: EthVersion,
/// Maximum allowed ETH/Snap message size.
max_message_size: usize,
/// Type marker
_pd: PhantomData<N>,
}
@@ -198,7 +212,12 @@ where
{
/// Create a new eth and snap protocol stream
const fn new(eth_version: EthVersion) -> Self {
Self { eth_version, _pd: PhantomData }
Self::with_max_message_size(eth_version, MAX_MESSAGE_SIZE)
}
/// Create a new eth and snap protocol stream with a custom max message size.
const fn with_max_message_size(eth_version: EthVersion, max_message_size: usize) -> Self {
Self { eth_version, max_message_size, _pd: PhantomData }
}
#[inline]
@@ -208,8 +227,8 @@ where
/// Decode a message from the stream
fn decode_message(&self, bytes: BytesMut) -> Result<EthSnapMessage<N>, EthSnapStreamError> {
if bytes.len() > MAX_MESSAGE_SIZE {
return Err(EthSnapStreamError::MessageTooLarge(bytes.len(), MAX_MESSAGE_SIZE));
if bytes.len() > self.max_message_size {
return Err(EthSnapStreamError::MessageTooLarge(bytes.len(), self.max_message_size));
}
if bytes.is_empty() {

View File

@@ -7,7 +7,7 @@
use crate::{
errors::{EthHandshakeError, EthStreamError},
handshake::EthereumEthHandshake,
message::{EthBroadcastMessage, ProtocolBroadcastMessage},
message::{EthBroadcastMessage, ProtocolBroadcastMessage, MAX_MESSAGE_SIZE},
p2pstream::HANDSHAKE_TIMEOUT,
CanDisconnect, DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, ProtocolMessage,
UnifiedStatus,
@@ -28,10 +28,6 @@ use tokio::time::timeout;
use tokio_stream::Stream;
use tracing::{debug, trace};
/// [`MAX_MESSAGE_SIZE`] is the maximum cap on the size of a protocol message.
// https://github.com/ethereum/go-ethereum/blob/30602163d5d8321fbc68afdcbbaf2362b2641bde/eth/protocols/eth/protocol.go#L50
pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
/// An un-authenticated [`EthStream`]. This is consumed and returns a [`EthStream`] after the
/// `Status` handshake is completed.
#[pin_project]
@@ -110,6 +106,8 @@ where
pub struct EthStreamInner<N> {
/// Negotiated eth version
version: EthVersion,
/// Maximum allowed ETH message size.
max_message_size: usize,
_pd: std::marker::PhantomData<N>,
}
@@ -119,7 +117,12 @@ where
{
/// Creates a new [`EthStreamInner`] with the given eth version
pub const fn new(version: EthVersion) -> Self {
Self { version, _pd: std::marker::PhantomData }
Self::with_max_message_size(version, MAX_MESSAGE_SIZE)
}
/// Creates a new [`EthStreamInner`] with the given eth version and message size limit.
pub const fn with_max_message_size(version: EthVersion, max_message_size: usize) -> Self {
Self { version, max_message_size, _pd: std::marker::PhantomData }
}
/// Returns the eth version
@@ -130,7 +133,7 @@ where
/// Decodes incoming bytes into an [`EthMessage`].
pub fn decode_message(&self, bytes: BytesMut) -> Result<EthMessage<N>, EthStreamError> {
if bytes.len() > MAX_MESSAGE_SIZE {
if bytes.len() > self.max_message_size {
return Err(EthStreamError::MessageTooBig(bytes.len()));
}
@@ -186,7 +189,17 @@ impl<S, N: NetworkPrimitives> EthStream<S, N> {
/// to manually handshake a peer.
#[inline]
pub const fn new(version: EthVersion, inner: S) -> Self {
Self { eth: EthStreamInner::new(version), inner }
Self::with_max_message_size(version, inner, MAX_MESSAGE_SIZE)
}
/// Creates a new unauthed [`EthStream`] with a custom max message size.
#[inline]
pub const fn with_max_message_size(
version: EthVersion,
inner: S,
max_message_size: usize,
) -> Self {
Self { eth: EthStreamInner::with_max_message_size(version, max_message_size), inner }
}
/// Returns the eth version.

View File

@@ -1,6 +1,6 @@
use crate::{
errors::{EthHandshakeError, EthStreamError, P2PStreamError},
ethstream::MAX_MESSAGE_SIZE,
message::MAX_MESSAGE_SIZE,
CanDisconnect,
};
use bytes::{Bytes, BytesMut};

View File

@@ -37,7 +37,7 @@ pub use tokio_util::codec::{
pub use crate::{
disconnect::CanDisconnect,
ethstream::{EthStream, EthStreamInner, UnauthedEthStream, MAX_MESSAGE_SIZE},
ethstream::{EthStream, EthStreamInner, UnauthedEthStream},
hello::{HelloMessage, HelloMessageBuilder, HelloMessageWithProtocols},
p2pstream::{
DisconnectP2P, P2PMessage, P2PMessageID, P2PStream, UnauthedP2PStream, HANDSHAKE_TIMEOUT,

View File

@@ -218,6 +218,7 @@ impl<St> RlpxProtocolMultiplexer<St> {
status: UnifiedStatus,
fork_filter: ForkFilter,
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
) -> Result<(RlpxSatelliteStream<St, EthStream<ProtocolProxy, N>>, UnifiedStatus), EthStreamError>
where
St: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
@@ -231,7 +232,11 @@ impl<St> RlpxProtocolMultiplexer<St> {
let their_status = handshake
.handshake(&mut unauth, status, fork_filter, HANDSHAKE_TIMEOUT)
.await?;
let eth_stream = EthStream::new(eth_cap, unauth.into_inner());
let eth_stream = EthStream::with_max_message_size(
eth_cap,
unauth.into_inner(),
eth_max_message_size,
);
Ok((eth_stream, their_status))
},
)
@@ -775,6 +780,7 @@ mod tests {
use super::*;
use crate::{
handshake::EthHandshake,
message::MAX_MESSAGE_SIZE,
test_utils::{
connect_passthrough, eth_handshake, eth_hello,
proto::{test_hello, TestProtoMessage},
@@ -842,6 +848,7 @@ mod tests {
other_status,
other_fork_filter,
Arc::new(EthHandshake::default()),
MAX_MESSAGE_SIZE,
)
.await
.unwrap();
@@ -877,6 +884,7 @@ mod tests {
status,
fork_filter,
Arc::new(EthHandshake::default()),
MAX_MESSAGE_SIZE,
)
.await
.unwrap();

View File

@@ -1,6 +1,5 @@
//! Decoding tests for [`PooledTransactions`]
use alloy_consensus::transaction::PooledTransaction;
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::hex;
use alloy_rlp::{Decodable, Encodable};
@@ -8,11 +7,17 @@ use reth_eth_wire::{EthNetworkPrimitives, EthVersion, PooledTransactions, Protoc
use std::{fs, path::PathBuf};
use test_fuzz::test_fuzz;
/// Pre-Osaka pooled transaction type using EIP-4844 sidecar format.
/// Test fixtures were generated with this format.
type PreOsakaPooledTransaction = alloy_consensus::EthereumTxEnvelope<
alloy_consensus::TxEip4844WithSidecar<alloy_eips::eip4844::BlobTransactionSidecar>,
>;
/// Helper function to ensure encode-decode roundtrip works for [`PooledTransactions`].
#[test_fuzz]
fn roundtrip_pooled_transactions(hex_data: Vec<u8>) -> Result<(), alloy_rlp::Error> {
let input_rlp = &mut &hex_data[..];
let txs: PooledTransactions = PooledTransactions::decode(input_rlp)?;
let txs: PooledTransactions<PreOsakaPooledTransaction> = PooledTransactions::decode(input_rlp)?;
// get the amount of bytes decoded in `decode` by subtracting the length of the original buf,
// from the length of the remaining bytes
@@ -25,7 +30,8 @@ fn roundtrip_pooled_transactions(hex_data: Vec<u8>) -> Result<(), alloy_rlp::Err
assert_eq!(expected_encoding, buf);
// now do another decoding, on what we encoded - this should succeed
let txs2: PooledTransactions = PooledTransactions::decode(&mut &buf[..]).unwrap();
let txs2: PooledTransactions<PreOsakaPooledTransaction> =
PooledTransactions::decode(&mut &buf[..]).unwrap();
// ensure that the payload length is the same
assert_eq!(txs.length(), txs2.length());
@@ -61,7 +67,7 @@ fn decode_blob_transaction_data() {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/blob_transaction");
let data = fs::read_to_string(network_data_path).expect("Unable to read file");
let hex_data = hex::decode(data.trim()).unwrap();
let _txs = PooledTransaction::decode(&mut &hex_data[..]).unwrap();
let _txs = PreOsakaPooledTransaction::decode(&mut &hex_data[..]).unwrap();
}
#[test]
@@ -71,5 +77,5 @@ fn decode_blob_rpc_transaction() {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/rpc_blob_transaction");
let data = fs::read_to_string(network_data_path).expect("Unable to read file");
let hex_data = hex::decode(data.trim()).unwrap();
let _txs = PooledTransaction::decode_2718(&mut hex_data.as_ref()).unwrap();
let _txs = PreOsakaPooledTransaction::decode_2718(&mut hex_data.as_ref()).unwrap();
}

View File

@@ -115,14 +115,14 @@ pub trait Peers: PeersInfo {
///
/// If the peer already exists, then this will update its tracked info.
fn add_peer(&self, peer: PeerId, tcp_addr: SocketAddr) {
self.add_peer_kind(peer, PeerKind::Static, tcp_addr, None);
self.add_peer_kind(peer, Some(PeerKind::Static), tcp_addr, None);
}
/// Adds a peer to the peer set with TCP and UDP `SocketAddr`.
///
/// If the peer already exists, then this will update its tracked info.
fn add_peer_with_udp(&self, peer: PeerId, tcp_addr: SocketAddr, udp_addr: SocketAddr) {
self.add_peer_kind(peer, PeerKind::Static, tcp_addr, Some(udp_addr));
self.add_peer_kind(peer, Some(PeerKind::Static), tcp_addr, Some(udp_addr));
}
/// Adds a trusted [`PeerId`] to the peer set.
@@ -132,12 +132,12 @@ pub trait Peers: PeersInfo {
/// Adds a trusted peer to the peer set with TCP `SocketAddr`.
fn add_trusted_peer(&self, peer: PeerId, tcp_addr: SocketAddr) {
self.add_peer_kind(peer, PeerKind::Trusted, tcp_addr, None);
self.add_peer_kind(peer, Some(PeerKind::Trusted), tcp_addr, None);
}
/// Adds a trusted peer with TCP and UDP `SocketAddr` to the peer set.
fn add_trusted_peer_with_udp(&self, peer: PeerId, tcp_addr: SocketAddr, udp_addr: SocketAddr) {
self.add_peer_kind(peer, PeerKind::Trusted, tcp_addr, Some(udp_addr));
self.add_peer_kind(peer, Some(PeerKind::Trusted), tcp_addr, Some(udp_addr));
}
/// Adds a peer to the known peer set, with the given kind.
@@ -146,7 +146,7 @@ pub trait Peers: PeersInfo {
fn add_peer_kind(
&self,
peer: PeerId,
kind: PeerKind,
kind: Option<PeerKind>,
tcp_addr: SocketAddr,
udp_addr: Option<SocketAddr>,
);

View File

@@ -125,7 +125,7 @@ where
fn add_peer_kind(
&self,
_peer: PeerId,
_kind: PeerKind,
_kind: Option<PeerKind>,
_tcp_addr: SocketAddr,
_udp_addr: Option<SocketAddr>,
) {

View File

@@ -16,6 +16,7 @@ use reth_eth_wire::{
EthNetworkPrimitives, HelloMessage, HelloMessageWithProtocols, NetworkPrimitives,
UnifiedStatus,
};
use reth_eth_wire_types::message::MAX_MESSAGE_SIZE;
use reth_ethereum_forks::{ForkFilter, Head};
use reth_network_peers::{mainnet_nodes, pk2id, sepolia_nodes, PeerId, TrustedPeer};
use reth_network_types::{PeersConfig, SessionsConfig};
@@ -94,6 +95,8 @@ pub struct NetworkConfig<C, N: NetworkPrimitives = EthNetworkPrimitives> {
/// This can be overridden to support custom handshake logic via the
/// [`NetworkConfigBuilder`].
pub handshake: Arc<dyn EthRlpxHandshake>,
/// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
pub eth_max_message_size: usize,
/// List of block number-hash pairs to check for required blocks.
/// If non-empty, peers that don't have these blocks will be filtered out.
pub required_block_hashes: Vec<BlockNumHash>,
@@ -216,6 +219,8 @@ pub struct NetworkConfigBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
/// The Ethereum P2P handshake, see also:
/// <https://github.com/ethereum/devp2p/blob/master/rlpx.md#initial-handshake>.
handshake: Arc<dyn EthRlpxHandshake>,
/// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
eth_max_message_size: usize,
/// List of block hashes to check for required blocks.
required_block_hashes: Vec<BlockNumHash>,
/// Optional network id
@@ -260,6 +265,7 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
transactions_manager_config: Default::default(),
nat: None,
handshake: Arc::new(EthHandshake::default()),
eth_max_message_size: MAX_MESSAGE_SIZE,
required_block_hashes: Vec::new(),
network_id: None,
}
@@ -580,6 +586,23 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
self
}
/// Sets the maximum allowed ETH message size for post-handshake ETH/Snap streams.
///
/// This does not affect the initial status handshake, which continues to use
/// [`MAX_MESSAGE_SIZE`].
pub const fn eth_max_message_size(mut self, max_message_size: usize) -> Self {
self.eth_max_message_size = max_message_size;
self
}
/// Sets the maximum allowed ETH message size for post-handshake ETH/Snap streams if present.
pub const fn eth_max_message_size_opt(mut self, max_message_size: Option<usize>) -> Self {
if let Some(max_message_size) = max_message_size {
self.eth_max_message_size = max_message_size;
}
self
}
/// Set the optional network id.
pub const fn network_id(mut self, network_id: Option<u64>) -> Self {
self.network_id = network_id;
@@ -618,6 +641,7 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
transactions_manager_config,
nat,
handshake,
eth_max_message_size,
required_block_hashes,
network_id,
} = self;
@@ -690,6 +714,7 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
transactions_manager_config,
nat,
handshake,
eth_max_message_size,
required_block_hashes,
}
}

View File

@@ -284,14 +284,22 @@ where
/// Handles [`GetBlockAccessLists`] queries.
///
/// For now this returns one empty BAL per requested hash.
/// EIP-8159 defines the final `BlockAccessLists` response semantics:
/// <https://eips.ethereum.org/EIPS/eip-8159>
fn on_block_access_lists_request(
&self,
_peer_id: PeerId,
request: GetBlockAccessLists,
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
) {
let access_lists = request.0.into_iter().map(|_| Bytes::new()).collect();
// TODO: BAL serving is not fully implemented yet. Per EIP-8159, unavailable BALs are
// returned as empty BAL entries while preserving request order, so we currently return
// one RLP-encoded empty BAL (`0xc0`) per requested hash.
let access_lists = request
.0
.into_iter()
.map(|_| Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]))
.collect();
let _ = response.send(Ok(BlockAccessLists(access_lists)));
}

View File

@@ -261,6 +261,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
transactions_manager_config: _,
nat,
handshake,
eth_max_message_size,
required_block_hashes,
} = config;
@@ -316,6 +317,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
fork_filter,
extra_protocols,
handshake,
eth_max_message_size,
);
let state = NetworkState::new(

View File

@@ -190,6 +190,16 @@ impl<N: NetworkPrimitives> NetworkHandle<N> {
pub fn secret_key(&self) -> &SecretKey {
&self.inner.secret_key
}
/// Returns the [`Discv4`] handle if discv4 is enabled.
pub fn discv4(&self) -> Option<&Discv4> {
self.inner.discv4.as_ref()
}
/// Returns the [`Discv5`] handle if discv5 is enabled.
pub fn discv5(&self) -> Option<&Discv5> {
self.inner.discv5.as_ref()
}
}
// === API Implementations ===
@@ -315,7 +325,7 @@ impl<N: NetworkPrimitives> Peers for NetworkHandle<N> {
fn add_peer_kind(
&self,
peer: PeerId,
kind: PeerKind,
kind: Option<PeerKind>,
tcp_addr: SocketAddr,
udp_addr: Option<SocketAddr>,
) {
@@ -516,7 +526,7 @@ pub(crate) enum NetworkHandleMessage<N: NetworkPrimitives = EthNetworkPrimitives
/// Marks a peer as trusted.
AddTrustedPeerId(PeerId),
/// Adds an address for a peer, including its ID, kind, and socket address.
AddPeerAddress(PeerId, PeerKind, PeerAddr),
AddPeerAddress(PeerId, Option<PeerKind>, PeerAddr),
/// Removes a peer from the peerset corresponding to the given kind.
RemovePeer(PeerId, PeerKind),
/// Disconnects a connection to a peer if it exists, optionally providing a disconnect reason.

View File

@@ -175,6 +175,7 @@ pub(crate) struct ActiveSession<N: NetworkPrimitives> {
pub(crate) terminate_message:
Option<(PollSender<ActiveSessionMessage<N>>, ActiveSessionMessage<N>)>,
/// The eth69 range info for the remote peer.
/// This is `None` for peers negotiating versions below `eth/69`.
pub(crate) range_info: Option<BlockRangeInfo>,
/// The eth69 range info for the local node (this node).
/// This represents the range of blocks that this node can serve to other peers.
@@ -1116,7 +1117,7 @@ mod tests {
GetBlockBodies, HelloMessageWithProtocols, P2PStream, StatusBuilder, UnauthedEthStream,
UnauthedP2PStream, UnifiedStatus,
};
use reth_eth_wire_types::{EthMessageID, RawCapabilityMessage};
use reth_eth_wire_types::{message::MAX_MESSAGE_SIZE, EthMessageID, RawCapabilityMessage};
use reth_ethereum_forks::EthereumHardfork;
use reth_network_peers::pk2id;
use reth_network_types::session::config::PROTOCOL_BREACH_REQUEST_TIMEOUT;
@@ -1192,6 +1193,7 @@ mod tests {
tokio::task::spawn(start_pending_incoming_session(
Arc::new(EthHandshake::default()),
MAX_MESSAGE_SIZE,
disconnect_rx,
session_id,
stream,

View File

@@ -118,6 +118,8 @@ pub struct SessionManager<N: NetworkPrimitives> {
metrics: SessionManagerMetrics,
/// The [`EthRlpxHandshake`] is used to perform the initial handshake with the peer.
handshake: Arc<dyn EthRlpxHandshake>,
/// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
eth_max_message_size: usize,
/// Shared local range information that gets propagated to active sessions.
/// This represents the range of blocks that this node can serve to other peers.
local_range_info: BlockRangeInfo,
@@ -137,6 +139,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
fork_filter: ForkFilter,
extra_protocols: RlpxSubProtocols,
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
) -> Self {
let (pending_sessions_tx, pending_sessions_rx) = mpsc::channel(config.session_event_buffer);
let (active_session_tx, active_session_rx) = mpsc::channel(config.session_event_buffer);
@@ -171,6 +174,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
disconnections_counter: Default::default(),
metrics: Default::default(),
handshake,
eth_max_message_size,
local_range_info,
}
}
@@ -282,6 +286,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
pending_events.clone(),
start_pending_incoming_session(
self.handshake.clone(),
self.eth_max_message_size,
disconnect_rx,
session_id,
stream,
@@ -324,6 +329,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
pending_events.clone(),
start_pending_outbound_session(
self.handshake.clone(),
self.eth_max_message_size,
disconnect_rx,
pending_events,
session_id,
@@ -553,6 +559,9 @@ impl<N: NetworkPrimitives> SessionManager<N> {
// the `SessionManager` always has an accurate view of total buffered broadcast
// pressure for a peer.
let broadcast_items = BroadcastItemCounter::new();
let remote_range_info = status.block_range_update().map(|update| {
BlockRangeInfo::new(update.earliest, update.latest, update.latest_hash)
});
let session = ActiveSession {
next_id: 0,
@@ -579,7 +588,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
internal_request_timeout: Arc::clone(&timeout),
protocol_breach_request_timeout: self.protocol_breach_request_timeout,
terminate_message: None,
range_info: None,
range_info: remote_range_info.clone(),
local_range_info: self.local_range_info.clone(),
range_update_interval,
last_sent_latest_block: None,
@@ -619,7 +628,7 @@ impl<N: NetworkPrimitives> SessionManager<N> {
messages,
direction,
timeout,
range_info: None,
range_info: remote_range_info,
})
}
PendingSessionEvent::Disconnected { remote_addr, session_id, direction, error } => {
@@ -887,6 +896,7 @@ pub(crate) async fn pending_session_with_timeout<F, N: NetworkPrimitives>(
#[expect(clippy::too_many_arguments)]
pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
disconnect_rx: oneshot::Receiver<()>,
session_id: SessionId,
stream: TcpStream,
@@ -900,6 +910,7 @@ pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
) {
authenticate(
handshake,
eth_max_message_size,
disconnect_rx,
events,
stream,
@@ -920,6 +931,7 @@ pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
#[expect(clippy::too_many_arguments)]
async fn start_pending_outbound_session<N: NetworkPrimitives>(
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
disconnect_rx: oneshot::Receiver<()>,
events: mpsc::Sender<PendingSessionEvent<N>>,
session_id: SessionId,
@@ -952,6 +964,7 @@ async fn start_pending_outbound_session<N: NetworkPrimitives>(
};
authenticate(
handshake,
eth_max_message_size,
disconnect_rx,
events,
stream,
@@ -971,6 +984,7 @@ async fn start_pending_outbound_session<N: NetworkPrimitives>(
#[expect(clippy::too_many_arguments)]
async fn authenticate<N: NetworkPrimitives>(
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
disconnect_rx: oneshot::Receiver<()>,
events: mpsc::Sender<PendingSessionEvent<N>>,
stream: TcpStream,
@@ -1003,6 +1017,7 @@ async fn authenticate<N: NetworkPrimitives>(
let auth = authenticate_stream(
handshake,
eth_max_message_size,
unauthed,
session_id,
remote_addr,
@@ -1056,6 +1071,7 @@ async fn get_ecies_stream<Io: AsyncRead + AsyncWrite + Unpin>(
#[expect(clippy::too_many_arguments)]
async fn authenticate_stream<N: NetworkPrimitives>(
handshake: Arc<dyn EthRlpxHandshake>,
eth_max_message_size: usize,
stream: UnauthedP2PStream<ECIESStream<TcpStream>>,
session_id: SessionId,
remote_addr: SocketAddr,
@@ -1134,7 +1150,8 @@ async fn authenticate_stream<N: NetworkPrimitives>(
.await
{
Ok(their_status) => {
let eth_stream = EthStream::new(eth_version, p2p_stream);
let eth_stream =
EthStream::with_max_message_size(eth_version, p2p_stream, eth_max_message_size);
(eth_stream.into(), their_status)
}
Err(err) => {
@@ -1163,7 +1180,7 @@ async fn authenticate_stream<N: NetworkPrimitives>(
}
let (multiplex_stream, their_status) = match multiplex_stream
.into_eth_satellite_stream(status, fork_filter, handshake)
.into_eth_satellite_stream(status, fork_filter, handshake, eth_max_message_size)
.await
{
Ok((multiplex_stream, their_status)) => (multiplex_stream, their_status),

View File

@@ -308,8 +308,13 @@ impl<N: NetworkPrimitives> NetworkState<N> {
}
/// Adds a peer and its address with the given kind to the peerset.
pub(crate) fn add_peer_kind(&mut self, peer_id: PeerId, kind: PeerKind, addr: PeerAddr) {
self.peers_manager.add_peer_kind(peer_id, Some(kind), addr, None)
pub(crate) fn add_peer_kind(
&mut self,
peer_id: PeerId,
kind: Option<PeerKind>,
addr: PeerAddr,
) {
self.peers_manager.add_peer_kind(peer_id, kind, addr, None)
}
/// Connects a peer and its address with the given kind

View File

@@ -195,7 +195,7 @@ impl LaunchContext {
should_save = true;
}
} else if !reth_config.prune.is_default() {
warn!(target: "reth::cli", "Pruning configuration is present in the config file, but no CLI arguments are provided. Using config from file.");
info!(target: "reth::cli", "Pruning configuration is present in the config file, but no CLI arguments are provided. Using config from file.");
}
if should_save {

View File

@@ -41,7 +41,7 @@ reth-engine-primitives.workspace = true
# ethereum
alloy-primitives.workspace = true
alloy-rpc-types-engine = { workspace = true, features = ["std", "jwt"] }
alloy-rpc-types-engine = { workspace = true, features = ["std", "jwt-aws-lc-rs"] }
alloy-consensus.workspace = true
alloy-eips.workspace = true

View File

@@ -4,6 +4,7 @@ use alloy_eips::BlockNumHash;
use alloy_primitives::B256;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
num::NonZeroUsize,
ops::Not,
path::PathBuf,
sync::OnceLock,
@@ -81,6 +82,8 @@ pub struct DefaultNetworkArgs {
pub tx_ingress_policy: TransactionIngressPolicy,
/// Default transaction propagation mode.
pub propagation_mode: TransactionPropagationMode,
/// Default enforce ENR fork ID setting.
pub enforce_enr_fork_id: bool,
}
impl DefaultNetworkArgs {
@@ -183,6 +186,12 @@ impl DefaultNetworkArgs {
self.propagation_mode = v;
self
}
/// Set the default enforce ENR fork ID setting.
pub const fn with_enforce_enr_fork_id(mut self, v: bool) -> Self {
self.enforce_enr_fork_id = v;
self
}
}
impl Default for DefaultNetworkArgs {
@@ -204,6 +213,7 @@ impl Default for DefaultNetworkArgs {
tx_propagation_policy: TransactionPropagationKind::default(),
tx_ingress_policy: TransactionIngressPolicy::default(),
propagation_mode: TransactionPropagationMode::Sqrt,
enforce_enr_fork_id: false,
}
}
}
@@ -384,6 +394,10 @@ pub struct NetworkArgs {
#[arg(long)]
pub network_id: Option<u64>,
/// Maximum allowed ETH message size in bytes. Default is 10 MiB.
#[arg(long = "eth-max-message-size", value_name = "BYTES")]
pub eth_max_message_size: Option<NonZeroUsize>,
/// Restrict network communication to the given IP networks (CIDR masks).
///
/// Comma separated list of CIDR network specifications.
@@ -398,7 +412,7 @@ pub struct NetworkArgs {
/// When enabled, peers discovered without a confirmed fork ID are not added to the peer set
/// until their fork ID is verified via EIP-868 ENR request. This filters out peers from other
/// networks that pollute the discovery table.
#[arg(long)]
#[arg(long, default_value_t = DefaultNetworkArgs::get_global().enforce_enr_fork_id)]
pub enforce_enr_fork_id: bool,
}
@@ -544,6 +558,7 @@ impl NetworkArgs {
))
.disable_tx_gossip(self.disable_tx_gossip)
.required_block_hashes(self.required_block_hashes.clone())
.eth_max_message_size_opt(self.eth_max_message_size.map(NonZeroUsize::get))
.network_id(self.network_id)
}
@@ -648,6 +663,7 @@ impl Default for NetworkArgs {
tx_propagation_policy,
tx_ingress_policy,
propagation_mode,
enforce_enr_fork_id,
} = DefaultNetworkArgs::get_global().clone();
Self {
discovery: DiscoveryArgs::default(),
@@ -680,8 +696,9 @@ impl Default for NetworkArgs {
propagation_mode,
required_block_hashes: vec![],
network_id: None,
eth_max_message_size: None,
netrestrict: None,
enforce_enr_fork_id: false,
enforce_enr_fork_id,
}
}
}
@@ -1095,6 +1112,37 @@ mod tests {
assert_eq!(args, default_args);
}
#[test]
fn parse_eth_max_message_size() {
let args = CommandParser::<NetworkArgs>::parse_from([
"reth",
"--eth-max-message-size",
"15728640",
])
.args;
assert_eq!(args.eth_max_message_size, Some(NonZeroUsize::new(15 * 1024 * 1024).unwrap()));
}
#[test]
fn parse_eth_max_message_size_zero_rejected() {
let result =
CommandParser::<NetworkArgs>::try_parse_from(["reth", "--eth-max-message-size", "0"]);
assert!(result.is_err());
}
#[test]
fn parse_eth_max_message_size_above_rlpx_cap() {
let result = CommandParser::<NetworkArgs>::try_parse_from([
"reth",
"--eth-max-message-size",
"16777216",
]);
assert!(result.is_ok());
let args = result.unwrap().args;
assert_eq!(args.eth_max_message_size, Some(NonZeroUsize::new(16 * 1024 * 1024).unwrap()));
}
#[test]
fn parse_required_block_hashes() {
let args = CommandParser::<NetworkArgs>::parse_from([

View File

@@ -79,6 +79,13 @@ where
Self::Right(r) => r.withdrawals(),
}
}
fn slot_number(&self) -> Option<u64> {
match self {
Self::Left(l) => l.slot_number(),
Self::Right(r) => r.slot_number(),
}
}
}
/// this structure enables the chaining of multiple `PayloadBuilder` implementations,

View File

@@ -72,7 +72,7 @@
//! },
//! ..Default::default()
//! };
//! let payload = EthBuiltPayload::new(Arc::new(SealedBlock::seal_slow(block)), U256::ZERO, None);
//! let payload = EthBuiltPayload::new(Arc::new(SealedBlock::seal_slow(block)), U256::ZERO, None, None);
//! Ok(payload)
//! }
//!

View File

@@ -89,6 +89,7 @@ impl PayloadJob for TestPayloadJob {
Arc::new(Block::<_>::default().seal_slow()),
U256::ZERO,
Some(Default::default()),
None,
))
}

View File

@@ -126,6 +126,28 @@ pub enum VersionSpecificValidationError {
/// root after Cancun
#[error("no parent beacon block root post-cancun")]
NoParentBeaconBlockRootPostCancun,
/// Thrown if the pre-V6 `PayloadAttributes` or `ExecutionPayload` contains a block access list
#[error("block access list not before V6")]
BlockAccessListNotSupportedBeforeV6,
/// Thrown if `engine_newPayload` contains no block access list
/// after Amsterdam
#[error("no block access list post-Amsterdam")]
NoBlockAccessListPostAmsterdam,
/// Thrown if `engine_newPayload` contains block access list
/// before Amsterdam
#[error("block access list pre-Amsterdam")]
HasBlockAccessListPreAmsterdam,
/// Thrown if the pre-V6 `PayloadAttributes` or `ExecutionPayload` contains a slot number
#[error("slot number not before V6")]
SlotNumberNotSupportedBeforeV6,
/// Thrown if `engine_newPayload` contains no slot number
/// after Amsterdam
#[error("no slot number post-Amsterdam")]
NoSlotNumberPostAmsterdam,
/// Thrown if `engine_newPayload` contains slot number
/// before Amsterdam
#[error("slot number pre-Amsterdam")]
HasSlotNumberPreAmsterdam,
}
/// Error validating payload received over `newPayload` API.

View File

@@ -156,6 +156,97 @@ pub fn validate_payload_timestamp(
return Err(EngineObjectValidationError::UnsupportedFork)
}
let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
if version.is_v6() && !is_amsterdam {
// From the Engine API spec:
// <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
//
// For `engine_getPayloadV6`
//
// 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
// built payload does not fall within the time frame of the Amsterdam fork.
return Err(EngineObjectValidationError::UnsupportedFork)
}
Ok(())
}
/// Validates the presence of the `block access lists` field according to the payload timestamp.
/// After Amsterdam, block access list field must be [Some].
/// Before Amsterdam, block access list field must be [None];
pub fn validate_block_access_list_presence<T: EthereumHardforks>(
chain_spec: &T,
version: EngineApiMessageVersion,
message_validation_kind: MessageValidationKind,
timestamp: u64,
has_block_access_list: bool,
) -> Result<(), EngineObjectValidationError> {
let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
match version {
EngineApiMessageVersion::V1 |
EngineApiMessageVersion::V2 |
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 => {
if has_block_access_list {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::BlockAccessListNotSupportedBeforeV6))
}
}
EngineApiMessageVersion::V6 => {
if is_amsterdam_active && !has_block_access_list {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
}
if !is_amsterdam_active && has_block_access_list {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
}
}
};
Ok(())
}
/// Validates the presence of the `slot number` field according to the payload timestamp.
/// After Amsterdam, slot number field must be [Some].
/// Before Amsterdam, slot number field must be [None];
pub fn validate_slot_number_presence<T: EthereumHardforks>(
chain_spec: &T,
version: EngineApiMessageVersion,
message_validation_kind: MessageValidationKind,
timestamp: u64,
has_slot_number: bool,
) -> Result<(), EngineObjectValidationError> {
let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
match version {
EngineApiMessageVersion::V1 |
EngineApiMessageVersion::V2 |
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 => {
if has_slot_number {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::SlotNumberNotSupportedBeforeV6))
}
}
EngineApiMessageVersion::V6 => {
if is_amsterdam_active && !has_slot_number {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
}
if !is_amsterdam_active && has_slot_number {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
}
}
};
Ok(())
}
@@ -181,7 +272,8 @@ pub fn validate_withdrawals_presence<T: EthereumHardforks>(
EngineApiMessageVersion::V2 |
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 => {
EngineApiMessageVersion::V5 |
EngineApiMessageVersion::V6 => {
if is_shanghai_active && !has_withdrawals {
return Err(message_validation_kind
.to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
@@ -282,7 +374,10 @@ pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
))
}
}
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 |
EngineApiMessageVersion::V6 => {
if !has_parent_beacon_block_root {
return Err(validation_kind
.to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
@@ -355,6 +450,25 @@ where
Type: PayloadAttributes,
T: EthereumHardforks,
{
// BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
validate_block_access_list_presence(
chain_spec,
version,
payload_or_attrs.message_validation_kind(),
payload_or_attrs.timestamp(),
payload_or_attrs.block_access_list().is_some(),
)?;
}
validate_slot_number_presence(
chain_spec,
version,
payload_or_attrs.message_validation_kind(),
payload_or_attrs.timestamp(),
payload_or_attrs.slot_number().is_some(),
)?;
validate_withdrawals_presence(
chain_spec,
version,
@@ -393,6 +507,10 @@ pub enum EngineApiMessageVersion {
///
/// Added in the Osaka hardfork.
V5 = 5,
/// Version 6
///
/// Added in the Amsterdam hardfork.
V6 = 6,
}
impl EngineApiMessageVersion {
@@ -421,6 +539,11 @@ impl EngineApiMessageVersion {
matches!(self, Self::V5)
}
/// Returns true if the version is V6.
pub const fn is_v6(&self) -> bool {
matches!(self, Self::V6)
}
/// Returns the method name for the given version.
pub const fn method_name(&self) -> &'static str {
match self {
@@ -428,7 +551,7 @@ impl EngineApiMessageVersion {
Self::V2 => "engine_newPayloadV2",
Self::V3 => "engine_newPayloadV3",
Self::V4 => "engine_newPayloadV4",
Self::V5 => "engine_newPayloadV5",
Self::V5 | Self::V6 => "engine_newPayloadV5",
}
}
}

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