Compare commits

...

58 Commits

Author SHA1 Message Date
Arsenii Kulikov
fe2297639c trace_span 2026-02-19 20:57:33 +04:00
Arsenii Kulikov
4e49b5dea2 optimize update_leaf 2026-02-19 20:55:57 +04:00
Arsenii Kulikov
bb963e634e optimize find_leaf 2026-02-19 20:55:51 +04:00
Alexey Shekhirin
c225132b81 ci(bench): drop root privileges for reth-bench (#22380) 2026-02-19 14:46:52 +00:00
radik878
dcc5d9ec30 fix(events): handle PipelineEvent::Unwound to clean up current_stage (#22340) 2026-02-19 13:48:57 +00:00
Alexey Shekhirin
6cd56b645b ci(bench): support running benchmarks on closed/merged PRs (#22378) 2026-02-19 13:16:03 +00:00
Emma Jamieson-Hoare
794dbff26e ci(hive): remove EIP-6110 deposit tests from expected failures (now passing) (#22377)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:56:36 +00:00
Emma Jamieson-Hoare
fcfbed0bbc ci(hive): ignore flaky reorg and sync timeout tests (#22376)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:55:20 +00:00
Alexey Shekhirin
70bcd475fe ci(bench): ABBA run order (#22335) 2026-02-19 12:40:44 +00:00
Emma Jamieson-Hoare
cd6e895a97 fix(rpc): return -32602 for PayloadAttributes structure validation errors (#22374)
Co-authored-by: yongkangc <chiayongkang@hotmail.com>
2026-02-19 12:32:31 +00:00
Emma Jamieson-Hoare
6552a3a9ab ci(hive): fix eels runner OOM crashes (#22373)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:04:22 +00:00
Derek Cofausper
6a91089542 ci(bench): fix cleanup to use sudo pkill and lazy unmount (#22372)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-19 11:23:03 +00:00
YK
a9a1e504b4 refactor(trie): simplify encode_account_leaf_value (#22366) 2026-02-19 10:36:44 +00:00
YK
e280f25885 feat(trie): expose storage_wait_time as dedicated Prometheus metric (#22359) 2026-02-19 10:36:26 +00:00
Arsenii Kulikov
37c4f908fa perf: store blinded node hashes on SparseNode::Branch (#22290)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-19 09:34:42 +00:00
Georgios Konstantopoulos
a157be3f3b perf(tasks): add LazyHandle<T>, use for hash post state (#22347)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 08:48:24 +00:00
Georgios Konstantopoulos
e0eb306b2b chore(engine): rename finish span to BlockExecutor::finish (#22356)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 07:29:18 +00:00
Micke
7f4f3f1eb9 fix(prune): correct deleted entries count when skip_filter is used (#22312) 2026-02-19 06:19:02 +00:00
Georgios Konstantopoulos
8970f82aaf perf(engine): prefetch first txs sequentially to avoid rayon scheduling stall (#22305)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-02-19 05:53:52 +00:00
Georgios Konstantopoulos
8529da976f fix(cli): store extradata as Bytes, decode hex in parser (#22344)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 04:38:16 +00:00
stevencartavia
8fa539225b refactor: remove duplicate apply_pre_execution_changes from Trace trait (#22333) 2026-02-19 04:32:42 +00:00
Doohyun Cho
93d546a36d perf(trie): preserve allocations in sparse trie wipe() (#21089) 2026-02-19 04:02:20 +00:00
zhygis
5c83eb0b06 feat(log): disable file logging by default for non-node commands (#21521)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
2026-02-19 03:16:47 +00:00
Georgios Konstantopoulos
cd32e3cc05 feat(reth-bench): add prometheus metrics scraper (#22244)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-02-19 03:13:40 +00:00
MergeBot
26470cadfc perf(trie): remove redundant HashMap lookup in sparse trie account state query (#22328)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 03:09:04 +00:00
Brian Picciano
506ab806e4 fix: propagate trie update diff result to trigger debug recorder writes (#22331)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 02:52:43 +00:00
Forostovec
c2e846093e fix(net): use continue instead of return in buffer_hashes loop (#22337) 2026-02-19 02:46:33 +00:00
dependabot[bot]
5df22b12d8 chore(deps): bump actions/upload-artifact from 4 to 6 (#22338)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 02:33:00 +00:00
dependabot[bot]
ff9700bb3b chore(deps): bump actions/github-script from 7 to 8 (#22339)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 02:32:57 +00:00
Georgios Konstantopoulos
85d35fa6c0 feat(tasks): add WorkerMap for named single-thread workers (#22262)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 02:27:02 +00:00
Georgios Konstantopoulos
47544d9a7e fix(txpool): ensure transactions are added to pending subpool in nonce order (#22308)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-18 20:31:04 +00:00
Alexey Shekhirin
ef33961aff ci(bench): download snapshot in parallel with builds (#22332) 2026-02-18 17:40:17 +00:00
Georgios Konstantopoulos
0e01a694a7 fix(storage): clarify storage settings mismatch warning (#22330)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 16:35:28 +00:00
Alexey Shekhirin
ee19320ee8 ci(bench): use ABBA run order to reduce variance (#22321) 2026-02-18 15:33:31 +00:00
Alexey Shekhirin
9251997c1f ci(bench): build baseline and feature binaries in parallel (#22323) 2026-02-18 14:30:58 +00:00
Brian Picciano
302993b45a feat(trie-debug): record SetRoot op in ParallelSparseTrie::set_root (#22324)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 14:03:42 +00:00
Brian Picciano
8d97ab63c6 perf: use stack-allocated [u8; 65] for StoredNibblesSubKey encoding (#22314)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-18 13:35:20 +00:00
Matthias Seitz
251f83ab0b refactor: replace TryFrom*Response traits with unified RpcResponseConverter (#22320)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 13:32:47 +00:00
Alexey Shekhirin
e6e0dde903 ci(bench): queue reth-bench jobs and report queue position in PR comment (#22318) 2026-02-18 12:53:12 +00:00
Georgios Konstantopoulos
b1b51261af feat(ci): granular status updates for reth-bench workflow (#22297)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 12:05:59 +00:00
Georgios Konstantopoulos
2ae5ef475e feat(ci): add workflow_dispatch trigger for reth-bench (#22298)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 11:22:22 +00:00
drhgencer
8861e2724f fix(txpool): notify subscribers when set_block_info promotes transaction (#22243)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 09:08:16 +00:00
Georgios Konstantopoulos
734ec4ffe6 feat(engine): add tracing spans to execute_block setup (#22304)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:23:24 +00:00
Georgios Konstantopoulos
cbcdf8dac0 chore(tracing): use underscores instead of spaces in span names (#22307)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:21:04 +00:00
Georgios Konstantopoulos
826e387c87 refactor(rpc): use ..Default::default() for SimCallResult initialization (#22309)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 05:42:22 +00:00
Forostovec
1c40188993 fix: correct message ID in NodeData version error (#22291) 2026-02-18 05:02:33 +00:00
Matthias Seitz
49a2df0d7a chore: bump alloy deps 1.7.1 -> 1.7.3 (#22296)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:02:04 +01:00
DaniPopes
a1d1b6def6 fix: prevent ANSI escape codes leaking into Tracy zone text (#22306) 2026-02-18 03:49:34 +00:00
Georgios Konstantopoulos
56bbb3ce2c feat(cli): add reth db prune-checkpoints command (#22288)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 01:25:53 +00:00
Georgios Konstantopoulos
5b1010322c docs: clarify StateWriteConfig is about database (MDBX) writes vs static files (#22299)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 01:13:31 +00:00
Georgios Konstantopoulos
a195b777eb perf(storage): skip plain state conversion in write_state for storage v2 (#22294)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2026-02-18 00:48:06 +00:00
Georgios Konstantopoulos
5045e6ef8b feat(bench): add wait time breakdown tables to CI report (#22293) 2026-02-17 23:44:03 +00:00
Alexey Shekhirin
b49cadb346 ci(bench): rename main/branch to baseline/feature, add ref args (#22284)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 23:00:01 +00:00
Georgios Konstantopoulos
aeb2c6e731 chore(primitives): remove legacy transaction roundtrip tests (#22292)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 21:15:14 +00:00
stevencartavia
477fed7a11 refactor(primitives): use alloy's EthereumReceipt type (#22254)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 20:30:52 +00:00
MergeBot
59993b974a fix(rpc): resolve AtBlockHash to single block in eth_getFilterChanges (#22283)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 20:15:33 +00:00
Georgios Konstantopoulos
9ecef47aff fix(provider): skip sender pruning during reorg when sender_recovery is full (#22271)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 19:15:03 +00:00
DaniPopes
0ba685386d refactor: dedup runtime initializations (#22263)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-17 17:35:31 +00:00
230 changed files with 4695 additions and 3643 deletions

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View File

@@ -2,55 +2,56 @@
#
# 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
set -euo pipefail
MC="mc --config-dir /home/ubuntu/.mc"
MODE="$1"
COMMIT="$2"
SOURCE_DIR="$2"
COMMIT="$3"
case "$MODE" in
main)
baseline|main)
BUCKET="minio/reth-binaries/${COMMIT}"
mkdir -p target/profiling-baseline
mkdir -p "${SOURCE_DIR}/target/profiling"
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
echo "Cache hit for baseline (${COMMIT}), downloading binary..."
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
chmod +x "${SOURCE_DIR}/target/profiling/reth"
else
echo "Cache miss for main (${COMMIT}), building from source..."
CURRENT_REF=$(git rev-parse HEAD)
git checkout "${COMMIT}"
echo "Cache miss for baseline (${COMMIT}), building 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}"
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
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
else
echo "Cache miss for ${BRANCH_SHA}, building from source..."
cd "${SOURCE_DIR}"
rustup show active-toolchain || rustup default stable
make profiling
make install-reth-bench
@@ -60,7 +61,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

View File

@@ -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)
@@ -76,14 +76,14 @@ def plot_latency_and_throughput(
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)
ax1.plot(feat_x, feat_lat, linewidth=0.8, label=branch_name)
ax1.plot(feat_x, feat_lat, linewidth=0.8, label=feature_name)
ax1.set_ylabel("Latency (ms)")
ax1.set_title("newPayload Latency per Block")
ax1.grid(True, alpha=0.3)
if baseline:
ax1.legend()
ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=branch_name)
ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=feature_name)
ax2.set_ylabel("Ggas/s")
ax2.set_title("Execution Throughput per Block")
ax2.grid(True, alpha=0.3)
@@ -105,7 +105,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 +116,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 +135,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 +163,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 +176,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 +188,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 +246,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)

View File

@@ -12,7 +12,8 @@ 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
@@ -23,8 +24,12 @@ cleanup() {
sleep 1
done
sudo kill -9 "$RETH_PID" 2>/dev/null || true
sleep 1
fi
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
@@ -74,8 +79,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 +92,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" \

125
.github/scripts/bench-reth-snapshot.sh vendored Executable file
View File

@@ -0,0 +1,125 @@
#!/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"
# Sync the new snapshot as the schelk baseline
sync
sudo schelk recover -y
# Save ETag marker
echo "$REMOTE_ETAG" > "$ETAG_FILE"
echo "Snapshot synced to schelk (ETag: ${REMOTE_ETAG})"

View File

@@ -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,6 +239,7 @@ def compute_paired_stats(
"p99_ci_ms": (p99_boot[hi] - p99_boot[lo]) / 2,
"mean_mgas_diff": mean_mgas_diff,
"mgas_ci": mgas_ci,
"blocks": max(blocks_per_pair),
}
@@ -268,11 +296,12 @@ def generate_comparison_table(
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 +323,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 +341,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 +396,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 +408,91 @@ 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]
all_feature = [r for run in feature_runs for r in run]
summary = compute_summary(all_branch, gas)
summary = compute_summary(all_feature, gas)
with open(args.output_summary, "w") as f:
json.dump(summary, f, indent=2)
print(f"Summary written to {args.output_summary}")
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 = []
for field, title in wait_fields:
b_stats = compute_wait_stats(all_baseline, field)
f_stats = compute_wait_stats(all_feature, field)
table = generate_wait_time_table(title, b_stats, f_stats, baseline_label, feature_label)
if table:
wait_time_tables.append(table)
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:

27
.github/scripts/bench-update-status.js vendored Normal file
View 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;

View File

@@ -59,10 +59,6 @@ engine-auth: [ ]
#
# System contract tests (already fixed and deployed):
#
# tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout and test_invalid_log_length
# System contract is already fixed and deployed; tests cover scenarios where contract is
# malformed which can't happen retroactively. No point in adding checks.
#
# tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment
# tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment
# Post-fork system contract deployment tests. Should fix for spec compliance but not realistic
@@ -71,32 +67,8 @@ eels/consume-engine:
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test_engine-zero_nonce]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_True]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_True]-reth
- tests/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

View File

@@ -16,12 +16,16 @@ 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 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
- Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts - No Transactions (Paris) (reth)
- Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
engine-cancun:
- Transaction Re-Org, New Payload on Revert Back (Cancun) (reth)
- Transaction Re-Org, Re-Org to Different Block (Cancun) (reth)
- Transaction Re-Org, Re-Org Out (Cancun) (reth)
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=False, Invalid P9 (Cancun) (reth)
- 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)

View File

@@ -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() {

View File

@@ -3,7 +3,7 @@
# The reth-bench job replays real blocks via the Engine API against a reth node
# backed by a local snapshot managed with schelk.
#
# It runs the main (baseline) binary and the branch (candidate) binary on the
# It runs the baseline binary and the feature (candidate) binary on the
# same block range (snapshot recovered between runs) to compare performance.
on:
@@ -18,7 +18,22 @@ on:
blocks:
description: "Number of blocks to benchmark"
required: false
default: "50"
default: "500"
type: string
warmup:
description: "Number of warmup blocks"
required: false
default: "100"
type: string
baseline:
description: "Baseline git ref (default: merge-base)"
required: false
default: ""
type: string
feature:
description: "Feature git ref (default: branch head)"
required: false
default: ""
type: string
env:
@@ -33,14 +48,13 @@ permissions:
contents: write
pull-requests: write
concurrency:
group: bench-${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
codspeed:
if: github.event_name != 'issue_comment'
if: github.event_name == 'push'
runs-on: depot-ubuntu-latest
concurrency:
group: bench-codspeed-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
strategy:
matrix:
partition: [1, 2]
@@ -75,17 +89,26 @@ jobs:
mode: instrumentation
token: ${{ secrets.CODSPEED_TOKEN }}
reth-bench:
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, 'derek bench')
name: reth-bench
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 120
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
SCHELK_MOUNT: /reth-bench
reth-bench-ack:
if: |
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, 'derek bench')) ||
github.event_name == 'workflow_dispatch'
name: reth-bench-ack
runs-on: ubuntu-latest
outputs:
pr: ${{ steps.args.outputs.pr }}
actor: ${{ steps.args.outputs.actor }}
blocks: ${{ steps.args.outputs.blocks }}
warmup: ${{ steps.args.outputs.warmup }}
baseline: ${{ steps.args.outputs.baseline }}
feature: ${{ steps.args.outputs.feature }}
baseline-name: ${{ steps.args.outputs.baseline-name }}
feature-name: ${{ steps.args.outputs.feature-name }}
comment-id: ${{ steps.ack.outputs.comment-id }}
steps:
- name: Check org membership
uses: actions/github-script@v7
if: github.event_name == 'issue_comment'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -104,77 +127,307 @@ jobs:
- name: Parse arguments
id: args
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const body = context.payload.comment.body.trim();
const known = new Set(['blocks', 'warmup']);
const defaults = { blocks: '500', warmup: '100' };
const unknown = [];
const invalid = [];
const args = body.replace(/^derek bench\s*/, '');
for (const part of args.split(/\s+/).filter(Boolean)) {
const eq = part.indexOf('=');
if (eq === -1) {
unknown.push(part);
continue;
}
const key = part.slice(0, eq);
const value = part.slice(eq + 1);
if (!known.has(key)) {
unknown.push(key);
} else if (!/^\d+$/.test(value)) {
invalid.push(`\`${key}=${value}\` (must be a positive integer)`);
} else {
defaults[key] = value;
}
}
const errors = [];
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
if (errors.length) {
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`derek bench [blocks=N] [warmup=N]\``;
await github.rest.issues.createComment({
let pr, actor, blocks, warmup, baseline, feature;
if (context.eventName === 'workflow_dispatch') {
actor = '${{ github.actor }}';
blocks = '${{ github.event.inputs.blocks }}' || '500';
warmup = '${{ github.event.inputs.warmup }}' || '100';
baseline = '${{ github.event.inputs.baseline }}';
feature = '${{ github.event.inputs.feature }}';
// Find PR for the selected branch
const branch = '${{ github.ref_name }}';
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg,
head: `${context.repo.owner}:${branch}`,
state: 'open',
per_page: 1,
});
core.setFailed(msg);
return;
pr = prs.length ? String(prs[0].number) : '';
if (!pr) {
core.info(`No open PR found for branch '${branch}', results will be in job summary`);
}
} else {
pr = String(context.issue.number);
actor = context.payload.comment.user.login;
const body = context.payload.comment.body.trim();
const intArgs = new Set(['blocks', 'warmup']);
const refArgs = new Set(['baseline', 'feature']);
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '' };
const unknown = [];
const invalid = [];
const args = body.replace(/^derek bench\s*/, '');
for (const part of args.split(/\s+/).filter(Boolean)) {
const eq = part.indexOf('=');
if (eq === -1) {
unknown.push(part);
continue;
}
const key = part.slice(0, eq);
const value = part.slice(eq + 1);
if (intArgs.has(key)) {
if (!/^\d+$/.test(value)) {
invalid.push(`\`${key}=${value}\` (must be a positive integer)`);
} else {
defaults[key] = value;
}
} else if (refArgs.has(key)) {
if (!value) {
invalid.push(`\`${key}=\` (must be a git ref)`);
} else {
defaults[key] = value;
}
} else {
unknown.push(key);
}
}
const errors = [];
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
if (errors.length) {
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`derek bench [blocks=N] [warmup=N] [baseline=REF] [feature=REF]\``;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg,
});
core.setFailed(msg);
return;
}
blocks = defaults.blocks;
warmup = defaults.warmup;
baseline = defaults.baseline;
feature = defaults.feature;
}
core.setOutput('blocks', defaults.blocks);
core.setOutput('warmup', defaults.warmup);
core.exportVariable('BENCH_BLOCKS', defaults.blocks);
core.exportVariable('BENCH_WARMUP_BLOCKS', defaults.warmup);
// Resolve display names for baseline/feature
let baselineName = baseline || 'main';
let featureName = feature;
if (!featureName) {
if (pr) {
const { data: prData } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(pr),
});
featureName = prData.head.ref;
} else {
featureName = '${{ github.ref_name }}';
}
}
core.setOutput('pr', pr || '');
core.setOutput('actor', actor);
core.setOutput('blocks', blocks);
core.setOutput('warmup', warmup);
core.setOutput('baseline', baseline);
core.setOutput('feature', feature);
core.setOutput('baseline-name', baselineName);
core.setOutput('feature-name', featureName);
- name: Acknowledge request
id: ack
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
if (context.eventName === 'issue_comment') {
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
}
const pr = '${{ steps.args.outputs.pr }}';
if (!pr) return;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
// Count queued/waiting bench runs ahead of this one
let queueMsg = '';
let ahead = 0;
try {
const statuses = ['queued', 'in_progress', 'waiting', 'requested', 'pending'];
const allRuns = [];
for (const status of statuses) {
const { data: { workflow_runs: r } } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'bench.yml',
status,
per_page: 100,
});
allRuns.push(...r);
}
// Only count runs that trigger reth-bench (not push-triggered codspeed runs)
const benchRuns = allRuns.filter(r => r.event === 'issue_comment' || r.event === 'workflow_dispatch');
const thisRun = benchRuns.find(r => r.id === context.runId);
const thisCreatedAt = thisRun ? new Date(thisRun.created_at) : new Date();
ahead = benchRuns.filter(r => r.id !== context.runId && new Date(r.created_at) <= thisCreatedAt).length;
if (ahead > 0) {
queueMsg = `\n🔢 **Queue position:** \`#${ahead + 1}\` (${ahead} job(s) ahead)`;
}
} catch (e) {
// Non-fatal — queue info is best-effort
core.info(`Could not fetch queue info: ${e.message}`);
}
const actor = '${{ steps.args.outputs.actor }}';
const blocks = '${{ steps.args.outputs.blocks }}';
const warmup = '${{ steps.args.outputs.warmup }}';
const baseline = '${{ steps.args.outputs.baseline-name }}';
const feature = '${{ steps.args.outputs.feature-name }}';
const config = `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\``;
const { data: comment } = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `🚀 Benchmark started! [View run](${runUrl})\n\n⏳ **Status:** Building binaries...\n\n**Config:** ${blocks} blocks, ${warmup} warmup blocks`,
issue_number: parseInt(pr),
body: `cc @${actor}\n\n🚀 Benchmark queued! [View run](${runUrl})\n\n⏳ **Status:** Waiting for runner...${queueMsg}\n\n${config}`,
});
core.setOutput('comment-id', comment.id);
core.setOutput('comment-id', String(comment.id));
core.setOutput('queue-position', String(ahead || 0));
- name: Poll queue position
if: steps.ack.outputs.comment-id && steps.ack.outputs.queue-position != '0'
uses: actions/github-script@v8
with:
script: |
const pr = '${{ steps.args.outputs.pr }}';
const commentId = parseInt('${{ steps.ack.outputs.comment-id }}');
const actor = '${{ steps.args.outputs.actor }}';
const blocks = '${{ steps.args.outputs.blocks }}';
const warmup = '${{ steps.args.outputs.warmup }}';
const baseline = '${{ steps.args.outputs.baseline-name }}';
const feature = '${{ steps.args.outputs.feature-name }}';
const config = `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\``;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
async function getQueuePosition() {
const statuses = ['queued', 'in_progress', 'waiting', 'requested', 'pending'];
const allRuns = [];
for (const status of statuses) {
const { data: { workflow_runs: r } } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'bench.yml',
status,
per_page: 100,
});
allRuns.push(...r);
}
const benchRuns = allRuns.filter(r => r.event === 'issue_comment' || r.event === 'workflow_dispatch');
const thisRun = benchRuns.find(r => r.id === context.runId);
const thisCreatedAt = thisRun ? new Date(thisRun.created_at) : new Date();
return benchRuns.filter(r => r.id !== context.runId && new Date(r.created_at) <= thisCreatedAt).length;
}
let lastPosition = parseInt('${{ steps.ack.outputs.queue-position }}');
const sleep = ms => new Promise(r => setTimeout(r, ms));
while (true) {
await sleep(10_000);
try {
const ahead = await getQueuePosition();
if (ahead !== lastPosition) {
lastPosition = ahead;
const queueMsg = ahead > 0
? `\n🔢 **Queue position:** \`#${ahead + 1}\` (${ahead} job(s) ahead)`
: '';
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
body: `cc @${actor}\n\n🚀 Benchmark queued! [View run](${runUrl})\n\n⏳ **Status:** Waiting for runner...${queueMsg}\n\n${config}`,
});
}
if (ahead === 0) break;
} catch (e) {
core.info(`Queue poll error: ${e.message}`);
}
}
reth-bench:
needs: reth-bench-ack
name: reth-bench
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 120
concurrency:
group: reth-bench-queue
cancel-in-progress: false
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
SCHELK_MOUNT: /reth-bench
BENCH_WORK_DIR: ${{ github.workspace }}/bench-work
BENCH_PR: ${{ needs.reth-bench-ack.outputs.pr }}
BENCH_ACTOR: ${{ needs.reth-bench-ack.outputs.actor }}
BENCH_BLOCKS: ${{ needs.reth-bench-ack.outputs.blocks }}
BENCH_WARMUP_BLOCKS: ${{ needs.reth-bench-ack.outputs.warmup }}
BENCH_COMMENT_ID: ${{ needs.reth-bench-ack.outputs.comment-id }}
steps:
- name: Resolve checkout ref
id: checkout-ref
uses: actions/github-script@v8
with:
script: |
if (!process.env.BENCH_PR) {
core.setOutput('ref', '${{ github.ref }}');
return;
}
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(process.env.BENCH_PR),
});
// For closed/merged PRs, the merge ref doesn't exist — use head SHA
if (pr.state !== 'open') {
core.info(`PR #${process.env.BENCH_PR} is ${pr.state}, using head SHA ${pr.head.sha}`);
core.setOutput('ref', pr.head.sha);
} else {
core.setOutput('ref', `refs/pull/${process.env.BENCH_PR}/merge`);
}
- uses: actions/checkout@v6
with:
submodules: true
fetch-depth: 0
ref: ${{ format('refs/pull/{0}/merge', github.event.issue.number) }}
ref: ${{ steps.checkout-ref.outputs.ref }}
- name: Resolve job URL and update status
if: env.BENCH_COMMENT_ID
uses: actions/github-script@v8
with:
script: |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
const job = jobs.jobs.find(j => j.name === 'reth-bench');
const jobUrl = job ? job.html_url : `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
core.exportVariable('BENCH_JOB_URL', jobUrl);
const blocks = process.env.BENCH_BLOCKS;
const warmup = process.env.BENCH_WARMUP_BLOCKS;
const baseline = '${{ needs.reth-bench-ack.outputs.baseline-name }}';
const feature = '${{ needs.reth-bench-ack.outputs.feature-name }}';
core.exportVariable('BENCH_CONFIG', `**Config:** ${blocks} blocks, ${warmup} warmup blocks, baseline: \`${baseline}\`, feature: \`${feature}\``);
const { buildBody } = require('./.github/scripts/bench-update-status.js');
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: parseInt(process.env.BENCH_COMMENT_ID),
body: buildBody('Building binaries...'),
});
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
@@ -187,7 +440,7 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
missing=()
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv; do
for cmd in mc schelk cpupower taskset stdbuf python3 curl make uv pzstd jq; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
@@ -197,14 +450,110 @@ jobs:
echo "All dependencies found"
# Build binaries
- name: Fetch or build main binaries
- name: Resolve PR head branch
id: pr-info
uses: actions/github-script@v8
with:
script: |
if (process.env.BENCH_PR) {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(process.env.BENCH_PR),
});
core.setOutput('head-ref', pr.head.ref);
core.setOutput('head-sha', pr.head.sha);
} else {
core.setOutput('head-ref', '${{ github.ref_name }}');
core.setOutput('head-sha', '${{ github.sha }}');
}
- name: Resolve refs
id: refs
run: |
MERGE_BASE=$(git merge-base HEAD origin/main 2>/dev/null || echo "${{ github.sha }}")
.github/scripts/bench-reth-build.sh main "$MERGE_BASE"
- name: Fetch or build branch binaries
BASELINE_ARG="${{ needs.reth-bench-ack.outputs.baseline }}"
FEATURE_ARG="${{ needs.reth-bench-ack.outputs.feature }}"
if [ -n "$BASELINE_ARG" ]; then
git fetch origin "$BASELINE_ARG" --quiet 2>/dev/null || true
BASELINE_REF=$(git rev-parse "$BASELINE_ARG" 2>/dev/null || git rev-parse "origin/$BASELINE_ARG" 2>/dev/null)
BASELINE_NAME="$BASELINE_ARG"
else
BASELINE_REF=$(git merge-base HEAD origin/main 2>/dev/null || echo "${{ github.sha }}")
BASELINE_NAME="main"
fi
if [ -n "$FEATURE_ARG" ]; then
git fetch origin "$FEATURE_ARG" --quiet 2>/dev/null || true
FEATURE_REF=$(git rev-parse "$FEATURE_ARG" 2>/dev/null || git rev-parse "origin/$FEATURE_ARG" 2>/dev/null)
FEATURE_NAME="$FEATURE_ARG"
else
FEATURE_REF="${{ steps.pr-info.outputs.head-sha }}"
FEATURE_NAME="${{ steps.pr-info.outputs.head-ref }}"
fi
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
echo "baseline-name=$BASELINE_NAME" >> "$GITHUB_OUTPUT"
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
echo "feature-name=$FEATURE_NAME" >> "$GITHUB_OUTPUT"
- name: Check if snapshot needs update
id: snapshot-check
run: |
BRANCH_SHA="${{ github.sha }}"
.github/scripts/bench-reth-build.sh branch "$BRANCH_SHA"
if .github/scripts/bench-reth-snapshot.sh --check; then
echo "needed=false" >> "$GITHUB_OUTPUT"
else
echo "needed=true" >> "$GITHUB_OUTPUT"
fi
- name: Update status (snapshot needed)
if: env.BENCH_COMMENT_ID && steps.snapshot-check.outputs.needed == 'true'
uses: actions/github-script@v8
with:
script: |
const s = require('./.github/scripts/bench-update-status.js');
await s({github, context, status: 'Building binaries & downloading snapshot...'});
- name: Prepare source dirs
run: |
BASELINE_REF="${{ steps.refs.outputs.baseline-ref }}"
if [ -d ../reth-baseline ]; then
git -C ../reth-baseline fetch origin "$BASELINE_REF"
else
git clone . ../reth-baseline
fi
git -C ../reth-baseline checkout "$BASELINE_REF"
ln -sfn "$(pwd)" ../reth-feature
- name: Build binaries and download snapshot in parallel
id: build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BENCH_REPO: ${{ github.repository }}
SNAPSHOT_NEEDED: ${{ steps.snapshot-check.outputs.needed }}
run: |
BASELINE_DIR="$(cd ../reth-baseline && pwd)"
FEATURE_DIR="$(cd ../reth-feature && pwd)"
.github/scripts/bench-reth-build.sh baseline "${BASELINE_DIR}" "${{ steps.refs.outputs.baseline-ref }}" &
PID_BASELINE=$!
.github/scripts/bench-reth-build.sh feature "${FEATURE_DIR}" "${{ steps.refs.outputs.feature-ref }}" &
PID_FEATURE=$!
PID_SNAPSHOT=
if [ "$SNAPSHOT_NEEDED" = "true" ]; then
.github/scripts/bench-reth-snapshot.sh &
PID_SNAPSHOT=$!
fi
FAIL=0
wait $PID_BASELINE || FAIL=1
wait $PID_FEATURE || FAIL=1
[ -n "$PID_SNAPSHOT" ] && { wait $PID_SNAPSHOT || FAIL=1; }
if [ $FAIL -ne 0 ]; then
echo "::error::One or more parallel tasks failed (builds / snapshot download)"
exit 1
fi
# System tuning for reproducible benchmarks
- name: System setup
@@ -248,57 +597,70 @@ jobs:
# Clean up any leftover state
- name: Pre-flight cleanup
run: |
pkill -9 reth || true
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || true
sudo pkill -9 reth || true
sleep 1
if mountpoint -q "$SCHELK_MOUNT"; then
sudo umount -l "$SCHELK_MOUNT" || true
sudo schelk recover -y || true
fi
rm -rf "$BENCH_WORK_DIR"
mkdir -p "$BENCH_WORK_DIR"
- name: Update status (running benchmarks)
if: steps.ack.outputs.comment-id
uses: actions/github-script@v7
if: success() && env.BENCH_COMMENT_ID
uses: actions/github-script@v8
with:
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ steps.ack.outputs.comment-id || 0 }},
body: `🚀 Benchmark started! [View run](${runUrl})\n\n⏳ **Status:** Running benchmarks (2 runs)...`,
});
const s = require('./.github/scripts/bench-update-status.js');
await s({github, context, status: 'Running benchmarks...'});
- name: "Run benchmark: baseline"
run: taskset -c 0 .github/scripts/bench-reth-run.sh baseline target/profiling-baseline/reth /tmp/bench-results-baseline
# Interleaved run order (B-F-F-B) to reduce systematic bias from
# thermal drift and cache warming.
- name: "Run benchmark: baseline (1/2)"
id: run-baseline-1
run: taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: branch"
run: taskset -c 0 .github/scripts/bench-reth-run.sh branch target/profiling/reth /tmp/bench-results-branch
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
run: taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: feature (2/2)"
id: run-feature-2
run: taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
- name: "Run benchmark: baseline (2/2)"
id: run-baseline-2
run: taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
# Results & charts
- name: Parse results
id: results
if: success()
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
BRANCH_SHA: ${{ github.sha }}
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
run: |
git fetch origin main --quiet
# Use the actual PR head commit, not HEAD (which is the merge commit
# refs/pull/N/merge and always has origin/main as a parent).
MERGE_BASE=$(git merge-base "${BRANCH_SHA}" origin/main 2>/dev/null || echo "${{ github.sha }}")
MAIN_HEAD=$(git rev-parse origin/main 2>/dev/null || echo "")
BEHIND_MAIN=0
if [ -n "$MAIN_HEAD" ] && [ "$MERGE_BASE" != "$MAIN_HEAD" ]; then
BEHIND_MAIN=$(git rev-list --count "${MERGE_BASE}..${MAIN_HEAD}" 2>/dev/null || echo "0")
git fetch origin "${BASELINE_NAME}" --quiet 2>/dev/null || true
BASELINE_HEAD=$(git rev-parse "origin/${BASELINE_NAME}" 2>/dev/null || echo "")
BEHIND_BASELINE=0
if [ -n "$BASELINE_HEAD" ] && [ "$BASELINE_REF" != "$BASELINE_HEAD" ]; then
BEHIND_BASELINE=$(git rev-list --count "${BASELINE_REF}..${BASELINE_HEAD}" 2>/dev/null || echo "0")
fi
SUMMARY_ARGS="--output-summary /tmp/bench-summary.json"
SUMMARY_ARGS="$SUMMARY_ARGS --output-markdown /tmp/bench-comment.md"
SUMMARY_ARGS="--output-summary $BENCH_WORK_DIR/summary.json"
SUMMARY_ARGS="$SUMMARY_ARGS --output-markdown $BENCH_WORK_DIR/comment.md"
SUMMARY_ARGS="$SUMMARY_ARGS --repo ${{ github.repository }}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-ref ${MERGE_BASE}"
SUMMARY_ARGS="$SUMMARY_ARGS --branch-name ${BRANCH_NAME}"
SUMMARY_ARGS="$SUMMARY_ARGS --branch-sha ${BRANCH_SHA}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-csv /tmp/bench-results-baseline/combined_latency.csv"
SUMMARY_ARGS="$SUMMARY_ARGS --branch-csv /tmp/bench-results-branch/combined_latency.csv"
SUMMARY_ARGS="$SUMMARY_ARGS --gas-csv /tmp/bench-results-branch/total_gas.csv"
if [ "$BEHIND_MAIN" -gt 0 ]; then
SUMMARY_ARGS="$SUMMARY_ARGS --behind-main $BEHIND_MAIN"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-ref ${BASELINE_REF}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-name ${BASELINE_NAME}"
SUMMARY_ARGS="$SUMMARY_ARGS --feature-name ${FEATURE_NAME}"
SUMMARY_ARGS="$SUMMARY_ARGS --feature-ref ${FEATURE_REF}"
SUMMARY_ARGS="$SUMMARY_ARGS --baseline-csv $BENCH_WORK_DIR/baseline-1/combined_latency.csv $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
SUMMARY_ARGS="$SUMMARY_ARGS --feature-csv $BENCH_WORK_DIR/feature-1/combined_latency.csv $BENCH_WORK_DIR/feature-2/combined_latency.csv"
SUMMARY_ARGS="$SUMMARY_ARGS --gas-csv $BENCH_WORK_DIR/feature-1/total_gas.csv"
if [ "$BEHIND_BASELINE" -gt 0 ]; then
SUMMARY_ARGS="$SUMMARY_ARGS --behind-baseline $BEHIND_BASELINE"
fi
# shellcheck disable=SC2086
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
@@ -306,30 +668,29 @@ jobs:
- name: Generate charts
if: success()
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
run: |
CHART_ARGS="/tmp/bench-results-branch/combined_latency.csv --output-dir /tmp/bench-charts"
CHART_ARGS="$CHART_ARGS --baseline /tmp/bench-results-baseline/combined_latency.csv"
CHART_ARGS="$CHART_ARGS --branch-name ${BRANCH_NAME}"
CHART_ARGS="--output-dir $BENCH_WORK_DIR/charts"
CHART_ARGS="$CHART_ARGS --feature $BENCH_WORK_DIR/feature-1/combined_latency.csv $BENCH_WORK_DIR/feature-2/combined_latency.csv"
CHART_ARGS="$CHART_ARGS --baseline $BENCH_WORK_DIR/baseline-1/combined_latency.csv $BENCH_WORK_DIR/baseline-2/combined_latency.csv"
CHART_ARGS="$CHART_ARGS --baseline-name ${BASELINE_NAME}"
CHART_ARGS="$CHART_ARGS --feature-name ${FEATURE_NAME}"
# shellcheck disable=SC2086
uv run --with matplotlib python3 .github/scripts/bench-reth-charts.py $CHART_ARGS
- name: Upload results
if: success()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: bench-reth-results
path: |
/tmp/bench-results-baseline/
/tmp/bench-results-branch/
/tmp/bench-summary.json
/tmp/bench-charts/
path: ${{ env.BENCH_WORK_DIR }}
- name: Push charts
id: push-charts
if: success()
if: success() && env.BENCH_PR
run: |
PR_NUMBER=${{ github.event.issue.number }}
PR_NUMBER=${{ env.BENCH_PR }}
RUN_ID=${{ github.run_id }}
CHART_DIR="pr/${PR_NUMBER}/${RUN_ID}"
@@ -341,7 +702,7 @@ jobs:
fi
mkdir -p "${CHART_DIR}"
cp /tmp/bench-charts/*.png "${CHART_DIR}/"
cp "$BENCH_WORK_DIR"/charts/*.png "${CHART_DIR}/"
git add "${CHART_DIR}"
git -c user.name="github-actions" -c user.email="github-actions@github.com" \
commit -m "bench charts for PR #${PR_NUMBER} run ${RUN_ID}"
@@ -350,42 +711,41 @@ jobs:
- name: Compare & comment
if: success()
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
let comment = '';
try {
comment = fs.readFileSync('/tmp/bench-comment.md', 'utf8');
comment = fs.readFileSync(process.env.BENCH_WORK_DIR + '/comment.md', 'utf8');
} catch (e) {
comment = '⚠️ Engine benchmark completed but failed to generate comparison.';
}
const sha = '${{ steps.push-charts.outputs.sha }}';
const prNumber = context.issue.number;
const prNumber = process.env.BENCH_PR;
const runId = '${{ github.run_id }}';
const baseUrl = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/${sha}/pr/${prNumber}/${runId}`;
const charts = [
{ file: 'latency_throughput.png', label: 'Latency, Throughput & Diff' },
{ file: 'wait_breakdown.png', label: 'Wait Time Breakdown' },
{ file: 'gas_vs_latency.png', label: 'Gas vs Latency' },
];
let chartMarkdown = '\n\n### Charts\n\n';
for (const chart of charts) {
chartMarkdown += `<details><summary>${chart.label}</summary>\n\n`;
chartMarkdown += `![${chart.label}](${baseUrl}/${chart.file})\n\n`;
chartMarkdown += `</details>\n\n`;
if (sha && prNumber) {
const baseUrl = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/${sha}/pr/${prNumber}/${runId}`;
const charts = [
{ file: 'latency_throughput.png', label: 'Latency, Throughput & Diff' },
{ file: 'wait_breakdown.png', label: 'Wait Time Breakdown' },
{ file: 'gas_vs_latency.png', label: 'Gas vs Latency' },
];
let chartMarkdown = '\n\n### Charts\n\n';
for (const chart of charts) {
chartMarkdown += `<details><summary>${chart.label}</summary>\n\n`;
chartMarkdown += `![${chart.label}](${baseUrl}/${chart.file})\n\n`;
chartMarkdown += `</details>\n\n`;
}
comment += chartMarkdown;
}
comment += chartMarkdown;
const requestedBy = '${{ github.event.comment.user.login }}';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = `cc @${requestedBy}\n\n✅ Benchmark complete! [View run](${runUrl})\n\n${comment}`;
const ackCommentId = '${{ steps.ack.outputs.comment-id }}';
const jobUrl = process.env.BENCH_JOB_URL || `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = `cc @${process.env.BENCH_ACTOR}\n\n✅ Benchmark complete! [View job](${jobUrl})\n\n${comment}`;
const ackCommentId = process.env.BENCH_COMMENT_ID;
if (ackCommentId) {
await github.rest.issues.updateComment({
@@ -395,22 +755,38 @@ jobs:
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
// No PR — write results to job summary
await core.summary.addRaw(body).write();
}
- name: Update status (failed)
if: failure() && env.BENCH_COMMENT_ID
uses: actions/github-script@v8
with:
script: |
const steps_status = [
['building binaries${{ steps.snapshot-check.outputs.needed == 'true' && ' & downloading snapshot' || '' }}', '${{ steps.build.outcome }}'],
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}'],
['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}'],
];
const failed = steps_status.find(([, o]) => o === 'failure');
const failedStep = failed ? failed[0] : 'unknown step';
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: parseInt(process.env.BENCH_COMMENT_ID),
body: `cc @${process.env.BENCH_ACTOR}\n\n❌ Benchmark failed while ${failedStep}. [View logs](${process.env.BENCH_JOB_URL})`,
});
- name: Upload node log
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: reth-node-log
path: |
/tmp/reth-bench-node-baseline.log
/tmp/reth-bench-node-branch.log
${{ env.BENCH_WORK_DIR }}/*/node.log
- name: Restore system settings
if: always()

View File

@@ -188,7 +188,8 @@ jobs:
- build-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:

157
Cargo.lock generated
View File

@@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "alloy-consensus"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4ff99651d46cef43767b5e8262ea228cd05287409ccb0c947cc25e70a952f9"
checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -149,9 +149,9 @@ dependencies = [
[[package]]
name = "alloy-consensus-any"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0701b0eda8051a2398591113e7862f807ccdd3315d0b441f06c2a0865a379b"
checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -164,9 +164,9 @@ dependencies = [
[[package]]
name = "alloy-contract"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c83c7a3c4e1151e8cac383d0a67ddf358f37e5ea51c95a1283d897c9de0a5a"
checksum = "ca63b7125a981415898ffe2a2a696c83696c9c6bdb1671c8a912946bbd8e49e7"
dependencies = [
"alloy-consensus",
"alloy-dyn-abi",
@@ -262,9 +262,9 @@ dependencies = [
[[package]]
name = "alloy-eips"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "def1626eea28d48c6cc0a6f16f34d4af0001906e4f889df6c660b39c86fd044d"
checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8"
dependencies = [
"alloy-eip2124",
"alloy-eip2930",
@@ -311,9 +311,9 @@ dependencies = [
[[package]]
name = "alloy-genesis"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d9d1aba3f914f0e8db9e4616ae37f3d811426d95bdccf44e47d0605ab202f6"
checksum = "7c9cf3b99f46615fbf7dc1add0c96553abb7bf88fc9ec70dfbe7ad0b47ba7fe8"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -365,9 +365,9 @@ dependencies = [
[[package]]
name = "alloy-json-rpc"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a"
checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7"
dependencies = [
"alloy-primitives",
"alloy-sol-types",
@@ -380,9 +380,9 @@ dependencies = [
[[package]]
name = "alloy-network"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b36c2a0ed74e48851f78415ca5b465211bd678891ba11e88fee09eac534bab1"
checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -406,9 +406,9 @@ dependencies = [
[[package]]
name = "alloy-network-primitives"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "636c8051da58802e757b76c3b65af610b95799f72423dc955737dec73de234fd"
checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -419,9 +419,9 @@ dependencies = [
[[package]]
name = "alloy-node-bindings"
version = "1.6.3"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce6930ce52e43b0768dc99ceeff5cb9e673e8c9f87d926914cd028b2e3f7233"
checksum = "e5a65d1ef42da862d7a528e95c170fa3066a81f23fbb9e778c213cf5e4063a0f"
dependencies = [
"alloy-genesis",
"alloy-hardforks 0.2.13",
@@ -484,9 +484,9 @@ dependencies = [
[[package]]
name = "alloy-provider"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dd56e2eafe8b1803e325867ac2c8a4c73c9fb5f341ffd8347f9344458c5922"
checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870"
dependencies = [
"alloy-chains",
"alloy-consensus",
@@ -529,9 +529,9 @@ dependencies = [
[[package]]
name = "alloy-pubsub"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eebf54983d4fccea08053c218ee5c288adf2e660095a243d0532a8070b43955"
checksum = "e8bd82953194dec221aa4cbbbb0b1e2df46066fe9d0333ac25b43a311e122d13"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -573,9 +573,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-client"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91577235d341a1bdbee30a463655d08504408a4d51e9f72edbfc5a622829f402"
checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -599,9 +599,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cff039bf01a17d76c0aace3a3a773d5f895eb4c68baaae729ec9da9e86c99c"
checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-engine",
@@ -612,9 +612,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-admin"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "564afceae126df73b95f78c81eb46e2ef689a45ace0fcdaf5c9a178693a5ccca"
checksum = "42325c117af3a9e49013f881c1474168db57978e02085fc9853a1c89e0562740"
dependencies = [
"alloy-genesis",
"alloy-primitives",
@@ -624,9 +624,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-anvil"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22250cf438b6a3926de67683c08163bfa1fd1efa47ee9512cbcd631b6b0243c"
checksum = "e0a3100b76987c1b1dc81f3abe592b7edc29e92b1242067a69d65e0030b35cf9"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -636,9 +636,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-any"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73234a141ecce14e2989748c04fcac23deee67a445e2c4c167cfb42d4dacd1b6"
checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a"
dependencies = [
"alloy-consensus-any",
"alloy-rpc-types-eth",
@@ -647,9 +647,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-beacon"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625af0c3ebd3c31322edb1fb6b8e3e518acc39e164ed07e422eaff05310ff2fa"
checksum = "4a22e13215866f5dfd5d3278f4c41f1fad9410dc68ce39022f58593c873c26f8"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -667,9 +667,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-debug"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779f70ab16a77e305571881b07a9bc6b0068ae6f962497baf5762825c5b839fb"
checksum = "e1b21e1ad18ff1b31ff1030e046462ab8168cf8894e6778cd805c8bdfe2bd649"
dependencies = [
"alloy-primitives",
"derive_more",
@@ -679,9 +679,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-engine"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10620d600cc46538f613c561ac9a923843c6c74c61f054828dcdb8dd18c72ec4"
checksum = "e4ac61f03f1edabccde1c687b5b25fff28f183afee64eaa2e767def3929e4457"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -700,9 +700,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-eth"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010e101dbebe0c678248907a2545b574a87d078d82c2f6f5d0e8e7c9a6149a10"
checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -722,9 +722,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-mev"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375e4bf001135fe4f344db6197fafed8c2b61e99fa14d3597f44cd413f79e45b"
checksum = "fe85bf3be739126aa593dca9fb3ab13ca93fa7873e6f2247be64d7f2cb15f34a"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -737,9 +737,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-trace"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be096f74d85e1f927580b398bf7bc5b4aa62326f149680ec0867e3c040c9aced"
checksum = "1ad79f1e27e161943b5a4f99fe5534ef0849876214be411e0032c12f38e94daa"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -751,9 +751,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-txpool"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14ab75189fbc29c5dd6f0bc1529bccef7b00773b458763f4d9d81a77ae4a1a2d"
checksum = "d459f902a2313737bc66d18ed094c25d2aeb268b74d98c26bbbda2aa44182ab0"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -763,9 +763,9 @@ dependencies = [
[[package]]
name = "alloy-serde"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e6d631f8b975229361d8af7b2c749af31c73b3cf1352f90e144ddb06227105e"
checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37"
dependencies = [
"alloy-primitives",
"arbitrary",
@@ -775,9 +775,9 @@ dependencies = [
[[package]]
name = "alloy-signer"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97f40010b5e8f79b70bf163b38cd15f529b18ca88c4427c0e43441ee54e4ed82"
checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d"
dependencies = [
"alloy-primitives",
"async-trait",
@@ -790,9 +790,9 @@ dependencies = [
[[package]]
name = "alloy-signer-local"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c4ec1cc27473819399a3f0da83bc1cef0ceaac8c1c93997696e46dc74377a58"
checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -879,9 +879,9 @@ dependencies = [
[[package]]
name = "alloy-transport"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a03bb3f02b9a7ab23dacd1822fa7f69aa5c8eefcdcf57fad085e0b8d76fb4334"
checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53"
dependencies = [
"alloy-json-rpc",
"auto_impl",
@@ -902,9 +902,9 @@ dependencies = [
[[package]]
name = "alloy-transport-http"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce599598ef8ebe067f3627509358d9faaa1ef94f77f834a7783cd44209ef55c"
checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e"
dependencies = [
"alloy-json-rpc",
"alloy-transport",
@@ -918,9 +918,9 @@ dependencies = [
[[package]]
name = "alloy-transport-ipc"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49963a2561ebd439549915ea61efb70a7b13b97500ec16ca507721c9d9957d07"
checksum = "c2ef85688e5ac2da72afc804e0a1f153a1f309f05a864b1998bbbed7804dbaab"
dependencies = [
"alloy-json-rpc",
"alloy-pubsub",
@@ -938,9 +938,9 @@ dependencies = [
[[package]]
name = "alloy-transport-ws"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ed38ea573c6658e0c2745af9d1f1773b1ed83aa59fbd9c286358ad469c3233a"
checksum = "b9f00445db69d63298e2b00a0ea1d859f00e6424a3144ffc5eba9c31da995e16"
dependencies = [
"alloy-pubsub",
"alloy-transport",
@@ -976,9 +976,9 @@ dependencies = [
[[package]]
name = "alloy-tx-macros"
version = "1.6.3"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397406cf04b11ca2a48e6f81804c70af3f40a36abf648e11dc7416043eb0834d"
checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc"
dependencies = [
"darling 0.21.3",
"proc-macro2",
@@ -2211,9 +2211,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.58"
version = "4.5.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2221,9 +2221,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.58"
version = "4.5.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24"
dependencies = [
"anstream",
"anstyle",
@@ -3599,6 +3599,7 @@ dependencies = [
"reth-primitives",
"reth-primitives-traits",
"reth-provider",
"reth-tasks",
"reth-tracing",
"secp256k1 0.30.0",
"serde",
@@ -7269,9 +7270,9 @@ dependencies = [
[[package]]
name = "rapidhash"
version = "4.3.0"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84816e4c99c467e92cf984ee6328caa976dfecd33a673544489d79ca2caaefe5"
checksum = "111325c42c4bafae99e777cd77b40dea9a2b30c69e9d8c74b6eccd7fba4337de"
dependencies = [
"rand 0.9.2",
"rustversion",
@@ -8707,19 +8708,13 @@ dependencies = [
"alloy-primitives",
"alloy-rlp",
"alloy-rpc-types-eth",
"alloy-serde",
"arbitrary",
"bincode 1.3.3",
"derive_more",
"modular-bitfield",
"proptest",
"proptest-arbitrary-interop",
"rand 0.8.5",
"rand 0.9.2",
"reth-codecs",
"reth-primitives-traits",
"reth-zstd-compressors",
"secp256k1 0.30.0",
"serde",
"serde_json",
"serde_with",
@@ -9321,6 +9316,7 @@ dependencies = [
"reth-stages-types",
"reth-storage-api",
"reth-storage-errors",
"reth-tasks",
"reth-tracing",
"reth-tracing-otlp",
"reth-transaction-pool",
@@ -9938,9 +9934,7 @@ dependencies = [
"dyn-clone",
"jsonrpsee-types",
"op-alloy-consensus",
"op-alloy-network",
"op-alloy-rpc-types",
"reth-ethereum-primitives",
"reth-evm",
"reth-primitives-traits",
"serde_json",
@@ -10332,12 +10326,12 @@ dependencies = [
"reth-chainspec",
"reth-db-api",
"reth-errors",
"reth-ethereum-primitives",
"reth-execution-types",
"reth-node-types",
"reth-primitives",
"reth-provider",
"reth-prune-types",
"reth-rpc-convert",
"reth-stages-types",
"reth-storage-api",
"reth-trie",
@@ -10351,6 +10345,7 @@ name = "reth-tasks"
version = "1.11.0"
dependencies = [
"crossbeam-utils",
"dashmap",
"futures-util",
"metrics",
"parking_lot",
@@ -12320,9 +12315,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.8+spec-1.1.0"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
@@ -12770,9 +12765,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"

View File

@@ -459,33 +459,33 @@ 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 }
@@ -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"

View 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));
}
}

View File

@@ -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;

View File

@@ -13,6 +13,7 @@ use crate::{
bench::{
context::BenchContext,
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
},
@@ -30,7 +31,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)]
@@ -155,6 +156,8 @@ impl Command {
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");
}
@@ -288,6 +291,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 +322,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)?;

View File

@@ -3,6 +3,7 @@
use crate::{
bench::{
context::BenchContext,
metrics_scraper::MetricsScraper,
output::{
NewPayloadResult, TotalGasOutput, TotalGasRow, GAS_OUTPUT_SUFFIX,
NEW_PAYLOAD_OUTPUT_SUFFIX,
@@ -54,6 +55,8 @@ impl Command {
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");
}
@@ -142,6 +145,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 +181,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);
}

View File

@@ -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,
@@ -135,6 +136,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.
@@ -204,6 +213,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")?;
@@ -395,6 +406,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?;
}
@@ -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!(

View File

@@ -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 }
@@ -125,7 +125,7 @@ arbitrary = [
"reth-stages-types/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",
"alloy-consensus/arbitrary",

View File

@@ -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();

View File

@@ -16,6 +16,7 @@ mod copy;
mod diff;
mod get;
mod list;
mod prune_checkpoints;
mod repair_trie;
mod settings;
mod state;
@@ -67,6 +68,8 @@ pub enum Subcommands {
Path,
/// Manage storage settings
Settings(settings::Command),
/// View or set prune checkpoints
PruneCheckpoints(prune_checkpoints::Command),
/// Gets storage size information for an account
AccountStorage(account_storage::Command),
/// Gets account state and storage at a specific block
@@ -83,7 +86,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 +208,11 @@ 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::AccountStorage(command) => {
db_exec!(self.env, tool, N, AccessRights::RO, {
command.execute(&tool)?;

View 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})"),
}
}

View File

@@ -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 {

View File

@@ -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?;

View File

@@ -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))
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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()?;

View File

@@ -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);

View File

@@ -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;

View File

@@ -60,11 +60,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());

View File

@@ -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)?;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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())
}
}

View File

@@ -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,
}
}
}

View File

@@ -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()

View File

@@ -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, .. } =
self.env.init::<N>(AccessRights::RW, runtime)?;
let target = self.command.unwind_target(provider_factory.clone())?;

View File

@@ -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

View File

@@ -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() },

View File

@@ -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?;
@@ -343,6 +343,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 +352,7 @@ mod tests {
&config,
evm_config,
consensus,
runtime,
)
.await
.unwrap();
@@ -509,6 +511,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 +520,7 @@ mod tests {
&config,
evm_config,
consensus,
runtime,
)
.await
.unwrap();

View File

@@ -352,14 +352,23 @@ where
/// produce fewer state changes and most workers would be idle overhead.
const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
/// Transaction count threshold below which sequential signature recovery is used.
/// Transaction count threshold below which sequential conversion is used.
///
/// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
/// (work-stealing setup, channel-based reorder) exceeds the cost of sequential ECDSA
/// recovery. Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach`
/// for small blocks.
/// (work-stealing setup, channel-based reorder) exceeds the cost of sequential conversion.
/// Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach` for small
/// blocks.
const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
/// Number of leading transactions to convert sequentially before entering the rayon
/// parallel path.
///
/// Rayon's work-stealing does not guarantee that index 0 is processed first, so the
/// ordered consumer can block for up to ~1ms waiting for the first slot. By converting
/// a small head sequentially and sending it immediately, execution can start without
/// waiting for rayon scheduling.
const PARALLEL_PREFETCH_COUNT: usize = 4;
/// Returns the multiproof chunk size adapted to the block's gas usage.
///
/// For blocks with ≤20M gas used, a smaller chunk size (30) yields better throughput.
@@ -382,7 +391,7 @@ where
///
/// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
/// sequential iteration to avoid rayon overhead. For larger blocks, uses rayon parallel
/// iteration with [`ForEachOrdered`] to recover signatures in parallel while streaming
/// iteration with [`ForEachOrdered`] to convert transactions in parallel while streaming
/// results to execution in the original transaction order.
#[expect(clippy::type_complexity)]
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
@@ -401,7 +410,7 @@ where
// Empty block — nothing to do.
} else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
// Sequential path for small blocks — avoids rayon work-stealing setup and
// channel-based reorder overhead when it costs more than the ECDSA recovery itself.
// channel-based reorder overhead when it costs more than sequential conversion.
debug!(
target: "engine::tree::payload_processor",
transaction_count,
@@ -409,36 +418,38 @@ where
);
self.executor.spawn_blocking(move || {
let _enter =
debug_span!(target: "engine::tree::payload_processor", "tx iterator").entered();
debug_span!(target: "engine::tree::payload_processor", "tx_iterator").entered();
let (transactions, convert) = transactions.into_parts();
for (idx, tx) in transactions.into_iter().enumerate() {
let tx = convert.convert(tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
if let Ok(tx) = &tx {
let _ = prewarm_tx.send((idx, tx.clone()));
}
let _ = execute_tx.send(tx);
}
convert_serial(transactions.into_iter(), &convert, &prewarm_tx, &execute_tx);
});
} else {
// Parallel path — recover signatures in parallel on rayon, stream results
// to execution in order via `for_each_ordered`.
//
// To avoid a ~1ms stall waiting for rayon to schedule index 0, the first
// few transactions are recovered sequentially and sent immediately before
// entering the parallel iterator for the remainder.
let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
self.executor.spawn_blocking(move || {
let _enter =
debug_span!(target: "engine::tree::payload_processor", "tx iterator").entered();
debug_span!(target: "engine::tree::payload_processor", "tx_iterator").entered();
let (transactions, convert) = transactions.into_parts();
transactions
.into_par_iter()
let mut all: Vec<_> = transactions.into_iter().collect();
let rest = all.split_off(prefetch.min(all.len()));
// Convert the first few transactions sequentially so execution can
// start immediately without waiting for rayon work-stealing.
convert_serial(all.into_iter(), &convert, &prewarm_tx, &execute_tx);
// Convert the remaining transactions in parallel.
rest.into_par_iter()
.enumerate()
.map(|(idx, tx)| {
.map(|(i, tx)| {
let idx = i + prefetch;
let tx = convert.convert(tx);
tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
let tx = WithTxEnv { tx_env, tx: Arc::new(tx) };
// Send to prewarming out of order with the original index.
let _ = prewarm_tx.send((idx, tx.clone()));
tx
})
@@ -496,7 +507,7 @@ where
{
let to_prewarm_task = to_prewarm_task.clone();
self.executor.spawn_blocking(move || {
self.executor.spawn_blocking_named("prewarm", move || {
let mode = if skip_prewarm {
PrewarmMode::Skipped
} else if let Some(bal) = bal {
@@ -550,7 +561,7 @@ where
let executor = self.executor.clone();
let parent_span = Span::current();
self.executor.spawn_blocking(move || {
self.executor.spawn_blocking_named("sparse-trie", move || {
let _enter = debug_span!(target: "engine::tree::payload_processor", parent: parent_span, "sparse_trie_task")
.entered();
@@ -708,6 +719,30 @@ where
}
}
/// Converts transactions sequentially and sends them to the prewarm and execute channels.
fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
iter: impl Iterator<Item = RawTx>,
convert: &C,
prewarm_tx: &mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>,
execute_tx: &mpsc::SyncSender<Result<WithTxEnv<TxEnv, Recovered>, Err>>,
) where
Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
TxEnv: Clone,
C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
{
for (idx, raw_tx) in iter.enumerate() {
let tx = convert.convert(raw_tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
if let Ok(tx) = &tx {
let _ = prewarm_tx.send((idx, tx.clone()));
}
let _ = execute_tx.send(tx);
}
}
/// Handle to all the spawned tasks.
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the

View File

@@ -139,7 +139,7 @@ where
let ctx = self.ctx.clone();
let span = Span::current();
self.executor.spawn_blocking(move || {
self.executor.spawn_blocking_named("prewarm-spawn", move || {
let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: span, "spawn_all").entered();
let pool_threads = executor.prewarming_pool().current_num_threads();
@@ -624,7 +624,7 @@ where
let to_multi_proof = to_multi_proof.clone();
let done_tx = done_tx.clone();
let rx = tx_receiver.clone();
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: &span, "prewarm worker", idx);
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: &span, "prewarm_worker", idx);
task_executor.prewarming_pool().spawn(move || {
let _enter = span.entered();
ctx.transact_batch(rx, to_multi_proof, done_tx);

View File

@@ -31,7 +31,7 @@ use reth_trie_sparse::{
SparseTrie,
};
use revm_primitives::{hash_map::Entry, B256Map};
use tracing::{debug, debug_span, error, instrument};
use tracing::{debug, debug_span, error, instrument, trace_span};
/// Maximum number of pending/prewarm updates that we accumulate in memory before actually applying.
const MAX_PENDING_UPDATES: usize = 100;
@@ -118,7 +118,7 @@ where
let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
let parent_span = tracing::Span::current();
executor.spawn_blocking(move || {
executor.spawn_blocking_named("trie-hashing", move || {
let _span = debug_span!(parent: parent_span, "run_hashing_task").entered();
Self::run_hashing_task(updates, hashed_state_tx)
});
@@ -158,7 +158,7 @@ where
SparseTrieTaskMessage::PrefetchProofs(targets)
}
MultiProofMessage::StateUpdate(_, state) => {
let _span = debug_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing state update", n = state.len()).entered();
let _span = debug_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
let hashed = evm_state_to_hashed_post_state(state);
SparseTrieTaskMessage::HashedState(hashed)
}
@@ -478,7 +478,7 @@ where
})
.par_bridge_buffered()
.map(|(address, updates, mut fetched, mut trie)| {
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage trie leaf updates", a=%address).entered();
let _enter = trace_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_trie_leaf_updates", a=%address).entered();
let mut targets = Vec::new();
trie.update_leaves(updates, |path, min_len| match fetched.entry(path) {
@@ -577,7 +577,7 @@ where
})
.par_bridge_buffered()
.for_each(|(address, trie)| {
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage root", ?address).entered();
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_root", ?address).entered();
trie.root().expect("updates are drained, trie should be revealed by now");
});
drop(span);
@@ -594,18 +594,7 @@ where
return true;
} else if let Some(account) = account.take() {
let storage_root = self.trie.storage_root(addr).expect("updates are drained, storage trie should be revealed by now");
let encoded = if account.is_none_or(|account| account.is_empty()) &&
storage_root == EMPTY_ROOT_HASH
{
Vec::new()
} else {
account_rlp_buf.clear();
account
.unwrap_or_default()
.into_trie_account(storage_root)
.encode(account_rlp_buf);
account_rlp_buf.clone()
};
let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
return false;
@@ -613,13 +602,13 @@ where
}
// Get the current account state either from the trie or from latest account update.
let trie_account = if let Some(LeafUpdate::Changed(encoded)) = self.account_updates.get(addr) {
Some(encoded).filter(|encoded| !encoded.is_empty())
} else if !self.account_updates.contains_key(addr) {
self.trie.get_account_value(addr)
} else {
let trie_account = match self.account_updates.get(addr) {
Some(LeafUpdate::Changed(encoded)) => {
Some(encoded).filter(|encoded| !encoded.is_empty())
}
// Needs to be revealed first
return true;
Some(LeafUpdate::Touched) => return true,
None => self.trie.get_account_value(addr),
};
let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
@@ -636,13 +625,7 @@ where
(trie_account.map(Into::into), self.trie.storage_root(addr).expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
};
let encoded = if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
Vec::new()
} else {
account_rlp_buf.clear();
account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
account_rlp_buf.clone()
};
let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
@@ -699,6 +682,21 @@ where
}
}
/// RLP-encodes the account as a [`TrieAccount`] leaf value, or returns empty for deletions.
fn encode_account_leaf_value(
account: Option<Account>,
storage_root: B256,
account_rlp_buf: &mut Vec<u8>,
) -> Vec<u8> {
if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
return Vec::new();
}
account_rlp_buf.clear();
account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
account_rlp_buf.clone()
}
/// Message type for the sparse trie task.
enum SparseTrieTaskMessage {
/// A hashed state update ready to be processed.
@@ -726,7 +724,7 @@ pub struct StateRootComputeOutcome {
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{keccak256, Address, U256};
use alloy_primitives::{keccak256, Address, B256, U256};
use reth_trie_sparse::ParallelSparseTrie;
#[test]
@@ -777,4 +775,33 @@ mod tests {
assert!(hashed_state_rx.recv().is_err());
handle.join().unwrap();
}
#[test]
fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
let mut account_rlp_buf = vec![0xAB];
let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
assert!(encoded.is_empty());
// Early return should not touch the caller's buffer.
assert_eq!(account_rlp_buf, vec![0xAB]);
}
#[test]
fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
let storage_root = B256::from([0x99; 32]);
let account = Some(Account {
nonce: 7,
balance: U256::from(42),
bytecode_hash: Some(B256::from([0xAA; 32])),
});
let mut account_rlp_buf = vec![0x00, 0x01];
let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
assert_eq!(decoded.nonce, 7);
assert_eq!(decoded.balance, U256::from(42));
assert_eq!(decoded.storage_root, storage_root);
assert_eq!(account_rlp_buf, encoded);
}
}

View File

@@ -54,6 +54,9 @@ use std::{
};
use tracing::{debug, debug_span, error, info, instrument, trace, warn};
/// Handle to a [`HashedPostState`] computed on a background thread.
type LazyHashedPostState = reth_tasks::LazyHandle<HashedPostState>;
/// Context providing access to tree state during validation.
///
/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
@@ -338,12 +341,16 @@ where
BlockOrPayload::Payload(_) => {
let payload_clone = input.clone();
let validator = self.validator.clone();
let (tx, rx) = tokio::sync::oneshot::channel();
self.payload_processor.executor().spawn_blocking(move || {
let BlockOrPayload::Payload(payload) = payload_clone else { unreachable!() };
let _ = tx.send(validator.convert_payload_to_block(payload));
});
Either::Left(rx)
let handle = self.payload_processor.executor().spawn_blocking_named(
"payload-convert",
move || {
let BlockOrPayload::Payload(payload) = payload_clone else {
unreachable!()
};
validator.convert_payload_to_block(payload)
},
);
Either::Left(handle)
}
BlockOrPayload::Block(_) => Either::Right(()),
};
@@ -353,9 +360,7 @@ where
let convert_to_block =
move |input: BlockOrPayload<T>| -> Result<SealedBlock<N::Block>, NewPayloadError> {
match convert_to_block {
Either::Left(rx) => rx.blocking_recv().map_err(|_| {
NewPayloadError::Other("block conversion task panicked".into())
})?,
Either::Left(handle) => handle.try_into_inner().expect("sole handle"),
Either::Right(()) => {
let BlockOrPayload::Block(block) = input else { unreachable!() };
Ok(block)
@@ -395,7 +400,7 @@ where
trace!(target: "engine::tree::payload_validator", "Fetching block state provider");
let _enter =
debug_span!(target: "engine::tree::payload_validator", "state provider").entered();
debug_span!(target: "engine::tree::payload_validator", "state_provider").entered();
let Some(provider_builder) =
ensure_ok!(self.state_provider_builder(parent_hash, ctx.state()))
else {
@@ -420,7 +425,7 @@ where
.into())
};
let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm env")
let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm_env")
.in_scope(|| self.evm_env_for(&input))
.map_err(NewPayloadError::other)?;
@@ -507,6 +512,21 @@ where
// needed. This frees up resources while state root computation continues.
let valid_block_tx = handle.terminate_caching(Some(output.clone()));
// Spawn hashed post state computation in background so it runs concurrently with
// block conversion and receipt root computation. This is a pure CPU-bound task
// (keccak256 hashing of all changed addresses and storage slots).
let hashed_state_output = output.clone();
let hashed_state_provider = self.provider.clone();
let hashed_state: LazyHashedPostState =
self.payload_processor.executor().spawn_blocking_named("hash-post-state", move || {
let _span = debug_span!(
target: "engine::tree::payload_validator",
"hashed_post_state",
)
.entered();
hashed_state_provider.hashed_post_state(&hashed_state_output.state)
});
let block = convert_to_block(input)?.with_senders(senders);
// Wait for the receipt root computation to complete.
@@ -526,7 +546,8 @@ where
&parent_block,
&output,
&mut ctx,
receipt_root_bloom
receipt_root_bloom,
hashed_state,
),
block
);
@@ -762,20 +783,27 @@ where
{
debug!(target: "engine::tree::payload_validator", "Executing block");
let mut db = State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.without_state_clear()
.build();
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.without_state_clear()
.build()
});
let spec_id = *env.evm_env.spec_id();
let evm = self.evm_config.evm_with_env(&mut db, env.evm_env);
let ctx =
self.execution_ctx_for(input).map_err(|e| InsertBlockErrorKind::Other(Box::new(e)))?;
let mut executor = self.evm_config.create_executor(evm, ctx);
let (spec_id, mut executor) = {
let _span = debug_span!(target: "engine::tree", "create_evm").entered();
let spec_id = *env.evm_env.spec_id();
let evm = self.evm_config.evm_with_env(&mut db, env.evm_env);
let ctx = self
.execution_ctx_for(input)
.map_err(|e| InsertBlockErrorKind::Other(Box::new(e)))?;
let executor = self.evm_config.create_executor(evm, ctx);
(spec_id, executor)
};
if !self.config.precompile_cache_disabled() {
// Only cache pure precompiles to avoid issues with stateful precompiles
let _span = debug_span!(target: "engine::tree", "setup_precompile_cache").entered();
executor.evm_mut().precompiles_mut().map_pure_precompiles(|address, precompile| {
let metrics = self
.precompile_cache_metrics
@@ -797,7 +825,9 @@ where
let (receipt_tx, receipt_rx) = crossbeam_channel::unbounded();
let (result_tx, result_rx) = tokio::sync::oneshot::channel();
let task_handle = ReceiptRootTaskHandle::new(receipt_rx, result_tx);
self.payload_processor.executor().spawn_blocking(move || task_handle.run(receipts_len));
self.payload_processor
.executor()
.spawn_blocking_named("receipt-root", move || task_handle.run(receipts_len));
let transaction_count = input.transaction_count();
let executor = executor.with_state_hook(Some(Box::new(handle.state_hook())));
@@ -815,13 +845,13 @@ where
// Finish execution and get the result
let post_exec_start = Instant::now();
let (_evm, result) = debug_span!(target: "engine::tree", "finish")
let (_evm, result) = debug_span!(target: "engine::tree", "BlockExecutor::finish")
.in_scope(|| executor.finish())
.map(|(evm, result)| (evm.into_db(), result))?;
self.metrics.record_post_execution(post_exec_start.elapsed());
// Merge transitions into bundle state
debug_span!(target: "engine::tree", "merge transitions")
debug_span!(target: "engine::tree", "merge_transitions")
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
let output = BlockExecutionOutput { result, state: db.take_bundle() };
@@ -860,7 +890,7 @@ where
// Apply pre-execution changes (e.g., beacon root update)
let pre_exec_start = Instant::now();
debug_span!(target: "engine::tree", "pre execution")
debug_span!(target: "engine::tree", "pre_execution")
.in_scope(|| executor.apply_pre_execution_changes())?;
self.metrics.record_pre_execution(pre_exec_start.elapsed());
@@ -924,8 +954,9 @@ where
fn compute_state_root_parallel(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// ParallelStateRoot which parts of the trie need to be recomputed.
@@ -943,8 +974,9 @@ where
/// trie updates for this block.
fn compute_state_root_serial(
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<(B256, TrieUpdates)> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// StateRoot which parts of the trie need to be recomputed.
@@ -982,7 +1014,7 @@ where
&self,
handle: &mut PayloadHandle<Tx, Err, R>,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
let Some(timeout) = self.config.state_root_task_timeout() else {
return Ok(handle.state_root());
@@ -1008,7 +1040,7 @@ where
let seq_overlay = overlay_factory;
let seq_hashed_state = hashed_state.clone();
self.payload_processor.executor().spawn_blocking(move || {
self.payload_processor.executor().spawn_blocking_named("serial-root", move || {
let result = Self::compute_state_root_serial(seq_overlay, &seq_hashed_state);
let _ = seq_tx.send(result);
});
@@ -1074,7 +1106,7 @@ where
fn compare_trie_updates_with_serial(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
debug!(target: "engine::tree::payload_validator", "Comparing trie updates with serial computation");
@@ -1090,17 +1122,20 @@ where
// Get a database provider to use as trie cursor factory
match overlay_factory.database_provider_ro() {
Ok(provider) => {
if let Err(err) = super::trie_updates::compare_trie_updates(
match super::trie_updates::compare_trie_updates(
&provider,
task_trie_updates,
serial_trie_updates,
) {
warn!(
target: "engine::tree::payload_validator",
%err,
"Error comparing trie updates"
);
return true;
Ok(has_diff) => return has_diff,
Err(err) => {
warn!(
target: "engine::tree::payload_validator",
%err,
"Error comparing trie updates"
);
return true;
}
}
}
Err(err) => {
@@ -1170,6 +1205,8 @@ where
///
/// If `receipt_root_bloom` is provided, it will be used instead of computing the receipt root
/// and logs bloom from the receipts.
///
/// The `hashed_state` handle wraps the background hashed post state computation.
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn validate_post_execution<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
&self,
@@ -1178,7 +1215,8 @@ where
output: &BlockExecutionOutput<N::Receipt>,
ctx: &mut TreeCtx<'_, N>,
receipt_root_bloom: Option<ReceiptRootBloom>,
) -> Result<HashedPostState, InsertBlockErrorKind>
hashed_state: LazyHashedPostState,
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
{
@@ -1213,14 +1251,10 @@ where
}
drop(_enter);
let _enter =
debug_span!(target: "engine::tree::payload_validator", "hashed_post_state").entered();
let hashed_state = self.provider.hashed_post_state(&output.state);
drop(_enter);
let _enter = debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution_with_hashed_state").entered();
if let Err(err) =
self.validator.validate_block_post_execution_with_hashed_state(&hashed_state, block)
if let Err(err) = self
.validator
.validate_block_post_execution_with_hashed_state(hashed_state.get(), block)
{
// call post-block hook
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
@@ -1444,7 +1478,7 @@ where
block: RecoveredBlock<N::Block>,
execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
ctx: &TreeCtx<'_, N>,
hashed_state: HashedPostState,
hashed_state: LazyHashedPostState,
trie_output: TrieUpdates,
overlay_factory: OverlayStateProviderFactory<P>,
) -> ExecutedBlock<N> {
@@ -1461,12 +1495,14 @@ where
overlay_blocks.iter().rev().map(|b| b.trie_data_handle()).collect();
// Create deferred handle with fallback inputs in case the background task hasn't completed.
let deferred_trie_data = DeferredTrieData::pending(
Arc::new(hashed_state),
Arc::new(trie_output),
anchor_hash,
ancestors,
);
// Resolve the lazy handle into Arc<HashedPostState>. By this point the hashed state has
// already been computed and used for state root verification, so .get() returns instantly.
let hashed_state = match hashed_state.try_into_inner() {
Ok(state) => Arc::new(state),
Err(handle) => Arc::new(handle.get().clone()),
};
let deferred_trie_data =
DeferredTrieData::pending(hashed_state, Arc::new(trie_output), anchor_hash, ancestors);
let deferred_handle_task = deferred_trie_data.clone();
let block_validation_metrics = self.metrics.block_validation.clone();
@@ -1552,7 +1588,9 @@ where
};
// Spawn task that computes trie data asynchronously.
self.payload_processor.executor().spawn_blocking(compute_trie_input_task);
self.payload_processor
.executor()
.spawn_blocking_named("trie-input", compute_trie_input_task);
ExecutedBlock::with_deferred_trie_data(
Arc::new(block),

View File

@@ -101,11 +101,13 @@ impl StorageTrieUpdatesDiff {
/// Compares the trie updates from state root task, regular state root calculation and database,
/// and logs the differences if there's any.
///
/// Returns `true` if there are differences.
pub(crate) fn compare_trie_updates(
trie_cursor_factory: impl TrieCursorFactory,
task: TrieUpdates,
regular: TrieUpdates,
) -> Result<(), DatabaseError> {
) -> Result<bool, DatabaseError> {
let mut task = adjust_trie_updates(task);
let mut regular = adjust_trie_updates(regular);
@@ -179,9 +181,10 @@ pub(crate) fn compare_trie_updates(
}
// log differences
let has_differences = diff.has_differences();
diff.log_differences();
Ok(())
Ok(has_differences)
}
fn compare_storage_trie_updates<C: TrieCursor>(

View File

@@ -106,6 +106,11 @@ where
self.cli.logs.log_file_directory.join(chain_spec.chain().to_string());
}
// Apply node-specific log defaults before initializing tracing
if matches!(self.cli.command, Commands::Node(_)) {
self.cli.logs.apply_node_defaults();
}
self.init_tracing(&runner)?;
// Install the prometheus recorder to be sure to record all metrics
@@ -144,6 +149,8 @@ where
N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
SubCmd: ExtendedCommand + Subcommand + fmt::Debug,
{
let rt = runner.runtime();
match cli.command {
Commands::Node(command) => {
// Validate RPC modules using the configured validator
@@ -169,13 +176,13 @@ where
command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
})
}
Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::InitState(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::InitState(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::Import(command) => {
runner.run_blocking_until_ctrl_c(command.execute::<N, _>(components))
runner.run_blocking_until_ctrl_c(command.execute::<N, _>(components, rt))
}
Commands::ImportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::ExportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::ImportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::ExportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
Commands::Db(command) => {
runner.run_blocking_command_until_exit(|ctx| command.execute::<N>(ctx))
@@ -189,7 +196,9 @@ where
Commands::Prune(command) => runner.run_command_until_exit(|ctx| command.execute::<N>(ctx)),
#[cfg(feature = "dev")]
Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
Commands::ReExecute(command) => runner.run_until_ctrl_c(command.execute::<N>(components)),
Commands::ReExecute(command) => {
runner.run_until_ctrl_c(command.execute::<N>(components, rt))
}
Commands::Ext(command) => command.execute(runner),
}
}

View File

@@ -209,6 +209,12 @@ impl<
self.logs.log_file_directory =
self.logs.log_file_directory.join(chain_spec.chain().to_string());
}
// Apply node-specific log defaults before initializing tracing
if matches!(self.command, Commands::Node(_)) {
self.logs.apply_node_defaults();
}
let _guard = self.init_tracing(&runner, Layers::new())?;
// Install the prometheus recorder to be sure to record all metrics
@@ -445,6 +451,42 @@ mod tests {
assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
}
#[test]
fn log_file_max_files_defaults() {
use reth_node_core::args::LogArgs;
// Node command without explicit --log.file.max-files should get Some(5) after
// apply_node_defaults
let mut cli = Cli::try_parse_args_from(["reth", "node"]).unwrap();
assert!(cli.logs.log_file_max_files.is_none());
cli.logs.apply_node_defaults();
assert_eq!(cli.logs.log_file_max_files, Some(LogArgs::DEFAULT_MAX_LOG_FILES_NODE));
// Non-node command without explicit --log.file.max-files should be None and
// effective_log_file_max_files returns 0
let cli = Cli::try_parse_args_from(["reth", "config"]).unwrap();
assert!(cli.logs.log_file_max_files.is_none());
assert_eq!(cli.logs.effective_log_file_max_files(), 0);
// Explicitly set value should be preserved for node command
let mut cli =
Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "10"]).unwrap();
assert_eq!(cli.logs.log_file_max_files, Some(10));
cli.logs.apply_node_defaults();
assert_eq!(cli.logs.log_file_max_files, Some(10));
// Explicitly set value should be preserved for non-node command
let cli =
Cli::try_parse_args_from(["reth", "config", "--log.file.max-files", "3"]).unwrap();
assert_eq!(cli.logs.log_file_max_files, Some(3));
assert_eq!(cli.logs.effective_log_file_max_files(), 3);
// Setting to 0 explicitly should work
let cli = Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "0"]).unwrap();
assert_eq!(cli.logs.log_file_max_files, Some(0));
assert_eq!(cli.logs.effective_log_file_max_files(), 0);
}
#[test]
fn parse_env_filter_directives() {
let temp_dir = tempfile::tempdir().unwrap();

View File

@@ -55,7 +55,7 @@ where
EthereumBuilderConfig::new()
.with_gas_limit(gas_limit)
.with_max_blobs_per_block(conf.max_blobs_per_block())
.with_extra_data(conf.extra_data_bytes()),
.with_extra_data(conf.extra_data()),
))
}
}

View File

@@ -20,7 +20,7 @@ use std::{
#[tokio::test]
async fn can_handle_blobs() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
let chain_spec = Arc::new(
@@ -91,7 +91,7 @@ async fn can_handle_blobs() -> eyre::Result<()> {
#[tokio::test]
async fn can_send_legacy_sidecar_post_activation() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
let chain_spec = Arc::new(
@@ -144,7 +144,7 @@ async fn can_send_legacy_sidecar_post_activation() -> eyre::Result<()> {
#[tokio::test]
async fn blob_conversion_at_osaka() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let current_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Osaka activates in 2 slots

View File

@@ -15,7 +15,7 @@ use std::sync::Arc;
#[tokio::test]
async fn can_run_dev_node() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let node_config = NodeConfig::test()
.with_chain(custom_chain())
@@ -36,7 +36,7 @@ async fn can_run_dev_node() -> eyre::Result<()> {
#[tokio::test]
async fn can_run_dev_node_custom_attributes() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let node_config = NodeConfig::test()
.with_chain(custom_chain())

View File

@@ -57,7 +57,7 @@ async fn can_run_eth_node() -> eyre::Result<()> {
#[cfg(unix)]
async fn can_run_eth_node_with_auth_engine_api_over_ipc() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
// Chain spec with test allocs
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
@@ -104,7 +104,7 @@ async fn can_run_eth_node_with_auth_engine_api_over_ipc() -> eyre::Result<()> {
#[cfg(unix)]
async fn test_failed_run_eth_node_with_no_auth_engine_api_over_ipc_opts() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
// Chain spec with test allocs
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
@@ -188,7 +188,7 @@ async fn test_engine_graceful_shutdown() -> eyre::Result<()> {
#[tokio::test]
async fn test_testing_build_block_v1_osaka() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
let chain_spec = Arc::new(

View File

@@ -24,7 +24,7 @@ use std::{sync::Arc, time::Duration};
#[tokio::test]
async fn maintain_txpool_stale_eviction() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let txpool = Pool::new(
OkValidator::default(),
@@ -97,7 +97,7 @@ async fn maintain_txpool_stale_eviction() -> eyre::Result<()> {
#[tokio::test]
async fn maintain_txpool_reorg() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let txpool = Pool::new(
OkValidator::default(),
@@ -229,7 +229,7 @@ async fn maintain_txpool_reorg() -> eyre::Result<()> {
#[tokio::test]
async fn maintain_txpool_commit() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let txpool = Pool::new(
OkValidator::default(),

View File

@@ -29,7 +29,7 @@ async fn debug_trace_call_matches_geth_prestate_snapshot() -> Result<()> {
let mut genesis: Genesis = MAINNET.genesis().clone();
genesis.coinbase = address!("0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5");
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let expected_frame = expected_snapshot_frame()?;
let prestate_mode = match &expected_frame {

View File

@@ -344,7 +344,7 @@ async fn test_eth_config() -> eyre::Result<()> {
async fn test_admin_external_ip() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
// Chain spec with test allocs
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();

View File

@@ -46,7 +46,7 @@ fn test_basic_setup() {
#[tokio::test]
async fn test_eth_launcher() {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let config = NodeConfig::test();
let db = create_test_rw_db();
let _builder =
@@ -81,7 +81,7 @@ fn test_eth_launcher_with_tokio_runtime() {
let custom_rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
main_rt.block_on(async {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let config = NodeConfig::test();
let db = create_test_rw_db();
let _builder =

View File

@@ -20,7 +20,7 @@ use tokio::sync::oneshot;
#[tokio::test(flavor = "multi_thread")]
async fn testing_rpc_build_block_works() -> eyre::Result<()> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let mut rpc_args = reth_node_core::args::RpcServerArgs::default().with_http();
rpc_args.http_api = Some(RpcModuleSelection::from_iter([RethRpcModule::Testing]));
let tempdir = tempdir().expect("temp datadir");

View File

@@ -15,35 +15,27 @@ workspace = true
# reth
reth-codecs = { workspace = true, optional = true }
reth-primitives-traits.workspace = true
reth-zstd-compressors = { workspace = true, optional = true }
# ethereum
alloy-eips = { workspace = true, features = ["k256"] }
alloy-primitives.workspace = true
alloy-consensus = { workspace = true, features = ["serde"] }
alloy-serde = { workspace = true, optional = true }
alloy-rpc-types-eth = { workspace = true, optional = true }
alloy-rlp.workspace = true
# misc
arbitrary = { workspace = true, optional = true, features = ["derive"] }
modular-bitfield = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_with = { workspace = true, optional = true }
[dev-dependencies]
derive_more.workspace = true
arbitrary.workspace = true
bincode.workspace = true
proptest.workspace = true
proptest-arbitrary-interop.workspace = true
rand_08.workspace = true
rand.workspace = true
alloy-rlp.workspace = true
reth-codecs = { workspace = true, features = ["test-utils"] }
reth-zstd-compressors.workspace = true
secp256k1 = { workspace = true, features = ["rand"] }
alloy-consensus = { workspace = true, features = ["serde", "arbitrary"] }
serde_json.workspace = true
serde_with.workspace = true
[features]
default = ["std"]
@@ -54,27 +46,21 @@ test-utils = [
std = [
"alloy-consensus/std",
"alloy-primitives/std",
"alloy-rlp/std",
"reth-primitives-traits/std",
"reth-zstd-compressors?/std",
"serde?/std",
"alloy-eips/std",
"derive_more/std",
"serde_with?/std",
"secp256k1/std",
"alloy-rpc-types-eth?/std",
"alloy-serde?/std",
"alloy-rlp/std",
"reth-zstd-compressors/std",
"serde_json/std",
"serde_with/std",
]
reth-codec = [
"std",
"dep:reth-codecs",
"dep:modular-bitfield",
"dep:reth-zstd-compressors",
]
arbitrary = [
"std",
"dep:arbitrary",
"alloy-consensus/arbitrary",
"alloy-consensus/k256",
"alloy-primitives/arbitrary",
@@ -82,10 +68,8 @@ arbitrary = [
"reth-primitives-traits/arbitrary",
"alloy-eips/arbitrary",
"alloy-rpc-types-eth?/arbitrary",
"alloy-serde?/arbitrary",
]
serde-bincode-compat = [
"dep:serde_with",
"alloy-consensus/serde-bincode-compat",
"alloy-eips/serde-bincode-compat",
"reth-primitives-traits/serde-bincode-compat",
@@ -93,15 +77,12 @@ serde-bincode-compat = [
]
serde = [
"dep:serde",
"dep:alloy-serde",
"alloy-consensus/serde",
"alloy-eips/serde",
"alloy-primitives/serde",
"reth-codecs?/serde",
"reth-primitives-traits/serde",
"rand/serde",
"rand_08/serde",
"secp256k1/serde",
"alloy-rpc-types-eth?/serde",
"rand/serde",
]
rpc = ["dep:alloy-rpc-types-eth"]

View File

@@ -9,15 +9,13 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
// Feature-only dep: activated by `reth-codec` feature for downstream consumers.
#[cfg(feature = "reth-codec")]
use reth_codecs as _;
mod receipt;
pub use receipt::*;
/// Kept for consistency tests
#[cfg(test)]
mod transaction;
pub use alloy_consensus::{transaction::PooledTransaction, TxType};
use alloy_consensus::{TxEip4844, TxEip4844WithSidecar};
use alloy_eips::eip7594::BlobTransactionSidecarVariant;
@@ -35,8 +33,7 @@ pub type PooledTransactionVariant =
/// Bincode-compatible serde implementations.
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub mod serde_bincode_compat {
pub use super::receipt::serde_bincode_compat::*;
pub use alloy_consensus::serde_bincode_compat::transaction::*;
pub use alloy_consensus::serde_bincode_compat::{transaction::*, EthereumReceipt as Receipt};
}
/// Type alias for the ethereum block

View File

@@ -1,42 +1,8 @@
use core::fmt::Debug;
use alloc::vec::Vec;
use alloy_consensus::{
Eip2718DecodableReceipt, Eip2718EncodableReceipt, Eip658Value, ReceiptEnvelope,
ReceiptWithBloom, RlpDecodableReceipt, RlpEncodableReceipt, TxReceipt, TxType, Typed2718,
};
use alloy_eips::eip2718::{Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718};
use alloy_primitives::{Bloom, Log, B256};
use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable};
use reth_primitives_traits::{proofs::ordered_trie_root_with_encoder, InMemorySize};
/// Helper trait alias with requirements for transaction type generic to be used within [`Receipt`].
pub trait TxTy:
Debug
+ Copy
+ Eq
+ Send
+ Sync
+ InMemorySize
+ Typed2718
+ TryFrom<u8, Error = Eip2718Error>
+ Decodable
+ 'static
{
}
impl<T> TxTy for T where
T: Debug
+ Copy
+ Eq
+ Send
+ Sync
+ InMemorySize
+ Typed2718
+ TryFrom<u8, Error = Eip2718Error>
+ Decodable
+ 'static
{
}
use alloy_consensus::TxType;
pub use alloy_consensus::{EthereumReceipt, TxTy};
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::B256;
use reth_primitives_traits::proofs::ordered_trie_root_with_encoder;
/// Raw ethereum receipt.
pub type Receipt<T = TxType> = EthereumReceipt<T>;
@@ -45,528 +11,25 @@ pub type Receipt<T = TxType> = EthereumReceipt<T>;
/// Receipt representation for RPC.
pub type RpcReceipt<T = TxType> = EthereumReceipt<T, alloy_rpc_types_eth::Log>;
/// Typed ethereum transaction receipt.
/// Receipt containing result of transaction execution.
#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "reth-codec", reth_codecs::add_arbitrary_tests(compact, rlp))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct EthereumReceipt<T = TxType, L = Log> {
/// Receipt type.
#[cfg_attr(feature = "serde", serde(rename = "type"))]
pub tx_type: T,
/// If transaction is executed successfully.
///
/// This is the `statusCode`
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "status"))]
pub success: bool,
/// Gas used
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
pub cumulative_gas_used: u64,
/// Log send from contracts.
pub logs: Vec<L>,
/// Calculates the receipt root for a header for the reference type of [`Receipt`].
///
/// NOTE: Prefer `proofs::calculate_receipt_root` if you have log blooms memoized.
pub fn calculate_receipt_root_no_memo<T: TxTy>(receipts: &[Receipt<T>]) -> B256 {
ordered_trie_root_with_encoder(receipts, |r, buf| {
alloy_consensus::TxReceipt::with_bloom_ref(r).encode_2718(buf)
})
}
#[cfg(feature = "rpc")]
impl<T> Receipt<T> {
/// Converts the logs of the receipt to RPC logs.
pub fn into_rpc(
self,
next_log_index: usize,
meta: alloy_consensus::transaction::TransactionMeta,
) -> RpcReceipt<T> {
let Self { tx_type, success, cumulative_gas_used, logs } = self;
let logs = alloy_rpc_types_eth::Log::collect_for_receipt(next_log_index, meta, logs);
RpcReceipt { tx_type, success, cumulative_gas_used, logs }
}
}
impl<T: TxTy> Receipt<T> {
/// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
self.success.length() +
self.cumulative_gas_used.length() +
bloom.length() +
self.logs.length()
}
/// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) {
self.success.encode(out);
self.cumulative_gas_used.encode(out);
bloom.encode(out);
self.logs.encode(out);
}
/// Returns RLP header for inner encoding.
pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header {
Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) }
}
/// RLP-decodes the receipt from the provided buffer. This does not expect a type byte or
/// network header.
pub fn rlp_decode_inner(
buf: &mut &[u8],
tx_type: T,
) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
let header = Header::decode(buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString);
}
let remaining = buf.len();
let success = Decodable::decode(buf)?;
let cumulative_gas_used = Decodable::decode(buf)?;
let logs_bloom = Decodable::decode(buf)?;
let logs = Decodable::decode(buf)?;
if buf.len() + header.payload_length != remaining {
return Err(alloy_rlp::Error::UnexpectedLength);
}
Ok(ReceiptWithBloom {
receipt: Self { cumulative_gas_used, tx_type, success, logs },
logs_bloom,
})
}
/// Calculates the receipt root for a header for the reference type of [Receipt].
///
/// NOTE: Prefer `proofs::calculate_receipt_root` if you have log blooms memoized.
pub fn calculate_receipt_root_no_memo(receipts: &[Self]) -> B256 {
ordered_trie_root_with_encoder(receipts, |r, buf| r.with_bloom_ref().encode_2718(buf))
}
}
impl<T: TxTy> Eip2718EncodableReceipt for Receipt<T> {
fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
!self.tx_type.is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload()
}
fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
if !self.tx_type.is_legacy() {
out.put_u8(self.tx_type.ty());
}
self.rlp_header_inner(bloom).encode(out);
self.rlp_encode_fields(bloom, out);
}
}
impl<T: TxTy> Eip2718DecodableReceipt for Receipt<T> {
fn typed_decode_with_bloom(ty: u8, buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
Ok(Self::rlp_decode_inner(buf, T::try_from(ty)?)?)
}
fn fallback_decode_with_bloom(buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
Ok(Self::rlp_decode_inner(buf, T::try_from(0)?)?)
}
}
impl<T: TxTy> RlpEncodableReceipt for Receipt<T> {
fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
let mut len = self.eip2718_encoded_length_with_bloom(bloom);
if !self.tx_type.is_legacy() {
len += Header {
list: false,
payload_length: self.eip2718_encoded_length_with_bloom(bloom),
}
.length();
}
len
}
fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
if !self.tx_type.is_legacy() {
Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) }
.encode(out);
}
self.eip2718_encode_with_bloom(bloom, out);
}
}
impl<T: TxTy> RlpDecodableReceipt for Receipt<T> {
fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
let header_buf = &mut &**buf;
let header = Header::decode(header_buf)?;
// Legacy receipt, reuse initial buffer without advancing
if header.list {
return Self::rlp_decode_inner(buf, T::try_from(0)?)
}
// Otherwise, advance the buffer and try decoding type flag followed by receipt
*buf = *header_buf;
let remaining = buf.len();
let tx_type = T::decode(buf)?;
let this = Self::rlp_decode_inner(buf, tx_type)?;
if buf.len() + header.payload_length != remaining {
return Err(alloy_rlp::Error::UnexpectedLength);
}
Ok(this)
}
}
impl<T, L> TxReceipt for EthereumReceipt<T, L>
where
T: TxTy,
L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
{
type Log = L;
fn status_or_post_state(&self) -> Eip658Value {
self.success.into()
}
fn status(&self) -> bool {
self.success
}
fn bloom(&self) -> Bloom {
alloy_primitives::logs_bloom(self.logs.iter().map(|l| l.as_ref()))
}
fn cumulative_gas_used(&self) -> u64 {
self.cumulative_gas_used
}
fn logs(&self) -> &[L] {
&self.logs
}
fn into_logs(self) -> Vec<L> {
self.logs
}
}
impl<T: TxTy> Typed2718 for Receipt<T> {
fn ty(&self) -> u8 {
self.tx_type.ty()
}
}
impl<T: TxTy> IsTyped2718 for Receipt<T> {
fn is_type(type_id: u8) -> bool {
<TxType as IsTyped2718>::is_type(type_id)
}
}
impl<T: TxTy> InMemorySize for Receipt<T> {
fn size(&self) -> usize {
size_of::<Self>() + self.logs.iter().map(|log| log.size()).sum::<usize>()
}
}
impl<T> From<ReceiptEnvelope<T>> for Receipt<TxType>
where
T: Into<Log>,
{
fn from(value: ReceiptEnvelope<T>) -> Self {
let value = value.into_primitives_receipt();
Self {
tx_type: value.tx_type(),
success: value.is_success(),
cumulative_gas_used: value.cumulative_gas_used(),
logs: value.into_logs(),
}
}
}
impl<T, L> From<EthereumReceipt<T, L>> for alloy_consensus::Receipt<L> {
fn from(value: EthereumReceipt<T, L>) -> Self {
Self {
status: value.success.into(),
cumulative_gas_used: value.cumulative_gas_used,
logs: value.logs,
}
}
}
impl<L> From<EthereumReceipt<TxType, L>> for ReceiptEnvelope<L>
where
L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
{
fn from(value: EthereumReceipt<TxType, L>) -> Self {
let tx_type = value.tx_type;
let receipt = value.into_with_bloom().map_receipt(Into::into);
match tx_type {
TxType::Legacy => Self::Legacy(receipt),
TxType::Eip2930 => Self::Eip2930(receipt),
TxType::Eip1559 => Self::Eip1559(receipt),
TxType::Eip4844 => Self::Eip4844(receipt),
TxType::Eip7702 => Self::Eip7702(receipt),
}
}
}
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub(super) mod serde_bincode_compat {
use alloc::{borrow::Cow, vec::Vec};
use alloy_consensus::TxType;
use alloy_eips::eip2718::Eip2718Error;
use alloy_primitives::{Log, U8};
use core::fmt::Debug;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs};
/// Bincode-compatible [`super::Receipt`] serde implementation.
///
/// Intended to use with the [`serde_with::serde_as`] macro in the following way:
/// ```rust
/// use alloy_consensus::TxType;
/// use reth_ethereum_primitives::{serde_bincode_compat, Receipt};
/// use serde::{de::DeserializeOwned, Deserialize, Serialize};
/// use serde_with::serde_as;
///
/// #[serde_as]
/// #[derive(Serialize, Deserialize)]
/// struct Data {
/// #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
/// receipt: Receipt<TxType>,
/// }
/// ```
#[derive(Debug, Serialize, Deserialize)]
#[serde(bound(deserialize = "T: TryFrom<u8, Error = Eip2718Error>"))]
pub struct Receipt<'a, T = TxType> {
/// Receipt type.
#[serde(deserialize_with = "deserde_txtype")]
pub tx_type: T,
/// If transaction is executed successfully.
///
/// This is the `statusCode`
pub success: bool,
/// Gas used
pub cumulative_gas_used: u64,
/// Log send from contracts.
pub logs: Cow<'a, Vec<Log>>,
}
/// Ensures that txtype is deserialized symmetrically as U8
fn deserde_txtype<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: TryFrom<u8, Error = Eip2718Error>,
{
U8::deserialize(deserializer)?.to::<u8>().try_into().map_err(serde::de::Error::custom)
}
impl<'a, T: Copy> From<&'a super::Receipt<T>> for Receipt<'a, T> {
fn from(value: &'a super::Receipt<T>) -> Self {
Self {
tx_type: value.tx_type,
success: value.success,
cumulative_gas_used: value.cumulative_gas_used,
logs: Cow::Borrowed(&value.logs),
}
}
}
impl<'a, T> From<Receipt<'a, T>> for super::Receipt<T> {
fn from(value: Receipt<'a, T>) -> Self {
Self {
tx_type: value.tx_type,
success: value.success,
cumulative_gas_used: value.cumulative_gas_used,
logs: value.logs.into_owned(),
}
}
}
impl<T: Copy + Serialize> SerializeAs<super::Receipt<T>> for Receipt<'_, T> {
fn serialize_as<S>(source: &super::Receipt<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Receipt::<'_>::from(source).serialize(serializer)
}
}
impl<'de, T: TryFrom<u8, Error = Eip2718Error>> DeserializeAs<'de, super::Receipt<T>>
for Receipt<'de, T>
{
fn deserialize_as<D>(deserializer: D) -> Result<super::Receipt<T>, D::Error>
where
D: Deserializer<'de>,
{
Receipt::<'_, T>::deserialize(deserializer).map(Into::into)
}
}
impl<T> reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat for super::Receipt<T>
where
T: Copy + Serialize + TryFrom<u8, Error = Eip2718Error> + Debug + 'static,
{
type BincodeRepr<'a> = Receipt<'a, T>;
fn as_repr(&self) -> Self::BincodeRepr<'_> {
self.into()
}
fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
repr.into()
}
}
#[cfg(test)]
mod tests {
use crate::{receipt::serde_bincode_compat, Receipt};
use alloy_consensus::TxType;
use arbitrary::Arbitrary;
use rand::Rng;
use serde_with::serde_as;
#[test]
fn test_receipt_bincode_roundtrip() {
#[serde_as]
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct Data {
#[serde_as(as = "serde_bincode_compat::Receipt<'_, TxType>")]
receipt: Receipt<TxType>,
}
let mut bytes = [0u8; 1024];
rand::rng().fill(bytes.as_mut_slice());
let data = Data {
receipt: Receipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
};
let encoded = bincode::serialize(&data).unwrap();
let decoded: Data = bincode::deserialize(&encoded).unwrap();
assert_eq!(decoded, data);
}
}
}
#[cfg(feature = "reth-codec")]
mod compact {
use super::*;
use reth_codecs::{
Compact,
__private::{modular_bitfield::prelude::*, Buf},
};
impl Receipt {
#[doc = "Used bytes by [`ReceiptFlags`]"]
pub const fn bitflag_encoded_bytes() -> usize {
1u8 as usize
}
#[doc = "Unused bits for new fields by [`ReceiptFlags`]"]
pub const fn bitflag_unused_bits() -> usize {
0u8 as usize
}
}
#[allow(non_snake_case, unused_parens)]
mod flags {
use super::*;
#[doc = "Fieldset that facilitates compacting the parent type. Used bytes: 1 | Unused bits: 0"]
#[bitfield]
#[derive(Clone, Copy, Debug, Default)]
pub struct ReceiptFlags {
pub tx_type_len: B2,
pub success_len: B1,
pub cumulative_gas_used_len: B4,
pub __zstd: B1,
}
impl ReceiptFlags {
#[doc = r" Deserializes this fieldset and returns it, alongside the original slice in an advanced position."]
pub fn from(mut buf: &[u8]) -> (Self, &[u8]) {
(Self::from_bytes([buf.get_u8()]), buf)
}
}
}
pub use flags::ReceiptFlags;
impl<T: Compact> Compact for Receipt<T> {
fn to_compact<B>(&self, buf: &mut B) -> usize
where
B: reth_codecs::__private::bytes::BufMut + AsMut<[u8]>,
{
let mut flags = ReceiptFlags::default();
let mut total_length = 0;
let mut buffer = reth_codecs::__private::bytes::BytesMut::new();
let tx_type_len = self.tx_type.to_compact(&mut buffer);
flags.set_tx_type_len(tx_type_len as u8);
let success_len = self.success.to_compact(&mut buffer);
flags.set_success_len(success_len as u8);
let cumulative_gas_used_len = self.cumulative_gas_used.to_compact(&mut buffer);
flags.set_cumulative_gas_used_len(cumulative_gas_used_len as u8);
self.logs.to_compact(&mut buffer);
let zstd = buffer.len() > 7;
if zstd {
flags.set___zstd(1);
}
let flags = flags.into_bytes();
total_length += flags.len() + buffer.len();
buf.put_slice(&flags);
if zstd {
reth_zstd_compressors::with_receipt_compressor(|compressor| {
let compressed = compressor.compress(&buffer).expect("Failed to compress.");
buf.put(compressed.as_slice());
});
} else {
buf.put(buffer);
}
total_length
}
fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
let (flags, mut buf) = ReceiptFlags::from(buf);
if flags.__zstd() != 0 {
reth_zstd_compressors::with_receipt_decompressor(|decompressor| {
let decompressed = decompressor.decompress(buf);
let original_buf = buf;
let mut buf: &[u8] = decompressed;
let (tx_type, new_buf) = T::from_compact(buf, flags.tx_type_len() as usize);
buf = new_buf;
let (success, new_buf) = bool::from_compact(buf, flags.success_len() as usize);
buf = new_buf;
let (cumulative_gas_used, new_buf) =
u64::from_compact(buf, flags.cumulative_gas_used_len() as usize);
buf = new_buf;
let (logs, _) = Vec::from_compact(buf, buf.len());
(Self { tx_type, success, cumulative_gas_used, logs }, original_buf)
})
} else {
let (tx_type, new_buf) = T::from_compact(buf, flags.tx_type_len() as usize);
buf = new_buf;
let (success, new_buf) = bool::from_compact(buf, flags.success_len() as usize);
buf = new_buf;
let (cumulative_gas_used, new_buf) =
u64::from_compact(buf, flags.cumulative_gas_used_len() as usize);
buf = new_buf;
let (logs, new_buf) = Vec::from_compact(buf, buf.len());
buf = new_buf;
let obj = Self { tx_type, success, cumulative_gas_used, logs };
(obj, buf)
}
}
}
}
#[cfg(feature = "reth-codec")]
pub use compact::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::TransactionSigned;
use alloy_consensus::{ReceiptWithBloom, TxReceipt, TxType};
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::{
address, b256, bloom, bytes, hex_literal::hex, Address, Bytes, Log, LogData,
address, b256, bloom, bytes, hex_literal::hex, Address, Bloom, Bytes, Log, LogData,
};
use alloy_rlp::Decodable;
use alloy_rlp::{Decodable, Encodable};
use reth_codecs::Compact;
use reth_primitives_traits::proofs::{
calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root,
@@ -784,6 +247,8 @@ mod tests {
#[test]
#[cfg(feature = "rpc")]
fn test_receipt_serde() {
use alloy_consensus::ReceiptEnvelope;
let input = r#"{"status":"0x1","cumulativeGasUsed":"0x175cc0e","logs":[{"address":"0xa18b9ca2a78660d44ab38ae72e72b18792ffe413","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000e7e7d8006cbff47bc6ac2dabf592c98e97502708","0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"],"data":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","blockHash":"0xbf9e6a368a399f996a0f0b27cab4191c028c3c99f5f76ea08a5b70b961475fcb","blockNumber":"0x164b59f","blockTimestamp":"0x68c9a713","transactionHash":"0x533aa9e57865675bb94f41aa2895c0ac81eee69686c77af16149c301e19805f1","transactionIndex":"0x14d","logIndex":"0x238","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000400000040000000000000004000000000000000000000000000000000000000000000020000000000000000000000000080000000000000000000000000200000020000000000000000000000000000000000000000000000000000000000000020000010000000000000000000000000000000000000000000000000000000000000","type":"0x2","transactionHash":"0x533aa9e57865675bb94f41aa2895c0ac81eee69686c77af16149c301e19805f1","transactionIndex":"0x14d","blockHash":"0xbf9e6a368a399f996a0f0b27cab4191c028c3c99f5f76ea08a5b70b961475fcb","blockNumber":"0x164b59f","gasUsed":"0xb607","effectiveGasPrice":"0x4a3ee768","from":"0xe7e7d8006cbff47bc6ac2dabf592c98e97502708","to":"0xa18b9ca2a78660d44ab38ae72e72b18792ffe413","contractAddress":null}"#;
let receipt: RpcReceipt = serde_json::from_str(input).unwrap();
let envelope: ReceiptEnvelope<alloy_rpc_types_eth::Log> =

View File

@@ -1,717 +0,0 @@
//! This file contains the legacy reth `TransactionSigned` type that has been replaced with
//! alloy's TxEnvelope To test for consistency this is kept
use alloc::vec::Vec;
use alloy_consensus::{
transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, SignerRecoverable, TxHashRef},
EthereumTxEnvelope, SignableTransaction, Signed, TxEip1559, TxEip2930, TxEip4844, TxEip7702,
TxLegacy, TxType, Typed2718,
};
use alloy_eips::{
eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718},
eip2930::AccessList,
eip7702::SignedAuthorization,
};
use alloy_primitives::{
bytes::BufMut, keccak256, Address, Bytes, ChainId, Signature, TxHash, TxKind, B256, U256,
};
use alloy_rlp::{Decodable, Encodable};
use core::hash::{Hash, Hasher};
use reth_primitives_traits::{
crypto::secp256k1::{recover_signer, recover_signer_unchecked},
sync::OnceLock,
transaction::signed::RecoveryError,
InMemorySize, SignedTransaction,
};
macro_rules! delegate {
($self:expr => $tx:ident.$method:ident($($arg:expr),*)) => {
match $self {
Transaction::Legacy($tx) => $tx.$method($($arg),*),
Transaction::Eip2930($tx) => $tx.$method($($arg),*),
Transaction::Eip1559($tx) => $tx.$method($($arg),*),
Transaction::Eip4844($tx) => $tx.$method($($arg),*),
Transaction::Eip7702($tx) => $tx.$method($($arg),*),
}
};
}
/// A raw transaction.
///
/// Transaction types were introduced in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718).
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))]
pub enum Transaction {
/// Legacy transaction (type `0x0`).
///
/// Traditional Ethereum transactions, containing parameters `nonce`, `gasPrice`, `gasLimit`,
/// `to`, `value`, `data`, `v`, `r`, and `s`.
///
/// These transactions do not utilize access lists nor do they incorporate EIP-1559 fee market
/// changes.
Legacy(TxLegacy),
/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)), type `0x1`.
///
/// The `accessList` specifies an array of addresses and storage keys that the transaction
/// plans to access, enabling gas savings on cross-contract calls by pre-declaring the accessed
/// contract and storage slots.
Eip2930(TxEip2930),
/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)), type `0x2`.
///
/// Unlike traditional transactions, EIP-1559 transactions use an in-protocol, dynamically
/// changing base fee per gas, adjusted at each block to manage network congestion.
///
/// - `maxPriorityFeePerGas`, specifying the maximum fee above the base fee the sender is
/// willing to pay
/// - `maxFeePerGas`, setting the maximum total fee the sender is willing to pay.
///
/// The base fee is burned, while the priority fee is paid to the miner who includes the
/// transaction, incentivizing miners to include transactions with higher priority fees per
/// gas.
Eip1559(TxEip1559),
/// Shard Blob Transactions ([EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)), type `0x3`.
///
/// Shard Blob Transactions introduce a new transaction type called a blob-carrying transaction
/// to reduce gas costs. These transactions are similar to regular Ethereum transactions but
/// include additional data called a blob.
///
/// Blobs are larger (~125 kB) and cheaper than the current calldata, providing an immutable
/// and read-only memory for storing transaction data.
///
/// EIP-4844, also known as proto-danksharding, implements the framework and logic of
/// danksharding, introducing new transaction formats and verification rules.
Eip4844(TxEip4844),
/// EOA Set Code Transactions ([EIP-7702](https://eips.ethereum.org/EIPS/eip-7702)), type `0x4`.
///
/// EOA Set Code Transactions give the ability to set contract code for an EOA in perpetuity
/// until re-assigned by the same EOA. This allows for adding smart contract functionality to
/// the EOA.
Eip7702(TxEip7702),
}
impl Transaction {
/// Returns [`TxType`] of the transaction.
pub const fn tx_type(&self) -> TxType {
match self {
Self::Legacy(_) => TxType::Legacy,
Self::Eip2930(_) => TxType::Eip2930,
Self::Eip1559(_) => TxType::Eip1559,
Self::Eip4844(_) => TxType::Eip4844,
Self::Eip7702(_) => TxType::Eip7702,
}
}
#[cfg(test)]
const fn input_mut(&mut self) -> &mut Bytes {
match self {
Self::Legacy(tx) => &mut tx.input,
Self::Eip2930(tx) => &mut tx.input,
Self::Eip1559(tx) => &mut tx.input,
Self::Eip4844(tx) => &mut tx.input,
Self::Eip7702(tx) => &mut tx.input,
}
}
}
impl Typed2718 for Transaction {
fn ty(&self) -> u8 {
delegate!(self => tx.ty())
}
}
impl alloy_consensus::Transaction for Transaction {
fn chain_id(&self) -> Option<ChainId> {
delegate!(self => tx.chain_id())
}
fn nonce(&self) -> u64 {
delegate!(self => tx.nonce())
}
fn gas_limit(&self) -> u64 {
delegate!(self => tx.gas_limit())
}
fn gas_price(&self) -> Option<u128> {
delegate!(self => tx.gas_price())
}
fn max_fee_per_gas(&self) -> u128 {
delegate!(self => tx.max_fee_per_gas())
}
fn max_priority_fee_per_gas(&self) -> Option<u128> {
delegate!(self => tx.max_priority_fee_per_gas())
}
fn max_fee_per_blob_gas(&self) -> Option<u128> {
delegate!(self => tx.max_fee_per_blob_gas())
}
fn priority_fee_or_price(&self) -> u128 {
delegate!(self => tx.priority_fee_or_price())
}
fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
delegate!(self => tx.effective_gas_price(base_fee))
}
fn is_dynamic_fee(&self) -> bool {
delegate!(self => tx.is_dynamic_fee())
}
fn kind(&self) -> alloy_primitives::TxKind {
delegate!(self => tx.kind())
}
fn is_create(&self) -> bool {
delegate!(self => tx.is_create())
}
fn value(&self) -> alloy_primitives::U256 {
delegate!(self => tx.value())
}
fn input(&self) -> &alloy_primitives::Bytes {
delegate!(self => tx.input())
}
fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
delegate!(self => tx.access_list())
}
fn blob_versioned_hashes(&self) -> Option<&[B256]> {
delegate!(self => tx.blob_versioned_hashes())
}
fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
delegate!(self => tx.authorization_list())
}
}
impl SignableTransaction<Signature> for Transaction {
fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) {
delegate!(self => tx.set_chain_id(chain_id))
}
fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
delegate!(self => tx.encode_for_signing(out))
}
fn payload_len_for_signature(&self) -> usize {
delegate!(self => tx.payload_len_for_signature())
}
fn into_signed(self, signature: Signature) -> Signed<Self> {
let tx_hash = delegate!(&self => tx.tx_hash(&signature));
Signed::new_unchecked(self, signature, tx_hash)
}
}
impl InMemorySize for Transaction {
fn size(&self) -> usize {
delegate!(self => tx.size())
}
}
#[cfg(any(test, feature = "reth-codec"))]
impl reth_codecs::Compact for Transaction {
// Serializes the TxType to the buffer if necessary, returning 2 bits of the type as an
// identifier instead of the length.
fn to_compact<B>(&self, buf: &mut B) -> usize
where
B: alloy_rlp::bytes::BufMut + AsMut<[u8]>,
{
let identifier = self.tx_type().to_compact(buf);
delegate!(self => tx.to_compact(buf));
identifier
}
// For backwards compatibility purposes, only 2 bits of the type are encoded in the identifier
// parameter. In the case of a [`COMPACT_EXTENDED_IDENTIFIER_FLAG`], the full transaction type
// is read from the buffer as a single byte.
//
// # Panics
//
// A panic will be triggered if an identifier larger than 3 is passed from the database. For
// optimism an identifier with value [`DEPOSIT_TX_TYPE_ID`] is allowed.
fn from_compact(buf: &[u8], identifier: usize) -> (Self, &[u8]) {
let (tx_type, buf) = TxType::from_compact(buf, identifier);
match tx_type {
TxType::Legacy => {
let (tx, buf) = TxLegacy::from_compact(buf, buf.len());
(Self::Legacy(tx), buf)
}
TxType::Eip2930 => {
let (tx, buf) = TxEip2930::from_compact(buf, buf.len());
(Self::Eip2930(tx), buf)
}
TxType::Eip1559 => {
let (tx, buf) = TxEip1559::from_compact(buf, buf.len());
(Self::Eip1559(tx), buf)
}
TxType::Eip4844 => {
let (tx, buf) = TxEip4844::from_compact(buf, buf.len());
(Self::Eip4844(tx), buf)
}
TxType::Eip7702 => {
let (tx, buf) = TxEip7702::from_compact(buf, buf.len());
(Self::Eip7702(tx), buf)
}
}
}
}
impl RlpEcdsaEncodableTx for Transaction {
fn rlp_encoded_fields_length(&self) -> usize {
delegate!(self => tx.rlp_encoded_fields_length())
}
fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
delegate!(self => tx.rlp_encode_fields(out))
}
fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
delegate!(self => tx.eip2718_encode_with_type(signature, tx.ty(), out))
}
fn eip2718_encode(&self, signature: &Signature, out: &mut dyn BufMut) {
delegate!(self => tx.eip2718_encode(signature, out))
}
fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
delegate!(self => tx.network_encode_with_type(signature, tx.ty(), out))
}
fn network_encode(&self, signature: &Signature, out: &mut dyn BufMut) {
delegate!(self => tx.network_encode(signature, out))
}
fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> TxHash {
delegate!(self => tx.tx_hash_with_type(signature, tx.ty()))
}
fn tx_hash(&self, signature: &Signature) -> TxHash {
delegate!(self => tx.tx_hash(signature))
}
}
/// Signed Ethereum transaction.
#[derive(Debug, Clone, Eq, derive_more::AsRef, derive_more::Deref)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(rlp))]
#[cfg_attr(feature = "test-utils", derive(derive_more::DerefMut))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TransactionSigned {
/// Transaction hash
#[cfg_attr(feature = "serde", serde(skip))]
hash: OnceLock<TxHash>,
/// The transaction signature values
signature: Signature,
/// Raw transaction info
#[deref]
#[as_ref]
#[cfg_attr(feature = "test-utils", deref_mut)]
transaction: Transaction,
}
impl TransactionSigned {
fn recalculate_hash(&self) -> B256 {
keccak256(self.encoded_2718())
}
}
impl Hash for TransactionSigned {
fn hash<H: Hasher>(&self, state: &mut H) {
self.signature.hash(state);
self.transaction.hash(state);
}
}
impl PartialEq for TransactionSigned {
fn eq(&self, other: &Self) -> bool {
self.signature == other.signature &&
self.transaction == other.transaction &&
self.tx_hash() == other.tx_hash()
}
}
impl TransactionSigned {
/// Creates a new signed transaction from the given transaction, signature and hash.
pub fn new(transaction: Transaction, signature: Signature, hash: B256) -> Self {
Self { hash: hash.into(), signature, transaction }
}
/// Returns the transaction hash.
#[inline]
pub fn hash(&self) -> &B256 {
self.hash.get_or_init(|| self.recalculate_hash())
}
/// Splits the transaction into parts.
pub fn into_parts(self) -> (Transaction, Signature, B256) {
let hash = *self.hash.get_or_init(|| self.recalculate_hash());
(self.transaction, self.signature, hash)
}
}
impl Typed2718 for TransactionSigned {
fn ty(&self) -> u8 {
self.transaction.ty()
}
}
impl alloy_consensus::Transaction for TransactionSigned {
fn chain_id(&self) -> Option<ChainId> {
self.transaction.chain_id()
}
fn nonce(&self) -> u64 {
self.transaction.nonce()
}
fn gas_limit(&self) -> u64 {
self.transaction.gas_limit()
}
fn gas_price(&self) -> Option<u128> {
self.transaction.gas_price()
}
fn max_fee_per_gas(&self) -> u128 {
self.transaction.max_fee_per_gas()
}
fn max_priority_fee_per_gas(&self) -> Option<u128> {
self.transaction.max_priority_fee_per_gas()
}
fn max_fee_per_blob_gas(&self) -> Option<u128> {
self.transaction.max_fee_per_blob_gas()
}
fn priority_fee_or_price(&self) -> u128 {
self.transaction.priority_fee_or_price()
}
fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
self.transaction.effective_gas_price(base_fee)
}
fn is_dynamic_fee(&self) -> bool {
self.transaction.is_dynamic_fee()
}
fn kind(&self) -> TxKind {
self.transaction.kind()
}
fn is_create(&self) -> bool {
self.transaction.is_create()
}
fn value(&self) -> U256 {
self.transaction.value()
}
fn input(&self) -> &Bytes {
self.transaction.input()
}
fn access_list(&self) -> Option<&AccessList> {
self.transaction.access_list()
}
fn blob_versioned_hashes(&self) -> Option<&[B256]> {
self.transaction.blob_versioned_hashes()
}
fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
self.transaction.authorization_list()
}
}
impl From<Signed<Transaction>> for TransactionSigned {
fn from(value: Signed<Transaction>) -> Self {
let (tx, sig, hash) = value.into_parts();
Self::new(tx, sig, hash)
}
}
impl From<TransactionSigned> for EthereumTxEnvelope<TxEip4844> {
fn from(value: TransactionSigned) -> Self {
let (tx, signature, hash) = value.into_parts();
match tx {
Transaction::Legacy(tx) => Signed::new_unchecked(tx, signature, hash).into(),
Transaction::Eip2930(tx) => Signed::new_unchecked(tx, signature, hash).into(),
Transaction::Eip1559(tx) => Signed::new_unchecked(tx, signature, hash).into(),
Transaction::Eip4844(tx) => Signed::new_unchecked(tx, signature, hash).into(),
Transaction::Eip7702(tx) => Signed::new_unchecked(tx, signature, hash).into(),
}
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a> arbitrary::Arbitrary<'a> for TransactionSigned {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
#[expect(unused_mut)]
let mut transaction = Transaction::arbitrary(u)?;
let secp = secp256k1::Secp256k1::new();
let key_pair = secp256k1::Keypair::new(&secp, &mut rand_08::thread_rng());
let signature = reth_primitives_traits::crypto::secp256k1::sign_message(
B256::from_slice(&key_pair.secret_bytes()[..]),
transaction.signature_hash(),
)
.unwrap();
Ok(Self { transaction, signature, hash: Default::default() })
}
}
impl InMemorySize for TransactionSigned {
fn size(&self) -> usize {
let Self { hash: _, signature, transaction } = self;
self.tx_hash().size() + signature.size() + transaction.size()
}
}
impl Encodable2718 for TransactionSigned {
fn type_flag(&self) -> Option<u8> {
(!self.transaction.is_legacy()).then(|| self.ty())
}
fn encode_2718_len(&self) -> usize {
delegate!(&self.transaction => tx.eip2718_encoded_length(&self.signature))
}
fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
delegate!(&self.transaction => tx.eip2718_encode(&self.signature, out))
}
fn trie_hash(&self) -> B256 {
*self.tx_hash()
}
}
impl Decodable2718 for TransactionSigned {
fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? {
TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
TxType::Eip2930 => {
let (tx, signature) = TxEip2930::rlp_decode_with_signature(buf)?;
Ok(Self {
transaction: Transaction::Eip2930(tx),
signature,
hash: Default::default(),
})
}
TxType::Eip1559 => {
let (tx, signature) = TxEip1559::rlp_decode_with_signature(buf)?;
Ok(Self {
transaction: Transaction::Eip1559(tx),
signature,
hash: Default::default(),
})
}
TxType::Eip4844 => {
let (tx, signature) = TxEip4844::rlp_decode_with_signature(buf)?;
Ok(Self {
transaction: Transaction::Eip4844(tx),
signature,
hash: Default::default(),
})
}
TxType::Eip7702 => {
let (tx, signature) = TxEip7702::rlp_decode_with_signature(buf)?;
Ok(Self {
transaction: Transaction::Eip7702(tx),
signature,
hash: Default::default(),
})
}
}
}
fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
let (tx, signature) = TxLegacy::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Legacy(tx), signature, hash: Default::default() })
}
}
impl Encodable for TransactionSigned {
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
self.network_encode(out);
}
fn length(&self) -> usize {
self.network_len()
}
}
impl Decodable for TransactionSigned {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
Self::network_decode(buf).map_err(Into::into)
}
}
#[cfg(any(test, feature = "reth-codec"))]
impl reth_codecs::Compact for TransactionSigned {
fn to_compact<B>(&self, buf: &mut B) -> usize
where
B: alloy_rlp::bytes::BufMut + AsMut<[u8]>,
{
use alloy_consensus::Transaction;
let start = buf.as_mut().len();
// Placeholder for bitflags.
// The first byte uses 4 bits as flags: IsCompressed[1bit], TxType[2bits], Signature[1bit]
buf.put_u8(0);
let sig_bit = self.signature.to_compact(buf) as u8;
let zstd_bit = self.transaction.input().len() >= 32;
let tx_bits = if zstd_bit {
let mut tmp = Vec::with_capacity(256);
reth_zstd_compressors::with_tx_compressor(|compressor| {
let tx_bits = self.transaction.to_compact(&mut tmp);
buf.put_slice(&compressor.compress(&tmp).expect("Failed to compress"));
tx_bits as u8
})
} else {
self.transaction.to_compact(buf) as u8
};
// Replace bitflags with the actual values
buf.as_mut()[start] = sig_bit | (tx_bits << 1) | ((zstd_bit as u8) << 3);
buf.as_mut().len() - start
}
fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) {
use alloy_rlp::bytes::Buf;
// The first byte uses 4 bits as flags: IsCompressed[1], TxType[2], Signature[1]
let bitflags = buf.get_u8() as usize;
let sig_bit = bitflags & 1;
let (signature, buf) = Signature::from_compact(buf, sig_bit);
let zstd_bit = bitflags >> 3;
let (transaction, buf) = if zstd_bit != 0 {
reth_zstd_compressors::with_tx_decompressor(|decompressor| {
// TODO: enforce that zstd is only present at a "top" level type
let transaction_type = (bitflags & 0b110) >> 1;
let (transaction, _) =
Transaction::from_compact(decompressor.decompress(buf), transaction_type);
(transaction, buf)
})
} else {
let transaction_type = bitflags >> 1;
Transaction::from_compact(buf, transaction_type)
};
(Self { signature, transaction, hash: Default::default() }, buf)
}
}
impl SignerRecoverable for TransactionSigned {
fn recover_signer(&self) -> Result<Address, RecoveryError> {
let signature_hash = self.signature_hash();
recover_signer(&self.signature, signature_hash)
}
fn recover_signer_unchecked(&self) -> Result<Address, RecoveryError> {
let signature_hash = self.signature_hash();
recover_signer_unchecked(&self.signature, signature_hash)
}
fn recover_unchecked_with_buf(&self, buf: &mut Vec<u8>) -> Result<Address, RecoveryError> {
self.encode_for_signing(buf);
let signature_hash = keccak256(buf);
recover_signer_unchecked(&self.signature, signature_hash)
}
}
impl TxHashRef for TransactionSigned {
fn tx_hash(&self) -> &TxHash {
self.hash.get_or_init(|| self.recalculate_hash())
}
}
impl IsTyped2718 for TransactionSigned {
fn is_type(type_id: u8) -> bool {
<alloy_consensus::TxEnvelope as IsTyped2718>::is_type(type_id)
}
}
impl SignedTransaction for TransactionSigned {}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::EthereumTxEnvelope;
use proptest::proptest;
use proptest_arbitrary_interop::arb;
use reth_codecs::Compact;
proptest! {
#[test]
fn test_roundtrip_compact_encode_envelope(reth_tx in arb::<TransactionSigned>()) {
let mut expected_buf = Vec::<u8>::new();
let expected_len = reth_tx.to_compact(&mut expected_buf);
let mut actual_but = Vec::<u8>::new();
let alloy_tx = EthereumTxEnvelope::<TxEip4844>::from(reth_tx);
let actual_len = alloy_tx.to_compact(&mut actual_but);
assert_eq!(actual_but, expected_buf);
assert_eq!(actual_len, expected_len);
}
#[test]
fn test_roundtrip_compact_decode_envelope(reth_tx in arb::<TransactionSigned>()) {
let mut buf = Vec::<u8>::new();
let len = reth_tx.to_compact(&mut buf);
let (actual_tx, _) = EthereumTxEnvelope::<TxEip4844>::from_compact(&buf, len);
let expected_tx = EthereumTxEnvelope::<TxEip4844>::from(reth_tx);
assert_eq!(actual_tx, expected_tx);
}
#[test]
fn test_roundtrip_compact_encode_envelope_zstd(mut reth_tx in arb::<TransactionSigned>()) {
// zstd only kicks in if the input is large enough
*reth_tx.transaction.input_mut() = vec![0;33].into();
let mut expected_buf = Vec::<u8>::new();
let expected_len = reth_tx.to_compact(&mut expected_buf);
let mut actual_but = Vec::<u8>::new();
let alloy_tx = EthereumTxEnvelope::<TxEip4844>::from(reth_tx);
let actual_len = alloy_tx.to_compact(&mut actual_but);
assert_eq!(actual_but, expected_buf);
assert_eq!(actual_len, expected_len);
}
#[test]
fn test_roundtrip_compact_decode_envelope_zstd(mut reth_tx in arb::<TransactionSigned>()) {
// zstd only kicks in if the input is large enough
*reth_tx.transaction.input_mut() = vec![0;33].into();
let mut buf = Vec::<u8>::new();
let len = reth_tx.to_compact(&mut buf);
let (actual_tx, _) = EthereumTxEnvelope::<TxEip4844>::from_compact(&buf, len);
let expected_tx = EthereumTxEnvelope::<TxEip4844>::from(reth_tx);
assert_eq!(actual_tx, expected_tx);
}
}
}

View File

@@ -411,7 +411,7 @@ impl ExecutionOutcome {
pub fn ethereum_receipts_root(&self, block_number: BlockNumber) -> Option<B256> {
self.generic_receipts_root_slow(
block_number,
reth_ethereum_primitives::Receipt::calculate_receipt_root_no_memo,
reth_ethereum_primitives::calculate_receipt_root_no_memo,
)
}
}

View File

@@ -258,15 +258,15 @@ pub async fn test_exex_context_with_chain_spec(
let genesis_hash = init_genesis(&provider_factory)?;
let provider = BlockchainProvider::new(provider_factory.clone())?;
let runtime = Runtime::test();
let network_manager = NetworkManager::new(
NetworkConfigBuilder::new(rng_secret_key())
NetworkConfigBuilder::new(rng_secret_key(), runtime.clone())
.with_unused_discovery_port()
.with_unused_listener_port()
.build(provider_factory.clone()),
)
.await?;
let network = network_manager.handle().clone();
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let task_executor = runtime.clone();
runtime.spawn_task(network_manager);

View File

@@ -133,7 +133,7 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
}
EthMessageID::NodeData => {
if version >= EthVersion::Eth67 {
return Err(MessageError::Invalid(version, EthMessageID::GetNodeData))
return Err(MessageError::Invalid(version, EthMessageID::NodeData))
}
EthMessage::NodeData(RequestPair::decode(buf)?)
}

View File

@@ -102,26 +102,18 @@ pub struct NetworkConfig<C, N: NetworkPrimitives = EthNetworkPrimitives> {
// === impl NetworkConfig ===
impl<N: NetworkPrimitives> NetworkConfig<(), N> {
/// Convenience method for creating the corresponding builder type
pub fn builder(secret_key: SecretKey) -> NetworkConfigBuilder<N> {
NetworkConfigBuilder::new(secret_key)
/// Convenience method for creating the corresponding builder type.
pub fn builder(secret_key: SecretKey, executor: Runtime) -> NetworkConfigBuilder<N> {
NetworkConfigBuilder::new(secret_key, executor)
}
/// Convenience method for creating the corresponding builder type with a random secret key.
pub fn builder_with_rng_secret_key() -> NetworkConfigBuilder<N> {
NetworkConfigBuilder::with_rng_secret_key()
pub fn builder_with_rng_secret_key(executor: Runtime) -> NetworkConfigBuilder<N> {
NetworkConfigBuilder::with_rng_secret_key(executor)
}
}
impl<C, N: NetworkPrimitives> NetworkConfig<C, N> {
/// Create a new instance with all mandatory fields set, rest is field with defaults.
pub fn new(client: C, secret_key: SecretKey) -> Self
where
C: ChainSpecProvider<ChainSpec: Hardforks>,
{
NetworkConfig::builder(secret_key).build(client)
}
/// Apply a function to the config.
pub fn apply<F>(self, f: F) -> Self
where
@@ -206,7 +198,7 @@ pub struct NetworkConfigBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
/// The default mode of the network.
network_mode: NetworkMode,
/// The executor to use for spawning tasks.
executor: Option<Runtime>,
executor: Runtime,
/// Sets the hello message for the p2p handshake in `RLPx`
hello_message: Option<HelloMessageWithProtocols>,
/// The executor to use for spawning tasks.
@@ -232,8 +224,8 @@ pub struct NetworkConfigBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
impl NetworkConfigBuilder<EthNetworkPrimitives> {
/// Creates the `NetworkConfigBuilder` with [`EthNetworkPrimitives`] types.
pub fn eth(secret_key: SecretKey) -> Self {
Self::new(secret_key)
pub fn eth(secret_key: SecretKey, executor: Runtime) -> Self {
Self::new(secret_key, executor)
}
}
@@ -242,12 +234,12 @@ impl NetworkConfigBuilder<EthNetworkPrimitives> {
#[expect(missing_docs)]
impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
/// Create a new builder instance with a random secret key.
pub fn with_rng_secret_key() -> Self {
Self::new(rng_secret_key())
pub fn with_rng_secret_key(executor: Runtime) -> Self {
Self::new(rng_secret_key(), executor)
}
/// Create a new builder instance with the given secret key.
pub fn new(secret_key: SecretKey) -> Self {
pub fn new(secret_key: SecretKey, executor: Runtime) -> Self {
Self {
secret_key,
dns_discovery_config: Some(Default::default()),
@@ -259,7 +251,7 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
peers_config: None,
sessions_config: None,
network_mode: Default::default(),
executor: None,
executor,
hello_message: None,
extra_protocols: Default::default(),
head: None,
@@ -340,10 +332,8 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
}
/// Sets the executor to use for spawning tasks.
///
/// If `None`, then [`tokio::spawn`] is used for spawning tasks.
pub fn with_task_executor(mut self, executor: Runtime) -> Self {
self.executor = Some(executor);
self.executor = executor;
self
}
@@ -691,11 +681,7 @@ impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
chain_id,
block_import: block_import.unwrap_or_else(|| Box::<ProofOfStakeBlockImport>::default()),
network_mode,
executor: executor.unwrap_or_else(|| match tokio::runtime::Handle::try_current() {
Ok(handle) => Runtime::with_existing_handle(handle)
.expect("failed to create runtime with existing handle"),
Err(_) => Runtime::test(),
}),
executor,
status,
hello_message,
extra_protocols,
@@ -749,7 +735,7 @@ mod tests {
fn builder() -> NetworkConfigBuilder {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
NetworkConfigBuilder::new(secret_key)
NetworkConfigBuilder::new(secret_key, Runtime::test())
}
#[test]

View File

@@ -51,6 +51,7 @@
//! };
//! use reth_network_peers::mainnet_nodes;
//! use reth_storage_api::noop::NoopProvider;
//! use reth_tasks::Runtime;
//!
//! // This block provider implementation is used for testing purposes.
//! let client = NoopProvider::default();
@@ -58,7 +59,7 @@
//! // The key that's used for encrypting sessions and to identify our node.
//! let local_key = rng_secret_key();
//!
//! let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key)
//! let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key, Runtime::test())
//! .boot_nodes(mainnet_nodes())
//! .build(client);
//!
@@ -80,6 +81,7 @@
//! };
//! use reth_network_peers::mainnet_nodes;
//! use reth_storage_api::noop::NoopProvider;
//! use reth_tasks::Runtime;
//! use reth_transaction_pool::TransactionPool;
//! async fn launch<Pool: TransactionPool>(pool: Pool) {
//! // This block provider implementation is used for testing purposes.
@@ -88,7 +90,7 @@
//! // The key that's used for encrypting sessions and to identify our node.
//! let local_key = rng_secret_key();
//!
//! let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key)
//! let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key, Runtime::test())
//! .boot_nodes(mainnet_nodes())
//! .build(client.clone());
//! let transactions_manager_config = config.transactions_manager_config.clone();

View File

@@ -157,8 +157,9 @@ impl NetworkManager {
/// # async fn f() {
/// use reth_chainspec::MAINNET;
/// use reth_network::{NetworkConfig, NetworkManager};
/// let config =
/// NetworkConfig::builder_with_rng_secret_key().build_with_noop_provider(MAINNET.clone());
/// use reth_tasks::Runtime;
/// let config = NetworkConfig::builder_with_rng_secret_key(Runtime::test())
/// .build_with_noop_provider(MAINNET.clone());
/// let manager = NetworkManager::eth(config).await;
/// # }
/// ```
@@ -378,6 +379,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
/// };
/// use reth_network_peers::mainnet_nodes;
/// use reth_storage_api::noop::NoopProvider;
/// use reth_tasks::Runtime;
/// use reth_transaction_pool::TransactionPool;
/// async fn launch<Pool: TransactionPool>(pool: Pool) {
/// // This block provider implementation is used for testing purposes.
@@ -386,7 +388,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
/// // The key that's used for encrypting sessions and to identify our node.
/// let local_key = rng_secret_key();
///
/// let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key)
/// let config = NetworkConfig::<_, EthNetworkPrimitives>::builder(local_key, Runtime::test())
/// .boot_nodes(mainnet_nodes())
/// .build(client.clone());
/// let transactions_manager_config = config.transactions_manager_config.clone();

View File

@@ -713,7 +713,7 @@ where
}
fn network_config_builder(secret_key: SecretKey) -> NetworkConfigBuilder {
NetworkConfigBuilder::new(secret_key)
NetworkConfigBuilder::new(secret_key, Runtime::test())
.listener_addr(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)))
.discovery_addr(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)))
.disable_dns_discovery()

View File

@@ -20,6 +20,7 @@ use reth_eth_wire_types::EthNetworkPrimitives;
use reth_network_api::{PeerKind, PeerRequest, PeerRequestSender};
use reth_network_peers::PeerId;
use reth_storage_api::noop::NoopProvider;
use reth_tasks::Runtime;
use reth_transaction_pool::test_utils::{testing_pool, TestPool};
use secp256k1::SecretKey;
use std::sync::Arc;
@@ -32,7 +33,7 @@ pub async fn new_tx_manager(
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let client = NoopProvider::default();
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
// let OS choose port
.listener_port(0)
.disable_discovery()

View File

@@ -381,7 +381,7 @@ impl<N: NetworkPrimitives> TransactionFetcher<N> {
let Some(TxFetchMetadata { retries, fallback_peers, .. }) =
self.hashes_fetch_inflight_and_pending_fetch.get(&hash)
else {
return
continue
};
if let Some(peer_id) = fallback_peer {

View File

@@ -2167,6 +2167,7 @@ mod tests {
sync::{NetworkSyncUpdater, SyncState},
};
use reth_storage_api::noop::NoopProvider;
use reth_tasks::Runtime;
use reth_transaction_pool::test_utils::{
testing_pool, MockTransaction, MockTransactionFactory, TestPool,
};
@@ -2196,7 +2197,7 @@ mod tests {
let client = NoopProvider::default();
let pool = testing_pool();
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(client);
@@ -2266,7 +2267,7 @@ mod tests {
let client = NoopProvider::default();
let pool = testing_pool();
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(client);
@@ -2332,7 +2333,7 @@ mod tests {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let client = NoopProvider::default();
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
// let OS choose port
.listener_port(0)
.disable_discovery()
@@ -2440,7 +2441,7 @@ mod tests {
let client = NoopProvider::default();
let pool = testing_pool();
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(client);
@@ -2518,7 +2519,7 @@ mod tests {
let client = NoopProvider::default();
let pool = testing_pool();
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(client);
@@ -2936,7 +2937,7 @@ mod tests {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let client = NoopProvider::default();
let network_config = NetworkConfigBuilder::new(secret_key)
let network_config = NetworkConfigBuilder::new(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.build(client.clone());

View File

@@ -21,6 +21,7 @@ use reth_network_p2p::{
use reth_network_peers::{mainnet_nodes, NodeRecord, TrustedPeer};
use reth_provider::test_utils::MockEthProvider;
use reth_storage_api::noop::NoopProvider;
use reth_tasks::Runtime;
use reth_tracing::init_test_tracing;
use reth_transaction_pool::test_utils::testing_pool;
use secp256k1::SecretKey;
@@ -207,8 +208,9 @@ async fn test_connect_with_boot_nodes() {
let mut discv4 = Discv4Config::builder();
discv4.add_boot_nodes(mainnet_nodes());
let config =
NetworkConfigBuilder::eth(secret_key).discovery(discv4).build(NoopProvider::default());
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.discovery(discv4)
.build(NoopProvider::default());
let network = NetworkManager::new(config).await.unwrap();
let handle = network.handle().clone();
@@ -229,7 +231,9 @@ async fn test_connect_with_builder() {
discv4.add_boot_nodes(mainnet_nodes());
let client = NoopProvider::default();
let config = NetworkConfigBuilder::eth(secret_key).discovery(discv4).build(client.clone());
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.discovery(discv4)
.build(client.clone());
let (handle, network, _, requests) = NetworkManager::new(config)
.await
.unwrap()
@@ -265,7 +269,9 @@ async fn test_connect_to_trusted_peer() {
let discv4 = Discv4Config::builder();
let client = NoopProvider::default();
let config = NetworkConfigBuilder::eth(secret_key).discovery(discv4).build(client.clone());
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.discovery(discv4)
.build(client.clone());
let transactions_manager_config = config.transactions_manager_config.clone();
let (handle, network, transactions, requests) = NetworkManager::new(config)
.await
@@ -381,7 +387,7 @@ async fn test_trusted_peer_only() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let peers_config = PeersConfig::test().with_trusted_nodes_only(true);
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)
@@ -444,7 +450,7 @@ async fn test_network_state_change() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let peers_config = PeersConfig::test();
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)
@@ -485,7 +491,7 @@ async fn test_exceed_outgoing_connections() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let peers_config = PeersConfig::test().with_max_outbound(1);
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)
@@ -526,7 +532,7 @@ async fn test_disconnect_incoming_when_exceeded_incoming_connections() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let peers_config = PeersConfig::test().with_max_inbound(0);
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)
@@ -643,7 +649,7 @@ async fn new_random_peer(
let peers_config =
PeersConfig::test().with_max_inbound(max_in_bound).with_trusted_nodes(trusted_nodes);
let config = NetworkConfigBuilder::new(secret_key)
let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)
@@ -715,7 +721,7 @@ async fn test_connect_peer_in_different_network_should_fail() {
// If the remote disconnect first, then we would not get a fatal protocol error. So set
// max_backoff_count to 0 to speed up the removal of the peer.
let peers_config = PeersConfig::default().with_max_backoff_count(0);
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.peer_config(peers_config)

View File

@@ -19,6 +19,7 @@ use reth_network::{
};
use reth_network_api::{Direction, NetworkInfo, PeerId, Peers};
use reth_provider::{noop::NoopProvider, test_utils::MockEthProvider};
use reth_tasks::Runtime;
use secp256k1::SecretKey;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -285,7 +286,7 @@ async fn test_connect_to_non_multiplex_peer() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.build(NoopProvider::default());

View File

@@ -11,6 +11,7 @@ use reth_network::{
};
use reth_network_api::{NetworkInfo, PeersInfo};
use reth_storage_api::noop::NoopProvider;
use reth_tasks::Runtime;
use secp256k1::SecretKey;
use tokio::net::TcpListener;
@@ -29,7 +30,7 @@ fn is_addr_in_use_kind(err: &NetworkError, kind: ServiceKind) -> bool {
#[tokio::test(flavor = "multi_thread")]
async fn test_is_default_syncing() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(NoopProvider::default());
@@ -40,13 +41,13 @@ async fn test_is_default_syncing() {
#[tokio::test(flavor = "multi_thread")]
async fn test_listener_addr_in_use() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.disable_discovery()
.listener_port(0)
.build(NoopProvider::default());
let network = NetworkManager::new(config).await.unwrap();
let listener_port = network.local_addr().port();
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(listener_port)
.disable_discovery()
.build(NoopProvider::default());
@@ -74,7 +75,7 @@ async fn test_discovery_addr_in_use() {
#[tokio::test(flavor = "multi_thread")]
async fn test_discv5_and_discv4_same_socket_fails() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(DEFAULT_DISCOVERY_PORT)
.discovery_v5(
reth_discv5::Config::builder((DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT).into())
@@ -105,7 +106,7 @@ async fn test_discv5_and_rlpx_same_socket_ok_without_discv4() {
.port();
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(test_port)
.disable_discv4_discovery()
.discovery_v5(
@@ -126,7 +127,7 @@ async fn test_discv5_and_rlpx_same_socket_ok_without_discv4() {
#[tokio::test(flavor = "multi_thread")]
async fn test_tcp_port_node_record_no_discovery() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.disable_discovery()
.build_with_noop_provider(MAINNET.clone());
@@ -144,7 +145,7 @@ async fn test_tcp_port_node_record_no_discovery() {
#[tokio::test(flavor = "multi_thread")]
async fn test_tcp_port_node_record_discovery() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.listener_port(0)
.discovery_port(0)
.disable_dns_discovery()
@@ -163,7 +164,7 @@ async fn test_tcp_port_node_record_discovery() {
#[tokio::test(flavor = "multi_thread")]
async fn test_node_record_address_with_nat() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.add_nat(Some(NatResolver::ExternalIp("10.1.1.1".parse().unwrap())))
.disable_discv4_discovery()
.disable_dns_discovery()
@@ -179,7 +180,7 @@ async fn test_node_record_address_with_nat() {
#[tokio::test(flavor = "multi_thread")]
async fn test_node_record_address_with_nat_disable_discovery() {
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
let config = NetworkConfigBuilder::eth(secret_key)
let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
.add_nat(Some(NatResolver::ExternalIp("10.1.1.1".parse().unwrap())))
.disable_discovery()
.disable_nat()

View File

@@ -985,8 +985,8 @@ impl<Node: FullNodeTypes<Types: NodeTypes<ChainSpec: Hardforks>>> BuilderContext
self.config().chain.clone(),
secret_key,
default_peers_path,
self.executor.clone(),
)
.with_task_executor(self.executor.clone())
.set_head(self.head);
Ok(builder)

View File

@@ -343,10 +343,7 @@ mod test {
payload_builder_handle: PayloadBuilderHandle::<EthEngineTypes>::noop(),
};
let task_executor = {
let runtime = tokio::runtime::Runtime::new().unwrap();
Runtime::with_existing_handle(runtime.handle().clone()).unwrap()
};
let task_executor = Runtime::test();
let node = NodeAdapter { components, task_executor, provider: NoopProvider::default() };

View File

@@ -25,6 +25,7 @@ reth-rpc-eth-types.workspace = true
reth-rpc-server-types.workspace = true
reth-rpc-convert.workspace = true
reth-transaction-pool.workspace = true
reth-tasks.workspace = true
reth-tracing.workspace = true
reth-config = { workspace = true, features = ["serde"] }
reth-discv4.workspace = true

View File

@@ -60,6 +60,17 @@ pub struct BenchmarkArgs {
#[arg(long, short, value_name = "BENCHMARK_OUTPUT", verbatim_doc_comment)]
pub output: Option<PathBuf>,
/// Optional Prometheus metrics endpoint to scrape after each block.
///
/// When provided, reth-bench will fetch metrics from this URL after each
/// `newPayload` / `forkchoiceUpdated` call, recording per-block execution
/// and state root durations. Results are written to `metrics.csv` in the
/// output directory.
///
/// Example: `http://127.0.0.1:9001/metrics`
#[arg(long = "metrics-url", value_name = "URL", verbatim_doc_comment)]
pub metrics_url: Option<String>,
/// Use `reth_newPayload` endpoint instead of `engine_newPayload*`.
///
/// The `reth_newPayload` endpoint is a reth-specific extension that takes `ExecutionData`

View File

@@ -48,8 +48,10 @@ pub struct LogArgs {
/// The maximum amount of log files that will be stored. If set to 0, background file logging
/// is disabled.
#[arg(long = "log.file.max-files", value_name = "COUNT", global = true, default_value_t = 5)]
pub log_file_max_files: usize,
///
/// Default: 5 for `node` command, 0 for non-node utility subcommands.
#[arg(long = "log.file.max-files", value_name = "COUNT", global = true)]
pub log_file_max_files: Option<usize>,
/// Write logs to journald.
#[arg(long = "log.journald", global = true)]
@@ -108,6 +110,28 @@ pub struct LogArgs {
}
impl LogArgs {
/// The default number of log files for the `node` subcommand.
pub const DEFAULT_MAX_LOG_FILES_NODE: usize = 5;
/// Returns the effective maximum number of log files.
///
/// If `log_file_max_files` was explicitly set, returns that value.
/// Otherwise returns 0 (file logging disabled).
///
/// Note: Callers should apply the node-specific default (5) before calling
/// `init_tracing` if the command is the `node` subcommand.
pub fn effective_log_file_max_files(&self) -> usize {
self.log_file_max_files.unwrap_or(0)
}
/// Applies the default `log_file_max_files` value for the `node` subcommand
/// if not explicitly set by the user.
pub const fn apply_node_defaults(&mut self) {
if self.log_file_max_files.is_none() {
self.log_file_max_files = Some(Self::DEFAULT_MAX_LOG_FILES_NODE);
}
}
/// Creates a [`LayerInfo`] instance.
fn layer_info(&self, format: LogFormat, filter: String, use_color: bool) -> LayerInfo {
LayerInfo::new(
@@ -124,7 +148,7 @@ impl LogArgs {
self.log_file_directory.clone().into(),
self.log_file_name.clone(),
self.log_file_max_size * MB_TO_BYTES,
self.log_file_max_files,
self.effective_log_file_max_files(),
)
}
@@ -154,7 +178,7 @@ impl LogArgs {
tracer = tracer.with_journald(self.journald_filter.clone());
}
if self.log_file_max_files > 0 {
if self.effective_log_file_max_files() > 0 {
let info = self.file_info();
let file = self.layer_info(self.log_file_format, self.log_file_filter.clone(), false);
tracer = tracer.with_file(file, info);

View File

@@ -39,6 +39,7 @@ use reth_network::{
HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives,
};
use reth_network_peers::{mainnet_nodes, TrustedPeer};
use reth_tasks::Runtime;
use secp256k1::SecretKey;
use std::str::FromStr;
use tracing::error;
@@ -326,6 +327,7 @@ impl NetworkArgs {
chain_spec: impl EthChainSpec,
secret_key: SecretKey,
default_peers_file: PathBuf,
executor: Runtime,
) -> NetworkConfigBuilder<N> {
let addr = self.resolved_addr();
let chain_bootnodes = self
@@ -345,7 +347,7 @@ impl NetworkArgs {
.with_enforce_enr_fork_id(self.enforce_enr_fork_id);
// Configure basic network stack
NetworkConfigBuilder::<N>::new(secret_key)
NetworkConfigBuilder::<N>::new(secret_key, executor)
.external_ip_resolver(self.nat.clone())
.sessions_config(
config.sessions.clone().with_upscaled_event_buffer(peers_config.max_peers()),
@@ -1097,6 +1099,7 @@ mod tests {
MAINNET.clone(),
secret_key,
peers_file.clone(),
Runtime::test(),
);
let net_cfg = builder.build_with_noop_provider(MAINNET.clone());

View File

@@ -1,5 +1,6 @@
use crate::{cli::config::PayloadBuilderConfig, version::default_extra_data};
use alloy_consensus::constants::MAXIMUM_EXTRA_DATA_SIZE;
use alloy_primitives::Bytes;
use clap::{
builder::{RangedU64ValueParser, TypedValueParser},
Arg, Args, Command,
@@ -8,7 +9,7 @@ use reth_cli_util::{
parse_duration_from_secs, parse_duration_from_secs_or_ms,
parsers::format_duration_as_secs_or_ms,
};
use std::{borrow::Cow, ffi::OsStr, sync::OnceLock, time::Duration};
use std::{ffi::OsStr, sync::OnceLock, time::Duration};
/// Global static payload builder defaults
static PAYLOAD_BUILDER_DEFAULTS: OnceLock<DefaultPayloadBuilderValues> = OnceLock::new();
@@ -80,12 +81,15 @@ impl Default for DefaultPayloadBuilderValues {
#[command(next_help_heading = "Builder")]
pub struct PayloadBuilderArgs {
/// Block extra data set by the payload builder.
///
/// If the value is a `0x`-prefixed hex string, it is decoded into raw bytes. Otherwise, the
/// raw UTF-8 bytes of the string are used.
#[arg(
long = "builder.extradata",
value_parser = ExtraDataValueParser::default(),
default_value_t = DefaultPayloadBuilderValues::get_global().extra_data.clone()
default_value = DefaultPayloadBuilderValues::get_global().extra_data.as_str()
)]
pub extra_data: String,
pub extra_data: Bytes,
/// Target gas limit for built blocks.
#[arg(long = "builder.gaslimit", alias = "miner.gaslimit", value_name = "GAS_LIMIT")]
@@ -130,7 +134,7 @@ impl Default for PayloadBuilderArgs {
fn default() -> Self {
let defaults = DefaultPayloadBuilderValues::get_global();
Self {
extra_data: defaults.extra_data.clone(),
extra_data: Bytes::from(defaults.extra_data.as_bytes().to_vec()),
interval: parse_duration_from_secs_or_ms(defaults.interval.as_str()).unwrap(),
gas_limit: None,
deadline: Duration::from_secs(defaults.deadline.parse().unwrap()),
@@ -141,8 +145,8 @@ impl Default for PayloadBuilderArgs {
}
impl PayloadBuilderConfig for PayloadBuilderArgs {
fn extra_data(&self) -> Cow<'_, str> {
self.extra_data.as_str().into()
fn extra_data(&self) -> Bytes {
self.extra_data.clone()
}
fn interval(&self) -> Duration {
@@ -171,7 +175,7 @@ impl PayloadBuilderConfig for PayloadBuilderArgs {
struct ExtraDataValueParser;
impl TypedValueParser for ExtraDataValueParser {
type Value = String;
type Value = Bytes;
fn parse_ref(
&self,
@@ -181,7 +185,19 @@ impl TypedValueParser for ExtraDataValueParser {
) -> Result<Self::Value, clap::Error> {
let val =
value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
if val.len() > MAXIMUM_EXTRA_DATA_SIZE {
let bytes = if let Some(hex) = val.strip_prefix("0x") {
alloy_primitives::hex::decode(hex).map_err(|e| {
clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
format!("Invalid hex in extradata: {e}"),
)
})?
} else {
val.as_bytes().to_vec()
};
if bytes.len() > MAXIMUM_EXTRA_DATA_SIZE {
return Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
format!(
@@ -189,7 +205,8 @@ impl TypedValueParser for ExtraDataValueParser {
),
))
}
Ok(val.to_string())
Ok(bytes.into())
}
}
@@ -232,7 +249,7 @@ mod tests {
extra_data.as_str(),
])
.args;
assert_eq!(args.extra_data, extra_data);
assert_eq!(args.extra_data.as_ref(), extra_data.as_bytes());
}
#[test]
@@ -246,6 +263,49 @@ mod tests {
assert!(args.is_err());
}
#[test]
fn test_valid_hex_extra_data() {
let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE));
let args = CommandParser::<PayloadBuilderArgs>::parse_from([
"reth",
"--builder.extradata",
hex.as_str(),
])
.args;
assert_eq!(args.extra_data.as_ref(), vec![0xab; MAXIMUM_EXTRA_DATA_SIZE].as_slice());
}
#[test]
fn test_oversized_hex_extra_data() {
let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1));
assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
"reth",
"--builder.extradata",
hex.as_str(),
])
.is_err());
}
#[test]
fn test_invalid_hex_extra_data() {
assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
"reth",
"--builder.extradata",
"0xZZZZ",
])
.is_err());
}
#[test]
fn test_odd_length_hex_extra_data() {
assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
"reth",
"--builder.extradata",
"0xabc",
])
.is_err());
}
#[test]
fn payload_builder_args_default_sanity_check() {
let default_args = PayloadBuilderArgs::default();

View File

@@ -5,7 +5,7 @@ use alloy_primitives::Bytes;
use reth_chainspec::{Chain, ChainKind, NamedChain};
use reth_network::{protocol::IntoRlpxSubProtocol, NetworkPrimitives};
use reth_transaction_pool::PoolConfig;
use std::{borrow::Cow, time::Duration};
use std::time::Duration;
/// 60M gas limit
const ETHEREUM_BLOCK_GAS_LIMIT_60M: u64 = 60_000_000;
@@ -15,13 +15,8 @@ const ETHEREUM_BLOCK_GAS_LIMIT_60M: u64 = 60_000_000;
/// This provides all basic payload builder settings and is implemented by the
/// [`PayloadBuilderArgs`](crate::args::PayloadBuilderArgs) type.
pub trait PayloadBuilderConfig {
/// Block extra data set by the payload builder.
fn extra_data(&self) -> Cow<'_, str>;
/// Returns the extra data as bytes.
fn extra_data_bytes(&self) -> Bytes {
self.extra_data().as_bytes().to_vec().into()
}
fn extra_data(&self) -> Bytes;
/// The interval at which the job should build a new payload after the last.
fn interval(&self) -> Duration;

View File

@@ -206,6 +206,10 @@ impl NodeState {
self.current_stage = Some(current_stage);
}
PipelineEvent::Unwound { stage_id, result } => {
info!(stage = %stage_id, checkpoint = %result.checkpoint.block_number, "Unwound stage");
self.current_stage = None;
}
_ => (),
}
}

View File

@@ -445,7 +445,7 @@ mod tests {
build_profile: "test",
};
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current()).unwrap();
let runtime = Runtime::test();
let hooks = Hooks::builder().build();

View File

@@ -346,6 +346,18 @@ mod block_bincode {
}
}
impl super::SerdeBincodeCompat for alloy_consensus::EthereumReceipt {
type BincodeRepr<'a> = alloy_consensus::serde_bincode_compat::EthereumReceipt<'a>;
fn as_repr(&self) -> Self::BincodeRepr<'_> {
self.into()
}
fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
repr.into()
}
}
#[cfg(feature = "op")]
impl super::SerdeBincodeCompat for op_alloy_consensus::OpReceipt {
type BincodeRepr<'a> = op_alloy_consensus::serde_bincode_compat::OpReceipt<'a>;

View File

@@ -78,6 +78,12 @@ impl InMemorySize for alloy_consensus::Receipt {
}
}
impl<T: InMemorySize> InMemorySize for alloy_consensus::EthereumReceipt<T> {
fn size(&self) -> usize {
core::mem::size_of::<Self>() + self.logs.iter().map(|log| log.size()).sum::<usize>()
}
}
impl InMemorySize for LogData {
fn size(&self) -> usize {
self.data.len() + core::mem::size_of_val(self.topics())

View File

@@ -8,6 +8,15 @@ use reth_db_api::{
use std::{fmt::Debug, ops::RangeBounds};
use tracing::debug;
/// Result of a single prune step in [`DbTxPruneExt::prune_table_with_range_step`].
#[derive(Debug, Clone, Copy)]
pub(crate) struct PruneStepResult {
/// `true` if the walker is finished, `false` if it may have more data to prune.
done: bool,
/// `true` if the current entry was deleted, `false` if it was skipped.
deleted: bool,
}
pub(crate) trait DbTxPruneExt: DbTxMut + DbTx {
/// Clear the entire table in a single operation.
///
@@ -91,17 +100,20 @@ pub(crate) trait DbTxPruneExt: DbTxMut + DbTx {
break false
}
let done = self.prune_table_with_range_step(
let result = self.prune_table_with_range_step(
&mut walker,
limiter,
&mut skip_filter,
&mut delete_callback,
)?;
if done {
if result.deleted {
deleted_entries += 1;
}
if result.done {
break true
}
deleted_entries += 1;
};
Ok((deleted_entries, done))
@@ -109,8 +121,6 @@ pub(crate) trait DbTxPruneExt: DbTxMut + DbTx {
/// Steps once with the given walker and prunes the entry in the table.
///
/// Returns `true` if the walker is finished, `false` if it may have more data to prune.
///
/// CAUTION: Pruner limits are not checked. This allows for a clean exit of a prune run that's
/// pruning different tables concurrently, by letting them step to the same height before
/// timing out.
@@ -120,18 +130,21 @@ pub(crate) trait DbTxPruneExt: DbTxMut + DbTx {
limiter: &mut PruneLimiter,
skip_filter: &mut impl FnMut(&TableRow<T>) -> bool,
delete_callback: &mut impl FnMut(TableRow<T>),
) -> Result<bool, DatabaseError> {
let Some(res) = walker.next() else { return Ok(true) };
) -> Result<PruneStepResult, DatabaseError> {
let Some(res) = walker.next() else {
return Ok(PruneStepResult { done: true, deleted: false })
};
let row = res?;
if !skip_filter(&row) {
if skip_filter(&row) {
Ok(PruneStepResult { done: false, deleted: false })
} else {
walker.delete_current()?;
limiter.increment_deleted_entries_count();
delete_callback(row);
Ok(PruneStepResult { done: false, deleted: true })
}
Ok(false)
}
/// Prune a DUPSORT table for the specified key range.

View File

@@ -24,7 +24,7 @@ where
pub fn bootstrap(config: EthConfig, executor: Runtime, eth_api: EthApi) -> Self {
let filter = EthFilter::new(eth_api.clone(), config.filter_config(), executor.clone());
let pubsub = EthPubSub::with_spawner(eth_api.clone(), executor);
let pubsub = EthPubSub::new(eth_api.clone(), executor);
Self { api: eth_api, filter, pubsub }
}

View File

@@ -15,7 +15,6 @@ workspace = true
# reth
reth-primitives-traits.workspace = true
reth-evm.workspace = true
reth-ethereum-primitives.workspace = true
# ethereum
alloy-primitives.workspace = true
@@ -29,7 +28,6 @@ alloy-evm = { workspace = true, features = ["rpc"] }
# optimism
op-alloy-consensus = { workspace = true, optional = true }
op-alloy-rpc-types = { workspace = true, optional = true }
op-alloy-network = { workspace = true, optional = true }
# io
jsonrpsee-types.workspace = true
@@ -48,7 +46,6 @@ default = []
op = [
"dep:op-alloy-consensus",
"dep:op-alloy-rpc-types",
"dep:op-alloy-network",
"reth-evm/op",
"reth-primitives-traits/op",
"alloy-evm/op",

View File

@@ -1,47 +0,0 @@
//! Conversion traits for block responses to primitive block types.
use alloy_network::Network;
use std::convert::Infallible;
/// Trait for converting network block responses to primitive block types.
pub trait TryFromBlockResponse<N: Network> {
/// The error type returned if the conversion fails.
type Error: core::error::Error + Send + Sync + Unpin;
/// Converts a network block response to a primitive block type.
///
/// # Returns
///
/// Returns `Ok(Self)` on successful conversion, or `Err(Self::Error)` if the conversion fails.
fn from_block_response(block_response: N::BlockResponse) -> Result<Self, Self::Error>
where
Self: Sized;
}
impl<N: Network, T> TryFromBlockResponse<N> for alloy_consensus::Block<T>
where
N::BlockResponse: Into<Self>,
{
type Error = Infallible;
fn from_block_response(block_response: N::BlockResponse) -> Result<Self, Self::Error> {
Ok(block_response.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::{Block, TxEnvelope};
use alloy_network::Ethereum;
use alloy_rpc_types_eth::BlockTransactions;
#[test]
fn test_try_from_block_response() {
let rpc_block: alloy_rpc_types_eth::Block =
alloy_rpc_types_eth::Block::new(Default::default(), BlockTransactions::Full(vec![]));
let result =
<Block<TxEnvelope> as TryFromBlockResponse<Ethereum>>::from_block_response(rpc_block);
assert!(result.is_ok());
}
}

View File

@@ -10,17 +10,12 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod block;
pub mod receipt;
mod rpc;
pub mod transaction;
pub use block::TryFromBlockResponse;
pub use receipt::TryFromReceiptResponse;
pub use rpc::*;
pub use transaction::{
RpcConvert, RpcConverter, TransactionConversionError, TryFromTransactionResponse, TryIntoSimTx,
TxInfoMapper,
RpcConvert, RpcConverter, TransactionConversionError, TryIntoSimTx, TxInfoMapper,
};
pub use alloy_evm::rpc::{CallFees, CallFeesError, EthTxEnvError, TryIntoTxEnv};

View File

@@ -1,102 +0,0 @@
//! Conversion traits for receipt responses to primitive receipt types.
use alloy_network::Network;
use std::convert::Infallible;
/// Trait for converting network receipt responses to primitive receipt types.
pub trait TryFromReceiptResponse<N: Network> {
/// The error type returned if the conversion fails.
type Error: core::error::Error + Send + Sync + Unpin;
/// Converts a network receipt response to a primitive receipt type.
///
/// # Returns
///
/// Returns `Ok(Self)` on successful conversion, or `Err(Self::Error)` if the conversion fails.
fn from_receipt_response(receipt_response: N::ReceiptResponse) -> Result<Self, Self::Error>
where
Self: Sized;
}
impl TryFromReceiptResponse<alloy_network::Ethereum> for reth_ethereum_primitives::Receipt {
type Error = Infallible;
fn from_receipt_response(
receipt_response: alloy_rpc_types_eth::TransactionReceipt,
) -> Result<Self, Self::Error> {
Ok(receipt_response.into_inner().into())
}
}
#[cfg(feature = "op")]
impl TryFromReceiptResponse<op_alloy_network::Optimism> for op_alloy_consensus::OpReceipt {
type Error = Infallible;
fn from_receipt_response(
receipt_response: op_alloy_rpc_types::OpTransactionReceipt,
) -> Result<Self, Self::Error> {
Ok(receipt_response.inner.inner.into_components().0.map_logs(Into::into))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::ReceiptEnvelope;
use alloy_network::Ethereum;
use reth_ethereum_primitives::Receipt;
#[test]
fn test_try_from_receipt_response() {
let rpc_receipt = alloy_rpc_types_eth::TransactionReceipt {
inner: ReceiptEnvelope::Eip1559(Default::default()),
transaction_hash: Default::default(),
transaction_index: None,
block_hash: None,
block_number: None,
gas_used: 0,
effective_gas_price: 0,
blob_gas_used: None,
blob_gas_price: None,
from: Default::default(),
to: None,
contract_address: None,
};
let result =
<Receipt as TryFromReceiptResponse<Ethereum>>::from_receipt_response(rpc_receipt);
assert!(result.is_ok());
}
#[cfg(feature = "op")]
#[test]
fn test_try_from_receipt_response_optimism() {
use alloy_consensus::ReceiptWithBloom;
use op_alloy_consensus::OpReceipt;
use op_alloy_network::Optimism;
use op_alloy_rpc_types::OpTransactionReceipt;
let op_receipt = OpTransactionReceipt {
inner: alloy_rpc_types_eth::TransactionReceipt {
inner: ReceiptWithBloom {
receipt: OpReceipt::Eip1559(Default::default()),
logs_bloom: Default::default(),
},
transaction_hash: Default::default(),
transaction_index: None,
block_hash: None,
block_number: None,
gas_used: 0,
effective_gas_price: 0,
blob_gas_used: None,
blob_gas_price: None,
from: Default::default(),
to: None,
contract_address: None,
},
l1_block_info: Default::default(),
};
let result =
<OpReceipt as TryFromReceiptResponse<Optimism>>::from_receipt_response(op_receipt);
assert!(result.is_ok());
}
}

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