mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
216 Commits
dan/static
...
bench-otlp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0e20422c | ||
|
|
c0f6997352 | ||
|
|
6a62c38498 | ||
|
|
294e215077 | ||
|
|
1589f0f684 | ||
|
|
563399c696 | ||
|
|
ea4d354105 | ||
|
|
6c908ca28f | ||
|
|
093621ffa7 | ||
|
|
451a20f0f5 | ||
|
|
a12b91937e | ||
|
|
7f12e7aaf8 | ||
|
|
01564a8f7a | ||
|
|
a12a32efff | ||
|
|
ec59698ef6 | ||
|
|
9f69a689b9 | ||
|
|
4527725c90 | ||
|
|
c57ecb937b | ||
|
|
ea12781417 | ||
|
|
adfa36e05a | ||
|
|
592c65be82 | ||
|
|
074daf8a8f | ||
|
|
bb55687f98 | ||
|
|
460d522443 | ||
|
|
a73f510766 | ||
|
|
fddf94c166 | ||
|
|
ddc3ecaca6 | ||
|
|
94d34450a6 | ||
|
|
df806b8c10 | ||
|
|
f624225185 | ||
|
|
9d0eab9560 | ||
|
|
e63ebac380 | ||
|
|
1a6ba945a0 | ||
|
|
999fa0676c | ||
|
|
d6b1d06772 | ||
|
|
cf2c24c072 | ||
|
|
406b95b555 | ||
|
|
e406928667 | ||
|
|
01bd1cc5fa | ||
|
|
792c8f2558 | ||
|
|
71cac26187 | ||
|
|
7def9f262a | ||
|
|
5ea37acbdb | ||
|
|
aa1cea6a5d | ||
|
|
f238a288c6 | ||
|
|
2580304b41 | ||
|
|
9e3950dbd9 | ||
|
|
e88e8e70bf | ||
|
|
73bd474600 | ||
|
|
be779c90a2 | ||
|
|
98fa44d99e | ||
|
|
8e89ec7685 | ||
|
|
0db52b60c0 | ||
|
|
20d53d039e | ||
|
|
9ed3b131b2 | ||
|
|
07c3467778 | ||
|
|
12a3022a2a | ||
|
|
84c85ccef6 | ||
|
|
851f32a4d3 | ||
|
|
3f81e1894c | ||
|
|
085592dedf | ||
|
|
e28dd31a7e | ||
|
|
151f92d43a | ||
|
|
c1ae2af8ca | ||
|
|
cdeba79590 | ||
|
|
29fbbadc50 | ||
|
|
c5107fe23c | ||
|
|
35e6059924 | ||
|
|
09859a2621 | ||
|
|
0aa77e8d90 | ||
|
|
72190e272b | ||
|
|
6b587560fa | ||
|
|
d41589a578 | ||
|
|
8966350c24 | ||
|
|
4a2456c908 | ||
|
|
99aea38920 | ||
|
|
0da679f87c | ||
|
|
6ca9856ce9 | ||
|
|
0b69f6ad7b | ||
|
|
37709c5a99 | ||
|
|
e6e637a265 | ||
|
|
b3cfe87795 | ||
|
|
7402820d62 | ||
|
|
cda19b07d6 | ||
|
|
6149ac6c0e | ||
|
|
e4b553563b | ||
|
|
2f4a128112 | ||
|
|
cd480190e9 | ||
|
|
9b1fcd9945 | ||
|
|
39b9c8ae4b | ||
|
|
c4bd3f145c | ||
|
|
909157859a | ||
|
|
ea47f1553c | ||
|
|
bb12b72e70 | ||
|
|
3a1872411b | ||
|
|
71c0015862 | ||
|
|
7c51bc934c | ||
|
|
823fbef1c7 | ||
|
|
d7b5c5e498 | ||
|
|
2c46aad8e5 | ||
|
|
e15a92a22b | ||
|
|
704292b3d5 | ||
|
|
31fa93889e | ||
|
|
d8de8afa95 | ||
|
|
26f4aab2a9 | ||
|
|
016c445dfa | ||
|
|
ae6edbd333 | ||
|
|
fc4d88bf99 | ||
|
|
22642baf5b | ||
|
|
76e139fb84 | ||
|
|
fcf6645242 | ||
|
|
f1272429db | ||
|
|
ad96bc4649 | ||
|
|
3e4da0881d | ||
|
|
9077faf595 | ||
|
|
68576b6edd | ||
|
|
d6a1fa65d0 | ||
|
|
0c219fe5bd | ||
|
|
b73ecdf4c1 | ||
|
|
f9f577be0d | ||
|
|
c2b0f2d1e2 | ||
|
|
02816ce06f | ||
|
|
c572a3559e | ||
|
|
a6f3abf483 | ||
|
|
8402a24a6a | ||
|
|
7834fdd70b | ||
|
|
218a869893 | ||
|
|
cc30b1e6cc | ||
|
|
eaa39eb99a | ||
|
|
a5d8fa3ae1 | ||
|
|
183c851804 | ||
|
|
66dadf0da3 | ||
|
|
f756673f3a | ||
|
|
fcf86b3f8b | ||
|
|
b2eb061fe2 | ||
|
|
1b09bf5a22 | ||
|
|
9de19783c2 | ||
|
|
757d9c1c92 | ||
|
|
2d27a96d9a | ||
|
|
fa4113eb1e | ||
|
|
91182f6535 | ||
|
|
6366201f16 | ||
|
|
c3227219a3 | ||
|
|
9a5d1a77d4 | ||
|
|
0e14f1a8a3 | ||
|
|
a684714f40 | ||
|
|
4363cc9237 | ||
|
|
87f26ce4b9 | ||
|
|
83620dae57 | ||
|
|
35fc3b684f | ||
|
|
75ca930237 | ||
|
|
514b2898aa | ||
|
|
d6af5793e5 | ||
|
|
01f3e58229 | ||
|
|
78c6c9c10f | ||
|
|
039c61e93f | ||
|
|
b545252285 | ||
|
|
6f7c8ad2c9 | ||
|
|
1204674e1a | ||
|
|
764246d5ea | ||
|
|
5356c0480e | ||
|
|
79e52ad2e0 | ||
|
|
c52ff7045c | ||
|
|
ec6e3032f0 | ||
|
|
cea62ade29 | ||
|
|
a66e38c08c | ||
|
|
843b5f3c3c | ||
|
|
c45ccc3e38 | ||
|
|
a6d6a21524 | ||
|
|
f1ed523b20 | ||
|
|
dc39df5746 | ||
|
|
c574a3f7b7 | ||
|
|
7bb5c579e0 | ||
|
|
614a68532b | ||
|
|
648a2b8cf1 | ||
|
|
9cfa8a9566 | ||
|
|
a1c1885fe2 | ||
|
|
dca5852213 | ||
|
|
c94b728af1 | ||
|
|
868ac9d77b | ||
|
|
1e2e33e951 | ||
|
|
598f228e21 | ||
|
|
996121f0a5 | ||
|
|
e7da50a502 | ||
|
|
3020540066 | ||
|
|
f82d143d0c | ||
|
|
bebc532e0e | ||
|
|
0df9791bea | ||
|
|
09adb83922 | ||
|
|
c12b6d4c90 | ||
|
|
7a78044587 | ||
|
|
f88538e033 | ||
|
|
63dff64b8a | ||
|
|
233590cefd | ||
|
|
40962ef6fc | ||
|
|
2f121b099b | ||
|
|
0470050c05 | ||
|
|
cbc416b82a | ||
|
|
3fddefbd38 | ||
|
|
f97a6530c1 | ||
|
|
80e3e1c79d | ||
|
|
ee37c25a4b | ||
|
|
c01f9688e2 | ||
|
|
815a75833e | ||
|
|
59c4e24296 | ||
|
|
d5b5caa439 | ||
|
|
47f1999654 | ||
|
|
3ac5637bd1 | ||
|
|
4cec99ed13 | ||
|
|
2f73835483 | ||
|
|
ed20a40649 | ||
|
|
080a9cfc10 | ||
|
|
c4cd5c9b7b | ||
|
|
ce2a194fb7 | ||
|
|
6dcab51c97 | ||
|
|
4db23809cc |
5
.changelog/cool-owls-rest.md
Normal file
5
.changelog/cool-owls-rest.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie-sparse: patch
|
||||
---
|
||||
|
||||
Fixed a bug in `merge_subtrie_updates` where source insertions did not cancel destination removals (and vice versa), causing inconsistent trie updates accumulated across multiple `root()` calls without intermediate `take_updates()`. Added a test covering the cross-cancellation behavior.
|
||||
5
.changelog/dark-ants-write.md
Normal file
5
.changelog/dark-ants-write.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-tasks: patch
|
||||
---
|
||||
|
||||
Added panic handler to all rayon thread pools that logs panics via `tracing::error` instead of aborting the process.
|
||||
5
.changelog/dry-koalas-wink.md
Normal file
5
.changelog/dry-koalas-wink.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-network-types: patch
|
||||
---
|
||||
|
||||
Increased default maximum concurrent outbound dials from 15 to 30.
|
||||
5
.changelog/fair-wolves-smile.md
Normal file
5
.changelog/fair-wolves-smile.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-payload-builder: minor
|
||||
---
|
||||
|
||||
Added observability metrics for payload resolve latency and new payload job creation latency to the payload builder service.
|
||||
10
.changelog/fine-koalas-shout.md
Normal file
10
.changelog/fine-koalas-shout.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
reth-chain-state: minor
|
||||
reth-engine-primitives: minor
|
||||
reth-engine-tree: minor
|
||||
reth-node-core: minor
|
||||
reth-node-events: minor
|
||||
reth: patch
|
||||
---
|
||||
|
||||
Added configurable slow block logging (`--engine.slow-block-threshold`) that emits a structured `warn!` log with detailed timing, state-operation counts, and cache hit-rate metrics for blocks whose total processing time exceeds the threshold. Introduced `ExecutionTimingStats`, `CacheStats`, `StateProviderStats`, and `SlowBlockInfo` types to carry execution statistics from block validation through persistence, and refactored `PersistenceResult` to carry commit duration alongside the last persisted block.
|
||||
6
.changelog/icy-tigers-buzz.md
Normal file
6
.changelog/icy-tigers-buzz.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-trie-db: minor
|
||||
reth-engine-tree: minor
|
||||
---
|
||||
|
||||
Added `PendingChangeset` and `PendingChangesetGuard` to `ChangesetCache` so concurrent readers wait for an in-progress computation instead of falling back to the expensive DB-based path. The guard automatically cancels the pending entry on drop (e.g. task panic), ensuring waiters always make progress.
|
||||
8
.changelog/merry-bees-break.md
Normal file
8
.changelog/merry-bees-break.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
reth-engine-primitives: minor
|
||||
reth-engine-tree: minor
|
||||
reth-node-core: minor
|
||||
reth-trie-parallel: minor
|
||||
---
|
||||
|
||||
Added `--engine.proof-jitter` CLI option behind the `trie-debug` feature flag. When set, each proof worker sleeps for a random duration up to the specified value before starting proof computation, useful for stress-testing timing-sensitive proof logic.
|
||||
5
.changelog/mild-foxes-cook.md
Normal file
5
.changelog/mild-foxes-cook.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-rpc-convert: patch
|
||||
---
|
||||
|
||||
Updated `alloy-evm` dependency to git revision `9bc2dba` and adapted `TxEnvConverter` impl to the updated `TryIntoTxEnv` trait signature that now includes a `Spec` generic parameter.
|
||||
6
.changelog/neat-ducks-whisper.md
Normal file
6
.changelog/neat-ducks-whisper.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-trie: patch
|
||||
reth-trie-sparse: patch
|
||||
---
|
||||
|
||||
Refactored test harness for sparse trie tests by extracting `TrieTestHarness` into a shared `reth-trie` test utility, replacing duplicated inline harness code across multiple test modules. Updated `proof_v2` return type to include an optional root hash, and converted `original_root` and `storage` from public fields to accessor methods.
|
||||
5
.changelog/nice-bears-cook.md
Normal file
5
.changelog/nice-bears-cook.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie: patch
|
||||
---
|
||||
|
||||
Removed the local `increment_and_strip_trailing_zeros` function and `PATH_ALL_ZEROS` static in `proof_v2`, replacing them with the equivalent `Nibbles::next_without_prefix` and `Nibbles::is_zeroes` builtins. Also replaced manual `.get()` calls on `state_mask`/`hash_mask` with direct field access and switched to `Nibbles::unpack_array` over the unsafe `unpack_unchecked`.
|
||||
7
.changelog/odd-frogs-break.md
Normal file
7
.changelog/odd-frogs-break.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
reth-cli-commands: minor
|
||||
reth-node-core: minor
|
||||
reth: patch
|
||||
---
|
||||
|
||||
Made v2 storage the default for all new databases, deprecating the `--storage.v2` flag to a hidden no-op kept for backwards compatibility. Updated CLI reference docs to remove the now-hidden flag from all command help pages.
|
||||
7
.changelog/proud-slugs-fly.md
Normal file
7
.changelog/proud-slugs-fly.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
reth-engine-tree: patch
|
||||
reth-trie-sparse: patch
|
||||
reth-tasks: patch
|
||||
---
|
||||
|
||||
Offloaded deallocation of expensive proof node buffers to a persistent background thread (`Runtime::spawn_drop`) to avoid blocking state root computation or lock-holding code.
|
||||
6
.changelog/rare-tigers-nod.md
Normal file
6
.changelog/rare-tigers-nod.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-trie-sparse: patch
|
||||
reth-engine-tree: patch
|
||||
---
|
||||
|
||||
Removed the `skip_proof_node_filtering` flag, `revealed_account_paths`/`revealed_paths` tracking, and the `filter_revealed_v2_proof_nodes` function from the sparse trie implementation. Also removed the corresponding skipped-nodes metrics, simplifying the proof node reveal path to always pass nodes directly to the sparse trie without pre-filtering.
|
||||
5
.changelog/rare-whales-pack.md
Normal file
5
.changelog/rare-whales-pack.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie-sparse: minor
|
||||
---
|
||||
|
||||
Added a comprehensive generic `SparseTrie` test suite covering `set_root`, `reveal_nodes`, `update_leaves`, `root`, `take_updates`, `commit_updates`, `prune`, `wipe`/`clear`, `get_leaf_value`, `find_leaf`, `size_hint`, and integration lifecycle scenarios. Tests are stamped out for all concrete `SparseTrie` implementations via a macro.
|
||||
5
.changelog/tall-fish-climb.md
Normal file
5
.changelog/tall-fish-climb.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-cli-commands: minor
|
||||
---
|
||||
|
||||
Added `reth_version` field to `SnapshotManifest` to record the Reth version that produced a snapshot. The field is optional and populated automatically during manifest generation.
|
||||
8
.changelog/vain-bats-bark.md
Normal file
8
.changelog/vain-bats-bark.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
reth-trie-common: minor
|
||||
reth-trie: minor
|
||||
reth-trie-parallel: minor
|
||||
reth-engine-tree: patch
|
||||
---
|
||||
|
||||
Moved `ProofV2Target`, `MultiProofTargetsV2`, and `ChunkedMultiProofTargetsV2` from `reth-trie-parallel::targets_v2` into a new `reth-trie-common::target_v2` module, making these types available at a lower level without pulling in the full parallel trie crate. Added a `multiproof_v2` method to `Proof` in `reth-trie` that generates a state multiproof using the V2 proof calculator with synchronous account value encoding.
|
||||
14
.changelog/vain-birds-laugh.md
Normal file
14
.changelog/vain-birds-laugh.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
reth-engine-tree: patch
|
||||
reth-evm-ethereum: patch
|
||||
reth-evm: patch
|
||||
reth-primitives-traits: patch
|
||||
reth-revm: patch
|
||||
reth-rpc-eth-api: patch
|
||||
reth-rpc-eth-types: patch
|
||||
reth-provider: patch
|
||||
example-custom-evm: patch
|
||||
example-precompile-cache: patch
|
||||
---
|
||||
|
||||
Bumped revm to v35.0.0, revm-inspectors to 0.35.0, and alloy-evm to 0.29.0. Updated call sites throughout the codebase to align with the new APIs, including `ExecutionResult` field changes (`gas_used` → `gas.used()`/`gas.final_refunded()`), removal of `.without_state_clear()`, updated `EthPrecompiles::new(spec)` constructor, and updated `block_hashes.lowest()` access.
|
||||
9
.changelog/vain-goats-cook.md
Normal file
9
.changelog/vain-goats-cook.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
reth-trie-sparse: minor
|
||||
reth-engine-primitives: minor
|
||||
reth-engine-tree: minor
|
||||
reth-node-core: minor
|
||||
reth-trie-common: patch
|
||||
---
|
||||
|
||||
Added an arena-based sparse trie implementation (`ArenaParallelSparseTrie`) using `slotmap` arena allocation for node storage, enabling parallel subtrie mutation without per-node hashing overhead. Added `ConfigurableSparseTrie` enum to switch between the arena and hash-map implementations, and a `--engine.enable-arena-sparse-trie` CLI flag to opt in at runtime.
|
||||
7
.changelog/young-horses-play.md
Normal file
7
.changelog/young-horses-play.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
reth-trie: major
|
||||
reth-trie-db: major
|
||||
reth-provider: minor
|
||||
---
|
||||
|
||||
Added `MaskedTrieCursorFactory` and `MaskedTrieCursor` to handle prefix-set-based hash invalidation at the cursor layer, replacing the `DatabaseTrieWitness` trait abstraction. Removed `with_prefix_sets_mut` from `TrieWitness` and deleted `DatabaseTrieWitness` — callers should now wrap their cursor factory with `MaskedTrieCursorFactory` to apply prefix sets during witness/proof computation.
|
||||
5
.changelog/zesty-clouds-wave.md
Normal file
5
.changelog/zesty-clouds-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie: patch
|
||||
---
|
||||
|
||||
Fixed a potential panic in `ProofCalculator` by clearing internal computation state (`branch_stack`, `child_stack`, `branch_path`, etc.) after errors, preventing stale state from causing `usize` underflow panics when the calculator is reused. Added a test verifying correct behavior after simulated mid-computation errors.
|
||||
@@ -20,6 +20,11 @@
|
||||
# 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-*
|
||||
|
||||
|
||||
276
.github/scripts/bench-metrics-proxy.py
vendored
Normal file
276
.github/scripts/bench-metrics-proxy.py
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Prometheus metrics proxy that fetches from a local reth node and
|
||||
re-exposes with additional benchmark labels.
|
||||
|
||||
Reads labels from a JSON file (updated by local-reth-bench.sh between runs)
|
||||
and injects them into every Prometheus metric line.
|
||||
|
||||
Returns empty 200 when reth is not running (clean Grafana gaps).
|
||||
"""
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
|
||||
def read_labels(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def inject_labels(metrics_bytes, label_str, label_names):
|
||||
"""Inject labels into Prometheus text format.
|
||||
|
||||
Operates on bytes and uses simple string ops instead of regex
|
||||
for speed on large payloads (reth exposes thousands of metrics).
|
||||
|
||||
Skips injecting into lines that already contain any of the label names
|
||||
to avoid duplicate labels (which Prometheus rejects).
|
||||
"""
|
||||
if not label_str:
|
||||
return metrics_bytes
|
||||
|
||||
label_bytes = label_str.encode("utf-8")
|
||||
# Pre-encode label names for fast duplicate detection
|
||||
label_name_bytes = [n.encode("utf-8") for n in label_names]
|
||||
out = []
|
||||
for line in metrics_bytes.split(b"\n"):
|
||||
# Skip comments and blank lines
|
||||
if line.startswith(b"#") or not line:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
brace = line.find(b"{")
|
||||
space = line.find(b" ")
|
||||
|
||||
if space == -1:
|
||||
# Malformed, pass through
|
||||
out.append(line)
|
||||
elif brace != -1 and brace < space:
|
||||
# Has labels: metric{existing="val"} 123
|
||||
close = line.find(b"}", brace)
|
||||
if close == -1:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
# Filter out labels that already exist in this line
|
||||
existing = line[brace + 1:close]
|
||||
inject = label_bytes
|
||||
if existing:
|
||||
for name in label_name_bytes:
|
||||
if name + b"=" in existing:
|
||||
# Rebuild inject string excluding this label
|
||||
inject = _remove_label(inject, name)
|
||||
if not inject:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
if close == brace + 1:
|
||||
# Empty braces: metric{} 123
|
||||
out.append(line[:close] + inject + line[close:])
|
||||
else:
|
||||
out.append(line[:close] + b"," + inject + line[close:])
|
||||
else:
|
||||
# No labels: metric 123
|
||||
out.append(line[:space] + b"{" + label_bytes + b"}" + line[space:])
|
||||
|
||||
return b"\n".join(out)
|
||||
|
||||
|
||||
def _remove_label(label_bytes, name):
|
||||
"""Remove a single label (name=\"...\") from a comma-separated label string."""
|
||||
parts = []
|
||||
for part in label_bytes.split(b","):
|
||||
if not part.startswith(name + b"="):
|
||||
parts.append(part)
|
||||
return b",".join(parts)
|
||||
|
||||
|
||||
def build_label_str(labels):
|
||||
"""Pre-format the label injection string: key1="val1",key2="val2" """
|
||||
if not labels:
|
||||
return ""
|
||||
return ",".join(f'{k}="{v}"' for k, v in sorted(labels.items()))
|
||||
|
||||
|
||||
def build_elapsed_gauge(labels):
|
||||
"""Build a bench_elapsed_seconds gauge from run_start_epoch in labels."""
|
||||
start = labels.get("run_start_epoch")
|
||||
if not start:
|
||||
return b""
|
||||
try:
|
||||
elapsed = time.time() - float(start)
|
||||
except (ValueError, TypeError):
|
||||
return b""
|
||||
# Build labels excluding internal keys
|
||||
display = {k: v for k, v in labels.items()
|
||||
if k not in ("run_start_epoch", "reference_epoch")}
|
||||
lstr = build_label_str(display)
|
||||
return (
|
||||
f"# HELP bench_elapsed_seconds Seconds since benchmark run started\n"
|
||||
f"# TYPE bench_elapsed_seconds gauge\n"
|
||||
f"bench_elapsed_seconds{{{lstr}}} {elapsed:.1f}\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def compute_timestamp_ms(labels):
|
||||
"""Compute a synthetic timestamp so all runs share a common time origin.
|
||||
|
||||
Returns the timestamp in milliseconds, or None if not enough info.
|
||||
Uses: reference_epoch + (now - run_start_epoch) → all runs overlay at
|
||||
the same Grafana time range.
|
||||
"""
|
||||
ref = labels.get("reference_epoch")
|
||||
start = labels.get("run_start_epoch")
|
||||
if not ref or not start:
|
||||
return None
|
||||
try:
|
||||
elapsed = time.time() - float(start)
|
||||
return int((float(ref) + elapsed) * 1000)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def inject_timestamps(metrics_bytes, timestamp_ms):
|
||||
"""Append a Prometheus timestamp (ms) to every data line.
|
||||
|
||||
Prometheus text format: metric{labels} value [timestamp_ms]
|
||||
Adding timestamps causes Prometheus to store all runs' samples
|
||||
at the same relative time, enabling natural overlay in Grafana.
|
||||
"""
|
||||
if timestamp_ms is None:
|
||||
return metrics_bytes
|
||||
|
||||
ts = str(timestamp_ms).encode("utf-8")
|
||||
out = []
|
||||
for line in metrics_bytes.split(b"\n"):
|
||||
if line.startswith(b"#") or not line:
|
||||
out.append(line)
|
||||
else:
|
||||
out.append(line + b" " + ts)
|
||||
return b"\n".join(out)
|
||||
|
||||
|
||||
class MetricsHandler(BaseHTTPRequestHandler):
|
||||
# Use HTTP/1.1 so Content-Length is respected and Prometheus
|
||||
# doesn't have to rely on connection close to detect end of body.
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self):
|
||||
src = self.client_address[0]
|
||||
try:
|
||||
resp = urlopen(self.server.upstream, timeout=2)
|
||||
metrics = resp.read()
|
||||
except (URLError, ConnectionError, OSError):
|
||||
# reth not running — return empty 200
|
||||
self._send(b"")
|
||||
#print(f" scrape from {src}: empty (reth not running)", flush=True)
|
||||
return
|
||||
|
||||
all_labels = read_labels(self.server.labels_file)
|
||||
# Internal keys — not injected as Prometheus labels
|
||||
internal = ("run_start_epoch", "reference_epoch")
|
||||
labels = {k: v for k, v in all_labels.items() if k not in internal}
|
||||
label_str = build_label_str(labels)
|
||||
label_names = sorted(labels.keys())
|
||||
|
||||
t0 = time.monotonic()
|
||||
result = inject_labels(metrics, label_str, label_names)
|
||||
result += build_elapsed_gauge(all_labels)
|
||||
ts_ms = compute_timestamp_ms(all_labels)
|
||||
result = inject_timestamps(result, ts_ms)
|
||||
dt = time.monotonic() - t0
|
||||
|
||||
self._send(result)
|
||||
print(f" scrape from {src}: {len(metrics)} -> {len(result)} bytes, "
|
||||
f"inject {dt*1000:.1f}ms", flush=True)
|
||||
|
||||
def _send(self, body):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain; version=0.0.4")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
if body:
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # suppress per-request logging
|
||||
|
||||
|
||||
def resolve_bind_address(subnet_cidr):
|
||||
"""Find the local IP address that belongs to the given subnet.
|
||||
|
||||
Uses ``ip -j addr show`` to enumerate interfaces and returns the first
|
||||
address that falls within *subnet_cidr* (e.g. ``10.10.0.0/24``).
|
||||
"""
|
||||
network = ipaddress.ip_network(subnet_cidr, strict=False)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-j", "addr", "show"],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
interfaces = json.loads(result.stdout)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError) as exc:
|
||||
print(f"Error: cannot enumerate interfaces: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for iface in interfaces:
|
||||
for addr_info in iface.get("addr_info", []):
|
||||
try:
|
||||
addr = ipaddress.ip_address(addr_info["local"])
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
if addr in network:
|
||||
return str(addr)
|
||||
|
||||
print(f"Error: no interface address found in subnet {subnet_cidr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Prometheus metrics proxy with label injection")
|
||||
parser.add_argument("--labels", default="/tmp/bench-metrics-labels.json",
|
||||
help="Path to JSON file with labels to inject (default: /tmp/bench-metrics-labels.json)")
|
||||
parser.add_argument("--upstream", default="http://127.0.0.1:9100/",
|
||||
help="Upstream reth metrics URL (default: http://127.0.0.1:9100/)")
|
||||
|
||||
bind_group = parser.add_mutually_exclusive_group()
|
||||
bind_group.add_argument("--bind", default=None,
|
||||
help="Address to bind the proxy (default: 0.0.0.0)")
|
||||
bind_group.add_argument("--subnet", default=None,
|
||||
help="Auto-detect bind address from a local interface in this subnet (e.g. 10.10.0.0/24)")
|
||||
|
||||
parser.add_argument("--port", type=int, default=9090,
|
||||
help="Port to bind the proxy (default: 9090)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.subnet:
|
||||
bind_addr = resolve_bind_address(args.subnet)
|
||||
elif args.bind:
|
||||
bind_addr = args.bind
|
||||
else:
|
||||
bind_addr = "0.0.0.0"
|
||||
|
||||
server = HTTPServer((bind_addr, args.port), MetricsHandler)
|
||||
server.upstream = args.upstream
|
||||
server.labels_file = args.labels
|
||||
|
||||
print(f"bench-metrics-proxy listening on {bind_addr}:{args.port}")
|
||||
print(f" upstream: {args.upstream}")
|
||||
print(f" labels: {args.labels}")
|
||||
sys.stdout.flush()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
40
.github/scripts/bench-reth-build.sh
vendored
40
.github/scripts/bench-reth-build.sh
vendored
@@ -22,6 +22,22 @@ MODE="$1"
|
||||
SOURCE_DIR="$2"
|
||||
COMMIT="$3"
|
||||
|
||||
# Tracy support: when BENCH_TRACY is "on" or "full", add Tracy cargo features
|
||||
# and frame pointers for accurate stack traces.
|
||||
EXTRA_FEATURES=""
|
||||
EXTRA_RUSTFLAGS=""
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
EXTRA_FEATURES="tracy,tracy-client/ondemand"
|
||||
EXTRA_RUSTFLAGS=" -C force-frame-pointers=yes"
|
||||
fi
|
||||
|
||||
# Cache suffix: hash of features+rustflags so different build configs get separate cache entries
|
||||
if [ -n "$EXTRA_FEATURES" ] || [ -n "$EXTRA_RUSTFLAGS" ]; then
|
||||
BUILD_SUFFIX="-$(echo "${EXTRA_FEATURES}${EXTRA_RUSTFLAGS}" | sha256sum | cut -c1-12)"
|
||||
else
|
||||
BUILD_SUFFIX=""
|
||||
fi
|
||||
|
||||
# Verify a cached reth binary was built from the expected commit.
|
||||
# `reth --version` outputs "Commit SHA: <full-sha>" on its own line.
|
||||
verify_binary() {
|
||||
@@ -42,7 +58,7 @@ verify_binary() {
|
||||
|
||||
case "$MODE" in
|
||||
baseline|main)
|
||||
BUCKET="minio/reth-binaries/${COMMIT}"
|
||||
BUCKET="minio/reth-binaries/${COMMIT}${BUILD_SUFFIX}"
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
|
||||
CACHE_VALID=false
|
||||
@@ -59,14 +75,23 @@ case "$MODE" in
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building baseline (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
cargo build --profile profiling --bin reth
|
||||
FEATURES_ARG=""
|
||||
WORKSPACE_ARG=""
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
FEATURES_ARG="--features ${EXTRA_FEATURES}"
|
||||
WORKSPACE_ARG="--workspace"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling --bin reth $WORKSPACE_ARG $FEATURES_ARG
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
fi
|
||||
;;
|
||||
|
||||
feature|branch)
|
||||
BRANCH_SHA="${4:-$COMMIT}"
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}"
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
@@ -85,7 +110,14 @@ case "$MODE" in
|
||||
echo "Building feature (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
rustup show active-toolchain || rustup default stable
|
||||
make profiling
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# Can't use `make profiling` when adding features; build explicitly
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling --workspace --bin reth --features "${EXTRA_FEATURES}"
|
||||
else
|
||||
make profiling
|
||||
fi
|
||||
make install-reth-bench
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
|
||||
|
||||
581
.github/scripts/bench-reth-local.sh
vendored
Executable file
581
.github/scripts/bench-reth-local.sh
vendored
Executable file
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# local-reth-bench.sh — Run the reth Engine API benchmark locally.
|
||||
#
|
||||
# Replicates the CI bench.yml workflow (build, snapshot, system tuning,
|
||||
# interleaved B-F-F-B execution, summary, charts) without any GitHub
|
||||
# Actions glue (no PR comments, no artifact upload, no Slack).
|
||||
#
|
||||
# Usage:
|
||||
# local-reth-bench.sh <baseline-ref> <feature-ref> [options]
|
||||
#
|
||||
# Options:
|
||||
# --blocks N Number of blocks to benchmark (default: 500)
|
||||
# --warmup N Number of warmup blocks (default: 100)
|
||||
# --cores N Limit reth to N CPU cores, 0 = all available (default: 0)
|
||||
# --samply Enable samply profiling
|
||||
# --tracy MODE Tracy profiling: off, on, full (default: off)
|
||||
# --tracy-filter F Tracy tracing filter (default: debug)
|
||||
# --no-tune Skip system tuning (useful on dev machines / macOS)
|
||||
#
|
||||
# Requires: the reth repo at RETH_REPO (default: ~/reth)
|
||||
#
|
||||
# Dependencies (install before first run):
|
||||
# mc (MinIO client), schelk, cpupower, taskset, stdbuf, python3, curl,
|
||||
# make, uv, pzstd, jq, Rust toolchain (cargo/rustup)
|
||||
#
|
||||
# The script delegates to the existing bench-reth-*.sh scripts in the reth
|
||||
# repo for the actual build, snapshot, and run steps.
|
||||
set -euo pipefail
|
||||
|
||||
# ── PATH ──────────────────────────────────────────────────────────────
|
||||
# Ensure cargo and user-local bins (mc, uv) are visible
|
||||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
RETH_REPO="${RETH_REPO:-$HOME/reth}"
|
||||
BLOCKS=500
|
||||
WARMUP=100
|
||||
CORES=0
|
||||
SAMPLY=false
|
||||
TRACY="off"
|
||||
TRACY_FILTER="debug"
|
||||
TUNE=true
|
||||
BASELINE_REF=""
|
||||
FEATURE_REF=""
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <baseline-ref> <feature-ref> [options]
|
||||
|
||||
Options:
|
||||
--blocks N Number of blocks to benchmark (default: 500)
|
||||
--warmup N Number of warmup blocks (default: 100)
|
||||
--cores N Limit reth to N CPU cores (default: 0 = all)
|
||||
--samply Enable samply profiling
|
||||
--tracy MODE Tracy profiling: off, on, full (default: off)
|
||||
on = tracing only (lower overhead)
|
||||
full = tracing + CPU sampling (higher overhead)
|
||||
--tracy-filter F Tracy tracing filter (default: debug)
|
||||
--no-tune Skip system tuning
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--blocks) BLOCKS="$2"; shift 2 ;;
|
||||
--warmup) WARMUP="$2"; shift 2 ;;
|
||||
--cores) CORES="$2"; shift 2 ;;
|
||||
--samply) SAMPLY=true; shift ;;
|
||||
--tracy) TRACY="$2"; shift 2 ;;
|
||||
--tracy-filter) TRACY_FILTER="$2"; shift 2 ;;
|
||||
--no-tune) TUNE=false; shift ;;
|
||||
--help|-h) usage ;;
|
||||
-*) echo "Unknown option: $1"; usage ;;
|
||||
*)
|
||||
if [ -z "$BASELINE_REF" ]; then
|
||||
BASELINE_REF="$1"
|
||||
elif [ -z "$FEATURE_REF" ]; then
|
||||
FEATURE_REF="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1"; usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$BASELINE_REF" ] || [ -z "$FEATURE_REF" ]; then
|
||||
echo "Error: both <baseline-ref> and <feature-ref> are required."
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate --tracy value
|
||||
case "$TRACY" in
|
||||
off|on|full) ;;
|
||||
*) echo "Error: --tracy must be off, on, or full (got: $TRACY)"; usage ;;
|
||||
esac
|
||||
|
||||
# Samply + tracy=full are mutually exclusive (both use perf sampling)
|
||||
if [ "$SAMPLY" = "true" ] && [ "$TRACY" = "full" ]; then
|
||||
echo "Warning: samply and tracy=full both use perf sampling; downgrading tracy to 'on'."
|
||||
TRACY="on"
|
||||
fi
|
||||
|
||||
# ── Check dependencies ───────────────────────────────────────────────
|
||||
missing=()
|
||||
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq cargo; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "Error: missing required tools: ${missing[*]}"
|
||||
echo "See the CI 'Install dependencies' step in .github/workflows/bench.yml for install instructions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
if ! command -v tracy-capture &>/dev/null; then
|
||||
echo "Error: tracy-capture is required for --tracy $TRACY"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure tools that run via sudo are in a sudo-visible path.
|
||||
# The bench scripts use `sudo schelk` / `sudo samply` but cargo installs
|
||||
# them to ~/.cargo/bin which sudo's secure_path doesn't include.
|
||||
for cmd in schelk samply; do
|
||||
if command -v "$cmd" &>/dev/null && ! sudo sh -c "command -v $cmd" &>/dev/null; then
|
||||
echo "Installing $cmd to /usr/local/bin (needed for sudo)..."
|
||||
sudo install "$(command -v "$cmd")" /usr/local/bin/
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d "$RETH_REPO/.git" ]; then
|
||||
echo "Error: RETH_REPO=$RETH_REPO is not a git repository."
|
||||
echo "Set RETH_REPO or clone reth to ~/reth"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Resolve paths ────────────────────────────────────────────────────
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPTS_DIR="${RETH_REPO}/.github/scripts"
|
||||
BENCH_WORK_DIR="${RETH_REPO}/../bench-work-$(date +%Y%m%d-%H%M%S)"
|
||||
BASELINE_SRC="${RETH_REPO}/../reth-baseline"
|
||||
FEATURE_SRC="${RETH_REPO}/../reth-feature"
|
||||
|
||||
mkdir -p "$BENCH_WORK_DIR"
|
||||
BENCH_WORK_DIR="$(cd "$BENCH_WORK_DIR" && pwd)"
|
||||
|
||||
# ── Global cleanup trap (restores system tuning on any exit) ─────────
|
||||
TUNING_APPLIED=false
|
||||
CSTATE_PID=
|
||||
METRICS_PROXY_PID=
|
||||
cleanup_global() {
|
||||
[ -n "$METRICS_PROXY_PID" ] && kill "$METRICS_PROXY_PID" 2>/dev/null || true
|
||||
if [ "$TUNING_APPLIED" = true ]; then
|
||||
echo
|
||||
echo "▸ Restoring system settings..."
|
||||
[ -n "$CSTATE_PID" ] && kill "$CSTATE_PID" 2>/dev/null || true
|
||||
sudo systemctl start irqbalance cron atd 2>/dev/null || true
|
||||
echo " System settings restored."
|
||||
fi
|
||||
}
|
||||
trap cleanup_global EXIT
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " reth local benchmark"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Baseline ref : $BASELINE_REF"
|
||||
echo " Feature ref : $FEATURE_REF"
|
||||
echo " Blocks : $BLOCKS"
|
||||
echo " Warmup : $WARMUP"
|
||||
echo " Cores : $CORES"
|
||||
echo " Samply : $SAMPLY"
|
||||
echo " Tracy : $TRACY"
|
||||
echo " Tracy filter : $TRACY_FILTER"
|
||||
echo " System tune : $TUNE"
|
||||
echo " Work dir : $BENCH_WORK_DIR"
|
||||
echo " Reth repo : $RETH_REPO"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo
|
||||
|
||||
# Enable sccache if available (matches CI's RUSTC_WRAPPER=sccache)
|
||||
if command -v sccache &>/dev/null; then
|
||||
export RUSTC_WRAPPER="sccache"
|
||||
fi
|
||||
|
||||
# Export env vars expected by the bench-reth-*.sh scripts
|
||||
export BENCH_BLOCKS="$BLOCKS"
|
||||
export BENCH_WARMUP_BLOCKS="$WARMUP"
|
||||
export BENCH_CORES="$CORES"
|
||||
export BENCH_SAMPLY="$SAMPLY"
|
||||
export BENCH_TRACY="$TRACY"
|
||||
export BENCH_TRACY_FILTER="$TRACY_FILTER"
|
||||
export BENCH_WORK_DIR
|
||||
export SCHELK_MOUNT="${SCHELK_MOUNT:-/reth-bench}"
|
||||
export BENCH_RPC_URL="${BENCH_RPC_URL:-https://ethereum.reth.rs/rpc}"
|
||||
export BENCH_METRICS_ADDR="127.0.0.1:9100"
|
||||
|
||||
# ── Step 1: Resolve refs to full SHAs ────────────────────────────────
|
||||
echo "▸ Resolving git refs..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
resolve_ref() {
|
||||
local ref="$1"
|
||||
git fetch origin "$ref" --quiet 2>/dev/null || true
|
||||
git rev-parse "$ref" 2>/dev/null \
|
||||
|| git rev-parse "origin/$ref" 2>/dev/null \
|
||||
|| { echo "Error: cannot resolve ref '$ref'"; exit 1; }
|
||||
}
|
||||
|
||||
BASELINE_SHA="$(resolve_ref "$BASELINE_REF")"
|
||||
FEATURE_SHA="$(resolve_ref "$FEATURE_REF")"
|
||||
echo " Baseline SHA : $BASELINE_SHA"
|
||||
echo " Feature SHA : $FEATURE_SHA"
|
||||
echo
|
||||
|
||||
# ── Step 2: Prepare source directories ───────────────────────────────
|
||||
echo "▸ Preparing source directories..."
|
||||
|
||||
prepare_source() {
|
||||
local src_dir="$1" ref="$2"
|
||||
if [ -d "$src_dir" ]; then
|
||||
git -C "$src_dir" fetch origin "$ref" 2>/dev/null || true
|
||||
else
|
||||
git clone --recurse-submodules "$RETH_REPO" "$src_dir"
|
||||
fi
|
||||
git -C "$src_dir" checkout "$ref" --force
|
||||
git -C "$src_dir" submodule update --init --recursive
|
||||
}
|
||||
|
||||
prepare_source "$BASELINE_SRC" "$BASELINE_SHA"
|
||||
prepare_source "$FEATURE_SRC" "$FEATURE_SHA"
|
||||
BASELINE_SRC="$(cd "$BASELINE_SRC" && pwd)"
|
||||
FEATURE_SRC="$(cd "$FEATURE_SRC" && pwd)"
|
||||
echo " Baseline src : $BASELINE_SRC"
|
||||
echo " Feature src : $FEATURE_SRC"
|
||||
echo
|
||||
|
||||
# ── Step 3: Check / download snapshot ────────────────────────────────
|
||||
echo "▸ Checking snapshot..."
|
||||
cd "$RETH_REPO"
|
||||
SNAPSHOT_NEEDED=false
|
||||
if ! "${SCRIPTS_DIR}/bench-reth-snapshot.sh" --check; then
|
||||
SNAPSHOT_NEEDED=true
|
||||
echo " Snapshot needs update."
|
||||
else
|
||||
echo " Snapshot is up-to-date."
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 4: Build binaries (+ snapshot download) in parallel ─────────
|
||||
echo "▸ Building binaries (parallel)..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
FAIL=0
|
||||
|
||||
"${SCRIPTS_DIR}/bench-reth-build.sh" baseline "$BASELINE_SRC" "$BASELINE_SHA" &
|
||||
PID_BASELINE=$!
|
||||
|
||||
"${SCRIPTS_DIR}/bench-reth-build.sh" feature "$FEATURE_SRC" "$FEATURE_SHA" &
|
||||
PID_FEATURE=$!
|
||||
|
||||
PID_SNAPSHOT=
|
||||
if [ "$SNAPSHOT_NEEDED" = "true" ]; then
|
||||
echo " Also downloading snapshot in parallel..."
|
||||
"${SCRIPTS_DIR}/bench-reth-snapshot.sh" &
|
||||
PID_SNAPSHOT=$!
|
||||
fi
|
||||
|
||||
wait $PID_BASELINE || FAIL=1
|
||||
wait $PID_FEATURE || FAIL=1
|
||||
[ -n "$PID_SNAPSHOT" ] && { wait $PID_SNAPSHOT || FAIL=1; }
|
||||
|
||||
if [ $FAIL -ne 0 ]; then
|
||||
echo "Error: one or more parallel tasks failed (builds / snapshot)"
|
||||
exit 1
|
||||
fi
|
||||
echo " Binaries built successfully."
|
||||
echo
|
||||
|
||||
# ── Step 5: System tuning (optional) ────────────────────────────────
|
||||
if [ "$TUNE" = "true" ]; then
|
||||
echo "▸ Applying system tuning..."
|
||||
|
||||
sudo cpupower frequency-set -g performance 2>/dev/null || true
|
||||
|
||||
# Disable turbo boost (Intel + AMD)
|
||||
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null || true
|
||||
echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost 2>/dev/null || true
|
||||
|
||||
sudo swapoff -a 2>/dev/null || true
|
||||
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space 2>/dev/null || true
|
||||
|
||||
# Disable SMT (hyperthreading)
|
||||
for cpu in /sys/devices/system/cpu/cpu*/topology/thread_siblings_list; do
|
||||
[ -f "$cpu" ] || continue
|
||||
first=$(cut -d, -f1 < "$cpu" | cut -d- -f1)
|
||||
current=$(echo "$cpu" | grep -o 'cpu[0-9]*' | grep -o '[0-9]*')
|
||||
if [ "$current" != "$first" ]; then
|
||||
echo 0 | sudo tee "/sys/devices/system/cpu/cpu${current}/online" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
echo " Online CPUs: $(nproc)"
|
||||
|
||||
# Disable transparent huge pages
|
||||
for p in /sys/kernel/mm/transparent_hugepage /sys/kernel/mm/transparent_hugepages; do
|
||||
if [ -d "$p" ]; then
|
||||
echo never | sudo tee "$p/enabled" 2>/dev/null || true
|
||||
echo never | sudo tee "$p/defrag" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Prevent deep C-states
|
||||
sudo sh -c 'exec 3<>/dev/cpu_dma_latency; echo -ne "\x00\x00\x00\x00" >&3; sleep infinity' &
|
||||
CSTATE_PID=$!
|
||||
|
||||
# Pin IRQs to core 0
|
||||
for irq in /proc/irq/*/smp_affinity_list; do
|
||||
echo 0 | sudo tee "$irq" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Stop noisy background services
|
||||
sudo systemctl stop irqbalance cron atd unattended-upgrades snapd 2>/dev/null || true
|
||||
|
||||
TUNING_APPLIED=true
|
||||
|
||||
# Log environment for reproducibility (matches CI)
|
||||
echo " === Benchmark environment ==="
|
||||
echo " Kernel : $(uname -r)"
|
||||
lscpu | grep -E 'Model name|CPU\(s\)|MHz|NUMA' | sed 's/^/ /'
|
||||
echo " Governor : $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo unknown)"
|
||||
echo " Freq : $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null || echo unknown)"
|
||||
echo " THP : $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || cat /sys/kernel/mm/transparent_hugepages/enabled 2>/dev/null || echo unknown)"
|
||||
free -h | sed 's/^/ /'
|
||||
echo " System tuning applied."
|
||||
echo
|
||||
fi
|
||||
|
||||
# ── Step 5b: Tracefs mount (tracy=full only) ─────────────────────────
|
||||
if [ "$TRACY" = "full" ] && [ "$(uname)" = "Linux" ]; then
|
||||
echo "▸ Mounting tracefs for Tracy full mode..."
|
||||
sudo mount -t tracefs tracefs /sys/kernel/tracing -o mode=755 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Tracy upload & viewer helpers ────────────────────────────────────
|
||||
TRACY_VIEWER_BASE="${TRACY_VIEWER_BASE:-}"
|
||||
|
||||
tracy_viewer_url() {
|
||||
local profile_url="$1"
|
||||
if [ -z "$TRACY_VIEWER_BASE" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
local encoded
|
||||
encoded=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$profile_url")
|
||||
echo "${TRACY_VIEWER_BASE}?profile_url=${encoded}"
|
||||
}
|
||||
|
||||
upload_tracy() {
|
||||
local label="$1" output_dir="$2" sha="$3"
|
||||
local tracy_file="$output_dir/tracy-profile.tracy"
|
||||
|
||||
if [ ! -f "$tracy_file" ]; then
|
||||
echo " Tracy: no profile found, skipping upload."
|
||||
return
|
||||
fi
|
||||
|
||||
local timestamp short_sha remote_name bucket mc_alias
|
||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
short_sha="${sha:0:7}"
|
||||
remote_name="${label}-${short_sha}-${timestamp}.tracy"
|
||||
bucket="${TRACY_BUCKET:-tracy-profiles}"
|
||||
mc_alias="${MC_ALIAS:-minio}"
|
||||
local minio_base="${TRACY_MINIO_URL:-http://minio.minio.svc.cluster.local:9000}"
|
||||
|
||||
echo " Tracy: uploading profile..."
|
||||
if mc cp "$tracy_file" "${mc_alias}/${bucket}/${remote_name}"; then
|
||||
local url="${minio_base}/${bucket}/${remote_name}"
|
||||
echo "$url" > "$output_dir/tracy_url.txt"
|
||||
local viewer
|
||||
viewer=$(tracy_viewer_url "$url")
|
||||
if [ -n "$viewer" ]; then
|
||||
echo "$viewer" > "$output_dir/tracy_viewer_url.txt"
|
||||
echo " Tracy: uploaded → $viewer"
|
||||
else
|
||||
echo " Tracy: uploaded → $url"
|
||||
fi
|
||||
else
|
||||
echo " Tracy: upload failed (non-fatal)."
|
||||
fi
|
||||
|
||||
# Delete large profile to free disk
|
||||
rm -f "$tracy_file"
|
||||
}
|
||||
|
||||
# ── Step 6: Pre-flight cleanup ───────────────────────────────────────
|
||||
echo "▸ Pre-flight cleanup..."
|
||||
pkill -f bench-metrics-proxy 2>/dev/null || true
|
||||
sudo pkill -9 reth 2>/dev/null || true
|
||||
sleep 1
|
||||
if mountpoint -q "$SCHELK_MOUNT" 2>/dev/null; then
|
||||
sudo umount -l "$SCHELK_MOUNT" 2>/dev/null || true
|
||||
sudo schelk recover -y 2>/dev/null || true
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 7: Interleaved benchmark runs (B-F-F-B) ────────────────────
|
||||
# This ordering reduces systematic bias from thermal drift and cache warming.
|
||||
BASELINE_BIN="${BASELINE_SRC}/target/profiling/reth"
|
||||
FEATURE_BIN="${FEATURE_SRC}/target/profiling/reth"
|
||||
|
||||
# Start metrics proxy (reth → label injection → Prometheus)
|
||||
LABELS_FILE="/tmp/bench-metrics-labels.json"
|
||||
echo '{}' > "$LABELS_FILE"
|
||||
METRICS_SUBNET="${METRICS_SUBNET:-10.10.0.0/24}"
|
||||
METRICS_PORT="${METRICS_PORT:-9090}"
|
||||
python3 "${SELF_DIR}/bench-metrics-proxy.py" \
|
||||
--labels "$LABELS_FILE" \
|
||||
--upstream "http://${BENCH_METRICS_ADDR}/" \
|
||||
--subnet "$METRICS_SUBNET" \
|
||||
--port "$METRICS_PORT" &
|
||||
METRICS_PROXY_PID=$!
|
||||
echo "▸ Metrics proxy started (PID $METRICS_PROXY_PID) on subnet ${METRICS_SUBNET}, port ${METRICS_PORT}"
|
||||
|
||||
# Unique benchmark ID: local-<timestamp> for local runs, ci-<run_id> for CI
|
||||
BENCH_ID="local-$(basename "$BENCH_WORK_DIR" | sed 's/bench-work-//')"
|
||||
# Reference epoch: shared time origin so all runs overlay in Grafana.
|
||||
# The proxy maps each run's elapsed time onto this common origin.
|
||||
BENCH_REFERENCE_EPOCH=$(date +%s)
|
||||
|
||||
write_labels() {
|
||||
local run_label="$1" run_type="$2" ref="$3" sha="$4"
|
||||
LAST_RUN_START=$(date +%s)
|
||||
cat > "$LABELS_FILE" <<-EOF
|
||||
{"benchmark_run":"${run_label}","run_type":"${run_type}","git_ref":"${ref}","bench_sha":"${sha}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
EOF
|
||||
}
|
||||
|
||||
run_bench() {
|
||||
local label="$1" binary="$2" output_dir="$3"
|
||||
echo "▸ Running benchmark: ${label}..."
|
||||
cd "$RETH_REPO"
|
||||
if command -v taskset &>/dev/null; then
|
||||
taskset -c 0 "${SCRIPTS_DIR}/bench-reth-run.sh" "$label" "$binary" "$output_dir"
|
||||
else
|
||||
"${SCRIPTS_DIR}/bench-reth-run.sh" "$label" "$binary" "$output_dir"
|
||||
fi
|
||||
echo " ✓ ${label} complete."
|
||||
echo
|
||||
}
|
||||
|
||||
write_labels "baseline-1" "baseline" "$BASELINE_REF" "$BASELINE_SHA"
|
||||
run_bench "baseline-1" "$BASELINE_BIN" "$BENCH_WORK_DIR/baseline-1"
|
||||
|
||||
write_labels "feature-1" "feature" "$FEATURE_REF" "$FEATURE_SHA"
|
||||
run_bench "feature-1" "$FEATURE_BIN" "$BENCH_WORK_DIR/feature-1"
|
||||
|
||||
write_labels "feature-2" "feature" "$FEATURE_REF" "$FEATURE_SHA"
|
||||
run_bench "feature-2" "$FEATURE_BIN" "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
write_labels "baseline-2" "baseline" "$BASELINE_REF" "$BASELINE_SHA"
|
||||
run_bench "baseline-2" "$BASELINE_BIN" "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
# ── Compute Grafana URL ──────────────────────────────────────────────
|
||||
GRAFANA_BASE_URL="https://tempoxyz.grafana.net/d/reth-bench-ghr/reth-bench-ghr"
|
||||
GRAFANA_DATASOURCE="ef57fux92e9z4e"
|
||||
LAST_RUN_DURATION=$(( $(date +%s) - LAST_RUN_START ))
|
||||
FROM_MS=$(( BENCH_REFERENCE_EPOCH * 1000 ))
|
||||
TO_MS=$(( (BENCH_REFERENCE_EPOCH + LAST_RUN_DURATION) * 1000 ))
|
||||
GRAFANA_URL="${GRAFANA_BASE_URL}?orgId=1&from=${FROM_MS}&to=${TO_MS}&timezone=browser&var-datasource=${GRAFANA_DATASOURCE}&var-job=reth-bench&var-benchmark_id=${BENCH_ID}&var-benchmark_run=\$__all"
|
||||
|
||||
# ── Step 8: Scan logs for errors ─────────────────────────────────────
|
||||
echo "▸ Scanning logs for errors..."
|
||||
ERRORS_FILE="$BENCH_WORK_DIR/errors.md"
|
||||
found_errors=false
|
||||
for run_dir in baseline-1 feature-1 feature-2 baseline-2; do
|
||||
LOG="$BENCH_WORK_DIR/$run_dir/node.log"
|
||||
[ -f "$LOG" ] || continue
|
||||
panics=$(grep -c -E 'panicked at' "$LOG" 2>/dev/null || true)
|
||||
errors=$(grep -c ' ERROR ' "$LOG" 2>/dev/null || true)
|
||||
if [ "$panics" -gt 0 ] || [ "$errors" -gt 0 ]; then
|
||||
if [ "$found_errors" = false ]; then
|
||||
printf '### ⚠️ Node Errors\n\n' >> "$ERRORS_FILE"
|
||||
found_errors=true
|
||||
fi
|
||||
printf '<details><summary><b>%s</b>: %d panic(s), %d error(s)</summary>\n\n' \
|
||||
"$run_dir" "$panics" "$errors" >> "$ERRORS_FILE"
|
||||
if [ "$panics" -gt 0 ]; then
|
||||
printf '**Panics:**\n```\n' >> "$ERRORS_FILE"
|
||||
grep -E 'panicked at' "$LOG" | head -10 >> "$ERRORS_FILE"
|
||||
printf '```\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
printf '**Errors (first 20):**\n```\n' >> "$ERRORS_FILE"
|
||||
grep ' ERROR ' "$LOG" | head -20 >> "$ERRORS_FILE"
|
||||
printf '```\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
printf '\n</details>\n\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
done
|
||||
if [ "$found_errors" = true ]; then
|
||||
echo " ⚠ Errors found — see $ERRORS_FILE"
|
||||
else
|
||||
echo " No errors found."
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 9: Parse results ───────────────────────────────────────────
|
||||
echo "▸ Parsing results..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
SUMMARY_ARGS=(
|
||||
--output-summary "$BENCH_WORK_DIR/summary.json"
|
||||
--output-markdown "$BENCH_WORK_DIR/comment.md"
|
||||
--repo "paradigmxyz/reth"
|
||||
--baseline-ref "$BASELINE_SHA"
|
||||
--baseline-name "$BASELINE_REF"
|
||||
--feature-name "$FEATURE_REF"
|
||||
--feature-ref "$FEATURE_SHA"
|
||||
--baseline-csv "$BENCH_WORK_DIR/baseline-1/combined_latency.csv" "$BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
--feature-csv "$BENCH_WORK_DIR/feature-1/combined_latency.csv" "$BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
--gas-csv "$BENCH_WORK_DIR/feature-1/total_gas.csv"
|
||||
--grafana-url "$GRAFANA_URL"
|
||||
)
|
||||
|
||||
python3 "${SCRIPTS_DIR}/bench-reth-summary.py" "${SUMMARY_ARGS[@]}"
|
||||
echo
|
||||
|
||||
# ── Step 10: Generate charts ─────────────────────────────────────────
|
||||
echo "▸ Generating charts..."
|
||||
CHART_ARGS=(
|
||||
--output-dir "$BENCH_WORK_DIR/charts"
|
||||
--feature "$BENCH_WORK_DIR/feature-1/combined_latency.csv" "$BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
--baseline "$BENCH_WORK_DIR/baseline-1/combined_latency.csv" "$BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
--baseline-name "$BASELINE_REF"
|
||||
--feature-name "$FEATURE_REF"
|
||||
)
|
||||
|
||||
if python3 -c "import matplotlib" 2>/dev/null; then
|
||||
python3 "${SCRIPTS_DIR}/bench-reth-charts.py" "${CHART_ARGS[@]}"
|
||||
elif command -v uv &>/dev/null; then
|
||||
uv run --with matplotlib python3 "${SCRIPTS_DIR}/bench-reth-charts.py" "${CHART_ARGS[@]}"
|
||||
else
|
||||
echo " Warning: matplotlib not available, skipping chart generation."
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 11: Upload Tracy profiles ────────────────────────────────────
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
echo "▸ Uploading Tracy profiles..."
|
||||
upload_tracy "baseline-1" "$BENCH_WORK_DIR/baseline-1" "$BASELINE_SHA"
|
||||
upload_tracy "feature-1" "$BENCH_WORK_DIR/feature-1" "$FEATURE_SHA"
|
||||
upload_tracy "feature-2" "$BENCH_WORK_DIR/feature-2" "$FEATURE_SHA"
|
||||
upload_tracy "baseline-2" "$BENCH_WORK_DIR/baseline-2" "$BASELINE_SHA"
|
||||
echo
|
||||
fi
|
||||
|
||||
# ── Done (system restore happens via EXIT trap) ─────────────────────
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Benchmark complete!"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Results : $BENCH_WORK_DIR/summary.json"
|
||||
echo " Markdown : $BENCH_WORK_DIR/comment.md"
|
||||
echo " Charts : $BENCH_WORK_DIR/charts/"
|
||||
if [ -f "$ERRORS_FILE" ]; then
|
||||
echo " Errors : $ERRORS_FILE"
|
||||
fi
|
||||
echo " Grafana : $GRAFANA_URL"
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
echo " ─── Tracy Profiles ───"
|
||||
for run_dir in baseline-1 feature-1 feature-2 baseline-2; do
|
||||
url_file="$BENCH_WORK_DIR/$run_dir/tracy_viewer_url.txt"
|
||||
if [ -f "$url_file" ]; then
|
||||
echo " $run_dir : $(cat "$url_file")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
155
.github/scripts/bench-reth-run.sh
vendored
155
.github/scripts/bench-reth-run.sh
vendored
@@ -6,6 +6,13 @@
|
||||
# Usage: bench-reth-run.sh <label> <binary> <output-dir>
|
||||
#
|
||||
# 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_RETH_NEW_PAYLOAD (true/false, default true)
|
||||
# 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)
|
||||
# BENCH_OTLP_TRACES_ENDPOINT (OTLP HTTP endpoint for traces, e.g. https://host/insert/opentelemetry/v1/traces)
|
||||
# BENCH_OTLP_LOGS_ENDPOINT (OTLP HTTP endpoint for logs, e.g. https://host/insert/opentelemetry/v1/logs)
|
||||
set -euo pipefail
|
||||
|
||||
LABEL="$1"
|
||||
@@ -17,6 +24,24 @@ LOG="${OUTPUT_DIR}/node.log"
|
||||
|
||||
cleanup() {
|
||||
kill "$TAIL_PID" 2>/dev/null || true
|
||||
# Stop tracy-capture first (SIGINT makes it disconnect and flush to disk)
|
||||
# Must happen before killing reth, otherwise reth keeps streaming data.
|
||||
if [ -n "${TRACY_PID:-}" ] && kill -0 "$TRACY_PID" 2>/dev/null; then
|
||||
echo "Stopping tracy-capture..."
|
||||
kill -INT "$TRACY_PID" 2>/dev/null || true
|
||||
for i in $(seq 1 30); do
|
||||
kill -0 "$TRACY_PID" 2>/dev/null || break
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo "Waiting for tracy-capture to finish writing... (${i}s)"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if kill -0 "$TRACY_PID" 2>/dev/null; then
|
||||
echo "tracy-capture still running after 30s, killing..."
|
||||
kill -9 "$TRACY_PID" 2>/dev/null || true
|
||||
fi
|
||||
wait "$TRACY_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${RETH_PID:-}" ] && sudo kill -0 "$RETH_PID" 2>/dev/null; then
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
# Send SIGINT to the inner reth process by exact name (not -f which
|
||||
@@ -53,8 +78,14 @@ cleanup() {
|
||||
fi
|
||||
}
|
||||
TAIL_PID=
|
||||
TRACY_PID=
|
||||
trap cleanup EXIT
|
||||
|
||||
# Clean up stale schelk state from a previous cancelled run.
|
||||
# If schelk thinks it's still mounted (e.g. a cancelled run skipped cleanup),
|
||||
# recover first to reset state.
|
||||
sudo schelk recover -y -k || true
|
||||
|
||||
# Mount
|
||||
sudo schelk mount -y
|
||||
sync
|
||||
@@ -73,6 +104,8 @@ if [ "${BENCH_CORES:-0}" -gt 0 ] && [ "$BENCH_CORES" -lt "$MAX_RETH" ]; then
|
||||
fi
|
||||
RETH_CPUS="1-${MAX_RETH}"
|
||||
|
||||
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
|
||||
|
||||
RETH_ARGS=(
|
||||
node
|
||||
--datadir "$DATADIR"
|
||||
@@ -87,16 +120,60 @@ RETH_ARGS=(
|
||||
--no-persist-peers
|
||||
)
|
||||
|
||||
# Big blocks mode requires the testing API and skip-invalid-transactions
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
RETH_ARGS+=(--http.api eth,net,web3,reth,testing --testing.skip-invalid-transactions)
|
||||
fi
|
||||
|
||||
# Append per-label extra node args (baseline or feature)
|
||||
EXTRA_NODE_ARGS=""
|
||||
case "$LABEL" in
|
||||
baseline*) EXTRA_NODE_ARGS="${BENCH_BASELINE_ARGS:-}" ;;
|
||||
feature*) EXTRA_NODE_ARGS="${BENCH_FEATURE_ARGS:-}" ;;
|
||||
esac
|
||||
if [ -n "$EXTRA_NODE_ARGS" ]; then
|
||||
# Word-split the string into individual args
|
||||
# shellcheck disable=SC2206
|
||||
RETH_ARGS+=($EXTRA_NODE_ARGS)
|
||||
fi
|
||||
|
||||
if [ -n "${BENCH_METRICS_ADDR:-}" ]; then
|
||||
RETH_ARGS+=(--metrics "$BENCH_METRICS_ADDR")
|
||||
fi
|
||||
|
||||
# OTLP traces and logs export
|
||||
if [ -n "${BENCH_OTLP_TRACES_ENDPOINT:-}" ]; then
|
||||
RETH_ARGS+=(--tracing-otlp="${BENCH_OTLP_TRACES_ENDPOINT}" --tracing-otlp.service-name=reth-bench)
|
||||
fi
|
||||
if [ -n "${BENCH_OTLP_LOGS_ENDPOINT:-}" ]; then
|
||||
RETH_ARGS+=(--logs-otlp="${BENCH_OTLP_LOGS_ENDPOINT}")
|
||||
fi
|
||||
|
||||
# Tracy profiling: add --log.tracy flags and set environment
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
RETH_ARGS+=(--log.tracy --log.tracy.filter "${BENCH_TRACY_FILTER:-debug}")
|
||||
if [ "${BENCH_TRACY}" = "on" ]; then
|
||||
export TRACY_NO_SYS_TRACE=1
|
||||
elif [ "${BENCH_TRACY}" = "full" ]; then
|
||||
export TRACY_SAMPLING_HZ="${BENCH_TRACY_SAMPLING_HZ:-1}"
|
||||
fi
|
||||
fi
|
||||
|
||||
SUDO_ENV=()
|
||||
if [ -n "${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then
|
||||
SUDO_ENV+=("OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES}")
|
||||
fi
|
||||
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
RETH_ARGS+=(--log.samply)
|
||||
SAMPLY="$(which samply)"
|
||||
sudo taskset -c "$RETH_CPUS" nice -n -20 \
|
||||
sudo env "${SUDO_ENV[@]}" taskset -c "$RETH_CPUS" nice -n -20 \
|
||||
"$SAMPLY" record --save-only --presymbolicate --rate 10000 \
|
||||
--output "$OUTPUT_DIR/samply-profile.json.gz" \
|
||||
-- "$BINARY" "${RETH_ARGS[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
else
|
||||
sudo taskset -c "$RETH_CPUS" nice -n -20 "$BINARY" "${RETH_ARGS[@]}" \
|
||||
sudo env "${SUDO_ENV[@]}" taskset -c "$RETH_CPUS" nice -n -20 "$BINARY" "${RETH_ARGS[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
fi
|
||||
|
||||
@@ -124,21 +201,65 @@ done
|
||||
# files are not root-owned (avoids EACCES on next checkout).
|
||||
BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
|
||||
|
||||
# Warmup
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "${BENCH_WARMUP_BLOCKS:-50}" \
|
||||
--reth-new-payload 2>&1 | sed -u "s/^/[bench] /"
|
||||
# Build optional flags
|
||||
EXTRA_BENCH_ARGS=()
|
||||
if [ "${BENCH_RETH_NEW_PAYLOAD:-true}" != "false" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--reth-new-payload)
|
||||
fi
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
fi
|
||||
|
||||
# Benchmark
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "$BENCH_BLOCKS" \
|
||||
--reth-new-payload \
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
# Big blocks mode: replay pre-generated payloads with gas ramp
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
# Count gas ramp blocks for reporting
|
||||
GAS_RAMP_COUNT=$(find "$BIG_BLOCKS_DIR/gas-ramp-dir" -name '*.json' | wc -l)
|
||||
echo "$GAS_RAMP_COUNT" > "$OUTPUT_DIR/gas_ramp_blocks.txt"
|
||||
echo "Gas ramp blocks: $GAS_RAMP_COUNT"
|
||||
|
||||
# Start tracy-capture so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
echo "Starting tracy-capture..."
|
||||
tracy-capture -f -o "$OUTPUT_DIR/tracy-profile.tracy" &
|
||||
TRACY_PID=$!
|
||||
sleep 0.5 # give tracy-capture time to connect
|
||||
fi
|
||||
|
||||
echo "Running big blocks benchmark (replay-payloads)..."
|
||||
$BENCH_NICE "$RETH_BENCH" replay-payloads \
|
||||
"${EXTRA_BENCH_ARGS[@]}" \
|
||||
--gas-ramp-dir "$BIG_BLOCKS_DIR/gas-ramp-dir" \
|
||||
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
else
|
||||
# Standard mode: warmup + new-payload-fcu
|
||||
# Warmup
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "${BENCH_WARMUP_BLOCKS:-50}" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
|
||||
|
||||
# Start tracy-capture after warmup so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
echo "Starting tracy-capture..."
|
||||
tracy-capture -f -o "$OUTPUT_DIR/tracy-profile.tracy" &
|
||||
TRACY_PID=$!
|
||||
sleep 0.5 # give tracy-capture time to connect
|
||||
fi
|
||||
|
||||
# Benchmark
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "$BENCH_BLOCKS" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" \
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
fi
|
||||
|
||||
# cleanup runs via trap
|
||||
|
||||
47
.github/scripts/bench-reth-summary.py
vendored
47
.github/scripts/bench-reth-summary.py
vendored
@@ -107,6 +107,10 @@ def compute_stats(combined: list[dict]) -> dict:
|
||||
mgas_s_values.append(r["gas_used"] / lat_s / 1_000_000)
|
||||
mean_mgas_s = sum(mgas_s_values) / len(mgas_s_values) if mgas_s_values else 0
|
||||
|
||||
total_latencies_ms = [r["total_latency_us"] / 1_000 for r in combined]
|
||||
wall_clock_s = sum(total_latencies_ms) / 1_000
|
||||
mean_total_lat_ms = sum(total_latencies_ms) / n
|
||||
|
||||
return {
|
||||
"n": n,
|
||||
"mean_ms": mean_lat,
|
||||
@@ -115,6 +119,8 @@ def compute_stats(combined: list[dict]) -> dict:
|
||||
"p90_ms": percentile(sorted_lat, 90),
|
||||
"p99_ms": percentile(sorted_lat, 99),
|
||||
"mean_mgas_s": mean_mgas_s,
|
||||
"wall_clock_s": wall_clock_s,
|
||||
"mean_total_lat_ms": mean_total_lat_ms,
|
||||
}
|
||||
|
||||
|
||||
@@ -139,13 +145,14 @@ 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]]:
|
||||
) -> tuple[list[tuple[float, float]], list[float], list[float], list[float]]:
|
||||
"""Match blocks and return paired latencies and per-block diffs.
|
||||
|
||||
Returns:
|
||||
pairs: list of (baseline_ms, feature_ms) tuples
|
||||
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
|
||||
"""
|
||||
baseline_by_block = {r["block_number"]: r for r in baseline}
|
||||
feature_by_block = {r["block_number"]: r for r in feature}
|
||||
@@ -154,6 +161,7 @@ def _paired_data(
|
||||
pairs = []
|
||||
lat_diffs_ms = []
|
||||
mgas_diffs = []
|
||||
total_lat_diffs_ms = []
|
||||
for bn in common_blocks:
|
||||
b = baseline_by_block[bn]
|
||||
f = feature_by_block[bn]
|
||||
@@ -168,7 +176,10 @@ def _paired_data(
|
||||
f["gas_used"] / f_lat_s / 1_000_000
|
||||
- b["gas_used"] / b_lat_s / 1_000_000
|
||||
)
|
||||
return pairs, lat_diffs_ms, mgas_diffs
|
||||
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
|
||||
|
||||
|
||||
def compute_paired_stats(
|
||||
@@ -183,12 +194,14 @@ def compute_paired_stats(
|
||||
all_pairs = []
|
||||
all_lat_diffs = []
|
||||
all_mgas_diffs = []
|
||||
all_total_lat_diffs = []
|
||||
blocks_per_pair = []
|
||||
for baseline, feature in zip(baseline_runs, feature_runs):
|
||||
pairs, lat_diffs, mgas_diffs = _paired_data(baseline, feature)
|
||||
pairs, lat_diffs, mgas_diffs, total_lat_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)
|
||||
blocks_per_pair.append(len(pairs))
|
||||
|
||||
if not all_lat_diffs:
|
||||
@@ -227,6 +240,11 @@ def compute_paired_stats(
|
||||
mgas_se = std_mgas_diff / math.sqrt(len(all_mgas_diffs)) if all_mgas_diffs else 0.0
|
||||
mgas_ci = T_CRITICAL * mgas_se
|
||||
|
||||
mean_total_diff = sum(all_total_lat_diffs) / len(all_total_lat_diffs) if all_total_lat_diffs else 0.0
|
||||
std_total_diff = stddev(all_total_lat_diffs, mean_total_diff) if len(all_total_lat_diffs) > 1 else 0.0
|
||||
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
|
||||
|
||||
return {
|
||||
"n": n,
|
||||
"mean_diff_ms": mean_diff,
|
||||
@@ -239,6 +257,7 @@ def compute_paired_stats(
|
||||
"p99_ci_ms": (p99_boot[hi] - p99_boot[lo]) / 2,
|
||||
"mean_mgas_diff": mean_mgas_diff,
|
||||
"mgas_ci": mgas_ci,
|
||||
"wall_clock_ci_ms": wall_clock_ci_ms,
|
||||
"blocks": max(blocks_per_pair),
|
||||
}
|
||||
|
||||
@@ -267,6 +286,10 @@ def fmt_mgas(v: float) -> str:
|
||||
return f"{v:.2f}"
|
||||
|
||||
|
||||
def fmt_s(v: float) -> str:
|
||||
return f"{v:.2f}s"
|
||||
|
||||
|
||||
def significance(pct: float, ci_pct: float, lower_is_better: bool) -> str:
|
||||
"""Return significance label: 'good', 'bad', or 'neutral'."""
|
||||
significant = abs(pct) > ci_pct
|
||||
@@ -304,6 +327,7 @@ def compute_changes(
|
||||
("p90", "p90_ms", "p90_ci_ms", "p90_ms", True),
|
||||
("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),
|
||||
]
|
||||
changes = {}
|
||||
for name, stat_key, ci_key, base_key, lower_is_better in metrics:
|
||||
@@ -326,6 +350,7 @@ def generate_comparison_table(
|
||||
baseline_name: str,
|
||||
feature_name: str,
|
||||
feature_sha: str,
|
||||
big_blocks: bool = False,
|
||||
) -> str:
|
||||
"""Generate a markdown comparison table between baseline and feature."""
|
||||
n = paired["blocks"]
|
||||
@@ -335,6 +360,7 @@ def generate_comparison_table(
|
||||
|
||||
mean_pct = pct(run1["mean_ms"], run2["mean_ms"])
|
||||
gas_pct = pct(run1["mean_mgas_s"], run2["mean_mgas_s"])
|
||||
wall_pct = pct(run1["wall_clock_s"], run2["wall_clock_s"])
|
||||
|
||||
p50_pct = pct(run1["p50_ms"], run2["p50_ms"])
|
||||
p90_pct = pct(run1["p90_ms"], run2["p90_ms"])
|
||||
@@ -348,6 +374,7 @@ def generate_comparison_table(
|
||||
# CI as a percentage of baseline mean
|
||||
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
|
||||
|
||||
base_url = f"https://github.com/{repo}/commit"
|
||||
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
|
||||
@@ -362,8 +389,9 @@ def generate_comparison_table(
|
||||
f"| P90 | {fmt_ms(run1['p90_ms'])} | {fmt_ms(run2['p90_ms'])} | {change_str(p90_pct, p90_ci_pct, lower_is_better=True)} |",
|
||||
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"*{n} blocks*",
|
||||
f"*{n} {'big blocks' if big_blocks else 'blocks'}*",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -394,6 +422,7 @@ def generate_markdown(
|
||||
summary: dict, comparison_table: str,
|
||||
wait_time_tables: list[str] | None = None,
|
||||
behind_baseline: int = 0, repo: str = "", baseline_ref: str = "", baseline_name: str = "",
|
||||
grafana_url: str | None = None,
|
||||
) -> str:
|
||||
"""Generate a markdown comment body."""
|
||||
lines = ["## Benchmark Results", ""]
|
||||
@@ -413,6 +442,9 @@ def generate_markdown(
|
||||
lines.append(table)
|
||||
lines.append("")
|
||||
lines.append("</details>")
|
||||
if grafana_url:
|
||||
lines.append("")
|
||||
lines.append(f"**[Grafana Dashboard]({grafana_url})**")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -439,6 +471,9 @@ def main():
|
||||
parser.add_argument("--feature-name", "--branch-name", default=None, help="Feature branch name")
|
||||
parser.add_argument("--feature-ref", "--branch-sha", "--feature-sha", default=None, help="Feature commit SHA")
|
||||
parser.add_argument("--behind-baseline", "--behind-main", type=int, default=0, help="Commits behind baseline")
|
||||
parser.add_argument("--big-blocks", action="store_true", default=False, help="Big blocks mode")
|
||||
parser.add_argument("--gas-ramp-blocks", type=int, default=0, help="Number of gas ramp blocks (big blocks mode)")
|
||||
parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run")
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(args.baseline_csv) != len(args.feature_csv):
|
||||
@@ -487,6 +522,7 @@ def main():
|
||||
baseline_name=baseline_name,
|
||||
feature_name=feature_name,
|
||||
feature_sha=feature_sha,
|
||||
big_blocks=args.big_blocks,
|
||||
)
|
||||
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)")
|
||||
@@ -517,6 +553,8 @@ def main():
|
||||
|
||||
summary = {
|
||||
"blocks": paired_stats["blocks"],
|
||||
"big_blocks": args.big_blocks,
|
||||
"gas_ramp_blocks": args.gas_ramp_blocks,
|
||||
"baseline": {
|
||||
"name": baseline_name,
|
||||
"ref": baseline_ref,
|
||||
@@ -542,6 +580,7 @@ def main():
|
||||
repo=args.repo,
|
||||
baseline_ref=baseline_ref,
|
||||
baseline_name=baseline_name,
|
||||
grafana_url=args.grafana_url,
|
||||
)
|
||||
|
||||
with open(args.output_markdown, "w") as f:
|
||||
|
||||
29
.github/scripts/bench-slack-notify.js
vendored
29
.github/scripts/bench-slack-notify.js
vendored
@@ -7,6 +7,8 @@
|
||||
// BENCH_PR – PR number (may be empty)
|
||||
// BENCH_ACTOR – GitHub user who triggered the bench
|
||||
// BENCH_JOB_URL – URL to the Actions job page
|
||||
// BENCH_BASELINE_ARGS – Extra CLI args for the baseline reth node
|
||||
// BENCH_FEATURE_ARGS – Extra CLI args for the feature reth node
|
||||
// BENCH_SAMPLY – 'true' if samply profiling was enabled
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
@@ -68,6 +70,7 @@ function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, re
|
||||
|
||||
function fmtMs(v) { return v.toFixed(2) + 'ms'; }
|
||||
function fmtMgas(v) { return v.toFixed(2); }
|
||||
function fmtS(v) { return v.toFixed(2) + 's'; }
|
||||
function fmtChange(ch) {
|
||||
if (!ch.pct && !ch.ci_pct) return ' ';
|
||||
const pctStr = `${ch.pct >= 0 ? '+' : ''}${ch.pct.toFixed(2)}%`;
|
||||
@@ -117,12 +120,27 @@ function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, re
|
||||
if (fl1) featureLine += ` | <${fl1}|Samply 1>`;
|
||||
if (fl2) featureLine += ` | <${fl2}|Samply 2>`;
|
||||
|
||||
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
|
||||
const countsLine = warmup
|
||||
? `*Warmup:* ${warmup} | *Blocks:* ${summary.blocks}`
|
||||
: `*Blocks:* ${summary.blocks}`;
|
||||
const cores = process.env.BENCH_CORES || '0';
|
||||
const countsParts = [];
|
||||
if (summary.big_blocks) {
|
||||
const gasRamp = summary.gas_ramp_blocks || 0;
|
||||
if (gasRamp > 0) countsParts.push(`*Gas Ramp:* ${gasRamp}`);
|
||||
countsParts.push(`*Big Blocks:* ${summary.blocks}`);
|
||||
} else {
|
||||
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
|
||||
if (warmup) countsParts.push(`*Warmup:* ${warmup}`);
|
||||
countsParts.push(`*Blocks:* ${summary.blocks}`);
|
||||
}
|
||||
if (cores !== '0') countsParts.push(`*Cores:* ${cores}`);
|
||||
const countsLine = countsParts.join(' | ');
|
||||
|
||||
const sectionText = [metaParts.join(' | '), '', baselineLine, featureLine, countsLine].join('\n');
|
||||
const baselineArgs = process.env.BENCH_BASELINE_ARGS || '';
|
||||
const featureArgs = process.env.BENCH_FEATURE_ARGS || '';
|
||||
const argsLines = [];
|
||||
if (baselineArgs) argsLines.push(`*Baseline Args:* \`${baselineArgs}\``);
|
||||
if (featureArgs) argsLines.push(`*Feature Args:* \`${featureArgs}\``);
|
||||
|
||||
const sectionText = [metaParts.join(' | '), '', baselineLine, featureLine, ...argsLines, countsLine].join('\n');
|
||||
|
||||
// Action buttons
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
@@ -166,6 +184,7 @@ function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, re
|
||||
[cell('P90'), cell(fmtMs(b.p90_ms)), cell(fmtMs(f.p90_ms)), cell(fmtChange(c.p90))],
|
||||
[cell('P99'), cell(fmtMs(b.p99_ms)), cell(fmtMs(f.p99_ms)), cell(fmtChange(c.p99))],
|
||||
[cell('Mgas/s'), cell(fmtMgas(b.mean_mgas_s)), cell(fmtMgas(f.mean_mgas_s)), cell(fmtChange(c.mgas_s))],
|
||||
[cell('Wall Clock'), cell(fmtS(b.wall_clock_s)), cell(fmtS(f.wall_clock_s)), cell(fmtChange(c.wall_clock))],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
13
.github/scripts/bench-slack-users.json
vendored
13
.github/scripts/bench-slack-users.json
vendored
@@ -9,5 +9,16 @@
|
||||
"gakonst": "U092SEPDM40",
|
||||
"Rjected": "U09F6SCKRGT",
|
||||
"DaniPopes": "U09FAT8EK2A",
|
||||
"emmajam": "U0A34UN92HW"
|
||||
"emmajam": "U0A34UN92HW",
|
||||
"onbjerg": "U09FB0UK5AA",
|
||||
"fgimenez": "U09G3GP7CSU",
|
||||
"rakita": "U09FB3Z2M7Y",
|
||||
"jxom": "U09F72MG083",
|
||||
"tmm": "U0AD0U8E88N",
|
||||
"pepyakin": "U0A7HKMGEHJ",
|
||||
"grandizzy": "U09F8DBDDRT",
|
||||
"SuperFluffy": "U095BKHB2Q4",
|
||||
"kamsz": "U0A2563UBRD",
|
||||
"zerosnacks": "U09FARPMN74",
|
||||
"samczsun": "U096R14E4H3"
|
||||
}
|
||||
|
||||
414
.github/scripts/build_pgo_bolt.sh
vendored
Executable file
414
.github/scripts/build_pgo_bolt.sh
vendored
Executable file
@@ -0,0 +1,414 @@
|
||||
#!/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 -euo 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
|
||||
23
.github/scripts/hive/expected_failures.yaml
vendored
23
.github/scripts/hive/expected_failures.yaml
vendored
@@ -16,28 +16,11 @@ rpc-compat:
|
||||
# syncing mode, the test expects syncing to be false on start
|
||||
- eth_syncing/check-syncing (reth)
|
||||
|
||||
# no fix due to https://github.com/paradigmxyz/reth/issues/8732
|
||||
engine-withdrawals:
|
||||
- Withdrawals Fork On Genesis (Paris) (reth)
|
||||
- Withdrawals Fork on Block 1 (Paris) (reth)
|
||||
- Withdrawals Fork on Block 2 (Paris) (reth)
|
||||
- Withdrawals Fork on Block 3 (Paris) (reth)
|
||||
- Withdraw to a single account (Paris) (reth)
|
||||
- Withdraw to two accounts (Paris) (reth)
|
||||
- Withdraw many accounts (Paris) (reth)
|
||||
- Withdraw zero amount (Paris) (reth)
|
||||
- Empty Withdrawals (Paris) (reth)
|
||||
- Corrupted Block Hash Payload (INVALID) (Paris) (reth)
|
||||
- Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org (Paris) (reth)
|
||||
engine-withdrawals: [ ]
|
||||
|
||||
engine-api: [ ]
|
||||
|
||||
# no fix due to https://github.com/paradigmxyz/reth/issues/8732
|
||||
engine-cancun:
|
||||
- Invalid PayloadAttributes, Missing BeaconRoot, Syncing=True (Cancun) (reth)
|
||||
# the test fails with older versions of the code for which it passed before, probably related to changes
|
||||
# in hive or its dependencies
|
||||
- Blob Transaction Ordering, Multiple Clients (Cancun) (reth)
|
||||
engine-cancun: [ ]
|
||||
|
||||
sync: [ ]
|
||||
|
||||
@@ -49,7 +32,7 @@ engine-auth: [ ]
|
||||
# The test artificially creates an empty account with storage, then tests EIP-7610's behavior.
|
||||
# On mainnet, ~25 such accounts exist as contract addresses (derived from keccak(prefix, caller,
|
||||
# nonce/salt), not from public keys). No private key exists for contract addresses. To trigger
|
||||
# this with EIP-7702, you'd need to recover a private key from one of the already deployed contract addresses - mathematically impossible.
|
||||
# this with EIP-7702, you'd need to recover a private key from one of the already deployed contract addresses - mathematically impossible.
|
||||
#
|
||||
# tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_*
|
||||
# Requires hash collision on create2 address to target already deployed accounts with storage.
|
||||
|
||||
11
.github/scripts/hive/ignored_tests.yaml
vendored
11
.github/scripts/hive/ignored_tests.yaml
vendored
@@ -11,17 +11,6 @@
|
||||
#
|
||||
# When a test should no longer be ignored, remove it from this list.
|
||||
|
||||
# flaky
|
||||
engine-withdrawals:
|
||||
- Withdrawals Fork on Block 1 - 8 Block Re-Org NewPayload (Paris) (reth)
|
||||
- Withdrawals Fork on Block 8 - 10 Block Re-Org NewPayload (Paris) (reth)
|
||||
- Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org (Paris) (reth)
|
||||
# P2P sync timing issue in hive Docker environment: secondary client returns SYNCING but
|
||||
# peer discovery/connection doesn't complete within the timeout when running with
|
||||
# --sim.parallelism 16. Not a correctness bug, purely a CI timing issue.
|
||||
- Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
|
||||
- Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts - No Transactions (Paris) (reth)
|
||||
- Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
|
||||
engine-cancun:
|
||||
- Transaction Re-Org, New Payload on Revert Back (Cancun) (reth)
|
||||
- Transaction Re-Org, Re-Org to Different Block (Cancun) (reth)
|
||||
|
||||
381
.github/workflows/bench.yml
vendored
381
.github/workflows/bench.yml
vendored
@@ -7,16 +7,12 @@
|
||||
# same block range (snapshot recovered between runs) to compare performance.
|
||||
|
||||
on:
|
||||
# TODO: Disabled temporarily for https://github.com/CodSpeedHQ/runner/issues/55
|
||||
# merge_group:
|
||||
push:
|
||||
branches: [main]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
blocks:
|
||||
description: "Number of blocks to benchmark"
|
||||
description: "Number of blocks to benchmark (or 'big' for big blocks mode)"
|
||||
required: false
|
||||
default: "500"
|
||||
type: string
|
||||
@@ -35,16 +31,46 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
wait_time:
|
||||
description: "Fixed wait time between blocks (e.g. 500ms, 1s)"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
baseline_args:
|
||||
description: "Extra CLI args for the baseline reth node"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
feature_args:
|
||||
description: "Extra CLI args for the feature reth node"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
samply:
|
||||
description: "Enable samply profiling"
|
||||
required: false
|
||||
default: "false"
|
||||
type: boolean
|
||||
reth_newPayload:
|
||||
description: "Use reth_newPayload RPC (server-side timing)"
|
||||
required: false
|
||||
default: "true"
|
||||
type: boolean
|
||||
cores:
|
||||
description: "Limit reth to N CPU cores (0 = all available)"
|
||||
required: false
|
||||
default: "0"
|
||||
type: string
|
||||
no_slack:
|
||||
description: "Suppress Slack notifications for benchmark results"
|
||||
required: false
|
||||
default: "true"
|
||||
type: boolean
|
||||
abba:
|
||||
description: "Run ABBA (BFFB) interleaved order; false = single AB pass"
|
||||
required: false
|
||||
default: "true"
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -60,46 +86,6 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: depot-ubuntu-latest
|
||||
concurrency:
|
||||
group: bench-codspeed-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
partition: [1, 2]
|
||||
total_partitions: [2]
|
||||
include:
|
||||
- partition: 1
|
||||
crates: "-p reth-primitives -p reth-trie-common -p reth-trie-sparse"
|
||||
- partition: 2
|
||||
crates: "-p reth-trie"
|
||||
name: codspeed (${{ matrix.partition }}/${{ matrix.total_partitions }})
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/merge', github.event.issue.number) || '' }}
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cargo-codspeed
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build --profile profiling --features test-utils ${{ matrix.crates }}
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
run: cargo codspeed run ${{ matrix.crates }}
|
||||
mode: instrumentation
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
reth-bench-ack:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && (startsWith(github.event.comment.body, '@decofe bench') || startsWith(github.event.comment.body, 'derek bench'))) ||
|
||||
@@ -116,7 +102,14 @@ 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 }}
|
||||
cores: ${{ steps.args.outputs.cores }}
|
||||
big-blocks: ${{ steps.args.outputs.big-blocks }}
|
||||
reth-new-payload: ${{ steps.args.outputs.reth-new-payload }}
|
||||
wait-time: ${{ steps.args.outputs.wait-time }}
|
||||
baseline-args: ${{ steps.args.outputs.baseline-args }}
|
||||
feature-args: ${{ steps.args.outputs.feature-args }}
|
||||
abba: ${{ steps.args.outputs.abba }}
|
||||
comment-id: ${{ steps.ack.outputs.comment-id }}
|
||||
steps:
|
||||
- name: Check org membership
|
||||
@@ -144,7 +137,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
let pr, actor, blocks, warmup, baseline, feature, samply, cores;
|
||||
let pr, actor, blocks, warmup, baseline, feature, samply, cores, bigBlocks;
|
||||
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
actor = '${{ github.actor }}';
|
||||
@@ -153,7 +146,14 @@ jobs:
|
||||
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';
|
||||
cores = '${{ github.event.inputs.cores }}' || '0';
|
||||
bigBlocks = blocks === 'big' ? 'true' : 'false';
|
||||
var rethNewPayload = '${{ github.event.inputs.reth_newPayload }}' !== 'false' ? 'true' : 'false';
|
||||
var abba = '${{ github.event.inputs.abba }}' !== 'false' ? 'true' : 'false';
|
||||
var waitTime = '${{ github.event.inputs.wait_time }}' || '';
|
||||
var baselineNodeArgs = '${{ github.event.inputs.baseline_args }}' || '';
|
||||
var featureNodeArgs = '${{ github.event.inputs.feature_args }}' || '';
|
||||
|
||||
// Find PR for the selected branch
|
||||
const branch = '${{ github.ref_name }}';
|
||||
@@ -173,37 +173,75 @@ jobs:
|
||||
actor = context.payload.comment.user.login;
|
||||
|
||||
const body = context.payload.comment.body.trim();
|
||||
const intArgs = new Set(['blocks', 'warmup', 'cores']);
|
||||
const intArgs = new Set(['warmup', 'cores']);
|
||||
const intOrKeywordArgs = new Map([['blocks', new Set(['big'])]]);
|
||||
const refArgs = new Set(['baseline', 'feature']);
|
||||
const boolArgs = new Set(['samply']);
|
||||
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', cores: '0' };
|
||||
const boolArgs = new Set(['samply', 'no-slack']);
|
||||
const boolDefaultTrue = new Set(['reth_newPayload', 'abba']);
|
||||
const durationArgs = new Set(['wait-time']);
|
||||
const stringArgs = new Set(['baseline-args', 'feature-args']);
|
||||
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', cores: '0', reth_newPayload: 'true', abba: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
|
||||
const unknown = [];
|
||||
const invalid = [];
|
||||
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
|
||||
for (const part of args.split(/\s+/).filter(Boolean)) {
|
||||
// Parse args, handling quoted values like key="value with spaces"
|
||||
const parts = [];
|
||||
const argRegex = /(\S+?="[^"]*"|\S+?='[^']*'|\S+)/g;
|
||||
let m;
|
||||
while ((m = argRegex.exec(args)) !== null) parts.push(m[1]);
|
||||
for (const part of parts) {
|
||||
const eq = part.indexOf('=');
|
||||
if (eq === -1) {
|
||||
if (boolArgs.has(part)) {
|
||||
defaults[part] = 'true';
|
||||
} else if (boolDefaultTrue.has(part)) {
|
||||
defaults[part] = 'true';
|
||||
} else {
|
||||
unknown.push(part);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const key = part.slice(0, eq);
|
||||
const value = part.slice(eq + 1);
|
||||
if (intArgs.has(key)) {
|
||||
let value = part.slice(eq + 1);
|
||||
// Strip surrounding quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (boolDefaultTrue.has(key)) {
|
||||
if (value === 'true' || value === 'false') {
|
||||
defaults[key] = value;
|
||||
} else {
|
||||
invalid.push(`\`${key}=${value}\` (must be true or false)`);
|
||||
}
|
||||
} else if (durationArgs.has(key)) {
|
||||
if (/^\d+(ms|s|m)$/.test(value)) {
|
||||
defaults[key] = value;
|
||||
} else {
|
||||
invalid.push(`\`${key}=${value}\` (must be a duration like 500ms, 1s, 2m)`);
|
||||
}
|
||||
} else if (intArgs.has(key)) {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
invalid.push(`\`${key}=${value}\` (must be a positive integer)`);
|
||||
} else {
|
||||
defaults[key] = value;
|
||||
}
|
||||
} else if (intOrKeywordArgs.has(key)) {
|
||||
const keywords = intOrKeywordArgs.get(key);
|
||||
if (keywords.has(value)) {
|
||||
defaults[key] = value;
|
||||
} else if (/^\d+$/.test(value)) {
|
||||
defaults[key] = value;
|
||||
} else {
|
||||
invalid.push(`\`${key}=${value}\` (must be a positive integer or one of: ${[...keywords].join(', ')})`);
|
||||
}
|
||||
} else if (refArgs.has(key)) {
|
||||
if (!value) {
|
||||
invalid.push(`\`${key}=\` (must be a git ref)`);
|
||||
} else {
|
||||
defaults[key] = value;
|
||||
}
|
||||
} else if (stringArgs.has(key)) {
|
||||
defaults[key] = value;
|
||||
} else {
|
||||
unknown.push(key);
|
||||
}
|
||||
@@ -212,7 +250,7 @@ jobs:
|
||||
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
|
||||
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
|
||||
if (errors.length) {
|
||||
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N] [warmup=N] [baseline=REF] [feature=REF] [samply] [cores=N]\``;
|
||||
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N|big] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [reth_newPayload=true|false] [abba=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]\``;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
@@ -227,7 +265,14 @@ jobs:
|
||||
baseline = defaults.baseline;
|
||||
feature = defaults.feature;
|
||||
samply = defaults.samply;
|
||||
var noSlack = defaults['no-slack'];
|
||||
cores = defaults.cores;
|
||||
bigBlocks = blocks === 'big' ? 'true' : 'false';
|
||||
var rethNewPayload = defaults.reth_newPayload;
|
||||
var abba = defaults.abba;
|
||||
var waitTime = defaults['wait-time'];
|
||||
var baselineNodeArgs = defaults['baseline-args'];
|
||||
var featureNodeArgs = defaults['feature-args'];
|
||||
}
|
||||
|
||||
// Resolve display names for baseline/feature
|
||||
@@ -255,7 +300,14 @@ jobs:
|
||||
core.setOutput('baseline-name', baselineName);
|
||||
core.setOutput('feature-name', featureName);
|
||||
core.setOutput('samply', samply);
|
||||
core.setOutput('no-slack', noSlack);
|
||||
core.setOutput('cores', cores);
|
||||
core.setOutput('big-blocks', bigBlocks);
|
||||
core.setOutput('reth-new-payload', rethNewPayload);
|
||||
core.setOutput('wait-time', waitTime);
|
||||
core.setOutput('baseline-args', baselineNodeArgs);
|
||||
core.setOutput('feature-args', featureNodeArgs);
|
||||
core.setOutput('abba', abba);
|
||||
|
||||
- name: Acknowledge request
|
||||
id: ack
|
||||
@@ -295,7 +347,6 @@ jobs:
|
||||
});
|
||||
allRuns.push(...r);
|
||||
}
|
||||
// Only count runs that trigger reth-bench (not push-triggered codspeed runs)
|
||||
const benchRuns = allRuns.filter(r => r.event === 'issue_comment' || r.event === 'workflow_dispatch');
|
||||
const thisRun = benchRuns.find(r => r.id === context.runId);
|
||||
const thisCreatedAt = thisRun ? new Date(thisRun.created_at) : new Date();
|
||||
@@ -314,10 +365,24 @@ 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 bigBlocks = '${{ steps.args.outputs.big-blocks }}' === 'true';
|
||||
const samplyNote = samply ? ', samply: `enabled`' : '';
|
||||
const noSlackNote = noSlack ? ', no-slack' : '';
|
||||
const cores = '${{ steps.args.outputs.cores }}';
|
||||
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
|
||||
const config = `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${coresNote}`;
|
||||
const rethNP = '${{ steps.args.outputs.reth-new-payload }}' !== 'false';
|
||||
const rethNPNote = !rethNP ? ', reth_newPayload: `disabled`' : '';
|
||||
const abbaEnabled = '${{ steps.args.outputs.abba }}' !== 'false';
|
||||
const abbaNote = !abbaEnabled ? ', abba: `disabled`' : '';
|
||||
const waitTimeVal = '${{ steps.args.outputs.wait-time }}';
|
||||
const waitTimeNote = waitTimeVal ? `, wait-time: \`${waitTimeVal}\`` : '';
|
||||
const baselineArgsVal = '${{ steps.args.outputs.baseline-args }}';
|
||||
const baselineArgsNote = baselineArgsVal ? `, baseline-args: \`${baselineArgsVal}\`` : '';
|
||||
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}${rethNPNote}${abbaNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`;
|
||||
|
||||
const { data: comment } = await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
@@ -342,10 +407,24 @@ 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 bigBlocks = '${{ steps.args.outputs.big-blocks }}' === 'true';
|
||||
const samplyNote = samply ? ', samply: `enabled`' : '';
|
||||
const noSlackNote = noSlack ? ', no-slack' : '';
|
||||
const cores = '${{ steps.args.outputs.cores }}';
|
||||
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
|
||||
const config = `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${coresNote}`;
|
||||
const rethNP = '${{ steps.args.outputs.reth-new-payload }}' !== 'false';
|
||||
const rethNPNote = !rethNP ? ', reth_newPayload: `disabled`' : '';
|
||||
const abbaEnabled = '${{ steps.args.outputs.abba }}' !== 'false';
|
||||
const abbaNote = !abbaEnabled ? ', abba: `disabled`' : '';
|
||||
const waitTimeVal = '${{ steps.args.outputs.wait-time }}';
|
||||
const waitTimeNote = waitTimeVal ? `, wait-time: \`${waitTimeVal}\`` : '';
|
||||
const baselineArgsVal = '${{ steps.args.outputs.baseline-args }}';
|
||||
const baselineArgsNote = baselineArgsVal ? `, baseline-args: \`${baselineArgsVal}\`` : '';
|
||||
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}${rethNPNote}${abbaNote}${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;
|
||||
@@ -409,8 +488,21 @@ jobs:
|
||||
BENCH_WARMUP_BLOCKS: ${{ needs.reth-bench-ack.outputs.warmup }}
|
||||
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_RETH_NEW_PAYLOAD: ${{ needs.reth-bench-ack.outputs.reth-new-payload }}
|
||||
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_COMMENT_ID: ${{ needs.reth-bench-ack.outputs.comment-id }}
|
||||
BENCH_NO_SLACK: ${{ needs.reth-bench-ack.outputs.no-slack }}
|
||||
BENCH_METRICS_ADDR: "127.0.0.1:9100"
|
||||
BENCH_OTLP_TRACES_ENDPOINT: ${{ secrets.BENCH_OTLP_TRACES_ENDPOINT }}
|
||||
BENCH_OTLP_LOGS_ENDPOINT: ${{ secrets.BENCH_OTLP_LOGS_ENDPOINT }}
|
||||
steps:
|
||||
- name: Clean up previous bench-work
|
||||
run: sudo rm -rf "$BENCH_WORK_DIR" 2>/dev/null || true
|
||||
|
||||
- name: Resolve checkout ref
|
||||
id: checkout-ref
|
||||
uses: actions/github-script@v8
|
||||
@@ -425,13 +517,11 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
pull_number: parseInt(process.env.BENCH_PR),
|
||||
});
|
||||
// For closed/merged PRs, the merge ref doesn't exist — use head SHA
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR #${process.env.BENCH_PR} is ${pr.state}, using head SHA ${pr.head.sha}`);
|
||||
core.setOutput('ref', pr.head.sha);
|
||||
} else {
|
||||
core.setOutput('ref', `refs/pull/${process.env.BENCH_PR}/merge`);
|
||||
}
|
||||
// Always use head SHA — the merge ref (refs/pull/N/merge) may not
|
||||
// exist if the PR has conflicts, was force-pushed, or was
|
||||
// merged/closed between this step and checkout.
|
||||
core.info(`PR #${process.env.BENCH_PR} (${pr.state}), using head SHA ${pr.head.sha}`);
|
||||
core.setOutput('ref', pr.head.sha);
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -459,10 +549,24 @@ 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 bigBlocks = process.env.BENCH_BIG_BLOCKS === 'true';
|
||||
const samplyNote = samply ? ', samply: `enabled`' : '';
|
||||
const noSlackNote = noSlack ? ', no-slack' : '';
|
||||
const cores = process.env.BENCH_CORES || '0';
|
||||
const coresNote = cores && cores !== '0' ? `, cores: \`${cores}\`` : '';
|
||||
core.exportVariable('BENCH_CONFIG', `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\`${samplyNote}${coresNote}`);
|
||||
const rethNP = (process.env.BENCH_RETH_NEW_PAYLOAD || 'true') !== 'false';
|
||||
const rethNPNote = !rethNP ? ', reth_newPayload: `disabled`' : '';
|
||||
const abbaEnabled = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const abbaNote = !abbaEnabled ? ', abba: `disabled`' : '';
|
||||
const waitTimeVal = process.env.BENCH_WAIT_TIME || '';
|
||||
const waitTimeNote = waitTimeVal ? `, wait-time: \`${waitTimeVal}\`` : '';
|
||||
const baselineArgsVal = process.env.BENCH_BASELINE_ARGS || '';
|
||||
const baselineArgsNote = baselineArgsVal ? `, baseline-args: \`${baselineArgsVal}\`` : '';
|
||||
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}${rethNPNote}${abbaNote}${waitTimeNote}${baselineArgsNote}${featureArgsNote}`);
|
||||
|
||||
const { buildBody } = require('./.github/scripts/bench-update-status.js');
|
||||
await github.rest.issues.updateComment({
|
||||
@@ -722,6 +826,45 @@ jobs:
|
||||
rm -rf "$BENCH_WORK_DIR"
|
||||
mkdir -p "$BENCH_WORK_DIR"
|
||||
|
||||
- name: Download big blocks
|
||||
if: env.BENCH_BIG_BLOCKS == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MC="mc --config-dir /home/ubuntu/.mc"
|
||||
BUCKET="minio/reth-snapshots/reth-1-minimal-nightly-previous-big-blocks.tar.zst"
|
||||
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
|
||||
rm -rf "$BIG_BLOCKS_DIR"; mkdir -p "$BIG_BLOCKS_DIR"
|
||||
echo "Downloading big blocks from $BUCKET..."
|
||||
$MC cat "$BUCKET" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
|
||||
echo "Big blocks downloaded to $BIG_BLOCKS_DIR"
|
||||
# Verify expected directory structure
|
||||
if [ ! -d "$BIG_BLOCKS_DIR/gas-ramp-dir" ] || [ ! -d "$BIG_BLOCKS_DIR/payloads" ]; then
|
||||
echo "::error::Big blocks archive missing expected gas-ramp-dir/ or payloads/ directories"
|
||||
ls -laR "$BIG_BLOCKS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
echo "Payload files: $(find "$BIG_BLOCKS_DIR/payloads" -name '*.json' | wc -l)"
|
||||
|
||||
- name: Start metrics proxy
|
||||
run: |
|
||||
BENCH_ID="ci-${{ github.run_id }}"
|
||||
BENCH_REFERENCE_EPOCH=$(date +%s)
|
||||
echo "BENCH_ID=${BENCH_ID}" >> "$GITHUB_ENV"
|
||||
echo "BENCH_REFERENCE_EPOCH=${BENCH_REFERENCE_EPOCH}" >> "$GITHUB_ENV"
|
||||
|
||||
LABELS_FILE="/tmp/bench-metrics-labels.json"
|
||||
echo '{}' > "$LABELS_FILE"
|
||||
echo "BENCH_LABELS_FILE=${LABELS_FILE}" >> "$GITHUB_ENV"
|
||||
|
||||
python3 .github/scripts/bench-metrics-proxy.py \
|
||||
--labels "$LABELS_FILE" \
|
||||
--upstream "http://${BENCH_METRICS_ADDR}/" \
|
||||
--subnet 10.10.0.0/24 \
|
||||
--port 9090 &
|
||||
PROXY_PID=$!
|
||||
echo "BENCH_METRICS_PROXY_PID=${PROXY_PID}" >> "$GITHUB_ENV"
|
||||
echo "Metrics proxy started (PID $PROXY_PID)"
|
||||
|
||||
- name: Update status (running benchmarks)
|
||||
if: success() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
@@ -735,19 +878,62 @@ jobs:
|
||||
# thermal drift and cache warming.
|
||||
- name: "Run benchmark: baseline (1/2)"
|
||||
id: run-baseline-1
|
||||
run: taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
|
||||
env:
|
||||
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-1,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
|
||||
run: |
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"baseline-1","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
|
||||
|
||||
- name: "Run benchmark: feature (1/2)"
|
||||
id: run-feature-1
|
||||
run: taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
|
||||
env:
|
||||
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-1,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
|
||||
run: |
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"feature-1","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
|
||||
|
||||
- name: "Run benchmark: feature (2/2)"
|
||||
id: run-feature-2
|
||||
run: taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
|
||||
env:
|
||||
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-2,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
|
||||
run: |
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"feature-2","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
- name: "Run benchmark: baseline (2/2)"
|
||||
id: run-baseline-2
|
||||
run: taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
|
||||
env:
|
||||
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-2,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
|
||||
run: |
|
||||
LAST_RUN_START=$(date +%s)
|
||||
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
- name: Stop metrics proxy & generate Grafana URL
|
||||
id: metrics
|
||||
if: "!cancelled()"
|
||||
run: |
|
||||
kill "$BENCH_METRICS_PROXY_PID" 2>/dev/null || true
|
||||
|
||||
LAST_RUN_DURATION=$(( $(date +%s) - BENCH_LAST_RUN_START ))
|
||||
FROM_MS=$(( BENCH_REFERENCE_EPOCH * 1000 ))
|
||||
TO_MS=$(( (BENCH_REFERENCE_EPOCH + LAST_RUN_DURATION) * 1000 ))
|
||||
GRAFANA_URL="https://tempoxyz.grafana.net/d/reth-bench-ghr/reth-bench-ghr?orgId=1&from=${FROM_MS}&to=${TO_MS}&timezone=browser&var-datasource=ef57fux92e9z4e&var-job=reth-bench&var-benchmark_id=${BENCH_ID}&var-benchmark_run=\$__all"
|
||||
echo "grafana-url=${GRAFANA_URL}" >> "$GITHUB_OUTPUT"
|
||||
echo "Grafana URL: ${GRAFANA_URL}"
|
||||
|
||||
- name: Scan logs for errors
|
||||
if: "!cancelled()"
|
||||
@@ -849,12 +1035,30 @@ jobs:
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-name ${BASELINE_NAME}"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --feature-name ${FEATURE_NAME}"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --feature-ref ${FEATURE_REF}"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-csv $BENCH_WORK_DIR/baseline-1/combined_latency.csv $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --feature-csv $BENCH_WORK_DIR/feature-1/combined_latency.csv $BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
BASELINE_CSVS="$BENCH_WORK_DIR/baseline-1/combined_latency.csv"
|
||||
FEATURE_CSVS="$BENCH_WORK_DIR/feature-1/combined_latency.csv"
|
||||
if [ "${BENCH_ABBA:-true}" = "true" ]; then
|
||||
BASELINE_CSVS="$BASELINE_CSVS $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
FEATURE_CSVS="$FEATURE_CSVS $BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
fi
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-csv $BASELINE_CSVS"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --feature-csv $FEATURE_CSVS"
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --gas-csv $BENCH_WORK_DIR/feature-1/total_gas.csv"
|
||||
if [ "$BEHIND_BASELINE" -gt 0 ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --behind-baseline $BEHIND_BASELINE"
|
||||
fi
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --big-blocks"
|
||||
# Read gas ramp blocks count from first baseline run (same for all runs)
|
||||
GAS_RAMP_FILE="$BENCH_WORK_DIR/baseline-1/gas_ramp_blocks.txt"
|
||||
if [ -f "$GAS_RAMP_FILE" ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --gas-ramp-blocks $(cat "$GAS_RAMP_FILE" | tr -d '[:space:]')"
|
||||
fi
|
||||
fi
|
||||
GRAFANA_URL='${{ steps.metrics.outputs.grafana-url }}'
|
||||
if [ -n "$GRAFANA_URL" ]; then
|
||||
SUMMARY_ARGS="$SUMMARY_ARGS --grafana-url $GRAFANA_URL"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
|
||||
|
||||
@@ -865,8 +1069,14 @@ jobs:
|
||||
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
|
||||
run: |
|
||||
CHART_ARGS="--output-dir $BENCH_WORK_DIR/charts"
|
||||
CHART_ARGS="$CHART_ARGS --feature $BENCH_WORK_DIR/feature-1/combined_latency.csv $BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
CHART_ARGS="$CHART_ARGS --baseline $BENCH_WORK_DIR/baseline-1/combined_latency.csv $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
FEATURE_CSVS="$BENCH_WORK_DIR/feature-1/combined_latency.csv"
|
||||
BASELINE_CSVS="$BENCH_WORK_DIR/baseline-1/combined_latency.csv"
|
||||
if [ "${BENCH_ABBA:-true}" = "true" ]; then
|
||||
FEATURE_CSVS="$FEATURE_CSVS $BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
BASELINE_CSVS="$BASELINE_CSVS $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
fi
|
||||
CHART_ARGS="$CHART_ARGS --feature $FEATURE_CSVS"
|
||||
CHART_ARGS="$CHART_ARGS --baseline $BASELINE_CSVS"
|
||||
CHART_ARGS="$CHART_ARGS --baseline-name ${BASELINE_NAME}"
|
||||
CHART_ARGS="$CHART_ARGS --feature-name ${FEATURE_NAME}"
|
||||
# shellcheck disable=SC2086
|
||||
@@ -874,7 +1084,7 @@ jobs:
|
||||
|
||||
- name: Upload results
|
||||
if: "!cancelled()"
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bench-reth-results
|
||||
path: ${{ env.BENCH_WORK_DIR }}
|
||||
@@ -906,7 +1116,7 @@ jobs:
|
||||
rm -rf "${TMP_DIR}"
|
||||
|
||||
- name: Compare & comment
|
||||
if: success()
|
||||
if: success() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
@@ -942,7 +1152,8 @@ jobs:
|
||||
|
||||
// Samply profile links (URLs point directly to Firefox Profiler)
|
||||
if (process.env.BENCH_SAMPLY === 'true') {
|
||||
const runs = ['baseline-1', 'feature-1', 'feature-2', 'baseline-2'];
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const runs = abba ? ['baseline-1', 'feature-1', 'feature-2', 'baseline-2'] : ['baseline-1', 'feature-1'];
|
||||
const links = [];
|
||||
for (const run of runs) {
|
||||
try {
|
||||
@@ -957,6 +1168,12 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
// Grafana dashboard link
|
||||
const grafanaUrl = '${{ steps.metrics.outputs.grafana-url }}';
|
||||
if (grafanaUrl) {
|
||||
comment += `\n\n### Grafana Dashboard\n\n[View real-time metrics](${grafanaUrl})\n`;
|
||||
}
|
||||
|
||||
// Node errors (panics / ERROR logs)
|
||||
try {
|
||||
const errors = fs.readFileSync(process.env.BENCH_WORK_DIR + '/errors.md', 'utf8');
|
||||
@@ -982,7 +1199,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Send Slack notification (success)
|
||||
if: success()
|
||||
if: success() && env.BENCH_NO_SLACK != 'true'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
@@ -998,12 +1215,13 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const steps_status = [
|
||||
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}'],
|
||||
['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
];
|
||||
const failed = steps_status.find(([, o]) => o === 'failure');
|
||||
const failedStep = failed ? failed[0] : 'unknown step';
|
||||
@@ -1032,12 +1250,13 @@ jobs:
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
with:
|
||||
script: |
|
||||
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
|
||||
const steps_status = [
|
||||
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}'],
|
||||
['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
];
|
||||
const failed = steps_status.find(([, o]) => o === 'failure');
|
||||
const failedStep = failed ? failed[0] : 'unknown step';
|
||||
|
||||
2
.github/workflows/docker-test.yml
vendored
2
.github/workflows/docker-test.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
*.dockerfile=Dockerfile
|
||||
|
||||
- name: Upload reth image
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
|
||||
49
.github/workflows/docker.yml
vendored
49
.github/workflows/docker.yml
vendored
@@ -28,12 +28,30 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
disable_pgo:
|
||||
description: "Disable PGO profiling entirely"
|
||||
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.disable_pgo)
|
||||
uses: ./.github/workflows/pgo-profile.yml
|
||||
with:
|
||||
pgo_blocks: ${{ github.event_name == 'workflow_dispatch' && inputs.pgo_blocks || '20' }}
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
if: github.repository == 'paradigmxyz/reth' && !failure() && !cancelled()
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-24.04
|
||||
needs: collect-pgo-profile
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@@ -45,7 +63,7 @@ jobs:
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -58,6 +76,30 @@ 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.disable_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.disable_pgo }}" == "true" ]]; then
|
||||
echo "use_pgo_bolt=false" >> "$GITHUB_OUTPUT"
|
||||
echo "pgo_profdata=" >> "$GITHUB_OUTPUT"
|
||||
echo "PGO disabled via workflow input"
|
||||
else
|
||||
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"
|
||||
fi
|
||||
|
||||
- name: Determine build parameters
|
||||
id: params
|
||||
run: |
|
||||
@@ -107,6 +149,9 @@ 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:
|
||||
|
||||
6
.github/workflows/hive.yml
vendored
6
.github/workflows/hive.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
chmod +x hive
|
||||
|
||||
- name: Upload hive assets
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hive_assets
|
||||
path: ./hive_assets
|
||||
@@ -198,13 +198,13 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hive_assets
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: reth-${{ matrix.storage }}
|
||||
path: /tmp
|
||||
|
||||
2
.github/workflows/kurtosis.yml
vendored
2
.github/workflows/kurtosis.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: artifacts
|
||||
path: /tmp
|
||||
|
||||
107
.github/workflows/pgo-profile.yml
vendored
Normal file
107
.github/workflows/pgo-profile.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
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 pkill -9 reth || true
|
||||
sleep 1
|
||||
if mountpoint -q "$SCHELK_MOUNT"; then
|
||||
sudo umount -l "$SCHELK_MOUNT" || true
|
||||
sudo schelk recover -y || true
|
||||
fi
|
||||
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: |
|
||||
if mountpoint -q "$SCHELK_MOUNT"; then
|
||||
sudo umount -l "$SCHELK_MOUNT" || true
|
||||
sudo schelk recover -y || true
|
||||
fi
|
||||
30
.github/workflows/pr-audit.yml
vendored
Normal file
30
.github/workflows/pr-audit.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Pull request audit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'cyclops'
|
||||
steps:
|
||||
- name: Publish event
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "${{ secrets.EVENTS_KEY }}" > ${{ runner.temp }}/key
|
||||
echo "${{ secrets.EVENTS_CERT }}" > ${{ runner.temp }}/cert
|
||||
|
||||
curl -sf -o /dev/null -X POST ${{ secrets.EVENTS_ARGS }} \
|
||||
-H "Content-Type: application/json" \
|
||||
--key ${{ runner.temp }}/key \
|
||||
--cert ${{ runner.temp }}/cert \
|
||||
-d '{
|
||||
"repository": "${{ github.repository }}",
|
||||
"event": "pr_audit",
|
||||
"data": {
|
||||
"pr_number": ${{ github.event.pull_request.number }},
|
||||
"sha": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
}'
|
||||
2
.github/workflows/release-reproducible.yml
vendored
2
.github/workflows/release-reproducible.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
110
.github/workflows/release.yml
vendored
110
.github/workflows/release.yml
vendored
@@ -13,6 +13,10 @@ on:
|
||||
description: "Enable dry run mode (builds artifacts but skips uploads and release creation)"
|
||||
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
|
||||
@@ -37,7 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version
|
||||
run: echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
run: echo "VERSION=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT
|
||||
id: extract_version
|
||||
outputs:
|
||||
VERSION: ${{ steps.extract_version.outputs.VERSION }}
|
||||
@@ -69,22 +73,22 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
configs:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-24.04-arm
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: ""
|
||||
native: true
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-14
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: "-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-14
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: ""
|
||||
build:
|
||||
- command: build
|
||||
binary: reth
|
||||
@@ -96,6 +100,7 @@ jobs:
|
||||
target: ${{ matrix.configs.target }}
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- name: Install cross main
|
||||
if: ${{ !matrix.configs.native }}
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --locked --git https://github.com/cross-rs/cross
|
||||
@@ -110,7 +115,12 @@ jobs:
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Reth
|
||||
run: make PROFILE=${{ matrix.configs.profile }} ${{ matrix.build.command }}-${{ matrix.configs.target }}
|
||||
run: |
|
||||
if [ "${{ matrix.configs.native }}" = "true" ]; then
|
||||
make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-native-${{ matrix.configs.target }}
|
||||
else
|
||||
make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-${{ matrix.configs.target }}
|
||||
fi
|
||||
- name: Move binary
|
||||
run: |
|
||||
mkdir artifacts
|
||||
@@ -131,22 +141,102 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
|
||||
- name: Upload signature
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
|
||||
collect-pgo-profile:
|
||||
uses: ./.github/workflows/pgo-profile.yml
|
||||
with:
|
||||
pgo_blocks: ${{ github.event_name == 'workflow_dispatch' && inputs.pgo_blocks || '20' }}
|
||||
secrets: inherit
|
||||
|
||||
build-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@v6
|
||||
with:
|
||||
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
|
||||
- name: Upload signature
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
|
||||
path: reth-${{ needs.extract-version.outputs.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc
|
||||
|
||||
draft-release:
|
||||
name: draft release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, extract-version]
|
||||
needs: [build, build-pgo, extract-version]
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
env:
|
||||
VERSION: ${{ needs.extract-version.outputs.VERSION }}
|
||||
@@ -160,7 +250,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
- name: Generate full changelog
|
||||
id: changelog
|
||||
run: |
|
||||
|
||||
6
.github/workflows/reproducible-build.yml
vendored
6
.github/workflows/reproducible-build.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
|
||||
|
||||
- name: Upload the hash
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: checksum-${{ matrix.machine }}
|
||||
path: |
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts from machine-1
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: checksum-machine-1
|
||||
path: machine-1/
|
||||
- name: Download artifacts from machine-2
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: checksum-machine-2
|
||||
path: machine-2/
|
||||
|
||||
15
.github/workflows/stage.yml
vendored
15
.github/workflows/stage.yml
vendored
@@ -51,15 +51,12 @@ jobs:
|
||||
- name: Run execution stage
|
||||
run: |
|
||||
reth stage run execution --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run account-hashing stage
|
||||
run: |
|
||||
reth stage run account-hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run storage hashing stage
|
||||
run: |
|
||||
reth stage run storage-hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run hashing stage
|
||||
run: |
|
||||
reth stage run hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
# NOTE: account-hashing, storage-hashing, and hashing stages are omitted.
|
||||
# With storage v2 (now default), these stages are no-ops because the
|
||||
# execution stage writes directly to HashedAccounts/HashedStorages.
|
||||
# Running them here is harmful: `stage run` unwinds before executing,
|
||||
# and the unwind reverts the hashed state that execution wrote, but
|
||||
# the no-op execute never restores it — causing merkle to fail.
|
||||
- name: Run merkle stage
|
||||
run: |
|
||||
reth stage run merkle --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
|
||||
72
CLAUDE.md
72
CLAUDE.md
@@ -191,6 +191,78 @@ The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating t
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
#### Titles
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
|
||||
|
||||
Examples:
|
||||
- `fix(rpc): correct gas estimation for ERC-20 transfers`
|
||||
- `perf: batch trie updates to reduce cursor overhead`
|
||||
- `feat(engine): add new_payload_interval metric`
|
||||
|
||||
#### Descriptions
|
||||
|
||||
Keep it short. Say what changed and why — nothing more.
|
||||
|
||||
**Do:**
|
||||
- Write 1–3 sentences summarizing the change
|
||||
- Explain _why_ if the diff doesn't make it obvious
|
||||
- Link related issues or EIPs
|
||||
- Include benchmark numbers for perf changes
|
||||
|
||||
**Don't:**
|
||||
- List every file changed — that's what the diff is for
|
||||
- Repeat the title in the body
|
||||
- Add "Files changed" or "Changes" sections
|
||||
- Write walls of text that go stale when the diff is updated
|
||||
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
Closes #<issue>
|
||||
|
||||
<what changed, 1-3 sentences>
|
||||
|
||||
<why, if not obvious from the diff>
|
||||
```
|
||||
|
||||
**Good example:**
|
||||
|
||||
```
|
||||
Closes #16800
|
||||
|
||||
Adds fallback for external IP resolution so node startup doesn't fail
|
||||
when STUN is unreachable. Falls back to the configured default.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```
|
||||
## Summary
|
||||
This PR introduces comprehensive improvements to the IP resolution system.
|
||||
|
||||
## Changes
|
||||
- Modified `crates/net/discv4/src/lib.rs` to add fallback
|
||||
- Modified `crates/net/discv4/src/config.rs` to add default IP
|
||||
- Added tests in `crates/net/discv4/src/tests/ip.rs`
|
||||
|
||||
## Files Changed
|
||||
- crates/net/discv4/src/lib.rs
|
||||
- crates/net/discv4/src/config.rs
|
||||
- crates/net/discv4/src/tests/ip.rs
|
||||
```
|
||||
|
||||
#### Labels and CI
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
|
||||
1037
Cargo.lock
generated
1037
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace.package]
|
||||
version = "1.11.1"
|
||||
version = "1.11.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.93"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -170,6 +170,7 @@ rust.rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
rust.unreachable_pub = "warn"
|
||||
rust.unused_must_use = "deny"
|
||||
rust.rust_2024_incompatible_pat = "warn"
|
||||
rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||
rustdoc.all = "warn"
|
||||
# rust.unnameable-types = "warn"
|
||||
|
||||
@@ -436,15 +437,15 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "34.0.0", default-features = false }
|
||||
revm-bytecode = { version = "8.0.0", default-features = false }
|
||||
revm-database = { version = "10.0.0", default-features = false }
|
||||
revm-state = { version = "9.0.0", default-features = false }
|
||||
revm-primitives = { version = "22.0.0", default-features = false }
|
||||
revm-interpreter = { version = "32.0.0", default-features = false }
|
||||
revm-database-interface = { version = "9.0.0", default-features = false }
|
||||
op-revm = { version = "15.0.0", default-features = false }
|
||||
revm-inspectors = "0.34.2"
|
||||
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 }
|
||||
op-revm = { version = "17.0.0", default-features = false }
|
||||
revm-inspectors = "0.36.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -454,7 +455,7 @@ alloy-sol-types = { version = "1.5.6", default-features = false }
|
||||
alloy-chains = { version = "0.2.5", default-features = false }
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.0", default-features = false }
|
||||
alloy-evm = { version = "0.27.2", default-features = false }
|
||||
alloy-evm = { version = "0.29.2", default-features = false }
|
||||
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
|
||||
alloy-trie = { version = "0.9.4", default-features = false }
|
||||
|
||||
@@ -489,14 +490,9 @@ alloy-transport-ipc = { version = "1.7.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.7.3", default-features = false }
|
||||
|
||||
# op
|
||||
alloy-op-evm = { version = "0.27.2", default-features = false }
|
||||
alloy-op-hardforks = "0.4.4"
|
||||
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
|
||||
op-alloy-network = { version = "0.23.1", default-features = false }
|
||||
op-alloy-consensus = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-jsonrpsee = { version = "0.23.1", default-features = false }
|
||||
op-alloy-flz = { version = "0.13.1", default-features = false }
|
||||
op-alloy-rpc-types = { version = "0.24.0", default-features = false }
|
||||
op-alloy-rpc-types-engine = { version = "0.24.0", default-features = false }
|
||||
op-alloy-consensus = { version = "0.24.0", default-features = false }
|
||||
|
||||
# misc
|
||||
either = { version = "1.15.0", default-features = false }
|
||||
@@ -508,6 +504,7 @@ bincode = "1.3"
|
||||
bitflags = "2.4"
|
||||
boyer-moore-magiclen = "0.2.16"
|
||||
bytes = { version = "1.11.1", default-features = false }
|
||||
blake3 = "1.8"
|
||||
brotli = "8"
|
||||
cfg-if = "1.0"
|
||||
clap = "4"
|
||||
@@ -523,6 +520,7 @@ humantime = "2.1"
|
||||
humantime-serde = "1.1"
|
||||
itertools = { version = "0.14", default-features = false }
|
||||
linked_hash_set = "0.1"
|
||||
libc = "0.2"
|
||||
lz4 = "1.28.1"
|
||||
modular-bitfield = "0.13.1"
|
||||
notify = { version = "8.0.0", default-features = false, features = ["macos_fsevent"] }
|
||||
@@ -541,6 +539,8 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
|
||||
serde_with = { version = "3", default-features = false, features = ["macros"] }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
shlex = "1.3"
|
||||
# https://github.com/orlp/slotmap/pull/148
|
||||
slotmap = { git = "https://github.com/DaniPopes/slotmap.git", branch = "dani/shrink-methods" }
|
||||
smallvec = "1"
|
||||
strum = { version = "0.27", default-features = false }
|
||||
strum_macros = "0.27"
|
||||
@@ -665,6 +665,7 @@ cipher = "0.4.3"
|
||||
comfy-table = "7.0"
|
||||
concat-kdf = "0.1.0"
|
||||
crossbeam-channel = "0.5.13"
|
||||
crossbeam-queue = "0.3"
|
||||
crossbeam-utils = "0.8"
|
||||
crossterm = "0.29.0"
|
||||
csv = "1.3.0"
|
||||
@@ -741,10 +742,8 @@ ipnet = "2.11"
|
||||
# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
|
||||
# op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-jsonrpsee = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
#
|
||||
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "1207e33" }
|
||||
#
|
||||
@@ -754,10 +753,6 @@ ipnet = "2.11"
|
||||
# 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 = "df124c0" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "df124c0" }
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "9bc2dba" }
|
||||
|
||||
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "3020ea8" }
|
||||
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# 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
|
||||
@@ -43,6 +45,18 @@ 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
|
||||
@@ -53,12 +67,21 @@ 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 [ -n "$RUSTFLAGS" ]; then \
|
||||
export RUSTFLAGS="$RUSTFLAGS"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
|
||||
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; \
|
||||
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)
|
||||
|
||||
8
Makefile
8
Makefile
@@ -17,6 +17,9 @@ FEATURES ?=
|
||||
# Cargo profile for builds. Default is for local builds, CI uses an override.
|
||||
PROFILE ?= release
|
||||
|
||||
# Extra RUSTFLAGS to append to build targets (e.g., "-C target-cpu=x86-64-v3")
|
||||
EXTRA_RUSTFLAGS ?=
|
||||
|
||||
# Extra flags for Cargo
|
||||
CARGO_INSTALL_EXTRA_FLAGS ?=
|
||||
|
||||
@@ -74,7 +77,7 @@ build-debug: ## Build the reth binary into `target/debug` directory.
|
||||
cargo build --bin reth --features "$(FEATURES)"
|
||||
# Builds the reth binary natively.
|
||||
build-native-%:
|
||||
cargo build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
$(if $(EXTRA_RUSTFLAGS),RUSTFLAGS="$(EXTRA_RUSTFLAGS)") cargo build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
|
||||
# The following commands use `cross` to build a cross-compile.
|
||||
#
|
||||
@@ -92,11 +95,12 @@ build-native-%:
|
||||
# on other systems. JEMALLOC_SYS_WITH_LG_PAGE=16 tells jemalloc to use 64-KiB
|
||||
# pages. See: https://github.com/paradigmxyz/reth/issues/6742
|
||||
build-aarch64-unknown-linux-gnu: export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
build-native-aarch64-unknown-linux-gnu: export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
|
||||
# Note: The additional rustc compiler flags are for intrinsics needed by MDBX.
|
||||
# See: https://github.com/cross-rs/cross/wiki/FAQ#undefined-reference-with-build-std
|
||||
build-%:
|
||||
RUSTFLAGS="-C link-arg=-lgcc -Clink-arg=-static-libgcc" \
|
||||
RUSTFLAGS="-C link-arg=-lgcc -Clink-arg=-static-libgcc $(EXTRA_RUSTFLAGS)" \
|
||||
cross build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
|
||||
# Unfortunately we can't easily use cross to build for Darwin because of licensing issues.
|
||||
|
||||
@@ -7,7 +7,7 @@ use alloy_primitives::address;
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::JwtSecret;
|
||||
use alloy_transport::layers::RetryBackoffLayer;
|
||||
use alloy_transport::layers::{RateLimitRetryPolicy, RetryBackoffLayer};
|
||||
use reqwest::Url;
|
||||
use reth_node_core::args::BenchmarkArgs;
|
||||
use tracing::info;
|
||||
@@ -31,6 +31,8 @@ pub(crate) struct BenchContext {
|
||||
pub(crate) is_optimism: bool,
|
||||
/// Whether to use `reth_newPayload` endpoint instead of `engine_newPayload*`.
|
||||
pub(crate) use_reth_namespace: bool,
|
||||
/// Whether to fetch and replay RLP-encoded blocks.
|
||||
pub(crate) rlp_blocks: bool,
|
||||
}
|
||||
|
||||
impl BenchContext {
|
||||
@@ -51,9 +53,15 @@ impl BenchContext {
|
||||
}
|
||||
}
|
||||
|
||||
// set up alloy client for blocks
|
||||
// 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)
|
||||
});
|
||||
let client = ClientBuilder::default()
|
||||
.layer(RetryBackoffLayer::new(10, 800, u64::MAX))
|
||||
.layer(RetryBackoffLayer::new_with_policy(10, 800, u64::MAX, retry_policy))
|
||||
.http(rpc_url.parse()?);
|
||||
let block_provider = RootProvider::<AnyNetwork>::new(client);
|
||||
|
||||
@@ -86,9 +94,11 @@ impl BenchContext {
|
||||
|
||||
// Computes the block range for the benchmark.
|
||||
//
|
||||
// - If `--advance` is provided, fetches the latest block and sets:
|
||||
// - If `--advance` is provided, fetches the latest block from the engine and sets:
|
||||
// - `from = head + 1`
|
||||
// - `to = head + advance`
|
||||
// - If only `--to` is provided, fetches the latest block from the engine and sets:
|
||||
// - `from = head`
|
||||
// - Otherwise, uses the values from `--from` and `--to`.
|
||||
let (from, to) = if let Some(advance) = bench_args.advance {
|
||||
if advance == 0 {
|
||||
@@ -101,6 +111,14 @@ impl BenchContext {
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block for --advance"))?;
|
||||
let head_number = head_block.header.number;
|
||||
(Some(head_number), Some(head_number + advance))
|
||||
} else if bench_args.from.is_none() && bench_args.to.is_some() {
|
||||
let head_block = auth_provider
|
||||
.get_block_by_number(BlockNumberOrTag::Latest)
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block from engine"))?;
|
||||
let head_number = head_block.header.number;
|
||||
info!(target: "reth-bench", "No --from provided, derived from engine head: {}", head_number);
|
||||
(Some(head_number), bench_args.to)
|
||||
} else {
|
||||
(bench_args.from, bench_args.to)
|
||||
};
|
||||
@@ -112,7 +130,7 @@ impl BenchContext {
|
||||
.full()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block from RPC"))?;
|
||||
let mut benchmark_mode = BenchMode::new(from, to, latest_block.into_inner().number())?;
|
||||
let mut benchmark_mode = BenchMode::new(from, to, latest_block.into_inner().number());
|
||||
|
||||
let first_block = match benchmark_mode {
|
||||
BenchMode::Continuous(start) => {
|
||||
@@ -142,7 +160,8 @@ impl BenchContext {
|
||||
};
|
||||
|
||||
let next_block = first_block.header.number + 1;
|
||||
let use_reth_namespace = bench_args.reth_new_payload;
|
||||
let rlp_blocks = bench_args.rlp_blocks;
|
||||
let use_reth_namespace = bench_args.reth_new_payload || rlp_blocks;
|
||||
Ok(Self {
|
||||
auth_provider,
|
||||
block_provider,
|
||||
@@ -150,6 +169,7 @@ impl BenchContext {
|
||||
next_block,
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use reth_chainspec::ChainSpec;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_ethereum_primitives::TransactionSigned;
|
||||
use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK};
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use tracing::info;
|
||||
|
||||
@@ -184,34 +185,32 @@ impl Command {
|
||||
Some(new_payload_version),
|
||||
)?;
|
||||
|
||||
let (version, params) = if self.reth_new_payload {
|
||||
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?)
|
||||
} else {
|
||||
(Some(version), params)
|
||||
};
|
||||
|
||||
// Save payload to file with version info for replay
|
||||
let payload_path =
|
||||
self.output.join(format!("payload_block_{}.json", block.header.number));
|
||||
let file = GasRampPayloadFile {
|
||||
version: version as u8,
|
||||
version: version.map(|v| v as u8),
|
||||
block_hash,
|
||||
params: params.clone(),
|
||||
execution_data: Some(execution_data.clone()),
|
||||
};
|
||||
let payload_json = serde_json::to_string_pretty(&file)?;
|
||||
std::fs::write(&payload_path, &payload_json)?;
|
||||
info!(target: "reth-bench", block_number = block.header.number, path = %payload_path.display(), "Saved payload");
|
||||
|
||||
let reth_data = self.reth_new_payload.then_some(execution_data);
|
||||
let _ = call_new_payload_with_reth(&provider, version, params, reth_data).await?;
|
||||
let _ = call_new_payload_with_reth(&provider, version, params).await?;
|
||||
|
||||
let forkchoice_state = ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash: block_hash,
|
||||
finalized_block_hash: block_hash,
|
||||
};
|
||||
call_forkchoice_updated_with_reth(
|
||||
&provider,
|
||||
version,
|
||||
forkchoice_state,
|
||||
self.reth_new_payload,
|
||||
)
|
||||
.await?;
|
||||
call_forkchoice_updated_with_reth(&provider, version, forkchoice_state).await?;
|
||||
|
||||
parent_header = block.header;
|
||||
parent_hash = block_hash;
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
block_to_new_payload, call_forkchoice_updated_with_reth, call_new_payload_with_reth,
|
||||
},
|
||||
};
|
||||
use alloy_provider::Provider;
|
||||
use alloy_provider::{ext::DebugApi, Provider};
|
||||
use alloy_rpc_types_engine::ForkchoiceState;
|
||||
use clap::Parser;
|
||||
use eyre::{Context, OptionExt};
|
||||
@@ -154,6 +154,7 @@ impl Command {
|
||||
mut next_block,
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let total_blocks = benchmark_mode.total_blocks();
|
||||
@@ -186,6 +187,21 @@ impl Command {
|
||||
}
|
||||
};
|
||||
|
||||
let rlp = if rlp_blocks {
|
||||
let rlp = match block_provider.debug_get_raw_block(next_block.into()).await {
|
||||
Ok(rlp) => rlp,
|
||||
Err(e) => {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}: {e}");
|
||||
let _ = error_sender
|
||||
.send(eyre::eyre!("Failed to fetch raw block {next_block}: {e}"));
|
||||
break;
|
||||
}
|
||||
};
|
||||
Some(rlp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let head_block_hash = block.header.hash;
|
||||
let safe_block_hash = block_provider
|
||||
.get_block_by_number(block.header.number.saturating_sub(32).into());
|
||||
@@ -207,7 +223,7 @@ impl Command {
|
||||
|
||||
next_block += 1;
|
||||
if let Err(e) = sender
|
||||
.send((block, head_block_hash, safe_block_hash, finalized_block_hash))
|
||||
.send((block, head_block_hash, safe_block_hash, finalized_block_hash, rlp))
|
||||
.await
|
||||
{
|
||||
tracing::error!(target: "reth-bench", "Failed to send block data: {e}");
|
||||
@@ -221,7 +237,7 @@ impl Command {
|
||||
let total_benchmark_duration = Instant::now();
|
||||
let mut total_wait_time = Duration::ZERO;
|
||||
|
||||
while let Some((block, head, safe, finalized)) = {
|
||||
while let Some((block, head, safe, finalized, rlp)) = {
|
||||
let wait_start = Instant::now();
|
||||
let result = receiver.recv().await;
|
||||
total_wait_time += wait_start.elapsed();
|
||||
@@ -240,11 +256,11 @@ impl Command {
|
||||
finalized_block_hash: finalized,
|
||||
};
|
||||
|
||||
let (version, params, execution_data) = block_to_new_payload(block, is_optimism)?;
|
||||
let (version, params) =
|
||||
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
|
||||
let start = Instant::now();
|
||||
let reth_data = use_reth_namespace.then_some(execution_data);
|
||||
let server_timings =
|
||||
call_new_payload_with_reth(&auth_provider, version, params, reth_data).await?;
|
||||
call_new_payload_with_reth(&auth_provider, version, params).await?;
|
||||
|
||||
let np_latency =
|
||||
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
|
||||
@@ -263,13 +279,7 @@ impl Command {
|
||||
};
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
call_forkchoice_updated_with_reth(
|
||||
&auth_provider,
|
||||
version,
|
||||
forkchoice_state,
|
||||
use_reth_namespace,
|
||||
)
|
||||
.await?;
|
||||
call_forkchoice_updated_with_reth(&auth_provider, version, forkchoice_state).await?;
|
||||
let fcu_latency = fcu_start.elapsed();
|
||||
|
||||
let total_latency = if server_timings.is_some() {
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
},
|
||||
valid_payload::{block_to_new_payload, call_new_payload_with_reth},
|
||||
};
|
||||
use alloy_provider::Provider;
|
||||
use alloy_provider::{ext::DebugApi, Provider};
|
||||
use clap::Parser;
|
||||
use csv::Writer;
|
||||
use eyre::{Context, OptionExt};
|
||||
@@ -51,6 +51,7 @@ impl Command {
|
||||
mut next_block,
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let total_blocks = benchmark_mode.total_blocks();
|
||||
@@ -83,8 +84,21 @@ impl Command {
|
||||
}
|
||||
};
|
||||
|
||||
let rlp = if rlp_blocks {
|
||||
let Ok(rlp) = block_provider.debug_get_raw_block(next_block.into()).await
|
||||
else {
|
||||
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}");
|
||||
let _ = error_sender
|
||||
.send(eyre::eyre!("Failed to fetch raw block {next_block}"));
|
||||
break;
|
||||
};
|
||||
Some(rlp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
next_block += 1;
|
||||
if let Err(e) = sender.send(block).await {
|
||||
if let Err(e) = sender.send((block, rlp)).await {
|
||||
tracing::error!(target: "reth-bench", "Failed to send block data: {e}");
|
||||
break;
|
||||
}
|
||||
@@ -96,7 +110,7 @@ impl Command {
|
||||
let total_benchmark_duration = Instant::now();
|
||||
let mut total_wait_time = Duration::ZERO;
|
||||
|
||||
while let Some(block) = {
|
||||
while let Some((block, rlp)) = {
|
||||
let wait_start = Instant::now();
|
||||
let result = receiver.recv().await;
|
||||
total_wait_time += wait_start.elapsed();
|
||||
@@ -108,12 +122,12 @@ impl Command {
|
||||
|
||||
debug!(target: "reth-bench", number=?block.header.number, "Sending payload to engine");
|
||||
|
||||
let (version, params, execution_data) = block_to_new_payload(block, is_optimism)?;
|
||||
let (version, params) =
|
||||
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
|
||||
|
||||
let start = Instant::now();
|
||||
let reth_data = use_reth_namespace.then_some(execution_data);
|
||||
let server_timings =
|
||||
call_new_payload_with_reth(&auth_provider, version, params, reth_data).await?;
|
||||
call_new_payload_with_reth(&auth_provider, version, params).await?;
|
||||
|
||||
let latency =
|
||||
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
|
||||
|
||||
@@ -22,14 +22,14 @@ pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct GasRampPayloadFile {
|
||||
/// Engine API version (1-5).
|
||||
pub(crate) version: u8,
|
||||
///
|
||||
/// `None` indicates that `reth_newPayload` should be used.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) version: Option<u8>,
|
||||
/// The block hash for FCU.
|
||||
pub(crate) block_hash: B256,
|
||||
/// The params to pass to newPayload.
|
||||
pub(crate) params: serde_json::Value,
|
||||
/// The execution data for `reth_newPayload`.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub(crate) execution_data: Option<alloy_rpc_types_engine::ExecutionData>,
|
||||
}
|
||||
|
||||
/// This represents the results of a single `newPayload` call in the benchmark, containing the gas
|
||||
@@ -114,16 +114,27 @@ impl CombinedResult {
|
||||
|
||||
impl std::fmt::Display for CombinedResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let np = &self.new_payload_result;
|
||||
write!(
|
||||
f,
|
||||
"Block {} processed at {:.4} Ggas/s, used {} total gas. Combined: {:.4} Ggas/s. fcu: {:?}, newPayload: {:?}",
|
||||
self.block_number,
|
||||
self.new_payload_result.gas_per_second() / GIGAGAS as f64,
|
||||
self.new_payload_result.gas_used,
|
||||
np.gas_per_second() / GIGAGAS as f64,
|
||||
np.gas_used,
|
||||
self.combined_gas_per_second() / GIGAGAS as f64,
|
||||
self.fcu_latency,
|
||||
self.new_payload_result.latency
|
||||
)
|
||||
np.latency,
|
||||
)?;
|
||||
if !np.execution_cache_wait.is_zero() {
|
||||
write!(f, ", execution cache wait: {:?}", np.execution_cache_wait)?;
|
||||
}
|
||||
if !np.sparse_trie_wait.is_zero() {
|
||||
write!(f, ", trie cache wait: {:?}", np.sparse_trie_wait)?;
|
||||
}
|
||||
if let Some(d) = np.persistence_wait {
|
||||
write!(f, ", persistence wait: {d:?}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use eyre::Context;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
@@ -161,7 +162,9 @@ struct GasRampPayload {
|
||||
/// Block number from filename.
|
||||
block_number: u64,
|
||||
/// Engine API version for newPayload.
|
||||
version: EngineApiMessageVersion,
|
||||
///
|
||||
/// `None` indicates that `reth_newPayload` should be used.
|
||||
version: Option<EngineApiMessageVersion>,
|
||||
/// The file contents.
|
||||
file: GasRampPayloadFile,
|
||||
}
|
||||
@@ -273,13 +276,10 @@ impl Command {
|
||||
"Executing gas ramp payload (newPayload + FCU)"
|
||||
);
|
||||
|
||||
let reth_data =
|
||||
if self.reth_new_payload { payload.file.execution_data.clone() } else { None };
|
||||
let _ = call_new_payload_with_reth(
|
||||
&auth_provider,
|
||||
payload.version,
|
||||
payload.file.params.clone(),
|
||||
reth_data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -288,20 +288,10 @@ impl Command {
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
call_forkchoice_updated_with_reth(
|
||||
&auth_provider,
|
||||
payload.version,
|
||||
fcu_state,
|
||||
self.reth_new_payload,
|
||||
)
|
||||
.await?;
|
||||
call_forkchoice_updated_with_reth(&auth_provider, payload.version, fcu_state).await?;
|
||||
|
||||
info!(target: "reth-bench", gas_ramp_payload = i + 1, "Gas ramp payload executed successfully");
|
||||
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(payload.block_number).await?;
|
||||
}
|
||||
|
||||
parent_hash = payload.file.block_hash;
|
||||
}
|
||||
|
||||
@@ -342,31 +332,34 @@ impl Command {
|
||||
"Sending newPayload"
|
||||
);
|
||||
|
||||
let params = serde_json::to_value((
|
||||
execution_payload.clone(),
|
||||
Vec::<B256>::new(),
|
||||
B256::ZERO,
|
||||
envelope.execution_requests.to_vec(),
|
||||
))?;
|
||||
let (version, params) = if self.reth_new_payload {
|
||||
let reth_data = ExecutionData {
|
||||
payload: execution_payload.clone().into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
parent_beacon_block_root: B256::ZERO,
|
||||
},
|
||||
PraguePayloadFields {
|
||||
requests: envelope.execution_requests.clone().into(),
|
||||
},
|
||||
),
|
||||
};
|
||||
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(reth_data),))?)
|
||||
} else {
|
||||
(
|
||||
Some(EngineApiMessageVersion::V4),
|
||||
serde_json::to_value((
|
||||
execution_payload.clone(),
|
||||
Vec::<B256>::new(),
|
||||
B256::ZERO,
|
||||
envelope.execution_requests.to_vec(),
|
||||
))?,
|
||||
)
|
||||
};
|
||||
|
||||
let reth_data = self.reth_new_payload.then(|| ExecutionData {
|
||||
payload: execution_payload.clone().into(),
|
||||
sidecar: ExecutionPayloadSidecar::v4(
|
||||
CancunPayloadFields {
|
||||
versioned_hashes: Vec::new(),
|
||||
parent_beacon_block_root: B256::ZERO,
|
||||
},
|
||||
PraguePayloadFields { requests: envelope.execution_requests.clone().into() },
|
||||
),
|
||||
});
|
||||
|
||||
let server_timings = call_new_payload_with_reth(
|
||||
&auth_provider,
|
||||
EngineApiMessageVersion::V4,
|
||||
params,
|
||||
reth_data,
|
||||
)
|
||||
.await?;
|
||||
let server_timings =
|
||||
call_new_payload_with_reth(&auth_provider, version, params).await?;
|
||||
|
||||
let np_latency =
|
||||
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
|
||||
@@ -391,13 +384,7 @@ impl Command {
|
||||
};
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
call_forkchoice_updated_with_reth(
|
||||
&auth_provider,
|
||||
EngineApiMessageVersion::V4,
|
||||
fcu_state,
|
||||
self.reth_new_payload,
|
||||
)
|
||||
.await?;
|
||||
call_forkchoice_updated_with_reth(&auth_provider, version, fcu_state).await?;
|
||||
let fcu_latency = fcu_start.elapsed();
|
||||
|
||||
let total_latency =
|
||||
@@ -558,13 +545,18 @@ impl Command {
|
||||
let file: GasRampPayloadFile = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
|
||||
let version = match file.version {
|
||||
1 => EngineApiMessageVersion::V1,
|
||||
2 => EngineApiMessageVersion::V2,
|
||||
3 => EngineApiMessageVersion::V3,
|
||||
4 => EngineApiMessageVersion::V4,
|
||||
5 => EngineApiMessageVersion::V5,
|
||||
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
|
||||
let version = if let Some(version) = file.version {
|
||||
match version {
|
||||
1 => EngineApiMessageVersion::V1,
|
||||
2 => EngineApiMessageVersion::V2,
|
||||
3 => EngineApiMessageVersion::V3,
|
||||
4 => EngineApiMessageVersion::V4,
|
||||
5 => EngineApiMessageVersion::V5,
|
||||
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
info!(
|
||||
|
||||
@@ -34,17 +34,16 @@ impl BenchMode {
|
||||
}
|
||||
|
||||
/// Create a [`BenchMode`] from optional `from` and `to` fields.
|
||||
pub fn new(from: Option<u64>, to: Option<u64>, latest_block: u64) -> Result<Self, eyre::Error> {
|
||||
///
|
||||
/// If only `--to` is provided, `from` is derived as `latest_block + 1`.
|
||||
pub const fn new(from: Option<u64>, to: Option<u64>, latest_block: u64) -> Self {
|
||||
// If neither `--from` nor `--to` are provided, we will run the benchmark continuously,
|
||||
// starting at the latest block.
|
||||
match (from, to) {
|
||||
(Some(from), Some(to)) => Ok(Self::Range(from..=to)),
|
||||
(None, None) => Ok(Self::Continuous(latest_block)),
|
||||
(Some(start), None) => Ok(Self::Continuous(start)),
|
||||
_ => {
|
||||
// both or neither are allowed, everything else is ambiguous
|
||||
Err(eyre::eyre!("`from` and `to` must be provided together, or not at all."))
|
||||
}
|
||||
(Some(from), Some(to)) => Self::Range(from..=to),
|
||||
(None, None) => Self::Continuous(latest_block),
|
||||
(Some(start), None) => Self::Continuous(start),
|
||||
(None, Some(to)) => Self::Range(latest_block + 1..=to),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! before sending additional calls.
|
||||
|
||||
use alloy_eips::eip7685::Requests;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_provider::{ext::EngineApi, network::AnyRpcBlock, Network, Provider};
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionData, ExecutionPayload, ExecutionPayloadInputV2, ExecutionPayloadSidecar,
|
||||
@@ -12,6 +12,7 @@ use alloy_rpc_types_engine::{
|
||||
use alloy_transport::TransportResult;
|
||||
use op_alloy_rpc_types_engine::OpExecutionPayloadV4;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use reth_rpc_api::RethNewPayloadInput;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error};
|
||||
@@ -169,7 +170,15 @@ where
|
||||
pub(crate) fn block_to_new_payload(
|
||||
block: AnyRpcBlock,
|
||||
is_optimism: bool,
|
||||
) -> eyre::Result<(EngineApiMessageVersion, serde_json::Value, ExecutionData)> {
|
||||
rlp: Option<Bytes>,
|
||||
reth_new_payload: bool,
|
||||
) -> eyre::Result<(Option<EngineApiMessageVersion>, serde_json::Value)> {
|
||||
if let Some(rlp) = rlp {
|
||||
return Ok((
|
||||
None,
|
||||
serde_json::to_value((RethNewPayloadInput::<ExecutionData>::BlockRlp(rlp),))?,
|
||||
));
|
||||
}
|
||||
let block = block
|
||||
.into_inner()
|
||||
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
|
||||
@@ -181,7 +190,14 @@ pub(crate) fn block_to_new_payload(
|
||||
|
||||
// Convert to execution payload
|
||||
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
|
||||
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)
|
||||
let (version, params, execution_data) =
|
||||
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)?;
|
||||
|
||||
if reth_new_payload {
|
||||
Ok((None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?))
|
||||
} else {
|
||||
Ok((Some(version), params))
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an execution payload and sidecar into versioned engine API params and an
|
||||
@@ -219,8 +235,8 @@ pub(crate) fn payload_to_new_payload(
|
||||
))?,
|
||||
)
|
||||
} else {
|
||||
// Extract actual Requests from RequestsOrHash
|
||||
let requests = prague.requests.requests_hash();
|
||||
// Preserve the original RequestsOrHash payload for engine_newPayloadV4.
|
||||
let requests = prague.requests.clone();
|
||||
(
|
||||
version,
|
||||
serde_json::to_value((
|
||||
@@ -266,17 +282,15 @@ pub(crate) fn payload_to_new_payload(
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn call_new_payload<N: Network, P: Provider<N>>(
|
||||
provider: P,
|
||||
version: EngineApiMessageVersion,
|
||||
version: Option<EngineApiMessageVersion>,
|
||||
params: serde_json::Value,
|
||||
) -> TransportResult<Option<NewPayloadTimingBreakdown>> {
|
||||
call_new_payload_with_reth(provider, version, params, None).await
|
||||
) -> eyre::Result<Option<NewPayloadTimingBreakdown>> {
|
||||
call_new_payload_with_reth(provider, version, params).await
|
||||
}
|
||||
|
||||
/// Response from `reth_newPayload` endpoint, which includes server-measured latency.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RethPayloadStatus {
|
||||
#[serde(flatten)]
|
||||
status: PayloadStatus,
|
||||
latency_us: u64,
|
||||
#[serde(default)]
|
||||
persistence_wait_us: Option<u64>,
|
||||
@@ -300,72 +314,50 @@ pub(crate) struct NewPayloadTimingBreakdown {
|
||||
}
|
||||
|
||||
/// Calls either `engine_newPayload*` or `reth_newPayload` depending on whether
|
||||
/// `reth_execution_data` is provided.
|
||||
/// `version` is provided.
|
||||
///
|
||||
/// When `reth_execution_data` is `Some`, uses the `reth_newPayload` endpoint which takes
|
||||
/// `ExecutionData` directly and waits for persistence and cache updates to complete.
|
||||
/// When `version` is `None`, uses `reth_newPayload` endpoint with provided params.
|
||||
///
|
||||
/// Returns the server-reported timing breakdown when using the reth namespace, or `None` for
|
||||
/// the standard engine namespace.
|
||||
pub(crate) async fn call_new_payload_with_reth<N: Network, P: Provider<N>>(
|
||||
provider: P,
|
||||
version: EngineApiMessageVersion,
|
||||
version: Option<EngineApiMessageVersion>,
|
||||
params: serde_json::Value,
|
||||
reth_execution_data: Option<ExecutionData>,
|
||||
) -> TransportResult<Option<NewPayloadTimingBreakdown>> {
|
||||
if let Some(execution_data) = reth_execution_data {
|
||||
let method = "reth_newPayload";
|
||||
let reth_params = serde_json::to_value((execution_data.clone(),))
|
||||
.expect("ExecutionData serialization cannot fail");
|
||||
) -> eyre::Result<Option<NewPayloadTimingBreakdown>> {
|
||||
let method = version.map(|v| v.method_name()).unwrap_or("reth_newPayload");
|
||||
|
||||
debug!(target: "reth-bench", method, "Sending newPayload");
|
||||
debug!(target: "reth-bench", method, "Sending newPayload");
|
||||
|
||||
let mut resp: RethPayloadStatus = provider.client().request(method, &reth_params).await?;
|
||||
let resp = loop {
|
||||
let resp: serde_json::Value = provider.client().request(method, ¶ms).await?;
|
||||
let status = PayloadStatus::deserialize(&resp)?;
|
||||
|
||||
while !resp.status.is_valid() {
|
||||
if resp.status.is_invalid() {
|
||||
error!(target: "reth-bench", status=?resp.status, "Invalid {method}");
|
||||
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
|
||||
std::io::Error::other(format!("Invalid {method}: {:?}", resp.status)),
|
||||
)))
|
||||
}
|
||||
if resp.status.is_syncing() {
|
||||
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
|
||||
"invalid range: no canonical state found for parent of requested block",
|
||||
))
|
||||
}
|
||||
resp = provider.client().request(method, &reth_params).await?;
|
||||
if status.is_valid() {
|
||||
break resp;
|
||||
}
|
||||
|
||||
Ok(Some(NewPayloadTimingBreakdown {
|
||||
latency: Duration::from_micros(resp.latency_us),
|
||||
persistence_wait: resp.persistence_wait_us.map(Duration::from_micros),
|
||||
execution_cache_wait: Duration::from_micros(resp.execution_cache_wait_us),
|
||||
sparse_trie_wait: Duration::from_micros(resp.sparse_trie_wait_us),
|
||||
}))
|
||||
} else {
|
||||
let method = version.method_name();
|
||||
|
||||
debug!(target: "reth-bench", method, "Sending newPayload");
|
||||
|
||||
let mut status: PayloadStatus = provider.client().request(method, ¶ms).await?;
|
||||
|
||||
while !status.is_valid() {
|
||||
if status.is_invalid() {
|
||||
error!(target: "reth-bench", ?status, ?params, "Invalid {method}",);
|
||||
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
|
||||
std::io::Error::other(format!("Invalid {method}: {status:?}")),
|
||||
)))
|
||||
}
|
||||
if status.is_syncing() {
|
||||
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
|
||||
"invalid range: no canonical state found for parent of requested block",
|
||||
))
|
||||
}
|
||||
status = provider.client().request(method, ¶ms).await?;
|
||||
if status.is_invalid() {
|
||||
return Err(eyre::eyre!("Invalid {method}: {status:?}"));
|
||||
}
|
||||
Ok(None)
|
||||
if status.is_syncing() {
|
||||
return Err(eyre::eyre!(
|
||||
"invalid range: no canonical state found for parent of requested block"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if version.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let resp: RethPayloadStatus = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(Some(NewPayloadTimingBreakdown {
|
||||
latency: Duration::from_micros(resp.latency_us),
|
||||
persistence_wait: resp.persistence_wait_us.map(Duration::from_micros),
|
||||
execution_cache_wait: Duration::from_micros(resp.execution_cache_wait_us),
|
||||
sparse_trie_wait: Duration::from_micros(resp.sparse_trie_wait_us),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Calls the correct `engine_forkchoiceUpdated` method depending on the given
|
||||
@@ -403,20 +395,25 @@ pub(crate) async fn call_forkchoice_updated_with_reth<
|
||||
P: Provider<N> + EngineApiValidWaitExt<N>,
|
||||
>(
|
||||
provider: P,
|
||||
message_version: EngineApiMessageVersion,
|
||||
message_version: Option<EngineApiMessageVersion>,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
use_reth: bool,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
if use_reth {
|
||||
if let Some(message_version) = message_version {
|
||||
call_forkchoice_updated(provider, message_version, forkchoice_state, None).await
|
||||
} else {
|
||||
let method = "reth_forkchoiceUpdated";
|
||||
let reth_params = serde_json::to_value((forkchoice_state,))
|
||||
.expect("ForkchoiceState serialization cannot fail");
|
||||
|
||||
debug!(target: "reth-bench", method, "Sending forkchoiceUpdated");
|
||||
|
||||
let mut resp: ForkchoiceUpdated = provider.client().request(method, &reth_params).await?;
|
||||
loop {
|
||||
let resp: ForkchoiceUpdated = provider.client().request(method, &reth_params).await?;
|
||||
|
||||
if resp.is_valid() {
|
||||
break Ok(resp)
|
||||
}
|
||||
|
||||
while !resp.is_valid() {
|
||||
if resp.is_invalid() {
|
||||
error!(target: "reth-bench", ?resp, "Invalid {method}");
|
||||
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
|
||||
@@ -428,11 +425,6 @@ pub(crate) async fn call_forkchoice_updated_with_reth<
|
||||
"invalid range: no canonical state found for parent of requested block",
|
||||
))
|
||||
}
|
||||
resp = provider.client().request(method, &reth_params).await?;
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
} else {
|
||||
call_forkchoice_updated(provider, message_version, forkchoice_state, None).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ alloy-signer.workspace = true
|
||||
alloy-signer-local.workspace = true
|
||||
rand.workspace = true
|
||||
revm-state.workspace = true
|
||||
criterion.workspace = true
|
||||
|
||||
[features]
|
||||
serde = [
|
||||
@@ -86,8 +85,3 @@ test-utils = [
|
||||
"reth-ethereum-primitives/test-utils",
|
||||
]
|
||||
rayon = ["dep:rayon"]
|
||||
|
||||
[[bench]]
|
||||
name = "canonical_hashes_range"
|
||||
harness = false
|
||||
required-features = ["test-utils"]
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use reth_chain_state::{
|
||||
test_utils::TestBlockBuilder, ExecutedBlock, MemoryOverlayStateProviderRef,
|
||||
};
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_storage_api::{noop::NoopProvider, BlockHashReader};
|
||||
|
||||
criterion_group!(benches, bench_canonical_hashes_range);
|
||||
criterion_main!(benches);
|
||||
|
||||
fn bench_canonical_hashes_range(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("canonical_hashes_range");
|
||||
|
||||
let scenarios = [("small", 10), ("medium", 100), ("large", 1000)];
|
||||
|
||||
for (name, num_blocks) in scenarios {
|
||||
group.bench_function(format!("{}_blocks_{}", name, num_blocks), |b| {
|
||||
let (provider, blocks) = setup_provider_with_blocks(num_blocks);
|
||||
let start_block = blocks[0].recovered_block().number;
|
||||
let end_block = blocks[num_blocks / 2].recovered_block().number;
|
||||
|
||||
b.iter(|| {
|
||||
black_box(
|
||||
provider
|
||||
.canonical_hashes_range(black_box(start_block), black_box(end_block))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let (provider, blocks) = setup_provider_with_blocks(500);
|
||||
let base_block = blocks[100].recovered_block().number;
|
||||
|
||||
let range_sizes = [1, 10, 50, 100, 250];
|
||||
for range_size in range_sizes {
|
||||
group.bench_function(format!("range_size_{}", range_size), |b| {
|
||||
let end_block = base_block + range_size;
|
||||
|
||||
b.iter(|| {
|
||||
black_box(
|
||||
provider
|
||||
.canonical_hashes_range(black_box(base_block), black_box(end_block))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Benchmark edge cases
|
||||
group.bench_function("no_in_memory_matches", |b| {
|
||||
let (provider, blocks) = setup_provider_with_blocks(100);
|
||||
let first_block = blocks[0].recovered_block().number;
|
||||
let start_block = first_block - 50;
|
||||
let end_block = first_block - 10;
|
||||
|
||||
b.iter(|| {
|
||||
black_box(
|
||||
provider
|
||||
.canonical_hashes_range(black_box(start_block), black_box(end_block))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("all_in_memory_matches", |b| {
|
||||
let (provider, blocks) = setup_provider_with_blocks(100);
|
||||
let first_block = blocks[0].recovered_block().number;
|
||||
let last_block = blocks[blocks.len() - 1].recovered_block().number;
|
||||
|
||||
b.iter(|| {
|
||||
black_box(
|
||||
provider
|
||||
.canonical_hashes_range(black_box(first_block), black_box(last_block + 1))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn setup_provider_with_blocks(
|
||||
num_blocks: usize,
|
||||
) -> (MemoryOverlayStateProviderRef<'static, EthPrimitives>, Vec<ExecutedBlock<EthPrimitives>>) {
|
||||
let mut builder = TestBlockBuilder::<EthPrimitives>::default();
|
||||
|
||||
let blocks: Vec<_> = builder.get_executed_blocks(1000..1000 + num_blocks as u64).collect();
|
||||
|
||||
let historical = Box::new(NoopProvider::default());
|
||||
let provider = MemoryOverlayStateProviderRef::new(historical, blocks.clone());
|
||||
|
||||
(provider, blocks)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
fmt,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use tracing::instrument;
|
||||
use tracing::{debug_span, instrument};
|
||||
|
||||
/// Shared handle to asynchronously populated trie data.
|
||||
///
|
||||
@@ -163,6 +163,8 @@ impl DeferredTrieData {
|
||||
anchor_hash: B256,
|
||||
ancestors: &[Self],
|
||||
) -> ComputedTrieData {
|
||||
let _span = debug_span!(target: "engine::tree::deferred_trie", "sort_inputs").entered();
|
||||
|
||||
#[cfg(feature = "rayon")]
|
||||
let (sorted_hashed_state, sorted_trie_updates) = rayon::join(
|
||||
|| match Arc::try_unwrap(hashed_state) {
|
||||
@@ -187,6 +189,10 @@ impl DeferredTrieData {
|
||||
},
|
||||
);
|
||||
|
||||
drop(_span);
|
||||
|
||||
let _span = debug_span!(target: "engine::tree::deferred_trie", "build_overlay").entered();
|
||||
|
||||
// Reuse parent's overlay if available and anchors match.
|
||||
// We can only reuse the parent's overlay if it was built on top of the same
|
||||
// persisted anchor. If the anchor has changed (e.g., due to persistence),
|
||||
@@ -205,6 +211,9 @@ impl DeferredTrieData {
|
||||
Arc::clone(&trie_input.state),
|
||||
Default::default(), // prefix_sets are per-block, not cumulative
|
||||
);
|
||||
let _span =
|
||||
debug_span!(target: "engine::tree::deferred_trie", "extend_overlay")
|
||||
.entered();
|
||||
// Only trigger COW clone if there's actually data to add.
|
||||
#[cfg(feature = "rayon")]
|
||||
{
|
||||
@@ -272,6 +281,7 @@ impl DeferredTrieData {
|
||||
sorted_hashed_state: &HashedPostStateSorted,
|
||||
sorted_trie_updates: &TrieUpdatesSorted,
|
||||
) -> TrieInputSorted {
|
||||
let _span = debug_span!(target: "engine::tree::deferred_trie", "merge_ancestors", num_ancestors = ancestors.len()).entered();
|
||||
let mut overlay = TrieInputSorted::default();
|
||||
|
||||
let state_mut = Arc::make_mut(&mut overlay.state);
|
||||
|
||||
69
crates/chain-state/src/execution_stats.rs
Normal file
69
crates/chain-state/src/execution_stats.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
//! Execution timing statistics for detailed block logging.
|
||||
//!
|
||||
//! This module provides types for collecting and passing execution timing statistics
|
||||
//! through the block processing pipeline, enabling unified detailed block logging after
|
||||
//! database commit.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use alloy_primitives::B256;
|
||||
|
||||
/// Statistics collected during block execution for cross-client performance analysis.
|
||||
///
|
||||
/// These statistics are populated during block validation and carried through to
|
||||
/// persistence, where they are used to emit a single unified log entry that includes
|
||||
/// complete timing information (including commit time).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExecutionTimingStats {
|
||||
/// Block number
|
||||
pub block_number: u64,
|
||||
/// Block hash
|
||||
pub block_hash: B256,
|
||||
/// Total gas used by the block
|
||||
pub gas_used: u64,
|
||||
/// Number of transactions in the block
|
||||
pub tx_count: usize,
|
||||
/// Time spent executing transactions (includes state reads)
|
||||
pub execution_duration: Duration,
|
||||
/// Time spent fetching state during execution (subset of `execution_duration`, includes cache
|
||||
/// hits)
|
||||
pub state_read_duration: Duration,
|
||||
/// Time spent computing state root hash
|
||||
pub state_hash_duration: Duration,
|
||||
/// Number of accounts read during execution
|
||||
pub accounts_read: usize,
|
||||
/// Number of storage slots read (SLOAD operations)
|
||||
pub storage_read: usize,
|
||||
/// Number of code reads (EXTCODE* operations)
|
||||
pub code_read: usize,
|
||||
/// Total bytes of code read
|
||||
pub code_bytes_read: usize,
|
||||
/// Number of accounts changed (balance/nonce updates)
|
||||
pub accounts_changed: usize,
|
||||
/// Number of accounts deleted (SELFDESTRUCT)
|
||||
pub accounts_deleted: usize,
|
||||
/// Number of storage slots changed (SSTORE operations)
|
||||
pub storage_slots_changed: usize,
|
||||
/// Number of storage slots deleted (set to zero)
|
||||
pub storage_slots_deleted: usize,
|
||||
/// Number of bytecodes created/changed (contract deployments)
|
||||
pub bytecodes_changed: usize,
|
||||
/// Total bytes of code written
|
||||
pub code_bytes_written: usize,
|
||||
/// Number of EIP-7702 delegations set
|
||||
pub eip7702_delegations_set: usize,
|
||||
/// Number of EIP-7702 delegations cleared
|
||||
pub eip7702_delegations_cleared: usize,
|
||||
/// Account cache hits
|
||||
pub account_cache_hits: usize,
|
||||
/// Account cache misses
|
||||
pub account_cache_misses: usize,
|
||||
/// Storage cache hits
|
||||
pub storage_cache_hits: usize,
|
||||
/// Storage cache misses
|
||||
pub storage_cache_misses: usize,
|
||||
/// Code cache hits
|
||||
pub code_cache_hits: usize,
|
||||
/// Code cache misses
|
||||
pub code_cache_misses: usize,
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod execution_stats;
|
||||
pub use execution_stats::ExecutionTimingStats;
|
||||
|
||||
mod in_memory;
|
||||
pub use in_memory::*;
|
||||
|
||||
|
||||
@@ -855,15 +855,9 @@ impl From<Genesis> for ChainSpec {
|
||||
// those networks we use the activation
|
||||
// blocks of those networks
|
||||
match genesis.config.chain_id {
|
||||
1 => {
|
||||
if ttd == MAINNET_PARIS_TTD {
|
||||
return Some(MAINNET_PARIS_BLOCK)
|
||||
}
|
||||
}
|
||||
11155111 => {
|
||||
if ttd == SEPOLIA_PARIS_TTD {
|
||||
return Some(SEPOLIA_PARIS_BLOCK)
|
||||
}
|
||||
1 if ttd == MAINNET_PARIS_TTD => return Some(MAINNET_PARIS_BLOCK),
|
||||
11155111 if ttd == SEPOLIA_PARIS_TTD => {
|
||||
return Some(SEPOLIA_PARIS_BLOCK)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::chainspec::ChainSpecParser;
|
||||
///
|
||||
/// This trait is supposed to be implemented by the main struct of the CLI.
|
||||
///
|
||||
/// It provides commonly used functionality for running commands and information about the CL, such
|
||||
/// It provides commonly used functionality for running commands and information about the CLI, such
|
||||
/// as the name and version.
|
||||
pub trait RethCli: Sized {
|
||||
/// The associated `ChainSpecParser` type
|
||||
|
||||
@@ -46,7 +46,7 @@ reth-prune.workspace = true
|
||||
reth-prune-types.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-stages.workspace = true
|
||||
reth-stages-types = { workspace = true, optional = true }
|
||||
reth-stages-types.workspace = true
|
||||
reth-static-file-types = { workspace = true, features = ["clap"] }
|
||||
reth-static-file.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
@@ -87,6 +87,8 @@ tokio-stream.workspace = true
|
||||
reqwest.workspace = true
|
||||
url.workspace = true
|
||||
metrics.workspace = true
|
||||
blake3.workspace = true
|
||||
rayon.workspace = true
|
||||
|
||||
# io
|
||||
fdlimit.workspace = true
|
||||
@@ -127,7 +129,7 @@ arbitrary = [
|
||||
"reth-trie-common?/test-utils",
|
||||
"reth-codecs/arbitrary",
|
||||
"reth-prune-types/arbitrary",
|
||||
"reth-stages-types?/arbitrary",
|
||||
"reth-stages-types/arbitrary",
|
||||
"reth-trie-common?/arbitrary",
|
||||
"alloy-consensus/arbitrary",
|
||||
"reth-primitives-traits/arbitrary",
|
||||
|
||||
@@ -73,17 +73,12 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
/// Returns the effective storage settings derived from `--storage.v2`.
|
||||
/// Returns the storage settings for new database initialization.
|
||||
///
|
||||
/// The base storage mode is determined by `--storage.v2`:
|
||||
/// - When `--storage.v2` is set: uses [`StorageSettings::v2()`] defaults
|
||||
/// - Otherwise: uses [`StorageSettings::base()`] defaults
|
||||
/// Always returns [`StorageSettings::v2()`] — v2 is the default for all new
|
||||
/// databases. Existing databases use the settings persisted in their metadata.
|
||||
pub fn storage_settings(&self) -> StorageSettings {
|
||||
if self.storage.v2 {
|
||||
StorageSettings::v2()
|
||||
} else {
|
||||
StorageSettings::base()
|
||||
}
|
||||
StorageSettings::v2()
|
||||
}
|
||||
|
||||
/// Initializes environment according to [`AccessRights`] and returns an instance of
|
||||
|
||||
@@ -19,6 +19,7 @@ mod list;
|
||||
mod prune_checkpoints;
|
||||
mod repair_trie;
|
||||
mod settings;
|
||||
mod stage_checkpoints;
|
||||
mod state;
|
||||
mod static_file_header;
|
||||
mod stats;
|
||||
@@ -70,6 +71,8 @@ pub enum Subcommands {
|
||||
Settings(settings::Command),
|
||||
/// View or set prune checkpoints
|
||||
PruneCheckpoints(prune_checkpoints::Command),
|
||||
// View or set stage checkpoints
|
||||
StageCheckpoints(stage_checkpoints::Command),
|
||||
/// Gets storage size information for an account
|
||||
AccountStorage(account_storage::Command),
|
||||
/// Gets account state and storage at a specific block
|
||||
@@ -99,14 +102,14 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
let static_files_path = data_dir.static_files();
|
||||
let exex_wal_path = data_dir.exex_wal();
|
||||
|
||||
// ensure the provided datadir exist
|
||||
// ensure the provided datadir exists
|
||||
eyre::ensure!(
|
||||
data_dir.data_dir().is_dir(),
|
||||
"Datadir does not exist: {:?}",
|
||||
data_dir.data_dir()
|
||||
);
|
||||
|
||||
// ensure the provided database exist
|
||||
// ensure the provided database exists
|
||||
eyre::ensure!(db_path.is_dir(), "Database does not exist: {:?}", db_path);
|
||||
|
||||
match self.command {
|
||||
@@ -213,6 +216,11 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::StageCheckpoints(command) => {
|
||||
db_exec!(self.env, tool, N, command.access_rights(), {
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::AccountStorage(command) => {
|
||||
db_exec!(self.env, tool, N, AccessRights::RO, {
|
||||
command.execute(&tool)?;
|
||||
|
||||
297
crates/cli/commands/src/db/stage_checkpoints.rs
Normal file
297
crates/cli/commands/src/db/stage_checkpoints.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
//! `reth db stage-checkpoints` command for viewing and setting stage checkpoint values.
|
||||
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_provider::{
|
||||
providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
|
||||
StageCheckpointWriter,
|
||||
};
|
||||
use reth_stages::StageId;
|
||||
|
||||
use crate::common::AccessRights;
|
||||
|
||||
/// `reth db stage-checkpoints` subcommand
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
#[command(subcommand)]
|
||||
command: Subcommands,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Returns database access rights required for the command.
|
||||
pub fn access_rights(&self) -> AccessRights {
|
||||
match &self.command {
|
||||
Subcommands::Get { .. } => AccessRights::RO,
|
||||
Subcommands::Set(_) => AccessRights::RW,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the command
|
||||
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
|
||||
match self.command {
|
||||
Subcommands::Get { stage } => Self::get(tool, stage),
|
||||
Subcommands::Set(args) => Self::set(tool, args),
|
||||
}
|
||||
}
|
||||
|
||||
fn get<N: ProviderNodeTypes>(tool: &DbTool<N>, stage: Option<StageArg>) -> eyre::Result<()> {
|
||||
let provider = tool.provider_factory.provider()?;
|
||||
|
||||
match stage {
|
||||
Some(stage) => {
|
||||
let stage_id = stage.into();
|
||||
let checkpoint = provider.get_stage_checkpoint(stage_id)?;
|
||||
println!("{stage_id}: {checkpoint:?}");
|
||||
}
|
||||
None => {
|
||||
let mut checkpoints = provider.get_all_checkpoints()?;
|
||||
checkpoints.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (stage, checkpoint) in checkpoints {
|
||||
println!("{stage}: {checkpoint:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set<N: ProviderNodeTypes>(tool: &DbTool<N>, args: SetArgs) -> eyre::Result<()> {
|
||||
let stage_id: StageId = args.stage.into();
|
||||
let provider_rw = tool.provider_factory.database_provider_rw()?;
|
||||
|
||||
let previous = provider_rw.get_stage_checkpoint(stage_id)?;
|
||||
let mut checkpoint = previous.unwrap_or_default();
|
||||
checkpoint.block_number = args.block_number;
|
||||
|
||||
if args.clear_stage_unit {
|
||||
checkpoint.stage_checkpoint = None;
|
||||
}
|
||||
|
||||
provider_rw.save_stage_checkpoint(stage_id, checkpoint)?;
|
||||
|
||||
provider_rw.commit()?;
|
||||
|
||||
println!("Updated checkpoint for {stage_id}: {checkpoint:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Subcommands {
|
||||
/// Get stage checkpoint(s) from database.
|
||||
Get {
|
||||
/// Specific stage to query. If omitted, shows all stages.
|
||||
#[arg(long, value_enum)]
|
||||
stage: Option<StageArg>,
|
||||
},
|
||||
/// Set a stage checkpoint.
|
||||
Set(SetArgs),
|
||||
}
|
||||
|
||||
/// Arguments for the `set` subcommand.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct SetArgs {
|
||||
/// Stage to update.
|
||||
#[arg(long, value_enum)]
|
||||
stage: StageArg,
|
||||
|
||||
/// Block number to set as stage checkpoint.
|
||||
#[arg(long)]
|
||||
block_number: u64,
|
||||
|
||||
/// Clear stage-specific unit checkpoint payload.
|
||||
#[arg(long)]
|
||||
clear_stage_unit: bool,
|
||||
}
|
||||
|
||||
/// CLI-friendly stage names.
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum StageArg {
|
||||
Era,
|
||||
Headers,
|
||||
Bodies,
|
||||
SenderRecovery,
|
||||
Execution,
|
||||
PruneSenderRecovery,
|
||||
MerkleUnwind,
|
||||
AccountHashing,
|
||||
StorageHashing,
|
||||
MerkleExecute,
|
||||
TransactionLookup,
|
||||
IndexStorageHistory,
|
||||
IndexAccountHistory,
|
||||
Prune,
|
||||
Finish,
|
||||
}
|
||||
|
||||
impl From<StageArg> for StageId {
|
||||
fn from(arg: StageArg) -> Self {
|
||||
match arg {
|
||||
StageArg::Era => Self::Era,
|
||||
StageArg::Headers => Self::Headers,
|
||||
StageArg::Bodies => Self::Bodies,
|
||||
StageArg::SenderRecovery => Self::SenderRecovery,
|
||||
StageArg::Execution => Self::Execution,
|
||||
StageArg::PruneSenderRecovery => Self::PruneSenderRecovery,
|
||||
StageArg::MerkleUnwind => Self::MerkleUnwind,
|
||||
StageArg::AccountHashing => Self::AccountHashing,
|
||||
StageArg::StorageHashing => Self::StorageHashing,
|
||||
StageArg::MerkleExecute => Self::MerkleExecute,
|
||||
StageArg::TransactionLookup => Self::TransactionLookup,
|
||||
StageArg::IndexStorageHistory => Self::IndexStorageHistory,
|
||||
StageArg::IndexAccountHistory => Self::IndexAccountHistory,
|
||||
StageArg::Prune => Self::Prune,
|
||||
StageArg::Finish => Self::Finish,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
use reth_provider::{
|
||||
test_utils::create_test_provider_factory, DBProvider, DatabaseProviderFactory,
|
||||
StageCheckpointReader, StageCheckpointWriter,
|
||||
};
|
||||
use reth_stages::StageCheckpoint;
|
||||
|
||||
#[test]
|
||||
fn parse_set_args() {
|
||||
let command = Command::parse_from([
|
||||
"stage-checkpoints",
|
||||
"set",
|
||||
"--stage",
|
||||
"headers",
|
||||
"--block-number",
|
||||
"123",
|
||||
]);
|
||||
|
||||
assert!(matches!(
|
||||
command.command,
|
||||
Subcommands::Set(SetArgs {
|
||||
stage: StageArg::Headers,
|
||||
block_number: 123,
|
||||
clear_stage_unit: false,
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_overwrites_block_number() {
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
|
||||
|
||||
{
|
||||
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
|
||||
provider_rw
|
||||
.save_stage_checkpoint(StageId::Headers, StageCheckpoint::new(10))
|
||||
.expect("save checkpoint");
|
||||
provider_rw.commit().expect("commit initial checkpoint");
|
||||
}
|
||||
|
||||
let command = Command {
|
||||
command: Subcommands::Set(SetArgs {
|
||||
stage: StageArg::Headers,
|
||||
block_number: 42,
|
||||
clear_stage_unit: false,
|
||||
}),
|
||||
};
|
||||
|
||||
command.execute(&tool).expect("execute command");
|
||||
|
||||
let provider = provider_factory.provider().expect("provider");
|
||||
let checkpoint = provider
|
||||
.get_stage_checkpoint(StageId::Headers)
|
||||
.expect("get stage checkpoint")
|
||||
.expect("missing stage checkpoint");
|
||||
|
||||
assert_eq!(checkpoint.block_number, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_preserves_stage_unit_checkpoint_unless_cleared() {
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
|
||||
|
||||
{
|
||||
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
|
||||
let checkpoint = StageCheckpoint::new(10).with_block_range(&StageId::Execution, 5, 10);
|
||||
provider_rw
|
||||
.save_stage_checkpoint(StageId::Execution, checkpoint)
|
||||
.expect("save checkpoint");
|
||||
provider_rw.commit().expect("commit initial checkpoint");
|
||||
}
|
||||
|
||||
Command {
|
||||
command: Subcommands::Set(SetArgs {
|
||||
stage: StageArg::Execution,
|
||||
block_number: 11,
|
||||
clear_stage_unit: false,
|
||||
}),
|
||||
}
|
||||
.execute(&tool)
|
||||
.expect("execute command");
|
||||
|
||||
let provider = provider_factory.provider().expect("provider");
|
||||
let checkpoint = provider
|
||||
.get_stage_checkpoint(StageId::Execution)
|
||||
.expect("get stage checkpoint")
|
||||
.expect("missing stage checkpoint");
|
||||
assert!(checkpoint.stage_checkpoint.is_some());
|
||||
|
||||
Command {
|
||||
command: Subcommands::Set(SetArgs {
|
||||
stage: StageArg::Execution,
|
||||
block_number: 12,
|
||||
clear_stage_unit: true,
|
||||
}),
|
||||
}
|
||||
.execute(&tool)
|
||||
.expect("execute command");
|
||||
|
||||
let checkpoint = provider_factory
|
||||
.provider()
|
||||
.expect("provider")
|
||||
.get_stage_checkpoint(StageId::Execution)
|
||||
.expect("get stage checkpoint")
|
||||
.expect("missing stage checkpoint");
|
||||
assert!(checkpoint.stage_checkpoint.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_preserves_checkpoint_progress() {
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
|
||||
|
||||
{
|
||||
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
|
||||
provider_rw
|
||||
.save_stage_checkpoint(StageId::MerkleExecute, StageCheckpoint::new(10))
|
||||
.expect("save checkpoint");
|
||||
provider_rw
|
||||
.save_stage_checkpoint_progress(StageId::MerkleExecute, vec![1, 2, 3])
|
||||
.expect("save progress");
|
||||
provider_rw.commit().expect("commit initial checkpoint");
|
||||
}
|
||||
|
||||
Command {
|
||||
command: Subcommands::Set(SetArgs {
|
||||
stage: StageArg::MerkleExecute,
|
||||
block_number: 20,
|
||||
clear_stage_unit: false,
|
||||
}),
|
||||
}
|
||||
.execute(&tool)
|
||||
.expect("execute command");
|
||||
|
||||
let provider = provider_factory.provider().expect("provider");
|
||||
let progress = provider
|
||||
.get_stage_checkpoint_progress(StageId::MerkleExecute)
|
||||
.expect("get stage checkpoint progress");
|
||||
|
||||
assert_eq!(progress, Some(vec![1, 2, 3]));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use reth_db_api::{
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_provider::providers::ProviderNodeTypes;
|
||||
use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory};
|
||||
use reth_storage_api::{BlockNumReader, StateProvider, StorageSettingsCache};
|
||||
use reth_tasks::spawn_scoped_os_thread;
|
||||
use std::{
|
||||
@@ -17,9 +17,9 @@ use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
|
||||
/// Log progress every 5 seconds
|
||||
/// Log progress every 30 seconds
|
||||
const LOG_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// The arguments for the `reth db state` command
|
||||
@@ -152,17 +152,11 @@ impl Command {
|
||||
let mut storage_keys = BTreeSet::new();
|
||||
|
||||
if history_in_rocksdb {
|
||||
error!(
|
||||
target: "reth::cli",
|
||||
"Historical storage queries with RocksDB backend are not yet supported. \
|
||||
Use MDBX for storage history or query current state without --block."
|
||||
);
|
||||
return Ok(());
|
||||
self.collect_staticfile_storage_keys(tool, address, &mut storage_keys)?;
|
||||
} else {
|
||||
self.collect_mdbx_storage_keys_parallel(tool, address, &mut storage_keys)?;
|
||||
}
|
||||
|
||||
// Collect keys from MDBX StorageChangeSets using parallel scanning
|
||||
self.collect_mdbx_storage_keys_parallel(tool, address, &mut storage_keys)?;
|
||||
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
address = %address,
|
||||
@@ -207,6 +201,63 @@ impl Command {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collects storage keys from static file StorageChangeSets (storage_v2).
|
||||
fn collect_staticfile_storage_keys<N: NodeTypesWithDB + ProviderNodeTypes>(
|
||||
&self,
|
||||
tool: &DbTool<N>,
|
||||
address: Address,
|
||||
keys: &mut BTreeSet<B256>,
|
||||
) -> eyre::Result<()> {
|
||||
let tip = tool.provider_factory.provider()?.best_block_number()?;
|
||||
|
||||
if tip == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
address = %address,
|
||||
tip,
|
||||
"Scanning static file storage changesets"
|
||||
);
|
||||
|
||||
let static_file_provider = tool.provider_factory.static_file_provider();
|
||||
let walker = static_file_provider.walk_storage_changeset_range(0..=tip);
|
||||
|
||||
let mut total_scanned = 0usize;
|
||||
let mut last_log = Instant::now();
|
||||
|
||||
for changeset_result in walker {
|
||||
let (block_addr, storage_entry) = changeset_result?;
|
||||
total_scanned += 1;
|
||||
|
||||
if block_addr.address() == address {
|
||||
keys.insert(storage_entry.key);
|
||||
}
|
||||
|
||||
if last_log.elapsed() >= LOG_INTERVAL {
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
address = %address,
|
||||
entries_scanned = total_scanned,
|
||||
unique_keys = keys.len(),
|
||||
"Scanning static file storage changesets"
|
||||
);
|
||||
last_log = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
address = %address,
|
||||
total_entries = total_scanned,
|
||||
unique_keys = keys.len(),
|
||||
"Finished static file storage changeset scan"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collects storage keys from MDBX StorageChangeSets using parallel block range scanning.
|
||||
fn collect_mdbx_storage_keys_parallel<N: NodeTypesWithDB + ProviderNodeTypes>(
|
||||
&self,
|
||||
|
||||
@@ -297,21 +297,18 @@ where
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Key(key) => {
|
||||
if key.kind == event::KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(true),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
KeyCode::Right => app.next_page(),
|
||||
KeyCode::Left => app.previous_page(),
|
||||
KeyCode::Char('G') => {
|
||||
app.mode = ViewMode::GoToPage;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Event::Key(key) if key.kind == event::KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(true),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
KeyCode::Right => app.next_page(),
|
||||
KeyCode::Left => app.previous_page(),
|
||||
KeyCode::Char('G') => {
|
||||
app.mode = ViewMode::GoToPage;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Key(_) => {}
|
||||
Event::Mouse(e) => match e.kind {
|
||||
MouseEventKind::ScrollDown => app.next(),
|
||||
MouseEventKind::ScrollUp => app.previous(),
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
use crate::common::EnvironmentArgs;
|
||||
use clap::Parser;
|
||||
use eyre::Result;
|
||||
use lz4::Decoder;
|
||||
use reqwest::{blocking::Client as BlockingClient, header::RANGE, Client, StatusCode};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_fs_util as fs;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs::OpenOptions,
|
||||
io::{self, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tar::Archive;
|
||||
use tokio::task;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use zstd::stream::read::Decoder as ZstdDecoder;
|
||||
|
||||
const BYTE_UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
const MERKLE_BASE_URL: &str = "https://downloads.merkle.io";
|
||||
const EXTENSION_TAR_LZ4: &str = ".tar.lz4";
|
||||
const EXTENSION_TAR_ZSTD: &str = ".tar.zst";
|
||||
|
||||
/// Global static download defaults
|
||||
static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
|
||||
|
||||
/// Download configuration defaults
|
||||
///
|
||||
/// Global defaults can be set via [`DownloadDefaults::try_init`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DownloadDefaults {
|
||||
/// List of available snapshot sources
|
||||
pub available_snapshots: Vec<Cow<'static, str>>,
|
||||
/// Default base URL for snapshots
|
||||
pub default_base_url: Cow<'static, str>,
|
||||
/// Default base URL for chain-aware snapshots.
|
||||
///
|
||||
/// When set, the chain ID is appended to form the full URL: `{base_url}/{chain_id}`.
|
||||
/// For example, given a base URL of `https://snapshots.example.com` and chain ID `1`,
|
||||
/// the resulting URL would be `https://snapshots.example.com/1`.
|
||||
///
|
||||
/// Falls back to [`default_base_url`](Self::default_base_url) when `None`.
|
||||
pub default_chain_aware_base_url: Option<Cow<'static, str>>,
|
||||
/// Optional custom long help text that overrides the generated help
|
||||
pub long_help: Option<String>,
|
||||
}
|
||||
|
||||
impl DownloadDefaults {
|
||||
/// Initialize the global download defaults with this configuration
|
||||
pub fn try_init(self) -> Result<(), Self> {
|
||||
DOWNLOAD_DEFAULTS.set(self)
|
||||
}
|
||||
|
||||
/// Get a reference to the global download defaults
|
||||
pub fn get_global() -> &'static DownloadDefaults {
|
||||
DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
|
||||
}
|
||||
|
||||
/// Default download configuration with defaults from merkle.io and publicnode
|
||||
pub fn default_download_defaults() -> Self {
|
||||
Self {
|
||||
available_snapshots: vec![
|
||||
Cow::Borrowed("https://www.merkle.io/snapshots (default, mainnet archive)"),
|
||||
Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
|
||||
],
|
||||
default_base_url: Cow::Borrowed(MERKLE_BASE_URL),
|
||||
default_chain_aware_base_url: None,
|
||||
long_help: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the long help text for the download URL argument using these defaults.
|
||||
///
|
||||
/// If a custom long_help is set, it will be returned. Otherwise, help text is generated
|
||||
/// from the available_snapshots list.
|
||||
pub fn long_help(&self) -> String {
|
||||
if let Some(ref custom_help) = self.long_help {
|
||||
return custom_help.clone();
|
||||
}
|
||||
|
||||
let mut help = String::from(
|
||||
"Specify a snapshot URL or let the command propose a default one.\n\nAvailable snapshot sources:\n",
|
||||
);
|
||||
|
||||
for source in &self.available_snapshots {
|
||||
help.push_str("- ");
|
||||
help.push_str(source);
|
||||
help.push('\n');
|
||||
}
|
||||
|
||||
help.push_str(
|
||||
"\nIf no URL is provided, the latest archive snapshot for the selected chain\nwill be proposed for download from ",
|
||||
);
|
||||
help.push_str(
|
||||
self.default_chain_aware_base_url.as_deref().unwrap_or(&self.default_base_url),
|
||||
);
|
||||
help.push_str(
|
||||
".\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
|
||||
);
|
||||
help
|
||||
}
|
||||
|
||||
/// Add a snapshot source to the list
|
||||
pub fn with_snapshot(mut self, source: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.available_snapshots.push(source.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace all snapshot sources
|
||||
pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
|
||||
self.available_snapshots = sources;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default base URL, e.g. `https://downloads.merkle.io`.
|
||||
pub fn with_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.default_base_url = url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default chain-aware base URL.
|
||||
pub fn with_chain_aware_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.default_chain_aware_base_url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set custom long help text, overriding the generated help
|
||||
pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
|
||||
self.long_help = Some(help.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DownloadDefaults {
|
||||
fn default() -> Self {
|
||||
Self::default_download_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DownloadCommand<C: ChainSpecParser> {
|
||||
#[command(flatten)]
|
||||
env: EnvironmentArgs<C>,
|
||||
|
||||
/// Custom URL to download the snapshot from
|
||||
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCommand<C> {
|
||||
pub async fn execute<N>(self) -> Result<()> {
|
||||
let data_dir = self.env.datadir.resolve_datadir(self.env.chain.chain());
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
|
||||
let url = match self.url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
let url = get_latest_snapshot_url(self.env.chain.chain().id()).await?;
|
||||
info!(target: "reth::cli", "Using default snapshot URL: {}", url);
|
||||
url
|
||||
}
|
||||
};
|
||||
|
||||
info!(target: "reth::cli",
|
||||
chain = %self.env.chain.chain(),
|
||||
dir = ?data_dir.data_dir(),
|
||||
url = %url,
|
||||
"Starting snapshot download and extraction"
|
||||
);
|
||||
|
||||
stream_and_extract(&url, data_dir.data_dir()).await?;
|
||||
info!(target: "reth::cli", "Snapshot downloaded and extracted successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser> DownloadCommand<C> {
|
||||
/// Returns the underlying chain being used to run this command
|
||||
pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
|
||||
Some(&self.env.chain)
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor process status and display progress every 100ms
|
||||
// to avoid overwhelming stdout
|
||||
struct DownloadProgress {
|
||||
downloaded: u64,
|
||||
total_size: u64,
|
||||
last_displayed: Instant,
|
||||
started_at: Instant,
|
||||
}
|
||||
|
||||
impl DownloadProgress {
|
||||
/// Creates new progress tracker with given total size
|
||||
fn new(total_size: u64) -> Self {
|
||||
let now = Instant::now();
|
||||
Self { downloaded: 0, total_size, last_displayed: now, started_at: now }
|
||||
}
|
||||
|
||||
/// Converts bytes to human readable format (B, KB, MB, GB)
|
||||
fn format_size(size: u64) -> String {
|
||||
let mut size = size as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1024.0 && unit_index < BYTE_UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, BYTE_UNITS[unit_index])
|
||||
}
|
||||
|
||||
/// Format duration as human readable string
|
||||
fn format_duration(duration: Duration) -> String {
|
||||
let secs = duration.as_secs();
|
||||
if secs < 60 {
|
||||
format!("{secs}s")
|
||||
} else if secs < 3600 {
|
||||
format!("{}m {}s", secs / 60, secs % 60)
|
||||
} else {
|
||||
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates progress bar
|
||||
fn update(&mut self, chunk_size: u64) -> Result<()> {
|
||||
self.downloaded += chunk_size;
|
||||
|
||||
// Only update display at most 10 times per second for efficiency
|
||||
if self.last_displayed.elapsed() >= Duration::from_millis(100) {
|
||||
let formatted_downloaded = Self::format_size(self.downloaded);
|
||||
let formatted_total = Self::format_size(self.total_size);
|
||||
let progress = (self.downloaded as f64 / self.total_size as f64) * 100.0;
|
||||
|
||||
// Calculate ETA based on current speed
|
||||
let elapsed = self.started_at.elapsed();
|
||||
let eta = if self.downloaded > 0 {
|
||||
let remaining = self.total_size.saturating_sub(self.downloaded);
|
||||
let speed = self.downloaded as f64 / elapsed.as_secs_f64();
|
||||
if speed > 0.0 {
|
||||
Duration::from_secs_f64(remaining as f64 / speed)
|
||||
} else {
|
||||
Duration::ZERO
|
||||
}
|
||||
} else {
|
||||
Duration::ZERO
|
||||
};
|
||||
let eta_str = Self::format_duration(eta);
|
||||
|
||||
// Pad with spaces to clear any previous longer line
|
||||
print!(
|
||||
"\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total}) ETA: {eta_str} ",
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
self.last_displayed = Instant::now();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to track progress while reading
|
||||
struct ProgressReader<R> {
|
||||
reader: R,
|
||||
progress: DownloadProgress,
|
||||
}
|
||||
|
||||
impl<R: Read> ProgressReader<R> {
|
||||
fn new(reader: R, total_size: u64) -> Self {
|
||||
Self { reader, progress: DownloadProgress::new(total_size) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for ProgressReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let bytes = self.reader.read(buf)?;
|
||||
if bytes > 0 &&
|
||||
let Err(e) = self.progress.update(bytes as u64)
|
||||
{
|
||||
return Err(io::Error::other(e));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported compression formats for snapshots
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CompressionFormat {
|
||||
Lz4,
|
||||
Zstd,
|
||||
}
|
||||
|
||||
impl CompressionFormat {
|
||||
/// Detect compression format from file extension
|
||||
fn from_url(url: &str) -> Result<Self> {
|
||||
let path =
|
||||
Url::parse(url).map(|u| u.path().to_string()).unwrap_or_else(|_| url.to_string());
|
||||
|
||||
if path.ends_with(EXTENSION_TAR_LZ4) {
|
||||
Ok(Self::Lz4)
|
||||
} else if path.ends_with(EXTENSION_TAR_ZSTD) {
|
||||
Ok(Self::Zstd)
|
||||
} else {
|
||||
Err(eyre::eyre!(
|
||||
"Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}",
|
||||
path
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a compressed tar archive to the target directory with progress tracking.
|
||||
fn extract_archive<R: Read>(
|
||||
reader: R,
|
||||
total_size: u64,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let progress_reader = ProgressReader::new(reader, total_size);
|
||||
|
||||
match format {
|
||||
CompressionFormat::Lz4 => {
|
||||
let decoder = Decoder::new(progress_reader)?;
|
||||
Archive::new(decoder).unpack(target_dir)?;
|
||||
}
|
||||
CompressionFormat::Zstd => {
|
||||
let decoder = ZstdDecoder::new(progress_reader)?;
|
||||
Archive::new(decoder).unpack(target_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "Extraction complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts a snapshot from a local file.
|
||||
fn extract_from_file(path: &Path, format: CompressionFormat, target_dir: &Path) -> Result<()> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let total_size = file.metadata()?.len();
|
||||
extract_archive(file, total_size, format, target_dir)
|
||||
}
|
||||
|
||||
const MAX_DOWNLOAD_RETRIES: u32 = 10;
|
||||
const RETRY_BACKOFF_SECS: u64 = 5;
|
||||
|
||||
/// Wrapper that tracks download progress while writing data.
|
||||
/// Used with [`io::copy`] to display progress during downloads.
|
||||
struct ProgressWriter<W> {
|
||||
inner: W,
|
||||
progress: DownloadProgress,
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ProgressWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
let _ = self.progress.update(n as u64);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a file with resume support using HTTP Range requests.
|
||||
/// Automatically retries on failure, resuming from where it left off.
|
||||
/// Returns the path to the downloaded file and its total size.
|
||||
fn resumable_download(url: &str, target_dir: &Path) -> Result<(PathBuf, u64)> {
|
||||
let file_name = Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|u| u.path_segments()?.next_back().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "snapshot.tar".to_string());
|
||||
|
||||
let final_path = target_dir.join(&file_name);
|
||||
let part_path = target_dir.join(format!("{file_name}.part"));
|
||||
|
||||
let client = BlockingClient::builder().timeout(Duration::from_secs(30)).build()?;
|
||||
|
||||
let mut total_size: Option<u64> = None;
|
||||
let mut last_error: Option<eyre::Error> = None;
|
||||
|
||||
let finalize_download = |size: u64| -> Result<(PathBuf, u64)> {
|
||||
fs::rename(&part_path, &final_path)?;
|
||||
info!(target: "reth::cli", "Download complete: {}", final_path.display());
|
||||
Ok((final_path.clone(), size))
|
||||
};
|
||||
|
||||
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
|
||||
let existing_size = fs::metadata(&part_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
if let Some(total) = total_size &&
|
||||
existing_size >= total
|
||||
{
|
||||
return finalize_download(total);
|
||||
}
|
||||
|
||||
if attempt > 1 {
|
||||
info!(target: "reth::cli",
|
||||
"Retry attempt {}/{} - resuming from {} bytes",
|
||||
attempt, MAX_DOWNLOAD_RETRIES, existing_size
|
||||
);
|
||||
}
|
||||
|
||||
let mut request = client.get(url);
|
||||
if existing_size > 0 {
|
||||
request = request.header(RANGE, format!("bytes={existing_size}-"));
|
||||
if attempt == 1 {
|
||||
info!(target: "reth::cli", "Resuming download from {} bytes", existing_size);
|
||||
}
|
||||
}
|
||||
|
||||
let response = match request.send().and_then(|r| r.error_for_status()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
last_error = Some(e.into());
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
info!(target: "reth::cli",
|
||||
"Download failed, retrying in {} seconds...", RETRY_BACKOFF_SECS
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let is_partial = response.status() == StatusCode::PARTIAL_CONTENT;
|
||||
|
||||
let size = if is_partial {
|
||||
response
|
||||
.headers()
|
||||
.get("Content-Range")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.split('/').next_back())
|
||||
.and_then(|v| v.parse().ok())
|
||||
} else {
|
||||
response.content_length()
|
||||
};
|
||||
|
||||
if total_size.is_none() {
|
||||
total_size = size;
|
||||
}
|
||||
|
||||
let current_total = total_size.ok_or_else(|| {
|
||||
eyre::eyre!("Server did not provide Content-Length or Content-Range header")
|
||||
})?;
|
||||
|
||||
let file = if is_partial && existing_size > 0 {
|
||||
OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&part_path)
|
||||
.map_err(|e| fs::FsPathError::open(e, &part_path))?
|
||||
} else {
|
||||
fs::create_file(&part_path)?
|
||||
};
|
||||
|
||||
let start_offset = if is_partial { existing_size } else { 0 };
|
||||
let mut progress = DownloadProgress::new(current_total);
|
||||
progress.downloaded = start_offset;
|
||||
|
||||
let mut writer = ProgressWriter { inner: BufWriter::new(file), progress };
|
||||
let mut reader = response;
|
||||
|
||||
let copy_result = io::copy(&mut reader, &mut writer);
|
||||
let flush_result = writer.inner.flush();
|
||||
println!();
|
||||
|
||||
if let Err(e) = copy_result.and(flush_result) {
|
||||
last_error = Some(e.into());
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
info!(target: "reth::cli",
|
||||
"Download interrupted, retrying in {} seconds...", RETRY_BACKOFF_SECS
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return finalize_download(current_total);
|
||||
}
|
||||
|
||||
Err(last_error
|
||||
.unwrap_or_else(|| eyre::eyre!("Download failed after {} attempts", MAX_DOWNLOAD_RETRIES)))
|
||||
}
|
||||
|
||||
/// Fetches the snapshot from a remote URL with resume support, then extracts it.
|
||||
fn download_and_extract(url: &str, format: CompressionFormat, target_dir: &Path) -> Result<()> {
|
||||
let (downloaded_path, total_size) = resumable_download(url, target_dir)?;
|
||||
|
||||
info!(target: "reth::cli", "Extracting snapshot...");
|
||||
let file = fs::open(&downloaded_path)?;
|
||||
extract_archive(file, total_size, format, target_dir)?;
|
||||
|
||||
fs::remove_file(&downloaded_path)?;
|
||||
info!(target: "reth::cli", "Removed downloaded archive");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads and extracts a snapshot, blocking until finished.
|
||||
///
|
||||
/// Supports both `file://` URLs for local files and HTTP(S) URLs for remote downloads.
|
||||
fn blocking_download_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
let format = CompressionFormat::from_url(url)?;
|
||||
|
||||
if let Ok(parsed_url) = Url::parse(url) &&
|
||||
parsed_url.scheme() == "file"
|
||||
{
|
||||
let file_path = parsed_url
|
||||
.to_file_path()
|
||||
.map_err(|_| eyre::eyre!("Invalid file:// URL path: {}", url))?;
|
||||
extract_from_file(&file_path, format, target_dir)
|
||||
} else {
|
||||
download_and_extract(url, format, target_dir)
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
let target_dir = target_dir.to_path_buf();
|
||||
let url = url.to_string();
|
||||
task::spawn_blocking(move || blocking_download_and_extract(&url, &target_dir)).await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Builds default URL for latest mainnet archive snapshot using configured defaults
|
||||
async fn get_latest_snapshot_url(chain_id: u64) -> Result<String> {
|
||||
let defaults = DownloadDefaults::get_global();
|
||||
let base_url = match &defaults.default_chain_aware_base_url {
|
||||
Some(url) => format!("{url}/{chain_id}"),
|
||||
None => defaults.default_base_url.to_string(),
|
||||
};
|
||||
let latest_url = format!("{base_url}/latest.txt");
|
||||
let filename = Client::new()
|
||||
.get(latest_url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(format!("{base_url}/{filename}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_download_defaults_builder() {
|
||||
let defaults = DownloadDefaults::default()
|
||||
.with_snapshot("https://example.com/snapshots (example)")
|
||||
.with_base_url("https://example.com");
|
||||
|
||||
assert_eq!(defaults.default_base_url, "https://example.com");
|
||||
assert_eq!(defaults.available_snapshots.len(), 3); // 2 defaults + 1 added
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_defaults_replace_snapshots() {
|
||||
let defaults = DownloadDefaults::default().with_snapshots(vec![
|
||||
Cow::Borrowed("https://custom1.com"),
|
||||
Cow::Borrowed("https://custom2.com"),
|
||||
]);
|
||||
|
||||
assert_eq!(defaults.available_snapshots.len(), 2);
|
||||
assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long_help_generation() {
|
||||
let defaults = DownloadDefaults::default();
|
||||
let help = defaults.long_help();
|
||||
|
||||
assert!(help.contains("Available snapshot sources:"));
|
||||
assert!(help.contains("merkle.io"));
|
||||
assert!(help.contains("publicnode.com"));
|
||||
assert!(help.contains("file://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long_help_override() {
|
||||
let custom_help = "This is custom help text for downloading snapshots.";
|
||||
let defaults = DownloadDefaults::default().with_long_help(custom_help);
|
||||
|
||||
let help = defaults.long_help();
|
||||
assert_eq!(help, custom_help);
|
||||
assert!(!help.contains("Available snapshot sources:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_chaining() {
|
||||
let defaults = DownloadDefaults::default()
|
||||
.with_base_url("https://custom.example.com")
|
||||
.with_snapshot("https://snapshot1.com")
|
||||
.with_snapshot("https://snapshot2.com")
|
||||
.with_long_help("Custom help for snapshots");
|
||||
|
||||
assert_eq!(defaults.default_base_url, "https://custom.example.com");
|
||||
assert_eq!(defaults.available_snapshots.len(), 4); // 2 defaults + 2 added
|
||||
assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_format_detection() {
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
|
||||
}
|
||||
}
|
||||
610
crates/cli/commands/src/download/config_gen.rs
Normal file
610
crates/cli/commands/src/download/config_gen.rs
Normal file
@@ -0,0 +1,610 @@
|
||||
use crate::download::{
|
||||
manifest::{ComponentManifest, ComponentSelection, SnapshotComponentType, SnapshotManifest},
|
||||
SelectionPreset,
|
||||
};
|
||||
use reth_chainspec::{EthereumHardfork, EthereumHardforks};
|
||||
use reth_config::config::{BlocksPerFileConfig, Config, PruneConfig, StaticFilesConfig};
|
||||
use reth_db::tables;
|
||||
use reth_db_api::transaction::{DbTx, DbTxMut};
|
||||
use reth_node_core::args::DefaultPruningValues;
|
||||
use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
|
||||
use reth_stages_types::StageCheckpoint;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
use tracing::info;
|
||||
|
||||
/// Minimum blocks to keep for receipts, matching `--minimal` prune settings.
|
||||
const MINIMUM_RECEIPTS_DISTANCE: u64 = 64;
|
||||
|
||||
/// Minimum blocks to keep for history/bodies, matching `--minimal` prune settings
|
||||
/// (`MINIMUM_UNWIND_SAFE_DISTANCE`).
|
||||
const MINIMUM_HISTORY_DISTANCE: u64 = 10064;
|
||||
|
||||
/// Writes a [`Config`] as TOML to `<data_dir>/reth.toml`.
|
||||
///
|
||||
/// If the file already exists, it is not overwritten. Returns `true` if the file was written.
|
||||
pub fn write_config(config: &Config, data_dir: &Path) -> eyre::Result<bool> {
|
||||
let config_path = data_dir.join("reth.toml");
|
||||
|
||||
if config_path.exists() {
|
||||
info!(target: "reth::cli",
|
||||
path = ?config_path,
|
||||
"reth.toml already exists, skipping config generation"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let toml_str = toml::to_string_pretty(config)?;
|
||||
reth_fs_util::write(&config_path, toml_str)?;
|
||||
|
||||
info!(target: "reth::cli",
|
||||
path = ?config_path,
|
||||
"Generated reth.toml based on downloaded components"
|
||||
);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Writes prune checkpoints to the provided write transaction.
|
||||
pub(crate) fn write_prune_checkpoints_tx<Tx>(
|
||||
tx: &Tx,
|
||||
config: &Config,
|
||||
snapshot_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
Tx: DbTx + DbTxMut,
|
||||
{
|
||||
let segments = &config.prune.segments;
|
||||
|
||||
// Collect (segment, mode) pairs for all configured prune segments
|
||||
let checkpoints: Vec<(PruneSegment, PruneMode)> = [
|
||||
(PruneSegment::SenderRecovery, segments.sender_recovery),
|
||||
(PruneSegment::TransactionLookup, segments.transaction_lookup),
|
||||
(PruneSegment::Receipts, segments.receipts),
|
||||
(PruneSegment::AccountHistory, segments.account_history),
|
||||
(PruneSegment::StorageHistory, segments.storage_history),
|
||||
(PruneSegment::Bodies, segments.bodies_history),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(segment, mode)| mode.map(|m| (segment, m)))
|
||||
.collect();
|
||||
|
||||
if checkpoints.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Look up the last tx number for the snapshot block from BlockBodyIndices
|
||||
let tx_number =
|
||||
tx.get::<tables::BlockBodyIndices>(snapshot_block)?.map(|indices| indices.last_tx_num());
|
||||
|
||||
for (segment, prune_mode) in &checkpoints {
|
||||
let checkpoint = PruneCheckpoint {
|
||||
block_number: Some(snapshot_block),
|
||||
tx_number,
|
||||
prune_mode: *prune_mode,
|
||||
};
|
||||
|
||||
tx.put::<tables::PruneCheckpoints>(*segment, checkpoint)?;
|
||||
|
||||
info!(target: "reth::cli",
|
||||
segment = %segment,
|
||||
block = snapshot_block,
|
||||
tx = ?tx_number,
|
||||
mode = ?prune_mode,
|
||||
"Set prune checkpoint"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage IDs for index stages whose output is stored in RocksDB and is never
|
||||
/// distributed in snapshots.
|
||||
const INDEX_STAGE_IDS: [&str; 3] =
|
||||
["TransactionLookup", "IndexAccountHistory", "IndexStorageHistory"];
|
||||
|
||||
/// Prune segments that correspond to the index stages.
|
||||
const INDEX_PRUNE_SEGMENTS: [PruneSegment; 3] =
|
||||
[PruneSegment::TransactionLookup, PruneSegment::AccountHistory, PruneSegment::StorageHistory];
|
||||
|
||||
/// Resets stage and prune checkpoints for stages whose output is not included
|
||||
/// in the snapshot inside an existing write transaction.
|
||||
///
|
||||
/// A snapshot's mdbx comes from a fully synced node, so it has stage checkpoints
|
||||
/// at the tip for `TransactionLookup`, `IndexAccountHistory`, and
|
||||
/// `IndexStorageHistory`. Since we don't distribute the rocksdb indices those
|
||||
/// stages produced, we must reset their checkpoints to block 0. Otherwise the
|
||||
/// pipeline would see "already done" and skip rebuilding entirely.
|
||||
///
|
||||
/// We intentionally do not reset `SenderRecovery`: sender static files are
|
||||
/// distributed for archive downloads, and non-archive downloads rely on the
|
||||
/// configured prune checkpoints for this segment.
|
||||
pub(crate) fn reset_index_stage_checkpoints_tx<Tx>(tx: &Tx) -> eyre::Result<()>
|
||||
where
|
||||
Tx: DbTx + DbTxMut,
|
||||
{
|
||||
for stage_id in INDEX_STAGE_IDS {
|
||||
tx.put::<tables::StageCheckpoints>(stage_id.to_string(), StageCheckpoint::default())?;
|
||||
|
||||
// Also clear any stage-specific progress data
|
||||
tx.delete::<tables::StageCheckpointProgresses>(stage_id.to_string(), None)?;
|
||||
|
||||
info!(target: "reth::cli", stage = stage_id, "Reset stage checkpoint to block 0");
|
||||
}
|
||||
|
||||
// Clear corresponding prune checkpoints so the pruner doesn't inherit
|
||||
// state from the source node
|
||||
for segment in INDEX_PRUNE_SEGMENTS {
|
||||
tx.delete::<tables::PruneCheckpoints>(segment, None)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a [`Config`] from per-component range selections.
|
||||
///
|
||||
/// When all data components are selected as `All`, no pruning is configured (archive node).
|
||||
/// Otherwise, `--minimal` style pruning is applied for missing/partial components.
|
||||
pub(crate) fn config_for_selections(
|
||||
selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
manifest: &SnapshotManifest,
|
||||
preset: Option<SelectionPreset>,
|
||||
chain_spec: Option<&impl EthereumHardforks>,
|
||||
) -> Config {
|
||||
let selection_for = |ty| selections.get(&ty).copied().unwrap_or(ComponentSelection::None);
|
||||
|
||||
let tx_sel = selection_for(SnapshotComponentType::Transactions);
|
||||
let senders_sel = selection_for(SnapshotComponentType::TransactionSenders);
|
||||
let receipt_sel = selection_for(SnapshotComponentType::Receipts);
|
||||
let account_cs_sel = selection_for(SnapshotComponentType::AccountChangesets);
|
||||
let storage_cs_sel = selection_for(SnapshotComponentType::StorageChangesets);
|
||||
|
||||
// Archive node — all data components present, no pruning
|
||||
let is_archive = [tx_sel, senders_sel, receipt_sel, account_cs_sel, storage_cs_sel]
|
||||
.iter()
|
||||
.all(|s| *s == ComponentSelection::All);
|
||||
|
||||
// Extract blocks_per_file from manifest for all component types
|
||||
let blocks_per_file = |ty: SnapshotComponentType| -> Option<u64> {
|
||||
match manifest.component(ty)? {
|
||||
ComponentManifest::Chunked(c) => Some(c.blocks_per_file),
|
||||
ComponentManifest::Single(_) => None,
|
||||
}
|
||||
};
|
||||
let static_files = StaticFilesConfig {
|
||||
blocks_per_file: BlocksPerFileConfig {
|
||||
headers: blocks_per_file(SnapshotComponentType::Headers),
|
||||
transactions: blocks_per_file(SnapshotComponentType::Transactions),
|
||||
receipts: blocks_per_file(SnapshotComponentType::Receipts),
|
||||
transaction_senders: blocks_per_file(SnapshotComponentType::TransactionSenders),
|
||||
account_change_sets: blocks_per_file(SnapshotComponentType::AccountChangesets),
|
||||
storage_change_sets: blocks_per_file(SnapshotComponentType::StorageChangesets),
|
||||
},
|
||||
};
|
||||
|
||||
if is_archive || matches!(preset, Some(SelectionPreset::Archive)) {
|
||||
return Config { static_files, ..Default::default() };
|
||||
}
|
||||
|
||||
if matches!(preset, Some(SelectionPreset::Full)) {
|
||||
let defaults = DefaultPruningValues::get_global();
|
||||
let mut segments = defaults.full_prune_modes.clone();
|
||||
|
||||
if defaults.full_bodies_history_use_pre_merge {
|
||||
segments.bodies_history = chain_spec.and_then(|chain_spec| {
|
||||
chain_spec
|
||||
.ethereum_fork_activation(EthereumHardfork::Paris)
|
||||
.block_number()
|
||||
.map(PruneMode::Before)
|
||||
});
|
||||
}
|
||||
|
||||
return Config {
|
||||
prune: PruneConfig { block_interval: PruneConfig::default().block_interval, segments },
|
||||
static_files,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let mut config = Config::default();
|
||||
let mut prune = PruneConfig::default();
|
||||
|
||||
if senders_sel != ComponentSelection::All {
|
||||
prune.segments.sender_recovery = Some(PruneMode::Full);
|
||||
}
|
||||
prune.segments.transaction_lookup = Some(PruneMode::Full);
|
||||
|
||||
if let Some(mode) = selection_to_prune_mode(tx_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
|
||||
prune.segments.bodies_history = Some(mode);
|
||||
}
|
||||
|
||||
if let Some(mode) = selection_to_prune_mode(receipt_sel, Some(MINIMUM_RECEIPTS_DISTANCE)) {
|
||||
prune.segments.receipts = Some(mode);
|
||||
}
|
||||
|
||||
if let Some(mode) = selection_to_prune_mode(account_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
|
||||
prune.segments.account_history = Some(mode);
|
||||
}
|
||||
|
||||
if let Some(mode) = selection_to_prune_mode(storage_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
|
||||
prune.segments.storage_history = Some(mode);
|
||||
}
|
||||
|
||||
config.prune = prune;
|
||||
config.static_files = static_files;
|
||||
config
|
||||
}
|
||||
|
||||
/// Converts a [`ComponentSelection`] to an optional [`PruneMode`].
|
||||
///
|
||||
/// `min_distance` enforces the minimum blocks required for this segment.
|
||||
/// When set, `None` and distances below the minimum are clamped to it
|
||||
/// instead of producing `PruneMode::Full` which reth would reject.
|
||||
fn selection_to_prune_mode(
|
||||
sel: ComponentSelection,
|
||||
min_distance: Option<u64>,
|
||||
) -> Option<PruneMode> {
|
||||
match sel {
|
||||
ComponentSelection::All => None,
|
||||
ComponentSelection::Distance(d) => {
|
||||
Some(PruneMode::Distance(min_distance.map_or(d, |min| d.max(min))))
|
||||
}
|
||||
ComponentSelection::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable prune config summary.
|
||||
pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
|
||||
let segments = &config.prune.segments;
|
||||
|
||||
[
|
||||
("sender_recovery", segments.sender_recovery),
|
||||
("transaction_lookup", segments.transaction_lookup),
|
||||
("bodies_history", segments.bodies_history),
|
||||
("receipts", segments.receipts),
|
||||
("account_history", segments.account_history),
|
||||
("storage_history", segments.storage_history),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_mode(mode: &PruneMode) -> String {
|
||||
match mode {
|
||||
PruneMode::Full => "\"full\"".to_string(),
|
||||
PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
|
||||
PruneMode::Before(b) => format!("{{ before = {b} }}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_db::Database;
|
||||
|
||||
/// Empty manifest for tests that only care about prune config.
|
||||
fn empty_manifest() -> SnapshotManifest {
|
||||
SnapshotManifest {
|
||||
block: 0,
|
||||
chain_id: 1,
|
||||
storage_version: 2,
|
||||
timestamp: 0,
|
||||
base_url: None,
|
||||
reth_version: None,
|
||||
components: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_prune_checkpoints_sets_all_segments() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
|
||||
|
||||
let mut selections = BTreeMap::new();
|
||||
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
|
||||
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
let snapshot_block = 21_000_000;
|
||||
|
||||
{
|
||||
let tx = db.tx_mut().unwrap();
|
||||
write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// Verify all expected segments have checkpoints
|
||||
let tx = db.tx().unwrap();
|
||||
for segment in [
|
||||
PruneSegment::SenderRecovery,
|
||||
PruneSegment::TransactionLookup,
|
||||
PruneSegment::Receipts,
|
||||
PruneSegment::AccountHistory,
|
||||
PruneSegment::StorageHistory,
|
||||
PruneSegment::Bodies,
|
||||
] {
|
||||
let checkpoint = tx
|
||||
.get::<tables::PruneCheckpoints>(segment)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
|
||||
assert_eq!(checkpoint.block_number, Some(snapshot_block));
|
||||
// No BlockBodyIndices in empty DB, so tx_number should be None
|
||||
assert_eq!(checkpoint.tx_number, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_prune_checkpoints_archive_no_checkpoints() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
|
||||
|
||||
// Archive node — no pruning configured, so no checkpoints written
|
||||
let mut selections = BTreeMap::new();
|
||||
for ty in SnapshotComponentType::ALL {
|
||||
selections.insert(ty, ComponentSelection::All);
|
||||
}
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
|
||||
{
|
||||
let tx = db.tx_mut().unwrap();
|
||||
write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
let tx = db.tx().unwrap();
|
||||
for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
|
||||
assert!(
|
||||
tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
|
||||
"expected no checkpoint for {segment} on archive node"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selections_all_no_pruning() {
|
||||
let mut selections = BTreeMap::new();
|
||||
for ty in SnapshotComponentType::ALL {
|
||||
selections.insert(ty, ComponentSelection::All);
|
||||
}
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
// Archive node — nothing pruned
|
||||
assert_eq!(config.prune.segments.transaction_lookup, None);
|
||||
assert_eq!(config.prune.segments.sender_recovery, None);
|
||||
assert_eq!(config.prune.segments.bodies_history, None);
|
||||
assert_eq!(config.prune.segments.receipts, None);
|
||||
assert_eq!(config.prune.segments.account_history, None);
|
||||
assert_eq!(config.prune.segments.storage_history, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selections_none_clamps_to_minimum_distance() {
|
||||
let mut selections = BTreeMap::new();
|
||||
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
|
||||
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
|
||||
assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
|
||||
// All segments clamped to their minimum distances
|
||||
assert_eq!(
|
||||
config.prune.segments.bodies_history,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
assert_eq!(
|
||||
config.prune.segments.receipts,
|
||||
Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
|
||||
);
|
||||
assert_eq!(
|
||||
config.prune.segments.account_history,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
assert_eq!(
|
||||
config.prune.segments.storage_history,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selections_distance_maps_bodies_history() {
|
||||
let mut selections = BTreeMap::new();
|
||||
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
|
||||
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
|
||||
selections
|
||||
.insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
|
||||
selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
|
||||
selections
|
||||
.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
|
||||
selections
|
||||
.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
|
||||
assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
|
||||
assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
|
||||
// Bodies follows tx selection
|
||||
assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
|
||||
assert_eq!(
|
||||
config.prune.segments.receipts,
|
||||
Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
|
||||
);
|
||||
assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
|
||||
assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_preset_matches_default_full_prune_config() {
|
||||
let mut selections = BTreeMap::new();
|
||||
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
|
||||
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
|
||||
selections
|
||||
.insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
|
||||
selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
|
||||
|
||||
let chain_spec = reth_chainspec::MAINNET.clone();
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
Some(SelectionPreset::Full),
|
||||
Some(chain_spec.as_ref()),
|
||||
);
|
||||
|
||||
assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
|
||||
assert_eq!(config.prune.segments.transaction_lookup, None);
|
||||
assert_eq!(
|
||||
config.prune.segments.receipts,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
assert_eq!(
|
||||
config.prune.segments.account_history,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
assert_eq!(
|
||||
config.prune.segments.storage_history,
|
||||
Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
|
||||
);
|
||||
|
||||
let paris_block = chain_spec
|
||||
.ethereum_fork_activation(EthereumHardfork::Paris)
|
||||
.block_number()
|
||||
.expect("mainnet Paris block should be known");
|
||||
assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_selections_all_no_pruning() {
|
||||
let mut selections = BTreeMap::new();
|
||||
for ty in SnapshotComponentType::ALL {
|
||||
selections.insert(ty, ComponentSelection::All);
|
||||
}
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
let desc = describe_prune_config(&config);
|
||||
// Archive node — no prune segments described
|
||||
assert!(desc.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_selections_with_distances() {
|
||||
let mut selections = BTreeMap::new();
|
||||
selections.insert(SnapshotComponentType::State, ComponentSelection::All);
|
||||
selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
|
||||
selections
|
||||
.insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
|
||||
selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
|
||||
let config = config_for_selections(
|
||||
&selections,
|
||||
&empty_manifest(),
|
||||
None,
|
||||
None::<&reth_chainspec::ChainSpec>,
|
||||
);
|
||||
let desc = describe_prune_config(&config);
|
||||
assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
|
||||
// Bodies follows tx selection
|
||||
assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
|
||||
assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
|
||||
|
||||
// Simulate a fully synced node: set stage checkpoints at tip
|
||||
let tip_checkpoint = StageCheckpoint::new(24_500_000);
|
||||
{
|
||||
let tx = db.tx_mut().unwrap();
|
||||
for stage_id in INDEX_STAGE_IDS {
|
||||
tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
|
||||
}
|
||||
for segment in INDEX_PRUNE_SEGMENTS {
|
||||
tx.put::<tables::PruneCheckpoints>(
|
||||
segment,
|
||||
PruneCheckpoint {
|
||||
block_number: Some(24_500_000),
|
||||
tx_number: None,
|
||||
prune_mode: PruneMode::Full,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Sender recovery checkpoints should be preserved by reset.
|
||||
tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
|
||||
.unwrap();
|
||||
tx.put::<tables::PruneCheckpoints>(
|
||||
PruneSegment::SenderRecovery,
|
||||
PruneCheckpoint {
|
||||
block_number: Some(24_500_000),
|
||||
tx_number: None,
|
||||
prune_mode: PruneMode::Full,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// Reset
|
||||
{
|
||||
let tx = db.tx_mut().unwrap();
|
||||
reset_index_stage_checkpoints_tx(&tx).unwrap();
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// Verify stage checkpoints are at block 0
|
||||
let tx = db.tx().unwrap();
|
||||
for stage_id in INDEX_STAGE_IDS {
|
||||
let checkpoint = tx
|
||||
.get::<tables::StageCheckpoints>(stage_id.to_string())
|
||||
.unwrap()
|
||||
.expect("checkpoint should exist");
|
||||
assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
|
||||
}
|
||||
|
||||
// Verify prune checkpoints are deleted
|
||||
for segment in INDEX_PRUNE_SEGMENTS {
|
||||
assert!(
|
||||
tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
|
||||
"prune checkpoint for {segment} should be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify sender checkpoints are left untouched.
|
||||
let sender_stage_checkpoint = tx
|
||||
.get::<tables::StageCheckpoints>("SenderRecovery".to_string())
|
||||
.unwrap()
|
||||
.expect("sender checkpoint should exist");
|
||||
assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
|
||||
|
||||
let sender_prune_checkpoint = tx
|
||||
.get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
|
||||
.unwrap()
|
||||
.expect("sender prune checkpoint should exist");
|
||||
assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
|
||||
}
|
||||
}
|
||||
1099
crates/cli/commands/src/download/manifest.rs
Normal file
1099
crates/cli/commands/src/download/manifest.rs
Normal file
File diff suppressed because it is too large
Load Diff
232
crates/cli/commands/src/download/manifest_cmd.rs
Normal file
232
crates/cli/commands/src/download/manifest_cmd.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use crate::download::manifest::generate_manifest;
|
||||
use clap::Parser;
|
||||
use eyre::{Result, WrapErr};
|
||||
use reth_db::{mdbx::DatabaseArguments, open_db_read_only, tables, Database};
|
||||
use reth_db_api::transaction::DbTx;
|
||||
use reth_stages_types::StageId;
|
||||
use reth_static_file_types::DEFAULT_BLOCKS_PER_STATIC_FILE;
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Generate modular chunk archives and a snapshot manifest from a source datadir.
|
||||
///
|
||||
/// Archive naming convention:
|
||||
/// - Chunked: `{component}-{start}-{end}.tar.zst` (e.g. `transactions-0-499999.tar.zst`)
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SnapshotManifestCommand {
|
||||
/// Source datadir containing static files.
|
||||
#[arg(long, short = 'd')]
|
||||
source_datadir: PathBuf,
|
||||
|
||||
/// Optional base URL where archives will be hosted.
|
||||
#[arg(long)]
|
||||
base_url: Option<String>,
|
||||
|
||||
/// Output directory where chunk archives and manifest.json are written.
|
||||
#[arg(long, short = 'o')]
|
||||
output_dir: PathBuf,
|
||||
|
||||
/// Block number this snapshot was taken at.
|
||||
///
|
||||
/// If omitted, this is inferred from the source datadir's `Finish` stage checkpoint.
|
||||
#[arg(long)]
|
||||
block: Option<u64>,
|
||||
|
||||
/// Chain ID.
|
||||
#[arg(long, default_value = "1")]
|
||||
chain_id: u64,
|
||||
|
||||
/// Blocks per archive file for chunked components.
|
||||
///
|
||||
/// If omitted, this is inferred from header static file ranges in the source datadir.
|
||||
#[arg(long)]
|
||||
blocks_per_file: Option<u64>,
|
||||
}
|
||||
|
||||
impl SnapshotManifestCommand {
|
||||
pub fn execute(self) -> Result<()> {
|
||||
let block = match self.block {
|
||||
Some(block) => block,
|
||||
None => infer_snapshot_block(&self.source_datadir)?,
|
||||
};
|
||||
let blocks_per_file = match self.blocks_per_file {
|
||||
Some(blocks_per_file) => blocks_per_file,
|
||||
None => infer_blocks_per_file(&self.source_datadir)?,
|
||||
};
|
||||
|
||||
info!(target: "reth::cli",
|
||||
dir = ?self.source_datadir,
|
||||
output = ?self.output_dir,
|
||||
block,
|
||||
blocks_per_file,
|
||||
"Packaging modular snapshot archives"
|
||||
);
|
||||
let start = Instant::now();
|
||||
let manifest = generate_manifest(
|
||||
&self.source_datadir,
|
||||
&self.output_dir,
|
||||
self.base_url.as_deref(),
|
||||
block,
|
||||
self.chain_id,
|
||||
blocks_per_file,
|
||||
)?;
|
||||
|
||||
let num_components = manifest.components.len();
|
||||
let json = serde_json::to_string_pretty(&manifest)?;
|
||||
let output = self.output_dir.join("manifest.json");
|
||||
reth_fs_util::write(&output, &json)?;
|
||||
info!(target: "reth::cli",
|
||||
path = ?output,
|
||||
components = num_components,
|
||||
block = manifest.block,
|
||||
elapsed = ?start.elapsed(),
|
||||
"Manifest written"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_snapshot_block(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
if let Ok(block) = infer_snapshot_block_from_db(source_datadir) {
|
||||
return Ok(block);
|
||||
}
|
||||
|
||||
let block = infer_snapshot_block_from_headers(source_datadir)?;
|
||||
warn!(
|
||||
target: "reth::cli",
|
||||
block,
|
||||
"Could not read Finish stage checkpoint from source DB, using header static-file tip"
|
||||
);
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
fn infer_snapshot_block_from_db(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let candidates = [source_datadir.join("db"), source_datadir.to_path_buf()];
|
||||
|
||||
for db_path in candidates {
|
||||
if !db_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let db = match open_db_read_only(&db_path, DatabaseArguments::default()) {
|
||||
Ok(db) => db,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let tx = db.tx()?;
|
||||
if let Some(checkpoint) = tx.get::<tables::StageCheckpoints>(StageId::Finish.to_string())? {
|
||||
return Ok(checkpoint.block_number);
|
||||
}
|
||||
}
|
||||
|
||||
eyre::bail!(
|
||||
"Could not infer --block from source DB (Finish checkpoint missing); pass --block manually"
|
||||
)
|
||||
}
|
||||
|
||||
fn infer_snapshot_block_from_headers(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let max_end = header_ranges(source_datadir)?
|
||||
.into_iter()
|
||||
.map(|(_, end)| end)
|
||||
.max()
|
||||
.ok_or_else(|| eyre::eyre!("No header static files found to infer --block"))?;
|
||||
Ok(max_end)
|
||||
}
|
||||
|
||||
fn infer_blocks_per_file(source_datadir: &std::path::Path) -> Result<u64> {
|
||||
let mut inferred = None;
|
||||
for (start, end) in header_ranges(source_datadir)? {
|
||||
let span = end.saturating_sub(start).saturating_add(1);
|
||||
if span == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(existing) = inferred {
|
||||
if existing != span {
|
||||
eyre::bail!(
|
||||
"Inconsistent header static file ranges; pass --blocks-per-file manually"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
inferred = Some(span);
|
||||
}
|
||||
}
|
||||
|
||||
inferred.ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"Could not infer --blocks-per-file from header static files; pass it manually (default is {DEFAULT_BLOCKS_PER_STATIC_FILE})"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn header_ranges(source_datadir: &std::path::Path) -> Result<Vec<(u64, u64)>> {
|
||||
let static_files_dir = source_datadir.join("static_files");
|
||||
let static_files_dir =
|
||||
if static_files_dir.exists() { static_files_dir } else { source_datadir.to_path_buf() };
|
||||
|
||||
let entries = std::fs::read_dir(&static_files_dir).wrap_err_with(|| {
|
||||
format!("Failed to read static files directory: {}", static_files_dir.display())
|
||||
})?;
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if let Some(range) = parse_headers_range(&file_name) {
|
||||
ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
fn parse_headers_range(file_name: &str) -> Option<(u64, u64)> {
|
||||
let remainder = file_name.strip_prefix("static_file_headers_")?;
|
||||
let (start, end_with_suffix) = remainder.split_once('_')?;
|
||||
|
||||
let start = start.parse::<u64>().ok()?;
|
||||
let end_digits: String = end_with_suffix.chars().take_while(|ch| ch.is_ascii_digit()).collect();
|
||||
let end = end_digits.parse::<u64>().ok()?;
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn parse_headers_range_works_with_suffixes() {
|
||||
assert_eq!(parse_headers_range("static_file_headers_0_499999"), Some((0, 499_999)));
|
||||
assert_eq!(
|
||||
parse_headers_range("static_file_headers_500000_999999.jar"),
|
||||
Some((500_000, 999_999))
|
||||
);
|
||||
assert_eq!(parse_headers_range("static_file_transactions_0_499999"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_blocks_per_file_from_header_ranges() {
|
||||
let dir = tempdir().unwrap();
|
||||
let sf = dir.path().join("static_files");
|
||||
std::fs::create_dir_all(&sf).unwrap();
|
||||
std::fs::write(sf.join("static_file_headers_0_499999"), []).unwrap();
|
||||
std::fs::write(sf.join("static_file_headers_500000_999999.jar"), []).unwrap();
|
||||
|
||||
assert_eq!(infer_blocks_per_file(dir.path()).unwrap(), 500_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_snapshot_block_from_headers_uses_max_end() {
|
||||
let dir = tempdir().unwrap();
|
||||
let sf = dir.path().join("static_files");
|
||||
std::fs::create_dir_all(&sf).unwrap();
|
||||
std::fs::write(sf.join("static_file_headers_0_499999"), []).unwrap();
|
||||
std::fs::write(sf.join("static_file_headers_500000_999999"), []).unwrap();
|
||||
|
||||
assert_eq!(infer_snapshot_block_from_headers(dir.path()).unwrap(), 999_999);
|
||||
}
|
||||
}
|
||||
2096
crates/cli/commands/src/download/mod.rs
Normal file
2096
crates/cli/commands/src/download/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
437
crates/cli/commands/src/download/tui.rs
Normal file
437
crates/cli/commands/src/download/tui.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use crate::download::{
|
||||
manifest::{ComponentSelection, SnapshotComponentType, SnapshotManifest},
|
||||
DownloadProgress, SelectionPreset,
|
||||
};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Result of the interactive component selector.
|
||||
pub struct SelectorOutput {
|
||||
/// User-confirmed selections with per-component ranges.
|
||||
pub selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
/// Last preset action used in the TUI, if any.
|
||||
pub preset: Option<SelectionPreset>,
|
||||
}
|
||||
|
||||
/// All distance presets. Groups filter this to only valid options.
|
||||
const DISTANCE_PRESETS: [ComponentSelection; 6] = [
|
||||
ComponentSelection::None,
|
||||
ComponentSelection::Distance(64),
|
||||
ComponentSelection::Distance(10_064),
|
||||
ComponentSelection::Distance(100_000),
|
||||
ComponentSelection::Distance(1_000_000),
|
||||
ComponentSelection::All,
|
||||
];
|
||||
|
||||
/// Presets for components that require at least 64 blocks (receipts).
|
||||
const RECEIPTS_PRESETS: [ComponentSelection; 5] = [
|
||||
ComponentSelection::Distance(64),
|
||||
ComponentSelection::Distance(10_064),
|
||||
ComponentSelection::Distance(100_000),
|
||||
ComponentSelection::Distance(1_000_000),
|
||||
ComponentSelection::All,
|
||||
];
|
||||
|
||||
/// Presets for components that require at least 10064 blocks (account/storage history).
|
||||
const HISTORY_PRESETS: [ComponentSelection; 4] = [
|
||||
ComponentSelection::Distance(10_064),
|
||||
ComponentSelection::Distance(100_000),
|
||||
ComponentSelection::Distance(1_000_000),
|
||||
ComponentSelection::All,
|
||||
];
|
||||
|
||||
/// A display group bundles one or more component types into a single TUI row.
|
||||
struct DisplayGroup {
|
||||
/// Display name shown in the TUI.
|
||||
name: &'static str,
|
||||
/// Underlying component types this group controls.
|
||||
types: Vec<SnapshotComponentType>,
|
||||
/// Whether this group is required and locked to All.
|
||||
required: bool,
|
||||
/// Valid presets for this group. Components with minimum distance requirements
|
||||
/// exclude presets that would produce invalid prune configs.
|
||||
presets: &'static [ComponentSelection],
|
||||
}
|
||||
|
||||
/// Build the display groups from available components in the manifest.
|
||||
fn build_groups(manifest: &SnapshotManifest) -> Vec<DisplayGroup> {
|
||||
let has = |ty: SnapshotComponentType| manifest.component(ty).is_some();
|
||||
|
||||
let mut groups = Vec::new();
|
||||
|
||||
if has(SnapshotComponentType::State) {
|
||||
groups.push(DisplayGroup {
|
||||
name: "State (mdbx)",
|
||||
types: vec![SnapshotComponentType::State],
|
||||
required: true,
|
||||
presets: &DISTANCE_PRESETS,
|
||||
});
|
||||
}
|
||||
|
||||
if has(SnapshotComponentType::Headers) {
|
||||
groups.push(DisplayGroup {
|
||||
name: "Headers",
|
||||
types: vec![SnapshotComponentType::Headers],
|
||||
required: true,
|
||||
presets: &DISTANCE_PRESETS,
|
||||
});
|
||||
}
|
||||
|
||||
if has(SnapshotComponentType::Transactions) {
|
||||
groups.push(DisplayGroup {
|
||||
name: "Transactions",
|
||||
types: vec![SnapshotComponentType::Transactions],
|
||||
required: false,
|
||||
presets: &HISTORY_PRESETS,
|
||||
});
|
||||
}
|
||||
|
||||
if has(SnapshotComponentType::Receipts) {
|
||||
groups.push(DisplayGroup {
|
||||
name: "Receipts",
|
||||
types: vec![SnapshotComponentType::Receipts],
|
||||
required: false,
|
||||
presets: &RECEIPTS_PRESETS,
|
||||
});
|
||||
}
|
||||
|
||||
// Bundle account + storage changesets as "State History"
|
||||
let has_acc = has(SnapshotComponentType::AccountChangesets);
|
||||
let has_stor = has(SnapshotComponentType::StorageChangesets);
|
||||
if has_acc || has_stor {
|
||||
let mut types = Vec::new();
|
||||
if has_acc {
|
||||
types.push(SnapshotComponentType::AccountChangesets);
|
||||
}
|
||||
if has_stor {
|
||||
types.push(SnapshotComponentType::StorageChangesets);
|
||||
}
|
||||
groups.push(DisplayGroup {
|
||||
name: "State History",
|
||||
types,
|
||||
required: false,
|
||||
presets: &HISTORY_PRESETS,
|
||||
});
|
||||
}
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
struct SelectorApp {
|
||||
manifest: SnapshotManifest,
|
||||
full_preset: BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
/// Display groups shown in the TUI.
|
||||
groups: Vec<DisplayGroup>,
|
||||
/// Current selection for each group.
|
||||
selections: Vec<ComponentSelection>,
|
||||
/// Last preset action invoked by user.
|
||||
preset: Option<SelectionPreset>,
|
||||
/// Current cursor position.
|
||||
cursor: usize,
|
||||
/// List state for ratatui.
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl SelectorApp {
|
||||
fn new(
|
||||
manifest: SnapshotManifest,
|
||||
full_preset: BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
) -> Self {
|
||||
let groups = build_groups(&manifest);
|
||||
|
||||
// Default to the minimal preset (matches --minimal prune config)
|
||||
let selections = groups.iter().map(|g| g.types[0].minimal_selection()).collect();
|
||||
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
|
||||
Self {
|
||||
manifest,
|
||||
full_preset,
|
||||
groups,
|
||||
selections,
|
||||
preset: Some(SelectionPreset::Minimal),
|
||||
cursor: 0,
|
||||
list_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle_right(&mut self) {
|
||||
if let Some(group) = self.groups.get(self.cursor) {
|
||||
if group.required {
|
||||
return;
|
||||
}
|
||||
let presets = group.presets;
|
||||
let current = self.selections[self.cursor];
|
||||
let idx = presets.iter().position(|p| *p == current).unwrap_or(0);
|
||||
self.selections[self.cursor] = presets[(idx + 1) % presets.len()];
|
||||
self.preset = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle_left(&mut self) {
|
||||
if let Some(group) = self.groups.get(self.cursor) {
|
||||
if group.required {
|
||||
return;
|
||||
}
|
||||
let presets = group.presets;
|
||||
let current = self.selections[self.cursor];
|
||||
let idx = presets.iter().position(|p| *p == current).unwrap_or(0);
|
||||
self.selections[self.cursor] = presets[(idx + presets.len() - 1) % presets.len()];
|
||||
self.preset = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn select_all(&mut self) {
|
||||
for sel in &mut self.selections {
|
||||
*sel = ComponentSelection::All;
|
||||
}
|
||||
self.preset = Some(SelectionPreset::Archive);
|
||||
}
|
||||
|
||||
fn select_minimal(&mut self) {
|
||||
for (i, group) in self.groups.iter().enumerate() {
|
||||
self.selections[i] = group.types[0].minimal_selection();
|
||||
}
|
||||
self.preset = Some(SelectionPreset::Minimal);
|
||||
}
|
||||
|
||||
fn select_full(&mut self) {
|
||||
for (i, group) in self.groups.iter().enumerate() {
|
||||
let mut selection = group.types[0].minimal_selection();
|
||||
for ty in &group.types {
|
||||
if let Some(sel) = self.full_preset.get(ty).copied() {
|
||||
selection = sel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.selections[i] = selection;
|
||||
}
|
||||
self.preset = Some(SelectionPreset::Full);
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
} else {
|
||||
self.cursor = self.groups.len().saturating_sub(1);
|
||||
}
|
||||
self.list_state.select(Some(self.cursor));
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
if self.cursor < self.groups.len() - 1 {
|
||||
self.cursor += 1;
|
||||
} else {
|
||||
self.cursor = 0;
|
||||
}
|
||||
self.list_state.select(Some(self.cursor));
|
||||
}
|
||||
|
||||
/// Build the flat component→selection map from grouped selections.
|
||||
fn selection_map(&self) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
|
||||
let mut map = BTreeMap::new();
|
||||
for (group, sel) in self.groups.iter().zip(&self.selections) {
|
||||
for ty in &group.types {
|
||||
map.insert(*ty, *sel);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Size for a single group, summing all component types in the group.
|
||||
fn group_size(&self, group_idx: usize) -> u64 {
|
||||
let sel = self.selections[group_idx];
|
||||
let distance = match sel {
|
||||
ComponentSelection::None => return 0,
|
||||
ComponentSelection::All => None,
|
||||
ComponentSelection::Distance(d) => Some(d),
|
||||
};
|
||||
self.groups[group_idx]
|
||||
.types
|
||||
.iter()
|
||||
.map(|ty| self.manifest.size_for_distance(*ty, distance))
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn total_selected_size(&self) -> u64 {
|
||||
(0..self.groups.len()).map(|i| self.group_size(i)).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the interactive component selector TUI.
|
||||
pub fn run_selector(
|
||||
manifest: SnapshotManifest,
|
||||
full_preset: &BTreeMap<SnapshotComponentType, ComponentSelection>,
|
||||
) -> eyre::Result<SelectorOutput> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let mut app = SelectorApp::new(manifest, full_preset.clone());
|
||||
let result = event_loop(&mut terminal, &mut app);
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn event_loop(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
app: &mut SelectorApp,
|
||||
) -> eyre::Result<SelectorOutput> {
|
||||
let tick_rate = Duration::from_millis(100);
|
||||
let mut last_tick = Instant::now();
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| render(f, app))?;
|
||||
|
||||
let timeout =
|
||||
tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if crossterm::event::poll(timeout)? &&
|
||||
let Event::Key(key) = event::read()? &&
|
||||
key.kind == event::KeyEventKind::Press
|
||||
{
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
eyre::bail!("Download cancelled by user");
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
return Ok(SelectorOutput {
|
||||
selections: app.selection_map(),
|
||||
preset: app.preset,
|
||||
});
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char(' ') => app.cycle_right(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.cycle_left(),
|
||||
KeyCode::Char('a') => app.select_all(),
|
||||
KeyCode::Char('f') => app.select_full(),
|
||||
KeyCode::Char('m') => app.select_minimal(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.move_up(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.move_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_selection(sel: &ComponentSelection) -> String {
|
||||
match sel {
|
||||
ComponentSelection::All => "All".to_string(),
|
||||
ComponentSelection::Distance(d) => format!("Last {d} blocks"),
|
||||
ComponentSelection::None => "None".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(f: &mut Frame<'_>, app: &mut SelectorApp) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(8), // Component list
|
||||
Constraint::Length(3), // Footer
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Header
|
||||
let block_info = if app.manifest.block > 0 {
|
||||
format!(" (block {})", app.manifest.block)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let header = Paragraph::new(format!(" Select snapshot components to download{}", block_info))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.block(Block::default().borders(Borders::ALL).title("reth download"));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// Component list
|
||||
let items: Vec<ListItem<'_>> = app
|
||||
.groups
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, group)| {
|
||||
let sel = &app.selections[i];
|
||||
let sel_str = format_selection(sel);
|
||||
|
||||
let size = app.group_size(i);
|
||||
let size_str =
|
||||
if size > 0 { DownloadProgress::format_size(size) } else { String::new() };
|
||||
|
||||
let required = if group.required { " (required)" } else { "" };
|
||||
|
||||
let at_max = *sel == *group.presets.last().unwrap_or(&ComponentSelection::All);
|
||||
let at_min = *sel == group.presets[0];
|
||||
let arrows = if group.required {
|
||||
" "
|
||||
} else if at_max {
|
||||
"◂ "
|
||||
} else if at_min {
|
||||
" ▸"
|
||||
} else {
|
||||
"◂ ▸"
|
||||
};
|
||||
|
||||
let style = if group.required {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
} else if matches!(sel, ComponentSelection::None) {
|
||||
Style::default().fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::Green)
|
||||
};
|
||||
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(format!(" {:<22}", group.name), style),
|
||||
Span::styled(
|
||||
format!("{arrows} {:<12}", sel_str),
|
||||
style.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(format!("{:>10}", size_str), style.add_modifier(Modifier::DIM)),
|
||||
Span::styled(required.to_string(), Style::default().fg(Color::DarkGray)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let total_str = DownloadProgress::format_size(app.total_selected_size());
|
||||
let list = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(format!("Components — Total: {total_str}")),
|
||||
)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::DarkGray))
|
||||
.highlight_symbol("▸ ");
|
||||
f.render_stateful_widget(list, chunks[1], &mut app.list_state);
|
||||
|
||||
// Footer
|
||||
let footer = Paragraph::new(
|
||||
" [←/→] adjust [m] minimal [f] full [a] archive [Enter] confirm [Esc] cancel",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(footer, chunks[2]);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ pub struct InitStateCommand<C: ChainSpecParser> {
|
||||
/// Specifies whether to initialize the state without relying on EVM historical data.
|
||||
///
|
||||
/// When enabled, and before inserting the state, it creates a dummy chain up to the last EVM
|
||||
/// block specified. It then, appends the first block provided block.
|
||||
/// block specified. It then appends the first provided block.
|
||||
///
|
||||
/// - **Note**: **Do not** import receipts and blocks beforehand, or this will fail or be
|
||||
/// ignored.
|
||||
|
||||
@@ -125,12 +125,12 @@ pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser> NodeCommand<C> {
|
||||
/// Parsers only the default CLI arguments
|
||||
/// Parses only the default CLI arguments
|
||||
pub fn parse_args() -> Self {
|
||||
Self::parse()
|
||||
}
|
||||
|
||||
/// Parsers only the default [`NodeCommand`] arguments from the given iterator
|
||||
/// Parses only the default [`NodeCommand`] arguments from the given iterator
|
||||
pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
|
||||
@@ -95,6 +95,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
}
|
||||
};
|
||||
|
||||
if min_block > max_block {
|
||||
eyre::bail!("--from ({min_block}) is beyond --to ({max_block}), nothing to re-execute");
|
||||
}
|
||||
|
||||
let num_tasks = self.num_tasks.unwrap_or_else(|| {
|
||||
std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
|
||||
});
|
||||
|
||||
@@ -127,12 +127,14 @@ fn unwind_and_copy<N: ProviderNodeTypes>(
|
||||
AccountHashingStage {
|
||||
clean_threshold: u64::MAX,
|
||||
commit_threshold: u64::MAX,
|
||||
commit_entries: u64::MAX,
|
||||
etl_config: EtlConfig::default(),
|
||||
}
|
||||
.execute(&provider, execute_input)?;
|
||||
StorageHashingStage {
|
||||
clean_threshold: u64::MAX,
|
||||
commit_threshold: u64::MAX,
|
||||
commit_entries: u64::MAX,
|
||||
etl_config: EtlConfig::default(),
|
||||
}
|
||||
.execute(&provider, execute_input)?;
|
||||
|
||||
@@ -107,7 +107,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
Comp: CliNodeComponents<N>,
|
||||
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
|
||||
{
|
||||
// Quit early if the stages requires a commit and `--commit` is not provided.
|
||||
// Quit early if the stage requires a commit and `--commit` is not provided.
|
||||
if self.requires_commit() && !self.commit {
|
||||
return Err(eyre::eyre!(
|
||||
"The stage {} requires overwriting existing static files and must commit, but `--commit` was not provided. Please pass `--commit` and try again.",
|
||||
@@ -282,14 +282,22 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
),
|
||||
StageEnum::AccountHashing => (
|
||||
Box::new(AccountHashingStage::new(
|
||||
HashingConfig { clean_threshold: 1, commit_threshold: batch_size },
|
||||
HashingConfig {
|
||||
clean_threshold: 1,
|
||||
commit_threshold: batch_size,
|
||||
commit_entries: u64::MAX,
|
||||
},
|
||||
etl_config,
|
||||
)),
|
||||
None,
|
||||
),
|
||||
StageEnum::StorageHashing => (
|
||||
Box::new(StorageHashingStage::new(
|
||||
HashingConfig { clean_threshold: 1, commit_threshold: batch_size },
|
||||
HashingConfig {
|
||||
clean_threshold: 1,
|
||||
commit_threshold: batch_size,
|
||||
commit_entries: u64::MAX,
|
||||
},
|
||||
etl_config,
|
||||
)),
|
||||
None,
|
||||
|
||||
@@ -71,7 +71,12 @@ impl CliRunner {
|
||||
) -> Result<(), E>
|
||||
where
|
||||
F: Future<Output = Result<(), E>>,
|
||||
E: Send + Sync + From<std::io::Error> + From<reth_tasks::PanickedTaskError> + 'static,
|
||||
E: Send
|
||||
+ Sync
|
||||
+ std::fmt::Display
|
||||
+ From<std::io::Error>
|
||||
+ From<reth_tasks::PanickedTaskError>
|
||||
+ 'static,
|
||||
{
|
||||
let (context, task_manager_handle) = cli_context(&self.runtime);
|
||||
|
||||
@@ -81,8 +86,8 @@ impl CliRunner {
|
||||
run_until_ctrl_c(command(context)),
|
||||
));
|
||||
|
||||
if command_res.is_err() {
|
||||
error!(target: "reth::cli", "shutting down due to error");
|
||||
if let Err(err) = &command_res {
|
||||
error!(target: "reth::cli", %err, "shutting down due to error");
|
||||
} else {
|
||||
debug!(target: "reth::cli", "shutting down gracefully");
|
||||
// after the command has finished or exit signal was received we shutdown the
|
||||
@@ -105,7 +110,12 @@ impl CliRunner {
|
||||
) -> Result<(), E>
|
||||
where
|
||||
F: Future<Output = Result<(), E>> + Send + 'static,
|
||||
E: Send + Sync + From<std::io::Error> + From<reth_tasks::PanickedTaskError> + 'static,
|
||||
E: Send
|
||||
+ Sync
|
||||
+ std::fmt::Display
|
||||
+ From<std::io::Error>
|
||||
+ From<reth_tasks::PanickedTaskError>
|
||||
+ 'static,
|
||||
{
|
||||
let (context, task_manager_handle) = cli_context(&self.runtime);
|
||||
|
||||
@@ -122,8 +132,8 @@ impl CliRunner {
|
||||
),
|
||||
));
|
||||
|
||||
if command_res.is_err() {
|
||||
error!(target: "reth::cli", "shutting down due to error");
|
||||
if let Err(err) = &command_res {
|
||||
error!(target: "reth::cli", %err, "shutting down due to error");
|
||||
} else {
|
||||
debug!(target: "reth::cli", "shutting down gracefully");
|
||||
self.runtime.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
|
||||
|
||||
@@ -332,11 +332,15 @@ pub struct HashingConfig {
|
||||
pub clean_threshold: u64,
|
||||
/// The maximum number of entities to process before committing progress to the database.
|
||||
pub commit_threshold: u64,
|
||||
/// The maximum number of changeset entries to process before committing progress. The stage
|
||||
/// commits after either `commit_threshold` blocks or `commit_entries` entries, whichever
|
||||
/// comes first. This bounds memory usage when blocks contain many state changes.
|
||||
pub commit_entries: u64,
|
||||
}
|
||||
|
||||
impl Default for HashingConfig {
|
||||
fn default() -> Self {
|
||||
Self { clean_threshold: 500_000, commit_threshold: 100_000 }
|
||||
Self { clean_threshold: 500_000, commit_threshold: 100_000, commit_entries: 30_000_000 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ thiserror.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
trie-debug = []
|
||||
std = [
|
||||
"reth-execution-types/std",
|
||||
"reth-ethereum-primitives/std",
|
||||
|
||||
@@ -9,26 +9,6 @@ pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
|
||||
/// How close to the canonical head we persist blocks.
|
||||
pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
|
||||
|
||||
/// Returns the default number of storage worker threads based on available parallelism.
|
||||
fn default_storage_worker_count() -> usize {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
std::thread::available_parallelism().map_or(8, |n| n.get() * 2)
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
8
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default number of account worker threads.
|
||||
///
|
||||
/// Account workers coordinate storage proof collection and account trie traversal.
|
||||
/// They are set to the same count as storage workers for simplicity.
|
||||
fn default_account_worker_count() -> usize {
|
||||
default_storage_worker_count()
|
||||
}
|
||||
|
||||
/// The size of proof targets chunk to spawn in one multiproof calculation.
|
||||
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 5;
|
||||
|
||||
@@ -46,10 +26,15 @@ pub const DEFAULT_RESERVED_CPU_CORES: usize = 1;
|
||||
/// Depth 4 means we keep roughly 16^4 = 65536 potential branch paths at most.
|
||||
pub const DEFAULT_SPARSE_TRIE_PRUNE_DEPTH: usize = 4;
|
||||
|
||||
/// Default maximum number of storage tries to keep after pruning.
|
||||
/// Default LFU hot-slot capacity for sparse trie pruning.
|
||||
///
|
||||
/// Storage tries beyond this limit are cleared (but allocations preserved).
|
||||
pub const DEFAULT_SPARSE_TRIE_MAX_STORAGE_TRIES: usize = 100;
|
||||
/// Limits the number of `(address, slot)` pairs retained across prune cycles.
|
||||
pub const DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS: usize = 1500;
|
||||
|
||||
/// Default LFU hot-account capacity for sparse trie pruning.
|
||||
///
|
||||
/// Limits the number of account addresses retained across prune cycles.
|
||||
pub const DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS: usize = 1000;
|
||||
|
||||
/// Default timeout for the state root task before spawning a sequential fallback.
|
||||
pub const DEFAULT_STATE_ROOT_TASK_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
@@ -147,23 +132,32 @@ pub struct TreeConfig {
|
||||
always_process_payload_attributes_on_canonical_head: bool,
|
||||
/// Whether to unwind canonical header to ancestor during forkchoice updates.
|
||||
allow_unwind_canonical_header: bool,
|
||||
/// Number of storage proof worker threads.
|
||||
storage_worker_count: usize,
|
||||
/// Number of account proof worker threads.
|
||||
account_worker_count: usize,
|
||||
/// Whether to disable cache metrics recording (can be expensive with large cached state).
|
||||
disable_cache_metrics: bool,
|
||||
/// Depth for sparse trie pruning after state root computation.
|
||||
sparse_trie_prune_depth: usize,
|
||||
/// Maximum number of storage tries to retain after pruning.
|
||||
sparse_trie_max_storage_tries: usize,
|
||||
/// LFU hot-slot capacity: max `(address, slot)` pairs retained across prune cycles.
|
||||
sparse_trie_max_hot_slots: usize,
|
||||
/// LFU hot-account capacity: max account addresses retained across prune cycles.
|
||||
sparse_trie_max_hot_accounts: usize,
|
||||
/// When set, blocks whose total processing time (execution + state reads + state root +
|
||||
/// DB commit) exceeds this duration trigger a structured `warn!` log with detailed timing,
|
||||
/// state-operation counts, and cache hit-rate metrics. `Duration::ZERO` logs every block.
|
||||
slow_block_threshold: Option<Duration>,
|
||||
/// Whether to fully disable sparse trie cache pruning between blocks.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to use the arena-based sparse trie implementation.
|
||||
enable_arena_sparse_trie: bool,
|
||||
/// Timeout for the state root task before spawning a sequential fallback computation.
|
||||
/// If `Some`, after waiting this duration for the state root task, a sequential state root
|
||||
/// computation is spawned in parallel and whichever finishes first is used.
|
||||
/// If `None`, the timeout fallback is disabled.
|
||||
state_root_task_timeout: Option<Duration>,
|
||||
/// Maximum random jitter applied before each proof computation (trie-debug only).
|
||||
/// When set, each proof worker sleeps for a random duration up to this value
|
||||
/// before starting a proof calculation.
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for TreeConfig {
|
||||
@@ -187,13 +181,16 @@ impl Default for TreeConfig {
|
||||
state_root_fallback: false,
|
||||
always_process_payload_attributes_on_canonical_head: false,
|
||||
allow_unwind_canonical_header: false,
|
||||
storage_worker_count: default_storage_worker_count(),
|
||||
account_worker_count: default_account_worker_count(),
|
||||
disable_cache_metrics: false,
|
||||
sparse_trie_prune_depth: DEFAULT_SPARSE_TRIE_PRUNE_DEPTH,
|
||||
sparse_trie_max_storage_tries: DEFAULT_SPARSE_TRIE_MAX_STORAGE_TRIES,
|
||||
sparse_trie_max_hot_slots: DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
|
||||
sparse_trie_max_hot_accounts: DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS,
|
||||
slow_block_threshold: None,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
enable_arena_sparse_trie: false,
|
||||
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,11 +217,11 @@ impl TreeConfig {
|
||||
state_root_fallback: bool,
|
||||
always_process_payload_attributes_on_canonical_head: bool,
|
||||
allow_unwind_canonical_header: bool,
|
||||
storage_worker_count: usize,
|
||||
account_worker_count: usize,
|
||||
disable_cache_metrics: bool,
|
||||
sparse_trie_prune_depth: usize,
|
||||
sparse_trie_max_storage_tries: usize,
|
||||
sparse_trie_max_hot_slots: usize,
|
||||
sparse_trie_max_hot_accounts: usize,
|
||||
slow_block_threshold: Option<Duration>,
|
||||
state_root_task_timeout: Option<Duration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -246,13 +243,16 @@ impl TreeConfig {
|
||||
state_root_fallback,
|
||||
always_process_payload_attributes_on_canonical_head,
|
||||
allow_unwind_canonical_header,
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
disable_cache_metrics,
|
||||
sparse_trie_prune_depth,
|
||||
sparse_trie_max_storage_tries,
|
||||
sparse_trie_max_hot_slots,
|
||||
sparse_trie_max_hot_accounts,
|
||||
slow_block_threshold,
|
||||
disable_sparse_trie_cache_pruning: false,
|
||||
enable_arena_sparse_trie: false,
|
||||
state_root_task_timeout,
|
||||
#[cfg(feature = "trie-debug")]
|
||||
proof_jitter: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,42 +479,6 @@ impl TreeConfig {
|
||||
self.has_enough_parallelism && !self.legacy_state_root
|
||||
}
|
||||
|
||||
/// Return the number of storage proof worker threads.
|
||||
pub const fn storage_worker_count(&self) -> usize {
|
||||
self.storage_worker_count
|
||||
}
|
||||
|
||||
/// Setter for the number of storage proof worker threads.
|
||||
///
|
||||
/// No-op if it's [`None`].
|
||||
pub const fn with_storage_worker_count_opt(
|
||||
mut self,
|
||||
storage_worker_count: Option<usize>,
|
||||
) -> Self {
|
||||
if let Some(count) = storage_worker_count {
|
||||
self.storage_worker_count = count;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the number of account proof worker threads.
|
||||
pub const fn account_worker_count(&self) -> usize {
|
||||
self.account_worker_count
|
||||
}
|
||||
|
||||
/// Setter for the number of account proof worker threads.
|
||||
///
|
||||
/// No-op if it's [`None`].
|
||||
pub const fn with_account_worker_count_opt(
|
||||
mut self,
|
||||
account_worker_count: Option<usize>,
|
||||
) -> Self {
|
||||
if let Some(count) = account_worker_count {
|
||||
self.account_worker_count = count;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether cache metrics recording is disabled.
|
||||
pub const fn disable_cache_metrics(&self) -> bool {
|
||||
self.disable_cache_metrics
|
||||
@@ -537,14 +501,43 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the maximum number of storage tries to retain after pruning.
|
||||
pub const fn sparse_trie_max_storage_tries(&self) -> usize {
|
||||
self.sparse_trie_max_storage_tries
|
||||
/// Returns the LFU hot-slot capacity for sparse trie pruning.
|
||||
pub const fn sparse_trie_max_hot_slots(&self) -> usize {
|
||||
self.sparse_trie_max_hot_slots
|
||||
}
|
||||
|
||||
/// Setter for maximum storage tries to retain.
|
||||
pub const fn with_sparse_trie_max_storage_tries(mut self, max_tries: usize) -> Self {
|
||||
self.sparse_trie_max_storage_tries = max_tries;
|
||||
/// Setter for LFU hot-slot capacity.
|
||||
pub const fn with_sparse_trie_max_hot_slots(mut self, max_hot_slots: usize) -> Self {
|
||||
self.sparse_trie_max_hot_slots = max_hot_slots;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the LFU hot-account capacity for sparse trie pruning.
|
||||
pub const fn sparse_trie_max_hot_accounts(&self) -> usize {
|
||||
self.sparse_trie_max_hot_accounts
|
||||
}
|
||||
|
||||
/// Setter for LFU hot-account capacity.
|
||||
pub const fn with_sparse_trie_max_hot_accounts(mut self, max_hot_accounts: usize) -> Self {
|
||||
self.sparse_trie_max_hot_accounts = max_hot_accounts;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the slow block threshold, if configured.
|
||||
///
|
||||
/// When `Some`, blocks whose total processing time exceeds this duration emit a structured
|
||||
/// warning with timing, state-operation, and cache-hit-rate details. `Duration::ZERO` logs
|
||||
/// every block.
|
||||
pub const fn slow_block_threshold(&self) -> Option<Duration> {
|
||||
self.slow_block_threshold
|
||||
}
|
||||
|
||||
/// Setter for slow block threshold.
|
||||
pub const fn with_slow_block_threshold(
|
||||
mut self,
|
||||
slow_block_threshold: Option<Duration>,
|
||||
) -> Self {
|
||||
self.slow_block_threshold = slow_block_threshold;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -559,6 +552,17 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether the arena-based sparse trie is enabled.
|
||||
pub const fn enable_arena_sparse_trie(&self) -> bool {
|
||||
self.enable_arena_sparse_trie
|
||||
}
|
||||
|
||||
/// Setter for whether to enable the arena-based sparse trie.
|
||||
pub const fn with_enable_arena_sparse_trie(mut self, value: bool) -> Self {
|
||||
self.enable_arena_sparse_trie = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the state root task timeout.
|
||||
pub const fn state_root_task_timeout(&self) -> Option<Duration> {
|
||||
self.state_root_task_timeout
|
||||
@@ -569,4 +573,17 @@ impl TreeConfig {
|
||||
self.state_root_task_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the proof jitter duration, if configured (trie-debug only).
|
||||
#[cfg(feature = "trie-debug")]
|
||||
pub const fn proof_jitter(&self) -> Option<Duration> {
|
||||
self.proof_jitter
|
||||
}
|
||||
|
||||
/// Setter for proof jitter (trie-debug only).
|
||||
#[cfg(feature = "trie-debug")]
|
||||
pub const fn with_proof_jitter(mut self, proof_jitter: Option<Duration>) -> Self {
|
||||
self.proof_jitter = proof_jitter;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use core::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
time::Duration,
|
||||
};
|
||||
use reth_chain_state::ExecutedBlock;
|
||||
use reth_chain_state::{ExecutedBlock, ExecutionTimingStats};
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_primitives_traits::{NodePrimitives, SealedBlock, SealedHeader};
|
||||
|
||||
@@ -32,6 +32,8 @@ pub enum ConsensusEngineEvent<N: NodePrimitives = EthPrimitives> {
|
||||
CanonicalChainCommitted(Box<SealedHeader<N::BlockHeader>>, Duration),
|
||||
/// The consensus engine processed an invalid block.
|
||||
InvalidBlock(Box<SealedBlock<N::Block>>),
|
||||
/// A slow block was detected after persistence, with its timing statistics.
|
||||
SlowBlock(SlowBlockInfo),
|
||||
}
|
||||
|
||||
impl<N: NodePrimitives> ConsensusEngineEvent<N> {
|
||||
@@ -73,6 +75,25 @@ where
|
||||
Self::BlockReceived(num_hash) => {
|
||||
write!(f, "BlockReceived({num_hash:?})")
|
||||
}
|
||||
Self::SlowBlock(info) => {
|
||||
write!(
|
||||
f,
|
||||
"SlowBlock(block={}, total={:?})",
|
||||
info.stats.block_number, info.total_duration
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a slow block detected after persistence.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SlowBlockInfo {
|
||||
/// The timing statistics for the slow block.
|
||||
pub stats: Box<ExecutionTimingStats>,
|
||||
/// The commit duration for the batch containing this block.
|
||||
pub commit_duration: Duration,
|
||||
/// The total duration (execution + `state_root` + commit).
|
||||
/// Note: `state_read` is a subset of execution and is not added separately.
|
||||
pub total_duration: Duration,
|
||||
}
|
||||
|
||||
@@ -98,22 +98,12 @@ reth-e2e-test-utils.workspace = true
|
||||
revm-state.workspace = true
|
||||
|
||||
assert_matches.workspace = true
|
||||
criterion.workspace = true
|
||||
eyre.workspace = true
|
||||
serde_json.workspace = true
|
||||
crossbeam-channel.workspace = true
|
||||
proptest.workspace = true
|
||||
rand.workspace = true
|
||||
rand_08.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "channel_perf"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "state_root_task"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
test-utils = [
|
||||
"reth-chain-state/test-utils",
|
||||
@@ -143,7 +133,12 @@ test-utils = [
|
||||
"reth-evm-ethereum/test-utils",
|
||||
"reth-tasks/test-utils",
|
||||
]
|
||||
trie-debug = ["reth-trie-sparse/trie-debug", "dep:serde_json"]
|
||||
trie-debug = [
|
||||
"reth-trie-sparse/trie-debug",
|
||||
"reth-trie-parallel/trie-debug",
|
||||
"reth-engine-primitives/trie-debug",
|
||||
"dep:serde_json",
|
||||
]
|
||||
rocksdb = [
|
||||
"reth-provider/rocksdb",
|
||||
"reth-prune/rocksdb",
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
//! Benchmark comparing `std::sync::mpsc` and `crossbeam` channels for `StateRootTask`.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use alloy_primitives::{B256, U256};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
||||
use proptest::test_runner::TestRunner;
|
||||
use rand::Rng;
|
||||
use revm_primitives::{Address, HashMap};
|
||||
use revm_state::{Account, AccountInfo, AccountStatus, EvmState, EvmStorage, EvmStorageSlot};
|
||||
use std::{hint::black_box, thread};
|
||||
|
||||
/// Creates a mock state with the specified number of accounts for benchmarking
|
||||
fn create_bench_state(num_accounts: usize) -> EvmState {
|
||||
let mut runner = TestRunner::deterministic();
|
||||
let mut rng = runner.rng().clone();
|
||||
let mut state_changes = HashMap::default();
|
||||
|
||||
for i in 0..num_accounts {
|
||||
let storage =
|
||||
EvmStorage::from_iter([(U256::from(i), EvmStorageSlot::new(U256::from(i + 1), 0))]);
|
||||
|
||||
let account = Account {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(100),
|
||||
nonce: 10,
|
||||
code_hash: B256::from_slice(&rng.random::<[u8; 32]>()),
|
||||
code: Default::default(),
|
||||
account_id: None,
|
||||
},
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
storage,
|
||||
status: AccountStatus::empty(),
|
||||
transaction_id: 0,
|
||||
};
|
||||
|
||||
let address = Address::with_last_byte(i as u8);
|
||||
state_changes.insert(address, account);
|
||||
}
|
||||
|
||||
state_changes
|
||||
}
|
||||
|
||||
/// Simulated `StateRootTask` with `std::sync::mpsc`
|
||||
struct StdStateRootTask {
|
||||
rx: std::sync::mpsc::Receiver<EvmState>,
|
||||
}
|
||||
|
||||
impl StdStateRootTask {
|
||||
const fn new(rx: std::sync::mpsc::Receiver<EvmState>) -> Self {
|
||||
Self { rx }
|
||||
}
|
||||
|
||||
fn run(self) {
|
||||
while let Ok(state) = self.rx.recv() {
|
||||
black_box(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulated `StateRootTask` with `crossbeam-channel`
|
||||
struct CrossbeamStateRootTask {
|
||||
rx: crossbeam_channel::Receiver<EvmState>,
|
||||
}
|
||||
|
||||
impl CrossbeamStateRootTask {
|
||||
const fn new(rx: crossbeam_channel::Receiver<EvmState>) -> Self {
|
||||
Self { rx }
|
||||
}
|
||||
|
||||
fn run(self) {
|
||||
while let Ok(state) = self.rx.recv() {
|
||||
black_box(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmarks the performance of different channel implementations for state streaming
|
||||
fn bench_state_stream(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("state_stream_channels");
|
||||
group.sample_size(10);
|
||||
|
||||
for size in &[1, 10, 100] {
|
||||
let bench_setup = || {
|
||||
let states: Vec<_> = (0..100).map(|_| create_bench_state(*size)).collect();
|
||||
states
|
||||
};
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("std_channel", size), size, |b, _| {
|
||||
b.iter_batched(
|
||||
bench_setup,
|
||||
|states| {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let task = StdStateRootTask::new(rx);
|
||||
|
||||
let processor = thread::spawn(move || {
|
||||
task.run();
|
||||
});
|
||||
|
||||
for state in states {
|
||||
tx.send(state).unwrap();
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
processor.join().unwrap();
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("crossbeam_channel", size), size, |b, _| {
|
||||
b.iter_batched(
|
||||
bench_setup,
|
||||
|states| {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let task = CrossbeamStateRootTask::new(rx);
|
||||
|
||||
let processor = thread::spawn(move || {
|
||||
task.run();
|
||||
});
|
||||
|
||||
for state in states {
|
||||
tx.send(state).unwrap();
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
processor.join().unwrap();
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_state_stream);
|
||||
criterion_main!(benches);
|
||||
@@ -1,272 +0,0 @@
|
||||
//! Benchmark for `StateRootTask` complete workflow, including sending state
|
||||
//! updates using the incoming messages sender and waiting for the final result.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use alloy_consensus::constants::KECCAK_EMPTY;
|
||||
use alloy_evm::block::StateChangeSource;
|
||||
use alloy_primitives::{Address, B256};
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use proptest::test_runner::TestRunner;
|
||||
use rand::Rng;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_db_common::init::init_genesis;
|
||||
use reth_engine_tree::tree::{
|
||||
precompile_cache::PrecompileCacheMap, ExecutionEnv, PayloadProcessor, StateProviderBuilder,
|
||||
TreeConfig,
|
||||
};
|
||||
use reth_ethereum_primitives::TransactionSigned;
|
||||
use reth_evm::OnStateHook;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_primitives_traits::{Account as RethAccount, Recovered, StorageEntry};
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, OverlayStateProviderFactory},
|
||||
test_utils::{create_test_provider_factory_with_chain_spec, MockNodeTypesWithDB},
|
||||
AccountReader, ChainSpecProvider, HashingWriter, ProviderFactory,
|
||||
};
|
||||
use revm_primitives::{HashMap, U256};
|
||||
use revm_state::{Account as RevmAccount, AccountInfo, AccountStatus, EvmState, EvmStorageSlot};
|
||||
use std::{hint::black_box, sync::Arc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BenchParams {
|
||||
num_accounts: usize,
|
||||
updates_per_account: usize,
|
||||
storage_slots_per_account: usize,
|
||||
selfdestructs_per_update: usize,
|
||||
}
|
||||
|
||||
/// Generates a series of random state updates with configurable accounts,
|
||||
/// storage, and self-destructs
|
||||
fn create_bench_state_updates(params: &BenchParams) -> Vec<EvmState> {
|
||||
let mut runner = TestRunner::deterministic();
|
||||
let mut rng = runner.rng().clone();
|
||||
let all_addresses: Vec<Address> =
|
||||
(0..params.num_accounts).map(|_| Address::random_with(&mut rng)).collect();
|
||||
let mut updates = Vec::with_capacity(params.updates_per_account);
|
||||
|
||||
for _ in 0..params.updates_per_account {
|
||||
let mut state_update = EvmState::default();
|
||||
let num_accounts_in_update = rng.random_range(1..=params.num_accounts);
|
||||
|
||||
// regular updates for randomly selected accounts
|
||||
for &address in &all_addresses[0..num_accounts_in_update] {
|
||||
// randomly choose to self-destruct with probability
|
||||
// (selfdestructs/accounts)
|
||||
let is_selfdestruct = rng
|
||||
.random_bool(params.selfdestructs_per_update as f64 / params.num_accounts as f64);
|
||||
|
||||
let account = if is_selfdestruct {
|
||||
RevmAccount {
|
||||
info: AccountInfo::default(),
|
||||
storage: HashMap::default(),
|
||||
status: AccountStatus::SelfDestructed,
|
||||
transaction_id: 0,
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
}
|
||||
} else {
|
||||
RevmAccount {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
},
|
||||
storage: (0..rng.random_range(0..=params.storage_slots_per_account))
|
||||
.map(|_| {
|
||||
(
|
||||
U256::from(rng.random::<u64>()),
|
||||
EvmStorageSlot::new_changed(
|
||||
U256::ZERO,
|
||||
U256::from(rng.random::<u64>()),
|
||||
0,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
status: AccountStatus::Touched,
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
transaction_id: 0,
|
||||
}
|
||||
};
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
|
||||
updates.push(state_update);
|
||||
}
|
||||
|
||||
updates
|
||||
}
|
||||
|
||||
fn convert_revm_to_reth_account(revm_account: &RevmAccount) -> Option<RethAccount> {
|
||||
match revm_account.status {
|
||||
AccountStatus::SelfDestructed => None,
|
||||
_ => Some(RethAccount {
|
||||
balance: revm_account.info.balance,
|
||||
nonce: revm_account.info.nonce,
|
||||
bytecode_hash: if revm_account.info.code_hash == KECCAK_EMPTY {
|
||||
None
|
||||
} else {
|
||||
Some(revm_account.info.code_hash)
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies state updates to the provider, ensuring self-destructs only affect
|
||||
/// existing accounts
|
||||
fn setup_provider(
|
||||
factory: &ProviderFactory<MockNodeTypesWithDB>,
|
||||
state_updates: &[EvmState],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for update in state_updates {
|
||||
let provider_rw = factory.provider_rw()?;
|
||||
|
||||
let mut account_updates = Vec::with_capacity(update.len());
|
||||
|
||||
for (address, account) in update {
|
||||
// only process self-destructs if account exists, always process
|
||||
// other updates
|
||||
let should_process = match account.status {
|
||||
AccountStatus::SelfDestructed => {
|
||||
provider_rw.basic_account(address).ok().flatten().is_some()
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if should_process {
|
||||
account_updates.push((
|
||||
*address,
|
||||
convert_revm_to_reth_account(account),
|
||||
(account.status == AccountStatus::Touched).then(|| {
|
||||
account
|
||||
.storage
|
||||
.iter()
|
||||
.map(|(slot, value)| StorageEntry {
|
||||
key: B256::from(*slot),
|
||||
value: value.present_value,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// update in the provider account and its storage (if available)
|
||||
for (address, account, maybe_storage) in account_updates {
|
||||
provider_rw.insert_account_for_hashing(std::iter::once((address, account)))?;
|
||||
if let Some(storage) = maybe_storage {
|
||||
provider_rw
|
||||
.insert_storage_for_hashing(std::iter::once((address, storage.into_iter())))?;
|
||||
}
|
||||
}
|
||||
|
||||
provider_rw.commit()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bench_state_root(c: &mut Criterion) {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let mut group = c.benchmark_group("state_root");
|
||||
|
||||
let scenarios = vec![
|
||||
BenchParams {
|
||||
num_accounts: 100,
|
||||
updates_per_account: 5,
|
||||
storage_slots_per_account: 10,
|
||||
selfdestructs_per_update: 2,
|
||||
},
|
||||
BenchParams {
|
||||
num_accounts: 1000,
|
||||
updates_per_account: 10,
|
||||
storage_slots_per_account: 20,
|
||||
selfdestructs_per_update: 5,
|
||||
},
|
||||
BenchParams {
|
||||
num_accounts: 500,
|
||||
updates_per_account: 8,
|
||||
storage_slots_per_account: 15,
|
||||
selfdestructs_per_update: 20,
|
||||
},
|
||||
];
|
||||
|
||||
for params in scenarios {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(
|
||||
"state_root_task",
|
||||
format!(
|
||||
"accounts_{}_updates_{}_slots_{}_selfdestructs_{}",
|
||||
params.num_accounts,
|
||||
params.updates_per_account,
|
||||
params.storage_slots_per_account,
|
||||
params.selfdestructs_per_update
|
||||
),
|
||||
),
|
||||
¶ms,
|
||||
|b, params| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let factory = create_test_provider_factory_with_chain_spec(Arc::new(
|
||||
ChainSpec::default(),
|
||||
));
|
||||
let genesis_hash = init_genesis(&factory).unwrap();
|
||||
let state_updates = create_bench_state_updates(params);
|
||||
setup_provider(&factory, &state_updates).expect("failed to setup provider");
|
||||
|
||||
let payload_processor = PayloadProcessor::new(
|
||||
reth_tasks::Runtime::test(),
|
||||
EthEvmConfig::new(factory.chain_spec()),
|
||||
&TreeConfig::default(),
|
||||
PrecompileCacheMap::default(),
|
||||
);
|
||||
let provider = BlockchainProvider::new(factory).unwrap();
|
||||
|
||||
(genesis_hash, payload_processor, provider, state_updates)
|
||||
},
|
||||
|(genesis_hash, mut payload_processor, provider, state_updates)| {
|
||||
black_box({
|
||||
let mut handle = payload_processor.spawn(
|
||||
ExecutionEnv::test_default(),
|
||||
(
|
||||
Vec::<
|
||||
Result<
|
||||
Recovered<TransactionSigned>,
|
||||
core::convert::Infallible,
|
||||
>,
|
||||
>::new(),
|
||||
std::convert::identity,
|
||||
),
|
||||
StateProviderBuilder::new(provider.clone(), genesis_hash, None),
|
||||
OverlayStateProviderFactory::new(
|
||||
provider,
|
||||
reth_trie_db::ChangesetCache::new(),
|
||||
),
|
||||
&TreeConfig::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut state_hook = handle.state_hook();
|
||||
|
||||
for (i, update) in state_updates.into_iter().enumerate() {
|
||||
state_hook.on_state(StateChangeSource::Transaction(i), &update);
|
||||
}
|
||||
drop(state_hook);
|
||||
|
||||
handle.state_root().expect("task failed")
|
||||
});
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_state_root);
|
||||
criterion_main!(benches);
|
||||
@@ -64,6 +64,7 @@ pub fn build_engine_orchestrator<N, Client, S, V, C>(
|
||||
sync_metrics_tx: MetricEventsSender,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
runtime: Runtime,
|
||||
) -> ChainOrchestrator<
|
||||
EngineHandler<
|
||||
EngineApiRequestHandler<EngineApiRequest<N::Payload, N::Primitives>, N::Primitives>,
|
||||
@@ -99,6 +100,7 @@ where
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
);
|
||||
|
||||
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
|
||||
|
||||
@@ -18,10 +18,20 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
/// Unified result of any persistence operation.
|
||||
#[derive(Debug)]
|
||||
pub struct PersistenceResult {
|
||||
/// The last block that was persisted, if any.
|
||||
pub last_block: Option<BlockNumHash>,
|
||||
/// The commit duration, only available for save-blocks operations.
|
||||
pub commit_duration: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Writes parts of reth's in memory tree state to the database and static files.
|
||||
///
|
||||
/// This is meant to be a spawned service that listens for various incoming persistence operations,
|
||||
@@ -86,18 +96,16 @@ where
|
||||
while let Ok(action) = self.incoming.recv() {
|
||||
match action {
|
||||
PersistenceAction::RemoveBlocksAbove(new_tip_num, sender) => {
|
||||
let result = self.on_remove_blocks_above(new_tip_num)?;
|
||||
let last_block = self.on_remove_blocks_above(new_tip_num)?;
|
||||
// send new sync metrics based on removed blocks
|
||||
let _ =
|
||||
self.sync_metrics_tx.send(MetricEvent::SyncHeight { height: new_tip_num });
|
||||
// we ignore the error because the caller may or may not care about the result
|
||||
let _ = sender.send(result);
|
||||
let _ = sender.send(PersistenceResult { last_block, commit_duration: None });
|
||||
}
|
||||
PersistenceAction::SaveBlocks(blocks, sender) => {
|
||||
let result = self.on_save_blocks(blocks)?;
|
||||
let result_number = result.map(|r| r.number);
|
||||
let result_number = result.last_block.map(|b| b.number);
|
||||
|
||||
// we ignore the error because the caller may or may not care about the result
|
||||
let _ = sender.send(result);
|
||||
|
||||
if let Some(block_number) = result_number {
|
||||
@@ -140,7 +148,7 @@ where
|
||||
fn on_save_blocks(
|
||||
&mut self,
|
||||
blocks: Vec<ExecutedBlock<N::Primitives>>,
|
||||
) -> Result<Option<BlockNumHash>, PersistenceError> {
|
||||
) -> Result<PersistenceResult, PersistenceError> {
|
||||
let first_block = blocks.first().map(|b| b.recovered_block.num_hash());
|
||||
let last_block = blocks.last().map(|b| b.recovered_block.num_hash());
|
||||
let block_count = blocks.len();
|
||||
@@ -157,10 +165,16 @@ where
|
||||
provider_rw.save_blocks(blocks, SaveBlocksMode::Full)?;
|
||||
|
||||
if let Some(finalized) = pending_finalized {
|
||||
provider_rw.save_finalized_block_number(finalized)?;
|
||||
provider_rw.save_finalized_block_number(finalized.min(last.number))?;
|
||||
if finalized > last.number {
|
||||
self.pending_finalized_block = Some(finalized);
|
||||
}
|
||||
}
|
||||
if let Some(safe) = pending_safe {
|
||||
provider_rw.save_safe_block_number(safe)?;
|
||||
provider_rw.save_safe_block_number(safe.min(last.number))?;
|
||||
if safe > last.number {
|
||||
self.pending_safe_block = Some(safe);
|
||||
}
|
||||
}
|
||||
|
||||
if self.pruner.is_pruning_needed(last.number) {
|
||||
@@ -175,10 +189,11 @@ where
|
||||
|
||||
debug!(target: "engine::persistence", first=?first_block, last=?last_block, "Saved range of blocks");
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
self.metrics.save_blocks_batch_size.record(block_count as f64);
|
||||
self.metrics.save_blocks_duration_seconds.record(start_time.elapsed());
|
||||
self.metrics.save_blocks_duration_seconds.record(elapsed);
|
||||
|
||||
Ok(last_block)
|
||||
Ok(PersistenceResult { last_block, commit_duration: Some(elapsed) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,13 +217,13 @@ pub enum PersistenceAction<N: NodePrimitives = EthPrimitives> {
|
||||
///
|
||||
/// First, header, transaction, and receipt-related data should be written to static files.
|
||||
/// Then the execution history-related data will be written to the database.
|
||||
SaveBlocks(Vec<ExecutedBlock<N>>, CrossbeamSender<Option<BlockNumHash>>),
|
||||
SaveBlocks(Vec<ExecutedBlock<N>>, CrossbeamSender<PersistenceResult>),
|
||||
|
||||
/// Removes block data above the given block number from the database.
|
||||
///
|
||||
/// This will first update checkpoints from the database, then remove actual block data from
|
||||
/// static files.
|
||||
RemoveBlocksAbove(u64, CrossbeamSender<Option<BlockNumHash>>),
|
||||
RemoveBlocksAbove(u64, CrossbeamSender<PersistenceResult>),
|
||||
|
||||
/// Update the persisted finalized block on disk
|
||||
SaveFinalizedBlock(u64),
|
||||
@@ -287,7 +302,7 @@ impl<T: NodePrimitives> PersistenceHandle<T> {
|
||||
pub fn save_blocks(
|
||||
&self,
|
||||
blocks: Vec<ExecutedBlock<T>>,
|
||||
tx: CrossbeamSender<Option<BlockNumHash>>,
|
||||
tx: CrossbeamSender<PersistenceResult>,
|
||||
) -> Result<(), SendError<PersistenceAction<T>>> {
|
||||
self.send_action(PersistenceAction::SaveBlocks(blocks, tx))
|
||||
}
|
||||
@@ -322,7 +337,7 @@ impl<T: NodePrimitives> PersistenceHandle<T> {
|
||||
pub fn remove_blocks_above(
|
||||
&self,
|
||||
block_num: u64,
|
||||
tx: CrossbeamSender<Option<BlockNumHash>>,
|
||||
tx: CrossbeamSender<PersistenceResult>,
|
||||
) -> Result<(), SendError<PersistenceAction<T>>> {
|
||||
self.send_action(PersistenceAction::RemoveBlocksAbove(block_num, tx))
|
||||
}
|
||||
@@ -382,8 +397,8 @@ mod tests {
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
|
||||
let hash = rx.recv().unwrap();
|
||||
assert_eq!(hash, None);
|
||||
let result = rx.recv().unwrap();
|
||||
assert!(result.last_block.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -401,12 +416,9 @@ mod tests {
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
|
||||
let BlockNumHash { hash: actual_hash, number: _ } = rx
|
||||
.recv_timeout(std::time::Duration::from_secs(10))
|
||||
.expect("test timed out")
|
||||
.expect("no hash returned");
|
||||
let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("test timed out");
|
||||
|
||||
assert_eq!(block_hash, actual_hash);
|
||||
assert_eq!(block_hash, result.last_block.unwrap().hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -420,8 +432,8 @@ mod tests {
|
||||
let (tx, rx) = crossbeam_channel::bounded(1);
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
let BlockNumHash { hash: actual_hash, number: _ } = rx.recv().unwrap().unwrap();
|
||||
assert_eq!(last_hash, actual_hash);
|
||||
let result = rx.recv().unwrap();
|
||||
assert_eq!(last_hash, result.last_block.unwrap().hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -438,8 +450,8 @@ mod tests {
|
||||
|
||||
handle.save_blocks(blocks, tx).unwrap();
|
||||
|
||||
let BlockNumHash { hash: actual_hash, number: _ } = rx.recv().unwrap().unwrap();
|
||||
assert_eq!(last_hash, actual_hash);
|
||||
let result = rx.recv().unwrap();
|
||||
assert_eq!(last_hash, result.last_block.unwrap().hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ use reth_trie::{
|
||||
updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
|
||||
MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
|
||||
};
|
||||
use revm_primitives::eip7907::MAX_CODE_SIZE;
|
||||
use std::{
|
||||
mem::size_of,
|
||||
sync::{
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
@@ -56,8 +54,17 @@ const fn fixed_cache_key_size_with_value<K>(value: usize) -> usize {
|
||||
raw_size.div_ceil(FIXED_CACHE_ALIGNMENT) * FIXED_CACHE_ALIGNMENT
|
||||
}
|
||||
|
||||
/// Size in bytes of a single code cache entry.
|
||||
const CODE_CACHE_ENTRY_SIZE: usize = fixed_cache_key_size_with_value::<Address>(MAX_CODE_SIZE);
|
||||
/// Estimated average bytecode size for cache budget calculation.
|
||||
///
|
||||
/// The fixed-cache stores `Option<Bytecode>` inline (pointer-sized), but each cached contract
|
||||
/// also holds bytecode on the heap. For budget estimation we use 8 KiB, which is close to the
|
||||
/// observed mainnet average (~7 KiB). Using `MAX_CODE_SIZE` (48 KiB) overestimates by ~7x,
|
||||
/// yielding only 4096 entries for a 228 MB code-cache budget when 16384 fit comfortably.
|
||||
const ESTIMATED_AVG_CODE_SIZE: usize = 8 * 1024;
|
||||
|
||||
/// Size in bytes of a single code cache entry (inline metadata + estimated heap).
|
||||
const CODE_CACHE_ENTRY_SIZE: usize =
|
||||
fixed_cache_key_size_with_value::<Address>(ESTIMATED_AVG_CODE_SIZE);
|
||||
|
||||
/// Size in bytes of a single storage cache entry.
|
||||
const STORAGE_CACHE_ENTRY_SIZE: usize =
|
||||
@@ -94,6 +101,10 @@ pub struct CachedStateProvider<S, const PREWARM: bool = false> {
|
||||
|
||||
/// Metrics for the cached state provider
|
||||
metrics: CachedStateMetrics,
|
||||
|
||||
/// Optional cache statistics for detailed block logging. Only tracked when slow block
|
||||
/// threshold is configured.
|
||||
cache_stats: Option<Arc<CacheStats>>,
|
||||
}
|
||||
|
||||
impl<S> CachedStateProvider<S> {
|
||||
@@ -104,7 +115,7 @@ impl<S> CachedStateProvider<S> {
|
||||
caches: ExecutionCache,
|
||||
metrics: CachedStateMetrics,
|
||||
) -> Self {
|
||||
Self { state_provider, caches, metrics }
|
||||
Self { state_provider, caches, metrics, cache_stats: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,14 +126,28 @@ impl<S> CachedStateProvider<S, true> {
|
||||
caches: ExecutionCache,
|
||||
metrics: CachedStateMetrics,
|
||||
) -> Self {
|
||||
Self { state_provider, caches, metrics }
|
||||
Self { state_provider, caches, metrics, cache_stats: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for the cached state provider, showing hits / misses / size for each cache.
|
||||
///
|
||||
/// This struct combines both the provider-level metrics (hits/misses tracked by the provider)
|
||||
/// and the fixed-cache internal stats (collisions, size, capacity).
|
||||
impl<S, const PREWARM: bool> CachedStateProvider<S, PREWARM> {
|
||||
/// Enables cache statistics tracking for detailed block logging.
|
||||
pub fn with_cache_stats(mut self, stats: Option<Arc<CacheStats>>) -> Self {
|
||||
self.cache_stats = stats;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the status of a key in the cache.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CachedStatus<T> {
|
||||
/// The key is not in the cache (or was invalidated). The value was recalculated.
|
||||
NotCached(T),
|
||||
/// The key exists in cache and has a specific value.
|
||||
Cached(T),
|
||||
}
|
||||
|
||||
/// Metrics for the cached state provider, showing hits / misses for each cache
|
||||
#[derive(Metrics, Clone)]
|
||||
#[metrics(scope = "sync.caching")]
|
||||
pub struct CachedStateMetrics {
|
||||
@@ -211,6 +236,73 @@ impl CachedStateMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache hit/miss statistics for detailed block logging.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CacheStats {
|
||||
/// Account cache hits
|
||||
account_hits: AtomicUsize,
|
||||
/// Account cache misses
|
||||
account_misses: AtomicUsize,
|
||||
/// Storage cache hits
|
||||
storage_hits: AtomicUsize,
|
||||
/// Storage cache misses
|
||||
storage_misses: AtomicUsize,
|
||||
/// Code cache hits
|
||||
code_hits: AtomicUsize,
|
||||
/// Code cache misses
|
||||
code_misses: AtomicUsize,
|
||||
}
|
||||
|
||||
impl CacheStats {
|
||||
pub(crate) fn record_account_hit(&self) {
|
||||
self.account_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_account_miss(&self) {
|
||||
self.account_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn account_hits(&self) -> usize {
|
||||
self.account_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn account_misses(&self) -> usize {
|
||||
self.account_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn record_storage_hit(&self) {
|
||||
self.storage_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_storage_miss(&self) {
|
||||
self.storage_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn storage_hits(&self) -> usize {
|
||||
self.storage_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn storage_misses(&self) -> usize {
|
||||
self.storage_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn record_code_hit(&self) {
|
||||
self.code_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn record_code_miss(&self) {
|
||||
self.code_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn code_hits(&self) -> usize {
|
||||
self.code_hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn code_misses(&self) -> usize {
|
||||
self.code_misses.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stats handler for fixed-cache that tracks collisions and size.
|
||||
///
|
||||
/// Note: Hits and misses are tracked directly by the [`CachedStateProvider`] via
|
||||
@@ -306,27 +398,36 @@ impl<S: AccountReader, const PREWARM: bool> AccountReader for CachedStateProvide
|
||||
match self.caches.get_or_try_insert_account_with(*address, || {
|
||||
self.state_provider.basic_account(address)
|
||||
})? {
|
||||
CachedStatus::NotCached(value) | CachedStatus::Cached(value) => Ok(value),
|
||||
// During prewarm we only record stats (not prometheus metrics)
|
||||
CachedStatus::NotCached(value) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_account_miss();
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
CachedStatus::Cached(value) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_account_hit();
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
} else if let Some(account) = self.caches.0.account_cache.get(address) {
|
||||
self.metrics.account_cache_hits.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_account_hit();
|
||||
}
|
||||
Ok(account)
|
||||
} else {
|
||||
self.metrics.account_cache_misses.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_account_miss();
|
||||
}
|
||||
self.state_provider.basic_account(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the status of a key in the cache.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CachedStatus<T> {
|
||||
/// The key is not in the cache (or was invalidated). The value was recalculated.
|
||||
NotCached(T),
|
||||
/// The key exists in cache and has a specific value.
|
||||
Cached(T),
|
||||
}
|
||||
|
||||
impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvider<S, PREWARM> {
|
||||
fn storage(
|
||||
&self,
|
||||
@@ -337,17 +438,31 @@ impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvide
|
||||
match self.caches.get_or_try_insert_storage_with(account, storage_key, || {
|
||||
self.state_provider.storage(account, storage_key).map(Option::unwrap_or_default)
|
||||
})? {
|
||||
CachedStatus::NotCached(value) | CachedStatus::Cached(value) => {
|
||||
// The slot that was never written to is indistinguishable from a slot
|
||||
// explicitly set to zero. We return `None` in both cases.
|
||||
// During prewarm we only record stats (not prometheus metrics)
|
||||
CachedStatus::NotCached(value) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_storage_miss();
|
||||
}
|
||||
Ok(Some(value).filter(|v| !v.is_zero()))
|
||||
}
|
||||
CachedStatus::Cached(value) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_storage_hit();
|
||||
}
|
||||
Ok(Some(value).filter(|v| !v.is_zero()))
|
||||
}
|
||||
}
|
||||
} else if let Some(value) = self.caches.0.storage_cache.get(&(account, storage_key)) {
|
||||
self.metrics.storage_cache_hits.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_storage_hit();
|
||||
}
|
||||
Ok(Some(value).filter(|v| !v.is_zero()))
|
||||
} else {
|
||||
self.metrics.storage_cache_misses.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_storage_miss();
|
||||
}
|
||||
self.state_provider.storage(account, storage_key)
|
||||
}
|
||||
}
|
||||
@@ -359,13 +474,31 @@ impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvi
|
||||
match self.caches.get_or_try_insert_code_with(*code_hash, || {
|
||||
self.state_provider.bytecode_by_hash(code_hash)
|
||||
})? {
|
||||
CachedStatus::NotCached(code) | CachedStatus::Cached(code) => Ok(code),
|
||||
// During prewarm we only record stats (not prometheus metrics)
|
||||
CachedStatus::NotCached(code) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_code_miss();
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
CachedStatus::Cached(code) => {
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_code_hit();
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
} else if let Some(code) = self.caches.0.code_cache.get(code_hash) {
|
||||
self.metrics.code_cache_hits.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_code_hit();
|
||||
}
|
||||
Ok(code)
|
||||
} else {
|
||||
self.metrics.code_cache_misses.increment(1);
|
||||
if let Some(stats) = &self.cache_stats {
|
||||
stats.record_code_miss();
|
||||
}
|
||||
self.state_provider.bytecode_by_hash(code_hash)
|
||||
}
|
||||
}
|
||||
@@ -707,7 +840,8 @@ impl ExecutionCache {
|
||||
}
|
||||
|
||||
self.0.account_cache.remove(addr);
|
||||
continue
|
||||
self.0.account_stats.decrement_size();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have an account that was modified, but it has a `None` account info, some wild
|
||||
@@ -837,8 +971,10 @@ impl SavedCache {
|
||||
self.caches.update_metrics(&self.metrics);
|
||||
}
|
||||
|
||||
/// Clears all caches, resetting them to empty state.
|
||||
pub(crate) fn clear(&self) {
|
||||
/// Clears all caches, resetting them to empty state,
|
||||
/// and updates the hash of the block this cache belongs to.
|
||||
pub(crate) fn clear_with_hash(&mut self, hash: B256) {
|
||||
self.hash = hash;
|
||||
self.caches.clear();
|
||||
}
|
||||
}
|
||||
@@ -1085,4 +1221,20 @@ mod tests {
|
||||
assert!(caches.0.account_cache.get(&addr1).is_none());
|
||||
assert!(caches.0.account_cache.get(&addr2).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_cache_capacity_with_default_budget() {
|
||||
// Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.
|
||||
let total_cache_size = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||
let code_budget = (total_cache_size * 556) / 10000; // 228 MB
|
||||
|
||||
let capacity = ExecutionCache::bytes_to_entries(code_budget, CODE_CACHE_ENTRY_SIZE);
|
||||
|
||||
// With ESTIMATED_AVG_CODE_SIZE (8 KiB) we expect 16384 entries.
|
||||
// If someone accidentally reverts to MAX_CODE_SIZE (48 KiB), this would drop to 4096.
|
||||
assert_eq!(
|
||||
capacity, 16384,
|
||||
"code cache should have 16384 entries with default 4 GB budget"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ use reth_trie::{
|
||||
MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
|
||||
};
|
||||
use std::{
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
sync::{
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -33,11 +36,6 @@ pub(crate) struct AtomicDuration {
|
||||
}
|
||||
|
||||
impl AtomicDuration {
|
||||
/// Returns a zero duration.
|
||||
pub(crate) const fn zero() -> Self {
|
||||
Self { nanos: AtomicU64::new(0) }
|
||||
}
|
||||
|
||||
/// Returns the duration as a [`Duration`]
|
||||
pub(crate) fn duration(&self) -> Duration {
|
||||
let nanos = self.nanos.load(Ordering::Relaxed);
|
||||
@@ -63,18 +61,10 @@ impl AtomicDuration {
|
||||
pub struct InstrumentedStateProvider<S> {
|
||||
/// The state provider
|
||||
state_provider: S,
|
||||
|
||||
/// Metrics for the instrumented state provider
|
||||
/// Prometheus metrics for the instrumented state provider
|
||||
metrics: StateProviderMetrics,
|
||||
|
||||
/// The total time we spend fetching storage over the lifetime of this state provider
|
||||
total_storage_fetch_latency: AtomicDuration,
|
||||
|
||||
/// The total time we spend fetching code over the lifetime of this state provider
|
||||
total_code_fetch_latency: AtomicDuration,
|
||||
|
||||
/// The total time we spend fetching accounts over the lifetime of this state provider
|
||||
total_account_fetch_latency: AtomicDuration,
|
||||
/// Shared fetch statistics, readable after the provider is consumed.
|
||||
stats: Arc<StateProviderStats>,
|
||||
}
|
||||
|
||||
impl<S> InstrumentedStateProvider<S>
|
||||
@@ -87,58 +77,33 @@ where
|
||||
Self {
|
||||
state_provider,
|
||||
metrics: StateProviderMetrics::new_with_labels(&[("source", source)]),
|
||||
total_storage_fetch_latency: AtomicDuration::zero(),
|
||||
total_code_fetch_latency: AtomicDuration::zero(),
|
||||
total_account_fetch_latency: AtomicDuration::zero(),
|
||||
stats: Arc::new(StateProviderStats::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> InstrumentedStateProvider<S> {
|
||||
/// Records the latency for a storage fetch, and increments the duration counter for the storage
|
||||
/// fetch.
|
||||
fn record_storage_fetch(&self, latency: Duration) {
|
||||
self.metrics.storage_fetch_latency.record(latency);
|
||||
self.total_storage_fetch_latency.add_duration(latency);
|
||||
}
|
||||
|
||||
/// Records the latency for a code fetch, and increments the duration counter for the code
|
||||
/// fetch.
|
||||
fn record_code_fetch(&self, latency: Duration) {
|
||||
self.metrics.code_fetch_latency.record(latency);
|
||||
self.total_code_fetch_latency.add_duration(latency);
|
||||
}
|
||||
|
||||
/// Records the latency for an account fetch, and increments the duration counter for the
|
||||
/// account fetch.
|
||||
fn record_account_fetch(&self, latency: Duration) {
|
||||
self.metrics.account_fetch_latency.record(latency);
|
||||
self.total_account_fetch_latency.add_duration(latency);
|
||||
}
|
||||
|
||||
/// Records the total latencies into their respective gauges and histograms.
|
||||
pub(crate) fn record_total_latency(&self) {
|
||||
let total_storage_fetch_latency = self.total_storage_fetch_latency.duration();
|
||||
self.metrics.total_storage_fetch_latency.record(total_storage_fetch_latency);
|
||||
self.metrics
|
||||
.total_storage_fetch_latency_gauge
|
||||
.set(total_storage_fetch_latency.as_secs_f64());
|
||||
|
||||
let total_code_fetch_latency = self.total_code_fetch_latency.duration();
|
||||
self.metrics.total_code_fetch_latency.record(total_code_fetch_latency);
|
||||
self.metrics.total_code_fetch_latency_gauge.set(total_code_fetch_latency.as_secs_f64());
|
||||
|
||||
let total_account_fetch_latency = self.total_account_fetch_latency.duration();
|
||||
self.metrics.total_account_fetch_latency.record(total_account_fetch_latency);
|
||||
self.metrics
|
||||
.total_account_fetch_latency_gauge
|
||||
.set(total_account_fetch_latency.as_secs_f64());
|
||||
/// Returns a shared reference to the accumulated fetch statistics.
|
||||
pub fn stats(&self) -> Arc<StateProviderStats> {
|
||||
Arc::clone(&self.stats)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Drop for InstrumentedStateProvider<S> {
|
||||
fn drop(&mut self) {
|
||||
self.record_total_latency();
|
||||
let total_storage_fetch_latency = self.stats.total_storage_fetch_latency.duration();
|
||||
self.metrics.total_storage_fetch_latency.record(total_storage_fetch_latency);
|
||||
self.metrics
|
||||
.total_storage_fetch_latency_gauge
|
||||
.set(total_storage_fetch_latency.as_secs_f64());
|
||||
|
||||
let total_code_fetch_latency = self.stats.total_code_fetch_latency.duration();
|
||||
self.metrics.total_code_fetch_latency.record(total_code_fetch_latency);
|
||||
self.metrics.total_code_fetch_latency_gauge.set(total_code_fetch_latency.as_secs_f64());
|
||||
|
||||
let total_account_fetch_latency = self.stats.total_account_fetch_latency.duration();
|
||||
self.metrics.total_account_fetch_latency.record(total_account_fetch_latency);
|
||||
self.metrics
|
||||
.total_account_fetch_latency_gauge
|
||||
.set(total_account_fetch_latency.as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +148,10 @@ impl<S: AccountReader> AccountReader for InstrumentedStateProvider<S> {
|
||||
fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
|
||||
let start = Instant::now();
|
||||
let res = self.state_provider.basic_account(address);
|
||||
self.record_account_fetch(start.elapsed());
|
||||
let elapsed = start.elapsed();
|
||||
self.metrics.account_fetch_latency.record(elapsed);
|
||||
self.stats.total_account_fetches.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats.total_account_fetch_latency.add_duration(elapsed);
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -196,7 +164,10 @@ impl<S: StateProvider> StateProvider for InstrumentedStateProvider<S> {
|
||||
) -> ProviderResult<Option<StorageValue>> {
|
||||
let start = Instant::now();
|
||||
let res = self.state_provider.storage(account, storage_key);
|
||||
self.record_storage_fetch(start.elapsed());
|
||||
let elapsed = start.elapsed();
|
||||
self.metrics.storage_fetch_latency.record(elapsed);
|
||||
self.stats.total_storage_fetches.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats.total_storage_fetch_latency.add_duration(elapsed);
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -205,7 +176,17 @@ impl<S: BytecodeReader> BytecodeReader for InstrumentedStateProvider<S> {
|
||||
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
|
||||
let start = Instant::now();
|
||||
let res = self.state_provider.bytecode_by_hash(code_hash);
|
||||
self.record_code_fetch(start.elapsed());
|
||||
let elapsed = start.elapsed();
|
||||
self.metrics.code_fetch_latency.record(elapsed);
|
||||
self.stats.total_code_fetches.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats.total_code_fetch_latency.add_duration(elapsed);
|
||||
self.stats.total_code_fetched_bytes.fetch_add(
|
||||
res.as_ref()
|
||||
.ok()
|
||||
.and_then(|code| code.as_ref().map(|code| code.len()))
|
||||
.unwrap_or_default(),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -308,3 +289,56 @@ impl<S: HashedPostStateProvider> HashedPostStateProvider for InstrumentedStatePr
|
||||
self.state_provider.hashed_post_state(bundle_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulated fetch statistics from an [`InstrumentedStateProvider`].
|
||||
///
|
||||
/// Shared via `Arc` so statistics can be read after the provider is consumed.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StateProviderStats {
|
||||
total_storage_fetches: AtomicUsize,
|
||||
total_storage_fetch_latency: AtomicDuration,
|
||||
|
||||
total_code_fetches: AtomicUsize,
|
||||
total_code_fetch_latency: AtomicDuration,
|
||||
total_code_fetched_bytes: AtomicUsize,
|
||||
|
||||
total_account_fetches: AtomicUsize,
|
||||
total_account_fetch_latency: AtomicDuration,
|
||||
}
|
||||
|
||||
impl StateProviderStats {
|
||||
/// Returns total number of storage fetches.
|
||||
pub fn total_storage_fetches(&self) -> usize {
|
||||
self.total_storage_fetches.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns total time spent on storage fetches.
|
||||
pub fn total_storage_fetch_latency(&self) -> Duration {
|
||||
self.total_storage_fetch_latency.duration()
|
||||
}
|
||||
|
||||
/// Returns total number of code fetches.
|
||||
pub fn total_code_fetches(&self) -> usize {
|
||||
self.total_code_fetches.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns total time spent on code fetches.
|
||||
pub fn total_code_fetch_latency(&self) -> Duration {
|
||||
self.total_code_fetch_latency.duration()
|
||||
}
|
||||
|
||||
/// Returns total amount of code fetched, in bytes.
|
||||
pub fn total_code_fetched_bytes(&self) -> usize {
|
||||
self.total_code_fetched_bytes.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns total number of account fetches.
|
||||
pub fn total_account_fetches(&self) -> usize {
|
||||
self.total_account_fetches.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns total time spent on account fetches.
|
||||
pub fn total_account_fetch_latency(&self) -> Duration {
|
||||
self.total_account_fetch_latency.duration()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ use alloy_rpc_types_engine::{
|
||||
};
|
||||
use error::{InsertBlockError, InsertBlockFatalError};
|
||||
use reth_chain_state::{
|
||||
CanonicalInMemoryState, ComputedTrieData, ExecutedBlock, MemoryOverlayStateProvider,
|
||||
NewCanonicalChain,
|
||||
CanonicalInMemoryState, ComputedTrieData, ExecutedBlock, ExecutionTimingStats,
|
||||
MemoryOverlayStateProvider, NewCanonicalChain,
|
||||
};
|
||||
use reth_consensus::{Consensus, FullConsensus};
|
||||
use reth_engine_primitives::{
|
||||
BeaconEngineMessage, BeaconOnNewPayloadError, ConsensusEngineEvent, ExecutionPayload,
|
||||
ForkchoiceStateTracker, NewPayloadTimings, OnForkChoiceUpdated,
|
||||
ForkchoiceStateTracker, NewPayloadTimings, OnForkChoiceUpdated, SlowBlockInfo,
|
||||
};
|
||||
use reth_errors::{ConsensusError, ProviderResult};
|
||||
use reth_evm::ConfigureEvm;
|
||||
@@ -42,7 +42,7 @@ use reth_tasks::{spawn_os_thread, utils::increase_thread_priority};
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use revm::interpreter::debug_unreachable;
|
||||
use state::TreeState;
|
||||
use std::{fmt::Debug, ops, sync::Arc, time::Duration};
|
||||
use std::{collections::HashMap, fmt::Debug, ops, sync::Arc, time::Duration};
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use tokio::sync::{
|
||||
@@ -65,7 +65,7 @@ pub mod precompile_cache;
|
||||
mod tests;
|
||||
mod trie_updates;
|
||||
|
||||
use crate::tree::error::AdvancePersistenceError;
|
||||
use crate::{persistence::PersistenceResult, tree::error::AdvancePersistenceError};
|
||||
pub use block_buffer::BlockBuffer;
|
||||
pub use cached_state::{CachedStateMetrics, CachedStateProvider, ExecutionCache, SavedCache};
|
||||
pub use invalid_headers::InvalidHeaderCache;
|
||||
@@ -273,9 +273,15 @@ where
|
||||
evm_config: C,
|
||||
/// Changeset cache for in-memory trie changesets
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Timing statistics for executed blocks, keyed by block hash.
|
||||
/// Stored here (not in `ExecutedBlock`) to avoid leaking observability concerns into the block
|
||||
/// type. Entries are removed when blocks are persisted or invalidated.
|
||||
execution_timing_stats: HashMap<B256, Box<ExecutionTimingStats>>,
|
||||
/// Whether the node uses hashed state as canonical storage (v2 mode).
|
||||
/// Cached at construction to avoid threading `StorageSettingsCache` bounds everywhere.
|
||||
use_hashed_state: bool,
|
||||
/// Task runtime for spawning blocking work on named, reusable threads.
|
||||
runtime: reth_tasks::Runtime,
|
||||
}
|
||||
|
||||
impl<N, P: Debug, T: PayloadTypes + Debug, V: Debug, C> std::fmt::Debug
|
||||
@@ -301,7 +307,9 @@ where
|
||||
.field("engine_kind", &self.engine_kind)
|
||||
.field("evm_config", &self.evm_config)
|
||||
.field("changeset_cache", &self.changeset_cache)
|
||||
.field("execution_timing_stats", &self.execution_timing_stats.len())
|
||||
.field("use_hashed_state", &self.use_hashed_state)
|
||||
.field("runtime", &self.runtime)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -342,6 +350,7 @@ where
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
use_hashed_state: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> Self {
|
||||
let (incoming_tx, incoming) = crossbeam_channel::unbounded();
|
||||
|
||||
@@ -363,7 +372,9 @@ where
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
execution_timing_stats: HashMap::new(),
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +396,7 @@ where
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
use_hashed_state: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> (Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>, UnboundedReceiver<EngineApiEvent<N>>)
|
||||
{
|
||||
let best_block_number = provider.best_block_number().unwrap_or(0);
|
||||
@@ -418,6 +430,7 @@ where
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
runtime,
|
||||
);
|
||||
let incoming = task.incoming_tx.clone();
|
||||
spawn_os_thread("engine", || {
|
||||
@@ -427,6 +440,14 @@ where
|
||||
(incoming, outgoing)
|
||||
}
|
||||
|
||||
/// Returns a [`TreeOutcome`] indicating the forkchoice head is valid and canonical.
|
||||
fn valid_outcome(state: ForkchoiceState) -> TreeOutcome<OnForkChoiceUpdated> {
|
||||
TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::new(
|
||||
PayloadStatusEnum::Valid,
|
||||
Some(state.head_block_hash),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Returns a new [`Sender`] to send messages to this type.
|
||||
pub fn sender(&self) -> Sender<FromEngine<EngineApiRequest<T, N>, N::Block>> {
|
||||
self.incoming_tx.clone()
|
||||
@@ -488,8 +509,8 @@ where
|
||||
recv(persistence_rx) -> result => {
|
||||
// Don't put it back - consumed (oneshot-like behavior)
|
||||
match result {
|
||||
Ok(value) => LoopEvent::PersistenceComplete {
|
||||
result: value,
|
||||
Ok(result) => LoopEvent::PersistenceComplete {
|
||||
result,
|
||||
start_time,
|
||||
},
|
||||
Err(_) => LoopEvent::Disconnected,
|
||||
@@ -1118,11 +1139,7 @@ where
|
||||
}
|
||||
|
||||
// The head block is already canonical
|
||||
let outcome = TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::new(
|
||||
PayloadStatusEnum::Valid,
|
||||
Some(state.head_block_hash),
|
||||
)));
|
||||
Ok(Some(outcome))
|
||||
Ok(Some(Self::valid_outcome(state)))
|
||||
}
|
||||
|
||||
/// Applies chain update for the new head block and processes payload attributes.
|
||||
@@ -1183,12 +1200,7 @@ where
|
||||
|
||||
// The head block is already canonical and we're not processing payload attributes,
|
||||
// so we're not triggering a payload job and can return right away
|
||||
|
||||
let outcome = TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::new(
|
||||
PayloadStatusEnum::Valid,
|
||||
Some(state.head_block_hash),
|
||||
)));
|
||||
return Ok(Some(outcome));
|
||||
return Ok(Some(Self::valid_outcome(state)));
|
||||
}
|
||||
|
||||
// Ensure we can apply a new chain update for the head block
|
||||
@@ -1208,11 +1220,7 @@ where
|
||||
return Ok(Some(TreeOutcome::new(updated)));
|
||||
}
|
||||
|
||||
let outcome = TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::new(
|
||||
PayloadStatusEnum::Valid,
|
||||
Some(state.head_block_hash),
|
||||
)));
|
||||
return Ok(Some(outcome));
|
||||
return Ok(Some(Self::valid_outcome(state)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -1367,15 +1375,16 @@ where
|
||||
/// Handles a completed persistence task.
|
||||
fn on_persistence_complete(
|
||||
&mut self,
|
||||
last_persisted_hash_num: Option<BlockNumHash>,
|
||||
result: PersistenceResult,
|
||||
start_time: Instant,
|
||||
) -> Result<(), AdvancePersistenceError> {
|
||||
self.metrics.engine.persistence_duration.record(start_time.elapsed());
|
||||
|
||||
let commit_duration = result.commit_duration;
|
||||
let Some(BlockNumHash {
|
||||
hash: last_persisted_block_hash,
|
||||
number: last_persisted_block_number,
|
||||
}) = last_persisted_hash_num
|
||||
}) = result.last_block
|
||||
else {
|
||||
// if this happened, then we persisted no blocks because we sent an empty vec of blocks
|
||||
warn!(target: "engine::tree", "Persistence task completed but did not persist any blocks");
|
||||
@@ -1416,11 +1425,13 @@ where
|
||||
// Spawn a background task to trigger computation so it's ready when the next payload
|
||||
// arrives.
|
||||
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
self.runtime.spawn_blocking_named("prepare-overlay", move || {
|
||||
let _ = overlay.get();
|
||||
});
|
||||
}
|
||||
|
||||
self.purge_timing_stats(last_persisted_block_number, commit_duration);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1575,7 +1586,7 @@ where
|
||||
{
|
||||
let (persistence_tx, persistence_rx) =
|
||||
std::sync::mpsc::channel();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
self.runtime.spawn_blocking_named("wait-persist", move || {
|
||||
let start = Instant::now();
|
||||
let result =
|
||||
rx.recv().expect("persistence state channel closed");
|
||||
@@ -1726,6 +1737,7 @@ where
|
||||
|
||||
// remove all buffered blocks below the backfill height
|
||||
self.state.buffer.remove_old_blocks(backfill_height);
|
||||
self.purge_timing_stats(backfill_height, None);
|
||||
// we remove all entries because now we're synced to the backfill target and consider this
|
||||
// the canonical chain
|
||||
self.canonical_in_memory_state.clear_state();
|
||||
@@ -1859,6 +1871,43 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes timing stats for blocks at or below `below_number`.
|
||||
///
|
||||
/// No-op when detailed block logging is disabled (no stats are recorded in that case).
|
||||
/// When `commit_duration` is provided and a slow block threshold is configured, checks
|
||||
/// each removed block against the threshold and emits a [`ConsensusEngineEvent::SlowBlock`]
|
||||
/// event for blocks that exceed it.
|
||||
fn purge_timing_stats(&mut self, below_number: u64, commit_duration: Option<Duration>) {
|
||||
let threshold = self.config.slow_block_threshold();
|
||||
let check_slow = commit_duration.is_some() && threshold.is_some();
|
||||
|
||||
// Two-pass: collect keys first because emit_event borrows &mut self.
|
||||
let keys_to_remove: Vec<B256> = self
|
||||
.execution_timing_stats
|
||||
.iter()
|
||||
.filter(|(_, stats)| stats.block_number <= below_number)
|
||||
.map(|(k, _)| *k)
|
||||
.collect();
|
||||
|
||||
for key in keys_to_remove {
|
||||
let stats = self.execution_timing_stats.remove(&key).expect("key just found");
|
||||
if check_slow {
|
||||
let commit_dur = commit_duration.expect("checked above");
|
||||
// state_read_duration is already included in execution_duration
|
||||
let total_duration =
|
||||
stats.execution_duration + stats.state_hash_duration + commit_dur;
|
||||
|
||||
if total_duration > threshold.expect("checked above") {
|
||||
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
|
||||
stats,
|
||||
commit_duration: commit_dur,
|
||||
total_duration,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits an outgoing event to the engine.
|
||||
fn emit_event(&mut self, event: impl Into<EngineApiEvent<N>>) {
|
||||
let event = event.into();
|
||||
@@ -2101,16 +2150,16 @@ where
|
||||
/// Prepares the invalid payload response for the given hash, checking the
|
||||
/// database for the parent hash and populating the payload status with the latest valid hash
|
||||
/// according to the engine api spec.
|
||||
fn prepare_invalid_response(&mut self, mut parent_hash: B256) -> ProviderResult<PayloadStatus> {
|
||||
// Edge case: the `latestValid` field is the zero hash if the parent block is the terminal
|
||||
// PoW block, which we need to identify by looking at the parent's block difficulty
|
||||
if let Some(parent) = self.sealed_header_by_hash(parent_hash)? &&
|
||||
!parent.difficulty().is_zero()
|
||||
{
|
||||
parent_hash = B256::ZERO;
|
||||
}
|
||||
fn prepare_invalid_response(&mut self, parent_hash: B256) -> ProviderResult<PayloadStatus> {
|
||||
let valid_parent_hash = match self.sealed_header_by_hash(parent_hash)? {
|
||||
// Edge case: the `latestValid` field is the zero hash if the parent block is the
|
||||
// terminal PoW block, which we need to identify by looking at the parent's block
|
||||
// difficulty
|
||||
Some(parent) if !parent.difficulty().is_zero() => Some(B256::ZERO),
|
||||
Some(_) => Some(parent_hash),
|
||||
None => self.latest_valid_hash_for_invalid_payload(parent_hash)?,
|
||||
};
|
||||
|
||||
let valid_parent_hash = self.latest_valid_hash_for_invalid_payload(parent_hash)?;
|
||||
Ok(PayloadStatus::from_status(PayloadStatusEnum::Invalid {
|
||||
validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
|
||||
})
|
||||
@@ -2171,18 +2220,26 @@ where
|
||||
|
||||
/// Finds any invalid ancestor for the given payload.
|
||||
///
|
||||
/// This function walks up the chain of buffered ancestors from the payload's block
|
||||
/// hash and checks if any ancestor is marked as invalid in the tree state.
|
||||
/// This function first checks if the block itself is in the invalid headers cache (to
|
||||
/// avoid re-executing a known-invalid block). Then it walks up the chain of buffered
|
||||
/// ancestors and checks if any ancestor is marked as invalid.
|
||||
///
|
||||
/// The check works by:
|
||||
/// 1. Finding the lowest buffered ancestor for the given block hash
|
||||
/// 2. If the ancestor is the same as the block hash itself, using the parent hash instead
|
||||
/// 3. Checking if this ancestor is in the `invalid_headers` map
|
||||
/// 1. Checking if the block hash itself is in the `invalid_headers` map
|
||||
/// 2. Finding the lowest buffered ancestor for the given block hash
|
||||
/// 3. If the ancestor is the same as the block hash itself, using the parent hash instead
|
||||
/// 4. Checking if this ancestor is in the `invalid_headers` map
|
||||
///
|
||||
/// Returns the invalid ancestor block info if found, or None if no invalid ancestor exists.
|
||||
fn find_invalid_ancestor(&mut self, payload: &T::ExecutionData) -> Option<BlockWithParent> {
|
||||
let parent_hash = payload.parent_hash();
|
||||
let block_hash = payload.block_hash();
|
||||
|
||||
// Check if the block itself is already known to be invalid, avoiding re-execution
|
||||
if let Some(entry) = self.state.invalid_headers.get(&block_hash) {
|
||||
return Some(entry);
|
||||
}
|
||||
|
||||
let mut lowest_buffered_ancestor = self.lowest_buffered_ancestor_or(block_hash);
|
||||
if lowest_buffered_ancestor == block_hash {
|
||||
lowest_buffered_ancestor = parent_hash;
|
||||
@@ -2583,6 +2640,51 @@ where
|
||||
Some(TreeEvent::Download(request))
|
||||
}
|
||||
|
||||
/// Handles a downloaded block that was successfully inserted as valid.
|
||||
///
|
||||
/// If the block matches the sync target head, returns [`TreeAction::MakeCanonical`].
|
||||
/// If it matches a non-head sync target (safe or finalized), makes it canonical inline
|
||||
/// and triggers a download for the remaining blocks towards the actual head.
|
||||
/// Otherwise, tries to connect buffered blocks.
|
||||
fn on_valid_downloaded_block(
|
||||
&mut self,
|
||||
block_num_hash: BlockNumHash,
|
||||
) -> Result<Option<TreeEvent>, InsertBlockFatalError> {
|
||||
// check if we just inserted a block that's part of sync targets,
|
||||
// i.e. head, safe, or finalized
|
||||
if let Some(sync_target) = self.state.forkchoice_state_tracker.sync_target_state() &&
|
||||
sync_target.contains(block_num_hash.hash)
|
||||
{
|
||||
debug!(target: "engine::tree", ?sync_target, "appended downloaded sync target block");
|
||||
|
||||
if sync_target.head_block_hash == block_num_hash.hash {
|
||||
// we just inserted the sync target head block, make it canonical
|
||||
return Ok(Some(TreeEvent::TreeAction(TreeAction::MakeCanonical {
|
||||
sync_target_head: block_num_hash.hash,
|
||||
})))
|
||||
}
|
||||
|
||||
// This block is part of the sync target (safe or finalized) but not the
|
||||
// head. Make it canonical and try to connect any buffered children, then
|
||||
// continue downloading towards the actual head if needed.
|
||||
self.make_canonical(block_num_hash.hash)?;
|
||||
self.try_connect_buffered_blocks(block_num_hash)?;
|
||||
|
||||
// Check if we've reached the sync target head after connecting buffered
|
||||
// blocks (e.g. the head block may have already been buffered).
|
||||
if self.state.tree_state.canonical_block_hash() != sync_target.head_block_hash {
|
||||
let target = self.lowest_buffered_ancestor_or(sync_target.head_block_hash);
|
||||
trace!(target: "engine::tree", %target, "sync target head not yet reached, downloading head block");
|
||||
return Ok(Some(TreeEvent::Download(DownloadRequest::single_block(target))))
|
||||
}
|
||||
|
||||
return Ok(None)
|
||||
}
|
||||
trace!(target: "engine::tree", "appended downloaded block");
|
||||
self.try_connect_buffered_blocks(block_num_hash)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Invoked with a block downloaded from the network
|
||||
///
|
||||
/// Returns an event with the appropriate action to take, such as:
|
||||
@@ -2605,22 +2707,11 @@ where
|
||||
|
||||
// try to append the block
|
||||
match self.insert_block(block) {
|
||||
Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
|
||||
// check if we just inserted a block that's part of sync targets,
|
||||
// i.e. head, safe, or finalized
|
||||
if let Some(sync_target) = self.state.forkchoice_state_tracker.sync_target_state() &&
|
||||
sync_target.contains(block_num_hash.hash)
|
||||
{
|
||||
debug!(target: "engine::tree", ?sync_target, "appended downloaded sync target block");
|
||||
|
||||
// we just inserted a block that we know is part of the canonical chain, so we
|
||||
// can make it canonical
|
||||
return Ok(Some(TreeEvent::TreeAction(TreeAction::MakeCanonical {
|
||||
sync_target_head: block_num_hash.hash,
|
||||
})))
|
||||
}
|
||||
trace!(target: "engine::tree", "appended downloaded block");
|
||||
self.try_connect_buffered_blocks(block_num_hash)?;
|
||||
Ok(
|
||||
InsertPayloadOk::Inserted(BlockStatus::Valid) |
|
||||
InsertPayloadOk::AlreadySeen(BlockStatus::Valid),
|
||||
) => {
|
||||
return self.on_valid_downloaded_block(block_num_hash);
|
||||
}
|
||||
Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
|
||||
// block is not connected to the canonical head, we need to download
|
||||
@@ -2700,7 +2791,12 @@ where
|
||||
&mut self,
|
||||
block_id: BlockWithParent,
|
||||
input: Input,
|
||||
execute: impl FnOnce(&mut V, Input, TreeCtx<'_, N>) -> Result<ExecutedBlock<N>, Err>,
|
||||
execute: impl FnOnce(
|
||||
&mut V,
|
||||
Input,
|
||||
TreeCtx<'_, N>,
|
||||
)
|
||||
-> Result<(ExecutedBlock<N>, Option<Box<ExecutionTimingStats>>), Err>,
|
||||
convert_to_block: impl FnOnce(&mut Self, Input) -> Result<SealedBlock<N::Block>, Err>,
|
||||
) -> Result<InsertPayloadOk, Err>
|
||||
where
|
||||
@@ -2770,7 +2866,12 @@ where
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let executed = execute(&mut self.payload_validator, input, ctx)?;
|
||||
let (executed, timing_stats) = execute(&mut self.payload_validator, input, ctx)?;
|
||||
|
||||
// Store timing stats for detailed block logging after persistence
|
||||
if let Some(stats) = timing_stats {
|
||||
self.execution_timing_stats.insert(executed.recovered_block().hash(), stats);
|
||||
}
|
||||
|
||||
// if the parent is the canonical head, we can insert the block as the pending block
|
||||
if self.state.tree_state.canonical_block_hash() == executed.recovered_block().parent_hash()
|
||||
@@ -3092,8 +3193,8 @@ where
|
||||
EngineMessage(FromEngine<EngineApiRequest<T, N>, N::Block>),
|
||||
/// A persistence task completed.
|
||||
PersistenceComplete {
|
||||
/// The result of the persistence operation.
|
||||
result: Option<BlockNumHash>,
|
||||
/// The unified result of the persistence operation.
|
||||
result: PersistenceResult,
|
||||
/// When the persistence operation started.
|
||||
start_time: Instant,
|
||||
},
|
||||
|
||||
@@ -39,12 +39,13 @@ use reth_trie_parallel::{
|
||||
root::ParallelStateRootError,
|
||||
};
|
||||
use reth_trie_sparse::{
|
||||
ParallelSparseTrie, ParallelismThresholds, RevealableSparseTrie, SparseStateTrie,
|
||||
ArenaParallelSparseTrie, ConfigurableSparseTrie, ParallelSparseTrie, ParallelismThresholds,
|
||||
RevealableSparseTrie, SparseStateTrie,
|
||||
};
|
||||
use std::{
|
||||
ops::Not,
|
||||
sync::{
|
||||
atomic::AtomicBool,
|
||||
atomic::{AtomicBool, AtomicUsize},
|
||||
mpsc::{self, channel},
|
||||
Arc,
|
||||
},
|
||||
@@ -64,9 +65,9 @@ use preserved_sparse_trie::{PreservedSparseTrie, SharedPreservedSparseTrie};
|
||||
/// Default parallelism thresholds to use with the [`ParallelSparseTrie`].
|
||||
///
|
||||
/// These values were determined by performing benchmarks using gradually increasing values to judge
|
||||
/// the affects. Below 100 throughput would generally be equal or slightly less, while above 150 it
|
||||
/// the effects. Below 100 throughput would generally be equal or slightly less, while above 150 it
|
||||
/// would deteriorate to the point where PST might as well not be used.
|
||||
pub const PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS: ParallelismThresholds =
|
||||
const PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS: ParallelismThresholds =
|
||||
ParallelismThresholds { min_revealed_nodes: 100, min_updated_nodes: 100 };
|
||||
|
||||
/// Default node capacity for shrinking the sparse trie. This is used to limit the number of trie
|
||||
@@ -131,12 +132,14 @@ where
|
||||
/// re-use allocated memory. Stored with the block hash it was computed for to enable trie
|
||||
/// preservation across sequential payload validations.
|
||||
sparse_state_trie: SharedPreservedSparseTrie,
|
||||
/// Sparse trie prune depth.
|
||||
sparse_trie_prune_depth: usize,
|
||||
/// Maximum storage tries to retain after pruning.
|
||||
sparse_trie_max_storage_tries: usize,
|
||||
/// LFU hot-slot capacity: max storage slots retained across prune cycles.
|
||||
sparse_trie_max_hot_slots: usize,
|
||||
/// LFU hot-account capacity: max account addresses retained across prune cycles.
|
||||
sparse_trie_max_hot_accounts: usize,
|
||||
/// Whether sparse trie cache pruning is fully disabled.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to use the arena-based sparse trie implementation.
|
||||
enable_arena_sparse_trie: bool,
|
||||
/// Whether to disable cache metrics recording.
|
||||
disable_cache_metrics: bool,
|
||||
}
|
||||
@@ -169,9 +172,10 @@ where
|
||||
precompile_cache_disabled: config.precompile_cache_disabled(),
|
||||
precompile_cache_map,
|
||||
sparse_state_trie: SharedPreservedSparseTrie::default(),
|
||||
sparse_trie_prune_depth: config.sparse_trie_prune_depth(),
|
||||
sparse_trie_max_storage_tries: config.sparse_trie_max_storage_tries(),
|
||||
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(),
|
||||
enable_arena_sparse_trie: config.enable_arena_sparse_trie(),
|
||||
disable_cache_metrics: config.disable_cache_metrics(),
|
||||
}
|
||||
}
|
||||
@@ -192,10 +196,10 @@ where
|
||||
let (execution_tx, execution_rx) = std::sync::mpsc::channel();
|
||||
let (sparse_trie_tx, sparse_trie_rx) = std::sync::mpsc::channel();
|
||||
|
||||
self.executor.spawn_blocking(move || {
|
||||
self.executor.spawn_blocking_named("wait-exec-cache", move || {
|
||||
let _ = execution_tx.send(execution_cache.wait_for_availability());
|
||||
});
|
||||
self.executor.spawn_blocking(move || {
|
||||
self.executor.spawn_blocking_named("wait-sparse-tri", move || {
|
||||
let _ = sparse_trie_tx.send(sparse_trie.wait_for_availability());
|
||||
});
|
||||
|
||||
@@ -282,40 +286,19 @@ where
|
||||
self.spawn_tx_iterator(transactions, env.transaction_count);
|
||||
|
||||
let span = Span::current();
|
||||
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
|
||||
|
||||
let parent_state_root = env.parent_state_root;
|
||||
let transaction_count = env.transaction_count;
|
||||
let chunk_size = config.multiproof_chunk_size();
|
||||
let state_root_handle = self.spawn_state_root(multiproof_provider_factory, &env, config);
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder,
|
||||
Some(to_multi_proof.clone()),
|
||||
Some(state_root_handle.to_multi_proof.clone()),
|
||||
bal,
|
||||
);
|
||||
|
||||
// Create and spawn the storage proof task.
|
||||
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
|
||||
let halve_workers = transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
|
||||
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, halve_workers);
|
||||
|
||||
// wire the sparse trie to the state root response receiver
|
||||
let (state_root_tx, state_root_rx) = channel();
|
||||
|
||||
// Spawn the sparse trie task using any stored trie and parallel trie configuration.
|
||||
self.spawn_sparse_trie_task(
|
||||
proof_handle,
|
||||
state_root_tx,
|
||||
from_multi_proof,
|
||||
parent_state_root,
|
||||
chunk_size,
|
||||
);
|
||||
|
||||
PayloadHandle {
|
||||
to_multi_proof: Some(to_multi_proof),
|
||||
state_root_handle: Some(state_root_handle),
|
||||
prewarm_handle,
|
||||
state_root: Some(state_root_rx),
|
||||
transactions: execution_rx,
|
||||
_span: span,
|
||||
}
|
||||
@@ -339,14 +322,57 @@ where
|
||||
self.spawn_tx_iterator(transactions, env.transaction_count);
|
||||
let prewarm_handle = self.spawn_caching_with(env, prewarm_rx, provider_builder, None, bal);
|
||||
PayloadHandle {
|
||||
to_multi_proof: None,
|
||||
state_root_handle: None,
|
||||
prewarm_handle,
|
||||
state_root: None,
|
||||
transactions: execution_rx,
|
||||
_span: Span::current(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns state root computation pipeline (multiproof + sparse trie tasks).
|
||||
///
|
||||
/// The returned [`StateRootHandle`] provides:
|
||||
/// - [`StateRootHandle::state_hook`] — an [`OnStateHook`] to stream state updates during
|
||||
/// execution.
|
||||
/// - [`StateRootHandle::state_root`] — blocks until the state root is computed and returns the
|
||||
/// state root.
|
||||
///
|
||||
/// The state hook **must** be dropped after execution to signal the end of state updates.
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
|
||||
pub fn spawn_state_root<F>(
|
||||
&mut self,
|
||||
multiproof_provider_factory: F,
|
||||
env: &ExecutionEnv<Evm>,
|
||||
config: &TreeConfig,
|
||||
) -> StateRootHandle
|
||||
where
|
||||
F: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
|
||||
|
||||
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
|
||||
#[cfg(feature = "trie-debug")]
|
||||
let task_ctx = task_ctx.with_proof_jitter(config.proof_jitter());
|
||||
let halve_workers = env.transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
|
||||
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, halve_workers);
|
||||
|
||||
let (state_root_tx, state_root_rx) = channel();
|
||||
|
||||
self.spawn_sparse_trie_task(
|
||||
proof_handle,
|
||||
state_root_tx,
|
||||
from_multi_proof,
|
||||
env.parent_state_root,
|
||||
config.multiproof_chunk_size(),
|
||||
);
|
||||
|
||||
StateRootHandle::new(to_multi_proof, state_root_rx)
|
||||
}
|
||||
|
||||
/// Transaction count threshold below which proof workers are halved, since fewer transactions
|
||||
/// produce fewer state changes and most workers would be idle overhead.
|
||||
const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
|
||||
@@ -409,6 +435,7 @@ where
|
||||
// few transactions are recovered sequentially and sent immediately before
|
||||
// entering the parallel iterator for the remainder.
|
||||
let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
|
||||
let executor = self.executor.clone();
|
||||
self.executor.spawn_blocking_named("tx-iterator", move || {
|
||||
let (transactions, convert) = transactions.into_parts();
|
||||
let mut all: Vec<_> = transactions.into_iter().collect();
|
||||
@@ -424,15 +451,15 @@ where
|
||||
.map(|(i, tx)| {
|
||||
let idx = i + prefetch;
|
||||
let tx = convert.convert(tx);
|
||||
(idx, tx)
|
||||
})
|
||||
.for_each_ordered_in(executor.cpu_pool(), |(idx, tx)| {
|
||||
let tx = tx.map(|tx| {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
let tx = WithTxEnv { tx_env, tx: Arc::new(tx) };
|
||||
let _ = prewarm_tx.send((idx, tx.clone()));
|
||||
tx
|
||||
});
|
||||
(idx, tx)
|
||||
})
|
||||
.for_each_ordered(|(idx, tx)| {
|
||||
let _ = execute_tx.send(tx);
|
||||
debug!(target: "engine::tree::payload_processor", idx, "yielded transaction");
|
||||
});
|
||||
@@ -465,6 +492,8 @@ where
|
||||
|
||||
let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
|
||||
|
||||
let executed_tx_index = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// configure prewarming
|
||||
let prewarm_ctx = PrewarmContext {
|
||||
env,
|
||||
@@ -473,6 +502,7 @@ where
|
||||
provider: provider_builder,
|
||||
metrics: PrewarmMetrics::default(),
|
||||
terminate_execution: Arc::new(AtomicBool::new(false)),
|
||||
executed_tx_index: Arc::clone(&executed_tx_index),
|
||||
precompile_cache_disabled: self.precompile_cache_disabled,
|
||||
precompile_cache_map: self.precompile_cache_map.clone(),
|
||||
};
|
||||
@@ -498,7 +528,7 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
CacheTaskHandle { saved_cache, to_prewarm_task: Some(to_prewarm_task) }
|
||||
CacheTaskHandle { saved_cache, to_prewarm_task: Some(to_prewarm_task), executed_tx_index }
|
||||
}
|
||||
|
||||
/// Returns the cache for the given parent hash.
|
||||
@@ -534,14 +564,15 @@ where
|
||||
) {
|
||||
let preserved_sparse_trie = self.sparse_state_trie.clone();
|
||||
let trie_metrics = self.trie_metrics.clone();
|
||||
let prune_depth = self.sparse_trie_prune_depth;
|
||||
let max_storage_tries = self.sparse_trie_max_storage_tries;
|
||||
let max_hot_slots = self.sparse_trie_max_hot_slots;
|
||||
let max_hot_accounts = self.sparse_trie_max_hot_accounts;
|
||||
let disable_cache_pruning = self.disable_sparse_trie_cache_pruning;
|
||||
let enable_arena_sparse_trie = self.enable_arena_sparse_trie;
|
||||
let executor = self.executor.clone();
|
||||
|
||||
let parent_span = Span::current();
|
||||
self.executor.spawn_blocking_named("sparse-trie", move || {
|
||||
increase_thread_priority();
|
||||
reth_tasks::once!(increase_thread_priority);
|
||||
|
||||
let _enter = debug_span!(target: "engine::tree::payload_processor", parent: parent_span, "sparse_trie_task")
|
||||
.entered();
|
||||
@@ -556,30 +587,39 @@ where
|
||||
.sparse_trie_cache_wait_duration_histogram
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
|
||||
let sparse_state_trie = preserved
|
||||
let mut sparse_state_trie = preserved
|
||||
.map(|preserved| preserved.into_trie_for(parent_state_root))
|
||||
.unwrap_or_else(|| {
|
||||
debug!(
|
||||
target: "engine::tree::payload_processor",
|
||||
"Creating new sparse trie - no preserved trie available"
|
||||
);
|
||||
let default_trie = RevealableSparseTrie::blind_from(
|
||||
ParallelSparseTrie::default().with_parallelism_thresholds(
|
||||
PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS,
|
||||
),
|
||||
);
|
||||
SparseStateTrie::new()
|
||||
let default_trie = if enable_arena_sparse_trie {
|
||||
RevealableSparseTrie::blind_from(
|
||||
ConfigurableSparseTrie::Arena(ArenaParallelSparseTrie::default()),
|
||||
)
|
||||
} else {
|
||||
RevealableSparseTrie::blind_from(
|
||||
ConfigurableSparseTrie::HashMap(
|
||||
ParallelSparseTrie::default().with_parallelism_thresholds(
|
||||
PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS,
|
||||
),
|
||||
),
|
||||
)
|
||||
};
|
||||
SparseStateTrie::default()
|
||||
.with_accounts_trie(default_trie.clone())
|
||||
.with_default_storage_trie(default_trie)
|
||||
.with_updates(true)
|
||||
});
|
||||
sparse_state_trie.set_hot_cache_capacities(max_hot_slots, max_hot_accounts);
|
||||
|
||||
let mut task = SparseTrieCacheTask::new_with_trie(
|
||||
&executor,
|
||||
from_multi_proof,
|
||||
proof_worker_handle,
|
||||
trie_metrics.clone(),
|
||||
sparse_state_trie.with_skip_proof_node_filtering(true),
|
||||
sparse_state_trie,
|
||||
chunk_size,
|
||||
);
|
||||
|
||||
@@ -606,9 +646,8 @@ where
|
||||
SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY,
|
||||
);
|
||||
guard.store(PreservedSparseTrie::cleared(trie));
|
||||
// Drop guard before deferred to release lock before expensive deallocations
|
||||
drop(guard);
|
||||
drop(deferred);
|
||||
executor.spawn_drop(deferred);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,8 +658,8 @@ where
|
||||
let deferred = if let Some(result) = task_result {
|
||||
let start = Instant::now();
|
||||
let (trie, deferred) = task.into_trie_for_reuse(
|
||||
prune_depth,
|
||||
max_storage_tries,
|
||||
max_hot_slots,
|
||||
max_hot_accounts,
|
||||
SPARSE_TRIE_MAX_NODES_SHRINK_CAPACITY,
|
||||
SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY,
|
||||
disable_cache_pruning,
|
||||
@@ -629,6 +668,12 @@ where
|
||||
trie_metrics
|
||||
.into_trie_for_reuse_duration_histogram
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
trie_metrics
|
||||
.sparse_trie_retained_memory_bytes
|
||||
.set(trie.memory_size() as f64);
|
||||
trie_metrics
|
||||
.sparse_trie_retained_storage_tries
|
||||
.set(trie.retained_storage_tries_count() as f64);
|
||||
guard.store(PreservedSparseTrie::anchored(trie, result.state_root));
|
||||
deferred
|
||||
} else {
|
||||
@@ -643,9 +688,8 @@ where
|
||||
guard.store(PreservedSparseTrie::cleared(trie));
|
||||
deferred
|
||||
};
|
||||
// Drop guard before deferred to release lock before expensive deallocations
|
||||
drop(guard);
|
||||
drop(deferred);
|
||||
executor.spawn_drop(deferred);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -725,20 +769,79 @@ fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle to a background state root computation task.
|
||||
///
|
||||
/// Unlike [`PayloadHandle`], this does not include transaction iteration or cache prewarming.
|
||||
/// It only provides access to the state root computation via [`Self::state_hook`] and
|
||||
/// [`Self::state_root`].
|
||||
///
|
||||
/// Created by [`PayloadProcessor::spawn_state_root`].
|
||||
#[derive(Debug)]
|
||||
pub struct StateRootHandle {
|
||||
/// Channel for evm state updates to the multiproof pipeline.
|
||||
to_multi_proof: CrossbeamSender<MultiProofMessage>,
|
||||
/// Receiver for the computed state root.
|
||||
state_root_rx: Option<mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>>>,
|
||||
}
|
||||
|
||||
impl StateRootHandle {
|
||||
/// Creates a new state root handle.
|
||||
pub const fn new(
|
||||
to_multi_proof: CrossbeamSender<MultiProofMessage>,
|
||||
state_root_rx: mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>>,
|
||||
) -> Self {
|
||||
Self { to_multi_proof, state_root_rx: Some(state_root_rx) }
|
||||
}
|
||||
|
||||
/// Returns a state hook that streams state updates to the background state root task.
|
||||
///
|
||||
/// The hook must be dropped after execution completes to signal the end of state updates.
|
||||
pub fn state_hook(&self) -> impl OnStateHook {
|
||||
let to_multi_proof = StateHookSender::new(self.to_multi_proof.clone());
|
||||
|
||||
move |source: StateChangeSource, state: &EvmState| {
|
||||
let _ =
|
||||
to_multi_proof.send(MultiProofMessage::StateUpdate(source.into(), state.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Awaits the state root computation result.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If called more than once.
|
||||
pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, ParallelStateRootError> {
|
||||
self.state_root_rx
|
||||
.take()
|
||||
.expect("state_root already taken")
|
||||
.recv()
|
||||
.map_err(|_| ParallelStateRootError::Other("sparse trie task dropped".to_string()))?
|
||||
}
|
||||
|
||||
/// Takes the state root receiver for use with custom waiting logic (e.g., timeouts).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If called more than once.
|
||||
pub const fn take_state_root_rx(
|
||||
&mut self,
|
||||
) -> mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>> {
|
||||
self.state_root_rx.take().expect("state_root already taken")
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle to all the spawned tasks.
|
||||
///
|
||||
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
|
||||
/// caching task without cloning the expensive `BundleState`.
|
||||
#[derive(Debug)]
|
||||
pub struct PayloadHandle<Tx, Err, R> {
|
||||
/// Channel for evm state updates
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
/// Handle to the background state root computation, if spawned.
|
||||
state_root_handle: Option<StateRootHandle>,
|
||||
// must include the receiver of the state root wired to the sparse trie
|
||||
prewarm_handle: CacheTaskHandle<R>,
|
||||
/// Stream of block transactions
|
||||
transactions: mpsc::Receiver<Result<Tx, Err>>,
|
||||
/// Receiver for the state root
|
||||
state_root: Option<mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>>>,
|
||||
/// Span for tracing
|
||||
_span: Span,
|
||||
}
|
||||
@@ -756,11 +859,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
skip_all
|
||||
)]
|
||||
pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, ParallelStateRootError> {
|
||||
self.state_root
|
||||
.take()
|
||||
.expect("state_root is None")
|
||||
.recv()
|
||||
.map_err(|_| ParallelStateRootError::Other("sparse trie task dropped".to_string()))?
|
||||
self.state_root_handle.as_mut().expect("state_root_handle is None").state_root()
|
||||
}
|
||||
|
||||
/// Takes the state root receiver out of the handle for use with custom waiting logic
|
||||
@@ -772,21 +871,14 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
pub const fn take_state_root_rx(
|
||||
&mut self,
|
||||
) -> mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>> {
|
||||
self.state_root.take().expect("state_root is None")
|
||||
self.state_root_handle.as_mut().expect("state_root_handle is None").take_state_root_rx()
|
||||
}
|
||||
|
||||
/// Returns a state hook to be used to send state updates to this task.
|
||||
///
|
||||
/// If a multiproof task is spawned the hook will notify it about new states.
|
||||
pub fn state_hook(&self) -> impl OnStateHook {
|
||||
// convert the channel into a `StateHookSender` that emits an event on drop
|
||||
let to_multi_proof = self.to_multi_proof.clone().map(StateHookSender::new);
|
||||
|
||||
move |source: StateChangeSource, state: &EvmState| {
|
||||
if let Some(sender) = &to_multi_proof {
|
||||
let _ = sender.send(MultiProofMessage::StateUpdate(source.into(), state.clone()));
|
||||
}
|
||||
}
|
||||
pub fn state_hook(&self) -> Option<impl OnStateHook> {
|
||||
self.state_root_handle.as_ref().map(|handle| handle.state_hook())
|
||||
}
|
||||
|
||||
/// Returns a clone of the caches used by prewarming
|
||||
@@ -799,6 +891,14 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.metrics().clone())
|
||||
}
|
||||
|
||||
/// Returns a reference to the shared executed transaction index counter.
|
||||
///
|
||||
/// The main execution loop should store `index + 1` after executing each transaction so that
|
||||
/// prewarm workers can skip transactions that have already been processed.
|
||||
pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
|
||||
&self.prewarm_handle.executed_tx_index
|
||||
}
|
||||
|
||||
/// Terminates the pre-warming transaction processing.
|
||||
///
|
||||
/// Note: This does not terminate the task yet.
|
||||
@@ -822,9 +922,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
|
||||
/// Returns iterator yielding transactions from the stream.
|
||||
pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
|
||||
core::iter::repeat_with(|| self.transactions.recv())
|
||||
.take_while(|res| res.is_ok())
|
||||
.map(|res| res.unwrap())
|
||||
self.transactions.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,6 +936,9 @@ pub struct CacheTaskHandle<R> {
|
||||
saved_cache: Option<SavedCache>,
|
||||
/// Channel to the spawned prewarm task if any
|
||||
to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<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>,
|
||||
}
|
||||
|
||||
impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
|
||||
@@ -925,7 +1026,7 @@ impl PayloadExecutionCache {
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
|
||||
pub(crate) fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
|
||||
let start = Instant::now();
|
||||
let cache = self.inner.read();
|
||||
let mut cache = self.inner.write();
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
|
||||
@@ -933,7 +1034,7 @@ impl PayloadExecutionCache {
|
||||
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
|
||||
}
|
||||
|
||||
if let Some(c) = cache.as_ref() {
|
||||
if let Some(c) = cache.as_mut() {
|
||||
let cached_hash = c.executed_block_hash();
|
||||
// Check that the cache hash matches the parent hash of the current block. It won't
|
||||
// match in case it's a fork block.
|
||||
@@ -954,11 +1055,11 @@ impl PayloadExecutionCache {
|
||||
);
|
||||
|
||||
if available {
|
||||
// If the has is available (no other threads are using it), but has a mismatching
|
||||
// parent hash, we can just clear it and keep using without re-creating from
|
||||
// scratch.
|
||||
if !hash_matches {
|
||||
c.clear();
|
||||
// Fork block: clear and update the hash on the ORIGINAL before cloning.
|
||||
// This prevents the canonical chain from matching on the stale hash
|
||||
// and picking up polluted data if the fork block fails.
|
||||
c.clear_with_hash(parent_hash);
|
||||
}
|
||||
return Some(c.clone())
|
||||
} else if hash_matches {
|
||||
@@ -971,12 +1072,6 @@ impl PayloadExecutionCache {
|
||||
None
|
||||
}
|
||||
|
||||
/// Clears the tracked cache
|
||||
#[expect(unused)]
|
||||
pub(crate) fn clear(&self) {
|
||||
self.inner.write().take();
|
||||
}
|
||||
|
||||
/// Waits until the execution cache becomes available for use.
|
||||
///
|
||||
/// This acquires a write lock to ensure exclusive access, then immediately releases it.
|
||||
@@ -1154,10 +1249,18 @@ mod tests {
|
||||
|
||||
execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
|
||||
|
||||
// When the parent hash doesn't match, the cache is cleared and returned for reuse
|
||||
// When the parent hash doesn't match (fork block), the cache is cleared,
|
||||
// hash updated on the original, and clone returned for reuse
|
||||
let different_hash = B256::from([4u8; 32]);
|
||||
let cache = execution_cache.get_cache_for(different_hash);
|
||||
assert!(cache.is_some(), "cache should be returned for reuse after clearing")
|
||||
assert!(cache.is_some(), "cache should be returned for reuse after clearing");
|
||||
|
||||
drop(cache);
|
||||
|
||||
// The stored cache now has the fork block's parent hash.
|
||||
// Canonical chain looking for original hash sees a mismatch → clears and reuses.
|
||||
let original = execution_cache.get_cache_for(hash);
|
||||
assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1366,7 +1469,7 @@ mod tests {
|
||||
None, // No BAL for test
|
||||
);
|
||||
|
||||
let mut state_hook = handle.state_hook();
|
||||
let mut state_hook = handle.state_hook().expect("state hook is None");
|
||||
|
||||
for (i, update) in state_updates.into_iter().enumerate() {
|
||||
state_hook.on_state(StateChangeSource::Transaction(i), &update);
|
||||
@@ -1381,4 +1484,61 @@ mod tests {
|
||||
"State root mismatch: task={root_from_task}, base={root_from_regular}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests the full prewarm lifecycle for a fork block:
|
||||
///
|
||||
/// 1. Cache is at canonical block 4.
|
||||
/// 2. Fork block (parent = block 2) checks out the cache via `get_cache_for`, simulating what
|
||||
/// `PrewarmCacheTask` does when it receives a `SavedCache`.
|
||||
/// 3. Prewarm populates the shared cache with fork-specific state.
|
||||
/// 4. While the prewarm clone is alive, the cache is unavailable (`usage_guard` > 1).
|
||||
/// 5. Prewarm drops without calling `save_cache` (fork block was invalid).
|
||||
/// 6. Canonical block 5 (parent = block 4) must get a cache with correct hash and no stale fork
|
||||
/// data.
|
||||
#[test]
|
||||
fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
|
||||
let execution_cache = PayloadExecutionCache::default();
|
||||
|
||||
// Canonical chain at block 4.
|
||||
let block4_hash = B256::from([4u8; 32]);
|
||||
execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
|
||||
|
||||
// Fork block arrives with parent = block 2. Prewarm task checks out the cache.
|
||||
// This simulates PrewarmCacheTask receiving a SavedCache clone from get_cache_for.
|
||||
let fork_parent = B256::from([2u8; 32]);
|
||||
let prewarm_cache = execution_cache.get_cache_for(fork_parent);
|
||||
assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
|
||||
let prewarm_cache = prewarm_cache.unwrap();
|
||||
assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
|
||||
|
||||
// Prewarm populates cache with fork-specific state (ancestor data for block 2).
|
||||
// Since ExecutionCache uses Arc<Inner>, this data is shared with the stored original.
|
||||
let fork_addr = Address::from([0xBB; 20]);
|
||||
let fork_key = B256::from([0xCC; 32]);
|
||||
prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
|
||||
|
||||
// While prewarm holds the clone, the usage_guard count > 1 → cache is in use.
|
||||
let during_prewarm = execution_cache.get_cache_for(block4_hash);
|
||||
assert!(
|
||||
during_prewarm.is_none(),
|
||||
"cache must be unavailable while prewarm holds a reference"
|
||||
);
|
||||
|
||||
// Fork block fails — prewarm task drops without calling save_cache/update_with_guard.
|
||||
drop(prewarm_cache);
|
||||
|
||||
// Canonical block 5 arrives (parent = block 4).
|
||||
// Stored hash = fork_parent (our fix), so get_cache_for sees a mismatch,
|
||||
// clears the stale fork data, and returns a cache with hash = block4_hash.
|
||||
let block5_cache = execution_cache.get_cache_for(block4_hash);
|
||||
assert!(
|
||||
block5_cache.is_some(),
|
||||
"canonical chain must get cache after fork prewarm is dropped"
|
||||
);
|
||||
assert_eq!(
|
||||
block5_cache.as_ref().unwrap().executed_block_hash(),
|
||||
block4_hash,
|
||||
"cache must carry the canonical parent hash, not the fork parent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use metrics::{Gauge, Histogram};
|
||||
use reth_metrics::Metrics;
|
||||
use reth_revm::state::EvmState;
|
||||
use reth_trie::{HashedPostState, HashedStorage};
|
||||
use reth_trie_parallel::targets_v2::MultiProofTargetsV2;
|
||||
use reth_trie_common::MultiProofTargetsV2;
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
|
||||
@@ -47,16 +47,6 @@ pub enum MultiProofMessage {
|
||||
PrefetchProofs(MultiProofTargetsV2),
|
||||
/// New state update from transaction execution with its source
|
||||
StateUpdate(Source, EvmState),
|
||||
/// State update that can be applied to the sparse trie without any new proofs.
|
||||
///
|
||||
/// It can be the case when all accounts and storage slots from the state update were already
|
||||
/// fetched and revealed.
|
||||
EmptyProof {
|
||||
/// The index of this proof in the sequence of state updates
|
||||
sequence_number: u64,
|
||||
/// The state update that was used to calculate the proof
|
||||
state: HashedPostState,
|
||||
},
|
||||
/// Pre-hashed state update from BAL conversion that can be applied directly without proofs.
|
||||
HashedStateUpdate(HashedPostState),
|
||||
/// Block Access List (EIP-7928; BAL) containing complete state changes for the block.
|
||||
@@ -77,10 +67,11 @@ pub enum MultiProofMessage {
|
||||
/// This should trigger once the block has been executed (after) the last state update has been
|
||||
/// sent. This triggers the exit condition of the multi proof task.
|
||||
#[derive(Deref, Debug)]
|
||||
pub(super) struct StateHookSender(CrossbeamSender<MultiProofMessage>);
|
||||
pub struct StateHookSender(CrossbeamSender<MultiProofMessage>);
|
||||
|
||||
impl StateHookSender {
|
||||
pub(crate) const fn new(inner: CrossbeamSender<MultiProofMessage>) -> Self {
|
||||
/// Creates a new [`StateHookSender`] wrapping the given channel sender.
|
||||
pub const fn new(inner: CrossbeamSender<MultiProofMessage>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
@@ -127,41 +118,6 @@ pub(crate) fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostStat
|
||||
#[derive(Metrics, Clone)]
|
||||
#[metrics(scope = "tree.root")]
|
||||
pub(crate) struct MultiProofTaskMetrics {
|
||||
/// Histogram of active storage workers processing proofs.
|
||||
pub active_storage_workers_histogram: Histogram,
|
||||
/// Histogram of active account workers processing proofs.
|
||||
pub active_account_workers_histogram: Histogram,
|
||||
/// Gauge for the maximum number of storage workers in the pool.
|
||||
pub max_storage_workers: Gauge,
|
||||
/// Gauge for the maximum number of account workers in the pool.
|
||||
pub max_account_workers: Gauge,
|
||||
/// Histogram of pending storage multiproofs in the queue.
|
||||
pub pending_storage_multiproofs_histogram: Histogram,
|
||||
/// Histogram of pending account multiproofs in the queue.
|
||||
pub pending_account_multiproofs_histogram: Histogram,
|
||||
|
||||
/// Histogram of the number of prefetch proof target accounts.
|
||||
pub prefetch_proof_targets_accounts_histogram: Histogram,
|
||||
/// Histogram of the number of prefetch proof target storages.
|
||||
pub prefetch_proof_targets_storages_histogram: Histogram,
|
||||
/// Histogram of the number of prefetch proof target chunks.
|
||||
pub prefetch_proof_chunks_histogram: Histogram,
|
||||
|
||||
/// Histogram of the number of state update proof target accounts.
|
||||
pub state_update_proof_targets_accounts_histogram: Histogram,
|
||||
/// Histogram of the number of state update proof target storages.
|
||||
pub state_update_proof_targets_storages_histogram: Histogram,
|
||||
/// Histogram of the number of state update proof target chunks.
|
||||
pub state_update_proof_chunks_histogram: Histogram,
|
||||
|
||||
/// Histogram of prefetch proof batch sizes (number of messages merged).
|
||||
pub prefetch_batch_size_histogram: Histogram,
|
||||
|
||||
/// Histogram of proof calculation durations.
|
||||
pub proof_calculation_duration_histogram: Histogram,
|
||||
|
||||
/// Histogram of sparse trie update durations.
|
||||
pub sparse_trie_update_duration_histogram: Histogram,
|
||||
/// Histogram of durations spent revealing multiproof results into the sparse trie.
|
||||
pub sparse_trie_reveal_multiproof_duration_histogram: Histogram,
|
||||
/// Histogram of durations spent coalescing multiple proof results from the channel.
|
||||
@@ -174,21 +130,24 @@ pub(crate) struct MultiProofTaskMetrics {
|
||||
pub sparse_trie_final_update_duration_histogram: Histogram,
|
||||
/// Histogram of sparse trie total durations.
|
||||
pub sparse_trie_total_duration_histogram: Histogram,
|
||||
|
||||
/// Histogram of state updates received.
|
||||
pub state_updates_received_histogram: Histogram,
|
||||
/// Histogram of proofs processed.
|
||||
pub proofs_processed_histogram: Histogram,
|
||||
/// Histogram of total time spent in the multiproof task.
|
||||
pub multiproof_task_total_duration_histogram: Histogram,
|
||||
/// Total time spent waiting for the first state update or prefetch request.
|
||||
pub first_update_wait_time_histogram: Histogram,
|
||||
/// Total time spent waiting for the last proof result.
|
||||
pub last_proof_wait_time_histogram: Histogram,
|
||||
/// Time spent preparing the sparse trie for reuse after state root computation.
|
||||
pub into_trie_for_reuse_duration_histogram: Histogram,
|
||||
/// Time spent waiting for preserved sparse trie cache to become available.
|
||||
pub sparse_trie_cache_wait_duration_histogram: Histogram,
|
||||
|
||||
/// Number of account leaf updates applied without needing a new proof (cache hits).
|
||||
pub sparse_trie_account_cache_hits: Histogram,
|
||||
/// Number of account leaf updates that required a new proof (cache misses).
|
||||
pub sparse_trie_account_cache_misses: Histogram,
|
||||
/// Number of storage leaf updates applied without needing a new proof (cache hits).
|
||||
pub sparse_trie_storage_cache_hits: Histogram,
|
||||
/// Number of storage leaf updates that required a new proof (cache misses).
|
||||
pub sparse_trie_storage_cache_misses: Histogram,
|
||||
|
||||
/// Retained memory of the preserved sparse trie cache in bytes.
|
||||
pub sparse_trie_retained_memory_bytes: Gauge,
|
||||
/// Number of storage tries retained in the preserved sparse trie cache.
|
||||
pub sparse_trie_retained_storage_tries: Gauge,
|
||||
}
|
||||
|
||||
/// Dispatches work items as a single unit or in chunks based on target size and worker
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use parking_lot::Mutex;
|
||||
use reth_trie_sparse::SparseStateTrie;
|
||||
use reth_trie_sparse::{ConfigurableSparseTrie, SparseStateTrie};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tracing::debug;
|
||||
|
||||
/// Type alias for the sparse trie type used in preservation.
|
||||
pub(super) type SparseTrie = SparseStateTrie;
|
||||
pub(super) type SparseTrie = SparseStateTrie<ConfigurableSparseTrie, ConfigurableSparseTrie>;
|
||||
|
||||
/// Shared handle to a preserved sparse trie that can be reused across payload validations.
|
||||
///
|
||||
|
||||
@@ -33,9 +33,9 @@ use reth_provider::{
|
||||
};
|
||||
use reth_revm::{database::StateProviderDatabase, state::EvmState};
|
||||
use reth_tasks::{pool::WorkerPool, Runtime};
|
||||
use reth_trie_parallel::targets_v2::MultiProofTargetsV2;
|
||||
use reth_trie_common::{MultiProofTargetsV2, ProofV2Target};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
Arc,
|
||||
};
|
||||
@@ -149,7 +149,7 @@ where
|
||||
});
|
||||
|
||||
while let Ok((index, tx)) = pending.recv() {
|
||||
if ctx.terminate_execution.load(Ordering::Relaxed) {
|
||||
if ctx.should_stop() {
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
"Termination requested, stopping transaction distribution"
|
||||
@@ -157,6 +157,11 @@ where
|
||||
break;
|
||||
}
|
||||
|
||||
// skip transactions already executed by the main loop
|
||||
if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tx_count += 1;
|
||||
let parent_span = Span::current();
|
||||
s.spawn(move |_| {
|
||||
@@ -202,13 +207,18 @@ where
|
||||
Tx: ExecutableTxFor<Evm>,
|
||||
{
|
||||
WorkerPool::with_worker_mut(|worker| {
|
||||
let Some((evm, metrics, terminate_execution)) =
|
||||
let Some(evm) =
|
||||
worker.get_or_init::<PrewarmEvmState<Evm>>(|| ctx.evm_for_ctx()).as_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if terminate_execution.load(Ordering::Relaxed) {
|
||||
if ctx.should_stop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip if main execution has already processed this transaction
|
||||
if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,25 +235,25 @@ where
|
||||
sender=%tx.signer(),
|
||||
"Error when executing prewarm transaction",
|
||||
);
|
||||
metrics.transaction_errors.increment(1);
|
||||
ctx.metrics.transaction_errors.increment(1);
|
||||
return;
|
||||
}
|
||||
};
|
||||
metrics.execution_duration.record(start.elapsed());
|
||||
ctx.metrics.execution_duration.record(start.elapsed());
|
||||
|
||||
if terminate_execution.load(Ordering::Relaxed) {
|
||||
if ctx.should_stop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if index > 0 {
|
||||
let (targets, storage_targets) = multiproof_targets_from_state(res.state);
|
||||
metrics.prefetch_storage_targets.record(storage_targets as f64);
|
||||
ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
|
||||
if let Some(to_multi_proof) = to_multi_proof {
|
||||
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
|
||||
}
|
||||
}
|
||||
|
||||
metrics.total_runtime.record(start.elapsed());
|
||||
ctx.metrics.total_runtime.record(start.elapsed());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,7 +358,7 @@ where
|
||||
bal.par_iter().for_each_init(
|
||||
|| (ctx.clone(), None::<CachedStateProvider<reth_provider::StateProviderBox>>),
|
||||
|(ctx, provider), account| {
|
||||
if ctx.terminate_execution.load(Ordering::Relaxed) {
|
||||
if ctx.should_stop() {
|
||||
return;
|
||||
}
|
||||
ctx.prefetch_bal_account(provider, account);
|
||||
@@ -442,7 +452,7 @@ where
|
||||
PrewarmTaskEvent::TerminateTransactionExecution => {
|
||||
// stop tx processing
|
||||
debug!(target: "engine::tree::prewarm", "Terminating prewarm execution");
|
||||
self.ctx.terminate_execution.store(true, Ordering::Relaxed);
|
||||
self.ctx.stop();
|
||||
}
|
||||
PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx } => {
|
||||
trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
|
||||
@@ -497,6 +507,10 @@ where
|
||||
pub metrics: PrewarmMetrics,
|
||||
/// 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
|
||||
/// loop. Prewarm workers skip transactions with `index < counter` since those have already
|
||||
/// been executed.
|
||||
pub executed_tx_index: Arc<AtomicUsize>,
|
||||
/// Whether the precompile cache is disabled.
|
||||
pub precompile_cache_disabled: bool,
|
||||
/// The precompile cache map.
|
||||
@@ -505,11 +519,8 @@ where
|
||||
|
||||
/// Per-thread EVM state initialised by [`PrewarmContext::evm_for_ctx`] and stored in
|
||||
/// [`WorkerPool`] workers via [`Worker::get_or_init`](reth_tasks::pool::Worker::get_or_init).
|
||||
type PrewarmEvmState<Evm> = Option<(
|
||||
EvmFor<Evm, StateProviderDatabase<reth_provider::StateProviderBox>>,
|
||||
PrewarmMetrics,
|
||||
Arc<AtomicBool>,
|
||||
)>;
|
||||
type PrewarmEvmState<Evm> =
|
||||
Option<EvmFor<Evm, StateProviderDatabase<reth_provider::StateProviderBox>>>;
|
||||
|
||||
impl<N, P, Evm> PrewarmContext<N, P, Evm>
|
||||
where
|
||||
@@ -517,7 +528,7 @@ where
|
||||
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
|
||||
Evm: ConfigureEvm<Primitives = N> + 'static,
|
||||
{
|
||||
/// Creates a per-thread EVM, metrics handle, and termination flag for prewarming.
|
||||
/// Creates a per-thread EVM for prewarming.
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
|
||||
fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
|
||||
let mut state_provider = match self.provider.build() {
|
||||
@@ -558,7 +569,7 @@ where
|
||||
|
||||
if !self.precompile_cache_disabled {
|
||||
// Only cache pure precompiles to avoid issues with stateful precompiles
|
||||
evm.precompiles_mut().map_pure_precompiles(|address, precompile| {
|
||||
evm.precompiles_mut().map_cacheable_precompiles(|address, precompile| {
|
||||
CachedPrecompile::wrap(
|
||||
precompile,
|
||||
self.precompile_cache_map.cache_for_address(*address),
|
||||
@@ -568,7 +579,19 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
Some((evm, self.metrics.clone(), self.terminate_execution.clone()))
|
||||
Some(evm)
|
||||
}
|
||||
|
||||
/// Returns `true` if prewarming should stop.
|
||||
#[inline]
|
||||
pub fn should_stop(&self) -> bool {
|
||||
self.terminate_execution.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Signals all prewarm tasks to stop execution.
|
||||
#[inline]
|
||||
pub fn stop(&self) {
|
||||
self.terminate_execution.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Prefetches a single account and all its storage slots from the BAL into the cache.
|
||||
@@ -620,8 +643,6 @@ where
|
||||
/// Returns a set of [`MultiProofTargetsV2`] and the total amount of storage targets, based on the
|
||||
/// given state.
|
||||
fn multiproof_targets_from_state(state: EvmState) -> (MultiProofTargetsV2, usize) {
|
||||
use reth_trie::proof_v2;
|
||||
|
||||
let mut targets = MultiProofTargetsV2::default();
|
||||
let mut storage_target_count = 0;
|
||||
for (addr, account) in state {
|
||||
@@ -647,7 +668,7 @@ fn multiproof_targets_from_state(state: EvmState) -> (MultiProofTargetsV2, usize
|
||||
}
|
||||
|
||||
let hashed_slot = keccak256(B256::new(key.to_be_bytes()));
|
||||
storage_slots.push(proof_v2::Target::from(hashed_slot));
|
||||
storage_slots.push(ProofV2Target::from(hashed_slot));
|
||||
}
|
||||
|
||||
storage_target_count += storage_slots.len();
|
||||
|
||||
@@ -12,25 +12,25 @@ use crate::tree::{
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rlp::{Decodable, Encodable};
|
||||
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
|
||||
use rayon::iter::ParallelIterator;
|
||||
use reth_primitives_traits::{Account, FastInstant as Instant, ParallelBridgeBuffered};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use reth_primitives_traits::{Account, FastInstant as Instant};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_trie::{
|
||||
proof_v2::Target, updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount,
|
||||
EMPTY_ROOT_HASH, TRIE_ACCOUNT_RLP_MAX_SIZE,
|
||||
updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount, EMPTY_ROOT_HASH,
|
||||
TRIE_ACCOUNT_RLP_MAX_SIZE,
|
||||
};
|
||||
use reth_trie_common::{MultiProofTargetsV2, ProofV2Target};
|
||||
use reth_trie_parallel::{
|
||||
proof_task::{
|
||||
AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofWorkerHandle,
|
||||
},
|
||||
root::ParallelStateRootError,
|
||||
targets_v2::MultiProofTargetsV2,
|
||||
};
|
||||
#[cfg(feature = "trie-debug")]
|
||||
use reth_trie_sparse::debug_recorder::TrieDebugRecorder;
|
||||
use reth_trie_sparse::{
|
||||
errors::SparseTrieResult, DeferredDrops, LeafUpdate, ParallelSparseTrie, SparseStateTrie,
|
||||
SparseTrie,
|
||||
errors::SparseTrieResult, ConfigurableSparseTrie, DeferredDrops, LeafUpdate,
|
||||
RevealableSparseTrie, SparseStateTrie, SparseTrie,
|
||||
};
|
||||
use revm_primitives::{hash_map::Entry, B256Map};
|
||||
use tracing::{debug, debug_span, error, instrument, trace_span};
|
||||
@@ -39,7 +39,7 @@ use tracing::{debug, debug_span, error, instrument, trace_span};
|
||||
const MAX_PENDING_UPDATES: usize = 100;
|
||||
|
||||
/// Sparse trie task implementation that uses in-memory sparse trie data to schedule proof fetching.
|
||||
pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparseTrie> {
|
||||
pub(super) struct SparseTrieCacheTask<A = ConfigurableSparseTrie, S = ConfigurableSparseTrie> {
|
||||
/// Sender for proof results.
|
||||
proof_result_tx: CrossbeamSender<ProofResultMessage>,
|
||||
/// Receiver for proof results directly from workers.
|
||||
@@ -92,6 +92,14 @@ pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparse
|
||||
account_rlp_buf: Vec<u8>,
|
||||
/// Whether the last state update has been received.
|
||||
finished_state_updates: bool,
|
||||
/// Accumulated account leaf update cache hits.
|
||||
account_cache_hits: u64,
|
||||
/// Accumulated account leaf update cache misses.
|
||||
account_cache_misses: u64,
|
||||
/// Accumulated storage leaf update cache hits.
|
||||
storage_cache_hits: u64,
|
||||
/// Accumulated storage leaf update cache misses.
|
||||
storage_cache_misses: u64,
|
||||
/// Pending proof targets queued for dispatch to proof workers.
|
||||
pending_targets: PendingTargets,
|
||||
/// Number of pending execution/prewarming updates received but not yet passed to
|
||||
@@ -142,6 +150,10 @@ where
|
||||
fetched_storage_targets: Default::default(),
|
||||
account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
|
||||
finished_state_updates: Default::default(),
|
||||
account_cache_hits: 0,
|
||||
account_cache_misses: 0,
|
||||
storage_cache_hits: 0,
|
||||
storage_cache_misses: 0,
|
||||
pending_targets: Default::default(),
|
||||
pending_updates: Default::default(),
|
||||
metrics,
|
||||
@@ -167,9 +179,7 @@ where
|
||||
MultiProofMessage::FinishedStateUpdates => {
|
||||
SparseTrieTaskMessage::FinishedStateUpdates
|
||||
}
|
||||
MultiProofMessage::EmptyProof { .. } | MultiProofMessage::BlockAccessList(_) => {
|
||||
continue
|
||||
}
|
||||
MultiProofMessage::BlockAccessList(_) => continue,
|
||||
MultiProofMessage::HashedStateUpdate(state) => {
|
||||
SparseTrieTaskMessage::HashedState(state)
|
||||
}
|
||||
@@ -189,8 +199,8 @@ where
|
||||
/// benchmarking purposes.
|
||||
pub(super) fn into_trie_for_reuse(
|
||||
self,
|
||||
prune_depth: usize,
|
||||
max_storage_tries: usize,
|
||||
max_hot_slots: usize,
|
||||
max_hot_accounts: usize,
|
||||
max_nodes_capacity: usize,
|
||||
max_values_capacity: usize,
|
||||
disable_pruning: bool,
|
||||
@@ -199,7 +209,7 @@ where
|
||||
let Self { mut trie, .. } = self;
|
||||
trie.commit_updates(updates);
|
||||
if !disable_pruning {
|
||||
trie.prune(prune_depth, max_storage_tries);
|
||||
trie.prune(max_hot_slots, max_hot_accounts);
|
||||
trie.shrink_to(max_nodes_capacity, max_values_capacity);
|
||||
}
|
||||
let deferred = trie.take_deferred_drops();
|
||||
@@ -332,6 +342,15 @@ where
|
||||
self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
|
||||
self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
|
||||
|
||||
self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
|
||||
self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
|
||||
self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
|
||||
self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
|
||||
self.account_cache_hits = 0;
|
||||
self.account_cache_misses = 0;
|
||||
self.storage_cache_hits = 0;
|
||||
self.storage_cache_misses = 0;
|
||||
|
||||
Ok(StateRootComputeOutcome {
|
||||
state_root,
|
||||
trie_updates: Arc::new(trie_updates),
|
||||
@@ -363,13 +382,13 @@ where
|
||||
}
|
||||
|
||||
for (address, slots) in targets.storage_targets {
|
||||
for slot in slots {
|
||||
// Only touch storages that are not yet present in the updates set.
|
||||
self.new_storage_updates
|
||||
.entry(address)
|
||||
.or_default()
|
||||
.entry(slot.key())
|
||||
.or_insert(LeafUpdate::Touched);
|
||||
if !slots.is_empty() {
|
||||
// Look up outer map once per address instead of once per slot.
|
||||
let new_updates = self.new_storage_updates.entry(address).or_default();
|
||||
for slot in slots {
|
||||
// Only touch storages that are not yet present in the updates set.
|
||||
new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
|
||||
}
|
||||
}
|
||||
|
||||
// Touch corresponding account leaf to make sure its revealed in accounts trie for
|
||||
@@ -386,19 +405,26 @@ where
|
||||
)]
|
||||
fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
|
||||
for (address, storage) in hashed_state_update.storages {
|
||||
for (slot, value) in storage.storage {
|
||||
let encoded = if value.is_zero() {
|
||||
Vec::new()
|
||||
} else {
|
||||
alloy_rlp::encode_fixed_size(&value).to_vec()
|
||||
};
|
||||
self.new_storage_updates
|
||||
.entry(address)
|
||||
.or_default()
|
||||
.insert(slot, LeafUpdate::Changed(encoded));
|
||||
if !storage.storage.is_empty() {
|
||||
// Look up outer maps once per address instead of once per slot.
|
||||
let new_updates = self.new_storage_updates.entry(address).or_default();
|
||||
let mut existing_updates = self.storage_updates.get_mut(&address);
|
||||
|
||||
// Remove an existing storage update if it exists.
|
||||
self.storage_updates.get_mut(&address).and_then(|updates| updates.remove(&slot));
|
||||
for (slot, value) in storage.storage {
|
||||
self.trie.record_slot_touch(address, slot);
|
||||
|
||||
let encoded = if value.is_zero() {
|
||||
Vec::new()
|
||||
} else {
|
||||
alloy_rlp::encode_fixed_size(&value).to_vec()
|
||||
};
|
||||
new_updates.insert(slot, LeafUpdate::Changed(encoded));
|
||||
|
||||
// Remove an existing storage update if it exists.
|
||||
if let Some(ref mut existing) = existing_updates {
|
||||
existing.remove(&slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure account is tracked in `account_updates` so that it is revealed in accounts
|
||||
@@ -411,6 +437,8 @@ where
|
||||
}
|
||||
|
||||
for (address, account) in hashed_state_update.accounts {
|
||||
self.trie.record_account_touch(address);
|
||||
|
||||
// Track account as touched.
|
||||
//
|
||||
// This might overwrite an existing update, which is fine, because storage root from it
|
||||
@@ -505,18 +533,22 @@ where
|
||||
let fetched = self.fetched_storage_targets.entry(*address).or_default();
|
||||
let mut targets = Vec::new();
|
||||
|
||||
let updates_len_before = updates.len();
|
||||
trie.update_leaves(updates, |path, min_len| match fetched.entry(path) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if min_len < *entry.get() {
|
||||
entry.insert(min_len);
|
||||
targets.push(Target::new(path).with_min_len(min_len));
|
||||
targets.push(ProofV2Target::new(path).with_min_len(min_len));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(min_len);
|
||||
targets.push(Target::new(path).with_min_len(min_len));
|
||||
targets.push(ProofV2Target::new(path).with_min_len(min_len));
|
||||
}
|
||||
})?;
|
||||
let updates_len_after = updates.len();
|
||||
self.storage_cache_hits += (updates_len_before - updates_len_after) as u64;
|
||||
self.storage_cache_misses += updates_len_after as u64;
|
||||
|
||||
if !targets.is_empty() {
|
||||
self.pending_targets.extend_storage_targets(address, targets);
|
||||
@@ -551,18 +583,76 @@ where
|
||||
if min_len < *entry.get() {
|
||||
entry.insert(min_len);
|
||||
self.pending_targets
|
||||
.push_account_target(Target::new(target).with_min_len(min_len));
|
||||
.push_account_target(ProofV2Target::new(target).with_min_len(min_len));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(min_len);
|
||||
self.pending_targets
|
||||
.push_account_target(Target::new(target).with_min_len(min_len));
|
||||
.push_account_target(ProofV2Target::new(target).with_min_len(min_len));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(account_updates.len() < updates_len_before)
|
||||
let updates_len_after = account_updates.len();
|
||||
self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
|
||||
self.account_cache_misses += updates_len_after as u64;
|
||||
|
||||
Ok(updates_len_after < updates_len_before)
|
||||
}
|
||||
|
||||
/// Computes storage roots for accounts whose storage updates are fully drained.
|
||||
///
|
||||
/// For each storage trie T that:
|
||||
/// 1. was modified in the current block,
|
||||
/// 2. all the storage updates are fully drained,
|
||||
/// 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
|
||||
.iter()
|
||||
.filter_map(|(address, updates)| updates.is_empty().then_some(*address))
|
||||
.collect();
|
||||
|
||||
struct SendStorageTriePtr<S>(*mut RevealableSparseTrie<S>);
|
||||
// SAFETY: this wrapper only forwards the pointer across rayon; deref invariants are
|
||||
// documented at the use site below.
|
||||
unsafe impl<S: Send> Send for SendStorageTriePtr<S> {}
|
||||
|
||||
let mut tries_to_compute_roots: Vec<(B256, SendStorageTriePtr<S>)> =
|
||||
Vec::with_capacity(addresses_to_compute_roots.len());
|
||||
for address in addresses_to_compute_roots {
|
||||
if let Some(trie) = self.trie.storage_tries_mut().get_mut(&address) &&
|
||||
!trie.is_root_cached()
|
||||
{
|
||||
tries_to_compute_roots.push((address, SendStorageTriePtr(trie)));
|
||||
}
|
||||
}
|
||||
|
||||
let parent_span = tracing::Span::current();
|
||||
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();
|
||||
// SAFETY:
|
||||
// - pointers are created from `storage_tries_mut().get_mut(address)` above;
|
||||
// - `addresses_to_compute_roots` comes from map iteration, so addresses are unique;
|
||||
// - we do not insert/remove entries between pointer collection and use, so pointers
|
||||
// stay valid and map reallocation cannot occur;
|
||||
// - each pointer is consumed by at most one rayon task, so no aliasing mutable access.
|
||||
unsafe { (*trie).root().expect("updates are drained, trie should be revealed by now") };
|
||||
});
|
||||
}
|
||||
|
||||
/// Iterates through all storage tries for which all updates were processed, computes their
|
||||
@@ -580,21 +670,7 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let span = debug_span!("compute_storage_roots").entered();
|
||||
self
|
||||
.trie
|
||||
.storage_tries_mut()
|
||||
.iter_mut()
|
||||
.filter(|(address, trie)| {
|
||||
self.storage_updates.get(*address).is_some_and(|updates| updates.is_empty()) &&
|
||||
!trie.is_root_cached()
|
||||
})
|
||||
.par_bridge_buffered()
|
||||
.for_each(|(address, trie)| {
|
||||
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_root", ?address).entered();
|
||||
trie.root().expect("updates are drained, trie should be revealed by now");
|
||||
});
|
||||
drop(span);
|
||||
self.compute_drained_storage_roots();
|
||||
|
||||
loop {
|
||||
let span = debug_span!("promote_updates", promoted = tracing::field::Empty).entered();
|
||||
@@ -734,13 +810,13 @@ impl PendingTargets {
|
||||
}
|
||||
|
||||
/// Adds a target to the account targets.
|
||||
fn push_account_target(&mut self, target: Target) {
|
||||
fn push_account_target(&mut self, target: ProofV2Target) {
|
||||
self.targets.account_targets.push(target);
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
/// Extends storage targets for the given address.
|
||||
fn extend_storage_targets(&mut self, address: &B256, targets: Vec<Target>) {
|
||||
fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
|
||||
self.len += targets.len();
|
||||
self.targets.storage_targets.entry(*address).or_default().extend(targets);
|
||||
}
|
||||
@@ -774,7 +850,7 @@ pub struct StateRootComputeOutcome {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::{keccak256, Address, B256, U256};
|
||||
use reth_trie_sparse::ParallelSparseTrie;
|
||||
use reth_trie_sparse::ArenaParallelSparseTrie;
|
||||
|
||||
#[test]
|
||||
fn test_run_hashing_task_hashed_state_update_forwards() {
|
||||
@@ -797,7 +873,7 @@ mod tests {
|
||||
let expected_state = hashed_state.clone();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
SparseTrieCacheTask::<ParallelSparseTrie, ParallelSparseTrie>::run_hashing_task(
|
||||
SparseTrieCacheTask::<ArenaParallelSparseTrie, ArenaParallelSparseTrie>::run_hashing_task(
|
||||
updates_rx,
|
||||
hashed_state_tx,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Types and traits for validating blocks and payloads.
|
||||
|
||||
use crate::tree::{
|
||||
cached_state::CachedStateProvider,
|
||||
cached_state::{CacheStats, CachedStateProvider},
|
||||
error::{InsertBlockError, InsertBlockErrorKind, InsertPayloadError},
|
||||
instrumented_state::InstrumentedStateProvider,
|
||||
instrumented_state::{InstrumentedStateProvider, StateProviderStats},
|
||||
payload_processor::PayloadProcessor,
|
||||
precompile_cache::{CachedPrecompile, CachedPrecompileMetrics, PrecompileCacheMap},
|
||||
sparse_trie::StateRootComputeOutcome,
|
||||
@@ -14,12 +14,14 @@ use alloy_consensus::transaction::{Either, TxHashRef};
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
|
||||
use alloy_evm::Evm;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_primitives::{map::B256Set, B256};
|
||||
#[cfg(feature = "trie-debug")]
|
||||
use reth_trie_sparse::debug_recorder::TrieDebugRecorder;
|
||||
|
||||
use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptRootTaskHandle};
|
||||
use reth_chain_state::{CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, LazyOverlay};
|
||||
use reth_chain_state::{
|
||||
CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, ExecutionTimingStats, LazyOverlay,
|
||||
};
|
||||
use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
|
||||
use reth_engine_primitives::{
|
||||
ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
|
||||
@@ -27,7 +29,7 @@ use reth_engine_primitives::{
|
||||
use reth_errors::{BlockExecutionError, ProviderResult};
|
||||
use reth_evm::{
|
||||
block::BlockExecutor, execute::ExecutableTxFor, ConfigureEvm, EvmEnvFor, ExecutionCtxFor,
|
||||
SpecFor,
|
||||
OnStateHook, SpecFor,
|
||||
};
|
||||
use reth_payload_primitives::{
|
||||
BuiltPayload, InvalidPayloadAttributesError, NewPayloadError, PayloadTypes,
|
||||
@@ -42,21 +44,36 @@ use reth_provider::{
|
||||
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
|
||||
StateProviderFactory, StateReader, StorageChangeSetReader, StorageSettingsCache,
|
||||
};
|
||||
use reth_revm::db::{states::bundle_state::BundleRetention, State};
|
||||
use reth_trie::{updates::TrieUpdates, HashedPostState, StateRoot};
|
||||
use reth_revm::db::{states::bundle_state::BundleRetention, BundleAccount, State};
|
||||
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState, StateRoot};
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
|
||||
use revm_primitives::Address;
|
||||
use revm_primitives::{Address, KECCAK_EMPTY};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
panic::{self, AssertUnwindSafe},
|
||||
sync::{mpsc::RecvTimeoutError, Arc},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::RecvTimeoutError,
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{debug, debug_span, error, info, instrument, trace, warn, Span};
|
||||
|
||||
/// Output of block or payload validation.
|
||||
pub type ValidationOutcome<N, E = InsertPayloadError<BlockTy<N>>> =
|
||||
Result<(ExecutedBlock<N>, Option<Box<ExecutionTimingStats>>), E>;
|
||||
|
||||
/// Handle to a [`HashedPostState`] computed on a background thread.
|
||||
type LazyHashedPostState = reth_tasks::LazyHandle<HashedPostState>;
|
||||
|
||||
/// Result type for block validation with optional timing stats.
|
||||
type InsertPayloadResult<N> = Result<
|
||||
(ExecutedBlock<N>, Option<Box<ExecutionTimingStats>>),
|
||||
InsertPayloadError<<N as NodePrimitives>::Block>,
|
||||
>;
|
||||
|
||||
/// Context providing access to tree state during validation.
|
||||
///
|
||||
/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
|
||||
@@ -85,7 +102,9 @@ impl<'a, N: NodePrimitives> TreeCtx<'a, N> {
|
||||
) -> Self {
|
||||
Self { state, canonical_in_memory_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, N: NodePrimitives> TreeCtx<'a, N> {
|
||||
/// Returns a reference to the engine tree state
|
||||
pub const fn state(&self) -> &EngineApiTreeState<N> {
|
||||
&*self.state
|
||||
@@ -276,7 +295,7 @@ where
|
||||
input: BlockOrPayload<T>,
|
||||
execution_err: InsertBlockErrorKind,
|
||||
parent_block: &SealedHeader<N::BlockHeader>,
|
||||
) -> Result<ExecutedBlock<N>, InsertPayloadError<N::Block>>
|
||||
) -> InsertPayloadResult<N>
|
||||
where
|
||||
V: PayloadValidator<T, Block = N::Block>,
|
||||
{
|
||||
@@ -329,7 +348,7 @@ where
|
||||
&mut self,
|
||||
input: BlockOrPayload<T>,
|
||||
mut ctx: TreeCtx<'_, N>,
|
||||
) -> ValidationOutcome<N, InsertPayloadError<N::Block>>
|
||||
) -> InsertPayloadResult<N>
|
||||
where
|
||||
V: PayloadValidator<T, Block = N::Block> + Clone,
|
||||
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
|
||||
@@ -481,25 +500,39 @@ where
|
||||
block_access_list,
|
||||
));
|
||||
|
||||
// Create optional cache stats for detailed block logging
|
||||
let slow_block_enabled = self.config.slow_block_threshold().is_some();
|
||||
let cache_stats = slow_block_enabled.then(|| Arc::new(CacheStats::default()));
|
||||
|
||||
// Use cached state provider before executing, used in execution after prewarming threads
|
||||
// complete
|
||||
if let Some((caches, cache_metrics)) = handle.caches().zip(handle.cache_metrics()) {
|
||||
state_provider =
|
||||
Box::new(CachedStateProvider::new(state_provider, caches, cache_metrics));
|
||||
state_provider = Box::new(
|
||||
CachedStateProvider::new(state_provider, caches, cache_metrics)
|
||||
.with_cache_stats(cache_stats.clone()),
|
||||
);
|
||||
};
|
||||
|
||||
if self.config.state_provider_metrics() {
|
||||
state_provider = Box::new(InstrumentedStateProvider::new(state_provider, "engine"));
|
||||
}
|
||||
let state_provider_stats = if slow_block_enabled || self.config.state_provider_metrics() {
|
||||
let instrumented_state_provider =
|
||||
InstrumentedStateProvider::new(state_provider, "engine");
|
||||
let stats = slow_block_enabled.then(|| instrumented_state_provider.stats());
|
||||
state_provider = Box::new(instrumented_state_provider);
|
||||
stats
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Execute the block and handle any execution errors.
|
||||
// The receipt root task is spawned before execution and receives receipts incrementally
|
||||
// as transactions complete, allowing parallel computation during execution.
|
||||
let execute_block_start = Instant::now();
|
||||
let (output, senders, receipt_root_rx) =
|
||||
match self.execute_block(state_provider, env, &input, &mut handle) {
|
||||
Ok(output) => output,
|
||||
Err(err) => return self.handle_execution_error(input, err, &parent_block),
|
||||
};
|
||||
let execution_duration = execute_block_start.elapsed();
|
||||
|
||||
// After executing the block we can stop prewarming transactions
|
||||
handle.stop_prewarming_execution();
|
||||
@@ -732,18 +765,37 @@ where
|
||||
.into())
|
||||
}
|
||||
|
||||
let timing_stats = state_provider_stats.map(|stats| {
|
||||
self.calculate_timing_stats(
|
||||
&block,
|
||||
stats,
|
||||
cache_stats,
|
||||
&output,
|
||||
execution_duration,
|
||||
root_elapsed,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(valid_block_tx) = valid_block_tx {
|
||||
let _ = valid_block_tx.send(());
|
||||
}
|
||||
|
||||
Ok(self.spawn_deferred_trie_task(
|
||||
// Create the overlay provider NOW, while we're on the engine loop thread and trie changeset
|
||||
// eviction cannot race with us. If we deferred this to the background task, persistence
|
||||
// could advance and evict changeset cache entries between factory creation and the task
|
||||
// actually running, causing expensive DB fallback computations when building the overlay.
|
||||
let changeset_provider =
|
||||
ensure_ok_post_block!(overlay_factory.database_provider_ro(), block);
|
||||
|
||||
let executed_block = self.spawn_deferred_trie_task(
|
||||
block,
|
||||
output,
|
||||
&ctx,
|
||||
hashed_state,
|
||||
trie_output,
|
||||
overlay_factory,
|
||||
))
|
||||
changeset_provider,
|
||||
);
|
||||
Ok((executed_block, timing_stats))
|
||||
}
|
||||
|
||||
/// Return sealed block header from database or in-memory state by hash.
|
||||
@@ -821,7 +873,6 @@ where
|
||||
State::builder()
|
||||
.with_database(StateProviderDatabase::new(state_provider))
|
||||
.with_bundle_update()
|
||||
.without_state_clear()
|
||||
.build()
|
||||
});
|
||||
|
||||
@@ -838,19 +889,21 @@ where
|
||||
|
||||
if !self.config.precompile_cache_disabled() {
|
||||
let _span = debug_span!(target: "engine::tree", "setup_precompile_cache").entered();
|
||||
executor.evm_mut().precompiles_mut().map_pure_precompiles(|address, precompile| {
|
||||
let metrics = self
|
||||
.precompile_cache_metrics
|
||||
.entry(*address)
|
||||
.or_insert_with(|| CachedPrecompileMetrics::new_with_address(*address))
|
||||
.clone();
|
||||
CachedPrecompile::wrap(
|
||||
precompile,
|
||||
self.precompile_cache_map.cache_for_address(*address),
|
||||
spec_id,
|
||||
Some(metrics),
|
||||
)
|
||||
});
|
||||
executor.evm_mut().precompiles_mut().map_cacheable_precompiles(
|
||||
|address, precompile| {
|
||||
let metrics = self
|
||||
.precompile_cache_metrics
|
||||
.entry(*address)
|
||||
.or_insert_with(|| CachedPrecompileMetrics::new_with_address(*address))
|
||||
.clone();
|
||||
CachedPrecompile::wrap(
|
||||
precompile,
|
||||
self.precompile_cache_map.cache_for_address(*address),
|
||||
spec_id,
|
||||
Some(metrics),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn background task to compute receipt root and logs bloom incrementally.
|
||||
@@ -864,7 +917,10 @@ where
|
||||
.spawn_blocking_named("receipt-root", move || task_handle.run(receipts_len));
|
||||
|
||||
let transaction_count = input.transaction_count();
|
||||
let executor = executor.with_state_hook(Some(Box::new(handle.state_hook())));
|
||||
let executed_tx_index = Arc::clone(handle.executed_tx_index());
|
||||
let executor = executor.with_state_hook(
|
||||
handle.state_hook().map(|hook| Box::new(hook) as Box<dyn OnStateHook>),
|
||||
);
|
||||
|
||||
let execution_start = Instant::now();
|
||||
|
||||
@@ -874,6 +930,7 @@ where
|
||||
transaction_count,
|
||||
handle.iter_transactions(),
|
||||
&receipt_tx,
|
||||
&executed_tx_index,
|
||||
)?;
|
||||
drop(receipt_tx);
|
||||
|
||||
@@ -893,8 +950,8 @@ where
|
||||
let execution_duration = execution_start.elapsed();
|
||||
self.metrics.record_block_execution(&output, execution_duration);
|
||||
self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
|
||||
|
||||
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
|
||||
|
||||
Ok((output, senders, result_rx))
|
||||
}
|
||||
|
||||
@@ -913,6 +970,7 @@ where
|
||||
transaction_count: usize,
|
||||
transactions: impl Iterator<Item = Result<Tx, Err>>,
|
||||
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
|
||||
executed_tx_index: &AtomicUsize,
|
||||
) -> Result<(E, Vec<Address>), BlockExecutionError>
|
||||
where
|
||||
E: BlockExecutor<Receipt = N::Receipt>,
|
||||
@@ -951,6 +1009,7 @@ where
|
||||
let _enter = debug_span!(
|
||||
target: "engine::tree",
|
||||
"execute tx",
|
||||
tx_index = senders.len() - 1,
|
||||
)
|
||||
.entered();
|
||||
trace!(target: "engine::tree", "Executing transaction");
|
||||
@@ -959,6 +1018,9 @@ where
|
||||
executor.execute_transaction(tx)?;
|
||||
self.metrics.record_transaction_execution(tx_start.elapsed());
|
||||
|
||||
// advance the shared counter so prewarm workers skip already-executed txs
|
||||
executed_tx_index.store(senders.len(), Ordering::Relaxed);
|
||||
|
||||
let current_len = executor.receipts().len();
|
||||
if current_len > last_sent_len {
|
||||
last_sent_len = current_len;
|
||||
@@ -1521,7 +1583,7 @@ where
|
||||
ctx: &TreeCtx<'_, N>,
|
||||
hashed_state: LazyHashedPostState,
|
||||
trie_output: Arc<TrieUpdates>,
|
||||
overlay_factory: OverlayStateProviderFactory<P>,
|
||||
changeset_provider: impl TrieCursorFactory + Send + 'static,
|
||||
) -> ExecutedBlock<N> {
|
||||
// Capture parent hash and ancestor overlays for deferred trie input construction.
|
||||
let (anchor_hash, overlay_blocks) = ctx
|
||||
@@ -1550,7 +1612,11 @@ where
|
||||
// Capture block info and cache handle for changeset computation
|
||||
let block_hash = block.hash();
|
||||
let block_number = block.number();
|
||||
let changeset_cache = self.changeset_cache.clone();
|
||||
|
||||
// Register a pending changeset entry so that concurrent readers will wait for
|
||||
// this computation to finish rather than falling back to the expensive DB path.
|
||||
// The guard ensures the pending entry is cancelled if the task panics.
|
||||
let pending_changeset_guard = self.changeset_cache.register_pending(block_hash);
|
||||
|
||||
// Spawn background task to compute trie data. Calling `wait_cloned` will compute from
|
||||
// the stored inputs and cache the result, so subsequent calls return immediately.
|
||||
@@ -1585,20 +1651,15 @@ where
|
||||
.record(anchored.trie_input.state.total_len() as f64);
|
||||
}
|
||||
|
||||
// Compute and cache changesets using the computed trie_updates
|
||||
// Compute and cache changesets using the computed trie_updates.
|
||||
// Use the pre-created provider to avoid races with changeset cache
|
||||
// eviction that can happen between task spawn and execution.
|
||||
let changeset_start = Instant::now();
|
||||
|
||||
// Get a provider from the overlay factory for trie cursor access
|
||||
let changeset_result =
|
||||
overlay_factory.database_provider_ro().and_then(|provider| {
|
||||
reth_trie::changesets::compute_trie_changesets(
|
||||
&provider,
|
||||
&computed.trie_updates,
|
||||
)
|
||||
.map_err(ProviderError::Database)
|
||||
});
|
||||
|
||||
match changeset_result {
|
||||
match reth_trie::changesets::compute_trie_changesets(
|
||||
&changeset_provider,
|
||||
&computed.trie_updates,
|
||||
) {
|
||||
Ok(changesets) => {
|
||||
debug!(
|
||||
target: "engine::tree::changeset",
|
||||
@@ -1607,7 +1668,7 @@ where
|
||||
"Computed and caching changesets"
|
||||
);
|
||||
|
||||
changeset_cache.insert(block_hash, block_number, Arc::new(changesets));
|
||||
pending_changeset_guard.resolve(block_number, Arc::new(changesets));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -1639,10 +1700,142 @@ where
|
||||
deferred_trie_data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Output of block or payload validation.
|
||||
pub type ValidationOutcome<N, E = InsertPayloadError<BlockTy<N>>> = Result<ExecutedBlock<N>, E>;
|
||||
fn calculate_timing_stats(
|
||||
&self,
|
||||
block: &RecoveredBlock<N::Block>,
|
||||
provider_stats: Arc<StateProviderStats>,
|
||||
cache_stats: Option<Arc<CacheStats>>,
|
||||
output: &BlockExecutionOutput<N::Receipt>,
|
||||
execution_duration: Duration,
|
||||
state_hash_duration: Duration,
|
||||
) -> Box<ExecutionTimingStats> {
|
||||
let accounts_read = provider_stats.total_account_fetches();
|
||||
let storage_read = provider_stats.total_storage_fetches();
|
||||
let code_read = provider_stats.total_code_fetches();
|
||||
let code_bytes_read = provider_stats.total_code_fetched_bytes();
|
||||
|
||||
// Write stats from BundleState (final state changes)
|
||||
let accounts_changed = output.state.state.len();
|
||||
let accounts_deleted =
|
||||
output.state.state.values().filter(|acc| acc.was_destroyed()).count();
|
||||
let storage_slots_changed =
|
||||
output.state.state.values().map(|account| account.storage.len()).sum::<usize>();
|
||||
let storage_slots_deleted = output
|
||||
.state
|
||||
.state
|
||||
.values()
|
||||
.flat_map(|account| account.storage.values())
|
||||
.filter(|slot| {
|
||||
slot.present_value.is_zero() && !slot.previous_or_original_value.is_zero()
|
||||
})
|
||||
.count();
|
||||
|
||||
// Helper: check if account represents a new contract deployment
|
||||
let is_new_deployment = |acc: &BundleAccount| -> bool {
|
||||
let has_code_now = acc.info.as_ref().is_some_and(|info| info.code_hash != KECCAK_EMPTY);
|
||||
let had_no_code_before = acc
|
||||
.original_info
|
||||
.as_ref()
|
||||
.map(|info| info.code_hash == KECCAK_EMPTY)
|
||||
.unwrap_or(true);
|
||||
has_code_now && had_no_code_before
|
||||
};
|
||||
|
||||
let bytecodes_changed =
|
||||
output.state.state.values().filter(|acc| is_new_deployment(acc)).count();
|
||||
|
||||
// Unique new code hashes to count actual bytes persisted (deduplicated)
|
||||
let unique_new_code_hashes: B256Set = output
|
||||
.state
|
||||
.state
|
||||
.values()
|
||||
.filter(|acc| is_new_deployment(acc))
|
||||
.filter_map(|acc| acc.info.as_ref().map(|info| info.code_hash))
|
||||
.collect();
|
||||
let code_bytes_written: usize = unique_new_code_hashes
|
||||
.iter()
|
||||
.filter_map(|hash| {
|
||||
output.state.contracts.get(hash).map(|bytecode| bytecode.original_bytes().len())
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Total time spent fetching state during execution
|
||||
let state_read_duration = provider_stats.total_account_fetch_latency() +
|
||||
provider_stats.total_storage_fetch_latency() +
|
||||
provider_stats.total_code_fetch_latency();
|
||||
|
||||
// EIP-7702 delegation tracking from bytecode changes
|
||||
// Count new EIP-7702 bytecodes as delegations set
|
||||
let eip7702_delegations_set =
|
||||
output.state.contracts.values().filter(|bytecode| bytecode.is_eip7702()).count();
|
||||
// Delegations cleared: accounts where bytecode changed FROM EIP-7702 TO empty
|
||||
// This detects when an EIP-7702 delegation is removed by setting code to empty
|
||||
// Note: Clearing a delegation does NOT destroy the account - it just empties the
|
||||
// bytecode
|
||||
let eip7702_delegations_cleared = output
|
||||
.state
|
||||
.state
|
||||
.values()
|
||||
.filter(|acc| {
|
||||
// Check if original bytecode was EIP-7702
|
||||
let original_was_eip7702 = acc
|
||||
.original_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.code.as_ref())
|
||||
.map(|bytecode| bytecode.is_eip7702())
|
||||
.unwrap_or(false);
|
||||
|
||||
// Check if current code is empty (delegation cleared)
|
||||
let code_now_empty =
|
||||
acc.info.as_ref().map(|info| info.code_hash == KECCAK_EMPTY).unwrap_or(false);
|
||||
|
||||
original_was_eip7702 && code_now_empty
|
||||
})
|
||||
.count();
|
||||
|
||||
// Get cache statistics for detailed block logging
|
||||
let (account_cache_hits, account_cache_misses) = cache_stats
|
||||
.as_ref()
|
||||
.map(|s| (s.account_hits(), s.account_misses()))
|
||||
.unwrap_or_default();
|
||||
let (storage_cache_hits, storage_cache_misses) = cache_stats
|
||||
.as_ref()
|
||||
.map(|s| (s.storage_hits(), s.storage_misses()))
|
||||
.unwrap_or_default();
|
||||
let (code_cache_hits, code_cache_misses) =
|
||||
cache_stats.as_ref().map(|s| (s.code_hits(), s.code_misses())).unwrap_or_default();
|
||||
|
||||
// Build execution timing stats for detailed block logging
|
||||
Box::new(ExecutionTimingStats {
|
||||
block_number: block.number(),
|
||||
block_hash: block.hash(),
|
||||
gas_used: output.result.gas_used,
|
||||
tx_count: block.transaction_count(),
|
||||
execution_duration,
|
||||
state_read_duration,
|
||||
state_hash_duration,
|
||||
accounts_read,
|
||||
storage_read,
|
||||
code_read,
|
||||
code_bytes_read,
|
||||
accounts_changed,
|
||||
accounts_deleted,
|
||||
storage_slots_changed,
|
||||
storage_slots_deleted,
|
||||
bytecodes_changed,
|
||||
code_bytes_written,
|
||||
eip7702_delegations_set,
|
||||
eip7702_delegations_cleared,
|
||||
account_cache_hits,
|
||||
account_cache_misses,
|
||||
storage_cache_hits,
|
||||
storage_cache_misses,
|
||||
code_cache_hits,
|
||||
code_cache_misses,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Strategy describing how to compute the state root.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
//! The [`PersistenceState`] tracks ongoing persistence operations and coordinates
|
||||
//! between the main execution thread and background persistence workers.
|
||||
|
||||
use crate::persistence::PersistenceResult;
|
||||
use alloy_eips::BlockNumHash;
|
||||
use alloy_primitives::B256;
|
||||
use crossbeam_channel::Receiver as CrossbeamReceiver;
|
||||
@@ -36,7 +37,7 @@ pub struct PersistenceState {
|
||||
/// Receiver end of channel where the result of the persistence task will be
|
||||
/// sent when done. A None value means there's no persistence task in progress.
|
||||
pub(crate) rx:
|
||||
Option<(CrossbeamReceiver<Option<BlockNumHash>>, Instant, CurrentPersistenceAction)>,
|
||||
Option<(CrossbeamReceiver<PersistenceResult>, Instant, CurrentPersistenceAction)>,
|
||||
}
|
||||
|
||||
impl PersistenceState {
|
||||
@@ -50,7 +51,7 @@ impl PersistenceState {
|
||||
pub(crate) fn start_remove(
|
||||
&mut self,
|
||||
new_tip_num: u64,
|
||||
rx: CrossbeamReceiver<Option<BlockNumHash>>,
|
||||
rx: CrossbeamReceiver<PersistenceResult>,
|
||||
) {
|
||||
self.rx =
|
||||
Some((rx, Instant::now(), CurrentPersistenceAction::RemovingBlocks { new_tip_num }));
|
||||
@@ -60,7 +61,7 @@ impl PersistenceState {
|
||||
pub(crate) fn start_save(
|
||||
&mut self,
|
||||
highest: BlockNumHash,
|
||||
rx: CrossbeamReceiver<Option<BlockNumHash>>,
|
||||
rx: CrossbeamReceiver<PersistenceResult>,
|
||||
) {
|
||||
self.rx = Some((rx, Instant::now(), CurrentPersistenceAction::SavingBlocks { highest }));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Contains a precompile cache backed by `schnellru::LruMap` (LRU by length).
|
||||
|
||||
use alloy_primitives::Bytes;
|
||||
use alloy_primitives::{
|
||||
map::{DefaultHashBuilder, FbBuildHasher},
|
||||
Bytes,
|
||||
};
|
||||
use moka::policy::EvictionPolicy;
|
||||
use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
|
||||
use reth_primitives_traits::dashmap::DashMap;
|
||||
@@ -13,7 +16,7 @@ const MAX_CACHE_SIZE: u32 = 10_000;
|
||||
|
||||
/// Stores caches for each precompile.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>>>)
|
||||
pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>, FbBuildHasher<20>>>)
|
||||
where
|
||||
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
|
||||
|
||||
@@ -37,9 +40,7 @@ where
|
||||
|
||||
/// Cache for precompiles, for each input stores the result.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrecompileCache<S>(
|
||||
moka::sync::Cache<Bytes, CacheEntry<S>, alloy_primitives::map::DefaultHashBuilder>,
|
||||
)
|
||||
pub struct PrecompileCache<S>(moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>)
|
||||
where
|
||||
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
|
||||
|
||||
@@ -168,11 +169,11 @@ where
|
||||
}
|
||||
|
||||
fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
|
||||
if let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) {
|
||||
if let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) &&
|
||||
input.gas >= entry.gas_used()
|
||||
{
|
||||
self.increment_by_one_precompile_cache_hits();
|
||||
if input.gas >= entry.gas_used() {
|
||||
return entry.to_precompile_result()
|
||||
}
|
||||
return entry.to_precompile_result()
|
||||
}
|
||||
|
||||
let calldata = input.data;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user