mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
239 Commits
fix-filter
...
ejh/hive-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c408bcd1dc | ||
|
|
9b50bb7c8d | ||
|
|
a66d49c190 | ||
|
|
ab2252c33d | ||
|
|
f82d143d0c | ||
|
|
8dbb015770 | ||
|
|
96be836679 | ||
|
|
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 | ||
|
|
f84d5e6d7f | ||
|
|
e63b6239d7 | ||
|
|
660a0dee90 | ||
|
|
f92c9b4370 | ||
|
|
f0e2522294 | ||
|
|
7103088adc | ||
|
|
663765af5c | ||
|
|
20cfb2d517 | ||
|
|
0bdf6e2f2e | ||
|
|
85abd41824 | ||
|
|
70fb03a530 | ||
|
|
96fce4dc4f | ||
|
|
728c7acd08 | ||
|
|
626c82db33 | ||
|
|
624fcbd345 | ||
|
|
aed47bc3f8 | ||
|
|
7680c1e4f6 | ||
|
|
93cb4068d2 | ||
|
|
2fba05dc67 | ||
|
|
ea143d4d31 | ||
|
|
fddb7dad10 | ||
|
|
af6d674cac | ||
|
|
de5688a76e | ||
|
|
d4cb91f0a5 | ||
|
|
d122c7b49c | ||
|
|
aed9014e1e | ||
|
|
d340114d52 | ||
|
|
7fc22f7b5b | ||
|
|
c8c5f8886d | ||
|
|
2f3c8d7d03 | ||
|
|
a90f8be67b | ||
|
|
7faca05344 | ||
|
|
2827b0aca0 | ||
|
|
d3bb2faf28 | ||
|
|
ef292ffa00 | ||
|
|
ea98d37bb3 | ||
|
|
f2b3201187 | ||
|
|
d1cbf6ca5a | ||
|
|
56bb47709c | ||
|
|
3703255d5d | ||
|
|
b431caf806 | ||
|
|
21dadb71c3 | ||
|
|
98c45a4245 | ||
|
|
ac2cc7b4e2 | ||
|
|
3931affcf2 | ||
|
|
93b7ae9286 | ||
|
|
7e7717bdaa | ||
|
|
815037e27d | ||
|
|
80bf5532ac | ||
|
|
028e99191a | ||
|
|
ab5f2db594 | ||
|
|
dc35fc8251 | ||
|
|
285c325d71 | ||
|
|
ca47a7e9f9 | ||
|
|
6d718d0c21 | ||
|
|
6a633a42f0 | ||
|
|
949111c953 | ||
|
|
742eb56949 | ||
|
|
4af4836ec1 | ||
|
|
3bc71e7ec0 | ||
|
|
03fbb6cafe | ||
|
|
b09b097a0b | ||
|
|
0fffdcdd23 | ||
|
|
bc33eb764a | ||
|
|
190157636e | ||
|
|
8e3bc6567c | ||
|
|
45b961c7b3 | ||
|
|
94818d7676 | ||
|
|
4c2a9a9b4a | ||
|
|
76c37f0f80 | ||
|
|
0275ff35fd | ||
|
|
3f011c8328 | ||
|
|
beac28dbb2 | ||
|
|
bce100c6c8 | ||
|
|
40e99a4a4f | ||
|
|
1ff88e43cd | ||
|
|
d23c244cd1 | ||
|
|
3de9259026 | ||
|
|
d24f0b1e05 | ||
|
|
bb1b9ec611 | ||
|
|
08fd55d8c9 | ||
|
|
70cab0d163 | ||
|
|
e530b1f6a1 | ||
|
|
ff5d375526 | ||
|
|
fab95c1f3a | ||
|
|
d1a92afb57 | ||
|
|
0517c12c90 | ||
|
|
237eb1675c | ||
|
|
b6bcd7e6bd | ||
|
|
48122300d7 | ||
|
|
13f214f160 | ||
|
|
f17592670d | ||
|
|
c225132b81 | ||
|
|
dcc5d9ec30 | ||
|
|
6cd56b645b | ||
|
|
794dbff26e | ||
|
|
fcfbed0bbc | ||
|
|
70bcd475fe | ||
|
|
cd6e895a97 | ||
|
|
6552a3a9ab | ||
|
|
6a91089542 | ||
|
|
a9a1e504b4 | ||
|
|
e280f25885 | ||
|
|
e4191ccea8 | ||
|
|
37c4f908fa | ||
|
|
a157be3f3b | ||
|
|
e0eb306b2b | ||
|
|
7f4f3f1eb9 | ||
|
|
8970f82aaf | ||
|
|
8529da976f | ||
|
|
8fa539225b | ||
|
|
93d546a36d | ||
|
|
5c83eb0b06 | ||
|
|
cd32e3cc05 | ||
|
|
26470cadfc | ||
|
|
506ab806e4 | ||
|
|
c2e846093e | ||
|
|
5df22b12d8 | ||
|
|
ff9700bb3b | ||
|
|
85d35fa6c0 | ||
|
|
47544d9a7e | ||
|
|
ef33961aff | ||
|
|
080ff004e3 | ||
|
|
0e01a694a7 | ||
|
|
ee19320ee8 | ||
|
|
9251997c1f | ||
|
|
302993b45a | ||
|
|
8d97ab63c6 | ||
|
|
251f83ab0b | ||
|
|
e6e0dde903 | ||
|
|
18599f1732 | ||
|
|
9fd35e2917 | ||
|
|
c1a5e20b50 | ||
|
|
b1b51261af | ||
|
|
2ae5ef475e | ||
|
|
0ff16ea053 | ||
|
|
8861e2724f | ||
|
|
734ec4ffe6 | ||
|
|
cbcdf8dac0 | ||
|
|
826e387c87 | ||
|
|
1c40188993 | ||
|
|
49a2df0d7a | ||
|
|
a1d1b6def6 | ||
|
|
56bbb3ce2c | ||
|
|
5b1010322c | ||
|
|
a195b777eb | ||
|
|
5045e6ef8b | ||
|
|
b49cadb346 | ||
|
|
aeb2c6e731 | ||
|
|
477fed7a11 | ||
|
|
59993b974a | ||
|
|
9ecef47aff | ||
|
|
0ba685386d | ||
|
|
3541bd7f65 | ||
|
|
a3aec0c662 | ||
|
|
0dfdaca3f0 | ||
|
|
c535a7fb5b | ||
|
|
bf6270b8a3 | ||
|
|
1e78685a6c | ||
|
|
1f1e320643 | ||
|
|
74ea20400e | ||
|
|
ffff5fbce2 | ||
|
|
f514892b41 | ||
|
|
d0ad4b0e18 | ||
|
|
cb6ed16485 | ||
|
|
a88eef91f4 | ||
|
|
f7e7afd51f | ||
|
|
102764285b | ||
|
|
4679c86003 | ||
|
|
7671838c61 | ||
|
|
8f4461c060 | ||
|
|
0119f3c612 | ||
|
|
094aaef5a1 | ||
|
|
32d03ff4d7 | ||
|
|
3368ce6485 | ||
|
|
9bc2388871 | ||
|
|
1728fa97c0 | ||
|
|
868248ec54 | ||
|
|
16ab4b8518 | ||
|
|
ce74466b93 | ||
|
|
992fc30ff5 | ||
|
|
3ece6b6047 | ||
|
|
dec9f93ad1 | ||
|
|
03484f76ec | ||
|
|
b870f04509 | ||
|
|
5277e59cc4 | ||
|
|
6271c2702f | ||
|
|
ce15ab9f55 | ||
|
|
0b1ec2dc89 | ||
|
|
5862c72880 | ||
|
|
8a7655ca5d | ||
|
|
cd20adc1d4 | ||
|
|
f0fe45d6bf | ||
|
|
00422207f4 | ||
|
|
28a94829e9 | ||
|
|
e081249f65 | ||
|
|
99fedf01f8 | ||
|
|
179e1bfc34 | ||
|
|
3adb5b9e58 | ||
|
|
57d7c98f66 | ||
|
|
5d9a43f2d4 | ||
|
|
defd0e8e5c |
5
.changelog/cool-suns-rest.md
Normal file
5
.changelog/cool-suns-rest.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-transaction-pool: minor
|
||||
---
|
||||
|
||||
Added support for optional custom stateless and stateful validation hooks in `EthTransactionValidator` via `set_additional_stateless_validation` and `set_additional_stateful_validation` methods. Also implemented a manual `Debug` impl to handle the non-`Debug` function pointer fields.
|
||||
5
.changelog/dull-koalas-play.md
Normal file
5
.changelog/dull-koalas-play.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie-sparse: patch
|
||||
---
|
||||
|
||||
Added recording of `SetRoot` operation in `ParallelSparseTrie::set_root` when the `trie-debug` feature is enabled.
|
||||
6
.changelog/evil-fish-smile.md
Normal file
6
.changelog/evil-fish-smile.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-rpc-convert: minor
|
||||
reth-storage-rpc-provider: minor
|
||||
---
|
||||
|
||||
Replaced the separate `TryFromBlockResponse`, `TryFromReceiptResponse`, and `TryFromTransactionResponse` traits with a unified `RpcResponseConverter` trait and default `EthRpcConverter` implementation. Removed the `op-alloy-network` dependency and refactored `RpcBlockchainProvider` to store a dynamic converter instance instead of relying on per-type trait bounds.
|
||||
5
.changelog/keen-geese-bake.md
Normal file
5
.changelog/keen-geese-bake.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-engine-tree: patch
|
||||
---
|
||||
|
||||
Added sub-phase timing histograms to the sparse trie event loop, tracking channel wait, proof coalescing, multiproof reveal, and trie update durations separately.
|
||||
5
.changelog/odd-donkeys-chirp.md
Normal file
5
.changelog/odd-donkeys-chirp.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-engine-tree: patch
|
||||
---
|
||||
|
||||
Fixed `compare_trie_updates` to return `bool` indicating whether differences were found, and updated the caller to properly use the return value instead of treating all successful comparisons as having no differences.
|
||||
5
.changelog/proud-crabs-bark.md
Normal file
5
.changelog/proud-crabs-bark.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-db-api: patch
|
||||
---
|
||||
|
||||
Changed `StoredNibblesSubKey` encoding to use a stack-allocated `[u8; 65]` array instead of a heap-allocated `Vec<u8>`, avoiding unnecessary heap allocation.
|
||||
5
.changelog/rich-lakes-bark.md
Normal file
5
.changelog/rich-lakes-bark.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-provider: patch
|
||||
---
|
||||
|
||||
Fixed sender pruning during block reorg to skip when sender_recovery is fully pruned, preventing a fatal crash when no sender data exists in static files.
|
||||
7
.changelog/safe-waves-read.md
Normal file
7
.changelog/safe-waves-read.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
reth-network-types: minor
|
||||
reth-network: minor
|
||||
reth-node-core: patch
|
||||
---
|
||||
|
||||
Added `PersistedPeerInfo` struct to persist richer peer metadata (kind, fork ID, reputation) to disk. Updated `PeersConfig::with_basic_nodes_from_file` to support both the new `PersistedPeerInfo` format and the legacy `Vec<NodeRecord>` format with automatic conversion, and updated `write_peers_to_file` to exclude backed-off and banned peers.
|
||||
5
.changelog/tall-stars-shout.md
Normal file
5
.changelog/tall-stars-shout.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-network: minor
|
||||
---
|
||||
|
||||
Added `fork_id` as a tiebreaker in peer selection when reputations are equal, preferring peers with a discovered `fork_id` as it indicates fork compatibility. Added a test to verify the tiebreaker behavior.
|
||||
5
.changelog/tidy-stars-cry.md
Normal file
5
.changelog/tidy-stars-cry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-trie-sparse: patch
|
||||
---
|
||||
|
||||
Fixed a bug where trie nodes could appear in both `updated_nodes` and `removed_nodes` simultaneously by removing entries from `removed_nodes` when a node is inserted as updated.
|
||||
5
.changelog/warm-pandas-cook.md
Normal file
5
.changelog/warm-pandas-cook.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-transaction-pool: patch
|
||||
---
|
||||
|
||||
Fixed a bug where transactions from the same sender were added to the pending subpool out of nonce order. Ensured `process_updates` runs before `add_new_transaction` so that lower-nonce promotions are enqueued before the newly inserted higher-nonce transaction, preserving correct ordering for live `BestTransactions` iterators.
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,7 +1,7 @@
|
||||
* @gakonst
|
||||
crates/chain-state/ @fgimenez @mattsse
|
||||
crates/chainspec/ @Rjected @joshieDo @mattsse
|
||||
crates/cli/ @mattsse
|
||||
crates/cli/ @mattsse @Rjected
|
||||
crates/config/ @shekhirin @mattsse @Rjected
|
||||
crates/consensus/ @mattsse @Rjected
|
||||
crates/e2e-test-utils/ @mattsse @Rjected @klkvr @fgimenez
|
||||
|
||||
91
.github/scripts/bench-reth-build.sh
vendored
91
.github/scripts/bench-reth-build.sh
vendored
@@ -2,55 +2,88 @@
|
||||
#
|
||||
# Builds (or fetches from cache) reth binaries for benchmarking.
|
||||
#
|
||||
# Usage: bench-reth-build.sh <main|branch> <commit> [branch-sha]
|
||||
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit> [branch-sha]
|
||||
#
|
||||
# main — build/fetch the baseline binary at <commit> (merge-base)
|
||||
# branch — build/fetch the candidate binary + reth-bench at <commit>
|
||||
# optional branch-sha is the PR head commit for cache key
|
||||
# baseline — build/fetch the baseline binary at <commit> (merge-base)
|
||||
# source-dir must be checked out at <commit>
|
||||
# feature — build/fetch the candidate binary + reth-bench at <commit>
|
||||
# source-dir must be checked out at <commit>
|
||||
# optional branch-sha is the PR head commit for cache key
|
||||
#
|
||||
# Outputs:
|
||||
# main: target/profiling-baseline/reth
|
||||
# branch: target/profiling/reth, reth-bench installed to cargo bin
|
||||
# baseline: <source-dir>/target/profiling/reth
|
||||
# feature: <source-dir>/target/profiling/reth, reth-bench installed to cargo bin
|
||||
#
|
||||
# Required: mc (MinIO client) configured at /home/ubuntu/.mc
|
||||
# Required: mc (MinIO client) with a configured alias
|
||||
set -euo pipefail
|
||||
|
||||
MC="mc --config-dir /home/ubuntu/.mc"
|
||||
MC="mc"
|
||||
MODE="$1"
|
||||
COMMIT="$2"
|
||||
SOURCE_DIR="$2"
|
||||
COMMIT="$3"
|
||||
|
||||
# Verify a cached reth binary was built from the expected commit.
|
||||
# `reth --version` outputs "Commit SHA: <full-sha>" on its own line.
|
||||
verify_binary() {
|
||||
local binary="$1" expected_commit="$2"
|
||||
local version binary_sha
|
||||
version=$("$binary" --version 2>/dev/null) || return 1
|
||||
binary_sha=$(echo "$version" | sed -n 's/^Commit SHA: *//p')
|
||||
if [ -z "$binary_sha" ]; then
|
||||
echo "Warning: could not extract commit SHA from version output"
|
||||
return 1
|
||||
fi
|
||||
if [ "$binary_sha" = "$expected_commit" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "Cache mismatch: binary built from ${binary_sha} but expected ${expected_commit}"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
main)
|
||||
baseline|main)
|
||||
BUCKET="minio/reth-binaries/${COMMIT}"
|
||||
mkdir -p target/profiling-baseline
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null; then
|
||||
echo "Cache hit for main (${COMMIT}), downloading binary..."
|
||||
$MC cp "${BUCKET}/reth" target/profiling-baseline/reth
|
||||
chmod +x target/profiling-baseline/reth
|
||||
else
|
||||
echo "Cache miss for main (${COMMIT}), building from source..."
|
||||
CURRENT_REF=$(git rev-parse HEAD)
|
||||
git checkout "${COMMIT}"
|
||||
echo "Cache hit for baseline (${COMMIT}), downloading binary..."
|
||||
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/reth"
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached baseline binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building baseline (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
cargo build --profile profiling --bin reth
|
||||
cp target/profiling/reth target/profiling-baseline/reth
|
||||
$MC cp target/profiling-baseline/reth "${BUCKET}/reth"
|
||||
git checkout "${CURRENT_REF}"
|
||||
$MC cp target/profiling/reth "${BUCKET}/reth"
|
||||
fi
|
||||
;;
|
||||
|
||||
branch)
|
||||
BRANCH_SHA="${3:-$COMMIT}"
|
||||
feature|branch)
|
||||
BRANCH_SHA="${4:-$COMMIT}"
|
||||
BUCKET="minio/reth-binaries/${BRANCH_SHA}"
|
||||
|
||||
CACHE_VALID=false
|
||||
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
|
||||
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
|
||||
mkdir -p target/profiling
|
||||
$MC cp "${BUCKET}/reth" target/profiling/reth
|
||||
mkdir -p "${SOURCE_DIR}/target/profiling"
|
||||
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
|
||||
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench
|
||||
chmod +x target/profiling/reth /home/ubuntu/.cargo/bin/reth-bench
|
||||
else
|
||||
echo "Cache miss for ${BRANCH_SHA}, building from source..."
|
||||
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
|
||||
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
|
||||
CACHE_VALID=true
|
||||
else
|
||||
echo "Cached feature binary is stale, rebuilding..."
|
||||
fi
|
||||
fi
|
||||
if [ "$CACHE_VALID" = false ]; then
|
||||
echo "Building feature (${COMMIT}) from source..."
|
||||
cd "${SOURCE_DIR}"
|
||||
rustup show active-toolchain || rustup default stable
|
||||
make profiling
|
||||
make install-reth-bench
|
||||
@@ -60,7 +93,7 @@ case "$MODE" in
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 <main|branch> <commit> [branch-sha]"
|
||||
echo "Usage: $0 <baseline|feature> <source-dir> <commit> [branch-sha]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
74
.github/scripts/bench-reth-charts.py
vendored
74
.github/scripts/bench-reth-charts.py
vendored
@@ -53,7 +53,7 @@ def parse_combined_csv(path: str) -> list[dict]:
|
||||
|
||||
def plot_latency_and_throughput(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
baseline_name: str = "main", branch_name: str = "branch",
|
||||
baseline_name: str = "baseline", feature_name: str = "feature",
|
||||
):
|
||||
num_plots = 3 if baseline else 2
|
||||
fig, axes = plt.subplots(num_plots, 1, figsize=(12, 4 * num_plots), sharex=True)
|
||||
@@ -73,22 +73,24 @@ def plot_latency_and_throughput(
|
||||
for r in baseline:
|
||||
lat_s = r["new_payload_latency_us"] / 1_000_000
|
||||
base_ggas.append(r["gas_used"] / lat_s / GIGAGAS if lat_s > 0 else 0)
|
||||
ax1.plot(base_x, base_lat, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
ax2.plot(base_x, base_ggas, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
l, = ax1.plot(base_x, base_lat, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
ax1.axhline(np.median(base_lat), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
|
||||
l, = ax2.plot(base_x, base_ggas, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
ax2.axhline(np.median(base_ggas), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
|
||||
|
||||
ax1.plot(feat_x, feat_lat, linewidth=0.8, label=branch_name)
|
||||
l, = ax1.plot(feat_x, feat_lat, linewidth=0.8, label=feature_name)
|
||||
ax1.axhline(np.median(feat_lat), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
|
||||
ax1.set_ylabel("Latency (ms)")
|
||||
ax1.set_title("newPayload Latency per Block")
|
||||
ax1.grid(True, alpha=0.3)
|
||||
if baseline:
|
||||
ax1.legend()
|
||||
ax1.legend()
|
||||
|
||||
ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=branch_name)
|
||||
l, = ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=feature_name)
|
||||
ax2.axhline(np.median(feat_ggas), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
|
||||
ax2.set_ylabel("Ggas/s")
|
||||
ax2.set_title("Execution Throughput per Block")
|
||||
ax2.grid(True, alpha=0.3)
|
||||
if baseline:
|
||||
ax2.legend()
|
||||
ax2.legend()
|
||||
|
||||
if baseline:
|
||||
ax3 = axes[2]
|
||||
@@ -105,7 +107,7 @@ def plot_latency_and_throughput(
|
||||
ax3.bar(blocks, diffs, width=1.0, color=colors, alpha=0.7, edgecolor="none")
|
||||
ax3.axhline(0, color="black", linewidth=0.5)
|
||||
ax3.set_ylabel("Δ Latency (%)")
|
||||
ax3.set_title("Per-Block newPayload Latency Change (branch vs main)")
|
||||
ax3.set_title("Per-Block newPayload Latency Change (feature vs baseline)")
|
||||
ax3.grid(True, alpha=0.3, axis="y")
|
||||
|
||||
axes[-1].set_xlabel("Block Number")
|
||||
@@ -116,7 +118,7 @@ def plot_latency_and_throughput(
|
||||
|
||||
def plot_wait_breakdown(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
baseline_name: str = "main", branch_name: str = "branch",
|
||||
baseline_name: str = "baseline", feature_name: str = "feature",
|
||||
):
|
||||
series = [
|
||||
("Persistence Wait", "persistence_wait_us"),
|
||||
@@ -135,7 +137,7 @@ def plot_wait_breakdown(
|
||||
fx = [r["block_number"] for r in feature if r[key] is not None]
|
||||
fy = [r[key] / 1_000 for r in feature if r[key] is not None]
|
||||
if fx:
|
||||
ax.plot(fx, fy, linewidth=0.8, label=branch_name)
|
||||
ax.plot(fx, fy, linewidth=0.8, label=feature_name)
|
||||
|
||||
ax.set_ylabel("ms")
|
||||
ax.set_title(label)
|
||||
@@ -163,7 +165,7 @@ def _add_regression(ax, x, y, color, label):
|
||||
|
||||
def plot_gas_vs_latency(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
baseline_name: str = "main", branch_name: str = "branch",
|
||||
baseline_name: str = "baseline", feature_name: str = "feature",
|
||||
):
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
|
||||
@@ -176,7 +178,7 @@ def plot_gas_vs_latency(
|
||||
fgas = [r["gas_used"] / 1_000_000 for r in feature]
|
||||
flat = [r["new_payload_latency_us"] / 1_000 for r in feature]
|
||||
ax.scatter(fgas, flat, s=8, alpha=0.6)
|
||||
_add_regression(ax, fgas, flat, "tab:orange", branch_name)
|
||||
_add_regression(ax, fgas, flat, "tab:orange", feature_name)
|
||||
|
||||
ax.set_xlabel("Gas Used (Mgas)")
|
||||
ax.set_ylabel("newPayload Latency (ms)")
|
||||
@@ -188,30 +190,56 @@ def plot_gas_vs_latency(
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def merge_csvs(paths: list[str]) -> list[dict]:
|
||||
"""Parse and merge multiple CSVs, averaging values for duplicate blocks."""
|
||||
by_block: dict[int, list[dict]] = {}
|
||||
for path in paths:
|
||||
for row in parse_combined_csv(path):
|
||||
by_block.setdefault(row["block_number"], []).append(row)
|
||||
|
||||
merged = []
|
||||
for bn in sorted(by_block):
|
||||
rows = by_block[bn]
|
||||
if len(rows) == 1:
|
||||
merged.append(rows[0])
|
||||
else:
|
||||
avg = {"block_number": bn}
|
||||
for key in ("gas_used", "new_payload_latency_us"):
|
||||
avg[key] = int(sum(r[key] for r in rows) / len(rows))
|
||||
for key in ("persistence_wait_us", "execution_cache_wait_us", "sparse_trie_wait_us"):
|
||||
vals = [r[key] for r in rows if r[key] is not None]
|
||||
avg[key] = int(sum(vals) / len(vals)) if vals else None
|
||||
merged.append(avg)
|
||||
return merged
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate benchmark charts")
|
||||
parser.add_argument("combined_csv", help="Path to combined_latency.csv (feature)")
|
||||
parser.add_argument(
|
||||
"--feature", nargs="+", required=True,
|
||||
help="Path(s) to feature combined_latency.csv",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", required=True, help="Output directory for PNG charts"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--baseline", help="Path to baseline (main) combined_latency.csv"
|
||||
"--baseline", nargs="+", help="Path(s) to baseline combined_latency.csv"
|
||||
)
|
||||
parser.add_argument("--baseline-name", default="main", help="Label for baseline")
|
||||
parser.add_argument("--branch-name", default="branch", help="Label for branch")
|
||||
parser.add_argument("--baseline-name", default="baseline", help="Label for baseline")
|
||||
parser.add_argument("--feature-name", "--branch-name", default="feature", help="Label for feature")
|
||||
args = parser.parse_args()
|
||||
|
||||
feature = parse_combined_csv(args.combined_csv)
|
||||
feature = merge_csvs(args.feature)
|
||||
if not feature:
|
||||
print("No results found in combined CSV", file=sys.stderr)
|
||||
print("No results found in feature CSV(s)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
baseline = None
|
||||
if args.baseline:
|
||||
baseline = parse_combined_csv(args.baseline)
|
||||
baseline = merge_csvs(args.baseline)
|
||||
if not baseline:
|
||||
print(
|
||||
"Warning: no results in baseline CSV, skipping comparison",
|
||||
"Warning: no results in baseline CSV(s), skipping comparison",
|
||||
file=sys.stderr,
|
||||
)
|
||||
baseline = None
|
||||
@@ -220,7 +248,7 @@ def main():
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bname = args.baseline_name
|
||||
fname = args.branch_name
|
||||
fname = args.feature_name
|
||||
plot_latency_and_throughput(feature, baseline, out_dir / "latency_throughput.png", bname, fname)
|
||||
plot_wait_breakdown(feature, baseline, out_dir / "wait_breakdown.png", bname, fname)
|
||||
plot_gas_vs_latency(feature, baseline, out_dir / "gas_vs_latency.png", bname, fname)
|
||||
|
||||
92
.github/scripts/bench-reth-run.sh
vendored
92
.github/scripts/bench-reth-run.sh
vendored
@@ -12,19 +12,45 @@ LABEL="$1"
|
||||
BINARY="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
LOG="/tmp/reth-bench-node-${LABEL}.log"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
LOG="${OUTPUT_DIR}/node.log"
|
||||
|
||||
cleanup() {
|
||||
kill "$TAIL_PID" 2>/dev/null || true
|
||||
if [ -n "${RETH_PID:-}" ] && sudo kill -0 "$RETH_PID" 2>/dev/null; then
|
||||
sudo kill "$RETH_PID"
|
||||
for i in $(seq 1 30); do
|
||||
sudo kill -0 "$RETH_PID" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
# Send SIGINT to the inner reth process by exact name (not -f which
|
||||
# would also match samply's cmdline containing "reth"). Samply will
|
||||
# capture reth's exit and save the profile.
|
||||
sudo pkill -INT -x reth 2>/dev/null || true
|
||||
# Wait for samply to finish writing the profile and exit
|
||||
for i in $(seq 1 120); do
|
||||
sudo pgrep -x samply > /dev/null 2>&1 || break
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo "Waiting for samply to finish writing profile... (${i}s)"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if sudo pgrep -x samply > /dev/null 2>&1; then
|
||||
echo "Samply still running after 120s, sending SIGTERM..."
|
||||
sudo pkill -x samply 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
sudo kill "$RETH_PID"
|
||||
for i in $(seq 1 30); do
|
||||
sudo kill -0 "$RETH_PID" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
sudo kill -9 "$RETH_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
# Fix ownership of reth-created files (reth runs as root)
|
||||
sudo chown -R "$(id -un):$(id -gn)" "$OUTPUT_DIR" 2>/dev/null || true
|
||||
if mountpoint -q "$SCHELK_MOUNT"; then
|
||||
sudo umount -l "$SCHELK_MOUNT" || true
|
||||
sudo schelk recover -y || true
|
||||
fi
|
||||
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || true
|
||||
}
|
||||
TAIL_PID=
|
||||
trap cleanup EXIT
|
||||
@@ -41,18 +67,38 @@ grep Cached /proc/meminfo
|
||||
# CPU layout: core 0 = OS/IRQs/reth-bench/aux, cores 1+ = reth node
|
||||
RETH_BENCH="$(which reth-bench)"
|
||||
ONLINE=$(nproc --all)
|
||||
RETH_CPUS="1-$(( ONLINE - 1 ))"
|
||||
sudo taskset -c "$RETH_CPUS" nice -n -20 "$BINARY" node \
|
||||
--datadir "$DATADIR" \
|
||||
--engine.accept-execution-requests-hash \
|
||||
--http \
|
||||
--http.port 8545 \
|
||||
--ws \
|
||||
--ws.api all \
|
||||
--authrpc.port 8551 \
|
||||
--disable-discovery \
|
||||
--no-persist-peers \
|
||||
> "$LOG" 2>&1 &
|
||||
MAX_RETH=$(( ONLINE - 1 ))
|
||||
if [ "${BENCH_CORES:-0}" -gt 0 ] && [ "$BENCH_CORES" -lt "$MAX_RETH" ]; then
|
||||
MAX_RETH=$BENCH_CORES
|
||||
fi
|
||||
RETH_CPUS="1-${MAX_RETH}"
|
||||
|
||||
RETH_ARGS=(
|
||||
node
|
||||
--datadir "$DATADIR"
|
||||
--log.file.directory "$OUTPUT_DIR/reth-logs"
|
||||
--engine.accept-execution-requests-hash
|
||||
--http
|
||||
--http.port 8545
|
||||
--ws
|
||||
--ws.api all
|
||||
--authrpc.port 8551
|
||||
--disable-discovery
|
||||
--no-persist-peers
|
||||
)
|
||||
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
RETH_ARGS+=(--log.samply)
|
||||
SAMPLY="$(which samply)"
|
||||
sudo 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[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
fi
|
||||
|
||||
RETH_PID=$!
|
||||
stdbuf -oL tail -f "$LOG" | sed -u "s/^/[reth] /" &
|
||||
@@ -74,8 +120,12 @@ for i in $(seq 1 60); do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Run reth-bench with high priority but as the current user so output
|
||||
# files are not root-owned (avoids EACCES on next checkout).
|
||||
BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
|
||||
|
||||
# Warmup
|
||||
sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
|
||||
$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" \
|
||||
@@ -83,7 +133,7 @@ sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
|
||||
--reth-new-payload 2>&1 | sed -u "s/^/[bench] /"
|
||||
|
||||
# Benchmark
|
||||
sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
|
||||
$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" \
|
||||
|
||||
127
.github/scripts/bench-reth-snapshot.sh
vendored
Executable file
127
.github/scripts/bench-reth-snapshot.sh
vendored
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Downloads the latest nightly snapshot into the schelk volume with
|
||||
# progress reporting to the GitHub PR comment.
|
||||
#
|
||||
# Skips the download if the local ETag marker matches the remote one.
|
||||
#
|
||||
# Usage: bench-reth-snapshot.sh [--check]
|
||||
# --check Only check if a download is needed; exits 0 if up-to-date, 1 if not.
|
||||
#
|
||||
# Required env:
|
||||
# SCHELK_MOUNT – schelk mount point (e.g. /reth-bench)
|
||||
# GITHUB_TOKEN – token for GitHub API calls (only for download)
|
||||
# BENCH_COMMENT_ID – PR comment ID to update (optional)
|
||||
# BENCH_REPO – owner/repo (e.g. paradigmxyz/reth)
|
||||
# BENCH_JOB_URL – link to the Actions job
|
||||
# BENCH_ACTOR – user who triggered the benchmark
|
||||
# BENCH_CONFIG – config summary line
|
||||
set -euo pipefail
|
||||
|
||||
BUCKET="minio/reth-snapshots/reth-1-minimal-nightly-previous.tar.zst"
|
||||
DATADIR="$SCHELK_MOUNT/datadir"
|
||||
ETAG_FILE="$HOME/.reth-bench-snapshot-etag"
|
||||
|
||||
# Get remote metadata via JSON for reliable parsing
|
||||
MC_STAT=$(mc stat --json "$BUCKET" 2>/dev/null || true)
|
||||
REMOTE_ETAG=$(echo "$MC_STAT" | jq -r '.etag // empty')
|
||||
if [ -z "$REMOTE_ETAG" ]; then
|
||||
echo "::warning::Failed to get ETag from mc stat, will re-download"
|
||||
REMOTE_ETAG="unknown-$(date +%s)"
|
||||
fi
|
||||
|
||||
LOCAL_ETAG=""
|
||||
[ -f "$ETAG_FILE" ] && LOCAL_ETAG=$(cat "$ETAG_FILE")
|
||||
|
||||
if [ "$REMOTE_ETAG" = "$LOCAL_ETAG" ]; then
|
||||
echo "Snapshot is up-to-date (ETag: ${REMOTE_ETAG})"
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Snapshot needs update (local: ${LOCAL_ETAG:-<none>}, remote: ${REMOTE_ETAG})"
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get compressed size for progress tracking
|
||||
TOTAL_BYTES=$(echo "$MC_STAT" | jq -r '.size // empty')
|
||||
if [ -z "$TOTAL_BYTES" ] || [ "$TOTAL_BYTES" = "0" ]; then
|
||||
echo "::error::Failed to get snapshot size from mc stat"
|
||||
exit 1
|
||||
fi
|
||||
echo "Snapshot size: $TOTAL_BYTES bytes ($(numfmt --to=iec "$TOTAL_BYTES"))"
|
||||
|
||||
# Prepare mount
|
||||
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || true
|
||||
sudo schelk mount -y
|
||||
sudo rm -rf "$DATADIR"
|
||||
sudo mkdir -p "$DATADIR"
|
||||
|
||||
update_comment() {
|
||||
local pct="$1"
|
||||
[ -z "${BENCH_COMMENT_ID:-}" ] && return 0
|
||||
local status="Building binaries & downloading snapshot… ${pct}%"
|
||||
local body
|
||||
body="$(printf 'cc @%s\n\n🚀 Benchmark started! [View job](%s)\n\n⏳ **Status:** %s\n\n%s' \
|
||||
"$BENCH_ACTOR" "$BENCH_JOB_URL" "$status" "$BENCH_CONFIG")"
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${BENCH_REPO}/issues/comments/${BENCH_COMMENT_ID}" \
|
||||
-d "$(jq -nc --arg body "$body" '{body: $body}')" \
|
||||
> /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Track compressed bytes flowing through the pipe
|
||||
DL_BYTES_FILE=$(mktemp)
|
||||
echo 0 > "$DL_BYTES_FILE"
|
||||
|
||||
# Start progress reporter in background
|
||||
(
|
||||
while true; do
|
||||
sleep 10
|
||||
CURRENT=$(cat "$DL_BYTES_FILE" 2>/dev/null || echo 0)
|
||||
if [ "$TOTAL_BYTES" -gt 0 ]; then
|
||||
PCT=$(( CURRENT * 100 / TOTAL_BYTES ))
|
||||
[ "$PCT" -gt 100 ] && PCT=100
|
||||
echo "Snapshot download: $(numfmt --to=iec "$CURRENT") / $(numfmt --to=iec "$TOTAL_BYTES") (${PCT}%)"
|
||||
update_comment "$PCT"
|
||||
fi
|
||||
done
|
||||
) &
|
||||
PROGRESS_PID=$!
|
||||
trap 'kill $PROGRESS_PID 2>/dev/null || true; rm -f "$DL_BYTES_FILE"' EXIT
|
||||
|
||||
# Download and extract; python byte counter tracks compressed bytes received
|
||||
mc cat "$BUCKET" | python3 -c "
|
||||
import sys
|
||||
count = 0
|
||||
while True:
|
||||
data = sys.stdin.buffer.read(1048576)
|
||||
if not data:
|
||||
break
|
||||
count += len(data)
|
||||
sys.stdout.buffer.write(data)
|
||||
with open('$DL_BYTES_FILE', 'w') as f:
|
||||
f.write(str(count))
|
||||
" | pzstd -d -p 6 | sudo tar -xf - -C "$DATADIR"
|
||||
|
||||
# Stop progress reporter
|
||||
kill $PROGRESS_PID 2>/dev/null || true
|
||||
wait $PROGRESS_PID 2>/dev/null || true
|
||||
|
||||
update_comment "100"
|
||||
echo "Snapshot download complete"
|
||||
|
||||
# Promote the new snapshot to become the schelk baseline (virgin volume).
|
||||
# This copies changed blocks from scratch → virgin so that future
|
||||
# `schelk recover` calls restore to this new state.
|
||||
sync
|
||||
sudo schelk promote -y
|
||||
|
||||
# Save ETag marker
|
||||
echo "$REMOTE_ETAG" > "$ETAG_FILE"
|
||||
echo "Snapshot promoted to schelk baseline (ETag: ${REMOTE_ETAG})"
|
||||
294
.github/scripts/bench-reth-summary.py
vendored
294
.github/scripts/bench-reth-summary.py
vendored
@@ -8,12 +8,12 @@ Usage:
|
||||
--baseline-csv <baseline_combined.csv> \
|
||||
[--repo <owner/repo>] \
|
||||
[--baseline-ref <sha>] \
|
||||
[--branch-name <name>] \
|
||||
[--branch-sha <sha>]
|
||||
[--feature-name <name>] \
|
||||
[--feature-sha <sha>]
|
||||
|
||||
Generates a paired statistical comparison between baseline (main) and branch.
|
||||
Generates a paired statistical comparison between baseline and feature.
|
||||
Matches blocks by number and computes per-block diffs to cancel out gas
|
||||
variance. Fails if baseline or branch CSV is missing or empty.
|
||||
variance. Fails if baseline or feature CSV is missing or empty.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -28,6 +28,14 @@ T_CRITICAL = 1.96 # two-tailed 95% confidence
|
||||
BOOTSTRAP_ITERATIONS = 10_000
|
||||
|
||||
|
||||
def _opt_int(row: dict, key: str) -> int | None:
|
||||
"""Return int value for a CSV field, or None if missing/empty."""
|
||||
v = row.get(key)
|
||||
if v is None or v == "":
|
||||
return None
|
||||
return int(v)
|
||||
|
||||
|
||||
def parse_combined_csv(path: str) -> list[dict]:
|
||||
"""Parse combined_latency.csv into a list of per-block dicts."""
|
||||
rows = []
|
||||
@@ -43,11 +51,9 @@ def parse_combined_csv(path: str) -> list[dict]:
|
||||
"new_payload_latency_us": int(row["new_payload_latency"]),
|
||||
"fcu_latency_us": int(row["fcu_latency"]),
|
||||
"total_latency_us": int(row["total_latency"]),
|
||||
"persistence_wait_us": int(row["persistence_wait"])
|
||||
if row.get("persistence_wait")
|
||||
else None,
|
||||
"execution_cache_wait_us": int(row.get("execution_cache_wait", 0)),
|
||||
"sparse_trie_wait_us": int(row.get("sparse_trie_wait", 0)),
|
||||
"persistence_wait_us": _opt_int(row, "persistence_wait"),
|
||||
"execution_cache_wait_us": _opt_int(row, "execution_cache_wait"),
|
||||
"sparse_trie_wait_us": _opt_int(row, "sparse_trie_wait"),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
@@ -112,26 +118,45 @@ def compute_stats(combined: list[dict]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def compute_wait_stats(combined: list[dict], field: str) -> dict:
|
||||
"""Compute mean/p50/p95 for a wait time field (in ms)."""
|
||||
values_ms = []
|
||||
for r in combined:
|
||||
v = r.get(field)
|
||||
if v is not None:
|
||||
values_ms.append(v / 1_000)
|
||||
if not values_ms:
|
||||
return {}
|
||||
n = len(values_ms)
|
||||
mean_val = sum(values_ms) / n
|
||||
sorted_vals = sorted(values_ms)
|
||||
return {
|
||||
"mean_ms": mean_val,
|
||||
"p50_ms": percentile(sorted_vals, 50),
|
||||
"p95_ms": percentile(sorted_vals, 95),
|
||||
}
|
||||
|
||||
|
||||
def _paired_data(
|
||||
baseline: list[dict], branch: list[dict]
|
||||
baseline: list[dict], feature: list[dict]
|
||||
) -> tuple[list[tuple[float, float]], list[float], list[float]]:
|
||||
"""Match blocks and return paired latencies and per-block diffs.
|
||||
|
||||
Returns:
|
||||
pairs: list of (baseline_ms, branch_ms) tuples
|
||||
lat_diffs_ms: list of branch − baseline latency diffs in ms
|
||||
mgas_diffs: list of branch − baseline Mgas/s diffs
|
||||
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
|
||||
"""
|
||||
baseline_by_block = {r["block_number"]: r for r in baseline}
|
||||
branch_by_block = {r["block_number"]: r for r in branch}
|
||||
common_blocks = sorted(set(baseline_by_block) & set(branch_by_block))
|
||||
feature_by_block = {r["block_number"]: r for r in feature}
|
||||
common_blocks = sorted(set(baseline_by_block) & set(feature_by_block))
|
||||
|
||||
pairs = []
|
||||
lat_diffs_ms = []
|
||||
mgas_diffs = []
|
||||
for bn in common_blocks:
|
||||
b = baseline_by_block[bn]
|
||||
f = branch_by_block[bn]
|
||||
f = feature_by_block[bn]
|
||||
b_ms = b["new_payload_latency_us"] / 1_000
|
||||
f_ms = f["new_payload_latency_us"] / 1_000
|
||||
pairs.append((b_ms, f_ms))
|
||||
@@ -148,21 +173,23 @@ def _paired_data(
|
||||
|
||||
def compute_paired_stats(
|
||||
baseline_runs: list[list[dict]],
|
||||
branch_runs: list[list[dict]],
|
||||
feature_runs: list[list[dict]],
|
||||
) -> dict:
|
||||
"""Compute paired statistics between baseline and branch runs.
|
||||
"""Compute paired statistics between baseline and feature runs.
|
||||
|
||||
Each pair (baseline_runs[i], branch_runs[i]) produces per-block diffs.
|
||||
Each pair (baseline_runs[i], feature_runs[i]) produces per-block diffs.
|
||||
All diffs are pooled for the final CI.
|
||||
"""
|
||||
all_pairs = []
|
||||
all_lat_diffs = []
|
||||
all_mgas_diffs = []
|
||||
for baseline, branch in zip(baseline_runs, branch_runs):
|
||||
pairs, lat_diffs, mgas_diffs = _paired_data(baseline, branch)
|
||||
blocks_per_pair = []
|
||||
for baseline, feature in zip(baseline_runs, feature_runs):
|
||||
pairs, lat_diffs, mgas_diffs = _paired_data(baseline, feature)
|
||||
all_pairs.extend(pairs)
|
||||
all_lat_diffs.extend(lat_diffs)
|
||||
all_mgas_diffs.extend(mgas_diffs)
|
||||
blocks_per_pair.append(len(pairs))
|
||||
|
||||
if not all_lat_diffs:
|
||||
return {}
|
||||
@@ -175,10 +202,10 @@ def compute_paired_stats(
|
||||
|
||||
# Bootstrap CI on difference-of-percentiles (resample paired blocks)
|
||||
base_lats = sorted([p[0] for p in all_pairs])
|
||||
branch_lats = sorted([p[1] for p in all_pairs])
|
||||
p50_diff = percentile(branch_lats, 50) - percentile(base_lats, 50)
|
||||
p90_diff = percentile(branch_lats, 90) - percentile(base_lats, 90)
|
||||
p99_diff = percentile(branch_lats, 99) - percentile(base_lats, 99)
|
||||
feature_lats = sorted([p[1] for p in all_pairs])
|
||||
p50_diff = percentile(feature_lats, 50) - percentile(base_lats, 50)
|
||||
p90_diff = percentile(feature_lats, 90) - percentile(base_lats, 90)
|
||||
p99_diff = percentile(feature_lats, 99) - percentile(base_lats, 99)
|
||||
|
||||
rng = random.Random(42)
|
||||
p50_boot, p90_boot, p99_boot = [], [], []
|
||||
@@ -212,16 +239,10 @@ def compute_paired_stats(
|
||||
"p99_ci_ms": (p99_boot[hi] - p99_boot[lo]) / 2,
|
||||
"mean_mgas_diff": mean_mgas_diff,
|
||||
"mgas_ci": mgas_ci,
|
||||
"blocks": max(blocks_per_pair),
|
||||
}
|
||||
|
||||
|
||||
def compute_summary(combined: list[dict], gas: list[dict]) -> dict:
|
||||
"""Compute aggregate metrics from parsed CSV data."""
|
||||
blocks = len(combined)
|
||||
return {
|
||||
"blocks": blocks,
|
||||
}
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
if seconds >= 60:
|
||||
@@ -246,33 +267,68 @@ def fmt_mgas(v: float) -> str:
|
||||
return f"{v:.2f}"
|
||||
|
||||
|
||||
def significance(pct: float, ci_pct: float, lower_is_better: bool) -> str:
|
||||
"""Return significance label: 'good', 'bad', or 'neutral'."""
|
||||
significant = abs(pct) > ci_pct
|
||||
if not significant:
|
||||
return "neutral"
|
||||
elif (pct < 0) == lower_is_better:
|
||||
return "good"
|
||||
else:
|
||||
return "bad"
|
||||
|
||||
|
||||
def change_str(pct: float, ci_pct: float, lower_is_better: bool) -> str:
|
||||
"""Format change% with paired CI significance.
|
||||
|
||||
Significant if the CI doesn't cross zero (i.e. |pct| > ci_pct).
|
||||
"""
|
||||
significant = abs(pct) > ci_pct
|
||||
if not significant:
|
||||
emoji = "⚪"
|
||||
elif (pct < 0) == lower_is_better:
|
||||
emoji = "✅"
|
||||
else:
|
||||
emoji = "❌"
|
||||
|
||||
sig = significance(pct, ci_pct, lower_is_better)
|
||||
emoji = {"good": "✅", "bad": "❌", "neutral": "⚪"}[sig]
|
||||
return f"{pct:+.2f}% {emoji} (±{ci_pct:.2f}%)"
|
||||
|
||||
|
||||
def compute_changes(
|
||||
baseline_stats: dict, feature_stats: dict, paired_stats: dict
|
||||
) -> dict:
|
||||
"""Pre-compute change percentages and significance for each metric."""
|
||||
def pct(base: float, feat: float) -> float:
|
||||
return (feat - base) / base * 100.0 if base > 0 else 0.0
|
||||
|
||||
def ci_pct(ci_ms: float, base_ms: float) -> float:
|
||||
return ci_ms / base_ms * 100.0 if base_ms > 0 else 0.0
|
||||
|
||||
metrics = [
|
||||
("mean", "mean_ms", "ci_ms", "mean_ms", True),
|
||||
("p50", "p50_ms", "p50_ci_ms", "p50_ms", True),
|
||||
("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),
|
||||
]
|
||||
changes = {}
|
||||
for name, stat_key, ci_key, base_key, lower_is_better in metrics:
|
||||
p = pct(baseline_stats[stat_key], feature_stats[stat_key])
|
||||
c = ci_pct(paired_stats[ci_key], baseline_stats[base_key])
|
||||
changes[name] = {
|
||||
"pct": round(p, 4),
|
||||
"ci_pct": round(c, 4),
|
||||
"sig": significance(p, c, lower_is_better),
|
||||
}
|
||||
return changes
|
||||
|
||||
|
||||
def generate_comparison_table(
|
||||
run1: dict,
|
||||
run2: dict,
|
||||
paired: dict,
|
||||
repo: str,
|
||||
baseline_ref: str,
|
||||
branch_name: str,
|
||||
branch_sha: str,
|
||||
baseline_name: str,
|
||||
feature_name: str,
|
||||
feature_sha: str,
|
||||
) -> str:
|
||||
"""Generate a markdown comparison table between baseline (main) and branch."""
|
||||
n = paired["n"]
|
||||
"""Generate a markdown comparison table between baseline and feature."""
|
||||
n = paired["blocks"]
|
||||
|
||||
def pct(base: float, feat: float) -> float:
|
||||
return (feat - base) / base * 100.0 if base > 0 else 0.0
|
||||
@@ -294,11 +350,11 @@ def generate_comparison_table(
|
||||
mgas_ci_pct = paired["mgas_ci"] / run1["mean_mgas_s"] * 100.0 if run1["mean_mgas_s"] > 0 else 0.0
|
||||
|
||||
base_url = f"https://github.com/{repo}/commit"
|
||||
baseline_label = f"[`main`]({base_url}/{baseline_ref})"
|
||||
branch_label = f"[`{branch_name}`]({base_url}/{branch_sha})"
|
||||
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
|
||||
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
|
||||
|
||||
lines = [
|
||||
f"| Metric | {baseline_label} | {branch_label} | Change |",
|
||||
f"| Metric | {baseline_label} | {feature_label} | Change |",
|
||||
"|--------|------|--------|--------|",
|
||||
f"| Mean | {fmt_ms(run1['mean_ms'])} | {fmt_ms(run2['mean_ms'])} | {change_str(mean_pct, lat_ci_pct, lower_is_better=True)} |",
|
||||
f"| StdDev | {fmt_ms(run1['stddev_ms'])} | {fmt_ms(run2['stddev_ms'])} | |",
|
||||
@@ -312,17 +368,51 @@ def generate_comparison_table(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_wait_time_table(
|
||||
title: str,
|
||||
baseline_stats: dict,
|
||||
feature_stats: dict,
|
||||
baseline_label: str,
|
||||
feature_label: str,
|
||||
) -> str:
|
||||
"""Generate a markdown table for a wait time metric."""
|
||||
if not baseline_stats or not feature_stats:
|
||||
return ""
|
||||
lines = [
|
||||
f"### {title}",
|
||||
"",
|
||||
f"| Metric | {baseline_label} | {feature_label} |",
|
||||
"|--------|------|--------|",
|
||||
f"| Mean | {fmt_ms(baseline_stats['mean_ms'])} | {fmt_ms(feature_stats['mean_ms'])} |",
|
||||
f"| P50 | {fmt_ms(baseline_stats['p50_ms'])} | {fmt_ms(feature_stats['p50_ms'])} |",
|
||||
f"| P95 | {fmt_ms(baseline_stats['p95_ms'])} | {fmt_ms(feature_stats['p95_ms'])} |",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_markdown(
|
||||
summary: dict, comparison_table: str,
|
||||
behind_main: int = 0, repo: str = "", baseline_ref: str = "",
|
||||
wait_time_tables: list[str] | None = None,
|
||||
behind_baseline: int = 0, repo: str = "", baseline_ref: str = "", baseline_name: str = "",
|
||||
) -> str:
|
||||
"""Generate a markdown comment body."""
|
||||
lines = ["## Benchmark Results", "", comparison_table]
|
||||
if behind_main > 0:
|
||||
s = "s" if behind_main > 1 else ""
|
||||
diff_link = f"https://github.com/{repo}/compare/{baseline_ref[:12]}...main"
|
||||
lines = ["## Benchmark Results", ""]
|
||||
if behind_baseline > 0:
|
||||
s = "s" if behind_baseline > 1 else ""
|
||||
diff_link = f"https://github.com/{repo}/compare/{baseline_ref[:12]}...{baseline_name}"
|
||||
lines.append(f"> ⚠️ Feature is [**{behind_baseline} commit{s} behind `{baseline_name}`**]({diff_link}). Consider rebasing for accurate results.")
|
||||
lines.append("")
|
||||
lines.append(f"> ⚠️ Branch is [**{behind_main} commit{s} behind `main`**]({diff_link}). Consider rebasing for accurate results.")
|
||||
lines.append(comparison_table)
|
||||
if wait_time_tables:
|
||||
lines.append("")
|
||||
lines.append("<details>")
|
||||
lines.append("<summary>Wait Time Breakdown</summary>")
|
||||
lines.append("")
|
||||
for table in wait_time_tables:
|
||||
if table:
|
||||
lines.append(table)
|
||||
lines.append("")
|
||||
lines.append("</details>")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -333,8 +423,8 @@ def main():
|
||||
help="Baseline combined_latency.csv files (A1, A2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--branch-csv", nargs="+", required=True,
|
||||
help="Branch combined_latency.csv files (B1, B2)",
|
||||
"--feature-csv", "--branch-csv", nargs="+", required=True,
|
||||
help="Feature combined_latency.csv files (B1, B2)",
|
||||
)
|
||||
parser.add_argument("--gas-csv", required=True, help="Path to total_gas.csv")
|
||||
parser.add_argument(
|
||||
@@ -345,65 +435,113 @@ def main():
|
||||
"--repo", default="paradigmxyz/reth", help="GitHub repo (owner/name)"
|
||||
)
|
||||
parser.add_argument("--baseline-ref", default=None, help="Baseline commit SHA")
|
||||
parser.add_argument("--branch-name", default=None, help="Branch name")
|
||||
parser.add_argument("--branch-sha", default=None, help="Branch commit SHA")
|
||||
parser.add_argument("--behind-main", type=int, default=0, help="Commits behind main")
|
||||
parser.add_argument("--baseline-name", default=None, help="Baseline display name")
|
||||
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")
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(args.baseline_csv) != len(args.branch_csv):
|
||||
print("Must provide equal number of baseline and branch CSVs", file=sys.stderr)
|
||||
if len(args.baseline_csv) != len(args.feature_csv):
|
||||
print("Must provide equal number of baseline and feature CSVs", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
baseline_runs = []
|
||||
branch_runs = []
|
||||
feature_runs = []
|
||||
for path in args.baseline_csv:
|
||||
data = parse_combined_csv(path)
|
||||
if not data:
|
||||
print(f"No results in {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
baseline_runs.append(data)
|
||||
for path in args.branch_csv:
|
||||
for path in args.feature_csv:
|
||||
data = parse_combined_csv(path)
|
||||
if not data:
|
||||
print(f"No results in {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
branch_runs.append(data)
|
||||
feature_runs.append(data)
|
||||
|
||||
gas = parse_gas_csv(args.gas_csv)
|
||||
|
||||
all_baseline = [r for run in baseline_runs for r in run]
|
||||
all_branch = [r for run in branch_runs for r in run]
|
||||
|
||||
summary = compute_summary(all_branch, gas)
|
||||
with open(args.output_summary, "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
print(f"Summary written to {args.output_summary}")
|
||||
all_feature = [r for run in feature_runs for r in run]
|
||||
|
||||
baseline_stats = compute_stats(all_baseline)
|
||||
branch_stats = compute_stats(all_branch)
|
||||
paired_stats = compute_paired_stats(baseline_runs, branch_runs)
|
||||
feature_stats = compute_stats(all_feature)
|
||||
paired_stats = compute_paired_stats(baseline_runs, feature_runs)
|
||||
|
||||
if not paired_stats:
|
||||
print("No common blocks between baseline and branch runs", file=sys.stderr)
|
||||
print("No common blocks between baseline and feature runs", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
baseline_ref = args.baseline_ref or "main"
|
||||
baseline_name = args.baseline_name or "baseline"
|
||||
feature_name = args.feature_name or "feature"
|
||||
feature_sha = args.feature_ref or "unknown"
|
||||
|
||||
comparison_table = generate_comparison_table(
|
||||
baseline_stats,
|
||||
branch_stats,
|
||||
feature_stats,
|
||||
paired_stats,
|
||||
repo=args.repo,
|
||||
baseline_ref=args.baseline_ref or "main",
|
||||
branch_name=args.branch_name or "branch",
|
||||
branch_sha=args.branch_sha or "unknown",
|
||||
baseline_ref=baseline_ref,
|
||||
baseline_name=baseline_name,
|
||||
feature_name=feature_name,
|
||||
feature_sha=feature_sha,
|
||||
)
|
||||
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)")
|
||||
|
||||
base_url = f"https://github.com/{args.repo}/commit"
|
||||
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
|
||||
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
|
||||
|
||||
wait_fields = [
|
||||
("persistence_wait_us", "Persistence Wait"),
|
||||
("sparse_trie_wait_us", "Trie Cache Update Wait"),
|
||||
("execution_cache_wait_us", "Execution Cache Update Wait"),
|
||||
]
|
||||
wait_time_tables = []
|
||||
wait_time_data = {}
|
||||
for field, title in wait_fields:
|
||||
b_stats = compute_wait_stats(all_baseline, field)
|
||||
f_stats = compute_wait_stats(all_feature, field)
|
||||
if b_stats and f_stats:
|
||||
wait_time_data[field] = {
|
||||
"title": title,
|
||||
"baseline": b_stats,
|
||||
"feature": f_stats,
|
||||
}
|
||||
table = generate_wait_time_table(title, b_stats, f_stats, baseline_label, feature_label)
|
||||
if table:
|
||||
wait_time_tables.append(table)
|
||||
|
||||
summary = {
|
||||
"blocks": paired_stats["blocks"],
|
||||
"baseline": {
|
||||
"name": baseline_name,
|
||||
"ref": baseline_ref,
|
||||
"stats": baseline_stats,
|
||||
},
|
||||
"feature": {
|
||||
"name": feature_name,
|
||||
"ref": feature_sha,
|
||||
"stats": feature_stats,
|
||||
},
|
||||
"paired": paired_stats,
|
||||
"changes": compute_changes(baseline_stats, feature_stats, paired_stats),
|
||||
"wait_times": wait_time_data,
|
||||
}
|
||||
with open(args.output_summary, "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
print(f"Summary written to {args.output_summary}")
|
||||
|
||||
markdown = generate_markdown(
|
||||
summary, comparison_table,
|
||||
behind_main=args.behind_main,
|
||||
wait_time_tables=wait_time_tables,
|
||||
behind_baseline=args.behind_baseline,
|
||||
repo=args.repo,
|
||||
baseline_ref=args.baseline_ref or "",
|
||||
baseline_ref=baseline_ref,
|
||||
baseline_name=baseline_name,
|
||||
)
|
||||
|
||||
with open(args.output_markdown, "w") as f:
|
||||
|
||||
342
.github/scripts/bench-slack-notify.js
vendored
Normal file
342
.github/scripts/bench-slack-notify.js
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
// Sends Slack notifications for reth-bench results.
|
||||
//
|
||||
// Reads from environment:
|
||||
// SLACK_BENCH_BOT_TOKEN – Slack Bot User OAuth Token (xoxb-...)
|
||||
// SLACK_BENCH_CHANNEL – Public channel ID for significant improvements
|
||||
// BENCH_WORK_DIR – Directory containing summary.json
|
||||
// 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_SAMPLY – 'true' if samply profiling was enabled
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const notify = require('./.github/scripts/bench-slack-notify.js');
|
||||
// await notify.success({ core, context });
|
||||
// await notify.failure({ core, context, failedStep: '...' });
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SLACK_API = 'https://slack.com/api/chat.postMessage';
|
||||
|
||||
function loadSlackUsers(repoRoot) {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(repoRoot, '.github', 'scripts', 'bench-slack-users.json'), 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
// Filter out non-user-ID entries (like _comment)
|
||||
const users = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!k.startsWith('_') && typeof v === 'string' && v.startsWith('U')) {
|
||||
users[k] = v;
|
||||
}
|
||||
}
|
||||
return users;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function postToSlack(token, channel, blocks, text, core, threadTs) {
|
||||
const payload = { channel, blocks, text, unfurl_links: false };
|
||||
if (threadTs) payload.thread_ts = threadTs;
|
||||
const resp = await fetch(SLACK_API, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
core.warning(`Slack API error (channel ${channel}): ${JSON.stringify(data)}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function cell(text) {
|
||||
const s = String(text);
|
||||
return { type: 'raw_text', text: s || ' ' };
|
||||
}
|
||||
|
||||
function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls }) {
|
||||
const b = summary.baseline.stats;
|
||||
const f = summary.feature.stats;
|
||||
const c = summary.changes;
|
||||
|
||||
const sigEmoji = { good: '\u2705', bad: '\u274c', neutral: '\u26aa' };
|
||||
|
||||
function fmtMs(v) { return v.toFixed(2) + 'ms'; }
|
||||
function fmtMgas(v) { return v.toFixed(2); }
|
||||
function fmtChange(ch) {
|
||||
if (!ch.pct && !ch.ci_pct) return ' ';
|
||||
const pctStr = `${ch.pct >= 0 ? '+' : ''}${ch.pct.toFixed(2)}%`;
|
||||
const ciStr = ch.ci_pct ? ` (\u00b1${ch.ci_pct.toFixed(2)}%)` : '';
|
||||
return `${pctStr}${ciStr} ${sigEmoji[ch.sig]}`;
|
||||
}
|
||||
|
||||
// Overall result for header
|
||||
const vals = Object.values(c);
|
||||
const hasBad = vals.some(v => v.sig === 'bad');
|
||||
const hasGood = vals.some(v => v.sig === 'good');
|
||||
let headerEmoji, headerResult;
|
||||
if (hasBad && hasGood) {
|
||||
headerEmoji = ':warning:';
|
||||
headerResult = 'Mixed Results';
|
||||
} else if (hasBad) {
|
||||
headerEmoji = ':x:';
|
||||
headerResult = 'Regression';
|
||||
} else if (hasGood) {
|
||||
headerEmoji = ':white_check_mark:';
|
||||
headerResult = 'Improvement';
|
||||
} else {
|
||||
headerEmoji = ':white_circle:';
|
||||
headerResult = 'No Difference';
|
||||
}
|
||||
|
||||
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
|
||||
const commitUrl = `https://github.com/${repo}/commit`;
|
||||
const baselineLink = `<${commitUrl}/${summary.baseline.ref}|${summary.baseline.name}>`;
|
||||
const featureLink = `<${commitUrl}/${summary.feature.ref}|${summary.feature.name}>`;
|
||||
|
||||
// Meta line
|
||||
const metaParts = [];
|
||||
if (prNumber) metaParts.push(`*<${prUrl}|PR #${prNumber}>*`);
|
||||
metaParts.push(`triggered by ${actorSlackId ? `<@${actorSlackId}>` : `@${actor}`}`);
|
||||
|
||||
// Baseline/feature lines with samply profile links
|
||||
let baselineLine = `*Baseline:* ${baselineLink}`;
|
||||
const bl1 = samplyUrls['baseline-1'];
|
||||
const bl2 = samplyUrls['baseline-2'];
|
||||
if (bl1) baselineLine += ` | <${bl1}|Samply 1>`;
|
||||
if (bl2) baselineLine += ` | <${bl2}|Samply 2>`;
|
||||
|
||||
let featureLine = `*Feature:* ${featureLink}`;
|
||||
const fl1 = samplyUrls['feature-1'];
|
||||
const fl2 = samplyUrls['feature-2'];
|
||||
if (fl1) featureLine += ` | <${fl1}|Samply 1>`;
|
||||
if (fl2) featureLine += ` | <${fl2}|Samply 2>`;
|
||||
|
||||
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
|
||||
const cores = process.env.BENCH_CORES || '0';
|
||||
const countsParts = [];
|
||||
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');
|
||||
|
||||
// Action buttons
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
const buttons = [
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
|
||||
url: jobUrl,
|
||||
action_id: 'ci_button',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'Diff :github:', emoji: true },
|
||||
url: diffUrl,
|
||||
action_id: 'diff_button',
|
||||
},
|
||||
];
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: `${headerEmoji} ${headerResult}`, emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: sectionText },
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
column_settings: [
|
||||
{ align: 'left' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
],
|
||||
rows: [
|
||||
[cell('Metric'), cell('Baseline'), cell('Feature'), cell('Change')],
|
||||
[cell('Mean'), cell(fmtMs(b.mean_ms)), cell(fmtMs(f.mean_ms)), cell(fmtChange(c.mean))],
|
||||
[cell('StdDev'), cell(fmtMs(b.stddev_ms)), cell(fmtMs(f.stddev_ms)), cell(' ')],
|
||||
[cell('P50'), cell(fmtMs(b.p50_ms)), cell(fmtMs(f.p50_ms)), cell(fmtChange(c.p50))],
|
||||
[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))],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: buttons,
|
||||
},
|
||||
];
|
||||
|
||||
// Wait times as a separate table block (sent as threaded reply due to Slack one-table limit)
|
||||
const threadBlocks = [];
|
||||
const waitTimes = summary.wait_times || {};
|
||||
const waitKeys = Object.keys(waitTimes);
|
||||
if (waitKeys.length > 0) {
|
||||
const waitRows = [
|
||||
[cell('Wait Time'), cell('Baseline'), cell('Feature')],
|
||||
];
|
||||
for (const key of waitKeys) {
|
||||
const wt = waitTimes[key];
|
||||
waitRows.push([cell(wt.title), cell(fmtMs(wt.baseline.mean_ms)), cell(fmtMs(wt.feature.mean_ms))]);
|
||||
}
|
||||
threadBlocks.push({
|
||||
type: 'table',
|
||||
column_settings: [
|
||||
{ align: 'left' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
],
|
||||
rows: waitRows,
|
||||
});
|
||||
}
|
||||
|
||||
return { blocks, threadBlocks };
|
||||
}
|
||||
|
||||
function buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep }) {
|
||||
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
|
||||
const actorMention = actorSlackId ? `<@${actorSlackId}>` : `@${actor}`;
|
||||
const parts = [
|
||||
prNumber ? `*<${prUrl}|PR #${prNumber}>*` : '',
|
||||
`by ${actorMention}`,
|
||||
`failed while *${failedStep}*`,
|
||||
].filter(Boolean);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
|
||||
url: jobUrl,
|
||||
action_id: 'ci_button',
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: ':rotating_light: Bench Failed', emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: parts.join(' | ') },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: buttons,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function success({ core, context }) {
|
||||
const token = process.env.SLACK_BENCH_BOT_TOKEN;
|
||||
if (!token) {
|
||||
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
let summary;
|
||||
try {
|
||||
summary = JSON.parse(fs.readFileSync(process.env.BENCH_WORK_DIR + '/summary.json', 'utf8'));
|
||||
} catch (e) {
|
||||
core.warning('Could not read summary.json for Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const prNumber = process.env.BENCH_PR;
|
||||
const actor = process.env.BENCH_ACTOR;
|
||||
const jobUrl = process.env.BENCH_JOB_URL ||
|
||||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
|
||||
// Load samply profile URLs (files exist when samply profiling was enabled)
|
||||
const samplyUrls = {};
|
||||
for (const run of ['baseline-1', 'baseline-2', 'feature-1', 'feature-2']) {
|
||||
try {
|
||||
const url = fs.readFileSync(
|
||||
path.join(process.env.BENCH_WORK_DIR, run, 'samply-profile-url.txt'), 'utf8'
|
||||
).trim();
|
||||
if (url) samplyUrls[run] = url;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
|
||||
const actorSlackId = slackUsers[actor];
|
||||
|
||||
const { blocks, threadBlocks } = buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls });
|
||||
const text = `Bench: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
|
||||
async function sendWithThread(ch) {
|
||||
const res = await postToSlack(token, ch, blocks, text, core);
|
||||
if (res.ok && res.ts && threadBlocks.length > 0) {
|
||||
for (const tb of threadBlocks) {
|
||||
await postToSlack(token, ch, [tb], 'Wait time breakdown', core, res.ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post to public channel if any metric shows significant improvement or regression
|
||||
const channel = process.env.SLACK_BENCH_CHANNEL;
|
||||
let postedToChannel = false;
|
||||
if (channel) {
|
||||
const changes = summary.changes || {};
|
||||
const hasImprovement = Object.values(changes).some(c => c.sig === 'good');
|
||||
if (hasImprovement) {
|
||||
await sendWithThread(channel);
|
||||
postedToChannel = true;
|
||||
} else {
|
||||
core.info('No significant improvement, skipping public channel notification');
|
||||
}
|
||||
}
|
||||
|
||||
// DM the actor only when results were not posted to the public channel
|
||||
if (!postedToChannel) {
|
||||
if (actorSlackId) {
|
||||
await sendWithThread(actorSlackId);
|
||||
} else {
|
||||
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
|
||||
}
|
||||
} else {
|
||||
core.info(`Results posted to channel, skipping DM to ${actor}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function failure({ core, context, failedStep }) {
|
||||
const token = process.env.SLACK_BENCH_BOT_TOKEN;
|
||||
if (!token) {
|
||||
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const prNumber = process.env.BENCH_PR;
|
||||
const actor = process.env.BENCH_ACTOR;
|
||||
const jobUrl = process.env.BENCH_JOB_URL ||
|
||||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
|
||||
const actorSlackId = slackUsers[actor];
|
||||
|
||||
const blocks = buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep });
|
||||
const text = `Bench failed while ${failedStep}`;
|
||||
|
||||
// Always DM the actor
|
||||
if (actorSlackId) {
|
||||
await postToSlack(token, actorSlackId, blocks, text, core);
|
||||
} else {
|
||||
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
|
||||
}
|
||||
|
||||
// Only DM for failures, don't post to public channel
|
||||
}
|
||||
|
||||
module.exports = { success, failure };
|
||||
13
.github/scripts/bench-slack-users.json
vendored
Normal file
13
.github/scripts/bench-slack-users.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_comment": "Maps GitHub usernames to Slack user IDs. Find yours: Slack profile > ··· > Copy member ID.",
|
||||
"shekhirin": "U09FAL2UMLJ",
|
||||
"mattsse": "U09FQNPMRT3",
|
||||
"klkvr": "U09FAK95FC2",
|
||||
"joshieDo": "U09LHN6GYAU",
|
||||
"mediocregopher": "U09FF75KMQU",
|
||||
"yongkangc": "U09FB0ECTD4",
|
||||
"gakonst": "U092SEPDM40",
|
||||
"Rjected": "U09F6SCKRGT",
|
||||
"DaniPopes": "U09FAT8EK2A",
|
||||
"emmajam": "U0A34UN92HW"
|
||||
}
|
||||
27
.github/scripts/bench-update-status.js
vendored
Normal file
27
.github/scripts/bench-update-status.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
// Updates the reth-bench PR comment with current status.
|
||||
//
|
||||
// Reads from environment:
|
||||
// BENCH_COMMENT_ID – GitHub comment ID to update
|
||||
// BENCH_JOB_URL – URL to the Actions job page
|
||||
// BENCH_CONFIG – Config line (blocks, warmup, refs)
|
||||
// BENCH_ACTOR – User who triggered the benchmark
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const s = require('./.github/scripts/bench-update-status.js');
|
||||
// await s({github, context, status: 'Building baseline binary...'});
|
||||
|
||||
function buildBody(status) {
|
||||
return `cc @${process.env.BENCH_ACTOR}\n\n🚀 Benchmark started! [View job](${process.env.BENCH_JOB_URL})\n\n⏳ **Status:** ${status}\n\n${process.env.BENCH_CONFIG}`;
|
||||
}
|
||||
|
||||
async function updateStatus({ github, context, status }) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: parseInt(process.env.BENCH_COMMENT_ID),
|
||||
body: buildBody(status),
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus.buildBody = buildBody;
|
||||
module.exports = updateStatus;
|
||||
2
.github/scripts/hive/Dockerfile
vendored
2
.github/scripts/hive/Dockerfile
vendored
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# We'll use cargo-chef to speed up the build
|
||||
#
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-trixie AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
|
||||
7
.github/scripts/hive/build_simulators.sh
vendored
7
.github/scripts/hive/build_simulators.sh
vendored
@@ -9,10 +9,13 @@ go build .
|
||||
|
||||
./hive -client reth # first builds and caches the client
|
||||
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
echo "Building images"
|
||||
# TODO: test code has been moved from https://github.com/ethereum/execution-spec-tests to https://github.com/ethereum/execution-specs we need to pin eels branch with `--sim.buildarg branch=<release-branch-name>` once we have the fusaka release tagged on the new repo
|
||||
./hive -client reth --sim "ethereum/eels" --sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/eels" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.1.0/fixtures_bal.tar.gz \
|
||||
--sim.buildarg branch=err-map-3 \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/rpc-compat" -sim.timelimit 1s || true &
|
||||
|
||||
60
.github/scripts/hive/expected_failures.yaml
vendored
60
.github/scripts/hive/expected_failures.yaml
vendored
@@ -18,30 +18,18 @@ rpc-compat:
|
||||
|
||||
# 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-api: [ ]
|
||||
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)
|
||||
|
||||
sync: [ ]
|
||||
sync: []
|
||||
|
||||
engine-auth: [ ]
|
||||
engine-auth: []
|
||||
|
||||
# EIP-7610 related tests (Revert creation in case of non-empty storage):
|
||||
#
|
||||
@@ -49,7 +37,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.
|
||||
@@ -97,6 +85,18 @@ eels/consume-engine:
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_False]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_True]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_True]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_False]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
|
||||
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Osaka-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Paris-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
@@ -147,6 +147,20 @@ eels/consume-engine:
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_engine_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
|
||||
|
||||
# Blob limit tests:
|
||||
#
|
||||
@@ -241,3 +255,17 @@ eels/consume-rlp:
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
|
||||
11
.github/scripts/hive/ignored_tests.yaml
vendored
11
.github/scripts/hive/ignored_tests.yaml
vendored
@@ -11,17 +11,16 @@
|
||||
#
|
||||
# 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)
|
||||
- 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)
|
||||
- Transaction Re-Org, Re-Org Out (Cancun) (reth)
|
||||
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=False, Invalid P9 (Cancun) (reth)
|
||||
# Hive test infra bug: geth sidecar switched to PathScheme for state storage, which has
|
||||
# strict trie integrity requirements incompatible with inserting intentionally invalid blocks.
|
||||
# Affects all clients, not just reth. Tracked: https://github.com/ethereum/hive/issues/1382
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=False, Invalid P8 (Cancun) (reth)
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=True, Invalid P8 (Cancun) (reth)
|
||||
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Cancun) (reth)
|
||||
engine-api:
|
||||
- Transaction Re-Org, Re-Org Out (Paris) (reth)
|
||||
|
||||
8
.github/scripts/hive/run_simulator.sh
vendored
8
.github/scripts/hive/run_simulator.sh
vendored
@@ -6,8 +6,14 @@ cd hivetests/
|
||||
sim="${1}"
|
||||
limit="${2}"
|
||||
|
||||
# Use lower parallelism for eels tests to avoid OOM-killing the runner
|
||||
parallelism=16
|
||||
if [[ "${sim}" == *"eels"* ]]; then
|
||||
parallelism=4
|
||||
fi
|
||||
|
||||
run_hive() {
|
||||
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism 16 --client reth 2>&1 | tee /tmp/log || true
|
||||
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism "${parallelism}" --client reth 2>&1 | tee /tmp/log || true
|
||||
}
|
||||
|
||||
check_log() {
|
||||
|
||||
969
.github/workflows/bench.yml
vendored
969
.github/workflows/bench.yml
vendored
File diff suppressed because it is too large
Load Diff
2
.github/workflows/docker-test.yml
vendored
2
.github/workflows/docker-test.yml
vendored
@@ -79,4 +79,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
path: ./artifacts
|
||||
78
.github/workflows/hive.yml
vendored
78
.github/workflows/hive.yml
vendored
@@ -5,7 +5,10 @@ name: hive
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: "0 */6 * * *"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -15,30 +18,34 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-reth-stable:
|
||||
uses: ./.github/workflows/docker-test.yml
|
||||
prepare-reth-stable:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
hive_target: hive-stable
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
cargo_features: "asm-keccak"
|
||||
artifact_name: "reth-stable"
|
||||
secrets: inherit
|
||||
|
||||
build-reth-edge:
|
||||
uses: ./.github/workflows/docker-test.yml
|
||||
prepare-reth-edge:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
hive_target: hive-edge
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
cargo_features: "asm-keccak edge"
|
||||
artifact_name: "reth-edge"
|
||||
secrets: inherit
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on:
|
||||
group: Reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ethereum/hive
|
||||
repository: Soubhik-10/hive
|
||||
ref: master
|
||||
path: hivetests
|
||||
|
||||
- name: Get hive commit hash
|
||||
@@ -124,27 +131,29 @@ jobs:
|
||||
# eth_ rpc methods
|
||||
- sim: ethereum/rpc-compat
|
||||
include:
|
||||
- eth_blockNumber
|
||||
# - eth_blockNumber
|
||||
- eth_call
|
||||
- eth_chainId
|
||||
- eth_createAccessList
|
||||
- eth_estimateGas
|
||||
- eth_feeHistory
|
||||
- eth_getBalance
|
||||
- eth_getBlockBy
|
||||
- eth_getBlockTransactionCountBy
|
||||
- eth_getCode
|
||||
- eth_getProof
|
||||
- eth_getStorage
|
||||
- eth_getTransactionBy
|
||||
- eth_getTransactionCount
|
||||
- eth_getTransactionReceipt
|
||||
- eth_sendRawTransaction
|
||||
- eth_syncing
|
||||
# debug_ rpc methods
|
||||
- debug_
|
||||
# - eth_chainId
|
||||
# - eth_createAccessList
|
||||
# - eth_estimateGas
|
||||
# - eth_feeHistory
|
||||
# - eth_getBalance
|
||||
# - eth_getBlockBy
|
||||
# - eth_getBlockTransactionCountBy
|
||||
# - eth_getCode
|
||||
# - eth_getProof
|
||||
# - eth_getStorage
|
||||
# - eth_getTransactionBy
|
||||
# - eth_getTransactionCount
|
||||
# - eth_getTransactionReceipt
|
||||
# - eth_sendRawTransaction
|
||||
# - eth_syncing
|
||||
# # debug_ rpc methods
|
||||
# - debug_
|
||||
|
||||
# consume-engine
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-engine
|
||||
@@ -165,6 +174,8 @@ jobs:
|
||||
limit: .*tests/paris.*
|
||||
|
||||
# consume-rlp
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/amsterdam.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/osaka.*
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
@@ -184,11 +195,12 @@ jobs:
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/paris.*
|
||||
needs:
|
||||
- build-reth-stable
|
||||
- build-reth-edge
|
||||
- prepare-reth-stable
|
||||
- prepare-reth-edge
|
||||
- prepare-hive
|
||||
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
@@ -219,7 +231,7 @@ jobs:
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ethereum/hive
|
||||
repository: Soubhik-10/hive
|
||||
ref: master
|
||||
path: hivetests
|
||||
|
||||
|
||||
61
.github/workflows/prepare-reth.yml
vendored
Normal file
61
.github/workflows/prepare-reth.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Prepare Reth Image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
required: true
|
||||
type: string
|
||||
description: "Docker image tag to use"
|
||||
binary_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "reth"
|
||||
description: "Binary name to build (reth or op-reth)"
|
||||
cargo_features:
|
||||
required: false
|
||||
type: string
|
||||
default: "asm-keccak"
|
||||
description: "Cargo features to enable"
|
||||
cargo_package:
|
||||
required: false
|
||||
type: string
|
||||
description: "Optional cargo package path"
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "artifacts"
|
||||
description: "Name for the uploaded artifact"
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: mkdir artifacts
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and export reth image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: .github/scripts/hive/Dockerfile
|
||||
tags: ${{ inputs.image_tag }}
|
||||
outputs: type=docker,dest=./artifacts/reth_image.tar
|
||||
build-args: |
|
||||
CARGO_BIN=${{ inputs.binary_name }}
|
||||
MANIFEST_PATH=${{ inputs.cargo_package }}
|
||||
FEATURES=${{ inputs.cargo_features }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Upload reth image
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -73,22 +73,22 @@ jobs:
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: "-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-24.04
|
||||
os: ubuntu-24.04-arm
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: ""
|
||||
- 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
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: true
|
||||
rustflags: ""
|
||||
build:
|
||||
- command: build
|
||||
binary: reth
|
||||
@@ -114,7 +114,7 @@ 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: make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-${{ matrix.configs.target }}
|
||||
- name: Move binary
|
||||
run: |
|
||||
mkdir artifacts
|
||||
|
||||
94
CLAUDE.md
94
CLAUDE.md
@@ -172,10 +172,97 @@ Before submitting changes, ensure:
|
||||
2. **Clippy**: No warnings
|
||||
3. **Tests Pass**: All unit and integration tests
|
||||
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
|
||||
5. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
5. **CLI Docs** (if CLI changed): Run `make update-book-cli` (see below)
|
||||
6. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
|
||||
### CLI Reference Docs (`book` CI Job)
|
||||
|
||||
The CLI reference pages under `docs/vocs/docs/pages/cli/` are **auto-generated** from the `reth` binary's `--help` output. **Do not edit these files manually** — any hand edits will be overwritten and CI will fail regardless.
|
||||
|
||||
When you add, remove, or modify CLI commands, subcommands, or flags, regenerate the CLI docs by running:
|
||||
|
||||
```bash
|
||||
make update-book-cli
|
||||
```
|
||||
|
||||
This builds `reth` in debug mode and runs `docs/cli/update.sh` to regenerate all CLI pages. Commit the resulting changes.
|
||||
|
||||
The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating the docs and running `git diff --exit-code`. If the committed docs don't match the generated output, CI fails. Manually editing these pages is never productive — always use `make update-book-cli`.
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
#### Titles
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
|
||||
|
||||
Examples:
|
||||
- `fix(rpc): correct gas estimation for ERC-20 transfers`
|
||||
- `perf: batch trie updates to reduce cursor overhead`
|
||||
- `feat(engine): add new_payload_interval metric`
|
||||
|
||||
#### Descriptions
|
||||
|
||||
Keep it short. Say what changed and why — nothing more.
|
||||
|
||||
**Do:**
|
||||
- Write 1–3 sentences summarizing the change
|
||||
- Explain _why_ if the diff doesn't make it obvious
|
||||
- Link related issues or EIPs
|
||||
- Include benchmark numbers for perf changes
|
||||
|
||||
**Don't:**
|
||||
- List every file changed — that's what the diff is for
|
||||
- Repeat the title in the body
|
||||
- Add "Files changed" or "Changes" sections
|
||||
- Write walls of text that go stale when the diff is updated
|
||||
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
Closes #<issue>
|
||||
|
||||
<what changed, 1-3 sentences>
|
||||
|
||||
<why, if not obvious from the diff>
|
||||
```
|
||||
|
||||
**Good example:**
|
||||
|
||||
```
|
||||
Closes #16800
|
||||
|
||||
Adds fallback for external IP resolution so node startup doesn't fail
|
||||
when STUN is unreachable. Falls back to the configured default.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```
|
||||
## Summary
|
||||
This PR introduces comprehensive improvements to the IP resolution system.
|
||||
|
||||
## Changes
|
||||
- Modified `crates/net/discv4/src/lib.rs` to add fallback
|
||||
- Modified `crates/net/discv4/src/config.rs` to add default IP
|
||||
- Added tests in `crates/net/discv4/src/tests/ip.rs`
|
||||
|
||||
## Files Changed
|
||||
- crates/net/discv4/src/lib.rs
|
||||
- crates/net/discv4/src/config.rs
|
||||
- crates/net/discv4/src/tests/ip.rs
|
||||
```
|
||||
|
||||
#### Labels and CI
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
@@ -455,5 +542,8 @@ cargo build --release
|
||||
cargo check --workspace --all-features
|
||||
|
||||
# Check documentation
|
||||
cargo docs --document-private-items
|
||||
cargo docs --document-private-items
|
||||
|
||||
# Regenerate CLI reference docs (after CLI changes)
|
||||
make update-book-cli
|
||||
```
|
||||
|
||||
493
Cargo.lock
generated
493
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
130
Cargo.toml
130
Cargo.toml
@@ -397,7 +397,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
|
||||
reth-payload-primitives = { path = "crates/payload/primitives" }
|
||||
reth-payload-validator = { path = "crates/payload/validator" }
|
||||
reth-payload-util = { path = "crates/payload/util" }
|
||||
reth-primitives = { path = "crates/primitives", default-features = false }
|
||||
reth-primitives = { path = "crates/primitives", default-features = false, features = ["__internal"] }
|
||||
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
|
||||
reth-provider = { path = "crates/storage/provider" }
|
||||
reth-prune = { path = "crates/prune/prune" }
|
||||
@@ -446,49 +446,48 @@ op-revm = { version = "15.0.0", default-features = false }
|
||||
revm-inspectors = "0.34.2"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
alloy-primitives = { version = "1.5.6", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-sol-types = { version = "1.5.6", default-features = false }
|
||||
alloy-dyn-abi = "1.5.0"
|
||||
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-sol-types = { version = "1.5.0", default-features = false }
|
||||
|
||||
alloy-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-eip7928 = { version = "0.3.0", default-features = false, features = ["rlp"] }
|
||||
alloy-evm = { version = "0.27.2", default-features = false }
|
||||
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
|
||||
alloy-trie = { version = "0.9.4", default-features = false }
|
||||
|
||||
alloy-hardforks = "0.4.5"
|
||||
|
||||
alloy-consensus = { version = "1.6.3", default-features = false }
|
||||
alloy-contract = { version = "1.6.3", default-features = false }
|
||||
alloy-eips = { version = "1.6.3", default-features = false }
|
||||
alloy-genesis = { version = "1.6.3", default-features = false }
|
||||
alloy-json-rpc = { version = "1.6.3", default-features = false }
|
||||
alloy-network = { version = "1.6.3", default-features = false }
|
||||
alloy-network-primitives = { version = "1.6.3", default-features = false }
|
||||
alloy-provider = { version = "1.6.3", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-client = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types = { version = "1.6.3", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.6.3", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.6.3", default-features = false }
|
||||
alloy-serde = { version = "1.6.3", default-features = false }
|
||||
alloy-signer = { version = "1.6.3", default-features = false }
|
||||
alloy-signer-local = { version = "1.6.3", default-features = false }
|
||||
alloy-transport = { version = "1.6.3" }
|
||||
alloy-transport-http = { version = "1.6.3", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.6.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.6.3", default-features = false }
|
||||
alloy-consensus = { version = "1.7.3", default-features = false }
|
||||
alloy-contract = { version = "1.7.3", default-features = false }
|
||||
alloy-eips = { version = "1.7.3", default-features = false }
|
||||
alloy-genesis = { version = "1.7.3", default-features = false }
|
||||
alloy-json-rpc = { version = "1.7.3", default-features = false }
|
||||
alloy-network = { version = "1.7.3", default-features = false }
|
||||
alloy-network-primitives = { version = "1.7.3", default-features = false }
|
||||
alloy-provider = { version = "1.7.3", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-client = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types = { version = "1.7.3", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.7.3", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.7.3", default-features = false }
|
||||
alloy-serde = { version = "1.7.3", default-features = false }
|
||||
alloy-signer = { version = "1.7.3", default-features = false }
|
||||
alloy-signer-local = { version = "1.7.3", default-features = false }
|
||||
alloy-transport = { version = "1.7.3" }
|
||||
alloy-transport-http = { version = "1.7.3", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.7.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.7.3", default-features = false }
|
||||
|
||||
# op
|
||||
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 }
|
||||
@@ -545,6 +544,7 @@ strum = { version = "0.27", default-features = false }
|
||||
strum_macros = "0.27"
|
||||
syn = "2.0"
|
||||
thiserror = { version = "2.0.0", default-features = false }
|
||||
thread-priority = "3.0.0"
|
||||
tar = "0.4.44"
|
||||
tracing = { version = "0.1.0", default-features = false, features = ["attributes"] }
|
||||
tracing-appender = "0.2"
|
||||
@@ -651,7 +651,7 @@ ethereum_ssz_derive = "0.10.1"
|
||||
jemalloc_pprof = { version = "0.8", default-features = false }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
tikv-jemallocator = "0.6"
|
||||
tracy-client = "0.18.0"
|
||||
tracy-client = { version = "0.18.0", features = ["demangle"] }
|
||||
snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
|
||||
|
||||
aes = "0.8.1"
|
||||
@@ -760,3 +760,65 @@ ipnet = "2.11"
|
||||
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
|
||||
# =============================================================================
|
||||
# BAL devnet2 patches (EIP-7778, EIP-7928 block access lists)
|
||||
# =============================================================================
|
||||
|
||||
# revm staging patches
|
||||
revm = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
op-revm = { git = "https://github.com/bluealloy/revm", branch = "main" }
|
||||
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", branch = "staging" }
|
||||
|
||||
# alloy-evm bal-devnet2 patches
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet2" }
|
||||
|
||||
# alloy bal-devnet2 patches
|
||||
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-consensus-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-contract = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-eips = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-genesis = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-network = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-provider = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-serde = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-signer = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
alloy-tx-macros = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
|
||||
|
||||
# op-alloy bal-devnet2 patches
|
||||
op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
|
||||
|
||||
9
Makefile
9
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 ?=
|
||||
|
||||
@@ -26,7 +29,7 @@ EF_TESTS_URL := https://github.com/ethereum/tests/archive/refs/tags/$(EF_TESTS_T
|
||||
EF_TESTS_DIR := ./testing/ef-tests/ethereum-tests
|
||||
|
||||
# The release tag of https://github.com/ethereum/execution-spec-tests to use for EEST tests
|
||||
EEST_TESTS_TAG := v4.5.0
|
||||
EEST_TESTS_TAG := bal@v5.0.0
|
||||
EEST_TESTS_URL := https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_TESTS_TAG)/fixtures_stable.tar.gz
|
||||
EEST_TESTS_DIR := ./testing/ef-tests/execution-spec-tests
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
@@ -96,7 +99,7 @@ build-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.
|
||||
|
||||
@@ -45,9 +45,6 @@ serde_json.workspace = true
|
||||
# Time handling
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
|
||||
# Path manipulation
|
||||
shellexpand.workspace = true
|
||||
|
||||
# CSV handling
|
||||
csv.workspace = true
|
||||
|
||||
|
||||
@@ -289,11 +289,7 @@ impl Args {
|
||||
/// Get the JWT secret path - either provided or derived from datadir
|
||||
pub(crate) fn jwt_secret_path(&self) -> PathBuf {
|
||||
match &self.jwt_secret {
|
||||
Some(path) => {
|
||||
let jwt_secret_str = path.to_string_lossy();
|
||||
let expanded = shellexpand::tilde(&jwt_secret_str);
|
||||
PathBuf::from(expanded.as_ref())
|
||||
}
|
||||
Some(path) => path.clone(),
|
||||
None => {
|
||||
// Use the same logic as reth: <datadir>/<chain>/jwt.hex
|
||||
let chain_path = self.datadir.clone().resolve_datadir(self.chain);
|
||||
@@ -308,10 +304,9 @@ impl Args {
|
||||
chain_path.data_dir().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the expanded output directory path
|
||||
/// Get the output directory path
|
||||
pub(crate) fn output_dir_path(&self) -> PathBuf {
|
||||
let expanded = shellexpand::tilde(&self.output_dir);
|
||||
PathBuf::from(expanded.as_ref())
|
||||
PathBuf::from(&self.output_dir)
|
||||
}
|
||||
|
||||
/// Get the effective warmup blocks value - either specified or defaults to blocks
|
||||
|
||||
@@ -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 {
|
||||
@@ -142,7 +144,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 +153,7 @@ impl BenchContext {
|
||||
next_block,
|
||||
is_optimism,
|
||||
use_reth_namespace,
|
||||
rlp_blocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::{
|
||||
helpers::{build_payload, parse_gas_limit, prepare_payload_request, rpc_block_to_header},
|
||||
output::GasRampPayloadFile,
|
||||
},
|
||||
valid_payload::{call_forkchoice_updated, call_new_payload_with_reth, payload_to_new_payload},
|
||||
valid_payload::{
|
||||
call_forkchoice_updated_with_reth, call_new_payload_with_reth, payload_to_new_payload,
|
||||
},
|
||||
};
|
||||
use alloy_eips::BlockNumberOrTag;
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
@@ -19,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;
|
||||
|
||||
@@ -147,7 +150,7 @@ impl Command {
|
||||
}
|
||||
}
|
||||
if self.reth_new_payload {
|
||||
info!("Using reth_newPayload endpoint");
|
||||
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
|
||||
}
|
||||
|
||||
let mut blocks_processed = 0u64;
|
||||
@@ -182,28 +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(&provider, version, forkchoice_state, None).await?;
|
||||
call_forkchoice_updated_with_reth(&provider, version, forkchoice_state).await?;
|
||||
|
||||
parent_header = block.header;
|
||||
parent_hash = block_hash;
|
||||
|
||||
@@ -773,6 +773,7 @@ impl Command {
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
},
|
||||
transactions: transactions.to_vec(),
|
||||
extra_data: None,
|
||||
|
||||
@@ -76,7 +76,7 @@ use alloy_primitives::{Address, B256};
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, RootProvider};
|
||||
use alloy_rpc_types_engine::{
|
||||
CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ForkchoiceState,
|
||||
PayloadAttributes, PayloadId,
|
||||
PayloadAttributes, PayloadId, PraguePayloadFields,
|
||||
};
|
||||
use eyre::OptionExt;
|
||||
use reth_chainspec::{ChainSpec, EthereumHardforks};
|
||||
@@ -138,6 +138,7 @@ pub(crate) fn prepare_payload_request(
|
||||
suggested_fee_recipient: Address::ZERO,
|
||||
withdrawals: shanghai_active.then(Vec::new),
|
||||
parent_beacon_block_root: cancun_active.then_some(B256::ZERO),
|
||||
slot_number: None,
|
||||
},
|
||||
forkchoice_state: ForkchoiceState {
|
||||
head_block_hash: parent_hash,
|
||||
@@ -230,14 +231,33 @@ pub(crate) async fn get_payload_with_sidecar(
|
||||
}
|
||||
4 => {
|
||||
let envelope = provider.get_payload_v4(payload_id).await?;
|
||||
Ok(envelope.into_payload_and_sidecar(
|
||||
parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V4")?,
|
||||
let versioned_hashes = versioned_hashes_from_commitments(
|
||||
&envelope.envelope_inner.blobs_bundle.commitments,
|
||||
);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V4")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
let prague_fields = PraguePayloadFields::new(envelope.execution_requests);
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.envelope_inner.execution_payload),
|
||||
ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
|
||||
))
|
||||
}
|
||||
5 => {
|
||||
let envelope = provider.get_payload_v5(payload_id).await?;
|
||||
Ok(envelope.into_payload_and_sidecar(
|
||||
parent_beacon_block_root.ok_or_eyre("parent_beacon_block_root required for V5")?,
|
||||
let versioned_hashes =
|
||||
versioned_hashes_from_commitments(&envelope.blobs_bundle.commitments);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V5")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
let prague_fields = PraguePayloadFields::new(envelope.execution_requests);
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.execution_payload),
|
||||
ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
|
||||
))
|
||||
}
|
||||
_ => panic!("This tool does not support getPayload versions past v5"),
|
||||
|
||||
156
bin/reth-bench/src/bench/metrics_scraper.rs
Normal file
156
bin/reth-bench/src/bench/metrics_scraper.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
//! Prometheus metrics scraper for reth-bench.
|
||||
//!
|
||||
//! Scrapes a node's Prometheus metrics endpoint after each block to record
|
||||
//! execution and state root durations with block-level granularity.
|
||||
|
||||
use csv::Writer;
|
||||
use eyre::Context;
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
use std::{path::Path, time::Duration};
|
||||
use tracing::info;
|
||||
|
||||
/// Suffix for the metrics CSV output file.
|
||||
pub(crate) const METRICS_OUTPUT_SUFFIX: &str = "metrics.csv";
|
||||
|
||||
/// A single row of scraped prometheus metrics for one block.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct MetricsRow {
|
||||
/// The block number.
|
||||
pub(crate) block_number: u64,
|
||||
/// EVM execution duration in seconds (from `sync_execution_execution_duration` gauge).
|
||||
pub(crate) execution_duration_secs: Option<f64>,
|
||||
/// State root computation duration in seconds (from
|
||||
/// `sync_block_validation_state_root_duration` gauge).
|
||||
pub(crate) state_root_duration_secs: Option<f64>,
|
||||
}
|
||||
|
||||
/// Scrapes a Prometheus metrics endpoint after each block to collect
|
||||
/// execution and state root durations.
|
||||
pub(crate) struct MetricsScraper {
|
||||
/// The full URL of the Prometheus metrics endpoint.
|
||||
url: String,
|
||||
/// Reusable HTTP client.
|
||||
client: Client,
|
||||
/// Collected metrics rows, one per block.
|
||||
rows: Vec<MetricsRow>,
|
||||
}
|
||||
|
||||
impl MetricsScraper {
|
||||
/// Creates a new scraper if a URL is provided.
|
||||
pub(crate) fn maybe_new(url: Option<String>) -> Option<Self> {
|
||||
url.map(|url| {
|
||||
info!(target: "reth-bench", %url, "Prometheus metrics scraping enabled");
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("failed to build reqwest client");
|
||||
Self { url, client, rows: Vec::new() }
|
||||
})
|
||||
}
|
||||
|
||||
/// Scrapes the metrics endpoint and records values for the given block.
|
||||
pub(crate) async fn scrape_after_block(&mut self, block_number: u64) -> eyre::Result<()> {
|
||||
let text = self
|
||||
.client
|
||||
.get(&self.url)
|
||||
.send()
|
||||
.await
|
||||
.wrap_err("failed to fetch metrics endpoint")?
|
||||
.error_for_status()
|
||||
.wrap_err("metrics endpoint returned error status")?
|
||||
.text()
|
||||
.await
|
||||
.wrap_err("failed to read metrics response body")?;
|
||||
|
||||
let execution = parse_gauge(&text, "sync_execution_execution_duration");
|
||||
let state_root = parse_gauge(&text, "sync_block_validation_state_root_duration");
|
||||
|
||||
self.rows.push(MetricsRow {
|
||||
block_number,
|
||||
execution_duration_secs: execution,
|
||||
state_root_duration_secs: state_root,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes collected metrics to a CSV file in the output directory.
|
||||
pub(crate) fn write_csv(&self, output_dir: &Path) -> eyre::Result<()> {
|
||||
let path = output_dir.join(METRICS_OUTPUT_SUFFIX);
|
||||
info!(target: "reth-bench", "Writing scraped metrics to file: {:?}", path);
|
||||
let mut writer = Writer::from_path(&path)?;
|
||||
for row in &self.rows {
|
||||
writer.serialize(row)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a Prometheus gauge value from exposition-format text.
|
||||
///
|
||||
/// Searches for lines starting with `name` followed by either a space or `{`
|
||||
/// (for labeled metrics), then parses the numeric value. Returns the last
|
||||
/// matching sample to handle metrics emitted with multiple label sets.
|
||||
fn parse_gauge(text: &str, name: &str) -> Option<f64> {
|
||||
let mut result = None;
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !line.starts_with(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure we match the full metric name, not a prefix of another metric.
|
||||
let rest = &line[name.len()..];
|
||||
if !rest.starts_with(' ') && !rest.starts_with('{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format: `metric_name{labels} value [timestamp]` or `metric_name value [timestamp]`
|
||||
// Value is always the second whitespace-separated token.
|
||||
let mut parts = line.split_whitespace();
|
||||
if let Some(value_str) = parts.nth(1) &&
|
||||
let Ok(v) = value_str.parse::<f64>()
|
||||
{
|
||||
result = Some(v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_gauge_simple() {
|
||||
let text = r#"# HELP sync_execution_execution_duration Duration of execution
|
||||
# TYPE sync_execution_execution_duration gauge
|
||||
sync_execution_execution_duration 0.123456
|
||||
"#;
|
||||
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), Some(0.123456));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gauge_missing() {
|
||||
let text = "some_other_metric 1.0\n";
|
||||
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gauge_with_labels() {
|
||||
let text = "sync_block_validation_state_root_duration{instance=\"node1\"} 0.5\n";
|
||||
assert_eq!(parse_gauge(text, "sync_block_validation_state_root_duration"), Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gauge_prefix_no_false_match() {
|
||||
let text =
|
||||
"sync_execution_execution_duration_total 99.0\nsync_execution_execution_duration 0.5\n";
|
||||
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), Some(0.5));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub(crate) mod helpers;
|
||||
pub use generate_big_block::{
|
||||
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
|
||||
};
|
||||
pub(crate) mod metrics_scraper;
|
||||
mod new_payload_fcu;
|
||||
mod new_payload_only;
|
||||
mod output;
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
bench::{
|
||||
context::BenchContext,
|
||||
helpers::parse_duration,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
|
||||
},
|
||||
@@ -20,9 +21,11 @@ use crate::{
|
||||
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
|
||||
},
|
||||
},
|
||||
valid_payload::{block_to_new_payload, call_forkchoice_updated, call_new_payload_with_reth},
|
||||
valid_payload::{
|
||||
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};
|
||||
@@ -30,7 +33,7 @@ use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
|
||||
use reth_node_core::args::BenchmarkArgs;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, info};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// `reth benchmark new-payload-fcu` command
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -151,12 +154,15 @@ 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();
|
||||
|
||||
let mut metrics_scraper = MetricsScraper::maybe_new(self.benchmark.metrics_url.clone());
|
||||
|
||||
if use_reth_namespace {
|
||||
info!("Using reth_newPayload endpoint");
|
||||
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
|
||||
}
|
||||
|
||||
let buffer_size = self.rpc_block_buffer_size;
|
||||
@@ -181,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());
|
||||
@@ -202,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}");
|
||||
@@ -216,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();
|
||||
@@ -235,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());
|
||||
@@ -258,7 +279,7 @@ impl Command {
|
||||
};
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
call_forkchoice_updated(&auth_provider, version, forkchoice_state, None).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() {
|
||||
@@ -288,6 +309,12 @@ impl Command {
|
||||
};
|
||||
info!(target: "reth-bench", progress, %combined_result);
|
||||
|
||||
if let Some(scraper) = metrics_scraper.as_mut() &&
|
||||
let Err(err) = scraper.scrape_after_block(block_number).await
|
||||
{
|
||||
warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
|
||||
}
|
||||
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(block_number).await?;
|
||||
}
|
||||
@@ -313,6 +340,10 @@ impl Command {
|
||||
write_benchmark_results(path, &gas_output_results, &combined_results)?;
|
||||
}
|
||||
|
||||
if let (Some(path), Some(scraper)) = (&self.benchmark.output, &metrics_scraper) {
|
||||
scraper.write_csv(path)?;
|
||||
}
|
||||
|
||||
let gas_output =
|
||||
TotalGasOutput::with_combined_results(gas_output_results, &combined_results)?;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::{
|
||||
bench::{
|
||||
context::BenchContext,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
NewPayloadResult, TotalGasOutput, TotalGasRow, GAS_OUTPUT_SUFFIX,
|
||||
NEW_PAYLOAD_OUTPUT_SUFFIX,
|
||||
@@ -10,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};
|
||||
@@ -50,10 +51,13 @@ 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();
|
||||
|
||||
let mut metrics_scraper = MetricsScraper::maybe_new(self.benchmark.metrics_url.clone());
|
||||
|
||||
if use_reth_namespace {
|
||||
info!("Using reth_newPayload endpoint");
|
||||
}
|
||||
@@ -80,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;
|
||||
}
|
||||
@@ -93,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();
|
||||
@@ -105,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());
|
||||
@@ -142,6 +159,12 @@ impl Command {
|
||||
let row =
|
||||
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
|
||||
results.push((row, new_payload_result));
|
||||
|
||||
if let Some(scraper) = metrics_scraper.as_mut() &&
|
||||
let Err(err) = scraper.scrape_after_block(block_number).await
|
||||
{
|
||||
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the spawned task encountered an error
|
||||
@@ -172,6 +195,10 @@ impl Command {
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
if let Some(scraper) = &metrics_scraper {
|
||||
scraper.write_csv(&path)?;
|
||||
}
|
||||
|
||||
info!(target: "reth-bench", "Finished writing benchmark output files to {:?}.", path);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(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
|
||||
|
||||
@@ -160,12 +160,11 @@ impl PersistenceSubscription {
|
||||
/// to pings.
|
||||
pub(crate) async fn setup_persistence_subscription(
|
||||
ws_url: Url,
|
||||
persistence_timeout: Duration,
|
||||
_persistence_timeout: Duration,
|
||||
) -> eyre::Result<PersistenceSubscription> {
|
||||
info!(target: "reth-bench", "Connecting to WebSocket at {} for persistence subscription", ws_url);
|
||||
|
||||
let ws_connect =
|
||||
WsConnect::new(ws_url.to_string()).with_keepalive_interval(persistence_timeout);
|
||||
let ws_connect = WsConnect::new(ws_url.to_string());
|
||||
let client = RpcClient::connect_pubsub(ws_connect)
|
||||
.await
|
||||
.wrap_err("Failed to connect to WebSocket RPC endpoint")?;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::{
|
||||
helpers::parse_duration,
|
||||
metrics_scraper::MetricsScraper,
|
||||
output::{
|
||||
write_benchmark_results, CombinedResult, GasRampPayloadFile, NewPayloadResult,
|
||||
TotalGasOutput, TotalGasRow,
|
||||
@@ -23,10 +24,10 @@ use crate::{
|
||||
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
|
||||
},
|
||||
},
|
||||
valid_payload::{call_forkchoice_updated, call_new_payload_with_reth},
|
||||
valid_payload::{call_forkchoice_updated_with_reth, call_new_payload_with_reth},
|
||||
};
|
||||
use alloy_primitives::B256;
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{
|
||||
CancunPayloadFields, ExecutionData, ExecutionPayloadEnvelopeV4, ExecutionPayloadSidecar,
|
||||
@@ -37,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},
|
||||
@@ -135,6 +137,14 @@ pub struct Command {
|
||||
/// and returns server-side timing breakdowns (latency, persistence wait, cache wait).
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
reth_new_payload: bool,
|
||||
|
||||
/// Optional Prometheus metrics endpoint to scrape after each block.
|
||||
///
|
||||
/// When provided, reth-bench will fetch metrics from this URL after each
|
||||
/// payload, recording per-block execution and state root durations.
|
||||
/// Results are written to `metrics.csv` in the output directory.
|
||||
#[arg(long = "metrics-url", value_name = "URL", verbatim_doc_comment)]
|
||||
metrics_url: Option<String>,
|
||||
}
|
||||
|
||||
/// A loaded payload ready for execution.
|
||||
@@ -152,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,
|
||||
}
|
||||
@@ -175,7 +187,7 @@ impl Command {
|
||||
);
|
||||
}
|
||||
if self.reth_new_payload {
|
||||
info!("Using reth_newPayload endpoint");
|
||||
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
|
||||
}
|
||||
|
||||
// Set up waiter based on configured options
|
||||
@@ -204,6 +216,8 @@ impl Command {
|
||||
(None, false) => None,
|
||||
};
|
||||
|
||||
let mut metrics_scraper = MetricsScraper::maybe_new(self.metrics_url.clone());
|
||||
|
||||
// Set up authenticated engine provider
|
||||
let jwt =
|
||||
std::fs::read_to_string(&self.jwt_secret).wrap_err("Failed to read JWT secret file")?;
|
||||
@@ -262,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?;
|
||||
|
||||
@@ -277,7 +288,7 @@ impl Command {
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
call_forkchoice_updated(&auth_provider, payload.version, fcu_state, None).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");
|
||||
|
||||
@@ -325,31 +336,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());
|
||||
@@ -373,10 +387,8 @@ impl Command {
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
|
||||
debug!(target: "reth-bench", method = "engine_forkchoiceUpdatedV3", ?fcu_state, "Sending forkchoiceUpdated");
|
||||
|
||||
let fcu_start = Instant::now();
|
||||
let fcu_result = auth_provider.fork_choice_updated_v3(fcu_state, None).await?;
|
||||
call_forkchoice_updated_with_reth(&auth_provider, version, fcu_state).await?;
|
||||
let fcu_latency = fcu_start.elapsed();
|
||||
|
||||
let total_latency =
|
||||
@@ -395,6 +407,12 @@ impl Command {
|
||||
let progress = format!("{}/{}", i + 1, payloads.len());
|
||||
info!(target: "reth-bench", progress, %combined_result);
|
||||
|
||||
if let Some(scraper) = metrics_scraper.as_mut() &&
|
||||
let Err(err) = scraper.scrape_after_block(block_number).await
|
||||
{
|
||||
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
|
||||
}
|
||||
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(block_number).await?;
|
||||
}
|
||||
@@ -403,7 +421,6 @@ impl Command {
|
||||
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
|
||||
results.push((gas_row, combined_result));
|
||||
|
||||
debug!(target: "reth-bench", ?fcu_result, "Payload executed successfully");
|
||||
parent_hash = block_hash;
|
||||
}
|
||||
|
||||
@@ -418,6 +435,10 @@ impl Command {
|
||||
write_benchmark_results(path, &gas_output_results, &combined_results)?;
|
||||
}
|
||||
|
||||
if let (Some(path), Some(scraper)) = (&self.output, &metrics_scraper) {
|
||||
scraper.write_csv(path)?;
|
||||
}
|
||||
|
||||
let gas_output =
|
||||
TotalGasOutput::with_combined_results(gas_output_results, &combined_results)?;
|
||||
info!(
|
||||
@@ -528,13 +549,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!(
|
||||
|
||||
@@ -240,6 +240,7 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
|
||||
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
|
||||
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
|
||||
ExecutionPayload::V4(p) => config.apply_to_payload_v3(&mut p.payload_inner),
|
||||
};
|
||||
|
||||
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
|
||||
@@ -254,6 +255,9 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => p.block_hash,
|
||||
ExecutionPayload::V2(p) => p.payload_inner.block_hash,
|
||||
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
|
||||
ExecutionPayload::V4(p) => {
|
||||
p.payload_inner.payload_inner.payload_inner.block_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -262,6 +266,9 @@ impl Command {
|
||||
ExecutionPayload::V1(p) => p.block_hash = new_hash,
|
||||
ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
|
||||
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
|
||||
ExecutionPayload::V4(p) => {
|
||||
p.payload_inner.payload_inner.payload_inner.block_hash = new_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -198,6 +214,20 @@ pub(crate) fn payload_to_new_payload(
|
||||
let execution_data = ExecutionData { payload: payload.clone(), sidecar: sidecar.clone() };
|
||||
|
||||
let (version, params) = match payload {
|
||||
ExecutionPayload::V4(payload) => {
|
||||
let cancun = sidecar.cancun().unwrap();
|
||||
let prague = sidecar.prague().unwrap();
|
||||
let requests = prague.requests.requests_hash();
|
||||
(
|
||||
EngineApiMessageVersion::V5,
|
||||
serde_json::to_value((
|
||||
payload,
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
}
|
||||
ExecutionPayload::V3(payload) => {
|
||||
let cancun = sidecar.cancun().unwrap();
|
||||
|
||||
@@ -266,17 +296,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 +328,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
|
||||
@@ -379,9 +385,12 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
|
||||
forkchoice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
|
||||
// FCU V3 is used for Cancun, Prague, and Amsterdam (there is no FCU V4-V6)
|
||||
match message_version {
|
||||
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
|
||||
EngineApiMessageVersion::V3 |
|
||||
EngineApiMessageVersion::V4 |
|
||||
EngineApiMessageVersion::V5 |
|
||||
EngineApiMessageVersion::V6 => {
|
||||
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
|
||||
}
|
||||
EngineApiMessageVersion::V2 => {
|
||||
@@ -392,3 +401,47 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls either `reth_forkchoiceUpdated` or the standard `engine_forkchoiceUpdated*` depending
|
||||
/// on `use_reth`.
|
||||
///
|
||||
/// When `use_reth` is true, uses the `reth_forkchoiceUpdated` endpoint which sends a regular FCU
|
||||
/// with no payload attributes.
|
||||
pub(crate) async fn call_forkchoice_updated_with_reth<
|
||||
N: Network,
|
||||
P: Provider<N> + EngineApiValidWaitExt<N>,
|
||||
>(
|
||||
provider: P,
|
||||
message_version: Option<EngineApiMessageVersion>,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
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");
|
||||
|
||||
loop {
|
||||
let resp: ForkchoiceUpdated = provider.client().request(method, &reth_params).await?;
|
||||
|
||||
if resp.is_valid() {
|
||||
break Ok(resp)
|
||||
}
|
||||
|
||||
if resp.is_invalid() {
|
||||
error!(target: "reth-bench", ?resp, "Invalid {method}");
|
||||
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
|
||||
std::io::Error::other(format!("Invalid {method}: {resp:?}")),
|
||||
)))
|
||||
}
|
||||
if resp.is_syncing() {
|
||||
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
|
||||
"invalid range: no canonical state found for parent of requested block",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ workspace = true
|
||||
# reth
|
||||
reth-ethereum-cli.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-primitives.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-db = { workspace = true, features = ["mdbx"] }
|
||||
reth-provider.workspace = true
|
||||
reth-revm.workspace = true
|
||||
@@ -69,7 +70,7 @@ aquamarine.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-node-bindings = "1.6.3"
|
||||
alloy-node-bindings = "1.5.2"
|
||||
alloy-provider = { workspace = true, features = ["reqwest"] }
|
||||
alloy-rpc-types-eth.workspace = true
|
||||
backon.workspace = true
|
||||
@@ -110,7 +111,6 @@ dev = ["reth-ethereum-cli/dev"]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"reth-primitives/asm-keccak",
|
||||
"reth-ethereum-cli/asm-keccak",
|
||||
"reth-node-ethereum/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
|
||||
@@ -124,9 +124,11 @@ pub mod providers {
|
||||
pub use reth_provider::*;
|
||||
}
|
||||
|
||||
/// Re-exported from `reth_primitives`.
|
||||
/// Re-exported primitives.
|
||||
#[allow(ambiguous_glob_reexports)]
|
||||
pub mod primitives {
|
||||
pub use reth_primitives::*;
|
||||
pub use reth_ethereum_primitives::*;
|
||||
pub use reth_primitives_traits::*;
|
||||
}
|
||||
|
||||
/// Re-exported from `reth_ethereum_consensus`.
|
||||
|
||||
@@ -772,6 +772,7 @@ impl<N: NodePrimitives> Default for ExecutedBlock<N> {
|
||||
requests: Default::default(),
|
||||
gas_used: 0,
|
||||
blob_gas_used: 0,
|
||||
block_access_list: Default::default(),
|
||||
},
|
||||
state: Default::default(),
|
||||
}),
|
||||
@@ -1061,14 +1062,6 @@ mod tests {
|
||||
) -> ProviderResult<Option<StorageValue>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn storage_by_hashed_key(
|
||||
&self,
|
||||
_address: Address,
|
||||
_hashed_storage_key: StorageKey,
|
||||
) -> ProviderResult<Option<StorageValue>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl BytecodeReader for MockStateProvider {
|
||||
|
||||
@@ -223,26 +223,6 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
|
||||
|
||||
self.historical.storage(address, storage_key)
|
||||
}
|
||||
|
||||
fn storage_by_hashed_key(
|
||||
&self,
|
||||
address: Address,
|
||||
hashed_storage_key: StorageKey,
|
||||
) -> ProviderResult<Option<StorageValue>> {
|
||||
let hashed_address = keccak256(address);
|
||||
let state = &self.trie_input().state;
|
||||
|
||||
if let Some(hs) = state.storages.get(&hashed_address) {
|
||||
if let Some(value) = hs.storage.get(&hashed_storage_key) {
|
||||
return Ok(Some(*value));
|
||||
}
|
||||
if hs.wiped {
|
||||
return Ok(Some(StorageValue::ZERO));
|
||||
}
|
||||
}
|
||||
|
||||
self.historical.storage_by_hashed_key(address, hashed_storage_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N> {
|
||||
|
||||
@@ -212,6 +212,7 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
|
||||
requests: Default::default(),
|
||||
gas_used: 0,
|
||||
blob_gas_used: 0,
|
||||
block_access_list: Default::default(),
|
||||
},
|
||||
state: BundleState::default(),
|
||||
}),
|
||||
|
||||
@@ -132,6 +132,6 @@ impl<H: BlockHeader> EthChainSpec for ChainSpec<H> {
|
||||
}
|
||||
|
||||
fn final_paris_total_difficulty(&self) -> Option<U256> {
|
||||
self.paris_block_and_final_difficulty.map(|(_, final_difficulty)| final_difficulty)
|
||||
self.get_final_paris_total_difficulty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use alloy_consensus::{
|
||||
};
|
||||
use alloy_eips::{
|
||||
eip1559::INITIAL_BASE_FEE, eip7685::EMPTY_REQUESTS_HASH, eip7840::BlobParams,
|
||||
eip7892::BlobScheduleBlobParams,
|
||||
eip7892::BlobScheduleBlobParams, eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
|
||||
};
|
||||
use alloy_genesis::{ChainConfig, Genesis};
|
||||
use alloy_primitives::{address, b256, Address, BlockNumber, B256, U256};
|
||||
@@ -76,6 +76,18 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(EMPTY_REQUESTS_HASH);
|
||||
|
||||
// If Amsterdam is activated at genesis we set block access list hash to empty hash.
|
||||
let block_access_list_hash = hardforks
|
||||
.fork(EthereumHardfork::Amsterdam)
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(EMPTY_BLOCK_ACCESS_LIST_HASH);
|
||||
|
||||
// If Amsterdam is activated at genesis we set slot number to 0.
|
||||
let slot_number = hardforks
|
||||
.fork(EthereumHardfork::Amsterdam)
|
||||
.active_at_timestamp(genesis.timestamp)
|
||||
.then_some(0);
|
||||
|
||||
Header {
|
||||
number: genesis.number.unwrap_or_default(),
|
||||
parent_hash: genesis.parent_hash.unwrap_or_default(),
|
||||
@@ -93,6 +105,8 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
|
||||
blob_gas_used,
|
||||
excess_blob_gas,
|
||||
requests_hash,
|
||||
block_access_list_hash,
|
||||
slot_number,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -298,6 +312,7 @@ pub fn create_chain_config(
|
||||
cancun_time: timestamp(EthereumHardfork::Cancun),
|
||||
prague_time: timestamp(EthereumHardfork::Prague),
|
||||
osaka_time: timestamp(EthereumHardfork::Osaka),
|
||||
amsterdam_time: timestamp(EthereumHardfork::Amsterdam),
|
||||
bpo1_time: timestamp(EthereumHardfork::Bpo1),
|
||||
bpo2_time: timestamp(EthereumHardfork::Bpo2),
|
||||
bpo3_time: timestamp(EthereumHardfork::Bpo3),
|
||||
@@ -855,15 +870,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)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -886,6 +895,7 @@ impl From<Genesis> for ChainSpec {
|
||||
(EthereumHardfork::Cancun.boxed(), genesis.config.cancun_time),
|
||||
(EthereumHardfork::Prague.boxed(), genesis.config.prague_time),
|
||||
(EthereumHardfork::Osaka.boxed(), genesis.config.osaka_time),
|
||||
(EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
|
||||
(EthereumHardfork::Bpo1.boxed(), genesis.config.bpo1_time),
|
||||
(EthereumHardfork::Bpo2.boxed(), genesis.config.bpo2_time),
|
||||
(EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
|
||||
@@ -1197,6 +1207,19 @@ impl ChainSpecBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable Amsterdam at genesis.
|
||||
pub fn amsterdam_activated(mut self) -> Self {
|
||||
self = self.osaka_activated();
|
||||
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(0));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable Amsterdam at the given timestamp.
|
||||
pub fn with_amsterdam_at(mut self, timestamp: u64) -> Self {
|
||||
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(timestamp));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the resulting [`ChainSpec`].
|
||||
///
|
||||
/// # Panics
|
||||
|
||||
@@ -18,6 +18,5 @@ alloy-genesis.workspace = true
|
||||
|
||||
# misc
|
||||
clap.workspace = true
|
||||
shellexpand.workspace = true
|
||||
eyre.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -73,7 +73,7 @@ pub trait ChainSpecParser: Clone + Send + Sync + 'static {
|
||||
/// A helper to parse a [`Genesis`](alloy_genesis::Genesis) as argument or from disk.
|
||||
pub fn parse_genesis(s: &str) -> eyre::Result<alloy_genesis::Genesis> {
|
||||
// try to read json from path first
|
||||
let raw = match fs::read_to_string(PathBuf::from(shellexpand::full(s)?.into_owned())) {
|
||||
let raw = match fs::read_to_string(PathBuf::from(s)) {
|
||||
Ok(raw) => raw,
|
||||
Err(io_err) => {
|
||||
// valid json may start with "\n", but must contain "{"
|
||||
|
||||
@@ -43,7 +43,7 @@ reth-node-metrics.workspace = true
|
||||
reth-ethereum-primitives = { workspace = true, optional = true }
|
||||
reth-provider.workspace = true
|
||||
reth-prune.workspace = true
|
||||
reth-prune-types = { workspace = true, optional = true }
|
||||
reth-prune-types.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-stages.workspace = true
|
||||
reth-stages-types = { workspace = true, optional = true }
|
||||
@@ -53,7 +53,7 @@ reth-tasks.workspace = true
|
||||
reth-storage-api.workspace = true
|
||||
reth-trie = { workspace = true, features = ["metrics"] }
|
||||
reth-trie-db = { workspace = true, features = ["metrics"] }
|
||||
reth-trie-common.workspace = true
|
||||
reth-trie-common = { workspace = true, optional = true }
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-discv4.workspace = true
|
||||
reth-discv5.workspace = true
|
||||
@@ -113,6 +113,7 @@ arbitrary = [
|
||||
"dep:proptest",
|
||||
"dep:arbitrary",
|
||||
"dep:proptest-arbitrary-interop",
|
||||
"dep:reth-trie-common",
|
||||
"reth-db-api/arbitrary",
|
||||
"reth-eth-wire/arbitrary",
|
||||
"reth-db/arbitrary",
|
||||
@@ -123,11 +124,11 @@ arbitrary = [
|
||||
"reth-codecs/test-utils",
|
||||
"reth-prune-types/test-utils",
|
||||
"reth-stages-types/test-utils",
|
||||
"reth-trie-common/test-utils",
|
||||
"reth-trie-common?/test-utils",
|
||||
"reth-codecs/arbitrary",
|
||||
"reth-prune-types?/arbitrary",
|
||||
"reth-prune-types/arbitrary",
|
||||
"reth-stages-types?/arbitrary",
|
||||
"reth-trie-common/arbitrary",
|
||||
"reth-trie-common?/arbitrary",
|
||||
"alloy-consensus/arbitrary",
|
||||
"reth-primitives-traits/arbitrary",
|
||||
"reth-ethereum-primitives/arbitrary",
|
||||
|
||||
@@ -89,13 +89,15 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
/// Initializes environment according to [`AccessRights`] and returns an instance of
|
||||
/// [`Environment`].
|
||||
///
|
||||
/// Internally builds a [`reth_tasks::Runtime`] attached to the current tokio handle for
|
||||
/// parallel storage I/O.
|
||||
pub fn init<N: CliNodeTypes>(&self, access: AccessRights) -> eyre::Result<Environment<N>>
|
||||
/// The provided `runtime` is used for parallel storage I/O.
|
||||
pub fn init<N: CliNodeTypes>(
|
||||
&self,
|
||||
access: AccessRights,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<Environment<N>>
|
||||
where
|
||||
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
|
||||
{
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
|
||||
let db_path = data_dir.db();
|
||||
let sf_path = data_dir.static_files();
|
||||
@@ -144,11 +146,24 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
})
|
||||
}
|
||||
};
|
||||
let rocksdb_provider = RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.with_read_only(!access.is_read_write())
|
||||
.build()?;
|
||||
let rocksdb_provider = if !access.is_read_write() && !RocksDBProvider::exists(&rocksdb_path)
|
||||
{
|
||||
// RocksDB database doesn't exist yet (e.g. datadir restored from a snapshot
|
||||
// or created before RocksDB storage). Create an empty one so read-only
|
||||
// commands can proceed.
|
||||
debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
|
||||
reth_fs_util::create_dir_all(&rocksdb_path)?;
|
||||
RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.build()?
|
||||
} else {
|
||||
RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.with_read_only(!access.is_read_write())
|
||||
.build()?
|
||||
};
|
||||
|
||||
let provider_factory =
|
||||
self.create_provider_factory(&config, db, sfp, rocksdb_provider, access, runtime)?;
|
||||
|
||||
@@ -14,7 +14,7 @@ use reth_db_api::{
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::{NodeTypesWithDB, NodeTypesWithDBAdapter};
|
||||
use reth_provider::{providers::ProviderNodeTypes, DBProvider, StaticFileProviderFactory};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_static_file_types::{ChangesetOffset, StaticFileSegment};
|
||||
use std::{
|
||||
hash::{BuildHasher, Hasher},
|
||||
time::{Duration, Instant},
|
||||
@@ -134,12 +134,12 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
.ok_or_else(|| eyre::eyre!("No static files found for segment: {}", segment))?;
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut hasher = checksum_hasher();
|
||||
let mut total = 0usize;
|
||||
let limit = limit.unwrap_or(usize::MAX);
|
||||
let mut checksummer = Checksummer::new(checksum_hasher(), limit);
|
||||
|
||||
let start_block = start_block.unwrap_or(0);
|
||||
let end_block = end_block.unwrap_or(u64::MAX);
|
||||
let is_change_based = segment.is_change_based();
|
||||
|
||||
info!(
|
||||
"Computing checksum for {} static files, start_block={}, end_block={}, limit={:?}",
|
||||
@@ -149,7 +149,8 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
if limit == usize::MAX { None } else { Some(limit) }
|
||||
);
|
||||
|
||||
'outer: for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
|
||||
let mut reached_limit = false;
|
||||
for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
|
||||
if block_range.end() < start_block || block_range.start() > end_block {
|
||||
continue;
|
||||
}
|
||||
@@ -167,28 +168,42 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
|
||||
let mut cursor = jar_provider.cursor()?;
|
||||
|
||||
while let Ok(Some(row)) = cursor.next_row() {
|
||||
for col_data in row.iter() {
|
||||
hasher.write(col_data);
|
||||
}
|
||||
if is_change_based {
|
||||
let offsets = jar_provider.read_changeset_offsets()?.ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"Missing changeset offsets sidecar for segment {} at range {}",
|
||||
segment,
|
||||
block_range
|
||||
)
|
||||
})?;
|
||||
let input = ChangeBasedChecksumInput {
|
||||
segment,
|
||||
block_range_start: block_range.start(),
|
||||
start_block,
|
||||
end_block,
|
||||
offsets: &offsets,
|
||||
};
|
||||
|
||||
total += 1;
|
||||
|
||||
if total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
|
||||
info!("Hashed {total} entries.");
|
||||
}
|
||||
|
||||
if total >= limit {
|
||||
break 'outer;
|
||||
reached_limit = checksum_change_based_segment(&mut checksummer, input, &mut cursor)?;
|
||||
} else {
|
||||
while let Some(row) = cursor.next_row()? {
|
||||
if checksummer.write_row(&row) {
|
||||
reached_limit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly drop provider before removing from cache to avoid deadlock
|
||||
drop(jar_provider);
|
||||
static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
|
||||
|
||||
if reached_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let checksum = hasher.finish();
|
||||
let (checksum, total) = checksummer.finish();
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
info!(
|
||||
@@ -267,7 +282,7 @@ impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N
|
||||
|
||||
total = index + 1;
|
||||
if total >= limit {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,3 +300,139 @@ impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N
|
||||
Ok((checksum, elapsed))
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulates a checksum over key-value entries, tracking count and limit.
|
||||
struct Checksummer<H> {
|
||||
hasher: H,
|
||||
total: usize,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl<H: Hasher> Checksummer<H> {
|
||||
fn new(hasher: H, limit: usize) -> Self {
|
||||
Self { hasher, total: 0, limit }
|
||||
}
|
||||
|
||||
/// Hash a row's columns (non-changeset segments). Returns `true` if the limit is reached.
|
||||
fn write_row(&mut self, row: &[&[u8]]) -> bool {
|
||||
for col in row {
|
||||
self.hasher.write(col);
|
||||
}
|
||||
self.advance()
|
||||
}
|
||||
|
||||
/// Hash a key + value as two separate writes, matching MDBX raw entry semantics.
|
||||
/// Write boundaries matter: foldhash rotates its accumulator by `len` on each `write`.
|
||||
fn write_entry(&mut self, key: &[u8], value: &[u8]) -> bool {
|
||||
self.hasher.write(key);
|
||||
self.hasher.write(value);
|
||||
self.advance()
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> bool {
|
||||
self.total += 1;
|
||||
if self.total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
|
||||
info!("Hashed {} entries.", self.total);
|
||||
}
|
||||
self.total >= self.limit
|
||||
}
|
||||
|
||||
fn finish(self) -> (u64, usize) {
|
||||
(self.hasher.finish(), self.total)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct MDBX `StorageChangeSets` key/value boundaries from a static-file row.
|
||||
///
|
||||
/// MDBX layout:
|
||||
/// - key: `BlockNumberAddress` => `[8B block_number][20B address]`
|
||||
/// - value: `StorageEntry` => `[32B storage_key][compact U256 value]`
|
||||
///
|
||||
/// Static-file row layout for `StorageBeforeTx`:
|
||||
/// - `[20B address][32B storage_key][compact U256 value]`
|
||||
fn split_storage_changeset_row(block_number: u64, row: &[u8]) -> eyre::Result<([u8; 28], &[u8])> {
|
||||
if row.len() < 20 {
|
||||
return Err(eyre::eyre!(
|
||||
"Storage changeset row too short: expected at least 20 bytes, got {}",
|
||||
row.len()
|
||||
));
|
||||
}
|
||||
|
||||
let mut key_buf = [0u8; 28];
|
||||
key_buf[..8].copy_from_slice(&block_number.to_be_bytes());
|
||||
key_buf[8..].copy_from_slice(&row[..20]);
|
||||
Ok((key_buf, &row[20..]))
|
||||
}
|
||||
|
||||
struct ChangeBasedChecksumInput<'a> {
|
||||
segment: StaticFileSegment,
|
||||
block_range_start: u64,
|
||||
start_block: u64,
|
||||
end_block: u64,
|
||||
offsets: &'a [ChangesetOffset],
|
||||
}
|
||||
|
||||
fn checksum_change_based_segment<H: Hasher>(
|
||||
checksummer: &mut Checksummer<H>,
|
||||
input: ChangeBasedChecksumInput<'_>,
|
||||
cursor: &mut reth_db::static_file::StaticFileCursor<'_>,
|
||||
) -> eyre::Result<bool> {
|
||||
let ChangeBasedChecksumInput { segment, block_range_start, start_block, end_block, offsets } =
|
||||
input;
|
||||
let is_storage = segment.is_storage_change_sets();
|
||||
let mut reached_limit = false;
|
||||
|
||||
for (offset_index, offset) in offsets.iter().enumerate() {
|
||||
let block_number = block_range_start + offset_index as u64;
|
||||
let include = block_number >= start_block && block_number <= end_block;
|
||||
|
||||
for _ in 0..offset.num_changes() {
|
||||
let row = cursor.next_row()?.ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"Unexpected EOF while checksumming {} static file at range starting {}",
|
||||
segment,
|
||||
block_range_start
|
||||
)
|
||||
})?;
|
||||
|
||||
if !include {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reconstruct MDBX key/value write boundaries. foldhash rotates
|
||||
// its accumulator by `len` on each write(), so boundaries must
|
||||
// match exactly.
|
||||
let done = if is_storage {
|
||||
// StorageChangeSets: MDBX key = BlockNumberAddress (28B),
|
||||
// value = compact StorageEntry. Column 0 is compact
|
||||
// StorageBeforeTx = [20B address][32B key][compact U256].
|
||||
let col = row[0];
|
||||
let (key, value) = split_storage_changeset_row(block_number, col)?;
|
||||
checksummer.write_entry(&key, value)
|
||||
} else {
|
||||
// AccountChangeSets: MDBX key = BlockNumber (8B),
|
||||
// value = compact AccountBeforeTx (= column 0).
|
||||
checksummer.write_entry(&block_number.to_be_bytes(), row[0])
|
||||
};
|
||||
|
||||
if done {
|
||||
reached_limit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if reached_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !reached_limit && cursor.next_row()?.is_some() {
|
||||
return Err(eyre::eyre!(
|
||||
"Changeset offsets do not cover all rows for {} at range starting {}",
|
||||
segment,
|
||||
block_range_start
|
||||
));
|
||||
}
|
||||
|
||||
Ok(reached_limit)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Command {
|
||||
)?;
|
||||
|
||||
if let Some(entry) = entry {
|
||||
let se: reth_primitives_traits::StorageEntry = entry.into();
|
||||
let se: reth_primitives_traits::StorageEntry = entry;
|
||||
println!("{}", serde_json::to_string_pretty(&se)?);
|
||||
} else {
|
||||
error!(target: "reth::cli", "No content for the given table key.");
|
||||
@@ -110,7 +110,7 @@ impl Command {
|
||||
let serializable: Vec<_> = changesets
|
||||
.into_iter()
|
||||
.map(|(addr, entry)| {
|
||||
let se: reth_primitives_traits::StorageEntry = entry.into();
|
||||
let se: reth_primitives_traits::StorageEntry = entry;
|
||||
(addr, se)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -16,8 +16,10 @@ mod copy;
|
||||
mod diff;
|
||||
mod get;
|
||||
mod list;
|
||||
mod prune_checkpoints;
|
||||
mod repair_trie;
|
||||
mod settings;
|
||||
mod stage_checkpoints;
|
||||
mod state;
|
||||
mod static_file_header;
|
||||
mod stats;
|
||||
@@ -67,6 +69,10 @@ pub enum Subcommands {
|
||||
Path,
|
||||
/// Manage storage settings
|
||||
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
|
||||
@@ -83,7 +89,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
/// provided command.
|
||||
macro_rules! db_exec {
|
||||
($env:expr, $tool:ident, $N:ident, $access_rights:expr, $command:block) => {
|
||||
let Environment { provider_factory, .. } = $env.init::<$N>($access_rights)?;
|
||||
let Environment { provider_factory, .. } =
|
||||
$env.init::<$N>($access_rights, ctx.task_executor.clone())?;
|
||||
|
||||
let $tool = DbTool::new(provider_factory)?;
|
||||
$command;
|
||||
@@ -204,6 +211,16 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::PruneCheckpoints(command) => {
|
||||
db_exec!(self.env, tool, N, command.access_rights(), {
|
||||
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)?;
|
||||
|
||||
221
crates/cli/commands/src/db/prune_checkpoints.rs
Normal file
221
crates/cli/commands/src/db/prune_checkpoints.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! `reth db prune-checkpoints` command for viewing and setting prune checkpoint values.
|
||||
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_provider::{providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory};
|
||||
use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
|
||||
use reth_storage_api::{PruneCheckpointReader, PruneCheckpointWriter};
|
||||
|
||||
use crate::common::AccessRights;
|
||||
|
||||
/// `reth db prune-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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Subcommands {
|
||||
/// Get prune checkpoint(s) from database.
|
||||
///
|
||||
/// Shows the current prune progress for each segment, including the highest
|
||||
/// pruned block/tx number and the active prune mode.
|
||||
Get {
|
||||
/// Specific segment to query. If omitted, shows all segments.
|
||||
#[arg(long, value_enum)]
|
||||
segment: Option<SegmentArg>,
|
||||
},
|
||||
/// Set a prune checkpoint for a segment.
|
||||
///
|
||||
/// WARNING: Manually setting checkpoints can cause data inconsistencies.
|
||||
/// Only use this if you know what you're doing (e.g., recovering from a
|
||||
/// corrupted checkpoint or forcing a re-prune from a specific block).
|
||||
Set(SetArgs),
|
||||
}
|
||||
|
||||
/// Arguments for the `set` subcommand
|
||||
#[derive(Debug, Args)]
|
||||
pub struct SetArgs {
|
||||
/// The prune segment to update
|
||||
#[arg(long, value_enum)]
|
||||
segment: SegmentArg,
|
||||
|
||||
/// Highest pruned block number
|
||||
#[arg(long)]
|
||||
block_number: Option<u64>,
|
||||
|
||||
/// Highest pruned transaction number
|
||||
#[arg(long)]
|
||||
tx_number: Option<u64>,
|
||||
|
||||
/// Prune mode to write: full, distance, or before
|
||||
#[arg(long, value_enum)]
|
||||
mode: PruneModeArg,
|
||||
|
||||
/// Value for distance or before mode (required unless mode is full)
|
||||
#[arg(long, required_if_eq_any([("mode", "distance"), ("mode", "before")]))]
|
||||
mode_value: Option<u64>,
|
||||
}
|
||||
|
||||
/// CLI-friendly prune segment names (excludes deprecated variants)
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum SegmentArg {
|
||||
SenderRecovery,
|
||||
TransactionLookup,
|
||||
Receipts,
|
||||
ContractLogs,
|
||||
AccountHistory,
|
||||
StorageHistory,
|
||||
Bodies,
|
||||
}
|
||||
|
||||
impl From<SegmentArg> for PruneSegment {
|
||||
fn from(arg: SegmentArg) -> Self {
|
||||
match arg {
|
||||
SegmentArg::SenderRecovery => Self::SenderRecovery,
|
||||
SegmentArg::TransactionLookup => Self::TransactionLookup,
|
||||
SegmentArg::Receipts => Self::Receipts,
|
||||
SegmentArg::ContractLogs => Self::ContractLogs,
|
||||
SegmentArg::AccountHistory => Self::AccountHistory,
|
||||
SegmentArg::StorageHistory => Self::StorageHistory,
|
||||
SegmentArg::Bodies => Self::Bodies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI-friendly prune mode
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum PruneModeArg {
|
||||
/// Prune all blocks
|
||||
Full,
|
||||
/// Keep the last N blocks (requires --mode-value)
|
||||
Distance,
|
||||
/// Prune blocks before a specific block number (requires --mode-value)
|
||||
Before,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute the command
|
||||
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
|
||||
match self.command {
|
||||
Subcommands::Get { segment } => Self::get(tool, segment),
|
||||
Subcommands::Set(args) => Self::set(tool, args),
|
||||
}
|
||||
}
|
||||
|
||||
fn get<N: ProviderNodeTypes>(
|
||||
tool: &DbTool<N>,
|
||||
segment: Option<SegmentArg>,
|
||||
) -> eyre::Result<()> {
|
||||
let provider = tool.provider_factory.provider()?;
|
||||
|
||||
match segment {
|
||||
Some(seg) => {
|
||||
let segment: PruneSegment = seg.into();
|
||||
match provider.get_prune_checkpoint(segment)? {
|
||||
Some(checkpoint) => print_checkpoint(segment, &checkpoint),
|
||||
None => println!("No checkpoint found for {segment}"),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut checkpoints = provider.get_prune_checkpoints()?;
|
||||
checkpoints.sort_by_key(|(seg, _)| *seg);
|
||||
if checkpoints.is_empty() {
|
||||
println!("No prune checkpoints found.");
|
||||
} else {
|
||||
println!(
|
||||
"{:<25} {:>15} {:>15} {:>20}",
|
||||
"Segment", "Block Number", "Tx Number", "Prune Mode"
|
||||
);
|
||||
println!("{}", "-".repeat(80));
|
||||
for (segment, checkpoint) in &checkpoints {
|
||||
println!(
|
||||
"{:<25} {:>15} {:>15} {:>20}",
|
||||
segment.to_string(),
|
||||
fmt_opt(checkpoint.block_number),
|
||||
fmt_opt(checkpoint.tx_number),
|
||||
fmt_mode(&checkpoint.prune_mode),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set<N: ProviderNodeTypes>(tool: &DbTool<N>, args: SetArgs) -> eyre::Result<()> {
|
||||
eyre::ensure!(
|
||||
args.block_number.is_some() || args.tx_number.is_some(),
|
||||
"at least one of --block-number or --tx-number must be provided"
|
||||
);
|
||||
|
||||
let prune_mode = match args.mode {
|
||||
PruneModeArg::Full => PruneMode::Full,
|
||||
PruneModeArg::Distance => PruneMode::Distance(
|
||||
args.mode_value
|
||||
.ok_or_else(|| eyre::eyre!("--mode-value is required for distance mode"))?,
|
||||
),
|
||||
PruneModeArg::Before => PruneMode::Before(
|
||||
args.mode_value
|
||||
.ok_or_else(|| eyre::eyre!("--mode-value is required for before mode"))?,
|
||||
),
|
||||
};
|
||||
|
||||
let segment: PruneSegment = args.segment.into();
|
||||
let checkpoint = PruneCheckpoint {
|
||||
block_number: args.block_number,
|
||||
tx_number: args.tx_number,
|
||||
prune_mode,
|
||||
};
|
||||
|
||||
let provider_rw = tool.provider_factory.database_provider_rw()?;
|
||||
|
||||
// Show previous value if any
|
||||
if let Some(prev) = provider_rw.get_prune_checkpoint(segment)? {
|
||||
println!("Previous checkpoint for {segment}:");
|
||||
print_checkpoint(segment, &prev);
|
||||
println!();
|
||||
}
|
||||
|
||||
provider_rw.save_prune_checkpoint(segment, checkpoint)?;
|
||||
provider_rw.commit()?;
|
||||
|
||||
println!("Updated checkpoint for {segment}:");
|
||||
print_checkpoint(segment, &checkpoint);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_checkpoint(segment: PruneSegment, checkpoint: &PruneCheckpoint) {
|
||||
println!(" Segment: {segment}");
|
||||
println!(" Block Number: {}", fmt_opt(checkpoint.block_number));
|
||||
println!(" Tx Number: {}", fmt_opt(checkpoint.tx_number));
|
||||
println!(" Prune Mode: {}", fmt_mode(&checkpoint.prune_mode));
|
||||
}
|
||||
|
||||
fn fmt_opt(val: Option<u64>) -> String {
|
||||
val.map_or("-".to_string(), |n| n.to_string())
|
||||
}
|
||||
|
||||
fn fmt_mode(mode: &PruneMode) -> String {
|
||||
match mode {
|
||||
PruneMode::Full => "Full".to_string(),
|
||||
PruneMode::Distance(d) => format!("Distance({d})"),
|
||||
PruneMode::Before(b) => format!("Before({b})"),
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use reth_cli_util::parse_socket_address;
|
||||
use reth_db_api::{
|
||||
cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO},
|
||||
database::Database,
|
||||
tables,
|
||||
transaction::{DbTx, DbTxMut},
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
@@ -21,13 +20,15 @@ use reth_node_metrics::{
|
||||
};
|
||||
use reth_provider::{providers::ProviderNodeTypes, ChainSpecProvider, StageCheckpointReader};
|
||||
use reth_stages::StageId;
|
||||
use reth_storage_api::StorageSettingsCache;
|
||||
use reth_tasks::TaskExecutor;
|
||||
use reth_trie::{
|
||||
verify::{Output, Verifier},
|
||||
Nibbles,
|
||||
};
|
||||
use reth_trie_common::{StorageTrieEntry, StoredNibbles, StoredNibblesSubKey};
|
||||
use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory};
|
||||
use reth_trie_db::{
|
||||
DatabaseHashedCursorFactory, DatabaseTrieCursorFactory, StorageTrieEntryLike, TrieTableAdapter,
|
||||
};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
time::{Duration, Instant},
|
||||
@@ -116,9 +117,13 @@ fn verify_only<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()> {
|
||||
let mut tx = db.tx()?;
|
||||
tx.disable_long_read_transaction_safety();
|
||||
|
||||
reth_trie_db::with_adapter!(tool.provider_factory, |A| do_verify_only::<_, A>(&tx))
|
||||
}
|
||||
|
||||
fn do_verify_only<TX: DbTx, A: TrieTableAdapter>(tx: &TX) -> eyre::Result<()> {
|
||||
// Create the verifier
|
||||
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(&tx);
|
||||
let trie_cursor_factory = DatabaseTrieCursorFactory::new(&tx);
|
||||
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(tx);
|
||||
let trie_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(tx);
|
||||
let verifier = Verifier::new(&trie_cursor_factory, hashed_cursor_factory)?;
|
||||
|
||||
let metrics = RepairTrieMetrics::new();
|
||||
@@ -209,17 +214,37 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
// Check that a pipeline sync isn't in progress.
|
||||
verify_checkpoints(provider_rw.as_ref())?;
|
||||
|
||||
let inconsistent_nodes = reth_trie_db::with_adapter!(tool.provider_factory, |A| {
|
||||
do_verify_and_repair::<_, A>(&mut provider_rw)?
|
||||
});
|
||||
|
||||
if inconsistent_nodes == 0 {
|
||||
info!("No inconsistencies found");
|
||||
} else {
|
||||
provider_rw.commit()?;
|
||||
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_verify_and_repair<N: ProviderNodeTypes, A: TrieTableAdapter>(
|
||||
provider_rw: &mut reth_provider::DatabaseProviderRW<N::DB, N>,
|
||||
) -> eyre::Result<usize>
|
||||
where
|
||||
<N::DB as reth_db_api::database::Database>::TXMut: DbTxMut + DbTx,
|
||||
{
|
||||
// Create cursors for making modifications with
|
||||
let tx = provider_rw.tx_mut();
|
||||
tx.disable_long_read_transaction_safety();
|
||||
let mut account_trie_cursor = tx.cursor_write::<tables::AccountsTrie>()?;
|
||||
let mut storage_trie_cursor = tx.cursor_dup_write::<tables::StoragesTrie>()?;
|
||||
let mut account_trie_cursor = tx.cursor_write::<A::AccountTrieTable>()?;
|
||||
let mut storage_trie_cursor = tx.cursor_dup_write::<A::StorageTrieTable>()?;
|
||||
|
||||
// Create the cursor factories. These cannot accept the `&mut` tx above because they require it
|
||||
// to be AsRef.
|
||||
// Create the cursor factories. These cannot accept the `&mut` tx above because they
|
||||
// require it to be AsRef.
|
||||
let tx = provider_rw.tx_ref();
|
||||
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(tx);
|
||||
let trie_cursor_factory = DatabaseTrieCursorFactory::new(tx);
|
||||
let trie_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(tx);
|
||||
|
||||
// Create the verifier
|
||||
let verifier = Verifier::new(&trie_cursor_factory, hashed_cursor_factory)?;
|
||||
@@ -257,17 +282,17 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
match output {
|
||||
Output::AccountExtra(path, _node) => {
|
||||
// Extra account node in trie, remove it
|
||||
let nibbles = StoredNibbles(path);
|
||||
if account_trie_cursor.seek_exact(nibbles)?.is_some() {
|
||||
let key: A::AccountKey = path.into();
|
||||
if account_trie_cursor.seek_exact(key)?.is_some() {
|
||||
account_trie_cursor.delete_current()?;
|
||||
}
|
||||
}
|
||||
Output::StorageExtra(account, path, _node) => {
|
||||
// Extra storage node in trie, remove it
|
||||
let nibbles = StoredNibblesSubKey(path);
|
||||
let subkey: A::StorageSubKey = path.into();
|
||||
if storage_trie_cursor
|
||||
.seek_by_key_subkey(account, nibbles.clone())?
|
||||
.filter(|e| e.nibbles == nibbles)
|
||||
.seek_by_key_subkey(account, subkey.clone())?
|
||||
.filter(|e| *e.nibbles() == subkey)
|
||||
.is_some()
|
||||
{
|
||||
storage_trie_cursor.delete_current()?;
|
||||
@@ -276,23 +301,23 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
Output::AccountWrong { path, expected: node, .. } |
|
||||
Output::AccountMissing(path, node) => {
|
||||
// Wrong/missing account node value, upsert it
|
||||
let nibbles = StoredNibbles(path);
|
||||
account_trie_cursor.upsert(nibbles, &node)?;
|
||||
let key: A::AccountKey = path.into();
|
||||
account_trie_cursor.upsert(key, &node)?;
|
||||
}
|
||||
Output::StorageWrong { account, path, expected: node, .. } |
|
||||
Output::StorageMissing(account, path, node) => {
|
||||
// Wrong/missing storage node value, upsert it
|
||||
// (We can't just use `upsert` method with a dup cursor, it's not properly
|
||||
// supported)
|
||||
let nibbles = StoredNibblesSubKey(path);
|
||||
let subkey: A::StorageSubKey = path.into();
|
||||
let entry = A::StorageValue::new(subkey.clone(), node);
|
||||
if storage_trie_cursor
|
||||
.seek_by_key_subkey(account, nibbles.clone())?
|
||||
.filter(|v| v.nibbles == nibbles)
|
||||
.seek_by_key_subkey(account, subkey.clone())?
|
||||
.filter(|v| *v.nibbles() == subkey)
|
||||
.is_some()
|
||||
{
|
||||
storage_trie_cursor.delete_current()?;
|
||||
}
|
||||
let entry = StorageTrieEntry { nibbles, node };
|
||||
storage_trie_cursor.upsert(account, &entry)?;
|
||||
}
|
||||
Output::Progress(path) => {
|
||||
@@ -304,14 +329,7 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
}
|
||||
}
|
||||
|
||||
if inconsistent_nodes == 0 {
|
||||
info!("No inconsistencies found");
|
||||
} else {
|
||||
provider_rw.commit()?;
|
||||
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(inconsistent_nodes as usize)
|
||||
}
|
||||
|
||||
/// Output progress information based on the last seen account path.
|
||||
|
||||
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]));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -44,11 +44,11 @@ pub struct ExportArgs {
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ExportEraCommand<C> {
|
||||
/// Execute `export-era` command
|
||||
pub async fn execute<N>(self) -> eyre::Result<()>
|
||||
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
{
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
|
||||
|
||||
// Either specified path or default to `<data-dir>/<chain>/era1-export/`
|
||||
let data_dir = match &self.export.path {
|
||||
|
||||
@@ -47,6 +47,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
pub async fn execute<N, Comp>(
|
||||
self,
|
||||
components: impl FnOnce(Arc<N::ChainSpec>) -> Comp,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
@@ -54,7 +55,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
{
|
||||
info!(target: "reth::cli", "reth {} starting", version_metadata().short_version);
|
||||
|
||||
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { provider_factory, config, .. } =
|
||||
self.env.init::<N>(AccessRights::RW, runtime.clone())?;
|
||||
|
||||
let components = components(provider_factory.chain_spec());
|
||||
|
||||
@@ -85,6 +87,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
&config,
|
||||
executor.clone(),
|
||||
consensus.clone(),
|
||||
runtime.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ pub async fn import_blocks_from_file<N>(
|
||||
config: &Config,
|
||||
executor: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
consensus: Arc<impl FullConsensus<N::Primitives> + 'static>,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<ImportResult>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
@@ -139,7 +140,7 @@ where
|
||||
total_decoded_blocks += file_client.headers_len();
|
||||
total_decoded_txns += file_client.total_transactions();
|
||||
|
||||
let (mut pipeline, events, _runtime) = build_import_pipeline_impl(
|
||||
let (mut pipeline, events) = build_import_pipeline_impl(
|
||||
config,
|
||||
provider_factory.clone(),
|
||||
&consensus,
|
||||
@@ -147,6 +148,7 @@ where
|
||||
static_file_producer.clone(),
|
||||
import_config.no_state,
|
||||
executor.clone(),
|
||||
runtime.clone(),
|
||||
)?;
|
||||
|
||||
// override the tip
|
||||
@@ -257,6 +259,7 @@ where
|
||||
///
|
||||
/// If configured to execute, all stages will run. Otherwise, only stages that don't require state
|
||||
/// will run.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn build_import_pipeline_impl<N, C, E>(
|
||||
config: &Config,
|
||||
provider_factory: ProviderFactory<N>,
|
||||
@@ -265,11 +268,8 @@ pub fn build_import_pipeline_impl<N, C, E>(
|
||||
static_file_producer: StaticFileProducer<ProviderFactory<N>>,
|
||||
disable_exec: bool,
|
||||
evm_config: E,
|
||||
) -> eyre::Result<(
|
||||
Pipeline<N>,
|
||||
impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>,
|
||||
reth_tasks::Runtime,
|
||||
)>
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<(Pipeline<N>, impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>)>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
C: FullConsensus<N::Primitives> + 'static,
|
||||
@@ -285,9 +285,6 @@ where
|
||||
.sealed_header(last_block_number)?
|
||||
.ok_or_else(|| ProviderError::HeaderNotFound(last_block_number.into()))?;
|
||||
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())
|
||||
.expect("failed to create runtime");
|
||||
|
||||
let mut header_downloader = ReverseHeadersDownloaderBuilder::new(config.stages.headers)
|
||||
.build(file_client.clone(), consensus.clone())
|
||||
.into_task_with(&runtime);
|
||||
@@ -333,5 +330,5 @@ where
|
||||
|
||||
let events = pipeline.events().map(Into::into);
|
||||
|
||||
Ok((pipeline, events, runtime))
|
||||
Ok((pipeline, events))
|
||||
}
|
||||
|
||||
@@ -64,13 +64,14 @@ impl TryFromChain for ChainKind {
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportEraCommand<C> {
|
||||
/// Execute `import-era` command
|
||||
pub async fn execute<N>(self) -> eyre::Result<()>
|
||||
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
{
|
||||
info!(target: "reth::cli", "reth {} starting", version_metadata().short_version);
|
||||
|
||||
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { provider_factory, config, .. } =
|
||||
self.env.init::<N>(AccessRights::RW, runtime)?;
|
||||
|
||||
let mut hash_collector = Collector::new(config.stages.etl.file_size, config.stages.etl.dir);
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ pub struct InitCommand<C: ChainSpecParser> {
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitCommand<C> {
|
||||
/// Execute the `init` command
|
||||
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(self) -> eyre::Result<()> {
|
||||
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(
|
||||
self,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()> {
|
||||
info!(target: "reth::cli", "reth init starting");
|
||||
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW, runtime)?;
|
||||
|
||||
let genesis_block_number = provider_factory.chain_spec().genesis_header().number();
|
||||
let hash = provider_factory
|
||||
|
||||
@@ -65,7 +65,7 @@ pub struct InitStateCommand<C: ChainSpecParser> {
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateCommand<C> {
|
||||
/// Execute the `init` command
|
||||
pub async fn execute<N>(self) -> eyre::Result<()>
|
||||
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<
|
||||
ChainSpec = C::ChainSpec,
|
||||
@@ -74,7 +74,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateC
|
||||
{
|
||||
info!(target: "reth::cli", "Reth init-state starting");
|
||||
|
||||
let Environment { config, provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { config, provider_factory, .. } =
|
||||
self.env.init::<N>(AccessRights::RW, runtime)?;
|
||||
|
||||
let static_file_provider = provider_factory.static_file_provider();
|
||||
let provider_rw = provider_factory.database_provider_rw()?;
|
||||
|
||||
@@ -16,6 +16,7 @@ use reth_node_core::{
|
||||
args::{DatadirArgs, NetworkArgs},
|
||||
utils::get_single_header,
|
||||
};
|
||||
use reth_tasks::Runtime;
|
||||
|
||||
pub mod bootnode;
|
||||
pub mod enode;
|
||||
@@ -194,17 +195,18 @@ impl<C: ChainSpecParser> DownloadArgs<C> {
|
||||
let rlpx_socket = (self.network.addr, self.network.port).into();
|
||||
let boot_nodes = self.chain.bootnodes().unwrap_or_default();
|
||||
|
||||
let net = NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key)
|
||||
.peer_config(config.peers_config_with_basic_nodes_from_file(None))
|
||||
.external_ip_resolver(self.network.nat.clone())
|
||||
.network_id(self.network.network_id)
|
||||
.boot_nodes(boot_nodes.clone())
|
||||
.apply(|builder| {
|
||||
self.network.discovery.apply_to_builder(builder, rlpx_socket, boot_nodes)
|
||||
})
|
||||
.build_with_noop_provider(self.chain.clone())
|
||||
.manager()
|
||||
.await?;
|
||||
let net =
|
||||
NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key, Runtime::test())
|
||||
.peer_config(config.peers_config_with_basic_nodes_from_file(None))
|
||||
.external_ip_resolver(self.network.nat.clone())
|
||||
.network_id(self.network.network_id)
|
||||
.boot_nodes(boot_nodes.clone())
|
||||
.apply(|builder| {
|
||||
self.network.discovery.apply_to_builder(builder, rlpx_socket, boot_nodes)
|
||||
})
|
||||
.build_with_noop_provider(self.chain.clone())
|
||||
.manager()
|
||||
.await?;
|
||||
let handle = net.handle().clone();
|
||||
tokio::task::spawn(net);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> PruneComma
|
||||
self,
|
||||
ctx: CliContext,
|
||||
) -> eyre::Result<()> {
|
||||
let env = self.env.init::<N>(AccessRights::RW)?;
|
||||
let env = self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
|
||||
let provider_factory = env.provider_factory;
|
||||
let config = env.config.prune;
|
||||
let data_dir = env.data_dir;
|
||||
|
||||
@@ -20,7 +20,10 @@ use reth_provider::{
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_stages::stages::calculate_gas_used_from_headers;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{sync::mpsc, task::JoinSet};
|
||||
@@ -46,6 +49,10 @@ pub struct Command<C: ChainSpecParser> {
|
||||
#[arg(long)]
|
||||
num_tasks: Option<u64>,
|
||||
|
||||
/// Number of blocks each worker processes before grabbing the next chunk.
|
||||
#[arg(long, default_value = "5000")]
|
||||
blocks_per_chunk: u64,
|
||||
|
||||
/// Continues with execution when an invalid block is encountered and collects these blocks.
|
||||
#[arg(long)]
|
||||
skip_invalid_blocks: bool,
|
||||
@@ -60,11 +67,15 @@ impl<C: ChainSpecParser> Command<C> {
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
|
||||
/// Execute `re-execute` command
|
||||
pub async fn execute<N>(self, components: impl CliComponentsBuilder<N>) -> eyre::Result<()>
|
||||
pub async fn execute<N>(
|
||||
self,
|
||||
components: impl CliComponentsBuilder<N>,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
{
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
|
||||
|
||||
let components = components(provider_factory.chain_spec());
|
||||
|
||||
@@ -88,12 +99,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
|
||||
});
|
||||
|
||||
let total_blocks = max_block - min_block;
|
||||
let total_gas = calculate_gas_used_from_headers(
|
||||
&provider_factory.static_file_provider(),
|
||||
min_block..=max_block,
|
||||
)?;
|
||||
let blocks_per_task = total_blocks / num_tasks;
|
||||
|
||||
let db_at = {
|
||||
let provider_factory = provider_factory.clone();
|
||||
@@ -105,18 +114,17 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
};
|
||||
|
||||
let skip_invalid_blocks = self.skip_invalid_blocks;
|
||||
let blocks_per_chunk = self.blocks_per_chunk;
|
||||
let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
|
||||
let (info_tx, mut info_rx) = mpsc::unbounded_channel();
|
||||
let cancellation = CancellationToken::new();
|
||||
let _guard = cancellation.drop_guard();
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
for i in 0..num_tasks {
|
||||
let start_block = min_block + i * blocks_per_task;
|
||||
let end_block =
|
||||
if i == num_tasks - 1 { max_block } else { start_block + blocks_per_task };
|
||||
// Shared counter for work stealing: workers atomically grab the next chunk of blocks.
|
||||
let next_block = Arc::new(AtomicU64::new(min_block));
|
||||
|
||||
// Spawn thread executing blocks
|
||||
let mut tasks = JoinSet::new();
|
||||
for _ in 0..num_tasks {
|
||||
let provider_factory = provider_factory.clone();
|
||||
let evm_config = components.evm_config().clone();
|
||||
let consensus = components.consensus().clone();
|
||||
@@ -124,95 +132,122 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let stats_tx = stats_tx.clone();
|
||||
let info_tx = info_tx.clone();
|
||||
let cancellation = cancellation.clone();
|
||||
let next_block = Arc::clone(&next_block);
|
||||
tasks.spawn_blocking(move || {
|
||||
let mut executor = evm_config.batch_executor(db_at(start_block - 1));
|
||||
let mut executor_created = Instant::now();
|
||||
let executor_lifetime = Duration::from_secs(120);
|
||||
|
||||
'blocks: for block in start_block..end_block {
|
||||
loop {
|
||||
if cancellation.is_cancelled() {
|
||||
// exit if the program is being terminated
|
||||
break
|
||||
break;
|
||||
}
|
||||
|
||||
let block = provider_factory
|
||||
.recovered_block(block.into(), TransactionVariant::NoHash)?
|
||||
.unwrap();
|
||||
// Atomically grab the next chunk of blocks.
|
||||
let chunk_start =
|
||||
next_block.fetch_add(blocks_per_chunk, Ordering::Relaxed);
|
||||
if chunk_start >= max_block {
|
||||
break;
|
||||
}
|
||||
let chunk_end = (chunk_start + blocks_per_chunk).min(max_block);
|
||||
|
||||
let result = match executor.execute_one(&block) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
if skip_invalid_blocks {
|
||||
executor = evm_config.batch_executor(db_at(block.number()));
|
||||
let _ = info_tx.send((block, eyre::Report::new(err)));
|
||||
continue
|
||||
}
|
||||
return Err(err.into())
|
||||
let mut executor = evm_config.batch_executor(db_at(chunk_start - 1));
|
||||
let mut executor_created = Instant::now();
|
||||
|
||||
'blocks: for block in chunk_start..chunk_end {
|
||||
if cancellation.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = consensus
|
||||
.validate_block_post_execution(&block, &result, None)
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to validate block {} {}", block.number(), block.hash())
|
||||
})
|
||||
{
|
||||
let correct_receipts =
|
||||
provider_factory.receipts_by_block(block.number().into())?.unwrap();
|
||||
let block = provider_factory
|
||||
.recovered_block(block.into(), TransactionVariant::NoHash)?
|
||||
.unwrap();
|
||||
|
||||
for (i, (receipt, correct_receipt)) in
|
||||
result.receipts.iter().zip(correct_receipts.iter()).enumerate()
|
||||
{
|
||||
if receipt != correct_receipt {
|
||||
let tx_hash = block.body().transactions()[i].tx_hash();
|
||||
error!(
|
||||
?receipt,
|
||||
?correct_receipt,
|
||||
index = i,
|
||||
?tx_hash,
|
||||
"Invalid receipt"
|
||||
);
|
||||
let expected_gas_used = correct_receipt.cumulative_gas_used() -
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
correct_receipts[i - 1].cumulative_gas_used()
|
||||
};
|
||||
let got_gas_used = receipt.cumulative_gas_used() -
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
result.receipts[i - 1].cumulative_gas_used()
|
||||
};
|
||||
if got_gas_used != expected_gas_used {
|
||||
let mismatch = GotExpected {
|
||||
expected: expected_gas_used,
|
||||
got: got_gas_used,
|
||||
};
|
||||
|
||||
error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
|
||||
if skip_invalid_blocks {
|
||||
executor = evm_config.batch_executor(db_at(block.number()));
|
||||
let _ = info_tx.send((block, err));
|
||||
continue 'blocks;
|
||||
}
|
||||
return Err(err);
|
||||
let result = match executor.execute_one(&block) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
if skip_invalid_blocks {
|
||||
executor =
|
||||
evm_config.batch_executor(db_at(block.number()));
|
||||
let _ =
|
||||
info_tx.send((block, eyre::Report::new(err)));
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
return Err(err.into())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = consensus
|
||||
.validate_block_post_execution(&block, &result, None)
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to validate block {} {}",
|
||||
block.number(),
|
||||
block.hash()
|
||||
)
|
||||
})
|
||||
{
|
||||
let correct_receipts = provider_factory
|
||||
.receipts_by_block(block.number().into())?
|
||||
.unwrap();
|
||||
|
||||
for (i, (receipt, correct_receipt)) in
|
||||
result.receipts.iter().zip(correct_receipts.iter()).enumerate()
|
||||
{
|
||||
if receipt != correct_receipt {
|
||||
let tx_hash =
|
||||
block.body().transactions()[i].tx_hash();
|
||||
error!(
|
||||
?receipt,
|
||||
?correct_receipt,
|
||||
index = i,
|
||||
?tx_hash,
|
||||
"Invalid receipt"
|
||||
);
|
||||
let expected_gas_used =
|
||||
correct_receipt.cumulative_gas_used() -
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
correct_receipts[i - 1]
|
||||
.cumulative_gas_used()
|
||||
};
|
||||
let got_gas_used = receipt.cumulative_gas_used() -
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
result.receipts[i - 1].cumulative_gas_used()
|
||||
};
|
||||
if got_gas_used != expected_gas_used {
|
||||
let mismatch = GotExpected {
|
||||
expected: expected_gas_used,
|
||||
got: got_gas_used,
|
||||
};
|
||||
|
||||
error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
|
||||
if skip_invalid_blocks {
|
||||
executor = evm_config
|
||||
.batch_executor(db_at(block.number()));
|
||||
let _ = info_tx.send((block, err));
|
||||
continue 'blocks;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
let _ = stats_tx.send(block.gas_used());
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
let _ = stats_tx.send(block.gas_used());
|
||||
|
||||
// Reset DB once in a while to avoid OOM or read tx timeouts
|
||||
if executor.size_hint() > 1_000_000 ||
|
||||
executor_created.elapsed() > executor_lifetime
|
||||
{
|
||||
executor = evm_config.batch_executor(db_at(block.number()));
|
||||
executor_created = Instant::now();
|
||||
// Reset DB once in a while to avoid OOM or read tx timeouts
|
||||
if executor.size_hint() > 1_000_000 ||
|
||||
executor_created.elapsed() > executor_lifetime
|
||||
{
|
||||
executor =
|
||||
evm_config.batch_executor(db_at(block.number()));
|
||||
executor_created = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ pub struct Command<C: ChainSpecParser> {
|
||||
|
||||
impl<C: ChainSpecParser> Command<C> {
|
||||
/// Execute `db` command
|
||||
pub async fn execute<N: CliNodeTypes>(self) -> eyre::Result<()>
|
||||
pub async fn execute<N: CliNodeTypes>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
|
||||
where
|
||||
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
|
||||
{
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW, runtime)?;
|
||||
|
||||
let tool = DbTool::new(provider_factory)?;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use reth_stages::{stages::ExecutionStage, Stage, StageCheckpoint, UnwindInput};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn dump_execution_stage<N, E, C>(
|
||||
db_tool: &DbTool<N>,
|
||||
from: u64,
|
||||
@@ -24,6 +25,7 @@ pub(crate) async fn dump_execution_stage<N, E, C>(
|
||||
should_run: bool,
|
||||
evm_config: E,
|
||||
consensus: C,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N: ProviderNodeTypes<DB = DatabaseEnv>,
|
||||
@@ -37,7 +39,6 @@ where
|
||||
unwind_and_copy(db_tool, from, tip_block_number, &output_db, evm_config.clone())?;
|
||||
|
||||
if should_run {
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
dry_run(
|
||||
ProviderFactory::<N>::new(
|
||||
output_db,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Databas
|
||||
to: BlockNumber,
|
||||
output_datadir: ChainPath<DataDirPath>,
|
||||
should_run: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> Result<()> {
|
||||
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
|
||||
|
||||
@@ -33,7 +34,6 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Databas
|
||||
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
|
||||
|
||||
if should_run {
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
dry_run(
|
||||
ProviderFactory::<N>::new(
|
||||
output_db,
|
||||
|
||||
@@ -17,13 +17,13 @@ pub(crate) async fn dump_hashing_storage_stage<N: ProviderNodeTypes<DB = Databas
|
||||
to: u64,
|
||||
output_datadir: ChainPath<DataDirPath>,
|
||||
should_run: bool,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> Result<()> {
|
||||
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
|
||||
|
||||
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
|
||||
|
||||
if should_run {
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
dry_run(
|
||||
ProviderFactory::<N>::new(
|
||||
output_db,
|
||||
|
||||
@@ -24,6 +24,7 @@ use reth_stages::{
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn dump_merkle_stage<N>(
|
||||
db_tool: &DbTool<N>,
|
||||
from: BlockNumber,
|
||||
@@ -32,6 +33,7 @@ pub(crate) async fn dump_merkle_stage<N>(
|
||||
should_run: bool,
|
||||
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
|
||||
consensus: impl FullConsensus<N::Primitives> + 'static,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> Result<()>
|
||||
where
|
||||
N: ProviderNodeTypes<DB = DatabaseEnv>,
|
||||
@@ -57,7 +59,6 @@ where
|
||||
unwind_and_copy(db_tool, (from, to), tip_block_number, &output_db, evm_config, consensus)?;
|
||||
|
||||
if should_run {
|
||||
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
dry_run(
|
||||
ProviderFactory::<N>::new(
|
||||
output_db,
|
||||
|
||||
@@ -72,30 +72,36 @@ pub struct StageCommand {
|
||||
}
|
||||
|
||||
macro_rules! handle_stage {
|
||||
($stage_fn:ident, $tool:expr, $command:expr) => {{
|
||||
($stage_fn:ident, $tool:expr, $command:expr, $runtime:expr) => {{
|
||||
let StageCommand { output_datadir, from, to, dry_run, .. } = $command;
|
||||
let output_datadir =
|
||||
output_datadir.with_chain($tool.chain().chain(), DatadirArgs::default());
|
||||
$stage_fn($tool, *from, *to, output_datadir, *dry_run).await?
|
||||
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $runtime).await?
|
||||
}};
|
||||
|
||||
($stage_fn:ident, $tool:expr, $command:expr, $executor:expr, $consensus:expr) => {{
|
||||
($stage_fn:ident, $tool:expr, $command:expr, $executor:expr, $consensus:expr, $runtime:expr) => {{
|
||||
let StageCommand { output_datadir, from, to, dry_run, .. } = $command;
|
||||
let output_datadir =
|
||||
output_datadir.with_chain($tool.chain().chain(), DatadirArgs::default());
|
||||
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $executor, $consensus).await?
|
||||
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $executor, $consensus, $runtime)
|
||||
.await?
|
||||
}};
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
|
||||
/// Execute `dump-stage` command
|
||||
pub async fn execute<N, Comp, F>(self, components: F) -> eyre::Result<()>
|
||||
pub async fn execute<N, Comp, F>(
|
||||
self,
|
||||
components: F,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
Comp: CliNodeComponents<N>,
|
||||
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
|
||||
{
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
|
||||
let Environment { provider_factory, .. } =
|
||||
self.env.init::<N>(AccessRights::RO, runtime.clone())?;
|
||||
let tool = DbTool::new(provider_factory)?;
|
||||
let components = components(tool.chain());
|
||||
let evm_config = components.evm_config().clone();
|
||||
@@ -103,12 +109,23 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
|
||||
match &self.command {
|
||||
Stages::Execution(cmd) => {
|
||||
handle_stage!(dump_execution_stage, &tool, cmd, evm_config, consensus)
|
||||
handle_stage!(
|
||||
dump_execution_stage,
|
||||
&tool,
|
||||
cmd,
|
||||
evm_config,
|
||||
consensus,
|
||||
runtime.clone()
|
||||
)
|
||||
}
|
||||
Stages::StorageHashing(cmd) => {
|
||||
handle_stage!(dump_hashing_storage_stage, &tool, cmd, runtime.clone())
|
||||
}
|
||||
Stages::AccountHashing(cmd) => {
|
||||
handle_stage!(dump_hashing_account_stage, &tool, cmd, runtime.clone())
|
||||
}
|
||||
Stages::StorageHashing(cmd) => handle_stage!(dump_hashing_storage_stage, &tool, cmd),
|
||||
Stages::AccountHashing(cmd) => handle_stage!(dump_hashing_account_stage, &tool, cmd),
|
||||
Stages::Merkle(cmd) => {
|
||||
handle_stage!(dump_merkle_stage, &tool, cmd, evm_config, consensus)
|
||||
handle_stage!(dump_merkle_stage, &tool, cmd, evm_config, consensus, runtime.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,11 +49,12 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
|
||||
Comp: CliNodeComponents<N>,
|
||||
{
|
||||
let executor = ctx.task_executor.clone();
|
||||
match self.command {
|
||||
Subcommands::Run(command) => command.execute::<N, _, _>(ctx, components).await,
|
||||
Subcommands::Drop(command) => command.execute::<N>().await,
|
||||
Subcommands::Dump(command) => command.execute::<N, _, _>(components).await,
|
||||
Subcommands::Unwind(command) => command.execute::<N, _, _>(components).await,
|
||||
Subcommands::Drop(command) => command.execute::<N>(executor).await,
|
||||
Subcommands::Dump(command) => command.execute::<N, _, _>(components, executor).await,
|
||||
Subcommands::Unwind(command) => command.execute::<N, _, _>(components, executor).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
// Does not do anything on windows.
|
||||
let _ = fdlimit::raise_fd_limit();
|
||||
|
||||
let runtime = ctx.task_executor.clone();
|
||||
let Environment { provider_factory, config, data_dir } =
|
||||
self.env.init::<N>(AccessRights::RW)?;
|
||||
self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
|
||||
|
||||
let mut provider_rw = provider_factory.database_provider_rw()?;
|
||||
let components = components(provider_factory.chain_spec());
|
||||
@@ -171,6 +172,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
provider_factory.chain_spec(),
|
||||
p2p_secret_key,
|
||||
default_peers_path,
|
||||
runtime.clone(),
|
||||
)
|
||||
.build(provider_factory.clone())
|
||||
.start_network()
|
||||
@@ -226,6 +228,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
provider_factory.chain_spec(),
|
||||
p2p_secret_key,
|
||||
default_peers_path,
|
||||
runtime.clone(),
|
||||
)
|
||||
.build(provider_factory.clone())
|
||||
.start_network()
|
||||
|
||||
@@ -46,12 +46,14 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>, F, Comp>(
|
||||
self,
|
||||
components: F,
|
||||
runtime: reth_tasks::Runtime,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
Comp: CliNodeComponents<N>,
|
||||
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
|
||||
{
|
||||
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
let Environment { provider_factory, config, data_dir: _ } =
|
||||
self.env.init::<N>(AccessRights::RW, runtime)?;
|
||||
|
||||
let target = self.command.unwind_target(provider_factory.clone())?;
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ impl CliRunner {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a clone of the underlying [`Runtime`](reth_tasks::Runtime).
|
||||
pub fn runtime(&self) -> reth_tasks::Runtime {
|
||||
self.runtime.clone()
|
||||
}
|
||||
|
||||
/// Executes an async block on the runtime and blocks until completion.
|
||||
pub fn block_on<F, T>(&self, fut: F) -> T
|
||||
where
|
||||
|
||||
@@ -19,6 +19,7 @@ reth-consensus.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eips.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-primitives = { workspace = true, features = ["rand"] }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
|
||||
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
|
||||
use alloy_primitives::B256;
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
|
||||
use reth_consensus::ConsensusError;
|
||||
use reth_primitives_traits::{
|
||||
@@ -86,6 +87,27 @@ pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), Conse
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that Amsterdam header fields are present in the block.
|
||||
///
|
||||
/// This checks that the `block_access_list_hash` and `slot_number` are set in the header,
|
||||
/// as required post-Amsterdam.
|
||||
///
|
||||
/// See [EIP-7928]: Block-level Access Lists
|
||||
/// See [EIP-7778]: Slot Number in Block Header
|
||||
///
|
||||
/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
|
||||
/// [EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778
|
||||
#[inline]
|
||||
pub fn validate_amsterdam_header_fields<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
|
||||
if header.block_access_list_hash().is_none() {
|
||||
return Err(ConsensusError::BlockAccessListHashMissing);
|
||||
}
|
||||
if header.slot_number().is_none() {
|
||||
return Err(ConsensusError::SlotNumberMissing);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures the block response data matches the header.
|
||||
///
|
||||
/// This ensures the body response items match the header's hashes:
|
||||
@@ -141,6 +163,27 @@ pub fn validate_block_pre_execution<B, ChainSpec>(
|
||||
block: &SealedBlock<B>,
|
||||
chain_spec: &ChainSpec,
|
||||
) -> Result<(), ConsensusError>
|
||||
where
|
||||
B: Block,
|
||||
ChainSpec: EthChainSpec + EthereumHardforks,
|
||||
{
|
||||
validate_block_pre_execution_with_tx_root(block, chain_spec, None)
|
||||
}
|
||||
|
||||
/// Validate a block without regard for state using an optional pre-computed transaction root.
|
||||
///
|
||||
/// - Compares the ommer hash in the block header to the block body
|
||||
/// - Compares the transactions root in the block header to the block body
|
||||
/// - Pre-execution transaction validation
|
||||
///
|
||||
/// If `transaction_root` is provided, it is used instead of recomputing the transaction trie
|
||||
/// root from the block body. The caller must ensure this value was derived from
|
||||
/// `block.body().calculate_tx_root()`.
|
||||
pub fn validate_block_pre_execution_with_tx_root<B, ChainSpec>(
|
||||
block: &SealedBlock<B>,
|
||||
chain_spec: &ChainSpec,
|
||||
transaction_root: Option<B256>,
|
||||
) -> Result<(), ConsensusError>
|
||||
where
|
||||
B: Block,
|
||||
ChainSpec: EthChainSpec + EthereumHardforks,
|
||||
@@ -148,8 +191,14 @@ where
|
||||
post_merge_hardfork_fields(block, chain_spec)?;
|
||||
|
||||
// Check transaction root
|
||||
if let Err(error) = block.ensure_transaction_root_valid() {
|
||||
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
|
||||
let expected_transaction_root = block.header().transactions_root();
|
||||
let calculated_transaction_root =
|
||||
transaction_root.unwrap_or_else(|| block.body().calculate_tx_root());
|
||||
if calculated_transaction_root != expected_transaction_root {
|
||||
return Err(ConsensusError::BodyTransactionRootDiff(
|
||||
GotExpected { got: calculated_transaction_root, expected: expected_transaction_root }
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -426,7 +475,7 @@ pub fn validate_against_parent_4844<H: BlockHeader>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_consensus::{BlockBody, Header, TxEip4844};
|
||||
use alloy_eips::eip4895::Withdrawals;
|
||||
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip4895::Withdrawals};
|
||||
use alloy_primitives::{Address, Bytes, Signature, U256};
|
||||
use rand::Rng;
|
||||
use reth_chainspec::ChainSpecBuilder;
|
||||
@@ -507,4 +556,66 @@ mod tests {
|
||||
// Test with custom larger limit - should pass
|
||||
assert!(validate_header_extra_data(&header_33, 64).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precomputed_tx_root_correct_passes() {
|
||||
let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
|
||||
|
||||
let transaction = mock_blob_tx(1, 1);
|
||||
let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
|
||||
|
||||
let header = Header {
|
||||
base_fee_per_gas: Some(1337),
|
||||
withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
|
||||
transactions_root: tx_root,
|
||||
blob_gas_used: Some(DATA_GAS_PER_BLOB),
|
||||
excess_blob_gas: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
let body = BlockBody {
|
||||
transactions: vec![transaction],
|
||||
ommers: vec![],
|
||||
withdrawals: Some(Withdrawals::default()),
|
||||
};
|
||||
|
||||
let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
|
||||
|
||||
// Some(correct_root) should pass just like None
|
||||
assert!(
|
||||
validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(tx_root)).is_ok()
|
||||
);
|
||||
assert!(validate_block_pre_execution_with_tx_root(&block, &chain_spec, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precomputed_tx_root_wrong_fails() {
|
||||
let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
|
||||
|
||||
let transaction = mock_blob_tx(1, 1);
|
||||
let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
|
||||
|
||||
let header = Header {
|
||||
base_fee_per_gas: Some(1337),
|
||||
withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
|
||||
transactions_root: tx_root,
|
||||
blob_gas_used: Some(DATA_GAS_PER_BLOB),
|
||||
excess_blob_gas: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
let body = BlockBody {
|
||||
transactions: vec![transaction],
|
||||
ommers: vec![],
|
||||
withdrawals: Some(Withdrawals::default()),
|
||||
};
|
||||
|
||||
let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
|
||||
|
||||
let wrong_root = B256::repeat_byte(0xff);
|
||||
assert!(matches!(
|
||||
validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(wrong_root))
|
||||
.unwrap_err(),
|
||||
ConsensusError::BodyTransactionRootDiff(diff)
|
||||
if diff.0.got == wrong_root && diff.0.expected == tx_root
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ use core::error::Error;
|
||||
/// When provided to [`FullConsensus::validate_block_post_execution`], this allows skipping
|
||||
/// the receipt root computation and using the pre-computed values instead.
|
||||
pub type ReceiptRootBloom = (B256, Bloom);
|
||||
|
||||
/// Pre-computed transaction root.
|
||||
///
|
||||
/// When provided to [`Consensus::validate_block_pre_execution_with_tx_root`], this allows
|
||||
/// skipping transaction trie reconstruction from the block body.
|
||||
pub type TransactionRoot = B256;
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{
|
||||
constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
|
||||
@@ -78,6 +84,22 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
|
||||
///
|
||||
/// Note: validating blocks does not include other validations of the Consensus
|
||||
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
|
||||
|
||||
/// Validate a block disregarding world state using an optional pre-computed transaction root.
|
||||
///
|
||||
/// If `transaction_root` is provided, the implementation should use the pre-computed
|
||||
/// transaction root instead of recomputing it from the block body. The value must have been
|
||||
/// derived from `block.body().calculate_tx_root()`.
|
||||
///
|
||||
/// By default this falls back to [`Self::validate_block_pre_execution`].
|
||||
fn validate_block_pre_execution_with_tx_root(
|
||||
&self,
|
||||
block: &SealedBlock<B>,
|
||||
transaction_root: Option<TransactionRoot>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
let _ = transaction_root;
|
||||
self.validate_block_pre_execution(block)
|
||||
}
|
||||
}
|
||||
|
||||
/// `HeaderValidator` is a protocol that validates headers and their relationships.
|
||||
@@ -309,6 +331,26 @@ pub enum ConsensusError {
|
||||
#[error("unexpected parent beacon block root")]
|
||||
ParentBeaconBlockRootUnexpected,
|
||||
|
||||
/// Error when the block access list hash is missing.
|
||||
#[error("missing block access list hash")]
|
||||
BlockAccessListHashMissing,
|
||||
|
||||
/// Error when an unexpected block access list hash is encountered.
|
||||
#[error("unexpected block access list hash")]
|
||||
BlockAccessListHashUnexpected,
|
||||
|
||||
/// Error when the block access list hash doesn't match the expected value.
|
||||
#[error("block access list hash mismatch: {0}")]
|
||||
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
|
||||
|
||||
/// Error when the slot number is missing.
|
||||
#[error("missing slot number")]
|
||||
SlotNumberMissing,
|
||||
|
||||
/// Error when an unexpected slot number is encountered.
|
||||
#[error("unexpected slot number")]
|
||||
SlotNumberUnexpected,
|
||||
|
||||
/// Error when blob gas used exceeds the maximum allowed.
|
||||
#[error("blob gas used {blob_gas_used} exceeds maximum allowance {max_blob_gas_per_block}")]
|
||||
BlobGasUsedExceedsMaxBlobGasPerBlock {
|
||||
|
||||
@@ -38,7 +38,6 @@ reth-ethereum-primitives.workspace = true
|
||||
reth-cli-commands.workspace = true
|
||||
reth-config.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
reth-primitives.workspace = true
|
||||
reth-db-common.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ where
|
||||
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
|
||||
Wallet,
|
||||
)> {
|
||||
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
let runtime = Runtime::test();
|
||||
|
||||
let network_config = NetworkArgs {
|
||||
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
|
||||
|
||||
@@ -15,7 +15,6 @@ use reth_provider::{
|
||||
};
|
||||
use reth_rpc_server_types::RpcModuleSelection;
|
||||
use reth_stages_types::StageId;
|
||||
use reth_tasks::Runtime;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use tempfile::TempDir;
|
||||
use tracing::{debug, info, span, Level};
|
||||
@@ -66,7 +65,7 @@ pub async fn setup_engine_with_chain_import(
|
||||
+ Copy
|
||||
+ 'static,
|
||||
) -> eyre::Result<ChainImportResult> {
|
||||
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
|
||||
let network_config = NetworkArgs {
|
||||
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
|
||||
@@ -149,6 +148,7 @@ pub async fn setup_engine_with_chain_import(
|
||||
&config,
|
||||
evm_config,
|
||||
consensus,
|
||||
runtime.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -275,8 +275,9 @@ mod tests {
|
||||
use crate::test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp};
|
||||
use reth_chainspec::{ChainSpecBuilder, MAINNET};
|
||||
use reth_db::mdbx::DatabaseArguments;
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_payload_builder::EthPayloadBuilderAttributes;
|
||||
use reth_primitives::SealedBlock;
|
||||
use reth_primitives_traits::SealedBlock;
|
||||
use reth_provider::{
|
||||
test_utils::MockNodeTypesWithDB, BlockHashReader, BlockNumReader, BlockReaderIdExt,
|
||||
};
|
||||
@@ -343,6 +344,7 @@ mod tests {
|
||||
let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
|
||||
// Use NoopConsensus to skip gas limit validation for test imports
|
||||
let consensus = reth_consensus::noop::NoopConsensus::arc();
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
|
||||
let result = import_blocks_from_file(
|
||||
&rlp_path,
|
||||
@@ -351,6 +353,7 @@ mod tests {
|
||||
&config,
|
||||
evm_config,
|
||||
consensus,
|
||||
runtime,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -446,7 +449,7 @@ mod tests {
|
||||
chain_spec: &ChainSpec,
|
||||
block_count: u64,
|
||||
temp_dir: &Path,
|
||||
) -> (Vec<SealedBlock>, PathBuf) {
|
||||
) -> (Vec<SealedBlock<Block>>, PathBuf) {
|
||||
let test_blocks = generate_test_blocks(chain_spec, block_count);
|
||||
assert_eq!(
|
||||
test_blocks.len(),
|
||||
@@ -509,6 +512,7 @@ mod tests {
|
||||
let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
|
||||
// Use NoopConsensus to skip gas limit validation for test imports
|
||||
let consensus = reth_consensus::noop::NoopConsensus::arc();
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
|
||||
let result = import_blocks_from_file(
|
||||
&rlp_path,
|
||||
@@ -517,6 +521,7 @@ mod tests {
|
||||
&config,
|
||||
evm_config,
|
||||
consensus,
|
||||
runtime,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -6,14 +6,13 @@ use alloy_primitives::{Address, B256, B64, U256};
|
||||
use alloy_rlp::Encodable;
|
||||
use reth_chainspec::{ChainSpec, EthereumHardforks};
|
||||
use reth_ethereum_primitives::{Block, BlockBody};
|
||||
use reth_primitives::SealedBlock;
|
||||
use reth_primitives_traits::Block as BlockTrait;
|
||||
use reth_primitives_traits::{Block as BlockTrait, SealedBlock};
|
||||
use std::{io::Write, path::Path};
|
||||
use tracing::debug;
|
||||
|
||||
/// Generate test blocks for a given chain spec
|
||||
pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlock> {
|
||||
let mut blocks: Vec<SealedBlock> = Vec::new();
|
||||
pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlock<Block>> {
|
||||
let mut blocks: Vec<SealedBlock<Block>> = Vec::new();
|
||||
let genesis_header = chain_spec.sealed_genesis_header();
|
||||
let mut parent_hash = genesis_header.hash();
|
||||
let mut parent_number = genesis_header.number();
|
||||
@@ -56,6 +55,8 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
|
||||
excess_blob_gas: None,
|
||||
parent_beacon_block_root: None,
|
||||
requests_hash: None,
|
||||
block_access_list_hash: None,
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
// Set required fields based on chain spec
|
||||
@@ -139,7 +140,7 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
|
||||
}
|
||||
|
||||
/// Write blocks to RLP file
|
||||
pub fn write_blocks_to_rlp(blocks: &[SealedBlock], path: &Path) -> std::io::Result<()> {
|
||||
pub fn write_blocks_to_rlp(blocks: &[SealedBlock<Block>], path: &Path) -> std::io::Result<()> {
|
||||
let mut file = std::fs::File::create(path)?;
|
||||
let mut total_bytes = 0;
|
||||
|
||||
@@ -173,7 +174,7 @@ pub fn write_blocks_to_rlp(blocks: &[SealedBlock], path: &Path) -> std::io::Resu
|
||||
}
|
||||
|
||||
/// Create FCU JSON for the tip of the chain
|
||||
pub fn create_fcu_json(tip: &SealedBlock) -> serde_json::Value {
|
||||
pub fn create_fcu_json(tip: &SealedBlock<Block>) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"params": [{
|
||||
"headBlockHash": format!("0x{:x}", tip.hash()),
|
||||
|
||||
@@ -227,6 +227,7 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::random(),
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
env.active_node_state_mut()?
|
||||
@@ -299,6 +300,7 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::random(),
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
|
||||
let fresh_fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
|
||||
|
||||
@@ -271,6 +271,7 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
};
|
||||
@@ -301,6 +302,7 @@ where
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
<<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes::from(
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
|
||||
|
||||
@@ -161,6 +161,7 @@ async fn test_testsuite_assert_mine_block() -> Result<()> {
|
||||
suggested_fee_recipient: Address::random(),
|
||||
withdrawals: None,
|
||||
parent_beacon_block_root: None,
|
||||
slot_number: None,
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
slot_number: None,
|
||||
};
|
||||
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
|
||||
}
|
||||
|
||||
@@ -840,6 +840,7 @@ mod tests {
|
||||
requests: Requests::default(),
|
||||
gas_used: 0,
|
||||
blob_gas_used: 0,
|
||||
block_access_list: Default::default(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ where
|
||||
.chain_spec
|
||||
.is_cancun_active_at_timestamp(timestamp)
|
||||
.then(B256::random),
|
||||
slot_number: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,32 +9,8 @@ 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 = 60;
|
||||
|
||||
/// The size of proof targets chunk optimized for small blocks (≤20M gas used).
|
||||
/// Benchmarks: <https://gist.github.com/yongkangc/fda9c24846f0ba891376bcf81b002008>
|
||||
pub const SMALL_BLOCK_MULTIPROOF_CHUNK_SIZE: usize = 30;
|
||||
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 5;
|
||||
|
||||
/// Gas threshold below which the small block chunk size is used.
|
||||
pub const SMALL_BLOCK_GAS_THRESHOLD: u64 = 20_000_000;
|
||||
@@ -127,8 +103,6 @@ pub struct TreeConfig {
|
||||
cross_block_cache_size: usize,
|
||||
/// Whether the host has enough parallelism to run state root task.
|
||||
has_enough_parallelism: bool,
|
||||
/// Whether multiproof task should chunk proof targets.
|
||||
multiproof_chunking_enabled: bool,
|
||||
/// Multiproof task chunk size for proof targets.
|
||||
multiproof_chunk_size: usize,
|
||||
/// Number of reserved CPU cores for non-reth processes
|
||||
@@ -153,10 +127,6 @@ 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.
|
||||
@@ -187,15 +157,12 @@ impl Default for TreeConfig {
|
||||
state_provider_metrics: false,
|
||||
cross_block_cache_size: DEFAULT_CROSS_BLOCK_CACHE_SIZE,
|
||||
has_enough_parallelism: has_enough_parallelism(),
|
||||
multiproof_chunking_enabled: true,
|
||||
multiproof_chunk_size: DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
|
||||
reserved_cpu_cores: DEFAULT_RESERVED_CPU_CORES,
|
||||
precompile_cache_disabled: false,
|
||||
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,
|
||||
@@ -221,15 +188,12 @@ impl TreeConfig {
|
||||
state_provider_metrics: bool,
|
||||
cross_block_cache_size: usize,
|
||||
has_enough_parallelism: bool,
|
||||
multiproof_chunking_enabled: bool,
|
||||
multiproof_chunk_size: usize,
|
||||
reserved_cpu_cores: usize,
|
||||
precompile_cache_disabled: bool,
|
||||
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,
|
||||
@@ -248,15 +212,12 @@ impl TreeConfig {
|
||||
state_provider_metrics,
|
||||
cross_block_cache_size,
|
||||
has_enough_parallelism,
|
||||
multiproof_chunking_enabled,
|
||||
multiproof_chunk_size,
|
||||
reserved_cpu_cores,
|
||||
precompile_cache_disabled,
|
||||
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,
|
||||
@@ -290,11 +251,6 @@ impl TreeConfig {
|
||||
self.max_execute_block_batch_size
|
||||
}
|
||||
|
||||
/// Return whether the multiproof task chunking is enabled.
|
||||
pub const fn multiproof_chunking_enabled(&self) -> bool {
|
||||
self.multiproof_chunking_enabled
|
||||
}
|
||||
|
||||
/// Return the multiproof task chunk size.
|
||||
pub const fn multiproof_chunk_size(&self) -> usize {
|
||||
self.multiproof_chunk_size
|
||||
@@ -458,15 +414,6 @@ impl TreeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for whether multiproof task should chunk proof targets.
|
||||
pub const fn with_multiproof_chunking_enabled(
|
||||
mut self,
|
||||
multiproof_chunking_enabled: bool,
|
||||
) -> Self {
|
||||
self.multiproof_chunking_enabled = multiproof_chunking_enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Setter for multiproof task chunk size for proof targets.
|
||||
pub const fn with_multiproof_chunk_size(mut self, multiproof_chunk_size: usize) -> Self {
|
||||
self.multiproof_chunk_size = multiproof_chunk_size;
|
||||
@@ -502,42 +449,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user