mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
7 Commits
devnet4
...
yk/state_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51cd4d1523 | ||
|
|
a6a7b6021c | ||
|
|
994ed1bbd8 | ||
|
|
29fdbdb7b3 | ||
|
|
3a82e17436 | ||
|
|
c5c9cd96d0 | ||
|
|
f84f6f02d9 |
@@ -1,6 +1,6 @@
|
||||
[profile.default]
|
||||
retries = { backoff = "exponential", count = 2, delay = "2s", jitter = true }
|
||||
slow-timeout = { period = "30s", terminate-after = 2 }
|
||||
slow-timeout = { period = "30s", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(general_state_tests)"
|
||||
|
||||
@@ -12,7 +12,7 @@ workflows:
|
||||
# Check that `A` activates the features of `B`.
|
||||
"propagate-feature",
|
||||
# These are the features to check:
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,tracy,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,otlp-logs,js-tracer,portable,keccak-cache-global,trie-debug",
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,js-tracer,portable,keccak-cache-global",
|
||||
# Do not try to add a new section to `[features]` of `A` only because `B` exposes that feature. There are edge-cases where this is still needed, but we can add them manually.
|
||||
"--left-side-feature-missing=ignore",
|
||||
# Ignore the case that `A` it outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on.
|
||||
|
||||
46
.github/CODEOWNERS
vendored
46
.github/CODEOWNERS
vendored
@@ -1,51 +1,45 @@
|
||||
* @gakonst
|
||||
crates/blockchain-tree-api/ @rakita @mattsse @Rjected
|
||||
crates/blockchain-tree/ @rakita @mattsse @Rjected
|
||||
crates/chain-state/ @fgimenez @mattsse
|
||||
crates/chainspec/ @Rjected @joshieDo @mattsse
|
||||
crates/cli/ @mattsse @Rjected
|
||||
crates/config/ @shekhirin @mattsse @Rjected
|
||||
crates/cli/ @mattsse
|
||||
crates/consensus/ @mattsse @Rjected
|
||||
crates/e2e-test-utils/ @mattsse @Rjected @klkvr @fgimenez
|
||||
crates/engine/ @mattsse @Rjected @mediocregopher @yongkangc
|
||||
crates/era/ @mattsse
|
||||
crates/era-downloader/ @mattsse
|
||||
crates/era-utils/ @mattsse
|
||||
crates/engine/ @mattsse @Rjected @fgimenez @mediocregopher @yongkangc
|
||||
crates/era/ @mattsse @RomanHodulak
|
||||
crates/errors/ @mattsse
|
||||
crates/ethereum-forks/ @mattsse @Rjected
|
||||
crates/ethereum/ @mattsse @Rjected
|
||||
crates/etl/ @joshieDo @shekhirin
|
||||
crates/evm/ @mattsse @Rjected @klkvr
|
||||
crates/evm/ @rakita @mattsse @Rjected
|
||||
crates/exex/ @shekhirin
|
||||
crates/fs-util/ @mattsse
|
||||
crates/metrics/ @mattsse @Rjected
|
||||
crates/net/ @mattsse @Rjected
|
||||
crates/net/downloaders/ @Rjected
|
||||
crates/node/ @mattsse @Rjected @klkvr
|
||||
crates/optimism/ @mattsse @Rjected @fgimenez
|
||||
crates/payload/ @mattsse @Rjected
|
||||
crates/primitives-traits/ @Rjected @mattsse @klkvr
|
||||
crates/primitives-traits/ @Rjected @RomanHodulak @mattsse @klkvr
|
||||
crates/primitives/ @Rjected @mattsse @klkvr
|
||||
crates/prune/ @shekhirin @joshieDo
|
||||
crates/ress/ @shekhirin @Rjected
|
||||
crates/revm/ @mattsse
|
||||
crates/rpc/ @mattsse @Rjected
|
||||
crates/ress @shekhirin @Rjected
|
||||
crates/revm/ @mattsse @rakita
|
||||
crates/rpc/ @mattsse @Rjected @RomanHodulak
|
||||
crates/stages/ @shekhirin @mediocregopher
|
||||
crates/static-file/ @joshieDo @shekhirin
|
||||
crates/stateless/ @mattsse
|
||||
crates/storage/codecs/ @joshieDo
|
||||
crates/storage/db-api/ @joshieDo
|
||||
crates/storage/db-api/ @joshieDo @rakita
|
||||
crates/storage/db-common/ @Rjected
|
||||
crates/storage/db/ @joshieDo
|
||||
crates/storage/errors/ @joshieDo
|
||||
crates/storage/libmdbx-rs/ @shekhirin
|
||||
crates/storage/db/ @joshieDo @rakita
|
||||
crates/storage/errors/ @rakita
|
||||
crates/storage/libmdbx-rs/ @rakita @shekhirin
|
||||
crates/storage/nippy-jar/ @joshieDo @shekhirin
|
||||
crates/storage/provider/ @joshieDo @shekhirin @yongkangc
|
||||
crates/storage/provider/ @rakita @joshieDo @shekhirin
|
||||
crates/storage/storage-api/ @joshieDo
|
||||
crates/tasks/ @mattsse @DaniPopes
|
||||
crates/tokio-util/ @mattsse
|
||||
crates/tracing/ @mattsse @shekhirin
|
||||
crates/tracing-otlp/ @mattsse @Rjected
|
||||
crates/tasks/ @mattsse
|
||||
crates/tokio-util/ @fgimenez
|
||||
crates/transaction-pool/ @mattsse @yongkangc
|
||||
crates/trie/ @Rjected @shekhirin @mediocregopher @yongkangc
|
||||
bin/reth/ @mattsse @shekhirin @Rjected
|
||||
bin/reth-bench/ @mattsse @Rjected @shekhirin @yongkangc
|
||||
crates/trie/ @Rjected @shekhirin @mediocregopher
|
||||
bin/reth-bench-compare/ @mediocregopher @shekhirin @yongkangc
|
||||
etc/ @Rjected @shekhirin
|
||||
.github/ @gakonst @DaniPopes
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -43,6 +43,7 @@ body:
|
||||
|
||||
- `~/.cache/reth/logs` on Linux
|
||||
- `~/Library/Caches/reth/logs` on macOS
|
||||
- `%localAppData%/reth/logs` on Windows
|
||||
render: text
|
||||
validations:
|
||||
required: false
|
||||
@@ -57,6 +58,8 @@ body:
|
||||
- Linux (ARM)
|
||||
- Mac (Intel)
|
||||
- Mac (Apple Silicon)
|
||||
- Windows (x86)
|
||||
- Windows (ARM)
|
||||
- type: dropdown
|
||||
id: container_type
|
||||
attributes:
|
||||
|
||||
1
.github/actionlint.yaml
vendored
1
.github/actionlint.yaml
vendored
@@ -5,4 +5,3 @@ self-hosted-runner:
|
||||
- depot-ubuntu-latest-4
|
||||
- depot-ubuntu-latest-8
|
||||
- depot-ubuntu-latest-16
|
||||
- available
|
||||
|
||||
88
.github/assets/check_rv32imac.sh
vendored
Executable file
88
.github/assets/check_rv32imac.sh
vendored
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set +e # Disable immediate exit on error
|
||||
|
||||
# Array of crates to check
|
||||
crates_to_check=(
|
||||
reth-codecs-derive
|
||||
reth-primitives
|
||||
reth-primitives-traits
|
||||
reth-network-peers
|
||||
reth-trie-common
|
||||
reth-trie-sparse
|
||||
reth-chainspec
|
||||
reth-consensus
|
||||
reth-consensus-common
|
||||
reth-prune-types
|
||||
reth-static-file-types
|
||||
reth-storage-errors
|
||||
reth-execution-errors
|
||||
reth-errors
|
||||
reth-execution-types
|
||||
reth-db-models
|
||||
reth-evm
|
||||
reth-revm
|
||||
reth-storage-api
|
||||
|
||||
## ethereum
|
||||
reth-evm-ethereum
|
||||
reth-ethereum-forks
|
||||
reth-ethereum-primitives
|
||||
reth-ethereum-consensus
|
||||
reth-stateless
|
||||
|
||||
## optimism
|
||||
reth-optimism-chainspec
|
||||
reth-optimism-forks
|
||||
reth-optimism-consensus
|
||||
reth-optimism-primitives
|
||||
reth-optimism-evm
|
||||
)
|
||||
|
||||
# Array to hold the results
|
||||
results=()
|
||||
# Flag to track if any command fails
|
||||
any_failed=0
|
||||
|
||||
for crate in "${crates_to_check[@]}"; do
|
||||
cmd="cargo +stable build -p $crate --target riscv32imac-unknown-none-elf --no-default-features"
|
||||
|
||||
if [ -n "$CI" ]; then
|
||||
echo "::group::$cmd"
|
||||
else
|
||||
printf "\n%s:\n %s\n" "$crate" "$cmd"
|
||||
fi
|
||||
|
||||
set +e # Disable immediate exit on error
|
||||
# Run the command and capture the return code
|
||||
$cmd
|
||||
ret_code=$?
|
||||
set -e # Re-enable immediate exit on error
|
||||
|
||||
# Store the result in the dictionary
|
||||
if [ $ret_code -eq 0 ]; then
|
||||
results+=("1:✅:$crate")
|
||||
else
|
||||
results+=("2:❌:$crate")
|
||||
any_failed=1
|
||||
fi
|
||||
|
||||
if [ -n "$CI" ]; then
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort the results by status and then by crate name
|
||||
IFS=$'\n' sorted_results=($(sort <<<"${results[*]}"))
|
||||
unset IFS
|
||||
|
||||
# Print summary
|
||||
echo -e "\nSummary of build results:"
|
||||
for result in "${sorted_results[@]}"; do
|
||||
status="${result#*:}"
|
||||
status="${status%%:*}"
|
||||
crate="${result##*:}"
|
||||
echo "$status $crate"
|
||||
done
|
||||
|
||||
# Exit with a non-zero status if any command fails
|
||||
exit $any_failed
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uxo pipefail
|
||||
set +e # Disable immediate exit on error
|
||||
|
||||
readarray -t crates < <(
|
||||
cargo metadata --format-version=1 --no-deps | jq -r '.packages[].name' | grep '^reth' | sort
|
||||
)
|
||||
# Array of crates to compile
|
||||
crates=($(cargo metadata --format-version=1 --no-deps | jq -r '.packages[].name' | grep '^reth' | sort))
|
||||
|
||||
# Array of crates to exclude
|
||||
# Used with the `contains` function.
|
||||
# shellcheck disable=SC2034
|
||||
exclude_crates=(
|
||||
# The following require investigation if they can be fixed
|
||||
@@ -22,7 +23,6 @@ exclude_crates=(
|
||||
reth-downloaders
|
||||
reth-e2e-test-utils
|
||||
reth-engine-service
|
||||
reth-execution-cache
|
||||
reth-engine-tree
|
||||
reth-engine-util
|
||||
reth-eth-wire
|
||||
@@ -40,6 +40,12 @@ exclude_crates=(
|
||||
reth-node-ethereum
|
||||
reth-node-events
|
||||
reth-node-metrics
|
||||
reth-optimism-cli
|
||||
reth-optimism-flashblocks
|
||||
reth-optimism-node
|
||||
reth-optimism-payload-builder
|
||||
reth-optimism-rpc
|
||||
reth-optimism-storage
|
||||
reth-rpc
|
||||
reth-rpc-api
|
||||
reth-rpc-api-testing-util
|
||||
@@ -56,7 +62,6 @@ exclude_crates=(
|
||||
reth-ress-provider
|
||||
# The following are not supposed to be working
|
||||
reth # all of the crates below
|
||||
reth-bb # binary-only, uses tokio features unsupported on wasm
|
||||
reth-storage-rpc-provider
|
||||
reth-invalid-block-hooks # reth-provider
|
||||
reth-libmdbx # mdbx
|
||||
@@ -65,7 +70,6 @@ exclude_crates=(
|
||||
reth-provider # tokio
|
||||
reth-prune # tokio
|
||||
reth-prune-static-files # reth-provider
|
||||
reth-tasks # tokio rt-multi-thread
|
||||
reth-stages-api # reth-provider, reth-prune
|
||||
reth-static-file # tokio
|
||||
reth-transaction-pool # c-kzg
|
||||
@@ -73,41 +77,77 @@ exclude_crates=(
|
||||
reth-trie-parallel # tokio
|
||||
reth-trie-sparse-parallel # rayon
|
||||
reth-testing-utils
|
||||
reth-optimism-txpool # reth-transaction-pool
|
||||
reth-era-downloader # tokio
|
||||
reth-era-utils # tokio
|
||||
reth-tracing-otlp
|
||||
reth-node-ethstats
|
||||
)
|
||||
|
||||
# Array to hold the results
|
||||
results=()
|
||||
# Flag to track if any command fails
|
||||
any_failed=0
|
||||
tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t reth-check)
|
||||
trap 'rm -rf -- "$tmpdir"' EXIT INT TERM
|
||||
|
||||
# Function to check if a value exists in an array
|
||||
contains() {
|
||||
local array="$1[@]"
|
||||
local seeking="$2"
|
||||
local element
|
||||
local seeking=$2
|
||||
local in=1
|
||||
for element in "${!array}"; do
|
||||
[[ "$element" == "$seeking" ]] && return 0
|
||||
if [[ "$element" == "$seeking" ]]; then
|
||||
in=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
return $in
|
||||
}
|
||||
|
||||
for crate in "${crates[@]}"; do
|
||||
if contains exclude_crates "$crate"; then
|
||||
echo "⏭️ $crate"
|
||||
results+=("3:⏭️:$crate")
|
||||
continue
|
||||
fi
|
||||
|
||||
outfile="$tmpdir/$crate.log"
|
||||
if cargo +stable build -p "$crate" --target wasm32-wasip1 --no-default-features --color never >"$outfile" 2>&1; then
|
||||
echo "✅ $crate"
|
||||
cmd="cargo +stable build -p $crate --target wasm32-wasip1 --no-default-features"
|
||||
|
||||
if [ -n "$CI" ]; then
|
||||
echo "::group::$cmd"
|
||||
else
|
||||
echo "❌ $crate"
|
||||
sed 's/^/ /' "$outfile"
|
||||
echo ""
|
||||
printf "\n%s:\n %s\n" "$crate" "$cmd"
|
||||
fi
|
||||
|
||||
set +e # Disable immediate exit on error
|
||||
# Run the command and capture the return code
|
||||
$cmd
|
||||
ret_code=$?
|
||||
set -e # Re-enable immediate exit on error
|
||||
|
||||
# Store the result in the dictionary
|
||||
if [ $ret_code -eq 0 ]; then
|
||||
results+=("1:✅:$crate")
|
||||
else
|
||||
results+=("2:❌:$crate")
|
||||
any_failed=1
|
||||
fi
|
||||
|
||||
if [ -n "$CI" ]; then
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort the results by status and then by crate name
|
||||
IFS=$'\n' sorted_results=($(sort <<<"${results[*]}"))
|
||||
unset IFS
|
||||
|
||||
# Print summary
|
||||
echo -e "\nSummary of build results:"
|
||||
for result in "${sorted_results[@]}"; do
|
||||
status="${result#*:}"
|
||||
status="${status%%:*}"
|
||||
crate="${result##*:}"
|
||||
echo "$status $crate"
|
||||
done
|
||||
|
||||
# Exit with a non-zero status if any command fails
|
||||
exit $any_failed
|
||||
@@ -7,7 +7,7 @@ FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y libclang-dev pkg-config
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
#
|
||||
# We prepare the build plan
|
||||
@@ -11,14 +11,8 @@ go build .
|
||||
|
||||
# Run each hive command in the background for each simulator and wait
|
||||
echo "Building images"
|
||||
./hive -client reth --sim "ethereum/eels/consume-engine" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/eels/consume-rlp" \
|
||||
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
|
||||
--sim.buildarg branch=forks/osaka \
|
||||
--sim.timelimit 1s || true &
|
||||
# TODO: test code has been moved from https://github.com/ethereum/execution-spec-tests to https://github.com/ethereum/execution-specs we need to pin eels branch with `--sim.buildarg branch=<release-branch-name>` once we have the fusaka release tagged on the new repo
|
||||
./hive -client reth --sim "ethereum/eels" --sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
|
||||
./hive -client reth --sim "ethereum/rpc-compat" -sim.timelimit 1s || true &
|
||||
@@ -44,6 +38,6 @@ for pid in "${saving_pids[@]}"; do
|
||||
done
|
||||
|
||||
# Make sure we don't rebuild images on the CI jobs
|
||||
git apply ../.github/scripts/hive/no_sim_build.diff
|
||||
git apply ../.github/assets/hive/no_sim_build.diff
|
||||
go build .
|
||||
mv ./hive ../hive_assets/
|
||||
@@ -1,6 +1,9 @@
|
||||
# tracked by https://github.com/paradigmxyz/reth/issues/13879
|
||||
rpc-compat:
|
||||
- debug_getRawBlock/get-invalid-number (reth)
|
||||
- debug_getRawHeader/get-invalid-number (reth)
|
||||
- debug_getRawReceipts/get-invalid-number (reth)
|
||||
- debug_getRawReceipts/get-block-n (reth)
|
||||
- debug_getRawTransaction/get-invalid-hash (reth)
|
||||
|
||||
- eth_getStorageAt/get-storage-invalid-key-too-large (reth)
|
||||
@@ -13,11 +16,28 @@ rpc-compat:
|
||||
# syncing mode, the test expects syncing to be false on start
|
||||
- eth_syncing/check-syncing (reth)
|
||||
|
||||
engine-withdrawals: [ ]
|
||||
# no fix due to https://github.com/paradigmxyz/reth/issues/8732
|
||||
engine-withdrawals:
|
||||
- Withdrawals Fork On Genesis (Paris) (reth)
|
||||
- Withdrawals Fork on Block 1 (Paris) (reth)
|
||||
- Withdrawals Fork on Block 2 (Paris) (reth)
|
||||
- Withdrawals Fork on Block 3 (Paris) (reth)
|
||||
- Withdraw to a single account (Paris) (reth)
|
||||
- Withdraw to two accounts (Paris) (reth)
|
||||
- Withdraw many accounts (Paris) (reth)
|
||||
- Withdraw zero amount (Paris) (reth)
|
||||
- Empty Withdrawals (Paris) (reth)
|
||||
- Corrupted Block Hash Payload (INVALID) (Paris) (reth)
|
||||
- Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org (Paris) (reth)
|
||||
|
||||
engine-api: [ ]
|
||||
|
||||
engine-cancun: [ ]
|
||||
# no fix due to https://github.com/paradigmxyz/reth/issues/8732
|
||||
engine-cancun:
|
||||
- Invalid PayloadAttributes, Missing BeaconRoot, Syncing=True (Cancun) (reth)
|
||||
# the test fails with older versions of the code for which it passed before, probably related to changes
|
||||
# in hive or its dependencies
|
||||
- Blob Transaction Ordering, Multiple Clients (Cancun) (reth)
|
||||
|
||||
sync: [ ]
|
||||
|
||||
@@ -29,7 +49,7 @@ engine-auth: [ ]
|
||||
# The test artificially creates an empty account with storage, then tests EIP-7610's behavior.
|
||||
# On mainnet, ~25 such accounts exist as contract addresses (derived from keccak(prefix, caller,
|
||||
# nonce/salt), not from public keys). No private key exists for contract addresses. To trigger
|
||||
# this with EIP-7702, you'd need to recover a private key from one of the already deployed contract addresses - mathematically impossible.
|
||||
# this with EIP-7702, you'd need to recover a private key from one of the already deployed contract addresses - mathematically impossible.
|
||||
#
|
||||
# tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_*
|
||||
# Requires hash collision on create2 address to target already deployed accounts with storage.
|
||||
@@ -39,6 +59,10 @@ 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
|
||||
@@ -47,8 +71,32 @@ 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
|
||||
@@ -11,16 +11,18 @@
|
||||
#
|
||||
# When a test should no longer be ignored, remove it from this list.
|
||||
|
||||
# flaky
|
||||
engine-withdrawals:
|
||||
- Withdrawals Fork on Block 1 - 8 Block Re-Org NewPayload (Paris) (reth)
|
||||
- Withdrawals Fork on Block 8 - 10 Block Re-Org NewPayload (Paris) (reth)
|
||||
- Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org (Paris) (reth)
|
||||
- Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
|
||||
engine-cancun:
|
||||
- Transaction Re-Org, New Payload on Revert Back (Cancun) (reth)
|
||||
- Transaction Re-Org, Re-Org to Different Block (Cancun) (reth)
|
||||
- Transaction Re-Org, Re-Org Out (Cancun) (reth)
|
||||
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=False, Invalid P9 (Cancun) (reth)
|
||||
# Hive test infra bug: geth sidecar switched to PathScheme for state storage, which has
|
||||
# strict trie integrity requirements incompatible with inserting intentionally invalid blocks.
|
||||
# Affects all clients, not just reth. Tracked: https://github.com/ethereum/hive/issues/1382
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=False, Invalid P8 (Cancun) (reth)
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=True, Invalid P8 (Cancun) (reth)
|
||||
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Cancun) (reth)
|
||||
engine-api:
|
||||
- Transaction Re-Org, Re-Org Out (Paris) (reth)
|
||||
- Transaction Re-Org, Re-Org to Different Block (Paris) (reth)
|
||||
@@ -30,3 +32,5 @@ engine-api:
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Transaction Signature, EmptyTxs=False, CanonicalReOrg=True, Invalid P9 (Paris) (reth)
|
||||
- Invalid Missing Ancestor Syncing ReOrg, Transaction Signature, EmptyTxs=False, CanonicalReOrg=False, Invalid P9 (Paris) (reth)
|
||||
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=True, Invalid P10 (Paris) (reth)
|
||||
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Paris) (reth)
|
||||
- Multiple New Payloads Extending Canonical Chain, Set Head to First Payload Received (Paris) (reth)
|
||||
@@ -6,20 +6,8 @@ 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.limit.exact=false \
|
||||
--sim.parallelism "${parallelism}" \
|
||||
--client reth \
|
||||
2>&1 | tee /tmp/log || true
|
||||
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism 16 --client reth 2>&1 | tee /tmp/log || true
|
||||
}
|
||||
|
||||
check_log() {
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Installs Geth (https://geth.ethereum.org) in $HOME/bin for x86_64 Linux.
|
||||
|
||||
set -exo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
GETH_BUILD=${GETH_BUILD:-"1.13.4-3f907d6a"}
|
||||
|
||||
36
.github/assets/kurtosis_op_network_params.yaml
vendored
Normal file
36
.github/assets/kurtosis_op_network_params.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
ethereum_package:
|
||||
participants:
|
||||
- el_type: reth
|
||||
el_extra_params:
|
||||
- "--rpc.eth-proof-window=100"
|
||||
cl_type: teku
|
||||
network_params:
|
||||
preset: minimal
|
||||
genesis_delay: 5
|
||||
additional_preloaded_contracts: '
|
||||
{
|
||||
"0x4e59b44847b379578588920cA78FbF26c0B4956C": {
|
||||
"balance": "0ETH",
|
||||
"code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3",
|
||||
"storage": {},
|
||||
"nonce": "1"
|
||||
}
|
||||
}'
|
||||
optimism_package:
|
||||
chains:
|
||||
chain0:
|
||||
participants:
|
||||
node0:
|
||||
el:
|
||||
type: op-geth
|
||||
cl:
|
||||
type: op-node
|
||||
node1:
|
||||
el:
|
||||
type: op-reth
|
||||
image: "ghcr.io/paradigmxyz/op-reth:kurtosis-ci"
|
||||
cl:
|
||||
type: op-node
|
||||
network_params:
|
||||
holocene_time_offset: 0
|
||||
isthmus_time_offset: 0
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -4,17 +4,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "A-dependencies"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
open-pull-requests-limit: 1
|
||||
groups:
|
||||
cargo-weekly:
|
||||
applies-to: "version-updates"
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
106
.github/scripts/bench-job-summary.js
vendored
106
.github/scripts/bench-job-summary.js
vendored
@@ -1,106 +0,0 @@
|
||||
// Generates a rich GitHub Actions job summary for reth-bench results.
|
||||
//
|
||||
// Reads from environment:
|
||||
// BENCH_WORK_DIR – Directory containing summary.json
|
||||
// BENCH_PR – PR number (may be empty)
|
||||
// BENCH_ACTOR – GitHub user who triggered the bench
|
||||
// BENCH_CORES – CPU core limit (0 = all)
|
||||
// BENCH_WARMUP_BLOCKS – Number of warmup blocks
|
||||
// BENCH_SAMPLY – 'true' if samply profiling was enabled
|
||||
// BENCH_ABBA – 'true' if ABBA interleaved order was used
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const jobSummary = require('./.github/scripts/bench-job-summary.js');
|
||||
// await jobSummary({ core, context, chartSha, grafanaUrl, runId });
|
||||
|
||||
const fs = require('fs');
|
||||
const { verdict, loadSamplyUrls, blocksLabel, metricRows, waitTimeRows } = require('./bench-utils');
|
||||
|
||||
module.exports = async function ({ core, context, chartSha, grafanaUrl, runId }) {
|
||||
let summary;
|
||||
try {
|
||||
summary = JSON.parse(fs.readFileSync(process.env.BENCH_WORK_DIR + '/summary.json', 'utf8'));
|
||||
} catch (e) {
|
||||
await core.summary.addRaw('⚠️ Benchmark completed but failed to load summary.').write();
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const prNumber = process.env.BENCH_PR;
|
||||
const actor = process.env.BENCH_ACTOR;
|
||||
const commitUrl = `https://github.com/${repo}/commit`;
|
||||
|
||||
const { emoji, label } = verdict(summary.changes);
|
||||
const baselineLink = `[\`${summary.baseline.name}\`](${commitUrl}/${summary.baseline.ref})`;
|
||||
const featureLink = `[\`${summary.feature.name}\`](${commitUrl}/${summary.feature.ref})`;
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
|
||||
// Header & metadata
|
||||
const metaParts = [];
|
||||
if (prNumber) metaParts.push(`**[PR #${prNumber}](https://github.com/${repo}/pull/${prNumber})**`);
|
||||
metaParts.push(`triggered by @${actor}`);
|
||||
|
||||
let md = `# ${emoji} ${label}\n\n`;
|
||||
md += metaParts.join(' · ') + '\n\n';
|
||||
md += `**Baseline:** ${baselineLink}\n`;
|
||||
md += `**Feature:** ${featureLink} ([diff](${diffUrl}))\n`;
|
||||
md += blocksLabel(summary).map(p => `**${p.key}:** ${p.value}`).join(' · ') + '\n\n';
|
||||
|
||||
// Main comparison table
|
||||
const rows = metricRows(summary);
|
||||
md += `| Metric | Baseline | Feature | Change |\n`;
|
||||
md += `|--------|----------|---------|--------|\n`;
|
||||
for (const r of rows) {
|
||||
md += `| ${r.label} | ${r.baseline} | ${r.feature} | ${r.change} |\n`;
|
||||
}
|
||||
md += '\n';
|
||||
|
||||
// Wait time breakdown
|
||||
const wtRows = waitTimeRows(summary);
|
||||
if (wtRows.length > 0) {
|
||||
md += `### Wait Time Breakdown\n\n`;
|
||||
md += `| Metric | Baseline | Feature |\n`;
|
||||
md += `|--------|----------|--------|\n`;
|
||||
for (const r of wtRows) {
|
||||
md += `| ${r.title} | ${r.baseline} | ${r.feature} |\n`;
|
||||
}
|
||||
md += '\n';
|
||||
}
|
||||
|
||||
// Charts
|
||||
if (chartSha) {
|
||||
const prNum = prNumber || '0';
|
||||
const baseUrl = `https://raw.githubusercontent.com/decofe/reth-bench-charts/${chartSha}/pr/${prNum}/${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' },
|
||||
];
|
||||
md += `### Charts\n\n`;
|
||||
for (const chart of charts) {
|
||||
md += `<details><summary>${chart.label}</summary>\n\n`;
|
||||
md += `\n\n`;
|
||||
md += `</details>\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Samply profiles
|
||||
const samplyUrls = loadSamplyUrls(process.env.BENCH_WORK_DIR);
|
||||
const samplyLinks = Object.entries(samplyUrls).map(([run, url]) => `- **${run}**: [Firefox Profiler](${url})`);
|
||||
if (samplyLinks.length > 0) {
|
||||
md += `### Samply Profiles\n\n${samplyLinks.join('\n')}\n\n`;
|
||||
}
|
||||
|
||||
// Grafana
|
||||
if (grafanaUrl) {
|
||||
md += `### Grafana Dashboard\n\n[View real-time metrics](${grafanaUrl})\n\n`;
|
||||
}
|
||||
|
||||
// Node errors
|
||||
try {
|
||||
const errors = fs.readFileSync(process.env.BENCH_WORK_DIR + '/errors.md', 'utf8');
|
||||
if (errors.trim()) md += '\n' + errors + '\n';
|
||||
} catch {}
|
||||
|
||||
await core.summary.addRaw(md).write();
|
||||
};
|
||||
276
.github/scripts/bench-metrics-proxy.py
vendored
276
.github/scripts/bench-metrics-proxy.py
vendored
@@ -1,276 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Prometheus metrics proxy that fetches from a local reth node and
|
||||
re-exposes with additional benchmark labels.
|
||||
|
||||
Reads labels from a JSON file (updated by local-reth-bench.sh between runs)
|
||||
and injects them into every Prometheus metric line.
|
||||
|
||||
Returns empty 200 when reth is not running (clean Grafana gaps).
|
||||
"""
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
|
||||
def read_labels(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def inject_labels(metrics_bytes, label_str, label_names):
|
||||
"""Inject labels into Prometheus text format.
|
||||
|
||||
Operates on bytes and uses simple string ops instead of regex
|
||||
for speed on large payloads (reth exposes thousands of metrics).
|
||||
|
||||
Skips injecting into lines that already contain any of the label names
|
||||
to avoid duplicate labels (which Prometheus rejects).
|
||||
"""
|
||||
if not label_str:
|
||||
return metrics_bytes
|
||||
|
||||
label_bytes = label_str.encode("utf-8")
|
||||
# Pre-encode label names for fast duplicate detection
|
||||
label_name_bytes = [n.encode("utf-8") for n in label_names]
|
||||
out = []
|
||||
for line in metrics_bytes.split(b"\n"):
|
||||
# Skip comments and blank lines
|
||||
if line.startswith(b"#") or not line:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
brace = line.find(b"{")
|
||||
space = line.find(b" ")
|
||||
|
||||
if space == -1:
|
||||
# Malformed, pass through
|
||||
out.append(line)
|
||||
elif brace != -1 and brace < space:
|
||||
# Has labels: metric{existing="val"} 123
|
||||
close = line.find(b"}", brace)
|
||||
if close == -1:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
# Filter out labels that already exist in this line
|
||||
existing = line[brace + 1:close]
|
||||
inject = label_bytes
|
||||
if existing:
|
||||
for name in label_name_bytes:
|
||||
if name + b"=" in existing:
|
||||
# Rebuild inject string excluding this label
|
||||
inject = _remove_label(inject, name)
|
||||
if not inject:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
if close == brace + 1:
|
||||
# Empty braces: metric{} 123
|
||||
out.append(line[:close] + inject + line[close:])
|
||||
else:
|
||||
out.append(line[:close] + b"," + inject + line[close:])
|
||||
else:
|
||||
# No labels: metric 123
|
||||
out.append(line[:space] + b"{" + label_bytes + b"}" + line[space:])
|
||||
|
||||
return b"\n".join(out)
|
||||
|
||||
|
||||
def _remove_label(label_bytes, name):
|
||||
"""Remove a single label (name=\"...\") from a comma-separated label string."""
|
||||
parts = []
|
||||
for part in label_bytes.split(b","):
|
||||
if not part.startswith(name + b"="):
|
||||
parts.append(part)
|
||||
return b",".join(parts)
|
||||
|
||||
|
||||
def build_label_str(labels):
|
||||
"""Pre-format the label injection string: key1="val1",key2="val2" """
|
||||
if not labels:
|
||||
return ""
|
||||
return ",".join(f'{k}="{v}"' for k, v in sorted(labels.items()))
|
||||
|
||||
|
||||
def build_elapsed_gauge(labels):
|
||||
"""Build a bench_elapsed_seconds gauge from run_start_epoch in labels."""
|
||||
start = labels.get("run_start_epoch")
|
||||
if not start:
|
||||
return b""
|
||||
try:
|
||||
elapsed = time.time() - float(start)
|
||||
except (ValueError, TypeError):
|
||||
return b""
|
||||
# Build labels excluding internal keys
|
||||
display = {k: v for k, v in labels.items()
|
||||
if k not in ("run_start_epoch", "reference_epoch")}
|
||||
lstr = build_label_str(display)
|
||||
return (
|
||||
f"# HELP bench_elapsed_seconds Seconds since benchmark run started\n"
|
||||
f"# TYPE bench_elapsed_seconds gauge\n"
|
||||
f"bench_elapsed_seconds{{{lstr}}} {elapsed:.1f}\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def compute_timestamp_ms(labels):
|
||||
"""Compute a synthetic timestamp so all runs share a common time origin.
|
||||
|
||||
Returns the timestamp in milliseconds, or None if not enough info.
|
||||
Uses: reference_epoch + (now - run_start_epoch) → all runs overlay at
|
||||
the same Grafana time range.
|
||||
"""
|
||||
ref = labels.get("reference_epoch")
|
||||
start = labels.get("run_start_epoch")
|
||||
if not ref or not start:
|
||||
return None
|
||||
try:
|
||||
elapsed = time.time() - float(start)
|
||||
return int((float(ref) + elapsed) * 1000)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def inject_timestamps(metrics_bytes, timestamp_ms):
|
||||
"""Append a Prometheus timestamp (ms) to every data line.
|
||||
|
||||
Prometheus text format: metric{labels} value [timestamp_ms]
|
||||
Adding timestamps causes Prometheus to store all runs' samples
|
||||
at the same relative time, enabling natural overlay in Grafana.
|
||||
"""
|
||||
if timestamp_ms is None:
|
||||
return metrics_bytes
|
||||
|
||||
ts = str(timestamp_ms).encode("utf-8")
|
||||
out = []
|
||||
for line in metrics_bytes.split(b"\n"):
|
||||
if line.startswith(b"#") or not line:
|
||||
out.append(line)
|
||||
else:
|
||||
out.append(line + b" " + ts)
|
||||
return b"\n".join(out)
|
||||
|
||||
|
||||
class MetricsHandler(BaseHTTPRequestHandler):
|
||||
# Use HTTP/1.1 so Content-Length is respected and Prometheus
|
||||
# doesn't have to rely on connection close to detect end of body.
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self):
|
||||
src = self.client_address[0]
|
||||
try:
|
||||
resp = urlopen(self.server.upstream, timeout=2)
|
||||
metrics = resp.read()
|
||||
except (URLError, ConnectionError, OSError):
|
||||
# reth not running — return empty 200
|
||||
self._send(b"")
|
||||
#print(f" scrape from {src}: empty (reth not running)", flush=True)
|
||||
return
|
||||
|
||||
all_labels = read_labels(self.server.labels_file)
|
||||
# Internal keys — not injected as Prometheus labels
|
||||
internal = ("run_start_epoch", "reference_epoch")
|
||||
labels = {k: v for k, v in all_labels.items() if k not in internal}
|
||||
label_str = build_label_str(labels)
|
||||
label_names = sorted(labels.keys())
|
||||
|
||||
t0 = time.monotonic()
|
||||
result = inject_labels(metrics, label_str, label_names)
|
||||
result += build_elapsed_gauge(all_labels)
|
||||
ts_ms = compute_timestamp_ms(all_labels)
|
||||
result = inject_timestamps(result, ts_ms)
|
||||
dt = time.monotonic() - t0
|
||||
|
||||
self._send(result)
|
||||
print(f" scrape from {src}: {len(metrics)} -> {len(result)} bytes, "
|
||||
f"inject {dt*1000:.1f}ms", flush=True)
|
||||
|
||||
def _send(self, body):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain; version=0.0.4")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
if body:
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # suppress per-request logging
|
||||
|
||||
|
||||
def resolve_bind_address(subnet_cidr):
|
||||
"""Find the local IP address that belongs to the given subnet.
|
||||
|
||||
Uses ``ip -j addr show`` to enumerate interfaces and returns the first
|
||||
address that falls within *subnet_cidr* (e.g. ``10.10.0.0/24``).
|
||||
"""
|
||||
network = ipaddress.ip_network(subnet_cidr, strict=False)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-j", "addr", "show"],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
interfaces = json.loads(result.stdout)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError) as exc:
|
||||
print(f"Error: cannot enumerate interfaces: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for iface in interfaces:
|
||||
for addr_info in iface.get("addr_info", []):
|
||||
try:
|
||||
addr = ipaddress.ip_address(addr_info["local"])
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
if addr in network:
|
||||
return str(addr)
|
||||
|
||||
print(f"Error: no interface address found in subnet {subnet_cidr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Prometheus metrics proxy with label injection")
|
||||
parser.add_argument("--labels", default="/tmp/bench-metrics-labels.json",
|
||||
help="Path to JSON file with labels to inject (default: /tmp/bench-metrics-labels.json)")
|
||||
parser.add_argument("--upstream", default="http://127.0.0.1:9100/",
|
||||
help="Upstream reth metrics URL (default: http://127.0.0.1:9100/)")
|
||||
|
||||
bind_group = parser.add_mutually_exclusive_group()
|
||||
bind_group.add_argument("--bind", default=None,
|
||||
help="Address to bind the proxy (default: 0.0.0.0)")
|
||||
bind_group.add_argument("--subnet", default=None,
|
||||
help="Auto-detect bind address from a local interface in this subnet (e.g. 10.10.0.0/24)")
|
||||
|
||||
parser.add_argument("--port", type=int, default=9090,
|
||||
help="Port to bind the proxy (default: 9090)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.subnet:
|
||||
bind_addr = resolve_bind_address(args.subnet)
|
||||
elif args.bind:
|
||||
bind_addr = args.bind
|
||||
else:
|
||||
bind_addr = "0.0.0.0"
|
||||
|
||||
server = HTTPServer((bind_addr, args.port), MetricsHandler)
|
||||
server.upstream = args.upstream
|
||||
server.labels_file = args.labels
|
||||
|
||||
print(f"bench-metrics-proxy listening on {bind_addr}:{args.port}")
|
||||
print(f" upstream: {args.upstream}")
|
||||
print(f" labels: {args.labels}")
|
||||
sys.stdout.flush()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
76
.github/scripts/bench-reth-build.sh
vendored
76
.github/scripts/bench-reth-build.sh
vendored
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds reth binaries for benchmarking from local source only.
|
||||
#
|
||||
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit>
|
||||
#
|
||||
# baseline — build the baseline binary at <commit> (merge-base)
|
||||
# source-dir must be checked out at <commit>
|
||||
# feature — build the candidate binary + reth-bench at <commit>
|
||||
# source-dir must be checked out at <commit>
|
||||
#
|
||||
# Outputs:
|
||||
# baseline: <source-dir>/target/profiling/reth (or reth-bb if BENCH_BIG_BLOCKS=true)
|
||||
# feature: <source-dir>/target/profiling/reth (or reth-bb), reth-bench installed to cargo bin
|
||||
#
|
||||
# Optional env: BENCH_BIG_BLOCKS (true/false) — build reth-bb instead of reth
|
||||
set -euxo pipefail
|
||||
|
||||
MODE="$1"
|
||||
SOURCE_DIR="$2"
|
||||
COMMIT="$3"
|
||||
|
||||
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
|
||||
# The node binary to build: reth-bb for big blocks, reth otherwise
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
NODE_BIN="reth-bb"
|
||||
NODE_PKG="-p reth-bb"
|
||||
else
|
||||
NODE_BIN="reth"
|
||||
NODE_PKG="--bin reth"
|
||||
fi
|
||||
|
||||
# Tracy support: when BENCH_TRACY is "on" or "full", add Tracy cargo features
|
||||
# and frame pointers for accurate stack traces.
|
||||
EXTRA_FEATURES=""
|
||||
EXTRA_RUSTFLAGS=""
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
EXTRA_FEATURES="tracy,tracy-client/ondemand"
|
||||
EXTRA_RUSTFLAGS=" -C force-frame-pointers=yes"
|
||||
fi
|
||||
|
||||
# Build the requested node binary with the benchmark profile.
|
||||
build_node_binary() {
|
||||
local features_arg=""
|
||||
local workspace_arg=""
|
||||
|
||||
cd "$SOURCE_DIR"
|
||||
if [ -n "$EXTRA_FEATURES" ]; then
|
||||
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
|
||||
features_arg="--features ${EXTRA_FEATURES}"
|
||||
workspace_arg="--workspace"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --locked --profile profiling $NODE_PKG $workspace_arg $features_arg
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
baseline|main)
|
||||
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
|
||||
build_node_binary
|
||||
;;
|
||||
|
||||
feature|branch)
|
||||
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
|
||||
rustup show active-toolchain || rustup default stable
|
||||
build_node_binary
|
||||
make -C "$SOURCE_DIR" install-reth-bench
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 <baseline|feature> <source-dir> <commit>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
260
.github/scripts/bench-reth-charts.py
vendored
260
.github/scripts/bench-reth-charts.py
vendored
@@ -1,260 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate benchmark charts from reth-bench CSV output.
|
||||
|
||||
Usage:
|
||||
bench-engine-charts.py <combined_csv> --output-dir <dir> [--baseline <baseline_csv>]
|
||||
|
||||
Generates three PNG charts:
|
||||
1. newPayload latency + Ggas/s per block (+ latency diff when baseline present)
|
||||
2. Wait breakdown (persistence, execution cache, sparse trie) per block
|
||||
3. Scatter plot of gas used vs latency
|
||||
|
||||
When --baseline is provided, charts overlay both datasets for comparison.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
except ImportError:
|
||||
print("matplotlib is required: pip install matplotlib", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
GIGAGAS = 1_000_000_000
|
||||
|
||||
|
||||
def parse_combined_csv(path: str) -> list[dict]:
|
||||
rows = []
|
||||
with open(path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(
|
||||
{
|
||||
"block_number": int(row["block_number"]),
|
||||
"gas_used": int(row["gas_used"]),
|
||||
"new_payload_latency_us": int(row["new_payload_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)),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def plot_latency_and_throughput(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
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)
|
||||
ax1, ax2 = axes[0], axes[1]
|
||||
|
||||
feat_x = [r["block_number"] for r in feature]
|
||||
feat_lat = [r["new_payload_latency_us"] / 1_000 for r in feature]
|
||||
feat_ggas = []
|
||||
for r in feature:
|
||||
lat_s = r["new_payload_latency_us"] / 1_000_000
|
||||
feat_ggas.append(r["gas_used"] / lat_s / GIGAGAS if lat_s > 0 else 0)
|
||||
|
||||
if baseline:
|
||||
base_x = [r["block_number"] for r in baseline]
|
||||
base_lat = [r["new_payload_latency_us"] / 1_000 for r in baseline]
|
||||
base_ggas = []
|
||||
for r in baseline:
|
||||
lat_s = r["new_payload_latency_us"] / 1_000_000
|
||||
base_ggas.append(r["gas_used"] / lat_s / GIGAGAS if lat_s > 0 else 0)
|
||||
l, = ax1.plot(base_x, base_lat, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
ax1.axhline(np.median(base_lat), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
|
||||
l, = ax2.plot(base_x, base_ggas, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
ax2.axhline(np.median(base_ggas), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
|
||||
|
||||
l, = ax1.plot(feat_x, feat_lat, linewidth=0.8, label=feature_name)
|
||||
ax1.axhline(np.median(feat_lat), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
|
||||
ax1.set_ylabel("Latency (ms)")
|
||||
ax1.set_title("newPayload Latency per Block")
|
||||
ax1.grid(True, alpha=0.3)
|
||||
ax1.legend()
|
||||
|
||||
l, = ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=feature_name)
|
||||
ax2.axhline(np.median(feat_ggas), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
|
||||
ax2.set_ylabel("Ggas/s")
|
||||
ax2.set_title("Execution Throughput per Block")
|
||||
ax2.grid(True, alpha=0.3)
|
||||
ax2.legend()
|
||||
|
||||
if baseline:
|
||||
ax3 = axes[2]
|
||||
base_by_block = {r["block_number"]: r["new_payload_latency_us"] for r in baseline}
|
||||
blocks, diffs = [], []
|
||||
for r in feature:
|
||||
bn = r["block_number"]
|
||||
if bn in base_by_block and base_by_block[bn] > 0:
|
||||
pct = (r["new_payload_latency_us"] - base_by_block[bn]) / base_by_block[bn] * 100
|
||||
blocks.append(bn)
|
||||
diffs.append(pct)
|
||||
if blocks:
|
||||
colors = ["green" if d <= 0 else "red" for d in diffs]
|
||||
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 (feature vs baseline)")
|
||||
ax3.grid(True, alpha=0.3, axis="y")
|
||||
|
||||
axes[-1].set_xlabel("Block Number")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=150)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_wait_breakdown(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
baseline_name: str = "baseline", feature_name: str = "feature",
|
||||
):
|
||||
series = [
|
||||
("Persistence Wait", "persistence_wait_us"),
|
||||
("State Cache Wait", "execution_cache_wait_us"),
|
||||
("Trie Cache Wait", "sparse_trie_wait_us"),
|
||||
]
|
||||
|
||||
fig, axes = plt.subplots(len(series), 1, figsize=(12, 3 * len(series)), sharex=True)
|
||||
for ax, (label, key) in zip(axes, series):
|
||||
if baseline:
|
||||
bx = [r["block_number"] for r in baseline if r[key] is not None]
|
||||
by = [r[key] / 1_000 for r in baseline if r[key] is not None]
|
||||
if bx:
|
||||
ax.plot(bx, by, linewidth=0.8, label=baseline_name, alpha=0.7)
|
||||
|
||||
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=feature_name)
|
||||
|
||||
ax.set_ylabel("ms")
|
||||
ax.set_title(label)
|
||||
ax.grid(True, alpha=0.3)
|
||||
if baseline:
|
||||
ax.legend()
|
||||
|
||||
axes[-1].set_xlabel("Block Number")
|
||||
fig.suptitle("Wait Time Breakdown per Block", fontsize=14, y=1.01)
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def _add_regression(ax, x, y, color, label):
|
||||
"""Add a linear regression line to the axes."""
|
||||
if len(x) < 2:
|
||||
return
|
||||
xa, ya = np.array(x), np.array(y)
|
||||
m, b = np.polyfit(xa, ya, 1)
|
||||
x_range = np.linspace(xa.min(), xa.max(), 100)
|
||||
ax.plot(x_range, m * x_range + b, color=color, linewidth=1.5, alpha=0.8,
|
||||
label=label)
|
||||
|
||||
|
||||
def plot_gas_vs_latency(
|
||||
feature: list[dict], baseline: list[dict] | None, out: Path,
|
||||
baseline_name: str = "baseline", feature_name: str = "feature",
|
||||
):
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
|
||||
if baseline:
|
||||
bgas = [r["gas_used"] / 1_000_000 for r in baseline]
|
||||
blat = [r["new_payload_latency_us"] / 1_000 for r in baseline]
|
||||
ax.scatter(bgas, blat, s=8, alpha=0.5)
|
||||
_add_regression(ax, bgas, blat, "tab:blue", baseline_name)
|
||||
|
||||
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", feature_name)
|
||||
|
||||
ax.set_xlabel("Gas Used (Mgas)")
|
||||
ax.set_ylabel("newPayload Latency (ms)")
|
||||
ax.set_title("Gas Used vs Latency")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=150)
|
||||
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(
|
||||
"--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", nargs="+", help="Path(s) to baseline combined_latency.csv"
|
||||
)
|
||||
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 = merge_csvs(args.feature)
|
||||
if not feature:
|
||||
print("No results found in feature CSV(s)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
baseline = None
|
||||
if args.baseline:
|
||||
baseline = merge_csvs(args.baseline)
|
||||
if not baseline:
|
||||
print(
|
||||
"Warning: no results in baseline CSV(s), skipping comparison",
|
||||
file=sys.stderr,
|
||||
)
|
||||
baseline = None
|
||||
|
||||
out_dir = Path(args.output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bname = args.baseline_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)
|
||||
|
||||
print(f"Charts written to {out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
567
.github/scripts/bench-reth-local.sh
vendored
567
.github/scripts/bench-reth-local.sh
vendored
@@ -1,567 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# local-reth-bench.sh — Run the reth Engine API benchmark locally.
|
||||
#
|
||||
# Replicates the CI bench.yml workflow (build, local snapshot validation, system tuning,
|
||||
# interleaved B-F-F-B execution, summary, charts) without any GitHub
|
||||
# Actions glue (no PR comments, no artifact upload, no Slack).
|
||||
#
|
||||
# Usage:
|
||||
# local-reth-bench.sh <baseline-ref> <feature-ref> [options]
|
||||
#
|
||||
# Options:
|
||||
# --blocks N Number of blocks to benchmark (default: 500)
|
||||
# --warmup N Number of warmup blocks (default: 100)
|
||||
# --cores N Limit reth to N CPU cores, 0 = all available (default: 0)
|
||||
# --samply Enable samply profiling
|
||||
# --tracy MODE Tracy profiling: off, on, full (default: off)
|
||||
# --tracy-filter F Tracy tracing filter (default: debug)
|
||||
# --no-tune Skip system tuning (useful on dev machines / macOS)
|
||||
#
|
||||
# Requires: the reth repo at RETH_REPO (default: ~/reth)
|
||||
#
|
||||
# Dependencies (install before first run):
|
||||
# schelk, cpupower, taskset, stdbuf, python3, curl,
|
||||
# make, uv, jq, Rust toolchain (cargo/rustup)
|
||||
# Optional:
|
||||
# mc for Tracy profile upload
|
||||
#
|
||||
# The script delegates to the existing bench-reth-*.sh scripts in the reth
|
||||
# repo for the actual build, snapshot, and run steps.
|
||||
set -euxo pipefail
|
||||
|
||||
# ── PATH ──────────────────────────────────────────────────────────────
|
||||
# Ensure cargo and user-local bins (uv) are visible
|
||||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
RETH_REPO="${RETH_REPO:-$HOME/reth}"
|
||||
BLOCKS=500
|
||||
WARMUP=100
|
||||
CORES=0
|
||||
SAMPLY=false
|
||||
TRACY="off"
|
||||
TRACY_FILTER="debug"
|
||||
TUNE=true
|
||||
BASELINE_REF=""
|
||||
FEATURE_REF=""
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <baseline-ref> <feature-ref> [options]
|
||||
|
||||
Options:
|
||||
--blocks N Number of blocks to benchmark (default: 500)
|
||||
--warmup N Number of warmup blocks (default: 100)
|
||||
--cores N Limit reth to N CPU cores (default: 0 = all)
|
||||
--samply Enable samply profiling
|
||||
--tracy MODE Tracy profiling: off, on, full (default: off)
|
||||
on = tracing only (lower overhead)
|
||||
full = tracing + CPU sampling (higher overhead)
|
||||
--tracy-filter F Tracy tracing filter (default: debug)
|
||||
--no-tune Skip system tuning
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--blocks) BLOCKS="$2"; shift 2 ;;
|
||||
--warmup) WARMUP="$2"; shift 2 ;;
|
||||
--cores) CORES="$2"; shift 2 ;;
|
||||
--samply) SAMPLY=true; shift ;;
|
||||
--tracy) TRACY="$2"; shift 2 ;;
|
||||
--tracy-filter) TRACY_FILTER="$2"; shift 2 ;;
|
||||
--no-tune) TUNE=false; shift ;;
|
||||
--help|-h) usage ;;
|
||||
-*) echo "Unknown option: $1"; usage ;;
|
||||
*)
|
||||
if [ -z "$BASELINE_REF" ]; then
|
||||
BASELINE_REF="$1"
|
||||
elif [ -z "$FEATURE_REF" ]; then
|
||||
FEATURE_REF="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1"; usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$BASELINE_REF" ] || [ -z "$FEATURE_REF" ]; then
|
||||
echo "Error: both <baseline-ref> and <feature-ref> are required."
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate --tracy value
|
||||
case "$TRACY" in
|
||||
off|on|full) ;;
|
||||
*) echo "Error: --tracy must be off, on, or full (got: $TRACY)"; usage ;;
|
||||
esac
|
||||
|
||||
# Samply + tracy=full are mutually exclusive (both use perf sampling)
|
||||
if [ "$SAMPLY" = "true" ] && [ "$TRACY" = "full" ]; then
|
||||
echo "Warning: samply and tracy=full both use perf sampling; downgrading tracy to 'on'."
|
||||
TRACY="on"
|
||||
fi
|
||||
|
||||
# ── Check dependencies ───────────────────────────────────────────────
|
||||
missing=()
|
||||
for cmd in schelk cpupower taskset stdbuf python3 curl make uv jq cargo; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "Error: missing required tools: ${missing[*]}"
|
||||
echo "See the CI 'Install dependencies' step in .github/workflows/bench.yml for install instructions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
if ! command -v tracy-capture &>/dev/null; then
|
||||
echo "Error: tracy-capture is required for --tracy $TRACY"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure tools that run via sudo are in a sudo-visible path.
|
||||
# The bench scripts use `sudo schelk` / `sudo samply` but cargo installs
|
||||
# them to ~/.cargo/bin which sudo's secure_path doesn't include.
|
||||
for cmd in schelk samply; do
|
||||
if command -v "$cmd" &>/dev/null && ! sudo sh -c "command -v $cmd" &>/dev/null; then
|
||||
echo "Installing $cmd to /usr/local/bin (needed for sudo)..."
|
||||
sudo install "$(command -v "$cmd")" /usr/local/bin/
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d "$RETH_REPO/.git" ]; then
|
||||
echo "Error: RETH_REPO=$RETH_REPO is not a git repository."
|
||||
echo "Set RETH_REPO or clone reth to ~/reth"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Resolve paths ────────────────────────────────────────────────────
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPTS_DIR="${RETH_REPO}/.github/scripts"
|
||||
BENCH_WORK_DIR="${RETH_REPO}/../bench-work-$(date +%Y%m%d-%H%M%S)"
|
||||
BASELINE_SRC="${RETH_REPO}/../reth-baseline"
|
||||
FEATURE_SRC="${RETH_REPO}/../reth-feature"
|
||||
|
||||
mkdir -p "$BENCH_WORK_DIR"
|
||||
BENCH_WORK_DIR="$(cd "$BENCH_WORK_DIR" && pwd)"
|
||||
|
||||
# ── Global cleanup trap (restores system tuning on any exit) ─────────
|
||||
TUNING_APPLIED=false
|
||||
CSTATE_PID=
|
||||
METRICS_PROXY_PID=
|
||||
cleanup_global() {
|
||||
[ -n "$METRICS_PROXY_PID" ] && kill "$METRICS_PROXY_PID" 2>/dev/null || true
|
||||
if [ "$TUNING_APPLIED" = true ]; then
|
||||
echo
|
||||
echo "▸ Restoring system settings..."
|
||||
[ -n "$CSTATE_PID" ] && kill "$CSTATE_PID" 2>/dev/null || true
|
||||
sudo systemctl start irqbalance cron atd 2>/dev/null || true
|
||||
echo " System settings restored."
|
||||
fi
|
||||
}
|
||||
trap cleanup_global EXIT
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " reth local benchmark"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Baseline ref : $BASELINE_REF"
|
||||
echo " Feature ref : $FEATURE_REF"
|
||||
echo " Blocks : $BLOCKS"
|
||||
echo " Warmup : $WARMUP"
|
||||
echo " Cores : $CORES"
|
||||
echo " Samply : $SAMPLY"
|
||||
echo " Tracy : $TRACY"
|
||||
echo " Tracy filter : $TRACY_FILTER"
|
||||
echo " System tune : $TUNE"
|
||||
echo " Work dir : $BENCH_WORK_DIR"
|
||||
echo " Reth repo : $RETH_REPO"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo
|
||||
|
||||
# Enable sccache if available (matches CI's RUSTC_WRAPPER=sccache)
|
||||
if command -v sccache &>/dev/null; then
|
||||
export RUSTC_WRAPPER="sccache"
|
||||
fi
|
||||
|
||||
# Export env vars expected by the bench-reth-*.sh scripts
|
||||
export BENCH_BLOCKS="$BLOCKS"
|
||||
export BENCH_WARMUP_BLOCKS="$WARMUP"
|
||||
export BENCH_CORES="$CORES"
|
||||
export BENCH_SAMPLY="$SAMPLY"
|
||||
export BENCH_TRACY="$TRACY"
|
||||
export BENCH_TRACY_FILTER="$TRACY_FILTER"
|
||||
export BENCH_WORK_DIR
|
||||
export SCHELK_MOUNT="${SCHELK_MOUNT:-/reth-bench}"
|
||||
export BENCH_RPC_URL="${BENCH_RPC_URL:-https://ethereum.reth.rs/rpc}"
|
||||
export BENCH_METRICS_ADDR="127.0.0.1:9100"
|
||||
|
||||
# ── Step 1: Resolve refs to full SHAs ────────────────────────────────
|
||||
echo "▸ Resolving git refs..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
resolve_ref() {
|
||||
local ref="$1"
|
||||
git fetch origin "$ref" --quiet 2>/dev/null || true
|
||||
git rev-parse "$ref" 2>/dev/null \
|
||||
|| git rev-parse "origin/$ref" 2>/dev/null \
|
||||
|| { echo "Error: cannot resolve ref '$ref'"; exit 1; }
|
||||
}
|
||||
|
||||
BASELINE_SHA="$(resolve_ref "$BASELINE_REF")"
|
||||
FEATURE_SHA="$(resolve_ref "$FEATURE_REF")"
|
||||
echo " Baseline SHA : $BASELINE_SHA"
|
||||
echo " Feature SHA : $FEATURE_SHA"
|
||||
echo
|
||||
|
||||
# ── Step 2: Prepare source directories ───────────────────────────────
|
||||
echo "▸ Preparing source directories..."
|
||||
|
||||
prepare_source() {
|
||||
local src_dir="$1" ref="$2"
|
||||
if [ -d "$src_dir" ]; then
|
||||
git -C "$src_dir" fetch origin "$ref" 2>/dev/null || true
|
||||
else
|
||||
git clone --recurse-submodules "$RETH_REPO" "$src_dir"
|
||||
fi
|
||||
git -C "$src_dir" checkout "$ref" --force
|
||||
git -C "$src_dir" submodule update --init --recursive
|
||||
}
|
||||
|
||||
prepare_source "$BASELINE_SRC" "$BASELINE_SHA"
|
||||
prepare_source "$FEATURE_SRC" "$FEATURE_SHA"
|
||||
BASELINE_SRC="$(cd "$BASELINE_SRC" && pwd)"
|
||||
FEATURE_SRC="$(cd "$FEATURE_SRC" && pwd)"
|
||||
echo " Baseline src : $BASELINE_SRC"
|
||||
echo " Feature src : $FEATURE_SRC"
|
||||
echo
|
||||
|
||||
# ── Step 3: Validate local snapshot ──────────────────────────────────
|
||||
echo "▸ Validating local snapshot..."
|
||||
cd "$RETH_REPO"
|
||||
"${SCRIPTS_DIR}/bench-reth-snapshot.sh"
|
||||
echo " Snapshot is ready."
|
||||
echo
|
||||
|
||||
# ── Step 4: Build binaries in parallel ───────────────────────────────
|
||||
echo "▸ Building binaries (parallel)..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
FAIL=0
|
||||
|
||||
"${SCRIPTS_DIR}/bench-reth-build.sh" baseline "$BASELINE_SRC" "$BASELINE_SHA" &
|
||||
PID_BASELINE=$!
|
||||
|
||||
"${SCRIPTS_DIR}/bench-reth-build.sh" feature "$FEATURE_SRC" "$FEATURE_SHA" &
|
||||
PID_FEATURE=$!
|
||||
|
||||
wait $PID_BASELINE || FAIL=1
|
||||
wait $PID_FEATURE || FAIL=1
|
||||
|
||||
if [ $FAIL -ne 0 ]; then
|
||||
echo "Error: one or more build tasks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo " Binaries built successfully."
|
||||
echo
|
||||
|
||||
# ── Step 5: System tuning (optional) ────────────────────────────────
|
||||
if [ "$TUNE" = "true" ]; then
|
||||
echo "▸ Applying system tuning..."
|
||||
|
||||
sudo cpupower frequency-set -g performance 2>/dev/null || true
|
||||
|
||||
# Disable turbo boost (Intel + AMD)
|
||||
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null || true
|
||||
echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost 2>/dev/null || true
|
||||
|
||||
sudo swapoff -a 2>/dev/null || true
|
||||
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space 2>/dev/null || true
|
||||
|
||||
# Disable SMT (hyperthreading)
|
||||
for cpu in /sys/devices/system/cpu/cpu*/topology/thread_siblings_list; do
|
||||
[ -f "$cpu" ] || continue
|
||||
first=$(cut -d, -f1 < "$cpu" | cut -d- -f1)
|
||||
current=$(echo "$cpu" | grep -o 'cpu[0-9]*' | grep -o '[0-9]*')
|
||||
if [ "$current" != "$first" ]; then
|
||||
echo 0 | sudo tee "/sys/devices/system/cpu/cpu${current}/online" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
echo " Online CPUs: $(nproc)"
|
||||
|
||||
# Disable transparent huge pages
|
||||
for p in /sys/kernel/mm/transparent_hugepage /sys/kernel/mm/transparent_hugepages; do
|
||||
if [ -d "$p" ]; then
|
||||
echo never | sudo tee "$p/enabled" 2>/dev/null || true
|
||||
echo never | sudo tee "$p/defrag" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Prevent deep C-states
|
||||
sudo sh -c 'exec 3<>/dev/cpu_dma_latency; echo -ne "\x00\x00\x00\x00" >&3; sleep infinity' &
|
||||
CSTATE_PID=$!
|
||||
|
||||
# Pin IRQs to core 0
|
||||
for irq in /proc/irq/*/smp_affinity_list; do
|
||||
echo 0 | sudo tee "$irq" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Stop noisy background services
|
||||
sudo systemctl stop irqbalance cron atd unattended-upgrades snapd 2>/dev/null || true
|
||||
|
||||
TUNING_APPLIED=true
|
||||
|
||||
# Log environment for reproducibility (matches CI)
|
||||
echo " === Benchmark environment ==="
|
||||
echo " Kernel : $(uname -r)"
|
||||
lscpu | grep -E 'Model name|CPU\(s\)|MHz|NUMA' | sed 's/^/ /'
|
||||
echo " Governor : $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo unknown)"
|
||||
echo " Freq : $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null || echo unknown)"
|
||||
echo " THP : $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || cat /sys/kernel/mm/transparent_hugepages/enabled 2>/dev/null || echo unknown)"
|
||||
free -h | sed 's/^/ /'
|
||||
echo " System tuning applied."
|
||||
echo
|
||||
fi
|
||||
|
||||
# ── Step 5b: Tracefs mount (tracy=full only) ─────────────────────────
|
||||
if [ "$TRACY" = "full" ] && [ "$(uname)" = "Linux" ]; then
|
||||
echo "▸ Mounting tracefs for Tracy full mode..."
|
||||
sudo mount -t tracefs tracefs /sys/kernel/tracing -o mode=755 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Tracy upload & viewer helpers ────────────────────────────────────
|
||||
TRACY_VIEWER_BASE="${TRACY_VIEWER_BASE:-}"
|
||||
|
||||
tracy_viewer_url() {
|
||||
local profile_url="$1"
|
||||
if [ -z "$TRACY_VIEWER_BASE" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
local encoded
|
||||
encoded=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$profile_url")
|
||||
echo "${TRACY_VIEWER_BASE}?profile_url=${encoded}"
|
||||
}
|
||||
|
||||
upload_tracy() {
|
||||
local label="$1" output_dir="$2" sha="$3"
|
||||
local tracy_file="$output_dir/tracy-profile.tracy"
|
||||
|
||||
if [ ! -f "$tracy_file" ]; then
|
||||
echo " Tracy: no profile found, skipping upload."
|
||||
return
|
||||
fi
|
||||
|
||||
local timestamp short_sha remote_name bucket mc_alias
|
||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
short_sha="${sha:0:7}"
|
||||
remote_name="${label}-${short_sha}-${timestamp}.tracy"
|
||||
bucket="${TRACY_BUCKET:-tracy-profiles}"
|
||||
mc_alias="${MC_ALIAS:-minio}"
|
||||
local minio_base="${TRACY_MINIO_URL:-http://minio.minio.svc.cluster.local:9000}"
|
||||
|
||||
echo " Tracy: uploading profile..."
|
||||
if mc cp "$tracy_file" "${mc_alias}/${bucket}/${remote_name}"; then
|
||||
local url="${minio_base}/${bucket}/${remote_name}"
|
||||
echo "$url" > "$output_dir/tracy_url.txt"
|
||||
local viewer
|
||||
viewer=$(tracy_viewer_url "$url")
|
||||
if [ -n "$viewer" ]; then
|
||||
echo "$viewer" > "$output_dir/tracy_viewer_url.txt"
|
||||
echo " Tracy: uploaded → $viewer"
|
||||
else
|
||||
echo " Tracy: uploaded → $url"
|
||||
fi
|
||||
else
|
||||
echo " Tracy: upload failed (non-fatal)."
|
||||
fi
|
||||
|
||||
# Delete large profile to free disk
|
||||
rm -f "$tracy_file"
|
||||
}
|
||||
|
||||
# ── Step 6: Pre-flight cleanup ───────────────────────────────────────
|
||||
echo "▸ Pre-flight cleanup..."
|
||||
pkill -f bench-metrics-proxy 2>/dev/null || true
|
||||
sudo systemctl stop "${RETH_SCOPE:-reth-bench.scope}" 2>/dev/null || true
|
||||
sudo systemctl reset-failed "${RETH_SCOPE:-reth-bench.scope}" 2>/dev/null || true
|
||||
sudo schelk recover -y --kill || sudo schelk full-recover -y || true
|
||||
echo
|
||||
|
||||
# ── Step 7: Interleaved benchmark runs (B-F-F-B) ────────────────────
|
||||
# This ordering reduces systematic bias from thermal drift and cache warming.
|
||||
BASELINE_BIN="${BASELINE_SRC}/target/profiling/reth"
|
||||
FEATURE_BIN="${FEATURE_SRC}/target/profiling/reth"
|
||||
|
||||
# Start metrics proxy (reth → label injection → Prometheus)
|
||||
LABELS_FILE="/tmp/bench-metrics-labels.json"
|
||||
echo '{}' > "$LABELS_FILE"
|
||||
METRICS_SUBNET="${METRICS_SUBNET:-10.10.0.0/24}"
|
||||
METRICS_PORT="${METRICS_PORT:-9090}"
|
||||
python3 "${SELF_DIR}/bench-metrics-proxy.py" \
|
||||
--labels "$LABELS_FILE" \
|
||||
--upstream "http://${BENCH_METRICS_ADDR}/" \
|
||||
--subnet "$METRICS_SUBNET" \
|
||||
--port "$METRICS_PORT" &
|
||||
METRICS_PROXY_PID=$!
|
||||
echo "▸ Metrics proxy started (PID $METRICS_PROXY_PID) on subnet ${METRICS_SUBNET}, port ${METRICS_PORT}"
|
||||
|
||||
# Unique benchmark ID: local-<timestamp> for local runs, ci-<run_id> for CI
|
||||
BENCH_ID="local-$(basename "$BENCH_WORK_DIR" | sed 's/bench-work-//')"
|
||||
# Reference epoch: shared time origin so all runs overlay in Grafana.
|
||||
# The proxy maps each run's elapsed time onto this common origin.
|
||||
BENCH_REFERENCE_EPOCH=$(date +%s)
|
||||
|
||||
write_labels() {
|
||||
local run_label="$1" run_type="$2" ref="$3" sha="$4"
|
||||
LAST_RUN_START=$(date +%s)
|
||||
cat > "$LABELS_FILE" <<-EOF
|
||||
{"benchmark_run":"${run_label}","run_type":"${run_type}","git_ref":"${ref}","bench_sha":"${sha}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
EOF
|
||||
}
|
||||
|
||||
run_bench() {
|
||||
local label="$1" binary="$2" output_dir="$3"
|
||||
echo "▸ Running benchmark: ${label}..."
|
||||
cd "$RETH_REPO"
|
||||
if command -v taskset &>/dev/null; then
|
||||
taskset -c 0 "${SCRIPTS_DIR}/bench-reth-run.sh" "$label" "$binary" "$output_dir"
|
||||
else
|
||||
"${SCRIPTS_DIR}/bench-reth-run.sh" "$label" "$binary" "$output_dir"
|
||||
fi
|
||||
echo " ✓ ${label} complete."
|
||||
echo
|
||||
}
|
||||
|
||||
write_labels "baseline-1" "baseline" "$BASELINE_REF" "$BASELINE_SHA"
|
||||
run_bench "baseline-1" "$BASELINE_BIN" "$BENCH_WORK_DIR/baseline-1"
|
||||
|
||||
write_labels "feature-1" "feature" "$FEATURE_REF" "$FEATURE_SHA"
|
||||
run_bench "feature-1" "$FEATURE_BIN" "$BENCH_WORK_DIR/feature-1"
|
||||
|
||||
write_labels "feature-2" "feature" "$FEATURE_REF" "$FEATURE_SHA"
|
||||
run_bench "feature-2" "$FEATURE_BIN" "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
write_labels "baseline-2" "baseline" "$BASELINE_REF" "$BASELINE_SHA"
|
||||
run_bench "baseline-2" "$BASELINE_BIN" "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
# ── Compute Grafana URL ──────────────────────────────────────────────
|
||||
GRAFANA_BASE_URL="https://tempoxyz.grafana.net/d/reth-bench-ghr/reth-bench-ghr"
|
||||
GRAFANA_DATASOURCE="ef57fux92e9z4e"
|
||||
LAST_RUN_DURATION=$(( $(date +%s) - LAST_RUN_START ))
|
||||
FROM_MS=$(( BENCH_REFERENCE_EPOCH * 1000 ))
|
||||
TO_MS=$(( (BENCH_REFERENCE_EPOCH + LAST_RUN_DURATION) * 1000 ))
|
||||
GRAFANA_URL="${GRAFANA_BASE_URL}?orgId=1&from=${FROM_MS}&to=${TO_MS}&timezone=browser&var-datasource=${GRAFANA_DATASOURCE}&var-job=reth-bench&var-benchmark_id=${BENCH_ID}&var-benchmark_run=\$__all"
|
||||
|
||||
# ── Step 8: Scan logs for errors ─────────────────────────────────────
|
||||
echo "▸ Scanning logs for errors..."
|
||||
ERRORS_FILE="$BENCH_WORK_DIR/errors.md"
|
||||
found_errors=false
|
||||
for run_dir in baseline-1 feature-1 feature-2 baseline-2; do
|
||||
LOG="$BENCH_WORK_DIR/$run_dir/node.log"
|
||||
[ -f "$LOG" ] || continue
|
||||
panics=$(grep -c -E 'panicked at' "$LOG" 2>/dev/null || true)
|
||||
errors=$(grep -c ' ERROR ' "$LOG" 2>/dev/null || true)
|
||||
if [ "$panics" -gt 0 ] || [ "$errors" -gt 0 ]; then
|
||||
if [ "$found_errors" = false ]; then
|
||||
printf '### ⚠️ Node Errors\n\n' >> "$ERRORS_FILE"
|
||||
found_errors=true
|
||||
fi
|
||||
printf '<details><summary><b>%s</b>: %d panic(s), %d error(s)</summary>\n\n' \
|
||||
"$run_dir" "$panics" "$errors" >> "$ERRORS_FILE"
|
||||
if [ "$panics" -gt 0 ]; then
|
||||
printf '**Panics:**\n```\n' >> "$ERRORS_FILE"
|
||||
grep -E 'panicked at' "$LOG" | head -10 >> "$ERRORS_FILE"
|
||||
printf '```\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
printf '**Errors (first 20):**\n```\n' >> "$ERRORS_FILE"
|
||||
grep ' ERROR ' "$LOG" | head -20 >> "$ERRORS_FILE"
|
||||
printf '```\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
printf '\n</details>\n\n' >> "$ERRORS_FILE"
|
||||
fi
|
||||
done
|
||||
if [ "$found_errors" = true ]; then
|
||||
echo " ⚠ Errors found — see $ERRORS_FILE"
|
||||
else
|
||||
echo " No errors found."
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 9: Parse results ───────────────────────────────────────────
|
||||
echo "▸ Parsing results..."
|
||||
cd "$RETH_REPO"
|
||||
|
||||
SUMMARY_ARGS=(
|
||||
--output-summary "$BENCH_WORK_DIR/summary.json"
|
||||
--output-markdown "$BENCH_WORK_DIR/comment.md"
|
||||
--repo "paradigmxyz/reth"
|
||||
--baseline-ref "$BASELINE_SHA"
|
||||
--baseline-name "$BASELINE_REF"
|
||||
--feature-name "$FEATURE_REF"
|
||||
--feature-ref "$FEATURE_SHA"
|
||||
--baseline-csv "$BENCH_WORK_DIR/baseline-1/combined_latency.csv" "$BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
--feature-csv "$BENCH_WORK_DIR/feature-1/combined_latency.csv" "$BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
--gas-csv "$BENCH_WORK_DIR/feature-1/total_gas.csv"
|
||||
--grafana-url "$GRAFANA_URL"
|
||||
)
|
||||
|
||||
python3 "${SCRIPTS_DIR}/bench-reth-summary.py" "${SUMMARY_ARGS[@]}"
|
||||
echo
|
||||
|
||||
# ── Step 10: Generate charts ─────────────────────────────────────────
|
||||
echo "▸ Generating charts..."
|
||||
CHART_ARGS=(
|
||||
--output-dir "$BENCH_WORK_DIR/charts"
|
||||
--feature "$BENCH_WORK_DIR/feature-1/combined_latency.csv" "$BENCH_WORK_DIR/feature-2/combined_latency.csv"
|
||||
--baseline "$BENCH_WORK_DIR/baseline-1/combined_latency.csv" "$BENCH_WORK_DIR/baseline-2/combined_latency.csv"
|
||||
--baseline-name "$BASELINE_REF"
|
||||
--feature-name "$FEATURE_REF"
|
||||
)
|
||||
|
||||
if python3 -c "import matplotlib" 2>/dev/null; then
|
||||
python3 "${SCRIPTS_DIR}/bench-reth-charts.py" "${CHART_ARGS[@]}"
|
||||
elif command -v uv &>/dev/null; then
|
||||
uv run --with matplotlib python3 "${SCRIPTS_DIR}/bench-reth-charts.py" "${CHART_ARGS[@]}"
|
||||
else
|
||||
echo " Warning: matplotlib not available, skipping chart generation."
|
||||
fi
|
||||
echo
|
||||
|
||||
# ── Step 11: Upload Tracy profiles ────────────────────────────────────
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
echo "▸ Uploading Tracy profiles..."
|
||||
upload_tracy "baseline-1" "$BENCH_WORK_DIR/baseline-1" "$BASELINE_SHA"
|
||||
upload_tracy "feature-1" "$BENCH_WORK_DIR/feature-1" "$FEATURE_SHA"
|
||||
upload_tracy "feature-2" "$BENCH_WORK_DIR/feature-2" "$FEATURE_SHA"
|
||||
upload_tracy "baseline-2" "$BENCH_WORK_DIR/baseline-2" "$BASELINE_SHA"
|
||||
echo
|
||||
fi
|
||||
|
||||
# ── Done (system restore happens via EXIT trap) ─────────────────────
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Benchmark complete!"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Results : $BENCH_WORK_DIR/summary.json"
|
||||
echo " Markdown : $BENCH_WORK_DIR/comment.md"
|
||||
echo " Charts : $BENCH_WORK_DIR/charts/"
|
||||
if [ -f "$ERRORS_FILE" ]; then
|
||||
echo " Errors : $ERRORS_FILE"
|
||||
fi
|
||||
echo " Grafana : $GRAFANA_URL"
|
||||
if [ "$TRACY" != "off" ]; then
|
||||
echo " ─── Tracy Profiles ───"
|
||||
for run_dir in baseline-1 feature-1 feature-2 baseline-2; do
|
||||
url_file="$BENCH_WORK_DIR/$run_dir/tracy_viewer_url.txt"
|
||||
if [ -f "$url_file" ]; then
|
||||
echo " $run_dir : $(cat "$url_file")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
352
.github/scripts/bench-reth-run.sh
vendored
352
.github/scripts/bench-reth-run.sh
vendored
@@ -1,352 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Runs a single reth-bench cycle: mount snapshot → start node → warmup →
|
||||
# benchmark → stop node → recover snapshot.
|
||||
#
|
||||
# Usage: bench-reth-run.sh <label> <binary> <output-dir>
|
||||
#
|
||||
# Required env: SCHELK_MOUNT, BENCH_RPC_URL, BENCH_BLOCKS, BENCH_WARMUP_BLOCKS
|
||||
# Optional env: BENCH_BIG_BLOCKS (true/false), BENCH_WORK_DIR (for big blocks path)
|
||||
# BENCH_BAL (false/true/feature/baseline; only used with big blocks)
|
||||
# BENCH_WAIT_TIME (duration like 500ms, default empty)
|
||||
# BENCH_BASELINE_ARGS (extra reth node args for baseline runs)
|
||||
# BENCH_FEATURE_ARGS (extra reth node args for feature runs)
|
||||
# BENCH_OTLP_TRACES_ENDPOINT (OTLP HTTP endpoint for traces, e.g. https://host/insert/opentelemetry/v1/traces)
|
||||
# BENCH_OTLP_LOGS_ENDPOINT (OTLP HTTP endpoint for logs, e.g. https://host/insert/opentelemetry/v1/logs)
|
||||
# BENCH_OTLP_DISABLED (true to skip OTLP export even if endpoints are set)
|
||||
set -euxo pipefail
|
||||
|
||||
LABEL="$1"
|
||||
BINARY="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
DATADIR_NAME="datadir"
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
LOG="${OUTPUT_DIR}/node.log"
|
||||
|
||||
RETH_SCOPE="${RETH_SCOPE:-reth-bench.scope}"
|
||||
|
||||
cleanup() {
|
||||
kill "$TAIL_PID" 2>/dev/null || true
|
||||
# Stop tracy-capture first (SIGINT makes it disconnect and flush to disk)
|
||||
# Must happen before killing reth, otherwise reth keeps streaming data.
|
||||
if [ -n "${TRACY_PID:-}" ] && kill -0 "$TRACY_PID" 2>/dev/null; then
|
||||
echo "Stopping tracy-capture..."
|
||||
kill -INT "$TRACY_PID" 2>/dev/null || true
|
||||
for i in $(seq 1 30); do
|
||||
kill -0 "$TRACY_PID" 2>/dev/null || break
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo "Waiting for tracy-capture to finish writing... (${i}s)"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if kill -0 "$TRACY_PID" 2>/dev/null; then
|
||||
echo "tracy-capture still running after 30s, killing..."
|
||||
kill -9 "$TRACY_PID" 2>/dev/null || true
|
||||
fi
|
||||
wait "$TRACY_PID" 2>/dev/null || true
|
||||
fi
|
||||
if sudo systemctl is-active "$RETH_SCOPE" >/dev/null 2>&1; then
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
# Send SIGINT to the inner reth process by exact name (not -f which
|
||||
# would also match samply's cmdline containing "reth"). Samply will
|
||||
# capture reth's exit and save the profile.
|
||||
sudo pkill -INT -x reth 2>/dev/null || true
|
||||
# Wait for samply to finish writing the profile and exit
|
||||
for i in $(seq 1 120); do
|
||||
sudo pgrep -x samply > /dev/null 2>&1 || break
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo "Waiting for samply to finish writing profile... (${i}s)"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if sudo pgrep -x samply > /dev/null 2>&1; then
|
||||
echo "Samply still running after 120s, sending SIGTERM..."
|
||||
sudo pkill -x samply 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
# Stop the entire systemd scope — kills all processes in the cgroup.
|
||||
# This is reliable regardless of process reparenting or PID wrapper issues.
|
||||
sudo systemctl stop "$RETH_SCOPE" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
sudo systemctl reset-failed "$RETH_SCOPE" 2>/dev/null || true
|
||||
# Fix ownership of reth-created files (reth runs as root)
|
||||
sudo chown -R "$(id -un):$(id -gn)" "$OUTPUT_DIR" 2>/dev/null || true
|
||||
# Let schelk recover the mounted volume in place so dm-era can restore only
|
||||
# the changed blocks and clean up its own state.
|
||||
sudo schelk recover -y --kill || true
|
||||
}
|
||||
TAIL_PID=
|
||||
TRACY_PID=
|
||||
trap cleanup EXIT
|
||||
|
||||
# Clean up stale state from a previous cancelled run.
|
||||
# Stop any leftover reth process in the scope, then recover schelk state.
|
||||
sudo systemctl stop "$RETH_SCOPE" 2>/dev/null || true
|
||||
sudo systemctl reset-failed "$RETH_SCOPE" 2>/dev/null || true
|
||||
sudo schelk recover -y --kill || sudo schelk full-recover -y || true
|
||||
|
||||
# Mount
|
||||
sudo schelk mount -y || true
|
||||
if [ ! -d "$DATADIR/db" ] || [ ! -d "$DATADIR/static_files" ]; then
|
||||
echo "::error::Failed to mount benchmark datadir at ${DATADIR}"
|
||||
ls -la "$SCHELK_MOUNT" || true
|
||||
ls -la "$DATADIR" || true
|
||||
exit 1
|
||||
fi
|
||||
sync
|
||||
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
|
||||
echo "=== Cache state after drop ==="
|
||||
free -h
|
||||
grep Cached /proc/meminfo
|
||||
|
||||
# Start reth
|
||||
# CPU layout: core 0 = OS/IRQs/reth-bench/aux, cores 1+ = reth node
|
||||
RETH_BENCH="$(which reth-bench)"
|
||||
ONLINE=$(nproc --all)
|
||||
MAX_RETH=$(( ONLINE - 1 ))
|
||||
if [ "${BENCH_CORES:-0}" -gt 0 ] && [ "$BENCH_CORES" -lt "$MAX_RETH" ]; then
|
||||
MAX_RETH=$BENCH_CORES
|
||||
fi
|
||||
RETH_CPUS="1-${MAX_RETH}"
|
||||
|
||||
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
|
||||
|
||||
RETH_ARGS=(
|
||||
node
|
||||
--datadir "$DATADIR"
|
||||
--log.file.directory "$OUTPUT_DIR/reth-logs"
|
||||
--engine.accept-execution-requests-hash
|
||||
--http
|
||||
--http.port 8545
|
||||
--ws
|
||||
--ws.api all
|
||||
--authrpc.port 8551
|
||||
--disable-discovery
|
||||
--no-persist-peers
|
||||
)
|
||||
|
||||
# Gate flag on binary support (older baselines may not have it).
|
||||
# Uses --help which exits immediately via clap without node init.
|
||||
SYNC_STATE_IDLE=false
|
||||
if "$BINARY" node --help 2>/dev/null | grep -qF -- '--debug.startup-sync-state-idle'; then
|
||||
RETH_ARGS+=(--debug.startup-sync-state-idle)
|
||||
SYNC_STATE_IDLE=true
|
||||
fi
|
||||
|
||||
# Append per-label extra node args (baseline or feature)
|
||||
EXTRA_NODE_ARGS=""
|
||||
case "$LABEL" in
|
||||
baseline*) EXTRA_NODE_ARGS="${BENCH_BASELINE_ARGS:-}" ;;
|
||||
feature*) EXTRA_NODE_ARGS="${BENCH_FEATURE_ARGS:-}" ;;
|
||||
esac
|
||||
if [ -n "$EXTRA_NODE_ARGS" ]; then
|
||||
# Word-split the string into individual args
|
||||
# shellcheck disable=SC2206
|
||||
RETH_ARGS+=($EXTRA_NODE_ARGS)
|
||||
fi
|
||||
|
||||
if [ -n "${BENCH_METRICS_ADDR:-}" ]; then
|
||||
RETH_ARGS+=(--metrics "$BENCH_METRICS_ADDR")
|
||||
fi
|
||||
|
||||
# OTLP traces and logs export
|
||||
if [ "${BENCH_OTLP_DISABLED:-false}" != "true" ]; then
|
||||
if [ -n "${BENCH_OTLP_TRACES_ENDPOINT:-}" ]; then
|
||||
RETH_ARGS+=(--tracing-otlp="${BENCH_OTLP_TRACES_ENDPOINT}" --tracing-otlp.service-name=reth-bench)
|
||||
fi
|
||||
if [ -n "${BENCH_OTLP_LOGS_ENDPOINT:-}" ]; then
|
||||
RETH_ARGS+=(--logs-otlp="${BENCH_OTLP_LOGS_ENDPOINT}" --logs-otlp.filter=debug)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Tracy profiling: add --log.tracy flags and set environment
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
RETH_ARGS+=(--log.tracy --log.tracy.filter "${BENCH_TRACY_FILTER:-debug}")
|
||||
if [ "${BENCH_TRACY}" = "on" ]; then
|
||||
export TRACY_NO_SYS_TRACE=1
|
||||
elif [ "${BENCH_TRACY}" = "full" ]; then
|
||||
export TRACY_SAMPLING_HZ="${BENCH_TRACY_SAMPLING_HZ:-1}"
|
||||
fi
|
||||
fi
|
||||
|
||||
SUDO_ENV=()
|
||||
if [ -n "${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then
|
||||
SUDO_ENV+=("OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES}")
|
||||
SUDO_ENV+=("OTEL_BSP_MAX_QUEUE_SIZE=65536" "OTEL_BLRP_MAX_QUEUE_SIZE=65536")
|
||||
fi
|
||||
|
||||
# Limit reth memory to 95% of available RAM to prevent OOM kills
|
||||
TOTAL_MEM_KB=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
|
||||
MEM_LIMIT=$(( TOTAL_MEM_KB * 95 / 100 * 1024 ))
|
||||
echo "Memory limit: $(( MEM_LIMIT / 1024 / 1024 ))MB (95% of $(( TOTAL_MEM_KB / 1024 ))MB)"
|
||||
|
||||
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
|
||||
RETH_ARGS+=(--log.samply)
|
||||
SAMPLY="$(which samply)"
|
||||
sudo systemd-run --quiet --scope --collect --unit="$RETH_SCOPE" \
|
||||
-p MemoryMax="$MEM_LIMIT" -p AllowedCPUs="$RETH_CPUS" \
|
||||
env "${SUDO_ENV[@]}" nice -n -20 \
|
||||
"$SAMPLY" record --save-only --presymbolicate --rate 10000 \
|
||||
--output "$OUTPUT_DIR/samply-profile.json.gz" \
|
||||
-- "$BINARY" "${RETH_ARGS[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
else
|
||||
sudo systemd-run --quiet --scope --collect --unit="$RETH_SCOPE" \
|
||||
-p MemoryMax="$MEM_LIMIT" -p AllowedCPUs="$RETH_CPUS" \
|
||||
env "${SUDO_ENV[@]}" nice -n -20 "$BINARY" "${RETH_ARGS[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
fi
|
||||
stdbuf -oL tail -f "$LOG" | sed -u "s/^/[reth] /" &
|
||||
TAIL_PID=$!
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://127.0.0.1:8545 -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
|
||||
> /dev/null 2>&1; then
|
||||
echo "reth (${LABEL}) RPC is up after ${i}s"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
echo "::error::reth (${LABEL}) failed to start within 60s"
|
||||
cat "$LOG"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait for the pipeline to finish (eth_syncing returns false) so the
|
||||
# engine is in live mode and can accept newPayload calls.
|
||||
# Only possible when --debug.startup-sync-state-idle is supported.
|
||||
if [ "$SYNC_STATE_IDLE" = "true" ]; then
|
||||
for i in $(seq 1 300); do
|
||||
SYNC_RESULT=$(curl -sf http://127.0.0.1:8545 -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' 2>/dev/null || true)
|
||||
if [ -n "$SYNC_RESULT" ] && jq -e '.result == false' <<< "$SYNC_RESULT" > /dev/null 2>&1; then
|
||||
echo "reth (${LABEL}) pipeline finished after ${i}s, engine is live"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 300 ]; then
|
||||
echo "::error::reth (${LABEL}) pipeline did not finish within 300s"
|
||||
cat "$LOG"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "reth (${LABEL}) binary does not support --debug.startup-sync-state-idle, skipping sync wait"
|
||||
fi
|
||||
|
||||
# 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)"
|
||||
|
||||
# Build optional flags
|
||||
EXTRA_BENCH_ARGS=(--reth-new-payload)
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
fi
|
||||
|
||||
if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
# Big blocks mode: replay pre-generated payloads
|
||||
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
|
||||
BENCH_BAL_MODE="${BENCH_BAL:-false}"
|
||||
|
||||
BB_BENCH_ARGS=(--reth-new-payload)
|
||||
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
|
||||
BB_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
|
||||
fi
|
||||
case "$BENCH_BAL_MODE" in
|
||||
false)
|
||||
;;
|
||||
true)
|
||||
BB_BENCH_ARGS+=(--bal)
|
||||
;;
|
||||
baseline)
|
||||
if [[ "$LABEL" == baseline* ]]; then
|
||||
BB_BENCH_ARGS+=(--bal)
|
||||
fi
|
||||
;;
|
||||
feature)
|
||||
if [[ "$LABEL" == feature* ]]; then
|
||||
BB_BENCH_ARGS+=(--bal)
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown BENCH_BAL value: $BENCH_BAL_MODE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Warmup
|
||||
WARMUP="${BENCH_WARMUP_BLOCKS:-50}"
|
||||
if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
|
||||
echo "Running big blocks warmup (${WARMUP} payloads)..."
|
||||
$BENCH_NICE "$RETH_BENCH" replay-payloads \
|
||||
"${BB_BENCH_ARGS[@]}" \
|
||||
--count "$WARMUP" \
|
||||
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" 2>&1 | sed -u "s/^/[bench] /"
|
||||
fi
|
||||
|
||||
# Start tracy-capture after warmup so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
echo "Starting tracy-capture..."
|
||||
tracy-capture -f -o "$OUTPUT_DIR/tracy-profile.tracy" &
|
||||
TRACY_PID=$!
|
||||
sleep 0.5 # give tracy-capture time to connect
|
||||
fi
|
||||
|
||||
# Benchmark — skip warmup payloads so they aren't measured
|
||||
BB_SKIP=0
|
||||
if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
|
||||
BB_SKIP="$WARMUP"
|
||||
fi
|
||||
if [ "${BENCH_BLOCKS:-0}" -gt 0 ] 2>/dev/null; then
|
||||
BB_BENCH_ARGS+=(--count "$BENCH_BLOCKS")
|
||||
fi
|
||||
|
||||
echo "Running big blocks benchmark (replay-payloads, skip=${BB_SKIP})..."
|
||||
$BENCH_NICE "$RETH_BENCH" replay-payloads \
|
||||
"${BB_BENCH_ARGS[@]}" \
|
||||
--skip "$BB_SKIP" \
|
||||
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
else
|
||||
# Standard mode: warmup + new-payload-fcu
|
||||
# Warmup
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "${BENCH_WARMUP_BLOCKS:-50}" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
|
||||
|
||||
# Start tracy-capture after warmup so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
echo "Starting tracy-capture..."
|
||||
tracy-capture -f -o "$OUTPUT_DIR/tracy-profile.tracy" &
|
||||
TRACY_PID=$!
|
||||
sleep 0.5 # give tracy-capture time to connect
|
||||
fi
|
||||
|
||||
# Benchmark
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "$BENCH_BLOCKS" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" \
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
fi
|
||||
|
||||
# cleanup runs via trap
|
||||
56
.github/scripts/bench-reth-snapshot.sh
vendored
56
.github/scripts/bench-reth-snapshot.sh
vendored
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Validates that the benchmark snapshot has already been populated into the
|
||||
# local schelk volume.
|
||||
#
|
||||
# Usage: bench-reth-snapshot.sh [--check]
|
||||
# --check Exit 0 if the local snapshot is ready, 10 if it is missing.
|
||||
#
|
||||
# Required env:
|
||||
# SCHELK_MOUNT – schelk mount point (e.g. /reth-bench)
|
||||
# Optional env:
|
||||
# BENCH_BIG_BLOCKS – true when validating the big-blocks snapshot datadir
|
||||
# BENCH_SNAPSHOT_NAME – expected snapshot label for log/error output
|
||||
set -euxo pipefail
|
||||
|
||||
: "${SCHELK_MOUNT:?SCHELK_MOUNT must be set}"
|
||||
|
||||
DATADIR_NAME="datadir"
|
||||
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
DATADIR_NAME="datadir-big-blocks"
|
||||
fi
|
||||
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
|
||||
|
||||
describe_snapshot() {
|
||||
if [ -n "${BENCH_SNAPSHOT_NAME:-}" ]; then
|
||||
printf '%s' "${BENCH_SNAPSHOT_NAME}"
|
||||
elif [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
|
||||
printf '%s' 'big-block weekly snapshot'
|
||||
else
|
||||
printf '%s' 'benchmark snapshot'
|
||||
fi
|
||||
}
|
||||
|
||||
snapshot_ready() {
|
||||
[ -d "$DATADIR/db" ] && [ -d "$DATADIR/static_files" ]
|
||||
}
|
||||
|
||||
EXPECTED_SNAPSHOT="$(describe_snapshot)"
|
||||
|
||||
sudo schelk recover -y --kill || sudo schelk full-recover -y || true
|
||||
sudo schelk mount -y || true
|
||||
|
||||
if snapshot_ready; then
|
||||
echo "Found local ${EXPECTED_SNAPSHOT} at ${DATADIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "::error::Missing local ${EXPECTED_SNAPSHOT} at ${DATADIR}. Benchmarks no longer download snapshots; pre-populate the local schelk data first."
|
||||
ls -la "$SCHELK_MOUNT" || true
|
||||
ls -la "$DATADIR" || true
|
||||
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
exit 10
|
||||
fi
|
||||
|
||||
exit 1
|
||||
646
.github/scripts/bench-reth-summary.py
vendored
646
.github/scripts/bench-reth-summary.py
vendored
@@ -1,646 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse reth-bench CSV output and generate a summary JSON + markdown comparison.
|
||||
|
||||
Usage:
|
||||
bench-reth-summary.py <combined_csv> <gas_csv> \
|
||||
--output-summary <summary.json> \
|
||||
--output-markdown <comment.md> \
|
||||
--baseline-csv <baseline_combined.csv> \
|
||||
[--repo <owner/repo>] \
|
||||
[--baseline-ref <sha>] \
|
||||
[--feature-name <name>] \
|
||||
[--feature-sha <sha>]
|
||||
|
||||
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 feature CSV is missing or empty.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
|
||||
GIGAGAS = 1_000_000_000
|
||||
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 = []
|
||||
with open(path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(
|
||||
{
|
||||
"block_number": int(row["block_number"]),
|
||||
"gas_used": int(row["gas_used"]),
|
||||
"gas_limit": int(row["gas_limit"]),
|
||||
"transaction_count": int(row["transaction_count"]),
|
||||
"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": _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
|
||||
|
||||
|
||||
def parse_gas_csv(path: str) -> list[dict]:
|
||||
"""Parse total_gas.csv into a list of per-block dicts."""
|
||||
rows = []
|
||||
with open(path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(
|
||||
{
|
||||
"block_number": int(row["block_number"]),
|
||||
"gas_used": int(row["gas_used"]),
|
||||
"time_us": int(row["time"]),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def stddev(values: list[float], mean: float) -> float:
|
||||
if len(values) < 2:
|
||||
return 0.0
|
||||
return math.sqrt(sum((v - mean) ** 2 for v in values) / (len(values) - 1))
|
||||
|
||||
|
||||
def percentile(sorted_vals: list[float], pct: int) -> float:
|
||||
if not sorted_vals:
|
||||
return 0.0
|
||||
idx = int(len(sorted_vals) * pct / 100)
|
||||
idx = min(idx, len(sorted_vals) - 1)
|
||||
return sorted_vals[idx]
|
||||
|
||||
|
||||
def compute_stats(combined: list[dict]) -> dict:
|
||||
"""Compute per-run statistics from parsed CSV data."""
|
||||
n = len(combined)
|
||||
if n == 0:
|
||||
return {}
|
||||
|
||||
latencies_ms = [r["new_payload_latency_us"] / 1_000 for r in combined]
|
||||
sorted_lat = sorted(latencies_ms)
|
||||
mean_lat = sum(latencies_ms) / n
|
||||
std_lat = stddev(latencies_ms, mean_lat)
|
||||
|
||||
mgas_s_values = []
|
||||
for r in combined:
|
||||
lat_s = r["new_payload_latency_us"] / 1_000_000
|
||||
if lat_s > 0:
|
||||
mgas_s_values.append(r["gas_used"] / lat_s / 1_000_000)
|
||||
mean_mgas_s = sum(mgas_s_values) / len(mgas_s_values) if mgas_s_values else 0
|
||||
|
||||
total_latencies_ms = [r["total_latency_us"] / 1_000 for r in combined]
|
||||
wall_clock_s = sum(total_latencies_ms) / 1_000
|
||||
mean_total_lat_ms = sum(total_latencies_ms) / n
|
||||
|
||||
# Persistence wait mean (for main table)
|
||||
persist_values_ms = []
|
||||
for r in combined:
|
||||
v = r.get("persistence_wait_us")
|
||||
if v is not None:
|
||||
persist_values_ms.append(v / 1_000)
|
||||
mean_persist_ms = sum(persist_values_ms) / len(persist_values_ms) if persist_values_ms else 0.0
|
||||
|
||||
return {
|
||||
"n": n,
|
||||
"mean_ms": mean_lat,
|
||||
"stddev_ms": std_lat,
|
||||
"p50_ms": percentile(sorted_lat, 50),
|
||||
"p90_ms": percentile(sorted_lat, 90),
|
||||
"p99_ms": percentile(sorted_lat, 99),
|
||||
"mean_mgas_s": mean_mgas_s,
|
||||
"wall_clock_s": wall_clock_s,
|
||||
"mean_total_lat_ms": mean_total_lat_ms,
|
||||
"mean_persist_ms": mean_persist_ms,
|
||||
}
|
||||
|
||||
|
||||
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], feature: list[dict]
|
||||
) -> tuple[list[tuple[float, float]], list[float], list[float], list[float], list[float]]:
|
||||
"""Match blocks and return paired latencies and per-block diffs.
|
||||
|
||||
Returns:
|
||||
pairs: list of (baseline_ms, feature_ms) tuples
|
||||
lat_diffs_ms: list of feature − baseline latency diffs in ms
|
||||
mgas_diffs: list of feature − baseline Mgas/s diffs
|
||||
total_lat_diffs_ms: list of feature − baseline total latency diffs in ms
|
||||
persist_diffs_ms: list of feature − baseline persistence wait diffs in ms
|
||||
"""
|
||||
baseline_by_block = {r["block_number"]: r for r in baseline}
|
||||
feature_by_block = {r["block_number"]: r for r in feature}
|
||||
common_blocks = sorted(set(baseline_by_block) & set(feature_by_block))
|
||||
|
||||
pairs = []
|
||||
lat_diffs_ms = []
|
||||
mgas_diffs = []
|
||||
total_lat_diffs_ms = []
|
||||
persist_diffs_ms = []
|
||||
for bn in common_blocks:
|
||||
b = baseline_by_block[bn]
|
||||
f = feature_by_block[bn]
|
||||
b_ms = b["new_payload_latency_us"] / 1_000
|
||||
f_ms = f["new_payload_latency_us"] / 1_000
|
||||
pairs.append((b_ms, f_ms))
|
||||
lat_diffs_ms.append(f_ms - b_ms)
|
||||
b_lat_s = b["new_payload_latency_us"] / 1_000_000
|
||||
f_lat_s = f["new_payload_latency_us"] / 1_000_000
|
||||
if b_lat_s > 0 and f_lat_s > 0:
|
||||
mgas_diffs.append(
|
||||
f["gas_used"] / f_lat_s / 1_000_000
|
||||
- b["gas_used"] / b_lat_s / 1_000_000
|
||||
)
|
||||
total_lat_diffs_ms.append(
|
||||
f["total_latency_us"] / 1_000 - b["total_latency_us"] / 1_000
|
||||
)
|
||||
b_persist = (b.get("persistence_wait_us") or 0) / 1_000
|
||||
f_persist = (f.get("persistence_wait_us") or 0) / 1_000
|
||||
persist_diffs_ms.append(f_persist - b_persist)
|
||||
return pairs, lat_diffs_ms, mgas_diffs, total_lat_diffs_ms, persist_diffs_ms
|
||||
|
||||
|
||||
def compute_paired_stats(
|
||||
baseline_runs: list[list[dict]],
|
||||
feature_runs: list[list[dict]],
|
||||
) -> dict:
|
||||
"""Compute paired statistics between baseline and feature runs.
|
||||
|
||||
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 = []
|
||||
all_total_lat_diffs = []
|
||||
all_persist_diffs = []
|
||||
blocks_per_pair = []
|
||||
for baseline, feature in zip(baseline_runs, feature_runs):
|
||||
pairs, lat_diffs, mgas_diffs, total_lat_diffs, persist_diffs = _paired_data(baseline, feature)
|
||||
all_pairs.extend(pairs)
|
||||
all_lat_diffs.extend(lat_diffs)
|
||||
all_mgas_diffs.extend(mgas_diffs)
|
||||
all_total_lat_diffs.extend(total_lat_diffs)
|
||||
all_persist_diffs.extend(persist_diffs)
|
||||
blocks_per_pair.append(len(pairs))
|
||||
|
||||
if not all_lat_diffs:
|
||||
return {}
|
||||
|
||||
n = len(all_lat_diffs)
|
||||
mean_diff = sum(all_lat_diffs) / n
|
||||
std_diff = stddev(all_lat_diffs, mean_diff)
|
||||
se = std_diff / math.sqrt(n) if n > 0 else 0.0
|
||||
ci = T_CRITICAL * se
|
||||
|
||||
# Bootstrap CI on difference-of-percentiles (resample paired blocks)
|
||||
base_lats = sorted([p[0] for p in all_pairs])
|
||||
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 = [], [], []
|
||||
for _ in range(BOOTSTRAP_ITERATIONS):
|
||||
sample = rng.choices(all_pairs, k=n)
|
||||
b_sorted = sorted(p[0] for p in sample)
|
||||
f_sorted = sorted(p[1] for p in sample)
|
||||
p50_boot.append(percentile(f_sorted, 50) - percentile(b_sorted, 50))
|
||||
p90_boot.append(percentile(f_sorted, 90) - percentile(b_sorted, 90))
|
||||
p99_boot.append(percentile(f_sorted, 99) - percentile(b_sorted, 99))
|
||||
p50_boot.sort()
|
||||
p90_boot.sort()
|
||||
p99_boot.sort()
|
||||
lo = int(BOOTSTRAP_ITERATIONS * 0.025)
|
||||
hi = int(BOOTSTRAP_ITERATIONS * 0.975)
|
||||
|
||||
mean_mgas_diff = sum(all_mgas_diffs) / len(all_mgas_diffs) if all_mgas_diffs else 0.0
|
||||
std_mgas_diff = stddev(all_mgas_diffs, mean_mgas_diff) if len(all_mgas_diffs) > 1 else 0.0
|
||||
mgas_se = std_mgas_diff / math.sqrt(len(all_mgas_diffs)) if all_mgas_diffs else 0.0
|
||||
mgas_ci = T_CRITICAL * mgas_se
|
||||
|
||||
mean_total_diff = sum(all_total_lat_diffs) / len(all_total_lat_diffs) if all_total_lat_diffs else 0.0
|
||||
std_total_diff = stddev(all_total_lat_diffs, mean_total_diff) if len(all_total_lat_diffs) > 1 else 0.0
|
||||
total_se = std_total_diff / math.sqrt(len(all_total_lat_diffs)) if all_total_lat_diffs else 0.0
|
||||
wall_clock_ci_ms = T_CRITICAL * total_se
|
||||
|
||||
mean_persist_diff = sum(all_persist_diffs) / len(all_persist_diffs) if all_persist_diffs else 0.0
|
||||
std_persist_diff = stddev(all_persist_diffs, mean_persist_diff) if len(all_persist_diffs) > 1 else 0.0
|
||||
persist_se = std_persist_diff / math.sqrt(len(all_persist_diffs)) if all_persist_diffs else 0.0
|
||||
persist_ci_ms = T_CRITICAL * persist_se
|
||||
|
||||
return {
|
||||
"n": n,
|
||||
"mean_diff_ms": mean_diff,
|
||||
"ci_ms": ci,
|
||||
"p50_diff_ms": p50_diff,
|
||||
"p50_ci_ms": (p50_boot[hi] - p50_boot[lo]) / 2,
|
||||
"p90_diff_ms": p90_diff,
|
||||
"p90_ci_ms": (p90_boot[hi] - p90_boot[lo]) / 2,
|
||||
"p99_diff_ms": p99_diff,
|
||||
"p99_ci_ms": (p99_boot[hi] - p99_boot[lo]) / 2,
|
||||
"mean_mgas_diff": mean_mgas_diff,
|
||||
"mgas_ci": mgas_ci,
|
||||
"wall_clock_ci_ms": wall_clock_ci_ms,
|
||||
"persist_ci_ms": persist_ci_ms,
|
||||
"blocks": max(blocks_per_pair),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
if seconds >= 60:
|
||||
return f"{seconds / 60:.1f}min"
|
||||
return f"{seconds}s"
|
||||
|
||||
|
||||
def format_gas(gas: int) -> str:
|
||||
if gas >= GIGAGAS:
|
||||
return f"{gas / GIGAGAS:.1f}G"
|
||||
if gas >= 1_000_000:
|
||||
return f"{gas / 1_000_000:.1f}M"
|
||||
return f"{gas:,}"
|
||||
|
||||
|
||||
|
||||
def fmt_ms(v: float) -> str:
|
||||
return f"{v:.2f}ms"
|
||||
|
||||
|
||||
def fmt_mgas(v: float) -> str:
|
||||
return f"{v:.2f}"
|
||||
|
||||
|
||||
def fmt_s(v: float) -> str:
|
||||
return f"{v:.2f}s"
|
||||
|
||||
|
||||
def display_bal_mode(bal_mode: str | None) -> str | None:
|
||||
if not bal_mode or bal_mode == "false":
|
||||
return None
|
||||
if bal_mode == "both":
|
||||
return "true"
|
||||
return bal_mode
|
||||
|
||||
|
||||
def significance(pct: float, ci_pct: float, lower_is_better: bool) -> str:
|
||||
"""Return significance label: 'good', 'bad', or 'neutral'."""
|
||||
significant = abs(pct) > ci_pct
|
||||
if not significant:
|
||||
return "neutral"
|
||||
elif (pct < 0) == lower_is_better:
|
||||
return "good"
|
||||
else:
|
||||
return "bad"
|
||||
|
||||
|
||||
def change_str(pct: float, ci_pct: float, lower_is_better: bool) -> str:
|
||||
"""Format change% with paired CI significance.
|
||||
|
||||
Significant if the CI doesn't cross zero (i.e. |pct| > ci_pct).
|
||||
"""
|
||||
sig = significance(pct, ci_pct, lower_is_better)
|
||||
emoji = {"good": "✅", "bad": "❌", "neutral": "⚪"}[sig]
|
||||
return f"{pct:+.2f}% {emoji} (±{ci_pct:.2f}%)"
|
||||
|
||||
|
||||
def compute_changes(
|
||||
baseline_stats: dict, feature_stats: dict, paired_stats: dict
|
||||
) -> dict:
|
||||
"""Pre-compute change percentages and significance for each metric."""
|
||||
def pct(base: float, feat: float) -> float:
|
||||
return (feat - base) / base * 100.0 if base > 0 else 0.0
|
||||
|
||||
def ci_pct(ci_ms: float, base_ms: float) -> float:
|
||||
return ci_ms / base_ms * 100.0 if base_ms > 0 else 0.0
|
||||
|
||||
metrics = [
|
||||
("mean", "mean_ms", "ci_ms", "mean_ms", True),
|
||||
("p50", "p50_ms", "p50_ci_ms", "p50_ms", True),
|
||||
("p90", "p90_ms", "p90_ci_ms", "p90_ms", True),
|
||||
("p99", "p99_ms", "p99_ci_ms", "p99_ms", True),
|
||||
("mgas_s", "mean_mgas_s", "mgas_ci", "mean_mgas_s", False),
|
||||
("wall_clock", "wall_clock_s", "wall_clock_ci_ms", "mean_total_lat_ms", True),
|
||||
("persist_wait", "mean_persist_ms", "persist_ci_ms", "mean_persist_ms", True),
|
||||
]
|
||||
changes = {}
|
||||
for name, stat_key, ci_key, base_key, lower_is_better in metrics:
|
||||
p = pct(baseline_stats[stat_key], feature_stats[stat_key])
|
||||
c = ci_pct(paired_stats[ci_key], baseline_stats[base_key])
|
||||
changes[name] = {
|
||||
"pct": round(p, 4),
|
||||
"ci_pct": round(c, 4),
|
||||
"sig": significance(p, c, lower_is_better),
|
||||
}
|
||||
return changes
|
||||
|
||||
|
||||
def generate_comparison_table(
|
||||
run1: dict,
|
||||
run2: dict,
|
||||
paired: dict,
|
||||
repo: str,
|
||||
baseline_ref: str,
|
||||
baseline_name: str,
|
||||
feature_name: str,
|
||||
feature_sha: str,
|
||||
big_blocks: bool = False,
|
||||
warmup_blocks: str | None = None,
|
||||
wait_time: str | None = None,
|
||||
bal_mode: str | None = None,
|
||||
) -> str:
|
||||
"""Generate a markdown comparison table between baseline and feature."""
|
||||
n = paired["blocks"]
|
||||
|
||||
def pct(base: float, feat: float) -> float:
|
||||
return (feat - base) / base * 100.0 if base > 0 else 0.0
|
||||
|
||||
mean_pct = pct(run1["mean_ms"], run2["mean_ms"])
|
||||
gas_pct = pct(run1["mean_mgas_s"], run2["mean_mgas_s"])
|
||||
wall_pct = pct(run1["wall_clock_s"], run2["wall_clock_s"])
|
||||
|
||||
p50_pct = pct(run1["p50_ms"], run2["p50_ms"])
|
||||
p90_pct = pct(run1["p90_ms"], run2["p90_ms"])
|
||||
p99_pct = pct(run1["p99_ms"], run2["p99_ms"])
|
||||
|
||||
persist_pct = pct(run1["mean_persist_ms"], run2["mean_persist_ms"])
|
||||
|
||||
# Bootstrap CIs as % of baseline percentile
|
||||
p50_ci_pct = paired["p50_ci_ms"] / run1["p50_ms"] * 100.0 if run1["p50_ms"] > 0 else 0.0
|
||||
p90_ci_pct = paired["p90_ci_ms"] / run1["p90_ms"] * 100.0 if run1["p90_ms"] > 0 else 0.0
|
||||
p99_ci_pct = paired["p99_ci_ms"] / run1["p99_ms"] * 100.0 if run1["p99_ms"] > 0 else 0.0
|
||||
|
||||
# CI as a percentage of baseline mean
|
||||
lat_ci_pct = paired["ci_ms"] / run1["mean_ms"] * 100.0 if run1["mean_ms"] > 0 else 0.0
|
||||
mgas_ci_pct = paired["mgas_ci"] / run1["mean_mgas_s"] * 100.0 if run1["mean_mgas_s"] > 0 else 0.0
|
||||
wall_ci_pct = paired["wall_clock_ci_ms"] / run1["mean_total_lat_ms"] * 100.0 if run1["mean_total_lat_ms"] > 0 else 0.0
|
||||
persist_ci_pct = paired["persist_ci_ms"] / run1["mean_persist_ms"] * 100.0 if run1["mean_persist_ms"] > 0 else 0.0
|
||||
|
||||
base_url = f"https://github.com/{repo}/commit"
|
||||
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
|
||||
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
|
||||
|
||||
lines = [
|
||||
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'])} | |",
|
||||
f"| P50 | {fmt_ms(run1['p50_ms'])} | {fmt_ms(run2['p50_ms'])} | {change_str(p50_pct, p50_ci_pct, lower_is_better=True)} |",
|
||||
f"| P90 | {fmt_ms(run1['p90_ms'])} | {fmt_ms(run2['p90_ms'])} | {change_str(p90_pct, p90_ci_pct, lower_is_better=True)} |",
|
||||
f"| P99 | {fmt_ms(run1['p99_ms'])} | {fmt_ms(run2['p99_ms'])} | {change_str(p99_pct, p99_ci_pct, lower_is_better=True)} |",
|
||||
f"| Mgas/s | {fmt_mgas(run1['mean_mgas_s'])} | {fmt_mgas(run2['mean_mgas_s'])} | {change_str(gas_pct, mgas_ci_pct, lower_is_better=False)} |",
|
||||
f"| Wall Clock | {fmt_s(run1['wall_clock_s'])} | {fmt_s(run2['wall_clock_s'])} | {change_str(wall_pct, wall_ci_pct, lower_is_better=True)} |",
|
||||
f"| Persist Wait | {fmt_ms(run1['mean_persist_ms'])} | {fmt_ms(run2['mean_persist_ms'])} | {change_str(persist_pct, persist_ci_pct, lower_is_better=True)} |",
|
||||
"",
|
||||
]
|
||||
meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}"]
|
||||
if warmup_blocks:
|
||||
meta_parts.append(f"{warmup_blocks} warmup")
|
||||
if wait_time:
|
||||
meta_parts.append(f"wait time: {wait_time}")
|
||||
display_mode = display_bal_mode(bal_mode)
|
||||
if big_blocks and display_mode:
|
||||
meta_parts.append(f"BAL: {display_mode}")
|
||||
lines.append(f"*{', '.join(meta_parts)}*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
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,
|
||||
wait_time_tables: list[str] | None = None,
|
||||
behind_baseline: int = 0, repo: str = "", baseline_ref: str = "", baseline_name: str = "",
|
||||
grafana_url: str | None = None,
|
||||
) -> str:
|
||||
"""Generate a markdown comment body."""
|
||||
lines = ["## Benchmark Results", ""]
|
||||
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(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>")
|
||||
if grafana_url:
|
||||
lines.append("")
|
||||
lines.append(f"**[Grafana Dashboard]({grafana_url})**")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Parse reth-bench ABBA results")
|
||||
parser.add_argument(
|
||||
"--baseline-csv", nargs="+", required=True,
|
||||
help="Baseline combined_latency.csv files (A1, A2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--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(
|
||||
"--output-summary", required=True, help="Output JSON summary path"
|
||||
)
|
||||
parser.add_argument("--output-markdown", required=True, help="Output markdown path")
|
||||
parser.add_argument(
|
||||
"--repo", default="paradigmxyz/reth", help="GitHub repo (owner/name)"
|
||||
)
|
||||
parser.add_argument("--baseline-ref", default=None, help="Baseline commit SHA")
|
||||
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")
|
||||
parser.add_argument("--big-blocks", action="store_true", default=False, help="Big blocks mode")
|
||||
parser.add_argument("--warmup-blocks", default=None, help="Number of warmup blocks")
|
||||
parser.add_argument("--wait-time", default=None, help="Wait time interval used between blocks")
|
||||
parser.add_argument("--bal-mode", default=None, help="BAL mode (true, feature, baseline)")
|
||||
parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run")
|
||||
args = parser.parse_args()
|
||||
|
||||
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 = []
|
||||
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.feature_csv:
|
||||
data = parse_combined_csv(path)
|
||||
if not data:
|
||||
print(f"No results in {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
feature_runs.append(data)
|
||||
|
||||
gas = parse_gas_csv(args.gas_csv)
|
||||
|
||||
all_baseline = [r for run in baseline_runs for r in run]
|
||||
all_feature = [r for run in feature_runs for r in run]
|
||||
|
||||
baseline_stats = compute_stats(all_baseline)
|
||||
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 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"
|
||||
bal_mode = display_bal_mode(args.bal_mode)
|
||||
|
||||
comparison_table = generate_comparison_table(
|
||||
baseline_stats,
|
||||
feature_stats,
|
||||
paired_stats,
|
||||
repo=args.repo,
|
||||
baseline_ref=baseline_ref,
|
||||
baseline_name=baseline_name,
|
||||
feature_name=feature_name,
|
||||
feature_sha=feature_sha,
|
||||
big_blocks=args.big_blocks,
|
||||
warmup_blocks=args.warmup_blocks,
|
||||
wait_time=args.wait_time,
|
||||
bal_mode=bal_mode,
|
||||
)
|
||||
print(f"Generated comparison ({paired_stats['n']} paired blocks, "
|
||||
f"mean diff {paired_stats['mean_diff_ms']:+.3f}ms ± {paired_stats['ci_ms']:.3f}ms)")
|
||||
|
||||
base_url = f"https://github.com/{args.repo}/commit"
|
||||
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
|
||||
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
|
||||
|
||||
wait_fields = [
|
||||
("persistence_wait_us", "Persistence Wait"),
|
||||
("sparse_trie_wait_us", "Trie Cache Update Wait"),
|
||||
("execution_cache_wait_us", "Execution Cache Update Wait"),
|
||||
]
|
||||
wait_time_tables = []
|
||||
wait_time_data = {}
|
||||
for field, title in wait_fields:
|
||||
b_stats = compute_wait_stats(all_baseline, field)
|
||||
f_stats = compute_wait_stats(all_feature, field)
|
||||
if b_stats and f_stats:
|
||||
wait_time_data[field] = {
|
||||
"title": title,
|
||||
"baseline": b_stats,
|
||||
"feature": f_stats,
|
||||
}
|
||||
table = generate_wait_time_table(title, b_stats, f_stats, baseline_label, feature_label)
|
||||
if table:
|
||||
wait_time_tables.append(table)
|
||||
|
||||
summary = {
|
||||
"blocks": paired_stats["blocks"],
|
||||
"big_blocks": args.big_blocks,
|
||||
"warmup_blocks": args.warmup_blocks,
|
||||
"wait_time": args.wait_time,
|
||||
"bal_mode": bal_mode,
|
||||
"baseline": {
|
||||
"name": baseline_name,
|
||||
"ref": baseline_ref,
|
||||
"stats": baseline_stats,
|
||||
},
|
||||
"feature": {
|
||||
"name": feature_name,
|
||||
"ref": feature_sha,
|
||||
"stats": feature_stats,
|
||||
},
|
||||
"paired": paired_stats,
|
||||
"changes": compute_changes(baseline_stats, feature_stats, paired_stats),
|
||||
"wait_times": wait_time_data,
|
||||
}
|
||||
with open(args.output_summary, "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
print(f"Summary written to {args.output_summary}")
|
||||
|
||||
markdown = generate_markdown(
|
||||
summary, comparison_table,
|
||||
wait_time_tables=wait_time_tables,
|
||||
behind_baseline=args.behind_baseline,
|
||||
repo=args.repo,
|
||||
baseline_ref=baseline_ref,
|
||||
baseline_name=baseline_name,
|
||||
grafana_url=args.grafana_url,
|
||||
)
|
||||
|
||||
with open(args.output_markdown, "w") as f:
|
||||
f.write(markdown)
|
||||
print(f"Markdown written to {args.output_markdown}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
336
.github/scripts/bench-scheduled-refs.sh
vendored
336
.github/scripts/bench-scheduled-refs.sh
vendored
@@ -1,336 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Resolves baseline and feature refs for scheduled benchmark runs.
|
||||
#
|
||||
# Supports three modes:
|
||||
# nightly — Queries the latest successful scheduled docker.yml run via
|
||||
# GitHub API to find the nightly Docker image commit. Compares
|
||||
# with the last successful feature ref to detect staleness.
|
||||
# hourly — Compares origin/main HEAD against the last successfully
|
||||
# benchmarked commit (falls back to HEAD~1 on first run).
|
||||
# Checks for in-progress sibling runs to avoid overlap.
|
||||
# release — Compares the latest GitHub release tag against the current
|
||||
# nightly Docker build. Baseline is the release tag commit,
|
||||
# feature is the nightly commit.
|
||||
#
|
||||
# Usage: bench-scheduled-refs.sh <force> <mode>
|
||||
# force — "true" to run even if no new commit (bypass skip logic)
|
||||
# mode — "nightly", "hourly", or "release"
|
||||
#
|
||||
# Outputs (via GITHUB_OUTPUT):
|
||||
# baseline-ref — commit SHA for baseline
|
||||
# feature-ref — commit SHA for feature
|
||||
# should-skip — "true" if no new commit since last run or sibling in progress
|
||||
# is-stale — "true" if latest nightly build is >24h old (nightly only)
|
||||
# stale-age-hours — age of the nightly build in hours (nightly only)
|
||||
# nightly-created — ISO timestamp of the nightly build (nightly only)
|
||||
# release-tag — release tag name (release mode only, e.g. "v2.0.0")
|
||||
#
|
||||
# Reads:
|
||||
# state/nightly-last-feature-ref (nightly, from decofe/reth-bench-charts repo)
|
||||
# state/hourly-last-feature-ref (hourly, from decofe/reth-bench-charts repo)
|
||||
# state/release-last-feature-ref (release, from decofe/reth-bench-charts repo)
|
||||
#
|
||||
# Requires: gh (GitHub CLI), jq, date, git (hourly mode), curl, DEREK_TOKEN env
|
||||
set -euxo pipefail
|
||||
|
||||
FORCE="${1:-false}"
|
||||
MODE="${2:-nightly}"
|
||||
REPO="${GITHUB_REPOSITORY:-paradigmxyz/reth}"
|
||||
|
||||
echo "Mode: $MODE, Force: $FORCE"
|
||||
|
||||
# ==========================================================================
|
||||
# Hourly mode: compare origin/main HEAD vs HEAD~1
|
||||
# ==========================================================================
|
||||
if [ "$MODE" = "hourly" ]; then
|
||||
|
||||
# --- Step 1: Resolve feature ref from git ---
|
||||
echo "::group::Resolving hourly refs from git"
|
||||
git fetch origin main --depth=2 --quiet
|
||||
FEATURE_REF=$(git rev-parse origin/main)
|
||||
echo "Feature (HEAD): $FEATURE_REF"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 2: Check for in-progress sibling runs ---
|
||||
echo "::group::Checking for in-progress sibling runs"
|
||||
CURRENT_RUN_ID="${GITHUB_RUN_ID:-0}"
|
||||
IN_PROGRESS=$(gh run list \
|
||||
-R "$REPO" \
|
||||
--workflow=bench-scheduled.yml \
|
||||
--status=in_progress \
|
||||
--json databaseId \
|
||||
--jq "[.[] | select(.databaseId != $CURRENT_RUN_ID)] | length")
|
||||
|
||||
SHOULD_SKIP="false"
|
||||
if [ "$IN_PROGRESS" -gt 0 ]; then
|
||||
echo "::warning::Previous bench run still in progress ($IN_PROGRESS sibling run(s) found). Skipping."
|
||||
SHOULD_SKIP="true"
|
||||
# Output a flag so the workflow can send a Slack alert
|
||||
echo "long-running=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No in-progress sibling runs"
|
||||
echo "long-running=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 3: Read last successful feature ref from charts repo ---
|
||||
echo "::group::Reading persisted state"
|
||||
LAST_FEATURE_REF=""
|
||||
STATE_URL="https://raw.githubusercontent.com/decofe/reth-bench-charts/state/state/hourly-last-feature-ref"
|
||||
if RAW=$(curl -sfL -H "Authorization: token ${DEREK_TOKEN}" "$STATE_URL"); then
|
||||
LAST_FEATURE_REF=$(echo "$RAW" | tr -d '[:space:]')
|
||||
echo "Previous feature ref: $LAST_FEATURE_REF"
|
||||
else
|
||||
echo "No persisted state found (first run)"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 4: Determine baseline and skip logic ---
|
||||
echo "::group::Resolving baseline and skip logic"
|
||||
if [ "$SHOULD_SKIP" = "true" ]; then
|
||||
BASELINE_REF=$(git rev-parse origin/main~1)
|
||||
echo "Already marked skip (sibling in progress)"
|
||||
elif [ -z "$LAST_FEATURE_REF" ]; then
|
||||
# First run: no previous state, fall back to HEAD~1
|
||||
BASELINE_REF=$(git rev-parse origin/main~1)
|
||||
echo "First run — using HEAD~1 as baseline"
|
||||
elif [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
|
||||
echo "No new commits on main, but force=true — running anyway"
|
||||
else
|
||||
SHOULD_SKIP="true"
|
||||
echo "No new commits on main since last run — will skip"
|
||||
fi
|
||||
else
|
||||
# Normal case: use last benchmarked commit as baseline
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
echo "New commit(s) on main detected — comparing against last benchmarked commit"
|
||||
fi
|
||||
|
||||
echo "Baseline: $BASELINE_REF"
|
||||
echo "Feature: $FEATURE_REF"
|
||||
echo "Skip: $SHOULD_SKIP"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 5: Write outputs ---
|
||||
{
|
||||
echo "baseline-ref=$BASELINE_REF"
|
||||
echo "feature-ref=$FEATURE_REF"
|
||||
echo "should-skip=$SHOULD_SKIP"
|
||||
echo "is-stale=false"
|
||||
echo "stale-age-hours=0"
|
||||
echo "nightly-created="
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# Release mode: compare latest GitHub release tag vs current nightly build
|
||||
# ==========================================================================
|
||||
if [ "$MODE" = "release" ]; then
|
||||
|
||||
# --- Step 1: Resolve feature ref from latest nightly Docker build ---
|
||||
echo "::group::Querying latest nightly docker build"
|
||||
RUNS_JSON=$(gh run list \
|
||||
-R "$REPO" \
|
||||
--workflow=docker.yml \
|
||||
--event=schedule \
|
||||
--status=completed \
|
||||
--limit 5 \
|
||||
--json headSha,createdAt,conclusion)
|
||||
|
||||
LATEST=$(echo "$RUNS_JSON" | jq -r '[.[] | select(.conclusion == "success")] | first // empty')
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo "::error::No successful scheduled docker.yml run found in the last 5 runs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FEATURE_REF=$(echo "$LATEST" | jq -r '.headSha')
|
||||
echo "Nightly commit (feature): $FEATURE_REF"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 2: Resolve baseline ref from latest GitHub release ---
|
||||
echo "::group::Resolving latest release tag"
|
||||
RELEASE_JSON=$(gh release view --repo "$REPO" --json tagName,targetCommitish,publishedAt 2>/dev/null || echo "{}")
|
||||
RELEASE_TAG=$(echo "$RELEASE_JSON" | jq -r '.tagName // empty')
|
||||
|
||||
if [ -z "$RELEASE_TAG" ]; then
|
||||
echo "::error::No release found on $REPO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve the tag to a commit SHA
|
||||
BASELINE_REF=$(gh api "repos/$REPO/git/ref/tags/$RELEASE_TAG" --jq '.object.sha' 2>/dev/null || true)
|
||||
|
||||
# If tag points to an annotated tag object, dereference to the commit
|
||||
if [ -n "$BASELINE_REF" ]; then
|
||||
OBJ_TYPE=$(gh api "repos/$REPO/git/tags/$BASELINE_REF" --jq '.object.type' 2>/dev/null || echo "commit")
|
||||
if [ "$OBJ_TYPE" = "commit" ]; then
|
||||
BASELINE_REF=$(gh api "repos/$REPO/git/tags/$BASELINE_REF" --jq '.object.sha' 2>/dev/null || echo "$BASELINE_REF")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$BASELINE_REF" ]; then
|
||||
echo "::error::Could not resolve release tag $RELEASE_TAG to a commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release tag: $RELEASE_TAG"
|
||||
echo "Release commit (baseline): $BASELINE_REF"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 3: Read last successful feature ref from charts repo ---
|
||||
echo "::group::Reading persisted state"
|
||||
LAST_FEATURE_REF=""
|
||||
STATE_URL="https://raw.githubusercontent.com/decofe/reth-bench-charts/state/state/release-last-feature-ref"
|
||||
if RAW=$(curl -sfL -H "Authorization: token ${DEREK_TOKEN}" "$STATE_URL"); then
|
||||
LAST_FEATURE_REF=$(echo "$RAW" | tr -d '[:space:]')
|
||||
echo "Previous feature ref: $LAST_FEATURE_REF"
|
||||
else
|
||||
echo "No persisted state found (first run)"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 4: Skip logic ---
|
||||
echo "::group::Resolving skip logic"
|
||||
SHOULD_SKIP="false"
|
||||
if [ -n "$LAST_FEATURE_REF" ] && [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
|
||||
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
|
||||
echo "No new nightly, but force=true — running anyway"
|
||||
else
|
||||
SHOULD_SKIP="true"
|
||||
echo "No new nightly since last release regression run — will skip"
|
||||
fi
|
||||
else
|
||||
echo "New nightly detected or first run"
|
||||
fi
|
||||
|
||||
echo "Baseline: $BASELINE_REF ($RELEASE_TAG)"
|
||||
echo "Feature: $FEATURE_REF"
|
||||
echo "Skip: $SHOULD_SKIP"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 5: Write outputs ---
|
||||
{
|
||||
echo "baseline-ref=$BASELINE_REF"
|
||||
echo "feature-ref=$FEATURE_REF"
|
||||
echo "should-skip=$SHOULD_SKIP"
|
||||
echo "is-stale=false"
|
||||
echo "stale-age-hours=0"
|
||||
echo "nightly-created="
|
||||
echo "long-running=false"
|
||||
echo "release-tag=$RELEASE_TAG"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==========================================================================
|
||||
# Nightly mode: query latest Docker nightly build (original logic)
|
||||
# ==========================================================================
|
||||
|
||||
# --- Step 1: Query latest successful scheduled docker.yml run ---
|
||||
echo "::group::Querying latest nightly docker build"
|
||||
|
||||
RUNS_JSON=$(gh run list \
|
||||
-R "$REPO" \
|
||||
--workflow=docker.yml \
|
||||
--event=schedule \
|
||||
--status=completed \
|
||||
--limit 5 \
|
||||
--json headSha,createdAt,conclusion)
|
||||
|
||||
# Find the most recent successful run
|
||||
LATEST=$(echo "$RUNS_JSON" | jq -r '[.[] | select(.conclusion == "success")] | first // empty')
|
||||
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo "::error::No successful scheduled docker.yml run found in the last 5 runs"
|
||||
echo "Runs found: $RUNS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FEATURE_REF=$(echo "$LATEST" | jq -r '.headSha')
|
||||
CREATED_AT=$(echo "$LATEST" | jq -r '.createdAt')
|
||||
echo "Latest nightly commit: $FEATURE_REF"
|
||||
echo "Built at: $CREATED_AT"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 2: Staleness check ---
|
||||
echo "::group::Checking staleness"
|
||||
NOW_EPOCH=$(date +%s)
|
||||
# Handle both GNU date (-d) and BSD date (-j -f) for cross-platform compat
|
||||
CREATED_EPOCH=$(date -d "$CREATED_AT" +%s 2>/dev/null || \
|
||||
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CREATED_AT" +%s 2>/dev/null || \
|
||||
date -j -f "%Y-%m-%dT%T%z" "$CREATED_AT" +%s 2>/dev/null || \
|
||||
{ echo "::error::Cannot parse date: $CREATED_AT"; exit 1; })
|
||||
|
||||
AGE_SECONDS=$(( NOW_EPOCH - CREATED_EPOCH ))
|
||||
AGE_HOURS=$(( AGE_SECONDS / 3600 ))
|
||||
IS_STALE="false"
|
||||
|
||||
if [ "$AGE_HOURS" -gt 24 ]; then
|
||||
IS_STALE="true"
|
||||
echo "::warning::STALE NIGHTLY: Build is ${AGE_HOURS}h old (>24h threshold)"
|
||||
echo "This indicates the nightly docker build failed — no new image was produced"
|
||||
else
|
||||
echo "Nightly build age: ${AGE_HOURS}h (within 24h threshold)"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 3: Read last successful feature ref from charts repo ---
|
||||
echo "::group::Reading persisted state"
|
||||
LAST_FEATURE_REF=""
|
||||
STATE_URL="https://raw.githubusercontent.com/decofe/reth-bench-charts/state/state/nightly-last-feature-ref"
|
||||
if RAW=$(curl -sfL -H "Authorization: token ${DEREK_TOKEN}" "$STATE_URL"); then
|
||||
LAST_FEATURE_REF=$(echo "$RAW" | tr -d '[:space:]')
|
||||
echo "Previous feature ref: $LAST_FEATURE_REF"
|
||||
else
|
||||
echo "No persisted state found (first run)"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 4: Determine baseline and skip logic ---
|
||||
echo "::group::Resolving refs"
|
||||
SHOULD_SKIP="false"
|
||||
BASELINE_REF="$FEATURE_REF" # default for first run
|
||||
|
||||
if [ "$IS_STALE" = "true" ]; then
|
||||
# Stale = error path, don't skip (will alert and fail downstream)
|
||||
SHOULD_SKIP="false"
|
||||
BASELINE_REF="${LAST_FEATURE_REF:-$FEATURE_REF}"
|
||||
echo "Stale nightly detected — will alert and fail"
|
||||
elif [ -z "$LAST_FEATURE_REF" ]; then
|
||||
# First run: baseline = feature (self-comparison to establish baseline)
|
||||
BASELINE_REF="$FEATURE_REF"
|
||||
echo "First run — will benchmark nightly against itself to establish baseline"
|
||||
elif [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
|
||||
# No new nightly since last successful run
|
||||
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
|
||||
echo "No new nightly, but force=true — running anyway"
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
else
|
||||
SHOULD_SKIP="true"
|
||||
echo "No new nightly since last run — will skip"
|
||||
fi
|
||||
else
|
||||
# Normal case: new nightly available
|
||||
BASELINE_REF="$LAST_FEATURE_REF"
|
||||
echo "New nightly detected"
|
||||
fi
|
||||
|
||||
echo "Baseline: $BASELINE_REF"
|
||||
echo "Feature: $FEATURE_REF"
|
||||
echo "Skip: $SHOULD_SKIP"
|
||||
echo "Stale: $IS_STALE"
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- Step 5: Write outputs ---
|
||||
{
|
||||
echo "baseline-ref=$BASELINE_REF"
|
||||
echo "feature-ref=$FEATURE_REF"
|
||||
echo "should-skip=$SHOULD_SKIP"
|
||||
echo "is-stale=$IS_STALE"
|
||||
echo "stale-age-hours=$AGE_HOURS"
|
||||
echo "nightly-created=$CREATED_AT"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
318
.github/scripts/bench-slack-notify.js
vendored
318
.github/scripts/bench-slack-notify.js
vendored
@@ -1,318 +0,0 @@
|
||||
// Sends Slack notifications for reth-bench results.
|
||||
//
|
||||
// Reads from environment:
|
||||
// SLACK_BENCH_BOT_TOKEN – Slack Bot User OAuth Token (xoxb-...)
|
||||
// SLACK_BENCH_CHANNEL – Public channel ID for significant improvements
|
||||
// BENCH_WORK_DIR – Directory containing summary.json
|
||||
// BENCH_PR – PR number (may be empty)
|
||||
// BENCH_ACTOR – GitHub user who triggered the bench
|
||||
// BENCH_JOB_URL – URL to the Actions job page
|
||||
// BENCH_BASELINE_ARGS – Extra CLI args for the baseline reth node
|
||||
// BENCH_FEATURE_ARGS – Extra CLI args for the feature reth node
|
||||
// BENCH_SAMPLY – 'true' if samply profiling was enabled
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const notify = require('./.github/scripts/bench-slack-notify.js');
|
||||
// await notify.success({ core, context });
|
||||
// await notify.failure({ core, context, failedStep: '...' });
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { fmtChange, fmtMs, verdict, loadSamplyUrls, blocksLabel, metricRows, waitTimeRows } = require('./bench-utils');
|
||||
|
||||
const SLACK_API = 'https://slack.com/api/chat.postMessage';
|
||||
|
||||
function loadSlackUsers(repoRoot) {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(repoRoot, '.github', 'scripts', 'bench-slack-users.json'), 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
// Filter out non-user-ID entries (like _comment)
|
||||
const users = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!k.startsWith('_') && typeof v === 'string' && v.startsWith('U')) {
|
||||
users[k] = v;
|
||||
}
|
||||
}
|
||||
return users;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function postToSlack(token, channel, blocks, text, core, threadTs) {
|
||||
const payload = { channel, blocks, text, unfurl_links: false };
|
||||
if (threadTs) payload.thread_ts = threadTs;
|
||||
const resp = await fetch(SLACK_API, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
core.warning(`Slack API error (channel ${channel}): ${JSON.stringify(data)}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function cell(text) {
|
||||
const s = String(text);
|
||||
return { type: 'raw_text', text: s || ' ' };
|
||||
}
|
||||
|
||||
// Slack shortcodes for verdict (Block Kit header doesn't support unicode emoji)
|
||||
const SLACK_VERDICT = {
|
||||
'⚠️': ':warning:',
|
||||
'❌': ':x:',
|
||||
'✅': ':white_check_mark:',
|
||||
'⚪': ':white_circle:',
|
||||
};
|
||||
|
||||
function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls }) {
|
||||
const { emoji, label } = verdict(summary.changes);
|
||||
const headerEmoji = SLACK_VERDICT[emoji] || emoji;
|
||||
|
||||
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
|
||||
const commitUrl = `https://github.com/${repo}/commit`;
|
||||
const baselineLink = `<${commitUrl}/${summary.baseline.ref}|${summary.baseline.name}>`;
|
||||
const featureLink = `<${commitUrl}/${summary.feature.ref}|${summary.feature.name}>`;
|
||||
|
||||
// Meta line
|
||||
const metaParts = [];
|
||||
if (prNumber) metaParts.push(`*<${prUrl}|PR #${prNumber}>*`);
|
||||
metaParts.push(`triggered by ${actorSlackId ? `<@${actorSlackId}>` : `@${actor}`}`);
|
||||
|
||||
// Baseline/feature lines with samply profile links
|
||||
let baselineLine = `*Baseline:* ${baselineLink}`;
|
||||
const bl1 = samplyUrls['baseline-1'];
|
||||
const bl2 = samplyUrls['baseline-2'];
|
||||
if (bl1) baselineLine += ` | <${bl1}|Samply 1>`;
|
||||
if (bl2) baselineLine += ` | <${bl2}|Samply 2>`;
|
||||
|
||||
let featureLine = `*Feature:* ${featureLink}`;
|
||||
const fl1 = samplyUrls['feature-1'];
|
||||
const fl2 = samplyUrls['feature-2'];
|
||||
if (fl1) featureLine += ` | <${fl1}|Samply 1>`;
|
||||
if (fl2) featureLine += ` | <${fl2}|Samply 2>`;
|
||||
|
||||
const countsLine = blocksLabel(summary).map(p => `*${p.key}:* ${p.value}`).join(' | ');
|
||||
|
||||
const baselineArgs = process.env.BENCH_BASELINE_ARGS || '';
|
||||
const featureArgs = process.env.BENCH_FEATURE_ARGS || '';
|
||||
const argsLines = [];
|
||||
if (baselineArgs) argsLines.push(`*Baseline Args:* \`${baselineArgs}\``);
|
||||
if (featureArgs) argsLines.push(`*Feature Args:* \`${featureArgs}\``);
|
||||
|
||||
const sectionText = [metaParts.join(' | '), '', baselineLine, featureLine, ...argsLines, countsLine].join('\n');
|
||||
|
||||
// Action buttons
|
||||
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
|
||||
const buttons = [
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
|
||||
url: jobUrl,
|
||||
action_id: 'ci_button',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'Diff :github:', emoji: true },
|
||||
url: diffUrl,
|
||||
action_id: 'diff_button',
|
||||
},
|
||||
];
|
||||
|
||||
// Build table rows from shared metricRows
|
||||
const rows = metricRows(summary);
|
||||
const tableRows = [
|
||||
[cell('Metric'), cell('Baseline'), cell('Feature'), cell('Change')],
|
||||
...rows.map(r => [cell(r.label), cell(r.baseline), cell(r.feature), cell(r.change || ' ')]),
|
||||
];
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: `${headerEmoji} ${label}`, emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: sectionText },
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
column_settings: [
|
||||
{ align: 'left' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
],
|
||||
rows: tableRows,
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: buttons,
|
||||
},
|
||||
];
|
||||
|
||||
// Wait times as a separate table block (sent as threaded reply due to Slack one-table limit)
|
||||
const threadBlocks = [];
|
||||
const wtRows = waitTimeRows(summary);
|
||||
if (wtRows.length > 0) {
|
||||
const waitTableRows = [
|
||||
[cell('Wait Time'), cell('Baseline'), cell('Feature')],
|
||||
...wtRows.map(r => [cell(r.title), cell(r.baseline), cell(r.feature)]),
|
||||
];
|
||||
threadBlocks.push({
|
||||
type: 'table',
|
||||
column_settings: [
|
||||
{ align: 'left' },
|
||||
{ align: 'right' },
|
||||
{ align: 'right' },
|
||||
],
|
||||
rows: waitTableRows,
|
||||
});
|
||||
}
|
||||
|
||||
return { blocks, threadBlocks };
|
||||
}
|
||||
|
||||
function buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep }) {
|
||||
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
|
||||
const actorMention = actorSlackId ? `<@${actorSlackId}>` : `@${actor}`;
|
||||
const parts = [
|
||||
prNumber ? `*<${prUrl}|PR #${prNumber}>*` : '',
|
||||
`by ${actorMention}`,
|
||||
`failed while *${failedStep}*`,
|
||||
].filter(Boolean);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
|
||||
url: jobUrl,
|
||||
action_id: 'ci_button',
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'header',
|
||||
text: { type: 'plain_text', text: ':rotating_light: Bench Failed', emoji: true },
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: parts.join(' | ') },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
elements: buttons,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function success({ core, context }) {
|
||||
const token = process.env.SLACK_BENCH_BOT_TOKEN;
|
||||
if (!token) {
|
||||
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
let summary;
|
||||
try {
|
||||
summary = JSON.parse(fs.readFileSync(process.env.BENCH_WORK_DIR + '/summary.json', 'utf8'));
|
||||
} catch (e) {
|
||||
core.warning('Could not read summary.json for Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const prNumber = process.env.BENCH_PR;
|
||||
const actor = process.env.BENCH_ACTOR;
|
||||
const jobUrl = process.env.BENCH_JOB_URL ||
|
||||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const samplyUrls = loadSamplyUrls(process.env.BENCH_WORK_DIR);
|
||||
|
||||
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
|
||||
const actorSlackId = slackUsers[actor];
|
||||
|
||||
const { blocks, threadBlocks } = buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls });
|
||||
const text = `Bench: ${summary.baseline.name} vs ${summary.feature.name}`;
|
||||
|
||||
async function sendWithThread(ch) {
|
||||
const res = await postToSlack(token, ch, blocks, text, core);
|
||||
if (res.ok && res.ts && threadBlocks.length > 0) {
|
||||
for (const tb of threadBlocks) {
|
||||
await postToSlack(token, ch, [tb], 'Wait time breakdown', core, res.ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const slackMode = process.env.BENCH_SLACK || 'always';
|
||||
|
||||
// Post to public channel if any metric shows significant improvement or regression
|
||||
const channel = process.env.SLACK_BENCH_CHANNEL;
|
||||
let postedToChannel = false;
|
||||
if (channel) {
|
||||
const changes = summary.changes || {};
|
||||
const hasImprovement = Object.values(changes).some(c => c.sig === 'good');
|
||||
if (hasImprovement) {
|
||||
await sendWithThread(channel);
|
||||
postedToChannel = true;
|
||||
} else {
|
||||
core.info('No significant improvement, skipping public channel notification');
|
||||
}
|
||||
}
|
||||
|
||||
// In on-win mode, only notify on improvement — skip DM fallback entirely
|
||||
if (slackMode === 'on-win') {
|
||||
if (!postedToChannel) {
|
||||
core.info('on-win mode: no improvement detected, skipping all notifications');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// DM the actor only when results were not posted to the public channel
|
||||
if (!postedToChannel) {
|
||||
if (actorSlackId) {
|
||||
await sendWithThread(actorSlackId);
|
||||
} else {
|
||||
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
|
||||
}
|
||||
} else {
|
||||
core.info(`Results posted to channel, skipping DM to ${actor}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function failure({ core, context, failedStep }) {
|
||||
const token = process.env.SLACK_BENCH_BOT_TOKEN;
|
||||
if (!token) {
|
||||
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const prNumber = process.env.BENCH_PR;
|
||||
const actor = process.env.BENCH_ACTOR;
|
||||
const jobUrl = process.env.BENCH_JOB_URL ||
|
||||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
|
||||
const actorSlackId = slackUsers[actor];
|
||||
|
||||
const blocks = buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep });
|
||||
const text = `Bench failed while ${failedStep}`;
|
||||
|
||||
// Always DM the actor
|
||||
if (actorSlackId) {
|
||||
await postToSlack(token, actorSlackId, blocks, text, core);
|
||||
} else {
|
||||
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
|
||||
}
|
||||
|
||||
// Only DM for failures, don't post to public channel
|
||||
}
|
||||
|
||||
module.exports = { success, failure };
|
||||
25
.github/scripts/bench-slack-users.json
vendored
25
.github/scripts/bench-slack-users.json
vendored
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"_comment": "Maps GitHub usernames to Slack user IDs. Find yours: Slack profile > ··· > Copy member ID.",
|
||||
"shekhirin": "U09FAL2UMLJ",
|
||||
"mattsse": "U09FQNPMRT3",
|
||||
"klkvr": "U09FAK95FC2",
|
||||
"joshieDo": "U09LHN6GYAU",
|
||||
"mediocregopher": "U09FF75KMQU",
|
||||
"yongkangc": "U09FB0ECTD4",
|
||||
"gakonst": "U092SEPDM40",
|
||||
"Rjected": "U09F6SCKRGT",
|
||||
"DaniPopes": "U09FAT8EK2A",
|
||||
"emmajam": "U0A34UN92HW",
|
||||
"onbjerg": "U09FB0UK5AA",
|
||||
"fgimenez": "U09G3GP7CSU",
|
||||
"rakita": "U09FB3Z2M7Y",
|
||||
"jxom": "U09F72MG083",
|
||||
"tmm": "U0AD0U8E88N",
|
||||
"pepyakin": "U0A7HKMGEHJ",
|
||||
"grandizzy": "U09F8DBDDRT",
|
||||
"SuperFluffy": "U095BKHB2Q4",
|
||||
"kamsz": "U0A2563UBRD",
|
||||
"zerosnacks": "U09FARPMN74",
|
||||
"samczsun": "U096R14E4H3",
|
||||
"laibe": "U09FARE0B9Q"
|
||||
}
|
||||
27
.github/scripts/bench-update-status.js
vendored
27
.github/scripts/bench-update-status.js
vendored
@@ -1,27 +0,0 @@
|
||||
// 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;
|
||||
150
.github/scripts/bench-upload-clickhouse.py
vendored
150
.github/scripts/bench-upload-clickhouse.py
vendored
@@ -1,150 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Upload bench-scheduled summary.json results to ClickHouse.
|
||||
|
||||
Reads the summary JSON produced by bench-reth-summary.py and inserts a row
|
||||
into the bench_dual_comparisons table so the PM dashboard can display results.
|
||||
|
||||
Usage:
|
||||
bench-upload-clickhouse.py \
|
||||
--summary <summary.json> \
|
||||
--workflow-name <name> \
|
||||
--chain <chain>
|
||||
|
||||
Environment variables:
|
||||
CLICKHOUSE_HOST ClickHouse host URL
|
||||
CLICKHOUSE_USER ClickHouse username
|
||||
CLICKHOUSE_PASSWORD ClickHouse password
|
||||
CLICKHOUSE_DATABASE ClickHouse database (default: "default")
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Upload benchmark results to ClickHouse")
|
||||
parser.add_argument("--summary", required=True, help="Path to summary.json")
|
||||
parser.add_argument("--workflow-name", required=True, help="Workflow name for ClickHouse")
|
||||
parser.add_argument("--chain", default="mainnet", help="Chain name")
|
||||
parser.add_argument("--grafana-url", default="", help="Grafana dashboard URL")
|
||||
parser.add_argument("--github-diff-url", default="", help="GitHub diff URL")
|
||||
parser.add_argument("--job-url", default="", help="CI job URL")
|
||||
args = parser.parse_args()
|
||||
|
||||
ch_host = os.environ.get("CLICKHOUSE_HOST", "")
|
||||
ch_user = os.environ.get("CLICKHOUSE_USER", "")
|
||||
ch_password = os.environ.get("CLICKHOUSE_PASSWORD", "")
|
||||
ch_database = os.environ.get("CLICKHOUSE_DATABASE", "default")
|
||||
ch_table = "bench_dual_comparisons"
|
||||
|
||||
if not ch_host or not ch_user or not ch_password:
|
||||
print("Missing ClickHouse credentials, skipping upload", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
with open(args.summary) as f:
|
||||
summary = json.load(f)
|
||||
|
||||
baseline = summary["baseline"]
|
||||
feature = summary["feature"]
|
||||
b_stats = baseline["stats"]
|
||||
f_stats = feature["stats"]
|
||||
changes = summary["changes"]
|
||||
blocks = summary["blocks"]
|
||||
|
||||
# Extract wait time data
|
||||
wait_times = summary.get("wait_times", {})
|
||||
def wait_mean(field):
|
||||
wt = wait_times.get(field, {})
|
||||
b = wt.get("baseline", {}).get("mean_ms", 0.0)
|
||||
f = wt.get("feature", {}).get("mean_ms", 0.0)
|
||||
return b, f
|
||||
|
||||
b_persist, f_persist = wait_mean("persistence_wait_us")
|
||||
b_exec_cache, f_exec_cache = wait_mean("execution_cache_wait_us")
|
||||
b_sparse, f_sparse = wait_mean("sparse_trie_wait_us")
|
||||
|
||||
# gas_per_second: summary uses mean_mgas_s (Mgas/s), ClickHouse stores gas/s
|
||||
b_gas_per_second = b_stats["mean_mgas_s"] * 1_000_000
|
||||
f_gas_per_second = f_stats["mean_mgas_s"] * 1_000_000
|
||||
|
||||
mean_change = changes.get("mean", {}).get("pct", 0.0)
|
||||
gas_change = changes.get("mgas_s", {}).get("pct", 0.0)
|
||||
latency_improved = 1 if mean_change < 0 else 0
|
||||
throughput_improved = 1 if gas_change > 0 else 0
|
||||
|
||||
big_blocks = "true" if summary.get("big_blocks", False) else "false"
|
||||
warmup_blocks = summary.get("warmup_blocks", 0) or 0
|
||||
|
||||
def esc(s):
|
||||
return str(s).replace("'", "\\'")
|
||||
|
||||
insert = f"""
|
||||
INSERT INTO {ch_database}.{ch_table} (
|
||||
workflow_name, chain,
|
||||
baseline_ref, baseline_commit,
|
||||
feature_ref, feature_commit,
|
||||
blocks,
|
||||
baseline_total_latency_ms, baseline_gas_per_second,
|
||||
baseline_latency_mean_ms, baseline_latency_median_ms,
|
||||
baseline_latency_p90_ms, baseline_latency_p99_ms,
|
||||
feature_total_latency_ms, feature_gas_per_second,
|
||||
feature_latency_mean_ms, feature_latency_median_ms,
|
||||
feature_latency_p90_ms, feature_latency_p99_ms,
|
||||
mean_latency_change_percent, gas_per_second_change_percent,
|
||||
latency_improved, throughput_improved,
|
||||
warmup_blocks, big_blocks,
|
||||
grafana_benchmark_url, github_diff_url, argo_workflow_url,
|
||||
baseline_persistence_wait_mean_ms, baseline_execution_cache_wait_mean_ms,
|
||||
baseline_sparse_trie_wait_mean_ms,
|
||||
feature_persistence_wait_mean_ms, feature_execution_cache_wait_mean_ms,
|
||||
feature_sparse_trie_wait_mean_ms
|
||||
) VALUES (
|
||||
'{esc(args.workflow_name)}', '{esc(args.chain)}',
|
||||
'{esc(baseline["ref"])}', '{esc(baseline["ref"])}',
|
||||
'{esc(feature["ref"])}', '{esc(feature["ref"])}',
|
||||
{blocks},
|
||||
{b_stats.get("wall_clock_s", 0) * 1000}, {b_gas_per_second},
|
||||
{b_stats["mean_ms"]}, {b_stats["p50_ms"]},
|
||||
{b_stats["p90_ms"]}, {b_stats["p99_ms"]},
|
||||
{f_stats.get("wall_clock_s", 0) * 1000}, {f_gas_per_second},
|
||||
{f_stats["mean_ms"]}, {f_stats["p50_ms"]},
|
||||
{f_stats["p90_ms"]}, {f_stats["p99_ms"]},
|
||||
{mean_change}, {gas_change},
|
||||
{latency_improved}, {throughput_improved},
|
||||
{warmup_blocks}, '{big_blocks}',
|
||||
'{esc(args.grafana_url)}', '{esc(args.github_diff_url)}', '{esc(args.job_url)}',
|
||||
{b_persist}, {b_exec_cache}, {b_sparse},
|
||||
{f_persist}, {f_exec_cache}, {f_sparse}
|
||||
);
|
||||
"""
|
||||
|
||||
# Build ClickHouse HTTP URL (credentials via headers, never in URL)
|
||||
host = ch_host.rstrip("/")
|
||||
if not host.startswith("http"):
|
||||
host = f"https://{host}:8443"
|
||||
|
||||
url = f"{host}/?database={ch_database}"
|
||||
|
||||
req = urllib.request.Request(url, data=insert.encode("utf-8"), method="POST")
|
||||
req.add_header("Content-Type", "text/plain")
|
||||
req.add_header("X-ClickHouse-User", ch_user)
|
||||
req.add_header("X-ClickHouse-Key", ch_password)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
if body.strip():
|
||||
print(f"ClickHouse response: {body}")
|
||||
print(f"Successfully uploaded benchmark results to ClickHouse ({args.workflow_name})")
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8")
|
||||
print(f"ClickHouse upload failed ({e.code}): {body}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
112
.github/scripts/bench-utils.js
vendored
112
.github/scripts/bench-utils.js
vendored
@@ -1,112 +0,0 @@
|
||||
// Shared utilities for reth-bench result rendering.
|
||||
//
|
||||
// Used by bench-job-summary.js and bench-slack-notify.js.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SIG_EMOJI = { good: '✅', bad: '❌', neutral: '⚪' };
|
||||
|
||||
function fmtMs(v) { return v.toFixed(2) + 'ms'; }
|
||||
function fmtMgas(v) { return v.toFixed(2); }
|
||||
function fmtS(v) { return v.toFixed(2) + 's'; }
|
||||
|
||||
function fmtChange(ch) {
|
||||
if (!ch || (!ch.pct && !ch.ci_pct)) return '';
|
||||
const pctStr = `${ch.pct >= 0 ? '+' : ''}${ch.pct.toFixed(2)}%`;
|
||||
const ciStr = ch.ci_pct ? ` (±${ch.ci_pct.toFixed(2)}%)` : '';
|
||||
return `${pctStr}${ciStr} ${SIG_EMOJI[ch.sig]}`;
|
||||
}
|
||||
|
||||
function verdict(changes) {
|
||||
const vals = Object.values(changes);
|
||||
const hasBad = vals.some(v => v.sig === 'bad');
|
||||
const hasGood = vals.some(v => v.sig === 'good');
|
||||
if (hasBad && hasGood) return { emoji: '⚠️', label: 'Mixed Results' };
|
||||
if (hasBad) return { emoji: '❌', label: 'Regression' };
|
||||
if (hasGood) return { emoji: '✅', label: 'Improvement' };
|
||||
return { emoji: '⚪', label: 'No Difference' };
|
||||
}
|
||||
|
||||
function loadSamplyUrls(workDir) {
|
||||
const urls = {};
|
||||
for (const run of ['baseline-1', 'baseline-2', 'feature-1', 'feature-2']) {
|
||||
try {
|
||||
const url = fs.readFileSync(path.join(workDir, run, 'samply-profile-url.txt'), 'utf8').trim();
|
||||
if (url) urls[run] = url;
|
||||
} catch {}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function balModeLabel(mode) {
|
||||
switch (mode) {
|
||||
case 'true':
|
||||
case 'feature':
|
||||
case 'baseline':
|
||||
return mode;
|
||||
case 'both':
|
||||
return 'true';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function blocksLabel(summary) {
|
||||
const parts = [];
|
||||
if (summary.big_blocks) {
|
||||
parts.push({ key: 'Big Blocks', value: summary.blocks });
|
||||
const balMode = balModeLabel(summary.bal_mode || summary.bal || process.env.BENCH_BAL || 'false');
|
||||
if (balMode) parts.push({ key: 'BAL', value: balMode });
|
||||
} else {
|
||||
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
|
||||
if (warmup) parts.push({ key: 'Warmup', value: warmup });
|
||||
parts.push({ key: 'Blocks', value: summary.blocks });
|
||||
}
|
||||
const cores = process.env.BENCH_CORES || '0';
|
||||
if (cores !== '0') parts.push({ key: 'Cores', value: cores });
|
||||
if (summary.wait_time) parts.push({ key: 'Wait time', value: summary.wait_time });
|
||||
return parts;
|
||||
}
|
||||
|
||||
// The 7 metric rows shared by all renderers.
|
||||
// Returns an array of { label, baseline, feature, change } objects.
|
||||
function metricRows(summary) {
|
||||
const b = summary.baseline.stats;
|
||||
const f = summary.feature.stats;
|
||||
const c = summary.changes;
|
||||
return [
|
||||
{ label: 'Mean', baseline: fmtMs(b.mean_ms), feature: fmtMs(f.mean_ms), change: fmtChange(c.mean) },
|
||||
{ label: 'StdDev', baseline: fmtMs(b.stddev_ms), feature: fmtMs(f.stddev_ms), change: '' },
|
||||
{ label: 'P50', baseline: fmtMs(b.p50_ms), feature: fmtMs(f.p50_ms), change: fmtChange(c.p50) },
|
||||
{ label: 'P90', baseline: fmtMs(b.p90_ms), feature: fmtMs(f.p90_ms), change: fmtChange(c.p90) },
|
||||
{ label: 'P99', baseline: fmtMs(b.p99_ms), feature: fmtMs(f.p99_ms), change: fmtChange(c.p99) },
|
||||
{ label: 'Mgas/s', baseline: fmtMgas(b.mean_mgas_s), feature: fmtMgas(f.mean_mgas_s), change: fmtChange(c.mgas_s) },
|
||||
{ label: 'Wall Clock', baseline: fmtS(b.wall_clock_s), feature: fmtS(f.wall_clock_s), change: fmtChange(c.wall_clock) },
|
||||
{ label: 'Persist Wait', baseline: fmtMs(b.mean_persist_ms || 0), feature: fmtMs(f.mean_persist_ms || 0), change: fmtChange(c.persist_wait) },
|
||||
];
|
||||
}
|
||||
|
||||
// Wait time rows: one row per metric showing mean values.
|
||||
function waitTimeRows(summary) {
|
||||
const waitTimes = summary.wait_times || {};
|
||||
const rows = [];
|
||||
for (const key of Object.keys(waitTimes)) {
|
||||
const wt = waitTimes[key];
|
||||
rows.push({ title: wt.title, baseline: fmtMs(wt.baseline.mean_ms), feature: fmtMs(wt.feature.mean_ms) });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SIG_EMOJI,
|
||||
fmtMs,
|
||||
fmtMgas,
|
||||
fmtS,
|
||||
fmtChange,
|
||||
verdict,
|
||||
loadSamplyUrls,
|
||||
blocksLabel,
|
||||
metricRows,
|
||||
waitTimeRows,
|
||||
};
|
||||
45
.github/scripts/check_rv32imac.sh
vendored
45
.github/scripts/check_rv32imac.sh
vendored
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uxo pipefail
|
||||
|
||||
crates_to_check=(
|
||||
reth-network-peers
|
||||
reth-trie-common
|
||||
reth-trie-sparse
|
||||
reth-chainspec
|
||||
reth-consensus
|
||||
reth-consensus-common
|
||||
reth-prune-types
|
||||
reth-static-file-types
|
||||
reth-storage-errors
|
||||
reth-execution-errors
|
||||
reth-errors
|
||||
reth-execution-types
|
||||
reth-db-models
|
||||
reth-evm
|
||||
reth-revm
|
||||
reth-storage-api
|
||||
|
||||
## ethereum
|
||||
reth-evm-ethereum
|
||||
reth-ethereum-forks
|
||||
reth-ethereum-primitives
|
||||
reth-ethereum-consensus
|
||||
)
|
||||
|
||||
any_failed=0
|
||||
tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t reth-check)
|
||||
trap 'rm -rf -- "$tmpdir"' EXIT INT TERM
|
||||
|
||||
for crate in "${crates_to_check[@]}"; do
|
||||
outfile="$tmpdir/$crate.log"
|
||||
if cargo +stable build -p "$crate" --target riscv32imac-unknown-none-elf --no-default-features --color never >"$outfile" 2>&1; then
|
||||
echo "✅ $crate"
|
||||
else
|
||||
echo "❌ $crate"
|
||||
sed 's/^/ /' "$outfile"
|
||||
echo ""
|
||||
any_failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit $any_failed
|
||||
14
.github/scripts/codspeed-build.sh
vendored
Executable file
14
.github/scripts/codspeed-build.sh
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# TODO: Benchmarks run WAY too slow due to excessive amount of iterations.
|
||||
|
||||
cmd=(cargo codspeed build --profile profiling)
|
||||
crates=(
|
||||
-p reth-primitives
|
||||
-p reth-trie
|
||||
-p reth-trie-common
|
||||
-p reth-trie-sparse
|
||||
)
|
||||
|
||||
"${cmd[@]}" --features test-utils "${crates[@]}"
|
||||
244
.github/scripts/fetch-grafana-dashboard.py
vendored
244
.github/scripts/fetch-grafana-dashboard.py
vendored
@@ -1,244 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fetch a Grafana dashboard and convert it to the portable import format.
|
||||
|
||||
Fetches the dashboard via API, replaces internal datasource/variable references
|
||||
with template variables, and adds __inputs/__requires/__elements so the JSON is
|
||||
importable on any Grafana instance.
|
||||
|
||||
Usage:
|
||||
export FETCH_GRAFANA_DASHBOARD_URL=https://<NAMESPACE>.grafana.net
|
||||
export FETCH_GRAFANA_DASHBOARD_TOKEN=glsa_...
|
||||
|
||||
python3 .github/scripts/fetch-grafana-dashboard.py <dashboard-uid> > output.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
PANEL_TYPE_NAMES = {
|
||||
"bargauge": "Bar gauge",
|
||||
"gauge": "Gauge",
|
||||
"heatmap": "Heatmap",
|
||||
"piechart": "Pie chart",
|
||||
"stat": "Stat",
|
||||
"table": "Table",
|
||||
"timeseries": "Time series",
|
||||
"barchart": "Bar chart",
|
||||
"text": "Text",
|
||||
"dashlist": "Dashboard list",
|
||||
"logs": "Logs",
|
||||
"nodeGraph": "Node Graph",
|
||||
"histogram": "Histogram",
|
||||
"candlestick": "Candlestick",
|
||||
"state-timeline": "State timeline",
|
||||
"status-history": "Status history",
|
||||
"geomap": "Geomap",
|
||||
"canvas": "Canvas",
|
||||
"news": "News",
|
||||
"xychart": "XY Chart",
|
||||
"trend": "Trend",
|
||||
"datagrid": "Datagrid",
|
||||
"flamegraph": "Flame Graph",
|
||||
"traces": "Traces",
|
||||
}
|
||||
|
||||
|
||||
def fetch_json(base_url: str, token: str, path: str) -> dict:
|
||||
url = f"{base_url}{path}"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def fetch_dashboard(base_url: str, token: str, uid: str) -> dict:
|
||||
return fetch_json(base_url, token, f"/api/dashboards/uid/{uid}")
|
||||
|
||||
|
||||
def fetch_grafana_version(base_url: str) -> str:
|
||||
req = urllib.request.Request(f"{base_url}/api/health")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read())
|
||||
# version string like "13.0.0-23940615780.patch2" -> take just the semver part
|
||||
version = data.get("version", "")
|
||||
# strip build metadata after the first hyphen if it looks like a pre-release
|
||||
parts = version.split("-")
|
||||
return parts[0] if parts else version
|
||||
|
||||
|
||||
def collect_panel_types(panels: list) -> set[str]:
|
||||
types = set()
|
||||
for panel in panels:
|
||||
ptype = panel.get("type", "")
|
||||
if ptype and ptype != "row":
|
||||
types.add(ptype)
|
||||
# nested panels inside collapsed rows
|
||||
for sub in panel.get("panels", []):
|
||||
sub_type = sub.get("type", "")
|
||||
if sub_type and sub_type != "row":
|
||||
types.add(sub_type)
|
||||
return types
|
||||
|
||||
|
||||
def has_expression_datasource(dashboard: dict) -> bool:
|
||||
return "__expr__" in json.dumps(dashboard)
|
||||
|
||||
|
||||
def make_exportable(dashboard: dict, grafana_version: str = "") -> dict:
|
||||
dash = json.loads(json.dumps(dashboard)) # deep copy
|
||||
|
||||
# --- Strip internal fields ---
|
||||
dash.pop("id", None)
|
||||
|
||||
# --- Rewrite links: point to the public repo instead of internal ---
|
||||
dash["links"] = [
|
||||
{
|
||||
"asDropdown": False,
|
||||
"icon": "external link",
|
||||
"includeVars": False,
|
||||
"keepTime": False,
|
||||
"tags": [],
|
||||
"targetBlank": True,
|
||||
"title": "Source (GitHub)",
|
||||
"tooltip": "View source file in repository",
|
||||
"type": "link",
|
||||
"url": "https://github.com/paradigmxyz/reth/tree/main/etc/grafana/dashboards",
|
||||
}
|
||||
]
|
||||
|
||||
# --- Datasource: victoriametrics -> prometheus ---
|
||||
dash_str = json.dumps(dash)
|
||||
dash_str = dash_str.replace("victoriametrics-metrics-datasource", "prometheus")
|
||||
dash = json.loads(dash_str)
|
||||
|
||||
# --- Templating: instance_label constant -> ${VAR_INSTANCE_LABEL} ---
|
||||
# Also strip default-value fields the API returns that are not needed for import
|
||||
STRIP_VAR_DEFAULTS = {"allowCustomValue", "regexApplyTo"}
|
||||
for var in dash.get("templating", {}).get("list", []):
|
||||
if var.get("name") == "instance_label" and var.get("type") == "constant":
|
||||
var["query"] = "${VAR_INSTANCE_LABEL}"
|
||||
var["current"] = {
|
||||
"value": "${VAR_INSTANCE_LABEL}",
|
||||
"text": "${VAR_INSTANCE_LABEL}",
|
||||
"selected": False,
|
||||
}
|
||||
var["options"] = [
|
||||
{
|
||||
"value": "${VAR_INSTANCE_LABEL}",
|
||||
"text": "${VAR_INSTANCE_LABEL}",
|
||||
"selected": False,
|
||||
}
|
||||
]
|
||||
# Clear current values for query/datasource vars (not meaningful for import)
|
||||
elif var.get("type") in ("query", "datasource"):
|
||||
var["current"] = {}
|
||||
# Remove noisy default fields
|
||||
for field in STRIP_VAR_DEFAULTS:
|
||||
var.pop(field, None)
|
||||
# Strip falsy defaults on query/datasource vars (API returns them, export omits them)
|
||||
if var.get("type") in ("query", "datasource"):
|
||||
for field in ("hide", "multi", "skipUrlSync"):
|
||||
if not var.get(field):
|
||||
var.pop(field, None)
|
||||
|
||||
# --- Build __inputs ---
|
||||
inputs = [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus",
|
||||
},
|
||||
]
|
||||
|
||||
if has_expression_datasource(dash):
|
||||
inputs.append(
|
||||
{
|
||||
"name": "DS_EXPRESSION",
|
||||
"label": "Expression",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "__expr__",
|
||||
}
|
||||
)
|
||||
|
||||
inputs.append(
|
||||
{
|
||||
"name": "VAR_INSTANCE_LABEL",
|
||||
"type": "constant",
|
||||
"label": "Instance Label",
|
||||
"value": "job",
|
||||
"description": "",
|
||||
}
|
||||
)
|
||||
|
||||
# --- Build __requires ---
|
||||
requires = []
|
||||
|
||||
if has_expression_datasource(dash):
|
||||
requires.append({"type": "datasource", "id": "__expr__", "version": "1.0.0"})
|
||||
|
||||
panel_types = collect_panel_types(dash.get("panels", []))
|
||||
for pt in sorted(panel_types):
|
||||
requires.append(
|
||||
{
|
||||
"type": "panel",
|
||||
"id": pt,
|
||||
"name": PANEL_TYPE_NAMES.get(pt, pt),
|
||||
"version": "",
|
||||
}
|
||||
)
|
||||
|
||||
requires.append(
|
||||
{"type": "grafana", "id": "grafana", "name": "Grafana", "version": grafana_version}
|
||||
)
|
||||
requires.append(
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
)
|
||||
|
||||
# --- Assemble output (with __inputs/__requires/__elements first) ---
|
||||
output = {
|
||||
"__inputs": inputs,
|
||||
"__elements": {},
|
||||
"__requires": requires,
|
||||
}
|
||||
output.update(dash)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <dashboard-uid>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
uid = sys.argv[1]
|
||||
base_url = os.environ.get("FETCH_GRAFANA_DASHBOARD_URL", "").rstrip("/")
|
||||
token = os.environ.get("FETCH_GRAFANA_DASHBOARD_TOKEN", "")
|
||||
|
||||
if not base_url or not token:
|
||||
print(
|
||||
"Error: FETCH_GRAFANA_DASHBOARD_URL and FETCH_GRAFANA_DASHBOARD_TOKEN env vars required",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
resp = fetch_dashboard(base_url, token, uid)
|
||||
dashboard = resp["dashboard"]
|
||||
|
||||
grafana_version = fetch_grafana_version(base_url)
|
||||
exported = make_exportable(dashboard, grafana_version)
|
||||
print(json.dumps(exported, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
.github/scripts/verify_image_arch.sh
vendored
53
.github/scripts/verify_image_arch.sh
vendored
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verifies that Docker images have the expected architectures.
|
||||
#
|
||||
# Usage:
|
||||
# ./verify_image_arch.sh <targets> <registry> <ethereum_tags>
|
||||
#
|
||||
# Environment:
|
||||
# DRY_RUN=true - Skip actual verification, just print what would be checked.
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
TARGETS="${1:-}"
|
||||
REGISTRY="${2:-}"
|
||||
ETHEREUM_TAGS="${3:-}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
|
||||
verify_image() {
|
||||
local image="$1"
|
||||
shift
|
||||
local expected_archs=("$@")
|
||||
|
||||
echo "Checking $image..."
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [dry-run] Would verify architectures: ${expected_archs[*]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
manifest=$(docker manifest inspect "$image" 2>/dev/null) || {
|
||||
echo "::error::Failed to inspect manifest for $image"
|
||||
return 1
|
||||
}
|
||||
|
||||
for arch in "${expected_archs[@]}"; do
|
||||
if ! echo "$manifest" | jq -e ".manifests[] | select(.platform.architecture == \"$arch\" and .platform.os == \"linux\")" > /dev/null; then
|
||||
echo "::error::Missing architecture $arch for $image"
|
||||
return 1
|
||||
fi
|
||||
echo " ✓ linux/$arch"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ "$TARGETS" == *"nightly"* ]]; then
|
||||
verify_image "${REGISTRY}/reth:nightly" amd64 arm64
|
||||
verify_image "${REGISTRY}/reth:nightly-profiling" amd64
|
||||
verify_image "${REGISTRY}/reth:nightly-edge-profiling" amd64
|
||||
else
|
||||
for tag in $(echo "$ETHEREUM_TAGS" | tr ',' ' '); do
|
||||
verify_image "$tag" amd64 arm64
|
||||
done
|
||||
fi
|
||||
|
||||
echo "All image architectures verified successfully"
|
||||
1000
.github/workflows/bench-scheduled.yml
vendored
1000
.github/workflows/bench-scheduled.yml
vendored
File diff suppressed because it is too large
Load Diff
1403
.github/workflows/bench.yml
vendored
1403
.github/workflows/bench.yml
vendored
File diff suppressed because it is too large
Load Diff
8
.github/workflows/book.yml
vendored
8
.github/workflows/book.yml
vendored
@@ -15,7 +15,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-8
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -47,10 +47,10 @@ jobs:
|
||||
echo "Vocs Build Complete"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v6
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: "./docs/vocs/docs/dist"
|
||||
|
||||
@@ -74,4 +74,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
65
.github/workflows/check-alloy.yml
vendored
65
.github/workflows/check-alloy.yml
vendored
@@ -1,65 +0,0 @@
|
||||
# Checks reth compilation against alloy branches to detect breaking changes.
|
||||
# Run on-demand via workflow_dispatch.
|
||||
|
||||
name: Check Alloy Breaking Changes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
alloy_branch:
|
||||
description: 'Branch/rev for alloy-rs/alloy (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
alloy_evm_branch:
|
||||
description: 'Branch/rev for alloy-rs/evm (alloy-evm, alloy-op-evm) (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
op_alloy_branch:
|
||||
description: 'Branch/rev for alloy-rs/op-alloy (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check compilation with patched alloy
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Apply alloy patches
|
||||
run: |
|
||||
ARGS=""
|
||||
if [ -n "${{ inputs.alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --alloy ${{ inputs.alloy_branch }}"
|
||||
fi
|
||||
if [ -n "${{ inputs.alloy_evm_branch }}" ]; then
|
||||
ARGS="$ARGS --evm ${{ inputs.alloy_evm_branch }}"
|
||||
fi
|
||||
if [ -n "${{ inputs.op_alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --op ${{ inputs.op_alloy_branch }}"
|
||||
fi
|
||||
|
||||
if [ -z "$ARGS" ]; then
|
||||
echo "No branches specified, nothing to patch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./scripts/patch-alloy.sh $ARGS
|
||||
|
||||
echo "=== Final patch section ==="
|
||||
tail -50 Cargo.toml
|
||||
|
||||
- name: Check workspace
|
||||
run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
3
.github/workflows/compact.yml
vendored
3
.github/workflows/compact.yml
vendored
@@ -18,11 +18,12 @@ env:
|
||||
name: compact-codec
|
||||
jobs:
|
||||
compact-codec:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
bin:
|
||||
- cargo run --bin reth --features "dev"
|
||||
- cargo run --bin op-reth --features "dev" --manifest-path crates/optimism/bin/Cargo.toml
|
||||
steps:
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
3
.github/workflows/dependencies.yml
vendored
3
.github/workflows/dependencies.yml
vendored
@@ -15,7 +15,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
uses: tempoxyz/ci/.github/workflows/cargo-update-pr.yml@main
|
||||
uses: ithacaxyz/ci/.github/workflows/cargo-update-pr.yml@main
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
54
.github/workflows/docker-git.yml
vendored
Normal file
54
.github/workflows/docker-git.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Publishes the Docker image, only to be used with `workflow_dispatch`. The
|
||||
# images from this workflow will be tagged with the git sha of the branch used
|
||||
# and will NOT tag it as `latest`.
|
||||
|
||||
name: docker-git
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
REPO_NAME: ${{ github.repository_owner }}/reth
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and push
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'Build and push the git-sha-tagged reth image'
|
||||
command: 'make PROFILE=maxperf GIT_SHA=$GIT_SHA docker-build-push-git-sha'
|
||||
- name: 'Build and push the git-sha-tagged op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME GIT_SHA=$GIT_SHA PROFILE=maxperf op-docker-build-push-git-sha'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
61
.github/workflows/docker-nightly.yml
vendored
Normal file
61
.github/workflows/docker-nightly.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Publishes the nightly Docker image.
|
||||
|
||||
name: docker-nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
env:
|
||||
REPO_NAME: ${{ github.repository_owner }}/reth
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and push
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'Build and push the nightly reth image'
|
||||
command: 'make PROFILE=maxperf docker-build-push-nightly'
|
||||
- name: 'Build and push the nightly profiling reth image'
|
||||
command: 'make PROFILE=profiling docker-build-push-nightly-profiling'
|
||||
- name: 'Build and push the nightly op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push-nightly'
|
||||
- name: 'Build and push the nightly profiling op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=profiling op-docker-build-push-nightly-profiling'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Remove bloatware
|
||||
uses: laverdet/remove-bloatware@v1.0.0
|
||||
with:
|
||||
docker: true
|
||||
lang: rust
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
30
.github/workflows/docker-tag-latest.yml
vendored
30
.github/workflows/docker-tag-latest.yml
vendored
@@ -14,6 +14,12 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
tag_op_reth:
|
||||
description: 'Tag op-reth image as latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
|
||||
@@ -41,3 +47,27 @@ jobs:
|
||||
- name: Push reth latest tag
|
||||
run: |
|
||||
docker push ghcr.io/${{ github.repository_owner }}/reth:latest
|
||||
|
||||
tag-op-reth-latest:
|
||||
name: Tag op-reth as latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ inputs.tag_op_reth }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Pull op-reth release image
|
||||
run: |
|
||||
docker pull ghcr.io/${{ github.repository_owner }}/op-reth:${{ inputs.version }}
|
||||
|
||||
- name: Tag op-reth as latest
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository_owner }}/op-reth:${{ inputs.version }} ghcr.io/${{ github.repository_owner }}/op-reth:latest
|
||||
|
||||
- name: Push op-reth latest tag
|
||||
run: |
|
||||
docker push ghcr.io/${{ github.repository_owner }}/op-reth:latest
|
||||
|
||||
82
.github/workflows/docker-test.yml
vendored
82
.github/workflows/docker-test.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: Build test Docker image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
hive_target:
|
||||
required: true
|
||||
type: string
|
||||
description: "Docker bake target to build (e.g. hive)"
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "artifacts"
|
||||
description: "Name for the uploaded artifact"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: mkdir -p artifacts
|
||||
|
||||
- name: Get git info
|
||||
id: git
|
||||
run: |
|
||||
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "describe=$(git describe --always --tags)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Detect fork
|
||||
id: fork
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Depot build (upstream only)
|
||||
- name: Set up Depot CLI
|
||||
if: steps.fork.outputs.is_fork == 'false'
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Build reth image (Depot)
|
||||
if: steps.fork.outputs.is_fork == 'false'
|
||||
uses: depot/bake-action@v1
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
files: docker-bake.hcl
|
||||
targets: ${{ inputs.hive_target }}
|
||||
push: false
|
||||
|
||||
# Docker build (forks)
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.fork.outputs.is_fork == 'true'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build reth image (Docker)
|
||||
if: steps.fork.outputs.is_fork == 'true'
|
||||
uses: docker/bake-action@v6
|
||||
env:
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: ${{ inputs.hive_target }}
|
||||
push: false
|
||||
set: |
|
||||
*.dockerfile=Dockerfile
|
||||
|
||||
- name: Upload reth image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
192
.github/workflows/docker.yml
vendored
192
.github/workflows/docker.yml
vendored
@@ -1,9 +1,4 @@
|
||||
# Publishes Docker images.
|
||||
#
|
||||
# Triggers:
|
||||
# - Push tag v*: builds release (RC or latest)
|
||||
# - Schedule: builds nightly + profiling
|
||||
# - Manual: builds git-sha or nightly
|
||||
# Publishes the Docker image.
|
||||
|
||||
name: docker
|
||||
|
||||
@@ -11,135 +6,84 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
description: "Build type"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- git-sha
|
||||
- nightly
|
||||
default: git-sha
|
||||
dry_run:
|
||||
description: "Skip pushing images (dry run)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
name: Build Docker images
|
||||
build-rc:
|
||||
if: contains(github.ref, '-rc')
|
||||
name: build and push as release candidate
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: "Build and push reth image"
|
||||
command: "make IMAGE_NAME=$IMAGE_NAME DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME PROFILE=maxperf docker-build-push"
|
||||
- name: "Build and push op-reth image"
|
||||
command: "make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get git info for vergen
|
||||
id: git
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "describe=$(git describe --always --tags)" >> "$GITHUB_OUTPUT"
|
||||
echo "dirty=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Determine build parameters
|
||||
id: params
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
REGISTRY="ghcr.io/${{ github.repository_owner }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Add 'latest' tag for non-RC releases
|
||||
if [[ ! "$VERSION" =~ -rc ]]; then
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${VERSION},${REGISTRY}/reth:latest" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "ethereum_set<<EOF"
|
||||
echo "ethereum.tags=${REGISTRY}/reth:${VERSION}"
|
||||
echo "ethereum.tags=${REGISTRY}/reth:latest"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
|
||||
echo "targets=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
|
||||
else
|
||||
# git-sha build
|
||||
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build and push images
|
||||
uses: depot/bake-action@v1
|
||||
env:
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
VERGEN_GIT_DIRTY: ${{ steps.git.outputs.dirty }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
files: docker-bake.hcl
|
||||
targets: ${{ steps.params.outputs.targets }}
|
||||
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
|
||||
set: |
|
||||
${{ steps.params.outputs.ethereum_set }}
|
||||
|
||||
- name: Verify image architectures
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
./.github/scripts/verify_image_arch.sh \
|
||||
"${{ steps.params.outputs.targets }}" \
|
||||
"ghcr.io/${{ github.repository_owner }}" \
|
||||
"${{ steps.params.outputs.ethereum_tags }}"
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
|
||||
notify:
|
||||
name: Notify on failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: failure() && github.event_name == 'schedule'
|
||||
build:
|
||||
if: ${{ !contains(github.ref, '-rc') }}
|
||||
name: build and push as latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: "Build and push reth image"
|
||||
command: "make IMAGE_NAME=$IMAGE_NAME DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME PROFILE=maxperf docker-build-push-latest"
|
||||
- name: "Build and push op-reth image"
|
||||
command: "make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push-latest"
|
||||
steps:
|
||||
- name: Slack Webhook Action
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_COLOR: danger
|
||||
SLACK_ICON_EMOJI: ":rotating_light:"
|
||||
SLACK_USERNAME: "GitHub Actions"
|
||||
SLACK_TITLE: ":rotating_light: Nightly Docker Build Failed"
|
||||
SLACK_MESSAGE: |
|
||||
The scheduled nightly Docker build failed.
|
||||
|
||||
*Commit:* `${{ github.sha }}`
|
||||
*Branch:* `${{ github.ref_name }}`
|
||||
*Run:* <https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}|View logs>
|
||||
|
||||
*Action required:* Re-run the workflow or investigate the build failure.
|
||||
|
||||
<@U0AAA8F0JEM> investigate and re-run if flaky
|
||||
SLACK_FOOTER: "paradigmxyz/reth · docker.yml"
|
||||
MSG_MINIMAL: true
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
|
||||
26
.github/workflows/e2e.yml
vendored
26
.github/workflows/e2e.yml
vendored
@@ -20,7 +20,7 @@ concurrency:
|
||||
jobs:
|
||||
test:
|
||||
name: e2e-testsuite
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
timeout-minutes: 90
|
||||
@@ -35,34 +35,12 @@ jobs:
|
||||
- name: Run e2e tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--no-fail-fast \
|
||||
--locked --features "asm-keccak" \
|
||||
--workspace \
|
||||
--exclude 'example-*' \
|
||||
--exclude 'exex-subscription' \
|
||||
--exclude 'reth-bench' \
|
||||
--exclude 'ef-tests' \
|
||||
--exclude 'op-reth' \
|
||||
--exclude 'reth' \
|
||||
-E 'binary(e2e_testsuite)'
|
||||
|
||||
rocksdb:
|
||||
name: e2e-rocksdb
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run RocksDB e2e tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--no-fail-fast \
|
||||
--locked \
|
||||
-p reth-e2e-test-utils \
|
||||
-E 'binary(rocksdb)'
|
||||
|
||||
62
.github/workflows/fetch-grafana-dashboard.yml
vendored
62
.github/workflows/fetch-grafana-dashboard.yml
vendored
@@ -1,62 +0,0 @@
|
||||
name: Fetch Grafana Dashboard
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dashboard_uid:
|
||||
description: "Grafana dashboard UID to export"
|
||||
required: true
|
||||
default: "2k8BXz24x"
|
||||
target_path:
|
||||
description: "Target file path in the repo (e.g. etc/grafana/dashboards/overview.json)"
|
||||
required: true
|
||||
default: "etc/grafana/dashboards/overview.json"
|
||||
|
||||
jobs:
|
||||
fetch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Fetch dashboard from Grafana
|
||||
env:
|
||||
FETCH_GRAFANA_DASHBOARD_URL: ${{ secrets.FETCH_GRAFANA_DASHBOARD_URL }}
|
||||
FETCH_GRAFANA_DASHBOARD_TOKEN: ${{ secrets.FETCH_GRAFANA_DASHBOARD_TOKEN }}
|
||||
run: |
|
||||
python3 .github/scripts/fetch-grafana-dashboard.py "${{ inputs.dashboard_uid }}" \
|
||||
> "${{ inputs.target_path }}"
|
||||
|
||||
- name: Check for changes
|
||||
id: diff
|
||||
run: |
|
||||
if git diff --quiet "${{ inputs.target_path }}"; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes detected."
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.diff.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TARGET="${{ inputs.target_path }}"
|
||||
FILENAME="$(basename "$TARGET")"
|
||||
BRANCH="chore/sync-grafana-${FILENAME%.*}-$(date +%Y%m%d-%H%M%S)"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
git add "$TARGET"
|
||||
git commit -m "chore: update Grafana dashboard ${FILENAME}"
|
||||
git push origin "$BRANCH"
|
||||
gh pr create \
|
||||
--title "chore: update Grafana dashboard ${FILENAME}" \
|
||||
--body "Automated export from Grafana (dashboard UID: \`${{ inputs.dashboard_uid }}\`, target: \`${TARGET}\`)."
|
||||
25
.github/workflows/grafana.yml
vendored
25
.github/workflows/grafana.yml
vendored
@@ -11,22 +11,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Validate dashboard format
|
||||
- name: Check for ${DS_PROMETHEUS} in overview.json
|
||||
run: |
|
||||
python3 -c "
|
||||
import json, sys
|
||||
with open('etc/grafana/dashboards/overview.json') as f:
|
||||
d = json.load(f)
|
||||
errors = []
|
||||
if '__inputs' not in d:
|
||||
errors.append('missing __inputs')
|
||||
if '__requires' not in d:
|
||||
errors.append('missing __requires')
|
||||
if d.get('id') is not None:
|
||||
errors.append('contains internal id field — use export-dashboard.py')
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print('✓ overview.json is a valid exported dashboard')
|
||||
"
|
||||
if grep -Fn '${DS_PROMETHEUS}' etc/grafana/dashboards/overview.json; then
|
||||
echo "Error: overview.json contains '\${DS_PROMETHEUS}' placeholder"
|
||||
echo "Please replace it with '\${datasource}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ overview.json does not contain '\${DS_PROMETHEUS}' placeholder"
|
||||
|
||||
42
.github/workflows/hive.yml
vendored
42
.github/workflows/hive.yml
vendored
@@ -5,7 +5,7 @@ name: hive
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: "0 */6 * * *"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -15,17 +15,16 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-reth:
|
||||
uses: ./.github/workflows/docker-test.yml
|
||||
prepare-reth:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
hive_target: hive
|
||||
artifact_name: "reth"
|
||||
secrets: inherit
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
@@ -48,11 +47,11 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ./hive_assets
|
||||
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
|
||||
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/assets/hive/build_simulators.sh') }}
|
||||
|
||||
- name: Build hive assets
|
||||
if: steps.cache-hive.outputs.cache-hit != 'true'
|
||||
run: .github/scripts/hive/build_simulators.sh
|
||||
run: .github/assets/hive/build_simulators.sh
|
||||
|
||||
- name: Load cached Docker images
|
||||
if: steps.cache-hive.outputs.cache-hit == 'true'
|
||||
@@ -68,7 +67,7 @@ jobs:
|
||||
chmod +x hive
|
||||
|
||||
- name: Upload hive assets
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: hive_assets
|
||||
path: ./hive_assets
|
||||
@@ -86,7 +85,7 @@ jobs:
|
||||
- sim: devp2p
|
||||
limit: discv4
|
||||
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
|
||||
# action on our side, remove from here when we get unexpected passes on these tests
|
||||
# action on our side, remove from here when we get unxpected passes on these tests
|
||||
# - sim: devp2p
|
||||
# limit: eth
|
||||
# include:
|
||||
@@ -176,11 +175,10 @@ jobs:
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/paris.*
|
||||
needs:
|
||||
- build-reth
|
||||
- prepare-reth
|
||||
- prepare-hive
|
||||
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# 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' }}
|
||||
name: run ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
@@ -189,19 +187,19 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: hive_assets
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: reth
|
||||
name: artifacts
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker images
|
||||
run: .github/scripts/hive/load_images.sh
|
||||
run: .github/assets/hive/load_images.sh
|
||||
|
||||
- name: Move hive binary
|
||||
run: |
|
||||
@@ -229,11 +227,11 @@ jobs:
|
||||
FILTER="/"
|
||||
fi
|
||||
echo "filter: $FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER"
|
||||
.github/assets/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER"
|
||||
|
||||
- name: Parse hive output
|
||||
run: |
|
||||
find hivetests/workspace/logs -type f -name "*.json" ! -name "hive.json" | xargs -I {} python .github/scripts/hive/parse.py {} --exclusion .github/scripts/hive/expected_failures.yaml --ignored .github/scripts/hive/ignored_tests.yaml
|
||||
find hivetests/workspace/logs -type f -name "*.json" ! -name "hive.json" | xargs -I {} python .github/assets/hive/parse.py {} --exclusion .github/assets/hive/expected_failures.yaml --ignored .github/assets/hive/ignored_tests.yaml
|
||||
|
||||
- name: Print simulator output
|
||||
if: ${{ failure() }}
|
||||
@@ -254,4 +252,4 @@ jobs:
|
||||
env:
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_HIVE_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
19
.github/workflows/integration.yml
vendored
19
.github/workflows/integration.yml
vendored
@@ -24,31 +24,36 @@ jobs:
|
||||
test:
|
||||
name: test / ${{ matrix.network }}
|
||||
if: github.event_name != 'schedule'
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
strategy:
|
||||
matrix:
|
||||
network: ["ethereum"]
|
||||
network: ["ethereum", "optimism"]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Geth
|
||||
run: .github/scripts/install_geth.sh
|
||||
run: .github/assets/install_geth.sh
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run tests
|
||||
- if: matrix.network == 'ethereum'
|
||||
name: Run tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--no-fail-fast \
|
||||
--locked --features "asm-keccak ${{ matrix.network }}" \
|
||||
--workspace --exclude ef-tests \
|
||||
-E "kind(test) and not binary(e2e_testsuite)"
|
||||
- if: matrix.network == 'optimism'
|
||||
name: Run tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--locked -p reth-optimism-node
|
||||
|
||||
integration-success:
|
||||
name: integration success
|
||||
@@ -64,7 +69,7 @@ jobs:
|
||||
|
||||
era-files:
|
||||
name: era1 file integration tests once a day
|
||||
if: github.event_name == 'schedule' && github.repository == 'paradigmxyz/reth'
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -76,4 +81,4 @@ jobs:
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: run era1 files integration tests
|
||||
run: cargo nextest run --no-fail-fast --release --package reth-era --test it -- --ignored
|
||||
run: cargo nextest run --release --package reth-era --test it -- --ignored
|
||||
|
||||
95
.github/workflows/kurtosis-op.yml
vendored
Normal file
95
.github/workflows/kurtosis-op.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
# Runs simple OP stack setup in Kurtosis
|
||||
|
||||
name: kurtosis-op
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
image_tag: ghcr.io/paradigmxyz/op-reth:kurtosis-ci
|
||||
binary_name: op-reth
|
||||
cargo_features: asm-keccak
|
||||
cargo_package: crates/optimism/bin/Cargo.toml
|
||||
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
name: run kurtosis
|
||||
runs-on: depot-ubuntu-latest
|
||||
needs:
|
||||
- prepare-reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: artifacts
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker image
|
||||
run: |
|
||||
docker load -i /tmp/reth_image.tar &
|
||||
wait
|
||||
docker image ls -a
|
||||
|
||||
- name: Install Foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
|
||||
- name: Run kurtosis
|
||||
run: |
|
||||
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
|
||||
sudo apt update
|
||||
sudo apt install kurtosis-cli
|
||||
kurtosis engine start
|
||||
kurtosis run --enclave op-devnet github.com/ethpandaops/optimism-package --args-file .github/assets/kurtosis_op_network_params.yaml
|
||||
ENCLAVE_ID=$(curl http://127.0.0.1:9779/api/enclaves | jq --raw-output 'keys[0]')
|
||||
GETH_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-2151908-node0-op-geth".public_ports.rpc.number')
|
||||
RETH_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-2151908-node1-op-reth".public_ports.rpc.number')
|
||||
echo "GETH_RPC=http://127.0.0.1:$GETH_PORT" >> $GITHUB_ENV
|
||||
echo "RETH_RPC=http://127.0.0.1:$RETH_PORT" >> $GITHUB_ENV
|
||||
|
||||
- name: Assert that clients advance
|
||||
run: |
|
||||
for i in {1..100}; do
|
||||
sleep 5
|
||||
BLOCK_GETH=$(cast bn --rpc-url $GETH_RPC)
|
||||
BLOCK_RETH=$(cast bn --rpc-url $RETH_RPC)
|
||||
|
||||
if [ $BLOCK_GETH -ge 100 ] && [ $BLOCK_RETH -ge 100 ] ; then exit 0; fi
|
||||
echo "Waiting for clients to advance..., Reth: $BLOCK_RETH Geth: $BLOCK_GETH"
|
||||
done
|
||||
kurtosis service logs -a op-devnet op-el-2151908-2-op-reth-op-node-op-kurtosis
|
||||
kurtosis service logs -a op-devnet op-cl-2151908-2-op-node-op-reth-op-kurtosis
|
||||
exit 1
|
||||
|
||||
notify-on-error:
|
||||
needs: test
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Webhook Action
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
19
.github/workflows/kurtosis.yml
vendored
19
.github/workflows/kurtosis.yml
vendored
@@ -5,7 +5,7 @@ name: kurtosis
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: "0 */6 * * *"
|
||||
|
||||
push:
|
||||
tags:
|
||||
@@ -19,28 +19,27 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-reth:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
uses: ./.github/workflows/docker-test.yml
|
||||
prepare-reth:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
hive_target: kurtosis
|
||||
secrets: inherit
|
||||
image_tag: ghcr.io/paradigmxyz/reth:kurtosis-ci
|
||||
binary_name: reth
|
||||
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
name: run kurtosis
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
needs:
|
||||
- build-reth
|
||||
- prepare-reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: artifacts
|
||||
path: /tmp
|
||||
@@ -66,4 +65,4 @@ jobs:
|
||||
env:
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_HIVE_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
4
.github/workflows/label-pr.yml
vendored
4
.github/workflows/label-pr.yml
vendored
@@ -16,8 +16,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Label PRs
|
||||
uses: actions/github-script@v9
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const label_pr = require('./.github/scripts/label_pr.js')
|
||||
const label_pr = require('./.github/assets/label_pr.js')
|
||||
await label_pr({github, context})
|
||||
|
||||
68
.github/workflows/lint.yml
vendored
68
.github/workflows/lint.yml
vendored
@@ -13,7 +13,7 @@ env:
|
||||
jobs:
|
||||
clippy-binaries:
|
||||
name: clippy binaries / ${{ matrix.type }}
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
clippy:
|
||||
name: clippy
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
wasm:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -76,10 +76,10 @@ jobs:
|
||||
- name: Run Wasm checks
|
||||
run: |
|
||||
sudo apt update && sudo apt install gcc-multilib
|
||||
.github/scripts/check_wasm.sh
|
||||
.github/assets/check_wasm.sh
|
||||
|
||||
riscv:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -94,11 +94,11 @@ jobs:
|
||||
cache-on-failure: true
|
||||
- uses: dcarbone/install-jq-action@v3
|
||||
- name: Run RISC-V checks
|
||||
run: .github/scripts/check_rv32imac.sh
|
||||
run: .github/assets/check_rv32imac.sh
|
||||
|
||||
crate-checks:
|
||||
name: crate-checks (${{ matrix.partition }}/${{ matrix.total_partitions }})
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
strategy:
|
||||
matrix:
|
||||
partition: [1, 2, 3]
|
||||
@@ -117,25 +117,30 @@ jobs:
|
||||
|
||||
msrv:
|
||||
name: MSRV
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- binary: reth
|
||||
- binary: op-reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.93" # MSRV
|
||||
toolchain: "1.88" # MSRV
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo build --bin reth --workspace
|
||||
- run: cargo build --bin "${{ matrix.binary }}" --workspace
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
docs:
|
||||
name: docs
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -153,7 +158,7 @@ jobs:
|
||||
|
||||
fmt:
|
||||
name: fmt
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -167,7 +172,7 @@ jobs:
|
||||
|
||||
udeps:
|
||||
name: udeps
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -182,7 +187,7 @@ jobs:
|
||||
|
||||
book:
|
||||
name: book
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -193,9 +198,10 @@ jobs:
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo build --bin reth --workspace
|
||||
- run: cargo build --bin op-reth --workspace
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
- run: ./docs/cli/update.sh target/debug/reth
|
||||
- run: ./docs/cli/update.sh target/debug/reth target/debug/op-reth
|
||||
- name: Check docs changes
|
||||
run: git diff --exit-code
|
||||
|
||||
@@ -237,16 +243,41 @@ jobs:
|
||||
- name: Ensure no arbitrary or proptest dependency on default build
|
||||
run: cargo tree --package reth -e=features,no-dev | grep -Eq "arbitrary|proptest" && exit 1 || exit 0
|
||||
|
||||
# Checks that selected crates can compile with power set of features
|
||||
features:
|
||||
name: features
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@clippy
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: cargo install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
- run: |
|
||||
cargo hack check \
|
||||
--package reth-codecs \
|
||||
--package reth-primitives-traits \
|
||||
--package reth-primitives \
|
||||
--feature-powerset \
|
||||
--depth 2
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
# Check crates correctly propagate features
|
||||
feature-propagation:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: taiki-e/cache-cargo-install-action@v3
|
||||
- uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: zepter
|
||||
- name: Eagerly pull dependencies
|
||||
@@ -254,7 +285,7 @@ jobs:
|
||||
- run: zepter run check
|
||||
|
||||
deny:
|
||||
uses: tempoxyz/ci/.github/workflows/deny.yml@main
|
||||
uses: ithacaxyz/ci/.github/workflows/deny.yml@main
|
||||
|
||||
lint-success:
|
||||
name: lint success
|
||||
@@ -272,6 +303,7 @@ jobs:
|
||||
- typos
|
||||
- grafana
|
||||
- no-test-deps
|
||||
- features
|
||||
- feature-propagation
|
||||
- deny
|
||||
timeout-minutes: 30
|
||||
|
||||
30
.github/workflows/pr-audit.yml
vendored
30
.github/workflows/pr-audit.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Pull request audit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'cyclops'
|
||||
steps:
|
||||
- name: Publish event
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "${{ secrets.EVENTS_KEY }}" > ${{ runner.temp }}/key
|
||||
echo "${{ secrets.EVENTS_CERT }}" > ${{ runner.temp }}/cert
|
||||
|
||||
curl -sf -o /dev/null -X POST ${{ secrets.EVENTS_ARGS }} \
|
||||
-H "Content-Type: application/json" \
|
||||
--key ${{ runner.temp }}/key \
|
||||
--cert ${{ runner.temp }}/cert \
|
||||
-d '{
|
||||
"repository": "${{ github.repository }}",
|
||||
"event": "pr_audit",
|
||||
"data": {
|
||||
"pr_number": ${{ github.event.pull_request.number }},
|
||||
"sha": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
}'
|
||||
56
.github/workflows/prepare-reth.yml
vendored
Normal file
56
.github/workflows/prepare-reth.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Prepare Reth Image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
required: true
|
||||
type: string
|
||||
description: "Docker image tag to use"
|
||||
binary_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "reth"
|
||||
description: "Binary name to build (reth or op-reth)"
|
||||
cargo_features:
|
||||
required: false
|
||||
type: string
|
||||
default: "asm-keccak"
|
||||
description: "Cargo features to enable"
|
||||
cargo_package:
|
||||
required: false
|
||||
type: string
|
||||
description: "Optional cargo package path"
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: mkdir artifacts
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and export reth image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: .github/assets/hive/Dockerfile
|
||||
tags: ${{ inputs.image_tag }}
|
||||
outputs: type=docker,dest=./artifacts/reth_image.tar
|
||||
build-args: |
|
||||
CARGO_BIN=${{ inputs.binary_name }}
|
||||
MANIFEST_PATH=${{ inputs.cargo_package }}
|
||||
FEATURES=${{ inputs.cargo_features }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Upload reth image
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./artifacts
|
||||
2
.github/workflows/release-reproducible.yml
vendored
2
.github/workflows/release-reproducible.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
@@ -17,9 +17,11 @@ on:
|
||||
env:
|
||||
REPO_NAME: ${{ github.repository_owner }}/reth
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
REPRODUCIBLE_IMAGE_NAME: ${{ github.repository_owner }}/reth-reproducible
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/reth
|
||||
DOCKER_OP_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
@@ -37,7 +39,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version
|
||||
run: echo "VERSION=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT
|
||||
run: echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
id: extract_version
|
||||
outputs:
|
||||
VERSION: ${{ steps.extract_version.outputs.VERSION }}
|
||||
@@ -73,27 +75,31 @@ jobs:
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: "-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"
|
||||
native: true
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-24.04-arm
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: ""
|
||||
native: true
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-14
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: "-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-14
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
rustflags: ""
|
||||
- target: x86_64-pc-windows-gnu
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: false
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
os: ubuntu-24.04
|
||||
profile: maxperf
|
||||
allow_fail: true
|
||||
build:
|
||||
- command: build
|
||||
binary: reth
|
||||
- command: op-build
|
||||
binary: op-reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
@@ -102,10 +108,9 @@ jobs:
|
||||
target: ${{ matrix.configs.target }}
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- name: Install cross main
|
||||
if: ${{ !matrix.configs.native }}
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --locked --git https://github.com/cross-rs/cross
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
@@ -117,18 +122,12 @@ jobs:
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Reth
|
||||
env:
|
||||
CC: clang
|
||||
run: |
|
||||
if [ "${{ matrix.configs.native }}" = "true" ]; then
|
||||
make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-native-${{ matrix.configs.target }}
|
||||
else
|
||||
make PROFILE=${{ matrix.configs.profile }} EXTRA_RUSTFLAGS="${{ matrix.configs.rustflags }}" ${{ matrix.build.command }}-${{ matrix.configs.target }}
|
||||
fi
|
||||
run: make PROFILE=${{ matrix.configs.profile }} ${{ matrix.build.command }}-${{ matrix.configs.target }}
|
||||
- name: Move binary
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mv "target/${{ matrix.configs.target }}/${{ matrix.configs.profile }}/${{ matrix.build.binary }}" ./artifacts
|
||||
[[ "${{ matrix.configs.target }}" == *windows* ]] && ext=".exe"
|
||||
mv "target/${{ matrix.configs.target }}/${{ matrix.configs.profile }}/${{ matrix.build.binary }}${ext}" ./artifacts
|
||||
|
||||
- name: Configure GPG and create artifacts
|
||||
env:
|
||||
@@ -145,19 +144,18 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
|
||||
- name: Upload signature
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
|
||||
|
||||
draft-release:
|
||||
name: draft release
|
||||
runs-on: ubuntu-latest
|
||||
@@ -175,7 +173,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
- name: Generate full changelog
|
||||
id: changelog
|
||||
run: |
|
||||
@@ -195,7 +193,7 @@ jobs:
|
||||
fi
|
||||
|
||||
body=$(cat <<- "ENDBODY"
|
||||

|
||||

|
||||
|
||||
## Testing Checklist (DELETE ME)
|
||||
|
||||
@@ -246,9 +244,21 @@ jobs:
|
||||
|:---:|:---:|:---:|:---|
|
||||
| <img src="https://www.svgrepo.com/download/473700/linux.svg" width="50"/> | x86_64 | [reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/473700/linux.svg" width="50"/> | aarch64 | [reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/513083/windows-174.svg" width="50"/> | x86_64 | [reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/511330/apple-173.svg" width="50"/> | x86_64 | [reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/511330/apple-173.svg" width="50"/> | aarch64 | [reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/473589/docker.svg" width="50"/> | Docker | [${{ env.IMAGE_NAME }}](${{ env.DOCKER_IMAGE_NAME_URL }}) | - |
|
||||
|
||||
### OP-Reth
|
||||
|
||||
| System | Architecture | Binary | PGP Signature |
|
||||
|:---:|:---:|:---:|:---|
|
||||
| <img src="https://www.svgrepo.com/download/473700/linux.svg" width="50"/> | x86_64 | [op-reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/473700/linux.svg" width="50"/> | aarch64 | [op-reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/513083/windows-174.svg" width="50"/> | x86_64 | [op-reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/511330/apple-173.svg" width="50"/> | x86_64 | [op-reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/511330/apple-173.svg" width="50"/> | aarch64 | [op-reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/op-reth-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) |
|
||||
| <img src="https://www.svgrepo.com/download/473589/docker.svg" width="50"/> | Docker | [${{ env.OP_IMAGE_NAME }}](${{ env.DOCKER_OP_IMAGE_NAME_URL }}) | - |
|
||||
ENDBODY
|
||||
)
|
||||
assets=()
|
||||
|
||||
7
.github/workflows/reproducible-build.yml
vendored
7
.github/workflows/reproducible-build.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
name: build reproducible binaries
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
|
||||
|
||||
- name: Upload the hash
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: checksum-${{ matrix.machine }}
|
||||
path: |
|
||||
@@ -56,12 +55,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts from machine-1
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: checksum-machine-1
|
||||
path: machine-1/
|
||||
- name: Download artifacts from machine-2
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: checksum-machine-2
|
||||
path: machine-2/
|
||||
|
||||
19
.github/workflows/stage.yml
vendored
19
.github/workflows/stage.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
name: stage-run-test
|
||||
# Only run stage commands test in merge groups
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
env:
|
||||
RUST_LOG: info,sync=error
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
cache-on-failure: true
|
||||
- name: Build reth
|
||||
run: |
|
||||
cargo install --locked --path bin/reth
|
||||
cargo install --features asm-keccak,jemalloc --path bin/reth
|
||||
- name: Run headers stage
|
||||
run: |
|
||||
reth stage run headers --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
@@ -51,12 +51,15 @@ jobs:
|
||||
- name: Run execution stage
|
||||
run: |
|
||||
reth stage run execution --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
# NOTE: account-hashing, storage-hashing, and hashing stages are omitted.
|
||||
# With storage v2 (now default), these stages are no-ops because the
|
||||
# execution stage writes directly to HashedAccounts/HashedStorages.
|
||||
# Running them here is harmful: `stage run` unwinds before executing,
|
||||
# and the unwind reverts the hashed state that execution wrote, but
|
||||
# the no-op execute never restores it — causing merkle to fail.
|
||||
- name: Run account-hashing stage
|
||||
run: |
|
||||
reth stage run account-hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run storage hashing stage
|
||||
run: |
|
||||
reth stage run storage-hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run hashing stage
|
||||
run: |
|
||||
reth stage run hashing --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
- name: Run merkle stage
|
||||
run: |
|
||||
reth stage run merkle --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
9
.github/workflows/sync-era.yml
vendored
9
.github/workflows/sync-era.yml
vendored
@@ -17,9 +17,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
name: sync (${{ matrix.chain.bin }})
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
env:
|
||||
RUST_LOG: info,sync=error
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -33,6 +32,12 @@ jobs:
|
||||
tip: "0x91c90676cab257a59cd956d7cb0bceb9b1a71d79755c23c7277a0697ccfaf8c4"
|
||||
block: 100000
|
||||
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
|
||||
- build: install-op
|
||||
bin: op-reth
|
||||
chain: base
|
||||
tip: "0xbb9b85352c7ebca6ba8efc63bd66cecd038c92ec8ebd02e153a3e0b197e672b7"
|
||||
block: 10000
|
||||
unwind-target: "0x118a6e922a8c6cab221fc5adfe5056d2b72d58c6580e9c5629de55299e2cf8de"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
|
||||
9
.github/workflows/sync.yml
vendored
9
.github/workflows/sync.yml
vendored
@@ -17,9 +17,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
name: sync (${{ matrix.chain.bin }})
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
env:
|
||||
RUST_LOG: info,sync=error
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -33,6 +32,12 @@ jobs:
|
||||
tip: "0x91c90676cab257a59cd956d7cb0bceb9b1a71d79755c23c7277a0697ccfaf8c4"
|
||||
block: 100000
|
||||
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
|
||||
- build: install-op
|
||||
bin: op-reth
|
||||
chain: base
|
||||
tip: "0xbb9b85352c7ebca6ba8efc63bd66cecd038c92ec8ebd02e153a3e0b197e672b7"
|
||||
block: 10000
|
||||
unwind-target: "0x118a6e922a8c6cab221fc5adfe5056d2b72d58c6580e9c5629de55299e2cf8de"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
|
||||
33
.github/workflows/unit.yml
vendored
33
.github/workflows/unit.yml
vendored
@@ -19,17 +19,29 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test / ${{ matrix.type }}
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
|
||||
name: test / ${{ matrix.type }} (${{ matrix.partition }}/${{ matrix.total_partitions }})
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ethereum]
|
||||
include:
|
||||
- type: ethereum
|
||||
features: asm-keccak ethereum
|
||||
exclude_args: ""
|
||||
args: --features "asm-keccak ethereum" --locked
|
||||
partition: 1
|
||||
total_partitions: 2
|
||||
- type: ethereum
|
||||
args: --features "asm-keccak ethereum" --locked
|
||||
partition: 2
|
||||
total_partitions: 2
|
||||
- type: optimism
|
||||
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
|
||||
partition: 1
|
||||
total_partitions: 2
|
||||
- type: optimism
|
||||
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
|
||||
partition: 2
|
||||
total_partitions: 2
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -47,15 +59,14 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--no-fail-fast \
|
||||
--features "${{ matrix.features }}" --locked \
|
||||
${{ matrix.exclude_args }} --workspace \
|
||||
${{ matrix.args }} --workspace \
|
||||
--exclude ef-tests --no-tests=warn \
|
||||
--partition hash:${{ matrix.partition }}/2 \
|
||||
-E "!kind(test) and not binary(e2e_testsuite)"
|
||||
|
||||
state:
|
||||
name: Ethereum state tests
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
env:
|
||||
RUST_LOG: info,sync=error
|
||||
RUST_BACKTRACE: 1
|
||||
@@ -86,11 +97,11 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo nextest run --no-fail-fast --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
|
||||
- run: cargo nextest run --release -p ef-tests --features "asm-keccak ef-tests"
|
||||
|
||||
doc:
|
||||
name: doc tests
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
runs-on: depot-ubuntu-latest
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
timeout-minutes: 30
|
||||
|
||||
36
.github/workflows/update-superchain.yml
vendored
Normal file
36
.github/workflows/update-superchain.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Update Superchain Config
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-superchain:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install required tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq zstd qpdf yq
|
||||
|
||||
- name: Run fetch_superchain_config.sh
|
||||
run: |
|
||||
chmod +x crates/optimism/chainspec/res/fetch_superchain_config.sh
|
||||
cd crates/optimism/chainspec/res
|
||||
./fetch_superchain_config.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
commit-message: "chore: update superchain config"
|
||||
title: "chore: update superchain config"
|
||||
body: "This PR updates the superchain configs via scheduled workflow."
|
||||
branch: "ci/update-superchain-config"
|
||||
delete-branch: true
|
||||
54
.github/workflows/windows.yml
vendored
Normal file
54
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Windows build
|
||||
|
||||
name: windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
check-reth:
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-pc-windows-gnu
|
||||
- uses: taiki-e/install-action@cross
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: mingw-w64
|
||||
run: sudo apt-get install -y mingw-w64
|
||||
- name: Check Reth
|
||||
run: cargo check --target x86_64-pc-windows-gnu
|
||||
|
||||
check-op-reth:
|
||||
runs-on: depot-ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-pc-windows-gnu
|
||||
- uses: taiki-e/install-action@cross
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: mingw-w64
|
||||
run: sudo apt-get install -y mingw-w64
|
||||
- name: Check OP-Reth
|
||||
run: cargo check -p op-reth --target x86_64-pc-windows-gnu
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,9 +12,6 @@ target/
|
||||
# Generated by Intellij-based IDEs.
|
||||
.idea
|
||||
|
||||
# ck-search metadata
|
||||
.ck
|
||||
|
||||
# Generated by MacOS
|
||||
.DS_Store
|
||||
|
||||
|
||||
549
AGENTS.md
549
AGENTS.md
@@ -1,549 +0,0 @@
|
||||
# Reth Development Guide for AI Agents
|
||||
|
||||
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
|
||||
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
|
||||
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
|
||||
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
|
||||
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
|
||||
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
|
||||
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
|
||||
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
|
||||
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
- **Modularity**: Each crate can be used as a standalone library
|
||||
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
|
||||
- **Extensibility**: Traits and generic types allow for different chain implementations
|
||||
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style and Standards
|
||||
|
||||
1. **Formatting**: Always use nightly rustfmt
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
```
|
||||
|
||||
2. **Linting**: Run clippy with all features
|
||||
```bash
|
||||
cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features
|
||||
```
|
||||
|
||||
3. **Testing**: Use nextest for faster test execution
|
||||
```bash
|
||||
cargo nextest run --workspace
|
||||
```
|
||||
|
||||
### Common Contribution Types
|
||||
|
||||
Based on actual recent PRs, here are typical contribution patterns:
|
||||
|
||||
#### 1. Small Bug Fixes (1-10 lines)
|
||||
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
|
||||
```rust
|
||||
// Changed a single line to fix logic error
|
||||
- parent_beacon_block_root: parent.parent_beacon_block_root(),
|
||||
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
|
||||
```
|
||||
|
||||
#### 2. Integration with Upstream Changes
|
||||
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
|
||||
```rust
|
||||
// Update code to use new APIs from dependencies
|
||||
- if self.fork_tracker.is_shanghai_activated() {
|
||||
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
|
||||
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
|
||||
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
|
||||
```
|
||||
|
||||
#### 3. Adding Comprehensive Tests
|
||||
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
|
||||
```rust
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth69_peers_can_connect() {
|
||||
// Create test network with specific protocol versions
|
||||
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
|
||||
// Test connection and version negotiation
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Making Components Generic
|
||||
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
|
||||
```rust
|
||||
// Before: Hardcoded to ChainSpec
|
||||
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
|
||||
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
|
||||
|
||||
// After: Generic over any chain spec type
|
||||
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
|
||||
+ where
|
||||
+ C: EthereumHardforks,
|
||||
+ {
|
||||
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
|
||||
```
|
||||
|
||||
#### 5. Resource Management Improvements
|
||||
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
|
||||
```rust
|
||||
// Add cleanup logic on startup
|
||||
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
|
||||
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
|
||||
+ }
|
||||
```
|
||||
|
||||
#### 6. Feature Additions
|
||||
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
|
||||
```rust
|
||||
// Add new filtering policies for transaction announcements
|
||||
pub struct ShardedMempoolAnnouncementFilter<T> {
|
||||
pub inner: T,
|
||||
pub shard_bits: u8,
|
||||
pub node_id: Option<B256>,
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
1. **Unit Tests**: Test individual functions and components
|
||||
2. **Integration Tests**: Test interactions between components
|
||||
3. **Benchmarks**: For performance-critical code
|
||||
4. **Fuzz Tests**: For parsing and serialization code
|
||||
5. **Property Tests**: For checking component correctness on a wide variety of inputs
|
||||
|
||||
Example test structure:
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_component_behavior() {
|
||||
// Arrange
|
||||
let component = Component::new();
|
||||
|
||||
// Act
|
||||
let result = component.operation();
|
||||
|
||||
// Assert
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
|
||||
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
|
||||
3. **Async/Await**: Use tokio for I/O-bound operations
|
||||
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
|
||||
2. **Handle Errors Properly**: Use `?` operator and proper error types
|
||||
|
||||
### What to Avoid
|
||||
|
||||
Based on PR patterns, avoid:
|
||||
|
||||
1. **Large, sweeping changes**: Keep PRs focused and reviewable
|
||||
2. **Mixing unrelated changes**: One logical change per PR
|
||||
3. **Ignoring CI failures**: All checks must pass
|
||||
4. **Incomplete implementations**: Finish features before submitting
|
||||
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
|
||||
|
||||
### CI Requirements
|
||||
|
||||
Before submitting changes, ensure:
|
||||
|
||||
1. **Format Check**: `cargo +nightly fmt --all --check`
|
||||
2. **Clippy**: No warnings
|
||||
3. **Tests Pass**: All unit and integration tests
|
||||
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
|
||||
5. **CLI Docs** (if CLI changed): Run `make update-book-cli` (see below)
|
||||
6. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
|
||||
### CLI Reference Docs (`book` CI Job)
|
||||
|
||||
The CLI reference pages under `docs/vocs/docs/pages/cli/` are **auto-generated** from the `reth` binary's `--help` output. **Do not edit these files manually** — any hand edits will be overwritten and CI will fail regardless.
|
||||
|
||||
When you add, remove, or modify CLI commands, subcommands, or flags, regenerate the CLI docs by running:
|
||||
|
||||
```bash
|
||||
make update-book-cli
|
||||
```
|
||||
|
||||
This builds `reth` in debug mode and runs `docs/cli/update.sh` to regenerate all CLI pages. Commit the resulting changes.
|
||||
|
||||
The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating the docs and running `git diff --exit-code`. If the committed docs don't match the generated output, CI fails. Manually editing these pages is never productive — always use `make update-book-cli`.
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
#### Titles
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
|
||||
|
||||
Examples:
|
||||
- `fix(rpc): correct gas estimation for ERC-20 transfers`
|
||||
- `perf: batch trie updates to reduce cursor overhead`
|
||||
- `feat(engine): add new_payload_interval metric`
|
||||
|
||||
#### Descriptions
|
||||
|
||||
Keep it short. Say what changed and why — nothing more.
|
||||
|
||||
**Do:**
|
||||
- Write 1–3 sentences summarizing the change
|
||||
- Explain _why_ if the diff doesn't make it obvious
|
||||
- Link related issues or EIPs
|
||||
- Include benchmark numbers for perf changes
|
||||
|
||||
**Don't:**
|
||||
- List every file changed — that's what the diff is for
|
||||
- Repeat the title in the body
|
||||
- Add "Files changed" or "Changes" sections
|
||||
- Write walls of text that go stale when the diff is updated
|
||||
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
Closes #<issue>
|
||||
|
||||
<what changed, 1-3 sentences>
|
||||
|
||||
<why, if not obvious from the diff>
|
||||
```
|
||||
|
||||
**Good example:**
|
||||
|
||||
```
|
||||
Closes #16800
|
||||
|
||||
Adds fallback for external IP resolution so node startup doesn't fail
|
||||
when STUN is unreachable. Falls back to the configured default.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```
|
||||
## Summary
|
||||
This PR introduces comprehensive improvements to the IP resolution system.
|
||||
|
||||
## Changes
|
||||
- Modified `crates/net/discv4/src/lib.rs` to add fallback
|
||||
- Modified `crates/net/discv4/src/config.rs` to add default IP
|
||||
- Added tests in `crates/net/discv4/src/tests/ip.rs`
|
||||
|
||||
## Files Changed
|
||||
- crates/net/discv4/src/lib.rs
|
||||
- crates/net/discv4/src/config.rs
|
||||
- crates/net/discv4/src/tests/ip.rs
|
||||
```
|
||||
|
||||
#### Labels and CI
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
* ... and so on, check the available labels for more options.
|
||||
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
|
||||
|
||||
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Logging**: Use `tracing` crate with appropriate levels
|
||||
```rust
|
||||
tracing::debug!(target: "reth::component", ?value, "description");
|
||||
```
|
||||
|
||||
2. **Metrics**: Add metrics for monitoring
|
||||
```rust
|
||||
metrics::counter!("reth_component_operations").increment(1);
|
||||
```
|
||||
|
||||
3. **Test Isolation**: Use separate test databases/directories
|
||||
|
||||
### Finding Where to Contribute
|
||||
|
||||
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
|
||||
2. **Review TODOs**: Search for `TODO` comments in the codebase
|
||||
3. **Improve Tests**: Areas with low test coverage are good targets
|
||||
4. **Documentation**: Improve code comments and documentation
|
||||
5. **Performance**: Profile and optimize hot paths (with benchmarks)
|
||||
|
||||
### Common PR Patterns
|
||||
|
||||
#### Small, Focused Changes
|
||||
Most PRs change only 1-5 files. Examples:
|
||||
- Single-line bug fixes
|
||||
- Adding a missing trait implementation
|
||||
- Updating error messages
|
||||
- Adding test cases for edge conditions
|
||||
|
||||
#### Integration Work
|
||||
When dependencies update (especially revm), code needs updating:
|
||||
- Check for breaking API changes
|
||||
- Update to use new features (like EIP implementations)
|
||||
- Ensure compatibility with new versions
|
||||
|
||||
#### Test Improvements
|
||||
Tests often need expansion for:
|
||||
- New protocol versions (ETH68, ETH69)
|
||||
- Edge cases in state transitions
|
||||
- Network behavior under specific conditions
|
||||
- Concurrent operations
|
||||
|
||||
#### Making Code More Generic
|
||||
Common refactoring pattern:
|
||||
- Replace concrete types with generics
|
||||
- Add trait bounds for flexibility
|
||||
- Enable reuse across different chain types
|
||||
|
||||
#### When to Comment
|
||||
|
||||
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
|
||||
|
||||
##### ✅ DO: Add Value
|
||||
|
||||
**Explain WHY and non-obvious behavior:**
|
||||
```rust
|
||||
// Process must handle allocations atomically to prevent race conditions
|
||||
// between dealloc on drop and concurrent limit checks
|
||||
unsafe impl GlobalAlloc for LimitedAllocator { ... }
|
||||
|
||||
// Binary search requires sorted input. Panics on unsorted slices.
|
||||
fn find_index(items: &[Item], target: &Item) -> Option<usize>
|
||||
|
||||
// Timeout set to 5s to match EVM block processing limits
|
||||
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
```
|
||||
|
||||
**Document constraints and assumptions:**
|
||||
```rust
|
||||
/// Returns heap size estimate.
|
||||
///
|
||||
/// Note: May undercount shared references (Rc/Arc). For precise
|
||||
/// accounting, combine with an allocator-based approach.
|
||||
fn deep_size_of(&self) -> usize
|
||||
```
|
||||
|
||||
**Explain complex logic:**
|
||||
```rust
|
||||
// We reset limits at task start because tokio reuses threads in
|
||||
// spawn_blocking pool. Without reset, second task inherits first
|
||||
// task's allocation count and immediately hits limit.
|
||||
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Describe Changes
|
||||
```rust
|
||||
// ❌ BAD - Describes the change, not the code
|
||||
// Changed from Vec to HashMap for O(1) lookups
|
||||
|
||||
// ✅ GOOD - Explains the decision
|
||||
// HashMap provides O(1) symbol lookups during trace replay
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - PR-specific context
|
||||
// Fix for issue #234 where memory wasn't freed
|
||||
|
||||
// ✅ GOOD - Documents the actual behavior
|
||||
// Explicitly drop allocations before limit check to ensure
|
||||
// accurate accounting
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - States the obvious
|
||||
// Increment counter
|
||||
counter += 1;
|
||||
|
||||
// ✅ GOOD - Explains non-obvious purpose
|
||||
// Track allocations across all threads for global limit enforcement
|
||||
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
```
|
||||
|
||||
✅ **Comment when:**
|
||||
- Non-obvious behavior or edge cases
|
||||
- Performance trade-offs
|
||||
- Safety requirements (unsafe blocks must always be documented)
|
||||
- Limitations or gotchas
|
||||
- Why simpler alternatives don't work
|
||||
|
||||
❌ **Don't comment when:**
|
||||
- Code is self-explanatory
|
||||
- Just restating the code in English
|
||||
- Describing what changed in this PR
|
||||
|
||||
##### The Test: "Will this make sense in 6 months?"
|
||||
|
||||
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
|
||||
|
||||
|
||||
#### Rust Style Guides
|
||||
|
||||
##### Type Ordering in Files
|
||||
|
||||
When defining structs, traits, and functions in a file, follow this ordering convention. The file's primary type (matching the file name) comes first, followed by supporting public types, then private types and helpers.
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
/// The primary type of this file (matches filename).
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// Followed by public auxiliary types that support the primary type
|
||||
|
||||
/// Configuration for the processor.
|
||||
pub struct PayloadProcessorConfig { ... }
|
||||
|
||||
/// Result type returned by processor operations.
|
||||
pub struct ProcessorResult { ... }
|
||||
|
||||
// Followed by public traits related to the primary type
|
||||
|
||||
pub trait ProcessorExt { ... }
|
||||
|
||||
// Followed by private helper types
|
||||
|
||||
struct InternalState { ... }
|
||||
|
||||
// Followed by private helper functions
|
||||
|
||||
fn validate_input() { ... }
|
||||
```
|
||||
|
||||
❌ **Bad**: Adding new traits and auxiliary types **above** the file's primary type (see [#22133](https://github.com/paradigmxyz/reth/pull/22133)):
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ❌ BAD - new auxiliary struct added before the file's main type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
// ❌ BAD - new trait added before the file's main type
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
// The file's primary type is buried below unrelated additions
|
||||
pub struct PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
✅ **Good**: New types go **after** the primary type:
|
||||
|
||||
```rust
|
||||
use ...;
|
||||
|
||||
// ✅ The file's primary type stays at the top
|
||||
pub struct PayloadProcessor { ... }
|
||||
|
||||
impl PayloadProcessor { ... }
|
||||
|
||||
// ✅ Auxiliary types follow the primary type
|
||||
pub struct CacheWaitDurations { ... }
|
||||
|
||||
pub trait WaitForCaches { ... }
|
||||
|
||||
impl WaitForCaches for PayloadProcessor { ... }
|
||||
```
|
||||
|
||||
### Example Contribution Workflow
|
||||
|
||||
Let's say you want to fix a bug where external IP resolution fails on startup:
|
||||
|
||||
1. **Create a branch**:
|
||||
```bash
|
||||
git checkout -b fix-external-ip-resolution
|
||||
```
|
||||
|
||||
2. **Find the relevant code**:
|
||||
```bash
|
||||
# Search for IP resolution code
|
||||
rg "external.*ip" --type rust
|
||||
```
|
||||
|
||||
3. **Reason about the problem, when the problem is identified, make the fix**:
|
||||
```rust
|
||||
// In crates/net/discv4/src/lib.rs
|
||||
pub fn resolve_external_ip() -> Option<IpAddr> {
|
||||
// Add fallback mechanism
|
||||
nat::external_ip()
|
||||
.or_else(|| nat::external_ip_from_stun())
|
||||
.or_else(|| Some(DEFAULT_IP))
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add a test**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_external_ip_fallback() {
|
||||
// Test that resolution has proper fallbacks
|
||||
}
|
||||
```
|
||||
|
||||
5. **Run checks** (IMPORTANT!):
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
cargo clippy --workspace --all-features # Make sure WHOLE WORKSPACE compiles!
|
||||
cargo nextest run -p reth-discv4
|
||||
```
|
||||
|
||||
6. **Commit with clear message**:
|
||||
```bash
|
||||
git commit -m "fix: add fallback for external IP resolution
|
||||
|
||||
Previously, node startup could fail if external IP resolution
|
||||
failed. This adds fallback mechanisms to ensure the node can
|
||||
always start with a reasonable default."
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
cargo +nightly fmt --all
|
||||
|
||||
# Run lints
|
||||
cargo +nightly clippy --workspace --all-features
|
||||
|
||||
# Run tests
|
||||
cargo nextest run --workspace
|
||||
|
||||
# Run specific benchmark
|
||||
cargo bench --bench bench_name
|
||||
|
||||
# Build optimized binary
|
||||
cargo build --release
|
||||
|
||||
# Check compilation for all features
|
||||
cargo check --workspace --all-features
|
||||
|
||||
# Check documentation
|
||||
cargo docs --document-private-items
|
||||
|
||||
# Regenerate CLI reference docs (after CLI changes)
|
||||
make update-book-cli
|
||||
```
|
||||
393
CLAUDE.md
Normal file
393
CLAUDE.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# Reth Development Guide for AI Agents
|
||||
|
||||
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
|
||||
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
|
||||
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
|
||||
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
|
||||
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
|
||||
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
|
||||
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
|
||||
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
|
||||
9 **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
- **Modularity**: Each crate can be used as a standalone library
|
||||
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
|
||||
- **Extensibility**: Traits and generic types allow for different implementations (Ethereum, Optimism, etc.)
|
||||
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style and Standards
|
||||
|
||||
1. **Formatting**: Always use nightly rustfmt
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
```
|
||||
|
||||
2. **Linting**: Run clippy with all features
|
||||
```bash
|
||||
RUSTFLAGS="-D warnings" cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features --locked
|
||||
```
|
||||
|
||||
3. **Testing**: Use nextest for faster test execution
|
||||
```bash
|
||||
cargo nextest run --workspace
|
||||
```
|
||||
|
||||
### Common Contribution Types
|
||||
|
||||
Based on actual recent PRs, here are typical contribution patterns:
|
||||
|
||||
#### 1. Small Bug Fixes (1-10 lines)
|
||||
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
|
||||
```rust
|
||||
// Changed a single line to fix logic error
|
||||
- parent_beacon_block_root: parent.parent_beacon_block_root(),
|
||||
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
|
||||
```
|
||||
|
||||
#### 2. Integration with Upstream Changes
|
||||
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
|
||||
```rust
|
||||
// Update code to use new APIs from dependencies
|
||||
- if self.fork_tracker.is_shanghai_activated() {
|
||||
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
|
||||
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
|
||||
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
|
||||
```
|
||||
|
||||
#### 3. Adding Comprehensive Tests
|
||||
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
|
||||
```rust
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth69_peers_can_connect() {
|
||||
// Create test network with specific protocol versions
|
||||
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
|
||||
// Test connection and version negotiation
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Making Components Generic
|
||||
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
|
||||
```rust
|
||||
// Before: Hardcoded to ChainSpec
|
||||
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
|
||||
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
|
||||
|
||||
// After: Generic over any chain spec type
|
||||
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
|
||||
+ where
|
||||
+ C: EthereumHardforks,
|
||||
+ {
|
||||
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
|
||||
```
|
||||
|
||||
#### 5. Resource Management Improvements
|
||||
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
|
||||
```rust
|
||||
// Add cleanup logic on startup
|
||||
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
|
||||
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
|
||||
+ }
|
||||
```
|
||||
|
||||
#### 6. Feature Additions
|
||||
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
|
||||
```rust
|
||||
// Add new filtering policies for transaction announcements
|
||||
pub struct ShardedMempoolAnnouncementFilter<T> {
|
||||
pub inner: T,
|
||||
pub shard_bits: u8,
|
||||
pub node_id: Option<B256>,
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
1. **Unit Tests**: Test individual functions and components
|
||||
2. **Integration Tests**: Test interactions between components
|
||||
3. **Benchmarks**: For performance-critical code
|
||||
4. **Fuzz Tests**: For parsing and serialization code
|
||||
5. **Property Tests**: For checking component correctness on a wide variety of inputs
|
||||
|
||||
Example test structure:
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_component_behavior() {
|
||||
// Arrange
|
||||
let component = Component::new();
|
||||
|
||||
// Act
|
||||
let result = component.operation();
|
||||
|
||||
// Assert
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
|
||||
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
|
||||
3. **Async/Await**: Use tokio for I/O-bound operations
|
||||
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
|
||||
2. **Handle Errors Properly**: Use `?` operator and proper error types
|
||||
|
||||
### What to Avoid
|
||||
|
||||
Based on PR patterns, avoid:
|
||||
|
||||
1. **Large, sweeping changes**: Keep PRs focused and reviewable
|
||||
2. **Mixing unrelated changes**: One logical change per PR
|
||||
3. **Ignoring CI failures**: All checks must pass
|
||||
4. **Incomplete implementations**: Finish features before submitting
|
||||
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
|
||||
|
||||
### CI Requirements
|
||||
|
||||
Before submitting changes, ensure:
|
||||
|
||||
1. **Format Check**: `cargo +nightly fmt --all --check`
|
||||
2. **Clippy**: No warnings with `RUSTFLAGS="-D warnings"`
|
||||
3. **Tests Pass**: All unit and integration tests
|
||||
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
|
||||
5. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
|
||||
|
||||
|
||||
### Opening PRs against <https://github.com/paradigmxyz/reth>
|
||||
|
||||
Label PRs appropriately, first check the available labels and then apply the relevant ones:
|
||||
* when changes are RPC related, add A-rpc label
|
||||
* when changes are docs related, add C-docs label
|
||||
* when changes are optimism related (e.g. new feature or exclusive changes to crates/optimism), add A-op-reth label
|
||||
* ... and so on, check the available labels for more options.
|
||||
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
|
||||
|
||||
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Logging**: Use `tracing` crate with appropriate levels
|
||||
```rust
|
||||
tracing::debug!(target: "reth::component", ?value, "description");
|
||||
```
|
||||
|
||||
2. **Metrics**: Add metrics for monitoring
|
||||
```rust
|
||||
metrics::counter!("reth_component_operations").increment(1);
|
||||
```
|
||||
|
||||
3. **Test Isolation**: Use separate test databases/directories
|
||||
|
||||
### Finding Where to Contribute
|
||||
|
||||
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
|
||||
2. **Review TODOs**: Search for `TODO` comments in the codebase
|
||||
3. **Improve Tests**: Areas with low test coverage are good targets
|
||||
4. **Documentation**: Improve code comments and documentation
|
||||
5. **Performance**: Profile and optimize hot paths (with benchmarks)
|
||||
|
||||
### Common PR Patterns
|
||||
|
||||
#### Small, Focused Changes
|
||||
Most PRs change only 1-5 files. Examples:
|
||||
- Single-line bug fixes
|
||||
- Adding a missing trait implementation
|
||||
- Updating error messages
|
||||
- Adding test cases for edge conditions
|
||||
|
||||
#### Integration Work
|
||||
When dependencies update (especially revm), code needs updating:
|
||||
- Check for breaking API changes
|
||||
- Update to use new features (like EIP implementations)
|
||||
- Ensure compatibility with new versions
|
||||
|
||||
#### Test Improvements
|
||||
Tests often need expansion for:
|
||||
- New protocol versions (ETH68, ETH69)
|
||||
- Edge cases in state transitions
|
||||
- Network behavior under specific conditions
|
||||
- Concurrent operations
|
||||
|
||||
#### Making Code More Generic
|
||||
Common refactoring pattern:
|
||||
- Replace concrete types with generics
|
||||
- Add trait bounds for flexibility
|
||||
- Enable reuse across different chain types (Ethereum, Optimism)
|
||||
|
||||
#### When to Comment
|
||||
|
||||
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
|
||||
|
||||
##### ✅ DO: Add Value
|
||||
|
||||
**Explain WHY and non-obvious behavior:**
|
||||
```rust
|
||||
// Process must handle allocations atomically to prevent race conditions
|
||||
// between dealloc on drop and concurrent limit checks
|
||||
unsafe impl GlobalAlloc for LimitedAllocator { ... }
|
||||
|
||||
// Binary search requires sorted input. Panics on unsorted slices.
|
||||
fn find_index(items: &[Item], target: &Item) -> Option
|
||||
|
||||
// Timeout set to 5s to match EVM block processing limits
|
||||
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
```
|
||||
|
||||
**Document constraints and assumptions:**
|
||||
```rust
|
||||
/// Returns heap size estimate.
|
||||
///
|
||||
/// Note: May undercount shared references (Rc/Arc). For precise
|
||||
/// accounting, combine with an allocator-based approach.
|
||||
fn deep_size_of(&self) -> usize
|
||||
```
|
||||
|
||||
**Explain complex logic:**
|
||||
```rust
|
||||
// We reset limits at task start because tokio reuses threads in
|
||||
// spawn_blocking pool. Without reset, second task inherits first
|
||||
// task's allocation count and immediately hits limit.
|
||||
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Describe Changes
|
||||
```rust
|
||||
// ❌ BAD - Describes the change, not the code
|
||||
// Changed from Vec to HashMap for O(1) lookups
|
||||
|
||||
// ✅ GOOD - Explains the decision
|
||||
// HashMap provides O(1) symbol lookups during trace replay
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - PR-specific context
|
||||
// Fix for issue #234 where memory wasn't freed
|
||||
|
||||
// ✅ GOOD - Documents the actual behavior
|
||||
// Explicitly drop allocations before limit check to ensure
|
||||
// accurate accounting
|
||||
```
|
||||
```rust
|
||||
// ❌ BAD - States the obvious
|
||||
// Increment counter
|
||||
counter += 1;
|
||||
|
||||
// ✅ GOOD - Explains non-obvious purpose
|
||||
// Track allocations across all threads for global limit enforcement
|
||||
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
```
|
||||
|
||||
✅ **Comment when:**
|
||||
- Non-obvious behavior or edge cases
|
||||
- Performance trade-offs
|
||||
- Safety requirements (unsafe blocks must always be documented)
|
||||
- Limitations or gotchas
|
||||
- Why simpler alternatives don't work
|
||||
|
||||
❌ **Don't comment when:**
|
||||
- Code is self-explanatory
|
||||
- Just restating the code in English
|
||||
- Describing what changed in this PR
|
||||
|
||||
##### The Test: "Will this make sense in 6 months?"
|
||||
|
||||
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
|
||||
|
||||
|
||||
### Example Contribution Workflow
|
||||
|
||||
Let's say you want to fix a bug where external IP resolution fails on startup:
|
||||
|
||||
1. **Create a branch**:
|
||||
```bash
|
||||
git checkout -b fix-external-ip-resolution
|
||||
```
|
||||
|
||||
2. **Find the relevant code**:
|
||||
```bash
|
||||
# Search for IP resolution code
|
||||
rg "external.*ip" --type rust
|
||||
```
|
||||
|
||||
3. **Reason about the problem, when the problem is identified, make the fix**:
|
||||
```rust
|
||||
// In crates/net/discv4/src/lib.rs
|
||||
pub fn resolve_external_ip() -> Option<IpAddr> {
|
||||
// Add fallback mechanism
|
||||
nat::external_ip()
|
||||
.or_else(|| nat::external_ip_from_stun())
|
||||
.or_else(|| Some(DEFAULT_IP))
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add a test**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_external_ip_fallback() {
|
||||
// Test that resolution has proper fallbacks
|
||||
}
|
||||
```
|
||||
|
||||
5. **Run checks**:
|
||||
```bash
|
||||
cargo +nightly fmt --all
|
||||
cargo clippy --all-features
|
||||
cargo test -p reth-discv4
|
||||
```
|
||||
|
||||
6. **Commit with clear message**:
|
||||
```bash
|
||||
git commit -m "fix: add fallback for external IP resolution
|
||||
|
||||
Previously, node startup could fail if external IP resolution
|
||||
failed. This adds fallback mechanisms to ensure the node can
|
||||
always start with a reasonable default."
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
cargo +nightly fmt --all
|
||||
|
||||
# Run lints
|
||||
RUSTFLAGS="-D warnings" cargo +nightly clippy --workspace --all-features --locked
|
||||
|
||||
# Run tests
|
||||
cargo nextest run --workspace
|
||||
|
||||
# Run specific benchmark
|
||||
cargo bench --bench bench_name
|
||||
|
||||
# Build optimized binary
|
||||
cargo build --release --features "jemalloc asm-keccak"
|
||||
|
||||
# Check compilation for all features
|
||||
cargo check --workspace --all-features
|
||||
|
||||
# Check documentation
|
||||
cargo docs --document-private-items
|
||||
```
|
||||
@@ -51,7 +51,9 @@ elsewhere.
|
||||
<!-- - **Asking in the support Telegram:** The [Foundry Support Telegram][support-tg] is a fast and easy way to ask questions. -->
|
||||
<!-- - **Opening a discussion:** This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top. -->
|
||||
|
||||
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by **opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top.
|
||||
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by *
|
||||
*opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "
|
||||
Discussions" tab at the top.
|
||||
|
||||
As Reth is still in heavy development, the documentation can be a bit scattered. The [Reth Docs][reth-docs] is our
|
||||
current best-effort attempt at keeping up-to-date information.
|
||||
|
||||
5456
Cargo.lock
generated
5456
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
328
Cargo.toml
328
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[workspace.package]
|
||||
version = "2.1.0"
|
||||
version = "1.9.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.93"
|
||||
rust-version = "1.88"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://paradigmxyz.github.io/reth"
|
||||
repository = "https://github.com/paradigmxyz/reth"
|
||||
@@ -9,8 +9,8 @@ exclude = [".github/"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"bin/reth-bb/",
|
||||
"bin/reth-bench/",
|
||||
"bin/reth-bench-compare/",
|
||||
"bin/reth/",
|
||||
"crates/storage/rpc-provider/",
|
||||
"crates/chain-state/",
|
||||
@@ -27,7 +27,7 @@ members = [
|
||||
"crates/engine/invalid-block-hooks/",
|
||||
"crates/engine/local",
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/execution-cache/",
|
||||
"crates/engine/service",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
@@ -72,15 +72,33 @@ members = [
|
||||
"crates/node/events/",
|
||||
"crates/node/metrics",
|
||||
"crates/node/types",
|
||||
"crates/optimism/bin",
|
||||
"crates/optimism/chainspec",
|
||||
"crates/optimism/cli",
|
||||
"crates/optimism/consensus",
|
||||
"crates/optimism/evm/",
|
||||
"crates/optimism/flashblocks/",
|
||||
"crates/optimism/hardforks/",
|
||||
"crates/optimism/node/",
|
||||
"crates/optimism/payload/",
|
||||
"crates/optimism/primitives/",
|
||||
"crates/optimism/reth/",
|
||||
"crates/optimism/rpc/",
|
||||
"crates/optimism/storage",
|
||||
"crates/optimism/txpool/",
|
||||
"crates/payload/basic/",
|
||||
"crates/payload/builder/",
|
||||
"crates/payload/builder-primitives/",
|
||||
"crates/payload/primitives/",
|
||||
"crates/payload/validator/",
|
||||
"crates/payload/util/",
|
||||
"crates/primitives-traits/",
|
||||
"crates/primitives/",
|
||||
"crates/prune/db",
|
||||
"crates/prune/prune",
|
||||
"crates/prune/types",
|
||||
"crates/ress/protocol",
|
||||
"crates/ress/provider",
|
||||
"crates/revm/",
|
||||
"crates/rpc/ipc/",
|
||||
"crates/rpc/rpc-api/",
|
||||
@@ -97,8 +115,11 @@ members = [
|
||||
"crates/stages/api/",
|
||||
"crates/stages/stages/",
|
||||
"crates/stages/types/",
|
||||
"crates/stateless",
|
||||
"crates/static-file/static-file",
|
||||
"crates/static-file/types/",
|
||||
"crates/storage/codecs/",
|
||||
"crates/storage/codecs/derive/",
|
||||
"crates/storage/db-api/",
|
||||
"crates/storage/db-common",
|
||||
"crates/storage/db-models/",
|
||||
@@ -109,6 +130,7 @@ members = [
|
||||
"crates/storage/nippy-jar/",
|
||||
"crates/storage/provider/",
|
||||
"crates/storage/storage-api/",
|
||||
"crates/storage/zstd-compressors/",
|
||||
"crates/tasks/",
|
||||
"crates/tokio-util/",
|
||||
"crates/tracing/",
|
||||
@@ -117,11 +139,13 @@ members = [
|
||||
"crates/trie/db",
|
||||
"crates/trie/parallel/",
|
||||
"crates/trie/sparse",
|
||||
"crates/trie/sparse-parallel/",
|
||||
"crates/trie/trie",
|
||||
"examples/beacon-api-sidecar-fetcher/",
|
||||
"examples/beacon-api-sse/",
|
||||
"examples/bsc-p2p",
|
||||
"examples/custom-dev-node/",
|
||||
"examples/custom-node/",
|
||||
"examples/custom-engine-types/",
|
||||
"examples/custom-evm/",
|
||||
"examples/custom-hardforks/",
|
||||
@@ -129,9 +153,11 @@ members = [
|
||||
"examples/custom-node-components/",
|
||||
"examples/custom-payload-builder/",
|
||||
"examples/custom-rlpx-subprotocol",
|
||||
"examples/custom-auth-http-middleware",
|
||||
"examples/custom-rpc-middleware",
|
||||
"examples/custom-node",
|
||||
"examples/db-access",
|
||||
"examples/engine-api-access",
|
||||
"examples/exex-hello-world",
|
||||
"examples/exex-subscription",
|
||||
"examples/exex-test",
|
||||
"examples/full-contract-state",
|
||||
@@ -142,6 +168,7 @@ members = [
|
||||
"examples/node-builder-api/",
|
||||
"examples/node-custom-rpc/",
|
||||
"examples/node-event-hooks/",
|
||||
"examples/op-db-access/",
|
||||
"examples/polygon-p2p/",
|
||||
"examples/rpc-db/",
|
||||
"examples/precompile-cache/",
|
||||
@@ -166,7 +193,6 @@ rust.rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
rust.unreachable_pub = "warn"
|
||||
rust.unused_must_use = "deny"
|
||||
rust.rust_2024_incompatible_pat = "warn"
|
||||
rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||
rustdoc.all = "warn"
|
||||
# rust.unnameable-types = "warn"
|
||||
|
||||
@@ -303,11 +329,6 @@ inherits = "release"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.maxperf-symbols]
|
||||
inherits = "maxperf"
|
||||
debug = "full"
|
||||
strip = "none"
|
||||
|
||||
[profile.reproducible]
|
||||
inherits = "release"
|
||||
panic = "abort"
|
||||
@@ -316,18 +337,20 @@ incremental = false
|
||||
|
||||
[workspace.dependencies]
|
||||
# reth
|
||||
op-reth = { path = "crates/optimism/bin" }
|
||||
reth = { path = "bin/reth" }
|
||||
reth-storage-rpc-provider = { path = "crates/storage/rpc-provider" }
|
||||
reth-basic-payload-builder = { path = "crates/payload/basic" }
|
||||
reth-bench = { path = "bin/reth-bench" }
|
||||
reth-bench-compare = { path = "bin/reth-bench-compare" }
|
||||
reth-chain-state = { path = "crates/chain-state" }
|
||||
reth-chainspec = { path = "crates/chainspec", default-features = false }
|
||||
reth-cli = { path = "crates/cli/cli" }
|
||||
reth-cli-commands = { path = "crates/cli/commands" }
|
||||
reth-cli-runner = { path = "crates/cli/runner" }
|
||||
reth-cli-util = { path = "crates/cli/util" }
|
||||
reth-codecs = { version = "0.3.1", default-features = false }
|
||||
reth-codecs-derive = "0.3.1"
|
||||
reth-codecs = { path = "crates/storage/codecs" }
|
||||
reth-codecs-derive = { path = "crates/storage/codecs/derive" }
|
||||
reth-config = { path = "crates/config", default-features = false }
|
||||
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
|
||||
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
|
||||
@@ -343,9 +366,9 @@ reth-downloaders = { path = "crates/net/downloaders" }
|
||||
reth-e2e-test-utils = { path = "crates/e2e-test-utils" }
|
||||
reth-ecies = { path = "crates/net/ecies" }
|
||||
reth-engine-local = { path = "crates/engine/local" }
|
||||
reth-execution-cache = { path = "crates/engine/execution-cache" }
|
||||
reth-engine-primitives = { path = "crates/engine/primitives", default-features = false }
|
||||
reth-engine-tree = { path = "crates/engine/tree" }
|
||||
reth-engine-service = { path = "crates/engine/service" }
|
||||
reth-engine-util = { path = "crates/engine/util" }
|
||||
reth-era = { path = "crates/era" }
|
||||
reth-era-downloader = { path = "crates/era-downloader" }
|
||||
@@ -363,6 +386,7 @@ reth-ethereum = { path = "crates/ethereum/reth" }
|
||||
reth-etl = { path = "crates/etl" }
|
||||
reth-evm = { path = "crates/evm/evm", default-features = false }
|
||||
reth-evm-ethereum = { path = "crates/ethereum/evm", default-features = false }
|
||||
reth-optimism-evm = { path = "crates/optimism/evm", default-features = false }
|
||||
reth-execution-errors = { path = "crates/evm/execution-errors", default-features = false }
|
||||
reth-execution-types = { path = "crates/evm/execution-types", default-features = false }
|
||||
reth-exex = { path = "crates/exex/exex" }
|
||||
@@ -389,13 +413,25 @@ reth-node-ethereum = { path = "crates/ethereum/node" }
|
||||
reth-node-ethstats = { path = "crates/node/ethstats" }
|
||||
reth-node-events = { path = "crates/node/events" }
|
||||
reth-node-metrics = { path = "crates/node/metrics" }
|
||||
reth-optimism-node = { path = "crates/optimism/node" }
|
||||
reth-node-types = { path = "crates/node/types" }
|
||||
reth-op = { path = "crates/optimism/reth", default-features = false }
|
||||
reth-optimism-chainspec = { path = "crates/optimism/chainspec", default-features = false }
|
||||
reth-optimism-cli = { path = "crates/optimism/cli", default-features = false }
|
||||
reth-optimism-consensus = { path = "crates/optimism/consensus", default-features = false }
|
||||
reth-optimism-forks = { path = "crates/optimism/hardforks", default-features = false }
|
||||
reth-optimism-payload-builder = { path = "crates/optimism/payload" }
|
||||
reth-optimism-primitives = { path = "crates/optimism/primitives", default-features = false }
|
||||
reth-optimism-rpc = { path = "crates/optimism/rpc" }
|
||||
reth-optimism-storage = { path = "crates/optimism/storage" }
|
||||
reth-optimism-txpool = { path = "crates/optimism/txpool" }
|
||||
reth-payload-builder = { path = "crates/payload/builder" }
|
||||
reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
|
||||
reth-payload-primitives = { path = "crates/payload/primitives" }
|
||||
reth-payload-validator = { path = "crates/payload/validator" }
|
||||
reth-payload-util = { path = "crates/payload/util" }
|
||||
reth-primitives-traits = { version = "0.3.1", default-features = false }
|
||||
reth-primitives = { path = "crates/primitives", default-features = false }
|
||||
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
|
||||
reth-provider = { path = "crates/storage/provider" }
|
||||
reth-prune = { path = "crates/prune/prune" }
|
||||
reth-prune-types = { path = "crates/prune/types", default-features = false }
|
||||
@@ -409,12 +445,13 @@ reth-rpc-engine-api = { path = "crates/rpc/rpc-engine-api" }
|
||||
reth-rpc-eth-api = { path = "crates/rpc/rpc-eth-api" }
|
||||
reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = false }
|
||||
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
|
||||
reth-optimism-flashblocks = { path = "crates/optimism/flashblocks" }
|
||||
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
|
||||
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
|
||||
reth-rpc-traits = { version = "0.3.1", default-features = false }
|
||||
reth-stages = { path = "crates/stages/stages" }
|
||||
reth-stages-api = { path = "crates/stages/api" }
|
||||
reth-stages-types = { path = "crates/stages/types", default-features = false }
|
||||
reth-stateless = { path = "crates/stateless", default-features = false }
|
||||
reth-static-file = { path = "crates/static-file/static-file" }
|
||||
reth-static-file-types = { path = "crates/static-file/types", default-features = false }
|
||||
reth-storage-api = { path = "crates/storage/storage-api", default-features = false }
|
||||
@@ -430,59 +467,73 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
|
||||
reth-trie-db = { path = "crates/trie/db" }
|
||||
reth-trie-parallel = { path = "crates/trie/parallel" }
|
||||
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.1", default-features = false }
|
||||
reth-trie-sparse-parallel = { path = "crates/trie/sparse-parallel" }
|
||||
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
|
||||
reth-ress-protocol = { path = "crates/ress/protocol" }
|
||||
reth-ress-provider = { path = "crates/ress/provider" }
|
||||
|
||||
# revm
|
||||
revm = { version = "=37.0.0", default-features = false }
|
||||
revm-bytecode = { version = "=10.0.0", default-features = false }
|
||||
revm-database = { version = "=13.0.0", default-features = false }
|
||||
revm-state = { version = "=11.0.0", default-features = false }
|
||||
revm-primitives = { version = "=23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "=35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "=11.0.0", default-features = false }
|
||||
revm-inspectors = "=0.39.0"
|
||||
revm = { version = "33.1.0", default-features = false }
|
||||
revm-bytecode = { version = "7.1.1", default-features = false }
|
||||
revm-database = { version = "9.0.5", default-features = false }
|
||||
revm-state = { version = "8.1.1", default-features = false }
|
||||
revm-primitives = { version = "21.0.2", default-features = false }
|
||||
revm-interpreter = { version = "31.1.0", default-features = false }
|
||||
revm-database-interface = { version = "8.0.5", default-features = false }
|
||||
op-revm = { version = "14.1.0", default-features = false }
|
||||
revm-inspectors = "0.33.2"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
alloy-primitives = { version = "1.5.6", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-sol-types = { version = "1.5.6", default-features = false }
|
||||
|
||||
alloy-chains = { version = "0.2.33", default-features = false }
|
||||
alloy-chains = { version = "0.2.5", default-features = false }
|
||||
alloy-dyn-abi = "1.4.1"
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.4", default-features = false }
|
||||
alloy-evm = { version = "0.33.0", default-features = false }
|
||||
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
|
||||
alloy-trie = { version = "0.9.4", default-features = false }
|
||||
alloy-eip7928 = { version = "0.1.0" }
|
||||
alloy-evm = { version = "0.25.1", default-features = false }
|
||||
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
|
||||
alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] }
|
||||
alloy-sol-macro = "1.5.0"
|
||||
alloy-sol-types = { version = "1.5.0", default-features = false }
|
||||
alloy-trie = { version = "0.9.1", default-features = false }
|
||||
|
||||
alloy-hardforks = "0.4.7"
|
||||
alloy-hardforks = "0.4.5"
|
||||
|
||||
alloy-consensus = { version = "2.0.1", default-features = false }
|
||||
alloy-contract = { version = "2.0.1", default-features = false }
|
||||
alloy-eips = { version = "2.0.1", default-features = false }
|
||||
alloy-genesis = { version = "2.0.1", default-features = false }
|
||||
alloy-json-rpc = { version = "2.0.1", default-features = false }
|
||||
alloy-network = { version = "2.0.1", default-features = false }
|
||||
alloy-network-primitives = { version = "2.0.1", default-features = false }
|
||||
alloy-provider = { version = "2.0.1", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-client = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types = { version = "2.0.1", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "2.0.1", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "2.0.1", default-features = false }
|
||||
alloy-serde = { version = "2.0.1", default-features = false }
|
||||
alloy-signer = { version = "2.0.1", default-features = false }
|
||||
alloy-signer-local = { version = "2.0.1", default-features = false }
|
||||
alloy-transport = { version = "2.0.1" }
|
||||
alloy-transport-http = { version = "2.0.1", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "2.0.1", default-features = false }
|
||||
alloy-transport-ws = { version = "2.0.1", default-features = false }
|
||||
alloy-consensus = { version = "1.1.3", default-features = false }
|
||||
alloy-contract = { version = "1.1.3", default-features = false }
|
||||
alloy-eips = { version = "1.1.3", default-features = false }
|
||||
alloy-genesis = { version = "1.1.3", default-features = false }
|
||||
alloy-json-rpc = { version = "1.1.3", default-features = false }
|
||||
alloy-network = { version = "1.1.3", default-features = false }
|
||||
alloy-network-primitives = { version = "1.1.3", default-features = false }
|
||||
alloy-provider = { version = "1.1.3", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-client = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types = { version = "1.1.3", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.1.3", default-features = false }
|
||||
alloy-serde = { version = "1.1.3", default-features = false }
|
||||
alloy-signer = { version = "1.1.3", default-features = false }
|
||||
alloy-signer-local = { version = "1.1.3", default-features = false }
|
||||
alloy-transport = { version = "1.1.3" }
|
||||
alloy-transport-http = { version = "1.1.3", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.1.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.1.3", default-features = false }
|
||||
|
||||
# op
|
||||
alloy-op-evm = { version = "0.25.0", default-features = false }
|
||||
alloy-op-hardforks = "0.4.4"
|
||||
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
|
||||
op-alloy-network = { version = "0.23.1", default-features = false }
|
||||
op-alloy-consensus = { version = "0.23.1", default-features = false }
|
||||
op-alloy-rpc-jsonrpsee = { version = "0.23.1", default-features = false }
|
||||
op-alloy-flz = { version = "0.13.1", default-features = false }
|
||||
|
||||
# misc
|
||||
either = { version = "1.15.0", default-features = false }
|
||||
@@ -493,76 +544,69 @@ backon = { version = "1.2", default-features = false, features = ["std-blocking-
|
||||
bincode = "1.3"
|
||||
bitflags = "2.4"
|
||||
boyer-moore-magiclen = "0.2.16"
|
||||
bytes = { version = "1.11.1", default-features = false }
|
||||
blake3 = "1.8"
|
||||
bytes = { version = "1.5", default-features = false }
|
||||
brotli = "8"
|
||||
cfg-if = "1.0"
|
||||
clap = "4"
|
||||
color-eyre = "0.6"
|
||||
dashmap = "6.0"
|
||||
derive_more = { version = "2", default-features = false, features = ["full"] }
|
||||
dirs-next = "2.0.0"
|
||||
dyn-clone = "1.0.17"
|
||||
eyre = "0.6"
|
||||
fdlimit = "0.3.0"
|
||||
fixed-map = { version = "0.9", default-features = false }
|
||||
humantime = "2.1"
|
||||
imbl = "7"
|
||||
humantime-serde = "1.1"
|
||||
itertools = { version = "0.14", default-features = false }
|
||||
linked_hash_set = "0.1"
|
||||
libc = "0.2"
|
||||
lz4 = "1.28.1"
|
||||
modular-bitfield = "0.13.1"
|
||||
modular-bitfield = "0.11.2"
|
||||
notify = { version = "8.0.0", default-features = false, features = ["macos_fsevent"] }
|
||||
nybbles = { version = "0.4.8", default-features = false }
|
||||
nybbles = { version = "0.4.2", default-features = false }
|
||||
once_cell = { version = "1.19", default-features = false, features = ["critical-section"] }
|
||||
parking_lot = "0.12"
|
||||
quanta = "0.12"
|
||||
paste = "1.0"
|
||||
rand = "0.9"
|
||||
rayon = "1.7"
|
||||
thread-priority = "3.0.0"
|
||||
rustc-hash = { version = "2.0", default-features = false }
|
||||
schnellru = "0.2"
|
||||
serde = { version = "1.0", default-features = false }
|
||||
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
|
||||
serde_with = { version = "3", default-features = false, features = ["macros"] }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
shellexpand = "3.0.0"
|
||||
shlex = "1.3"
|
||||
slotmap = "1"
|
||||
smallvec = "1"
|
||||
strum = { version = "0.27", default-features = false }
|
||||
strum_macros = "0.27"
|
||||
syn = "2.0"
|
||||
thiserror = { version = "2.0.0", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tracing = { version = "0.1.0", default-features = false, features = ["attributes"] }
|
||||
tracing = { version = "0.1.0", default-features = false }
|
||||
tracing-appender = "0.2"
|
||||
url = { version = "2.3", default-features = false }
|
||||
zstd = "0.13"
|
||||
byteorder = "1"
|
||||
fixed-cache = { version = "0.1.7", features = ["stats"] }
|
||||
mini-moka = "0.10"
|
||||
moka = "0.12"
|
||||
tar-no-std = { version = "0.4.2", default-features = false }
|
||||
miniz_oxide = { version = "0.9.0", default-features = false }
|
||||
tar-no-std = { version = "0.3.2", default-features = false }
|
||||
miniz_oxide = { version = "0.8.4", default-features = false }
|
||||
chrono = "0.4.41"
|
||||
|
||||
# metrics
|
||||
metrics = "0.24.0"
|
||||
metrics-derive = "0.1.1"
|
||||
metrics-exporter-prometheus = { version = "0.18.0", default-features = false }
|
||||
metrics-derive = "0.1"
|
||||
metrics-exporter-prometheus = { version = "0.16.0", default-features = false }
|
||||
metrics-process = "2.1.0"
|
||||
metrics-util = { default-features = false, version = "0.20.0" }
|
||||
metrics-util = { default-features = false, version = "0.19.0" }
|
||||
|
||||
# proc-macros
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
||||
# tokio
|
||||
tokio = { version = "1.51.1", default-features = false }
|
||||
tokio = { version = "1.44.2", default-features = false }
|
||||
tokio-stream = "0.1.11"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
|
||||
# async
|
||||
@@ -572,10 +616,10 @@ async-trait = "0.1.68"
|
||||
futures = "0.3"
|
||||
futures-core = "0.3"
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
hyper = "1.9"
|
||||
hyper = "1.3"
|
||||
hyper-util = "0.1.5"
|
||||
pin-project = "1.0.12"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream"] }
|
||||
reqwest = { version = "0.12", default-features = false }
|
||||
tracing-futures = "0.2"
|
||||
tower = "0.5"
|
||||
tower-http = "0.6"
|
||||
@@ -595,6 +639,7 @@ jsonrpsee-types = "0.26.0"
|
||||
http = "1.0"
|
||||
http-body = "1.0"
|
||||
http-body-util = "0.1.2"
|
||||
jsonwebtoken = "9"
|
||||
proptest-arbitrary-interop = "0.1.0"
|
||||
|
||||
# crypto
|
||||
@@ -608,7 +653,7 @@ rand_08 = { package = "rand", version = "0.8" }
|
||||
c-kzg = "2.1.5"
|
||||
|
||||
# config
|
||||
toml = "0.9"
|
||||
toml = "0.8"
|
||||
|
||||
# rocksdb
|
||||
rocksdb = { version = "0.24" }
|
||||
@@ -618,47 +663,42 @@ opentelemetry_sdk = "0.31"
|
||||
opentelemetry = "0.31"
|
||||
opentelemetry-otlp = "0.31"
|
||||
opentelemetry-semantic-conventions = "0.31"
|
||||
opentelemetry-appender-tracing = "0.31"
|
||||
tracing-opentelemetry = "0.32"
|
||||
|
||||
# misc-testing
|
||||
arbitrary = "1.3"
|
||||
assert_matches = "1.5.0"
|
||||
criterion = { package = "codspeed-criterion-compat", version = "4.3" }
|
||||
insta = "1.47"
|
||||
criterion = { package = "codspeed-criterion-compat", version = "2.7" }
|
||||
insta = "1.41"
|
||||
proptest = "1.7"
|
||||
proptest-derive = "0.7"
|
||||
proptest-derive = "0.5"
|
||||
similar-asserts = { version = "1.5.0", features = ["serde"] }
|
||||
tempfile = "3.20"
|
||||
test-fuzz = "7"
|
||||
rstest = "0.26.1"
|
||||
rstest = "0.24.0"
|
||||
test-case = "3"
|
||||
|
||||
# ssz encoding
|
||||
ethereum_ssz = "0.10.1"
|
||||
ethereum_ssz_derive = "0.10.1"
|
||||
ethereum_ssz = "0.9.0"
|
||||
ethereum_ssz_derive = "0.9.0"
|
||||
|
||||
# allocators
|
||||
jemalloc_pprof = { version = "0.8", default-features = false }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
tikv-jemalloc-sys = "0.6"
|
||||
tikv-jemallocator = "0.6"
|
||||
tracy-client = { version = "0.18.0", features = ["demangle"] }
|
||||
tracy-client = "0.18.0"
|
||||
snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
|
||||
|
||||
aes = "0.8.1"
|
||||
ahash = "0.8"
|
||||
anyhow = "1.0"
|
||||
bindgen = { version = "0.72", default-features = false }
|
||||
block-padding = "0.3"
|
||||
cc = "1.2.15"
|
||||
bindgen = { version = "0.71", default-features = false }
|
||||
block-padding = "0.3.2"
|
||||
cc = "=1.2.15"
|
||||
cipher = "0.4.3"
|
||||
comfy-table = "7.0"
|
||||
concat-kdf = "0.1.0"
|
||||
crossbeam-channel = "0.5.13"
|
||||
crossbeam-queue = "0.3"
|
||||
crossbeam-utils = "0.8"
|
||||
crossterm = "0.29.0"
|
||||
crossterm = "0.28.0"
|
||||
csv = "1.3.0"
|
||||
ctrlc = "3.4"
|
||||
ctr = "0.9.2"
|
||||
@@ -671,53 +711,79 @@ hmac = "0.12.1"
|
||||
human_bytes = "0.4.1"
|
||||
indexmap = "2"
|
||||
interprocess = "2.2.0"
|
||||
lz4_flex = { version = "0.12", default-features = false }
|
||||
lz4_flex = { version = "0.11", default-features = false }
|
||||
memmap2 = "0.9.4"
|
||||
mev-share-sse = { version = "0.5.0", default-features = false }
|
||||
num-traits = "0.2.15"
|
||||
page_size = "0.6.0"
|
||||
parity-scale-codec = "3.2.1"
|
||||
plain_hasher = "0.2"
|
||||
pretty_assertions = "1.4"
|
||||
ratatui = { version = "0.30", default-features = false }
|
||||
ringbuffer = "0.16.0"
|
||||
ratatui = { version = "0.29", default-features = false }
|
||||
ringbuffer = "0.15.0"
|
||||
rmp-serde = "1.3"
|
||||
roaring = "0.11.3"
|
||||
roaring = "0.10.2"
|
||||
rolling-file = "0.2.0"
|
||||
sha3 = "0.10.5"
|
||||
snap = "1.1.1"
|
||||
socket2 = { version = "0.6", default-features = false }
|
||||
sysinfo = { version = "0.38", default-features = false }
|
||||
socket2 = { version = "0.5", default-features = false }
|
||||
sysinfo = { version = "0.33", default-features = false }
|
||||
tracing-journald = "0.3"
|
||||
tracing-logfmt = "0.3.7"
|
||||
tracing-logfmt = "0.3.3"
|
||||
tracing-samply = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
tracing-tracy = "0.11"
|
||||
triehash = "0.8"
|
||||
typenum = "1.15.0"
|
||||
vergen = "9.1.0"
|
||||
vergen = "9.0.4"
|
||||
visibility = "0.1.1"
|
||||
walkdir = "2.3.3"
|
||||
vergen-git2 = "9.1.0"
|
||||
vergen-git2 = "1.0.5"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
[patch.crates-io]
|
||||
revm = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "a2c7a41977b468d016a339f560acb76e002766f3" }
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6" }
|
||||
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
# [patch.crates-io]
|
||||
# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
|
||||
# op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
# op-alloy-rpc-jsonrpsee = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
|
||||
#
|
||||
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "1207e33" }
|
||||
#
|
||||
# jsonrpsee = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
|
||||
# jsonrpsee-core = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
|
||||
# jsonrpsee-server = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
|
||||
# jsonrpsee-http-client = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
|
||||
# jsonrpsee-types = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
|
||||
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "a69f0b45a6b0286e16072cb8399e02ce6ceca353" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "a69f0b45a6b0286e16072cb8399e02ce6ceca353" }
|
||||
|
||||
12
Cross.toml
12
Cross.toml
@@ -15,15 +15,23 @@ pre-build = [
|
||||
"apt-get update && apt-get install --assume-yes --no-install-recommends llvm-dev libclang-dev clang",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
# Why do we need a custom Dockerfile on Windows:
|
||||
# 1. `reth-libmdbx` stopped working with MinGW 9.3 that cross image comes with.
|
||||
# 2. To be able to update the version of MinGW, we need to also update the Ubuntu that the image is based on.
|
||||
#
|
||||
# Also see https://github.com/cross-rs/cross/issues/1667
|
||||
# Inspired by https://github.com/cross-rs/cross/blob/9e2298e17170655342d3248a9c8ac37ef92ba38f/docker/Dockerfile.x86_64-pc-windows-gnu#L51
|
||||
dockerfile = "./Dockerfile.x86_64-pc-windows-gnu"
|
||||
|
||||
[target.riscv64gc-unknown-linux-gnu]
|
||||
image = "ubuntu:24.04"
|
||||
pre-build = [
|
||||
"apt update",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libclang-dev make",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu libclang-dev make",
|
||||
]
|
||||
env.passthrough = [
|
||||
"CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc",
|
||||
"CXX_riscv64gc_unknown_linux_gnu=riscv64-linux-gnu-g++",
|
||||
]
|
||||
|
||||
[build.env]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.7-labs
|
||||
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.93 AS chef
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
|
||||
15
Dockerfile.cross
Normal file
15
Dockerfile.cross
Normal file
@@ -0,0 +1,15 @@
|
||||
# This image is meant to enable cross-architecture builds.
|
||||
# It assumes the reth binary has already been compiled for `$TARGETPLATFORM` and is
|
||||
# locatable in `./dist/bin/$TARGETARCH`
|
||||
FROM --platform=$TARGETPLATFORM ubuntu:22.04
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
# Filled by docker buildx
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY ./dist/bin/$TARGETARCH/reth /usr/local/bin/reth
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/reth"]
|
||||
@@ -1,90 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Dockerfile for reth, optimized for Depot builds
|
||||
# Usage:
|
||||
# reth: --build-arg BINARY=reth
|
||||
|
||||
FROM rust:1.93 AS builder
|
||||
WORKDIR /app
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
RUN apt-get update && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
# Install sccache for compilation caching
|
||||
RUN cargo install sccache --locked
|
||||
ENV RUSTC_WRAPPER=sccache
|
||||
ENV SCCACHE_DIR=/sccache
|
||||
ENV SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev
|
||||
|
||||
# Binary to build
|
||||
ARG BINARY=reth
|
||||
|
||||
# Manifest path for the binary
|
||||
ARG MANIFEST_PATH=bin/reth
|
||||
|
||||
# Build profile, release by default
|
||||
ARG BUILD_PROFILE=release
|
||||
ENV BUILD_PROFILE=$BUILD_PROFILE
|
||||
|
||||
# Extra Cargo flags (can be overridden, otherwise set per-platform below)
|
||||
ARG RUSTFLAGS=""
|
||||
|
||||
# Extra Cargo features
|
||||
ARG FEATURES=""
|
||||
ENV FEATURES=$FEATURES
|
||||
|
||||
# Git info for vergen (since .git is excluded from Docker context)
|
||||
ARG VERGEN_GIT_SHA=""
|
||||
ARG VERGEN_GIT_DESCRIBE=""
|
||||
ARG VERGEN_GIT_DIRTY="false"
|
||||
ENV VERGEN_GIT_SHA=$VERGEN_GIT_SHA
|
||||
ENV VERGEN_GIT_DESCRIBE=$VERGEN_GIT_DESCRIBE
|
||||
ENV VERGEN_GIT_DIRTY=$VERGEN_GIT_DIRTY
|
||||
|
||||
# Build application
|
||||
# Platform-specific RUSTFLAGS: amd64 uses x86-64-v3 (Haswell+) with pclmulqdq for rocksdb
|
||||
ARG TARGETPLATFORM
|
||||
COPY --exclude=.git . .
|
||||
RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
|
||||
--mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=shared \
|
||||
export RUSTC_WRAPPER=sccache SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev SCCACHE_DIR=/sccache && \
|
||||
sccache --start-server && \
|
||||
if [ -n "$RUSTFLAGS" ]; then \
|
||||
export RUSTFLAGS="$RUSTFLAGS"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
|
||||
fi && \
|
||||
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml && \
|
||||
sccache --show-stats
|
||||
|
||||
# Copy binary to a known location (ARG not resolved in COPY)
|
||||
# Note: Custom profiles like maxperf/profiling output to target/<profile>/, not target/release/
|
||||
RUN cp /app/target/$BUILD_PROFILE/$BINARY /app/binary || \
|
||||
cp /app/target/release/$BINARY /app/binary
|
||||
|
||||
FROM ubuntu:24.04 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Binary name for entrypoint
|
||||
ARG BINARY=reth
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy binary from build stage and create canonical symlink for entrypoint
|
||||
COPY --from=builder /app/binary /usr/local/bin/
|
||||
RUN mv /usr/local/bin/binary /usr/local/bin/$BINARY && \
|
||||
ln -s /usr/local/bin/$BINARY /usr/local/bin/reth-binary && \
|
||||
chmod +x /usr/local/bin/$BINARY
|
||||
|
||||
# Copy licenses
|
||||
COPY LICENSE-* ./
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/reth-binary"]
|
||||
79
Dockerfile.x86_64-pc-windows-gnu
Normal file
79
Dockerfile.x86_64-pc-windows-gnu
Normal file
@@ -0,0 +1,79 @@
|
||||
FROM ubuntu:24.04 AS cross-base
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Use HTTPS for package sources
|
||||
RUN apt-get update && apt-get install --assume-yes --no-install-recommends ca-certificates
|
||||
RUN find /etc/apt/ -type f \( -name '*.list' -o -name '*.sources' \) -exec sed -i 's|http://|https://|g' {} +
|
||||
|
||||
# Configure APT retries and timeouts to handle network issues
|
||||
RUN echo 'Acquire::Retries \"3\";' > /etc/apt/apt.conf.d/80-retries && \
|
||||
echo 'Acquire::http::Timeout \"60\";' >> /etc/apt/apt.conf.d/80-retries && \
|
||||
echo 'Acquire::ftp::Timeout \"60\";' >> /etc/apt/apt.conf.d/80-retries
|
||||
|
||||
# configure fallback mirrors
|
||||
RUN sed -i 's|URIs: https://archive.ubuntu.com/ubuntu/|URIs: https://mirror.cov.ukservers.com/ubuntu/ https://archive.ubuntu.com/ubuntu/ https://mirror.ox.ac.uk/sites/archive.ubuntu.com/ubuntu/|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
|
||||
RUN apt-get update && apt-get install --assume-yes --no-install-recommends git
|
||||
|
||||
RUN git clone https://github.com/cross-rs/cross /cross
|
||||
WORKDIR /cross/docker
|
||||
RUN git checkout baf457efc2555225af47963475bd70e8d2f5993f
|
||||
|
||||
# xargo doesn't work with Rust 1.89 and higher: https://github.com/cross-rs/cross/issues/1701.
|
||||
#
|
||||
# When this PR https://github.com/cross-rs/cross/pull/1580 is merged,
|
||||
# we can update the checkout above and remove this replacement.
|
||||
RUN sed -i 's|sh rustup-init.sh -y --no-modify-path --profile minimal|sh rustup-init.sh -y --no-modify-path --profile minimal --default-toolchain=1.88.0|' xargo.sh
|
||||
|
||||
RUN cp common.sh lib.sh / && /common.sh
|
||||
RUN cp cmake.sh / && /cmake.sh
|
||||
RUN cp xargo.sh / && /xargo.sh
|
||||
|
||||
FROM cross-base AS build
|
||||
|
||||
RUN apt-get install --assume-yes --no-install-recommends libz-mingw-w64-dev g++-mingw-w64-x86-64 gfortran-mingw-w64-x86-64
|
||||
|
||||
# Install Wine using OpenSUSE repository because official one is often lagging behind
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get install --assume-yes --no-install-recommends wget gpg && \
|
||||
mkdir -pm755 /etc/apt/keyrings && curl -fsSL \
|
||||
https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_24.04/Release.key \
|
||||
| tee /etc/apt/keyrings/obs-winehq.key >/dev/null && \
|
||||
echo "deb [arch=amd64,i386 signed-by=/etc/apt/keyrings/obs-winehq.key] \
|
||||
https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_24.04/ ./" \
|
||||
| tee /etc/apt/sources.list.d/obs-winehq.list && \
|
||||
apt-get update && apt-get install --assume-yes --install-recommends winehq-stable
|
||||
|
||||
# run-detectors are responsible for calling the correct interpreter for exe
|
||||
# files. For some reason it does not work inside a docker container (it works
|
||||
# fine in the host). So we replace the usual paths of run-detectors to run wine
|
||||
# directly. This only affects the guest, we are not messing up with the host.
|
||||
#
|
||||
# See /usr/share/doc/binfmt-support/detectors
|
||||
RUN mkdir -p /usr/lib/binfmt-support/ && \
|
||||
rm -f /usr/lib/binfmt-support/run-detectors /usr/bin/run-detectors && \
|
||||
ln -s /usr/bin/wine /usr/lib/binfmt-support/run-detectors && \
|
||||
ln -s /usr/bin/wine /usr/bin/run-detectors
|
||||
|
||||
RUN cp windows-entry.sh /
|
||||
ENTRYPOINT ["/windows-entry.sh"]
|
||||
|
||||
RUN cp toolchain.cmake /opt/toolchain.cmake
|
||||
|
||||
# for why we always link with pthread support, see:
|
||||
# https://github.com/cross-rs/cross/pull/1123#issuecomment-1312287148
|
||||
ENV CROSS_TOOLCHAIN_PREFIX=x86_64-w64-mingw32-
|
||||
ENV CROSS_TOOLCHAIN_SUFFIX=-posix
|
||||
ENV CROSS_SYSROOT=/usr/x86_64-w64-mingw32
|
||||
ENV CROSS_TARGET_RUNNER="env -u CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER wine"
|
||||
ENV CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$CROSS_TOOLCHAIN_PREFIX"gcc"$CROSS_TOOLCHAIN_SUFFIX" \
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER="$CROSS_TARGET_RUNNER" \
|
||||
AR_x86_64_pc_windows_gnu="$CROSS_TOOLCHAIN_PREFIX"ar \
|
||||
CC_x86_64_pc_windows_gnu="$CROSS_TOOLCHAIN_PREFIX"gcc"$CROSS_TOOLCHAIN_SUFFIX" \
|
||||
CXX_x86_64_pc_windows_gnu="$CROSS_TOOLCHAIN_PREFIX"g++"$CROSS_TOOLCHAIN_SUFFIX" \
|
||||
CMAKE_TOOLCHAIN_FILE_x86_64_pc_windows_gnu=/opt/toolchain.cmake \
|
||||
BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--sysroot=$CROSS_SYSROOT -idirafter/usr/include" \
|
||||
CROSS_CMAKE_SYSTEM_NAME=Windows \
|
||||
CROSS_CMAKE_SYSTEM_PROCESSOR=AMD64 \
|
||||
CROSS_CMAKE_CRT=gnu \
|
||||
CROSS_CMAKE_OBJECT_FLAGS="-ffunction-sections -fdata-sections -m64"
|
||||
46
DockerfileOp
Normal file
46
DockerfileOp
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
# Builds a cargo-chef plan
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
|
||||
ARG BUILD_PROFILE=maxperf
|
||||
ENV BUILD_PROFILE=$BUILD_PROFILE
|
||||
|
||||
ARG RUSTFLAGS=""
|
||||
ENV RUSTFLAGS="$RUSTFLAGS"
|
||||
|
||||
ARG FEATURES=""
|
||||
ENV FEATURES=$FEATURES
|
||||
|
||||
RUN cargo chef cook --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path recipe.json --manifest-path /app/crates/optimism/bin/Cargo.toml
|
||||
|
||||
COPY . .
|
||||
RUN cargo build --profile $BUILD_PROFILE --features "$FEATURES" --bin op-reth --manifest-path /app/crates/optimism/bin/Cargo.toml
|
||||
|
||||
RUN ls -la /app/target/$BUILD_PROFILE/op-reth
|
||||
RUN cp /app/target/$BUILD_PROFILE/op-reth /app/op-reth
|
||||
|
||||
FROM ubuntu AS runtime
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates libssl-dev pkg-config strace && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/op-reth /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/op-reth
|
||||
COPY LICENSE-* ./
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546 7545 8551
|
||||
ENTRYPOINT ["/usr/local/bin/op-reth"]
|
||||
15
DockerfileOp.cross
Normal file
15
DockerfileOp.cross
Normal file
@@ -0,0 +1,15 @@
|
||||
# This image is meant to enable cross-architecture builds.
|
||||
# It assumes the reth binary has already been compiled for `$TARGETPLATFORM` and is
|
||||
# locatable in `./dist/bin/$TARGETARCH`
|
||||
FROM --platform=$TARGETPLATFORM ubuntu:22.04
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
# Filled by docker buildx
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY ./dist/bin/$TARGETARCH/op-reth /usr/local/bin/op-reth
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/op-reth"]
|
||||
@@ -24,3 +24,18 @@
|
||||
parameters.
|
||||
- Update version specific validation checks in the `EngineValidator` trait.
|
||||
|
||||
## Op-Reth changes
|
||||
|
||||
### Updates to the engine API
|
||||
|
||||
Opstack tries to be as close to the L1 engine API as much as possible. Isthmus (Prague equivalent) introduced the first
|
||||
deviation from the L1 engine API with an additional field in the `ExecutionPayload`. For this reason the op engine API
|
||||
has its own server traits `OpEngineApi`.
|
||||
Adding a new versioned endpoint requires the same changes as for L1 just for the dedicated OP types.
|
||||
|
||||
### Hardforks
|
||||
|
||||
Opstack has dedicated hardforks (e.g. Isthmus), that can be entirely opstack specific (e.g. Holocene) or can be an L1
|
||||
equivalent hardfork. Since opstack sticks to the L1 header primitive, a new L1 equivalent hardfork also requires new
|
||||
equivalent consensus checks. For this reason these `OpHardfork` must be mapped to L1 `EthereumHardfork`, for example:
|
||||
`OpHardfork::Isthmus` corresponds to `EthereumHardfork::Prague`. These mappings must be defined in the `ChainSpec`.
|
||||
|
||||
@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022-2026 Reth Contributors
|
||||
Copyright 2022-2025 Reth Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022-2026 Reth Contributors
|
||||
Copyright (c) 2022-2025 Reth Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
214
Makefile
214
Makefile
@@ -12,14 +12,16 @@ FULL_DB_TOOLS_DIR := $(shell pwd)/$(DB_TOOLS_DIR)/
|
||||
CARGO_TARGET_DIR ?= target
|
||||
|
||||
# List of features to use when building. Can be overridden via the environment.
|
||||
FEATURES ?=
|
||||
# No jemalloc on Windows
|
||||
ifeq ($(OS),Windows_NT)
|
||||
FEATURES ?= asm-keccak min-debug-logs
|
||||
else
|
||||
FEATURES ?= jemalloc asm-keccak min-debug-logs
|
||||
endif
|
||||
|
||||
# Cargo profile for builds. Default is for local builds, CI uses an override.
|
||||
PROFILE ?= release
|
||||
|
||||
# Extra RUSTFLAGS to append to build targets (e.g., "-C target-cpu=x86-64-v3")
|
||||
EXTRA_RUSTFLAGS ?=
|
||||
|
||||
# Extra flags for Cargo
|
||||
CARGO_INSTALL_EXTRA_FLAGS ?=
|
||||
|
||||
@@ -33,6 +35,9 @@ EEST_TESTS_TAG := v4.5.0
|
||||
EEST_TESTS_URL := https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_TESTS_TAG)/fixtures_stable.tar.gz
|
||||
EEST_TESTS_DIR := ./testing/ef-tests/execution-spec-tests
|
||||
|
||||
# The docker image name
|
||||
DOCKER_IMAGE_NAME ?= ghcr.io/paradigmxyz/reth
|
||||
|
||||
##@ Help
|
||||
|
||||
.PHONY: help
|
||||
@@ -48,6 +53,13 @@ install: ## Build and install the reth binary under `$(CARGO_HOME)/bin`.
|
||||
--profile "$(PROFILE)" \
|
||||
$(CARGO_INSTALL_EXTRA_FLAGS)
|
||||
|
||||
.PHONY: install-op
|
||||
install-op: ## Build and install the op-reth binary under `$(CARGO_HOME)/bin`.
|
||||
cargo install --path crates/optimism/bin --bin op-reth --force --locked \
|
||||
--features "$(FEATURES)" \
|
||||
--profile "$(PROFILE)" \
|
||||
$(CARGO_INSTALL_EXTRA_FLAGS)
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the reth binary into `target` directory.
|
||||
cargo build --bin reth --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
@@ -70,20 +82,31 @@ build-%-reproducible:
|
||||
LC_ALL=C \
|
||||
TZ=UTC \
|
||||
JEMALLOC_OVERRIDE=/usr/lib/x86_64-linux-gnu/libjemalloc.a \
|
||||
cargo build --bin reth --features "$(FEATURES)" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
cargo build --bin reth --features "$(FEATURES) jemalloc-unprefixed" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
|
||||
|
||||
.PHONY: build-debug
|
||||
build-debug: ## Build the reth binary into `target/debug` directory.
|
||||
cargo build --bin reth --features "$(FEATURES)"
|
||||
.PHONY: build-debug-op
|
||||
build-debug-op: ## Build the op-reth binary into `target/debug` directory.
|
||||
cargo build --bin op-reth --features "$(FEATURES)" --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
.PHONY: build-op
|
||||
build-op: ## Build the op-reth binary into `target` directory.
|
||||
cargo build --bin op-reth --features "$(FEATURES)" --profile "$(PROFILE)" --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
# Builds the reth binary natively.
|
||||
build-native-%:
|
||||
$(if $(EXTRA_RUSTFLAGS),RUSTFLAGS="$(EXTRA_RUSTFLAGS)") cargo build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
cargo build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
|
||||
op-build-native-%:
|
||||
cargo build --bin op-reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)" --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
# The following commands use `cross` to build a cross-compile.
|
||||
#
|
||||
# These commands require that:
|
||||
#
|
||||
# - `cross` is installed (`cargo install --locked cross`).
|
||||
# - `cross` is installed (`cargo install cross`).
|
||||
# - Docker is running.
|
||||
# - The current user is in the `docker` group.
|
||||
#
|
||||
@@ -95,14 +118,22 @@ build-native-%:
|
||||
# on other systems. JEMALLOC_SYS_WITH_LG_PAGE=16 tells jemalloc to use 64-KiB
|
||||
# pages. See: https://github.com/paradigmxyz/reth/issues/6742
|
||||
build-aarch64-unknown-linux-gnu: export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
build-native-aarch64-unknown-linux-gnu: export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
op-build-aarch64-unknown-linux-gnu: export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
|
||||
# No jemalloc on Windows
|
||||
build-x86_64-pc-windows-gnu: FEATURES := $(filter-out jemalloc jemalloc-prof,$(FEATURES))
|
||||
op-build-x86_64-pc-windows-gnu: FEATURES := $(filter-out jemalloc jemalloc-prof,$(FEATURES))
|
||||
|
||||
# Note: The additional rustc compiler flags are for intrinsics needed by MDBX.
|
||||
# See: https://github.com/cross-rs/cross/wiki/FAQ#undefined-reference-with-build-std
|
||||
build-%:
|
||||
RUSTFLAGS="-C link-arg=-lgcc -Clink-arg=-static-libgcc $(EXTRA_RUSTFLAGS)" \
|
||||
RUSTFLAGS="-C link-arg=-lgcc -Clink-arg=-static-libgcc" \
|
||||
cross build --bin reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)"
|
||||
|
||||
op-build-%:
|
||||
RUSTFLAGS="-C link-arg=-lgcc -Clink-arg=-static-libgcc" \
|
||||
cross build --bin op-reth --target $* --features "$(FEATURES)" --profile "$(PROFILE)" --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
# Unfortunately we can't easily use cross to build for Darwin because of licensing issues.
|
||||
# If we wanted to, we would need to build a custom Docker image with the SDK available.
|
||||
#
|
||||
@@ -113,6 +144,11 @@ build-x86_64-apple-darwin:
|
||||
$(MAKE) build-native-x86_64-apple-darwin
|
||||
build-aarch64-apple-darwin:
|
||||
$(MAKE) build-native-aarch64-apple-darwin
|
||||
op-build-x86_64-apple-darwin:
|
||||
$(MAKE) op-build-native-x86_64-apple-darwin
|
||||
op-build-aarch64-apple-darwin:
|
||||
$(MAKE) op-build-native-aarch64-apple-darwin
|
||||
|
||||
build-deb-%:
|
||||
@case "$*" in \
|
||||
x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu|riscv64gc-unknown-linux-gnu) \
|
||||
@@ -148,6 +184,8 @@ build-release-tarballs: ## Create a series of `.tar.gz` files in the BIN_DIR dir
|
||||
$(call tarball_release_binary,"x86_64-unknown-linux-gnu","reth","")
|
||||
$(MAKE) build-aarch64-unknown-linux-gnu
|
||||
$(call tarball_release_binary,"aarch64-unknown-linux-gnu","reth","")
|
||||
$(MAKE) build-x86_64-pc-windows-gnu
|
||||
$(call tarball_release_binary,"x86_64-pc-windows-gnu","reth.exe","")
|
||||
|
||||
##@ Test
|
||||
|
||||
@@ -157,7 +195,7 @@ COV_FILE := lcov.info
|
||||
.PHONY: test-unit
|
||||
test-unit: ## Run unit tests.
|
||||
cargo install cargo-nextest --locked
|
||||
cargo nextest run --no-fail-fast $(UNIT_TEST_ARGS)
|
||||
cargo nextest run $(UNIT_TEST_ARGS)
|
||||
|
||||
|
||||
.PHONY: cov-unit
|
||||
@@ -190,7 +228,7 @@ $(EEST_TESTS_DIR):
|
||||
|
||||
.PHONY: ef-tests
|
||||
ef-tests: $(EF_TESTS_DIR) $(EEST_TESTS_DIR) ## Runs Legacy and EEST tests.
|
||||
cargo nextest run --no-fail-fast -p ef-tests --release --features ef-tests
|
||||
cargo nextest run -p ef-tests --release --features ef-tests
|
||||
|
||||
##@ reth-bench
|
||||
|
||||
@@ -204,6 +242,127 @@ install-reth-bench: ## Build and install the reth binary under `$(CARGO_HOME)/bi
|
||||
--features "$(FEATURES)" \
|
||||
--profile "$(PROFILE)"
|
||||
|
||||
##@ Docker
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push
|
||||
docker-build-push: ## Build and push a cross-arch Docker image tagged with the latest git tag.
|
||||
$(call docker_build_push,$(GIT_TAG),$(GIT_TAG))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push-git-sha
|
||||
docker-build-push-git-sha: ## Build and push a cross-arch Docker image tagged with the latest git sha.
|
||||
$(call docker_build_push,$(GIT_SHA),$(GIT_SHA))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push-latest
|
||||
docker-build-push-latest: ## Build and push a cross-arch Docker image tagged with the latest git tag and `latest`.
|
||||
$(call docker_build_push,$(GIT_TAG),latest)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: docker-build-push-nightly
|
||||
docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call docker_build_push,nightly,nightly)
|
||||
|
||||
# Create a cross-arch Docker image with the given tags and push it
|
||||
define docker_build_push
|
||||
$(MAKE) build-x86_64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/amd64
|
||||
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/amd64/reth
|
||||
|
||||
$(MAKE) build-aarch64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/arm64
|
||||
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/arm64/reth
|
||||
|
||||
docker buildx build --file ./Dockerfile.cross . \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(1) \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(2) \
|
||||
--provenance=false \
|
||||
--push
|
||||
endef
|
||||
|
||||
##@ Optimism docker
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push
|
||||
op-docker-build-push: ## Build and push a cross-arch Docker image tagged with the latest git tag.
|
||||
$(call op_docker_build_push,$(GIT_TAG),$(GIT_TAG))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push-git-sha
|
||||
op-docker-build-push-git-sha: ## Build and push a cross-arch Docker image tagged with the latest git sha.
|
||||
$(call op_docker_build_push,$(GIT_SHA),$(GIT_SHA))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push-latest
|
||||
op-docker-build-push-latest: ## Build and push a cross-arch Docker image tagged with the latest git tag and `latest`.
|
||||
$(call op_docker_build_push,$(GIT_TAG),latest)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: op-docker-build-push-nightly
|
||||
op-docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call op_docker_build_push,nightly,nightly)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: docker-build-push-nightly-profiling
|
||||
docker-build-push-nightly-profiling: ## Build and push cross-arch Docker image with profiling profile tagged with nightly-profiling.
|
||||
$(call docker_build_push,nightly-profiling,nightly-profiling)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: op-docker-build-push-nightly-profiling
|
||||
op-docker-build-push-nightly-profiling: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call op_docker_build_push,nightly-profiling,nightly-profiling)
|
||||
|
||||
|
||||
# Create a cross-arch Docker image with the given tags and push it
|
||||
define op_docker_build_push
|
||||
$(MAKE) op-build-x86_64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/amd64
|
||||
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/amd64/op-reth
|
||||
|
||||
$(MAKE) op-build-aarch64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/arm64
|
||||
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/arm64/op-reth
|
||||
|
||||
docker buildx build --file ./DockerfileOp.cross . \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(1) \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(2) \
|
||||
--provenance=false \
|
||||
--push
|
||||
endef
|
||||
|
||||
##@ Other
|
||||
|
||||
.PHONY: clean
|
||||
@@ -231,21 +390,30 @@ db-tools: ## Compile MDBX debugging tools.
|
||||
@echo "Run \"$(DB_TOOLS_DIR)/mdbx_chk\" for the MDBX db file integrity check."
|
||||
|
||||
.PHONY: update-book-cli
|
||||
update-book-cli: build-debug ## Update book cli documentation.
|
||||
update-book-cli: build-debug build-debug-op## Update book cli documentation.
|
||||
@echo "Updating book cli doc..."
|
||||
@./docs/cli/update.sh $(CARGO_TARGET_DIR)/debug/reth
|
||||
@./docs/cli/update.sh $(CARGO_TARGET_DIR)/debug/reth $(CARGO_TARGET_DIR)/debug/op-reth
|
||||
|
||||
.PHONY: profiling
|
||||
profiling: ## Builds `reth` with optimisations, but also symbols.
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features jemalloc,asm-keccak
|
||||
|
||||
.PHONY: profiling-op
|
||||
profiling-op: ## Builds `op-reth` with optimisations, but also symbols.
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features jemalloc,asm-keccak --bin op-reth --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
.PHONY: maxperf
|
||||
maxperf: ## Builds `reth` with the most aggressive optimisations.
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --features jemalloc,asm-keccak
|
||||
|
||||
.PHONY: maxperf-op
|
||||
maxperf-op: ## Builds `op-reth` with the most aggressive optimisations.
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --features jemalloc,asm-keccak --bin op-reth --manifest-path crates/optimism/bin/Cargo.toml
|
||||
|
||||
.PHONY: maxperf-no-asm
|
||||
maxperf-no-asm: ## Builds `reth` with the most aggressive optimisations, minus the "asm-keccak" feature.
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --no-default-features --features jemalloc,min-debug-logs,otlp,otlp-logs,reth-revm/portable,js-tracer,keccak-cache-global,rocksdb
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --features jemalloc
|
||||
|
||||
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
@@ -260,12 +428,23 @@ clippy:
|
||||
--all-features \
|
||||
-- -D warnings
|
||||
|
||||
clippy-op-dev:
|
||||
cargo +nightly clippy \
|
||||
--bin op-reth \
|
||||
--workspace \
|
||||
--lib \
|
||||
--examples \
|
||||
--tests \
|
||||
--benches \
|
||||
--locked \
|
||||
--all-features
|
||||
|
||||
lint-typos: ensure-typos
|
||||
typos
|
||||
|
||||
ensure-typos:
|
||||
@if ! command -v typos &> /dev/null; then \
|
||||
echo "typos not found. Please install it by running the command 'cargo install --locked typos-cli' or refer to the following link for more information: https://github.com/crate-ci/typos"; \
|
||||
echo "typos not found. Please install it by running the command 'cargo install typos-cli' or refer to the following link for more information: https://github.com/crate-ci/typos"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -324,6 +503,7 @@ rustdocs: ## Runs `cargo docs` to generate the Rust documents in the `target/doc
|
||||
cargo-test:
|
||||
cargo test \
|
||||
--workspace \
|
||||
--bin "op-reth" \
|
||||
--lib --examples \
|
||||
--tests \
|
||||
--benches \
|
||||
|
||||
69
README.md
69
README.md
@@ -7,9 +7,9 @@
|
||||
|
||||
**Modular, contributor-friendly and blazing-fast implementation of the Ethereum protocol**
|
||||
|
||||

|
||||

|
||||
|
||||
**[Install](https://reth.rs/installation/installation)**
|
||||
**[Install](https://paradigmxyz.github.io/reth/installation/installation.html)**
|
||||
| [User Docs](https://reth.rs)
|
||||
| [Developer Docs](./docs)
|
||||
| [Crate Docs](https://reth.rs/docs)
|
||||
@@ -20,41 +20,44 @@
|
||||
|
||||
## What is Reth?
|
||||
|
||||
Reth (short for Rust Ethereum, [pronunciation](https://x.com/kelvinfichter/status/1597653609411268608)) is a production-ready Ethereum execution layer client focused on modularity, performance, and user-friendliness. Reth is compatible with all Ethereum Consensus Layer (CL) implementations that support the [Engine API](https://github.com/ethereum/execution-apis/tree/a0d03086564ab1838b462befbc083f873dcf0c0f/src/engine). It is built and driven forward by [Paradigm](https://paradigm.xyz/), and is licensed under the Apache and MIT licenses.
|
||||
|
||||
> **Note:** OP-Reth has moved to [ethereum-optimism/optimism](https://github.com/ethereum-optimism/optimism). Git history has been preserved.
|
||||
Reth (short for Rust Ethereum, [pronunciation](https://x.com/kelvinfichter/status/1597653609411268608)) is a new Ethereum full node implementation that is focused on being user-friendly, highly modular, as well as being fast and efficient. Reth is an Execution Layer (EL) and is compatible with all Ethereum Consensus Layer (CL) implementations that support the [Engine API](https://github.com/ethereum/execution-apis/tree/a0d03086564ab1838b462befbc083f873dcf0c0f/src/engine). It is originally built and driven forward by [Paradigm](https://paradigm.xyz/), and is licensed under the Apache and MIT licenses.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Modularity**: Every component is built to be used as a library: well-tested, documented and benchmarked. Import crates, mix and match, and innovate on top of them. Learn more about the project's components [here](./docs/repo/layout.md).
|
||||
2. **Performance**: Built with Rust, [Alloy](https://github.com/alloy-rs/alloy/), [revm](https://github.com/bluealloy/revm/), and [Foundry](https://github.com/foundry-rs/foundry/) — battle-tested and optimized for speed. Check the [ethPandaOps Lab Dashboard](https://lab.ethpandaops.io/ethereum/execution/timings) for a third-party comparison against other Ethereum clients.
|
||||
Here's what that looks like in practice, measured with [reth-bench](https://github.com/paradigmxyz/reth/tree/main/bin/reth-bench) on Ethereum Mainnet:
|
||||
As a full Ethereum node, Reth allows users to connect to the Ethereum network and interact with the Ethereum blockchain. This includes sending and receiving transactions/logs/traces, as well as accessing and interacting with smart contracts. Building a successful Ethereum node requires creating a high-quality implementation that is both secure and efficient, as well as being easy to use on consumer hardware. It also requires building a strong community of contributors who can help support and improve the software.
|
||||
|
||||

|
||||
More concretely, our goals are:
|
||||
|
||||
3. **Free for anyone to use any way they want**: Apache/MIT licensed, no business license restrictions.
|
||||
4. **Client Diversity**: More client implementations make Ethereum more antifragile.
|
||||
5. **Support as many EVM chains as possible**: Reth can sync Ethereum and other EVM chains. If you're building one, reach out.
|
||||
6. **Configurability**: Profiles for different use cases — from high-performance RPC operators to hobbyists on consumer hardware.
|
||||
1. **Modularity**: Every component of Reth is built to be used as a library: well-tested, heavily documented and benchmarked. We envision that developers will import the node's crates, mix and match, and innovate on top of them. Examples of such usage include but are not limited to spinning up standalone P2P networks, talking directly to a node's database, or "unbundling" the node into the components you need. To achieve that, we are licensing Reth under the Apache/MIT permissive license. You can learn more about the project's components [here](./docs/repo/layout.md).
|
||||
2. **Performance**: Reth aims to be fast, so we use Rust and the [Erigon staged-sync](https://erigon.substack.com/p/erigon-stage-sync-and-control-flows) node architecture. We also use our Ethereum libraries (including [Alloy](https://github.com/alloy-rs/alloy/) and [revm](https://github.com/bluealloy/revm/)) which we've battle-tested and optimized via [Foundry](https://github.com/foundry-rs/foundry/).
|
||||
3. **Free for anyone to use any way they want**: Reth is free open source software, built for the community, by the community. By licensing the software under the Apache/MIT license, we want developers to use it without being bound by business licenses, or having to think about the implications of GPL-like licenses.
|
||||
4. **Client Diversity**: The Ethereum protocol becomes more antifragile when no node implementation dominates. This ensures that if there's a software bug, the network does not finalize a bad block. By building a new client, we hope to contribute to Ethereum's antifragility.
|
||||
5. **Support as many EVM chains as possible**: We aspire that Reth can full-sync not only Ethereum, but also other chains like Optimism, Polygon, BNB Smart Chain, and more. If you're working on any of these projects, please reach out.
|
||||
6. **Configurability**: We want to solve for node operators that care about fast historical queries, but also for hobbyists who cannot operate on large hardware. We also want to support teams and individuals who want both sync from genesis and via "fast sync". We envision that Reth will be configurable enough and provide configurable "profiles" for the tradeoffs that each team faces.
|
||||
|
||||
## Status
|
||||
|
||||
Reth is production ready, and suitable for usage in mission-critical environments such as staking or high-uptime services. We also actively recommend professional node operators to switch to Reth in production for performance and cost reasons in use cases where high performance with great margins is required such as RPC, MEV, Indexing, Simulations, and P2P activities.
|
||||
|
||||
- We released **Reth 2.0** in April 2026. See the [release notes](https://github.com/paradigmxyz/reth/releases/tag/v2.0.0) and [blog post](https://www.paradigm.xyz/2026/04/releasing-reth-2-0).
|
||||
- We released 1.0 "production-ready" stable Reth in June 2024.
|
||||
- Reth completed an audit with [Sigma Prime](https://sigmaprime.io/), the developers of [Lighthouse](https://github.com/sigp/lighthouse), the Rust Consensus Layer implementation. Find it [here](./audit/sigma_prime_audit_v2.pdf).
|
||||
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)).
|
||||
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024, the last beta release.
|
||||
- We released [beta](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) on Monday March 4, 2024, our first breaking change to the database model, providing faster query speed, smaller database footprint, and allowing "history" to be mounted on separate drives.
|
||||
- We shipped iterative improvements until the last alpha release on February 28, 2024, [0.1.0-alpha.21](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.21).
|
||||
- We [initially announced](https://www.paradigm.xyz/2023/06/reth-alpha) [0.1.0-alpha.1](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.1) on June 20, 2023.
|
||||
More historical context below:
|
||||
|
||||
### Storage compatibility
|
||||
- We released 1.0 "production-ready" stable Reth in June 2024.
|
||||
- Reth completed an audit with [Sigma Prime](https://sigmaprime.io/), the developers of [Lighthouse](https://github.com/sigp/lighthouse), the Rust Consensus Layer implementation. Find it [here](./audit/sigma_prime_audit_v2.pdf).
|
||||
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)). We will publish the results soon.
|
||||
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024,the last beta release.
|
||||
- We released [beta](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) on Monday March 4, 2024, our first breaking change to the database model, providing faster query speed, smaller database footprint, and allowing "history" to be mounted on separate drives.
|
||||
- We shipped iterative improvements until the last alpha release on February 28, 2024, [0.1.0-alpha.21](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.21).
|
||||
- We [initially announced](https://www.paradigm.xyz/2023/06/reth-alpha) [0.1.0-alpha.1](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.1) on June 20, 2023.
|
||||
|
||||
Storage V2 is the default for new nodes in Reth 2.0. Existing V1 nodes continue to work, but V1 support will be removed in a future release — all users are encouraged to migrate. V2 snapshots are available at [snapshots.reth.rs](https://snapshots.reth.rs/).
|
||||
### Database compatibility
|
||||
|
||||

|
||||
We do not have any breaking database changes since beta.1, and we do not plan any in the near future.
|
||||
|
||||
Reth [v0.2.0-beta.1](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) includes
|
||||
a [set of breaking database changes](https://github.com/paradigmxyz/reth/pull/5191) that makes it impossible to use database files produced by earlier versions.
|
||||
|
||||
If you had a database produced by alpha versions of Reth, you need to drop it with `reth db drop`
|
||||
(using the same arguments such as `--config` or `--datadir` that you passed to `reth node`), and resync using the same `reth node` command you've used before.
|
||||
|
||||
## For Users
|
||||
|
||||
@@ -74,8 +77,8 @@ For a general overview of the crates, see [Project Layout](./docs/repo/layout.md
|
||||
|
||||
If you want to contribute, or follow along with contributor discussion, you can use our [main telegram](https://t.me/paradigm_reth) to chat with us about the development of Reth!
|
||||
|
||||
- Our contributor guidelines can be found in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||
- See our [contributor docs](./docs) for more information on the project. A good starting point is [Project Layout](./docs/repo/layout.md).
|
||||
- Our contributor guidelines can be found in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||
- See our [contributor docs](./docs) for more information on the project. A good starting point is [Project Layout](./docs/repo/layout.md).
|
||||
|
||||
### Building and testing
|
||||
|
||||
@@ -85,7 +88,7 @@ When updating this, also update:
|
||||
- .github/workflows/lint.yml
|
||||
-->
|
||||
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.93.0](https://blog.rust-lang.org/2026/01/22/Rust-1.93.0/).
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.88.0](https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/).
|
||||
|
||||
See the docs for detailed instructions on how to [build from source](https://reth.rs/installation/source/).
|
||||
|
||||
@@ -120,9 +123,9 @@ If you have any questions, first see if the answer to your question can be found
|
||||
|
||||
If the answer is not there:
|
||||
|
||||
- Join the [Telegram][tg-url] to get help, or
|
||||
- Open a [discussion](https://github.com/paradigmxyz/reth/discussions/new) with your question, or
|
||||
- Open an issue with [the bug](https://github.com/paradigmxyz/reth/issues/new?assignees=&labels=C-bug%2CS-needs-triage&projects=&template=bug.yml)
|
||||
- Join the [Telegram][tg-url] to get help, or
|
||||
- Open a [discussion](https://github.com/paradigmxyz/reth/discussions/new) with your question, or
|
||||
- Open an issue with [the bug](https://github.com/paradigmxyz/reth/issues/new?assignees=&labels=C-bug%2CS-needs-triage&projects=&template=bug.yml)
|
||||
|
||||
## Security
|
||||
|
||||
@@ -134,9 +137,9 @@ Reth is a new implementation of the Ethereum protocol. In the process of develop
|
||||
|
||||
None of this would have been possible without them, so big shoutout to the teams below:
|
||||
|
||||
- [Geth](https://github.com/ethereum/go-ethereum/): We would like to express our heartfelt gratitude to the go-ethereum team for their outstanding contributions to Ethereum over the years. Their tireless efforts and dedication have helped to shape the Ethereum ecosystem and make it the vibrant and innovative community it is today. Thank you for your hard work and commitment to the project.
|
||||
- [Erigon](https://github.com/ledgerwatch/erigon) (fka Turbo-Geth): Erigon pioneered the ["Staged Sync" architecture](https://erigon.substack.com/p/erigon-stage-sync-and-control-flows) that Reth is using, as well as [introduced MDBX](https://github.com/ledgerwatch/erigon/wiki/Choice-of-storage-engine) as the database of choice. We thank Erigon for pushing the state of the art research on the performance limits of Ethereum nodes.
|
||||
- [Akula](https://github.com/akula-bft/akula/): Reth uses forks of the Apache versions of Akula's [MDBX Bindings](https://github.com/paradigmxyz/reth/pull/132), [FastRLP](https://github.com/paradigmxyz/reth/pull/63) and [ECIES](https://github.com/paradigmxyz/reth/pull/80). Given that these packages were already released under the Apache License, and they implement standardized solutions, we decided not to reimplement them to iterate faster. We thank the Akula team for their contributions to the Rust Ethereum ecosystem and for publishing these packages.
|
||||
- [Geth](https://github.com/ethereum/go-ethereum/): We would like to express our heartfelt gratitude to the go-ethereum team for their outstanding contributions to Ethereum over the years. Their tireless efforts and dedication have helped to shape the Ethereum ecosystem and make it the vibrant and innovative community it is today. Thank you for your hard work and commitment to the project.
|
||||
- [Erigon](https://github.com/ledgerwatch/erigon) (fka Turbo-Geth): Erigon pioneered the ["Staged Sync" architecture](https://erigon.substack.com/p/erigon-stage-sync-and-control-flows) that Reth is using, as well as [introduced MDBX](https://github.com/ledgerwatch/erigon/wiki/Choice-of-storage-engine) as the database of choice. We thank Erigon for pushing the state of the art research on the performance limits of Ethereum nodes.
|
||||
- [Akula](https://github.com/akula-bft/akula/): Reth uses forks of the Apache versions of Akula's [MDBX Bindings](https://github.com/paradigmxyz/reth/pull/132), [FastRLP](https://github.com/paradigmxyz/reth/pull/63) and [ECIES](https://github.com/paradigmxyz/reth/pull/80). Given that these packages were already released under the Apache License, and they implement standardized solutions, we decided not to reimplement them to iterate faster. We thank the Akula team for their contributions to the Rust Ethereum ecosystem and for publishing these packages.
|
||||
|
||||
## Warning
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,107 +0,0 @@
|
||||
[package]
|
||||
name = "reth-bb"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Reth node configured for big block payload execution"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-ethereum-cli.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-node-ethereum.workspace = true
|
||||
reth-node-builder.workspace = true
|
||||
reth-node-api.workspace = true
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-engine-primitives = { workspace = true, features = ["std"] }
|
||||
reth-engine-tree.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-payload-primitives.workspace = true
|
||||
reth-provider.workspace = true
|
||||
reth-rpc-api.workspace = true
|
||||
reth-rpc-engine-api.workspace = true
|
||||
reth-evm.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-ethereum-forks.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
reth-chain-state.workspace = true
|
||||
reth-errors.workspace = true
|
||||
reth-storage-errors.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-rpc-types = { workspace = true, features = ["engine"] }
|
||||
alloy-primitives.workspace = true
|
||||
alloy-rlp.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eips.workspace = true
|
||||
alloy-evm.workspace = true
|
||||
|
||||
# tracing
|
||||
tracing.workspace = true
|
||||
|
||||
# misc
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
jsonrpsee = { workspace = true, features = ["server", "macros"] }
|
||||
async-trait.workspace = true
|
||||
derive_more.workspace = true
|
||||
crossbeam-channel.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
revm.workspace = true
|
||||
revm-primitives.workspace = true
|
||||
alloy-hardforks.workspace = true
|
||||
metrics.workspace = true
|
||||
|
||||
# std
|
||||
eyre.workspace = true
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc",
|
||||
"asm-keccak",
|
||||
"keccak-cache-global",
|
||||
"min-debug-logs",
|
||||
]
|
||||
|
||||
jemalloc = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-node-core/jemalloc",
|
||||
"reth-ethereum-cli/jemalloc",
|
||||
"reth-provider/jemalloc",
|
||||
]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"reth-ethereum-cli/asm-keccak",
|
||||
"reth-node-ethereum/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
"alloy-evm/asm-keccak",
|
||||
"revm/asm-keccak",
|
||||
"revm-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
keccak-cache-global = [
|
||||
"reth-node-core/keccak-cache-global",
|
||||
"reth-node-ethereum/keccak-cache-global",
|
||||
"alloy-primitives/keccak-cache-global",
|
||||
]
|
||||
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-ethereum-cli/min-debug-logs",
|
||||
"reth-node-core/min-debug-logs",
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
name = "reth-bb"
|
||||
path = "src/main.rs"
|
||||
@@ -1,67 +0,0 @@
|
||||
# reth-bb
|
||||
|
||||
A modified reth node for benchmarking **big block** execution — payloads that merge transactions from multiple consecutive blocks into a single block to simulate high-gas workloads.
|
||||
|
||||
> **Not for production use.** reth-bb disables some consensus-related validations to allow artificially large blocks. It is intended solely for performance benchmarking.
|
||||
|
||||
## How it works
|
||||
|
||||
reth-bb extends the standard Ethereum node with:
|
||||
|
||||
1. **Multi-segment execution** — a custom `reth_newPayload` handler that accepts optional `BigBlockData` alongside the payload. When present, the block is executed in multiple segments, each with its own EVM environment (matching the original blocks that were merged).
|
||||
|
||||
2. **Relaxed consensus** — the gas-limit bound-divisor check and blob gas validation are skipped, since merged blocks exceed single-block limits.
|
||||
|
||||
## Quick start
|
||||
|
||||
The full workflow has four steps: **build** binaries, **generate** big blocks, **start** reth-bb, and **replay** the payloads.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A synced reth datadir for the target chain (e.g. hoodi)
|
||||
- Rust toolchain
|
||||
|
||||
### 1. Build
|
||||
|
||||
```bash
|
||||
cargo build --profile profiling -p reth-bb -p reth-bench
|
||||
```
|
||||
|
||||
### 2. Generate big blocks
|
||||
|
||||
Fetch consecutive blocks from an RPC and merge them until a target gas is reached. Use `--from-block` set to the block number following the one the node is currently synced to (i.e. the next block the node would process):
|
||||
|
||||
```bash
|
||||
reth-bench generate-big-block \
|
||||
--rpc-url https://rpc.hoodi.ethpandaops.io \
|
||||
--chain hoodi \
|
||||
--from-block 910020 \
|
||||
--target-gas 2G \
|
||||
--num-big-blocks 5 \
|
||||
--output-dir /tmp/payloads
|
||||
```
|
||||
|
||||
This produces one JSON file per big block in the output directory.
|
||||
|
||||
### 3. Start reth-bb
|
||||
|
||||
```bash
|
||||
reth-bb node \
|
||||
--datadir /data/reth/hoodi \
|
||||
--chain hoodi \
|
||||
--http --http.api debug,eth \
|
||||
--authrpc.jwtsecret /tmp/jwt.hex \
|
||||
-d
|
||||
```
|
||||
|
||||
### 4. Replay payloads
|
||||
|
||||
```bash
|
||||
reth-bench replay-payloads \
|
||||
--engine-rpc-url http://localhost:8551 \
|
||||
--jwt-secret /tmp/jwt.hex \
|
||||
--payload-dir /tmp/payloads \
|
||||
--reth-new-payload
|
||||
```
|
||||
|
||||
The `--reth-new-payload` flag is required for big blocks — it uses the `reth_newPayload` endpoint which carries the multi-segment execution metadata.
|
||||
@@ -1,577 +0,0 @@
|
||||
//! Big-block executor.
|
||||
//!
|
||||
//! Provides [`BbBlockExecutor`] and [`BbBlockExecutorFactory`] which handle
|
||||
//! segment boundaries within big-block payloads.
|
||||
//!
|
||||
//! [`BbBlockExecutor`] wraps [`EthBlockExecutor`] and intercepts
|
||||
//! `execute_transaction` to apply segment-boundary changes.
|
||||
|
||||
use crate::evm_config::BigBlockSegment;
|
||||
use alloy_eips::eip7685::Requests;
|
||||
use alloy_evm::{
|
||||
block::{
|
||||
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
|
||||
BlockExecutorFor, ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
|
||||
},
|
||||
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
|
||||
precompiles::PrecompilesMap,
|
||||
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
|
||||
};
|
||||
use alloy_primitives::B256;
|
||||
use reth_ethereum_primitives::{Receipt, TransactionSigned};
|
||||
use reth_evm_ethereum::RethReceiptBuilder;
|
||||
use revm::{
|
||||
context::{BlockEnv, TxEnv},
|
||||
context_interface::result::{EVMError, HaltReason},
|
||||
handler::PrecompileProvider,
|
||||
interpreter::InterpreterResult,
|
||||
primitives::hardfork::SpecId,
|
||||
Inspector,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbEvmPlan — runtime segment tracking state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Runtime state for segment boundary tracking.
|
||||
pub(crate) struct BbEvmPlan {
|
||||
/// The segment boundaries and environments.
|
||||
pub(crate) segments: Vec<BigBlockSegment>,
|
||||
/// Index of the next segment to switch to (starts at 1).
|
||||
pub(crate) next_segment: usize,
|
||||
/// Number of user transactions executed so far.
|
||||
pub(crate) tx_counter: usize,
|
||||
/// Block hashes to seed for inter-segment BLOCKHASH resolution.
|
||||
/// Includes both prior block hashes and inter-segment hashes.
|
||||
pub(crate) block_hashes_to_seed: Vec<(u64, B256)>,
|
||||
}
|
||||
|
||||
impl BbEvmPlan {
|
||||
/// Creates a new `BbEvmPlan` from segments and hardfork flags.
|
||||
pub(crate) fn new(segments: Vec<BigBlockSegment>) -> Self {
|
||||
// Pre-compute all inter-segment block hashes.
|
||||
let mut block_hashes_to_seed = Vec::new();
|
||||
for seg in segments.iter().skip(1) {
|
||||
let finished_block_number = seg.evm_env.block_env.number.saturating_to::<u64>() - 1;
|
||||
let finished_block_hash = seg.ctx.parent_hash;
|
||||
block_hashes_to_seed.push((finished_block_number, finished_block_hash));
|
||||
}
|
||||
|
||||
Self { segments, next_segment: 1, tx_counter: 0, block_hashes_to_seed }
|
||||
}
|
||||
|
||||
/// Returns the 256 block hashes relevant to a segment with the given block
|
||||
/// number. BLOCKHASH can look back 256 blocks, so we select entries in
|
||||
/// `[block_number - 256, block_number)`.
|
||||
pub(crate) fn hashes_for_block(&self, block_number: u64) -> Vec<(u64, B256)> {
|
||||
let min = block_number.saturating_sub(256);
|
||||
self.block_hashes_to_seed
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(n, _)| *n >= min && *n < block_number)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BbEvmPlan {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BbEvmPlan")
|
||||
.field("segments", &self.segments)
|
||||
.field("next_segment", &self.next_segment)
|
||||
.field("tx_counter", &self.tx_counter)
|
||||
.field("block_hashes_to_seed", &self.block_hashes_to_seed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbBlockExecutor — handles segment boundaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Function pointer that seeds block hashes into the DB's block hash cache.
|
||||
///
|
||||
/// Injected from `ConfigureEvm::create_executor` where the concrete `State<DB>`
|
||||
/// type is known, allowing `BbBlockExecutor` to reseed the ring buffer at
|
||||
/// segment boundaries without requiring additional trait bounds on `DB`.
|
||||
pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
|
||||
|
||||
/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
|
||||
/// changes for big-block execution.
|
||||
///
|
||||
/// At segment boundaries, the inner executor is finished (applying its
|
||||
/// end-of-block logic: post-execution system calls, withdrawal balance
|
||||
/// increments) and a new one is constructed for the next segment (applying
|
||||
/// its start-of-block logic: EIP-2935/EIP-4788 system calls).
|
||||
///
|
||||
/// Gas counters reset at each boundary so that each segment's real gas limit
|
||||
/// is used (preserving correct GASLIMIT opcode behavior). Accumulated offsets
|
||||
/// are applied to receipts and totals in `finish()`.
|
||||
pub(crate) struct BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: Database,
|
||||
{
|
||||
/// The inner executor. `None` transiently during `apply_segment_boundary`.
|
||||
inner: Option<EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder>>,
|
||||
plan: Option<BbEvmPlan>,
|
||||
/// Requests accumulated from segments that have been finished at
|
||||
/// boundaries. Merged into the final result in `finish()`.
|
||||
accumulated_requests: Requests,
|
||||
/// Cumulative gas used by all segments that have been finished at
|
||||
/// boundaries. Added to receipts and the final gas total in `finish()`.
|
||||
gas_used_offset: u64,
|
||||
/// Cumulative blob gas used by all segments that have been finished at
|
||||
/// boundaries.
|
||||
blob_gas_used_offset: u64,
|
||||
/// Shared state hook that survives inner executor finish/reconstruct
|
||||
/// cycles at segment boundaries. Each inner executor receives a
|
||||
/// forwarding hook that delegates to this shared instance.
|
||||
shared_hook: Arc<Mutex<Option<Box<dyn OnStateHook>>>>,
|
||||
/// Callback to reseed block hashes into the DB's cache at segment
|
||||
/// boundaries. See [`BlockHashSeeder`].
|
||||
block_hash_seeder: Option<BlockHashSeeder<DB>>,
|
||||
}
|
||||
|
||||
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: StateDB,
|
||||
I: Inspector<EthEvmContext<DB>>,
|
||||
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
|
||||
EthEvm<DB, I, P>: Evm<
|
||||
DB = DB,
|
||||
Tx = TxEnv,
|
||||
HaltReason = HaltReason,
|
||||
Error = EVMError<DB::Error>,
|
||||
Spec = SpecId,
|
||||
BlockEnv = BlockEnv,
|
||||
>,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
evm: EthEvm<DB, I, P>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
spec: Spec,
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
plan: Option<BbEvmPlan>,
|
||||
block_hash_seeder: Option<BlockHashSeeder<DB>>,
|
||||
) -> Self {
|
||||
let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
|
||||
Self {
|
||||
inner: Some(inner),
|
||||
plan,
|
||||
accumulated_requests: Requests::default(),
|
||||
gas_used_offset: 0,
|
||||
blob_gas_used_offset: 0,
|
||||
shared_hook: Arc::new(Mutex::new(None)),
|
||||
block_hash_seeder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a forwarding `OnStateHook` that delegates to the shared hook.
|
||||
fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
|
||||
let shared = self.shared_hook.clone();
|
||||
Some(Box::new(move |source: StateChangeSource, state: &revm::state::EvmState| {
|
||||
if let Some(hook) = shared.lock().unwrap().as_mut() {
|
||||
hook.on_state(source, state);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const fn inner(&self) -> &EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
|
||||
self.inner.as_ref().expect("inner executor must exist")
|
||||
}
|
||||
|
||||
const fn inner_mut(
|
||||
&mut self,
|
||||
) -> &mut EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
|
||||
self.inner.as_mut().expect("inner executor must exist")
|
||||
}
|
||||
|
||||
fn reseed_block_hashes_for(&mut self, block_number: u64) {
|
||||
let Some(seeder) = self.block_hash_seeder else { return };
|
||||
let hashes = match &self.plan {
|
||||
Some(plan) => plan.hashes_for_block(block_number),
|
||||
None => return,
|
||||
};
|
||||
seeder(self.inner_mut().evm_mut().db_mut(), &hashes);
|
||||
}
|
||||
|
||||
fn maybe_apply_boundary(&mut self) -> Result<(), BlockExecutionError> {
|
||||
loop {
|
||||
let plan = match &self.plan {
|
||||
Some(p) => p,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if plan.next_segment >= plan.segments.len() ||
|
||||
plan.tx_counter != plan.segments[plan.next_segment].start_tx
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.apply_segment_boundary()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_segment_boundary(&mut self) -> Result<(), BlockExecutionError> {
|
||||
let plan = self.plan.as_mut().expect("plan must exist");
|
||||
let seg_idx = plan.next_segment;
|
||||
let prev_seg_idx = seg_idx - 1;
|
||||
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
seg_idx,
|
||||
tx_counter = plan.tx_counter,
|
||||
"Applying segment boundary"
|
||||
);
|
||||
|
||||
// Swap the inner executor's ctx to the finished segment's values so
|
||||
// that finish() applies the correct withdrawals and post-execution
|
||||
// system calls for that segment.
|
||||
let prev_segment = &plan.segments[prev_seg_idx];
|
||||
let prev_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: prev_segment.ctx.parent_hash,
|
||||
parent_beacon_block_root: prev_segment.ctx.parent_beacon_block_root,
|
||||
ommers: prev_segment.ctx.ommers,
|
||||
withdrawals: prev_segment.ctx.withdrawals.clone(),
|
||||
extra_data: prev_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: prev_segment.ctx.tx_count_hint,
|
||||
slot_number: prev_segment.ctx.slot_number,
|
||||
};
|
||||
|
||||
// Clone the next segment's data before we consume inner.
|
||||
let new_segment = &plan.segments[seg_idx];
|
||||
let new_block_env = new_segment.evm_env.block_env.clone();
|
||||
let mut new_cfg_env = new_segment.evm_env.cfg_env.clone();
|
||||
new_cfg_env.disable_base_fee = true;
|
||||
let new_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: new_segment.ctx.parent_hash,
|
||||
parent_beacon_block_root: new_segment.ctx.parent_beacon_block_root,
|
||||
ommers: new_segment.ctx.ommers,
|
||||
withdrawals: new_segment.ctx.withdrawals.clone(),
|
||||
extra_data: new_segment.ctx.extra_data.clone(),
|
||||
tx_count_hint: new_segment.ctx.tx_count_hint,
|
||||
slot_number: new_segment.ctx.slot_number,
|
||||
};
|
||||
|
||||
plan.next_segment += 1;
|
||||
|
||||
// Finish the inner executor for the completed segment. This applies
|
||||
// post-execution system calls (EIP-7002/7251) and withdrawal balance
|
||||
// increments via EthBlockExecutor::finish().
|
||||
let mut inner = self.inner.take().expect("inner executor must exist");
|
||||
inner.ctx = prev_ctx;
|
||||
let spec = inner.spec.clone();
|
||||
let receipt_builder = inner.receipt_builder;
|
||||
let (mut evm, result) = inner.finish()?;
|
||||
|
||||
// Receipts already have globally-correct cumulative_gas_used (fixed
|
||||
// up in commit_transaction). Update the offset with this segment's
|
||||
// gas so that subsequent segments' receipts are adjusted correctly.
|
||||
self.gas_used_offset += result.gas_used;
|
||||
self.blob_gas_used_offset += result.blob_gas_used;
|
||||
self.accumulated_requests.extend(result.requests);
|
||||
|
||||
let last_receipt_cumulative =
|
||||
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
|
||||
let seg_block_number = prev_segment.evm_env.block_env.number.saturating_to::<u64>();
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
prev_seg_idx,
|
||||
seg_block_number,
|
||||
segment_gas_used = result.gas_used,
|
||||
gas_used_offset = self.gas_used_offset,
|
||||
last_receipt_cumulative,
|
||||
receipt_count = result.receipts.len(),
|
||||
"Finished segment"
|
||||
);
|
||||
|
||||
// Swap EVM env to the next segment's values (using real gas_limit).
|
||||
let ctx = evm.ctx_mut();
|
||||
ctx.block = new_block_env;
|
||||
ctx.cfg = new_cfg_env;
|
||||
|
||||
// Build a new inner executor for the next segment. gas_used starts
|
||||
// at 0 so the per-transaction gas check uses this segment's real
|
||||
// gas_limit correctly.
|
||||
let mut new_inner = EthBlockExecutor::new(evm, new_ctx, spec, receipt_builder);
|
||||
|
||||
// Carry forward receipts from prior segments.
|
||||
new_inner.receipts = result.receipts;
|
||||
|
||||
// Re-install the forwarding state hook so the parallel state root
|
||||
// task continues to receive state changes.
|
||||
if self.shared_hook.lock().unwrap().is_some() {
|
||||
new_inner.set_state_hook(self.forwarding_hook());
|
||||
}
|
||||
|
||||
self.inner = Some(new_inner);
|
||||
|
||||
// Reseed the block hash cache for the new segment's 256-block window
|
||||
// before applying pre-execution changes (which may use BLOCKHASH).
|
||||
let new_block_number = self.plan.as_ref().unwrap().segments[seg_idx]
|
||||
.evm_env
|
||||
.block_env
|
||||
.number
|
||||
.saturating_to::<u64>();
|
||||
self.reseed_block_hashes_for(new_block_number);
|
||||
|
||||
// Apply pre-execution changes for the new segment (EIP-2935, EIP-4788).
|
||||
self.inner_mut().apply_pre_execution_changes()?;
|
||||
|
||||
trace!(target: "engine::bb::evm", "Started segment {seg_idx}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, DB, I, P, Spec> BlockExecutor for BbBlockExecutor<'a, DB, I, P, Spec>
|
||||
where
|
||||
DB: StateDB,
|
||||
I: Inspector<EthEvmContext<DB>>,
|
||||
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
|
||||
EthEvm<DB, I, P>: Evm<
|
||||
DB = DB,
|
||||
Tx = TxEnv,
|
||||
HaltReason = HaltReason,
|
||||
Error = EVMError<DB::Error>,
|
||||
Spec = SpecId,
|
||||
BlockEnv = BlockEnv,
|
||||
>,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
type Transaction = TransactionSigned;
|
||||
type Receipt = Receipt;
|
||||
type Evm = EthEvm<DB, I, P>;
|
||||
type Result = EthTxResult<HaltReason, alloy_consensus::TxType>;
|
||||
|
||||
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
|
||||
// Swap the EVM's block_env and executor ctx to the first segment's
|
||||
// values so that the initial EIP-2935/EIP-4788 system calls use the
|
||||
// correct block number and parent hash. Without this, the outer big
|
||||
// block header's block_number (which is synthetic) would be used,
|
||||
// writing to wrong EIP-2935 slots and corrupting state.
|
||||
if let Some(seg0) = self.plan.as_ref().map(|p| &p.segments[0]) {
|
||||
let block_env = seg0.evm_env.block_env.clone();
|
||||
let block_number = block_env.number.saturating_to::<u64>();
|
||||
let mut cfg_env = seg0.evm_env.cfg_env.clone();
|
||||
cfg_env.disable_base_fee = true;
|
||||
let seg0_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: seg0.ctx.parent_hash,
|
||||
parent_beacon_block_root: seg0.ctx.parent_beacon_block_root,
|
||||
ommers: seg0.ctx.ommers,
|
||||
withdrawals: seg0.ctx.withdrawals.clone(),
|
||||
extra_data: seg0.ctx.extra_data.clone(),
|
||||
tx_count_hint: seg0.ctx.tx_count_hint,
|
||||
slot_number: seg0.ctx.slot_number,
|
||||
};
|
||||
|
||||
let inner = self.inner_mut();
|
||||
let evm_ctx = inner.evm.ctx_mut();
|
||||
evm_ctx.block = block_env;
|
||||
evm_ctx.cfg = cfg_env;
|
||||
inner.ctx = seg0_ctx;
|
||||
|
||||
self.reseed_block_hashes_for(block_number);
|
||||
}
|
||||
|
||||
self.inner_mut().apply_pre_execution_changes()
|
||||
}
|
||||
|
||||
fn execute_transaction_without_commit(
|
||||
&mut self,
|
||||
tx: impl ExecutableTx<Self>,
|
||||
) -> Result<Self::Result, BlockExecutionError> {
|
||||
self.maybe_apply_boundary()?;
|
||||
self.inner_mut().execute_transaction_without_commit(tx)
|
||||
}
|
||||
|
||||
fn commit_transaction(
|
||||
&mut self,
|
||||
output: Self::Result,
|
||||
) -> Result<GasOutput, BlockExecutionError> {
|
||||
let gas_used = self.inner_mut().commit_transaction(output)?;
|
||||
|
||||
// Fix up cumulative_gas_used on the just-committed receipt so that
|
||||
// the receipt root task (which reads receipts incrementally) sees
|
||||
// globally-correct values across all segments.
|
||||
let offset = self.gas_used_offset;
|
||||
if offset > 0 &&
|
||||
let Some(receipt) = self.inner_mut().receipts.last_mut()
|
||||
{
|
||||
receipt.cumulative_gas_used += offset;
|
||||
}
|
||||
|
||||
if let Some(plan) = &mut self.plan {
|
||||
plan.tx_counter += 1;
|
||||
}
|
||||
Ok(gas_used)
|
||||
}
|
||||
|
||||
fn finish(
|
||||
mut self,
|
||||
) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
|
||||
// Swap the inner executor's ctx to the last segment's ctx so that
|
||||
// EthBlockExecutor::finish() applies the correct withdrawal balance
|
||||
// increments and post-execution system calls.
|
||||
if let Some(last_seg) = self.plan.as_ref().map(|p| p.segments.last().unwrap()) {
|
||||
let last_ctx = EthBlockExecutionCtx {
|
||||
parent_hash: last_seg.ctx.parent_hash,
|
||||
parent_beacon_block_root: last_seg.ctx.parent_beacon_block_root,
|
||||
ommers: last_seg.ctx.ommers,
|
||||
withdrawals: last_seg.ctx.withdrawals.clone(),
|
||||
extra_data: last_seg.ctx.extra_data.clone(),
|
||||
tx_count_hint: last_seg.ctx.tx_count_hint,
|
||||
slot_number: last_seg.ctx.slot_number,
|
||||
};
|
||||
self.inner_mut().ctx = last_ctx;
|
||||
}
|
||||
let inner = self.inner.take().expect("inner executor must exist");
|
||||
let (evm, mut result) = inner.finish()?;
|
||||
|
||||
// Receipts already have globally-correct cumulative_gas_used (fixed
|
||||
// up in commit_transaction). Add the offset to the totals so they
|
||||
// reflect gas across all segments.
|
||||
let last_segment_gas = result.gas_used;
|
||||
result.gas_used += self.gas_used_offset;
|
||||
result.blob_gas_used += self.blob_gas_used_offset;
|
||||
|
||||
let last_receipt_cumulative =
|
||||
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
|
||||
debug!(
|
||||
target: "engine::bb::evm",
|
||||
last_segment_gas,
|
||||
gas_used_offset = self.gas_used_offset,
|
||||
total_gas_used = result.gas_used,
|
||||
last_receipt_cumulative,
|
||||
receipt_count = result.receipts.len(),
|
||||
"Finished final segment"
|
||||
);
|
||||
|
||||
// Merge requests accumulated from earlier segment boundaries into
|
||||
// the final result.
|
||||
if !self.accumulated_requests.is_empty() {
|
||||
let mut merged = self.accumulated_requests;
|
||||
merged.extend(result.requests);
|
||||
result.requests = merged;
|
||||
}
|
||||
|
||||
Ok((evm, result))
|
||||
}
|
||||
|
||||
fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
|
||||
if self.plan.is_some() {
|
||||
// Store the real hook in the shared slot and give the inner
|
||||
// executor a lightweight forwarder. This way the hook survives
|
||||
// inner executor finish/reconstruct cycles at segment boundaries.
|
||||
*self.shared_hook.lock().unwrap() = hook;
|
||||
let fwd = self.forwarding_hook();
|
||||
self.inner_mut().set_state_hook(fwd);
|
||||
} else {
|
||||
self.inner_mut().set_state_hook(hook);
|
||||
}
|
||||
}
|
||||
|
||||
fn evm_mut(&mut self) -> &mut Self::Evm {
|
||||
self.inner_mut().evm_mut()
|
||||
}
|
||||
|
||||
fn evm(&self) -> &Self::Evm {
|
||||
self.inner().evm()
|
||||
}
|
||||
|
||||
fn receipts(&self) -> &[Self::Receipt] {
|
||||
self.inner().receipts()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbBlockExecutorFactory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Block executor factory that produces [`BbBlockExecutor`] for
|
||||
/// boundary-aware big-block execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbBlockExecutorFactory<Spec> {
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
spec: Spec,
|
||||
evm_factory: EthEvmFactory,
|
||||
/// Staged plan consumed by the next [`BbBlockExecutor`].
|
||||
pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
|
||||
}
|
||||
|
||||
impl<Spec> BbBlockExecutorFactory<Spec> {
|
||||
pub fn new(
|
||||
receipt_builder: RethReceiptBuilder,
|
||||
spec: Spec,
|
||||
evm_factory: EthEvmFactory,
|
||||
) -> Self {
|
||||
Self { receipt_builder, spec, evm_factory, staged_plan: Arc::new(Mutex::new(None)) }
|
||||
}
|
||||
|
||||
pub const fn evm_factory(&self) -> &EthEvmFactory {
|
||||
&self.evm_factory
|
||||
}
|
||||
|
||||
pub const fn spec(&self) -> &Spec {
|
||||
&self.spec
|
||||
}
|
||||
|
||||
pub const fn receipt_builder(&self) -> &RethReceiptBuilder {
|
||||
&self.receipt_builder
|
||||
}
|
||||
|
||||
pub(crate) fn stage_plan(&self, plan: BbEvmPlan) {
|
||||
*self.staged_plan.lock().unwrap() = Some(plan);
|
||||
}
|
||||
|
||||
fn take_plan(&self) -> Option<BbEvmPlan> {
|
||||
self.staged_plan.lock().unwrap().take()
|
||||
}
|
||||
|
||||
pub(crate) fn create_executor_with_seeder<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: EthEvm<DB, I, PrecompilesMap>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
block_hash_seeder: Option<BlockHashSeeder<DB>>,
|
||||
) -> BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>
|
||||
where
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec,
|
||||
DB: StateDB + 'a,
|
||||
I: Inspector<EthEvmContext<DB>> + 'a,
|
||||
{
|
||||
let plan = self.take_plan();
|
||||
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, block_hash_seeder)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Spec> BlockExecutorFactory for BbBlockExecutorFactory<Spec>
|
||||
where
|
||||
Spec: alloy_evm::eth::spec::EthExecutorSpec + 'static,
|
||||
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
|
||||
{
|
||||
type EvmFactory = EthEvmFactory;
|
||||
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
|
||||
type Transaction = TransactionSigned;
|
||||
type Receipt = Receipt;
|
||||
|
||||
fn evm_factory(&self) -> &Self::EvmFactory {
|
||||
&self.evm_factory
|
||||
}
|
||||
|
||||
fn create_executor<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: EthEvm<DB, I, PrecompilesMap>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
) -> impl BlockExecutorFor<'a, Self, DB, I>
|
||||
where
|
||||
DB: StateDB + 'a,
|
||||
I: Inspector<EthEvmContext<DB>> + 'a,
|
||||
{
|
||||
let plan = self.take_plan();
|
||||
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
//! Big-block EVM configuration.
|
||||
//!
|
||||
//! Wraps [`EthEvmConfig`] to create executors that handle multi-segment
|
||||
//! big-block execution internally. At transaction boundaries defined by
|
||||
//! [`BigBlockData`], the executor swaps the EVM environment (block env,
|
||||
//! cfg env) and applies pre/post execution changes for each segment.
|
||||
|
||||
pub(crate) use reth_engine_primitives::BigBlockData;
|
||||
|
||||
use crate::{
|
||||
evm::{BbBlockExecutorFactory, BbEvmPlan},
|
||||
BigBlockMap,
|
||||
};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_evm::eth::EthBlockExecutionCtx;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rpc_types::engine::ExecutionData;
|
||||
use core::convert::Infallible;
|
||||
use reth_chainspec::{ChainSpec, EthChainSpec};
|
||||
use reth_ethereum_forks::Hardforks;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_evm::{
|
||||
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
|
||||
NextBlockEnvAttributes,
|
||||
};
|
||||
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
|
||||
use reth_primitives_traits::{SealedBlock, SealedHeader};
|
||||
use revm::primitives::hardfork::SpecId;
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
use alloy_evm::{eth::spec::EthExecutorSpec, EthEvmFactory};
|
||||
use reth_evm::{EvmEnvFor, ExecutionCtxFor};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Execution plan types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A single execution segment within a big block.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BigBlockSegment {
|
||||
/// Transaction index at which this segment starts.
|
||||
pub start_tx: usize,
|
||||
/// The EVM environment for this segment.
|
||||
pub evm_env: EvmEnv,
|
||||
/// The execution context for this segment.
|
||||
pub ctx: EthBlockExecutionCtx<'static>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BbEvmConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// EVM configuration for big-block execution.
|
||||
///
|
||||
/// Wraps [`EthEvmConfig`] and a shared [`BigBlockMap`]. When a big-block
|
||||
/// payload is received, the plan is staged on the [`BbBlockExecutorFactory`]
|
||||
/// and consumed when the executor is created. Block hashes for inter-segment
|
||||
/// BLOCKHASH resolution are reseeded into `State::block_hashes` at each
|
||||
/// segment boundary via a [`BlockHashSeeder`](crate::evm::BlockHashSeeder)
|
||||
/// callback injected in [`ConfigureEvm::create_executor`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbEvmConfig<C = ChainSpec> {
|
||||
/// The inner Ethereum EVM configuration (used for env computation).
|
||||
pub inner: EthEvmConfig<C>,
|
||||
/// Shared map of pending big-block metadata.
|
||||
pub pending: BigBlockMap,
|
||||
/// Block executor factory for big-block execution.
|
||||
executor_factory: BbBlockExecutorFactory<Arc<C>>,
|
||||
/// Block assembler.
|
||||
block_assembler: EthBlockAssembler<C>,
|
||||
}
|
||||
|
||||
impl<C> BbEvmConfig<C> {
|
||||
/// Creates a new big-block EVM configuration.
|
||||
pub fn new(inner: EthEvmConfig<C>, pending: BigBlockMap) -> Self
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
let chain_spec = inner.chain_spec().clone();
|
||||
let executor_factory = BbBlockExecutorFactory::new(
|
||||
RethReceiptBuilder::default(),
|
||||
chain_spec,
|
||||
EthEvmFactory::default(),
|
||||
);
|
||||
let block_assembler = inner.block_assembler.clone();
|
||||
|
||||
Self { inner, pending, executor_factory, block_assembler }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block hash seeder for State<DB>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Reseeds `State::block_hashes` with the given hashes.
|
||||
///
|
||||
/// This is used as a [`BlockHashSeeder`](crate::evm::BlockHashSeeder) callback,
|
||||
/// injected into [`BbBlockExecutor`](crate::evm::BbBlockExecutor) from
|
||||
/// `ConfigureEvm::create_executor` where the concrete `State<DB>` type is known.
|
||||
/// At each segment boundary the executor calls this to populate the ring buffer
|
||||
/// with the 256 block hashes relevant to the new segment's block number window.
|
||||
fn seed_state_block_hashes<DB>(state: &mut &mut revm::database::State<DB>, hashes: &[(u64, B256)]) {
|
||||
for &(number, hash) in hashes {
|
||||
state.block_hashes.insert(number, hash);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConfigureEvm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> ConfigureEvm for BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
type Primitives = EthPrimitives;
|
||||
type Error = Infallible;
|
||||
type NextBlockEnvCtx = NextBlockEnvAttributes;
|
||||
type BlockExecutorFactory = BbBlockExecutorFactory<Arc<C>>;
|
||||
type BlockAssembler = EthBlockAssembler<C>;
|
||||
|
||||
fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
|
||||
&self.executor_factory
|
||||
}
|
||||
|
||||
fn block_assembler(&self) -> &Self::BlockAssembler {
|
||||
&self.block_assembler
|
||||
}
|
||||
|
||||
fn evm_env(&self, header: &Header) -> Result<EvmEnv<SpecId>, Self::Error> {
|
||||
self.inner.evm_env(header)
|
||||
}
|
||||
|
||||
fn next_evm_env(
|
||||
&self,
|
||||
parent: &Header,
|
||||
attributes: &NextBlockEnvAttributes,
|
||||
) -> Result<EvmEnv, Self::Error> {
|
||||
self.inner.next_evm_env(parent, attributes)
|
||||
}
|
||||
|
||||
fn context_for_block<'a>(
|
||||
&self,
|
||||
block: &'a SealedBlock<reth_ethereum_primitives::Block>,
|
||||
) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
|
||||
self.inner.context_for_block(block)
|
||||
}
|
||||
|
||||
fn context_for_next_block(
|
||||
&self,
|
||||
parent: &SealedHeader,
|
||||
attributes: Self::NextBlockEnvCtx,
|
||||
) -> Result<EthBlockExecutionCtx<'_>, Self::Error> {
|
||||
self.inner.context_for_next_block(parent, attributes)
|
||||
}
|
||||
|
||||
fn create_executor<'a, DB, I>(
|
||||
&'a self,
|
||||
evm: reth_evm::EvmFor<Self, &'a mut revm::database::State<DB>, I>,
|
||||
ctx: EthBlockExecutionCtx<'a>,
|
||||
) -> impl alloy_evm::block::BlockExecutorFor<
|
||||
'a,
|
||||
Self::BlockExecutorFactory,
|
||||
&'a mut revm::database::State<DB>,
|
||||
I,
|
||||
>
|
||||
where
|
||||
DB: Database,
|
||||
I: reth_evm::InspectorFor<Self, &'a mut revm::database::State<DB>> + 'a,
|
||||
{
|
||||
// Use create_executor_with_seeder to inject a concrete seeder that
|
||||
// can reseed State::block_hashes at segment boundaries. The seeder
|
||||
// is a function pointer that knows the concrete State<DB> type,
|
||||
// allowing the generic BbBlockExecutor to reseed without additional
|
||||
// trait bounds on DB.
|
||||
self.executor_factory.create_executor_with_seeder(
|
||||
evm,
|
||||
ctx,
|
||||
Some(seed_state_block_hashes::<DB>),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConfigureEngineEvm — intercepts payload methods for big blocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> ConfigureEngineEvm<ExecutionData> for BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result<EvmEnvFor<Self>, Self::Error> {
|
||||
let payload_hash = payload.block_hash();
|
||||
let has_plan = self.pending.lock().unwrap().contains_key(&payload_hash);
|
||||
|
||||
if has_plan {
|
||||
// Compute the env from the first segment BEFORE removing the
|
||||
// entry (stage_plan_for_payload removes it).
|
||||
let first_exec_data = {
|
||||
let pending = self.pending.lock().unwrap();
|
||||
let bb_data = pending.get(&payload_hash).unwrap();
|
||||
bb_data.env_switches[0].1.clone()
|
||||
};
|
||||
let mut env = self.inner.evm_env_for_payload(&first_exec_data)?;
|
||||
|
||||
// Disable basefee validation: transactions from different
|
||||
// original blocks may have gas prices below the big block's
|
||||
// effective basefee.
|
||||
env.cfg_env.disable_base_fee = true;
|
||||
|
||||
// Now stage the plan on the factory (removes the entry).
|
||||
self.stage_plan_for_payload(&payload_hash);
|
||||
|
||||
Ok(env)
|
||||
} else {
|
||||
self.inner.evm_env_for_payload(payload)
|
||||
}
|
||||
}
|
||||
|
||||
fn context_for_payload<'a>(
|
||||
&self,
|
||||
payload: &'a ExecutionData,
|
||||
) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
|
||||
self.inner.context_for_payload(payload)
|
||||
}
|
||||
|
||||
fn tx_iterator_for_payload(
|
||||
&self,
|
||||
payload: &ExecutionData,
|
||||
) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
|
||||
self.inner.tx_iterator_for_payload(payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plan construction and staging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl<C> BbEvmConfig<C>
|
||||
where
|
||||
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
|
||||
{
|
||||
/// Takes the big-block plan for a payload hash, builds a [`BbEvmPlan`],
|
||||
/// and stages it on the factory.
|
||||
///
|
||||
/// Must be called before `evm_with_env` is invoked for this payload.
|
||||
/// In practice, this is called from `evm_env_for_payload` in the
|
||||
/// engine pipeline.
|
||||
pub fn stage_plan_for_payload(&self, payload_hash: &B256) {
|
||||
let bb = match self.pending.lock().unwrap().remove(payload_hash) {
|
||||
Some(bb) => bb,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let segments: Vec<_> = bb
|
||||
.env_switches
|
||||
.into_iter()
|
||||
.map(|(start_tx, exec_data)| {
|
||||
let evm_env = self.inner.evm_env_for_payload(&exec_data).unwrap();
|
||||
let ctx = self.inner.context_for_payload(&exec_data).unwrap();
|
||||
let ctx = EthBlockExecutionCtx {
|
||||
tx_count_hint: ctx.tx_count_hint,
|
||||
parent_hash: ctx.parent_hash,
|
||||
parent_beacon_block_root: ctx.parent_beacon_block_root,
|
||||
ommers: &[],
|
||||
withdrawals: ctx.withdrawals.map(|w| std::borrow::Cow::Owned(w.into_owned())),
|
||||
extra_data: ctx.extra_data,
|
||||
slot_number: ctx.slot_number,
|
||||
};
|
||||
BigBlockSegment { start_tx, evm_env, ctx }
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
target: "engine::bb",
|
||||
?payload_hash,
|
||||
segments = segments.len(),
|
||||
seed_hashes = bb.prior_block_hashes.len(),
|
||||
"Staging multi-segment plan"
|
||||
);
|
||||
|
||||
let mut plan = BbEvmPlan::new(segments);
|
||||
|
||||
// Add prior block hashes to the seeding list.
|
||||
plan.block_hashes_to_seed.extend(bb.prior_block_hashes);
|
||||
|
||||
plan.block_hashes_to_seed.sort_unstable_by_key(|(n, _)| *n);
|
||||
|
||||
self.executor_factory.stage_plan(plan);
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
//! reth-bb: a modified reth node for benchmarking big block execution.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
mod evm;
|
||||
mod evm_config;
|
||||
|
||||
use alloy_primitives::B256;
|
||||
|
||||
use alloy_rpc_types::engine::{ExecutionData, ForkchoiceState, ForkchoiceUpdated};
|
||||
use async_trait::async_trait;
|
||||
use clap::Parser;
|
||||
use evm_config::{BbEvmConfig, BigBlockData};
|
||||
use jsonrpsee::core::RpcResult;
|
||||
use reth_chainspec::{ChainSpec, EthereumHardforks, Hardforks};
|
||||
use reth_consensus::noop::NoopConsensus;
|
||||
use reth_engine_primitives::ConsensusEngineHandle;
|
||||
use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, interface::Cli};
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_node_api::{AddOnsContext, FullNodeComponents, NodeTypes, PayloadTypes};
|
||||
use reth_node_builder::{
|
||||
components::{
|
||||
BasicPayloadServiceBuilder, ComponentsBuilder, ConsensusBuilder, ExecutorBuilder,
|
||||
},
|
||||
node::FullNodeTypes,
|
||||
rpc::{
|
||||
BasicEngineApiBuilder, BasicEngineValidatorBuilder, EngineApiBuilder, EngineValidatorAddOn,
|
||||
EngineValidatorBuilder, PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, RpcHandle,
|
||||
RpcHooks,
|
||||
},
|
||||
BuilderContext, Node,
|
||||
};
|
||||
use reth_node_ethereum::{
|
||||
EthEngineTypes, EthereumEngineValidatorBuilder, EthereumEthApiBuilder, EthereumNetworkBuilder,
|
||||
EthereumNode, EthereumPayloadBuilder, EthereumPoolBuilder,
|
||||
};
|
||||
use reth_payload_primitives::ExecutionPayload;
|
||||
use reth_primitives_traits::SealedBlock;
|
||||
use reth_provider::EthStorage;
|
||||
use reth_rpc_api::{RethNewPayloadInput, RethPayloadStatus};
|
||||
use reth_rpc_engine_api::EngineApiError;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::{info, trace};
|
||||
|
||||
/// Shared map for big block data, keyed by payload hash.
|
||||
pub type BigBlockMap = Arc<Mutex<HashMap<B256, BigBlockData<ExecutionData>>>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom RPC trait for big-block payloads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Big-block extension of the `reth_` engine API.
|
||||
#[jsonrpsee::proc_macros::rpc(server, namespace = "reth")]
|
||||
pub trait BbRethEngineApi {
|
||||
/// `reth_newPayload` with optional big-block data.
|
||||
#[method(name = "newPayload")]
|
||||
async fn reth_new_payload(
|
||||
&self,
|
||||
payload: RethNewPayloadInput<ExecutionData>,
|
||||
wait_for_persistence: Option<bool>,
|
||||
wait_for_caches: Option<bool>,
|
||||
big_block_data: Option<BigBlockData<ExecutionData>>,
|
||||
) -> RpcResult<RethPayloadStatus>;
|
||||
|
||||
/// `reth_forkchoiceUpdated` – pass-through.
|
||||
#[method(name = "forkchoiceUpdated")]
|
||||
async fn reth_forkchoice_updated(
|
||||
&self,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
) -> RpcResult<ForkchoiceUpdated>;
|
||||
}
|
||||
|
||||
/// Server-side implementation of `BbRethEngineApi`.
|
||||
#[derive(Debug)]
|
||||
struct BbRethEngineApiHandler {
|
||||
pending: BigBlockMap,
|
||||
engine: ConsensusEngineHandle<EthEngineTypes>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BbRethEngineApiServer for BbRethEngineApiHandler {
|
||||
async fn reth_new_payload(
|
||||
&self,
|
||||
input: RethNewPayloadInput<ExecutionData>,
|
||||
wait_for_persistence: Option<bool>,
|
||||
wait_for_caches: Option<bool>,
|
||||
big_block_data: Option<BigBlockData<ExecutionData>>,
|
||||
) -> RpcResult<RethPayloadStatus> {
|
||||
let wait_for_persistence = wait_for_persistence.unwrap_or(true);
|
||||
let wait_for_caches = wait_for_caches.unwrap_or(true);
|
||||
trace!(
|
||||
target: "rpc::engine",
|
||||
wait_for_persistence,
|
||||
wait_for_caches,
|
||||
has_big_block_data = big_block_data.is_some(),
|
||||
"Serving bb reth_newPayload"
|
||||
);
|
||||
|
||||
let payload = match input {
|
||||
RethNewPayloadInput::ExecutionData(data) => data,
|
||||
RethNewPayloadInput::BlockRlp(rlp) => {
|
||||
let block = alloy_rlp::Decodable::decode(&mut rlp.as_ref())
|
||||
.map_err(|err| EngineApiError::Internal(Box::new(err)))?;
|
||||
<EthEngineTypes as PayloadTypes>::block_to_payload(SealedBlock::new_unhashed(block))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(data) = big_block_data {
|
||||
let hash = ExecutionPayload::block_hash(&payload);
|
||||
self.pending.lock().unwrap().insert(hash, data);
|
||||
}
|
||||
|
||||
let (status, timings) = self
|
||||
.engine
|
||||
.reth_new_payload(payload, wait_for_persistence, wait_for_caches)
|
||||
.await
|
||||
.map_err(EngineApiError::from)?;
|
||||
|
||||
Ok(RethPayloadStatus {
|
||||
status,
|
||||
latency_us: timings.latency.as_micros() as u64,
|
||||
persistence_wait_us: timings.persistence_wait.as_micros() as u64,
|
||||
execution_cache_wait_us: timings.execution_cache_wait.map(|d| d.as_micros() as u64),
|
||||
sparse_trie_wait_us: timings.sparse_trie_wait.map(|d| d.as_micros() as u64),
|
||||
})
|
||||
}
|
||||
|
||||
async fn reth_forkchoice_updated(
|
||||
&self,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
) -> RpcResult<ForkchoiceUpdated> {
|
||||
trace!(target: "rpc::engine", "Serving reth_forkchoiceUpdated");
|
||||
self.engine
|
||||
.fork_choice_updated(forkchoice_state, None)
|
||||
.await
|
||||
.map_err(|e| EngineApiError::from(e).into())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node add-ons wrapper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Add-ons for the big-block node.
|
||||
#[derive(Debug)]
|
||||
pub struct BbAddOns {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl BbAddOns {
|
||||
const fn new(pending: BigBlockMap) -> Self {
|
||||
Self { pending }
|
||||
}
|
||||
|
||||
fn make_rpc_add_ons<N: FullNodeComponents>(
|
||||
&self,
|
||||
) -> RpcAddOns<
|
||||
N,
|
||||
EthereumEthApiBuilder,
|
||||
EthereumEngineValidatorBuilder,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>,
|
||||
>
|
||||
where
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
{
|
||||
RpcAddOns::new(
|
||||
EthereumEthApiBuilder::default(),
|
||||
EthereumEngineValidatorBuilder::default(),
|
||||
BasicEngineApiBuilder::default(),
|
||||
BasicEngineValidatorBuilder::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> reth_node_api::NodeAddOns<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
|
||||
Payload = EthEngineTypes,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type Handle =
|
||||
RpcHandle<N, <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi>;
|
||||
|
||||
async fn launch_add_ons(self, ctx: AddOnsContext<'_, N>) -> eyre::Result<Self::Handle> {
|
||||
let engine_handle = ctx.beacon_engine_handle.clone();
|
||||
let pending = self.pending.clone();
|
||||
let rpc_add_ons = self.make_rpc_add_ons::<N>();
|
||||
|
||||
rpc_add_ons
|
||||
.launch_add_ons_with(ctx, move |container| {
|
||||
let handler = BbRethEngineApiHandler { pending, engine: engine_handle };
|
||||
let bb_module = BbRethEngineApiServer::into_rpc(handler);
|
||||
container.auth_module.replace_auth_methods(bb_module.remove_context())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> RethRpcAddOns<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
|
||||
Payload = EthEngineTypes,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
|
||||
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
|
||||
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type EthApi = <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi;
|
||||
|
||||
fn hooks_mut(&mut self) -> &mut RpcHooks<N, Self::EthApi> {
|
||||
unimplemented!("BbAddOns does not support dynamic hook mutation")
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> EngineValidatorAddOn<N> for BbAddOns
|
||||
where
|
||||
N: FullNodeComponents,
|
||||
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
|
||||
{
|
||||
type ValidatorBuilder = BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>;
|
||||
|
||||
fn engine_validator_builder(&self) -> Self::ValidatorBuilder {
|
||||
BasicEngineValidatorBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom executor builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Executor builder that creates a [`BbEvmConfig`].
|
||||
#[derive(Debug)]
|
||||
pub struct BbExecutorBuilder {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl<Node> ExecutorBuilder<Node> for BbExecutorBuilder
|
||||
where
|
||||
Node: FullNodeTypes<
|
||||
Types: NodeTypes<
|
||||
ChainSpec: reth_ethereum_forks::Hardforks
|
||||
+ alloy_evm::eth::spec::EthExecutorSpec
|
||||
+ EthereumHardforks,
|
||||
Primitives = EthPrimitives,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
type EVM = BbEvmConfig<<Node::Types as NodeTypes>::ChainSpec>;
|
||||
|
||||
async fn build_evm(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::EVM> {
|
||||
Ok(BbEvmConfig::new(EthEvmConfig::new(ctx.chain_spec()), self.pending))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Node type for big block execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BbNode {
|
||||
pending: BigBlockMap,
|
||||
}
|
||||
|
||||
impl BbNode {
|
||||
const fn new(pending: BigBlockMap) -> Self {
|
||||
Self { pending }
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTypes for BbNode {
|
||||
type Primitives = EthPrimitives;
|
||||
type ChainSpec = ChainSpec;
|
||||
type Storage = EthStorage;
|
||||
type Payload = EthEngineTypes;
|
||||
}
|
||||
|
||||
impl<N> Node<N> for BbNode
|
||||
where
|
||||
N: FullNodeTypes<Types = Self>,
|
||||
{
|
||||
type ComponentsBuilder = ComponentsBuilder<
|
||||
N,
|
||||
EthereumPoolBuilder,
|
||||
BasicPayloadServiceBuilder<EthereumPayloadBuilder>,
|
||||
EthereumNetworkBuilder,
|
||||
BbExecutorBuilder,
|
||||
BbConsensusBuilder,
|
||||
>;
|
||||
|
||||
type AddOns = BbAddOns;
|
||||
|
||||
fn components_builder(&self) -> Self::ComponentsBuilder {
|
||||
EthereumNode::components()
|
||||
.executor(BbExecutorBuilder { pending: self.pending.clone() })
|
||||
.consensus(BbConsensusBuilder)
|
||||
}
|
||||
|
||||
fn add_ons(&self) -> Self::AddOns {
|
||||
BbAddOns::new(self.pending.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Consensus builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Consensus builder for big block execution.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct BbConsensusBuilder;
|
||||
|
||||
impl<Node> ConsensusBuilder<Node> for BbConsensusBuilder
|
||||
where
|
||||
Node: FullNodeTypes<Types: NodeTypes<Primitives = EthPrimitives>>,
|
||||
{
|
||||
type Consensus = NoopConsensus;
|
||||
|
||||
async fn build_consensus(self, _ctx: &BuilderContext<Node>) -> eyre::Result<Self::Consensus> {
|
||||
Ok(NoopConsensus::default())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn main() {
|
||||
reth_cli_util::sigsegv_handler::install();
|
||||
|
||||
if std::env::var_os("RUST_BACKTRACE").is_none() {
|
||||
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
|
||||
}
|
||||
|
||||
let pending: BigBlockMap = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
if let Err(err) = Cli::<EthereumChainSpecParser>::parse().run(async move |builder, _| {
|
||||
info!(target: "reth::cli", "Launching big block node");
|
||||
let handle = builder.launch_node(BbNode::new(pending.clone())).await?;
|
||||
|
||||
handle.wait_for_node_exit().await
|
||||
}) {
|
||||
eprintln!("Error: {err:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
96
bin/reth-bench-compare/Cargo.toml
Normal file
96
bin/reth-bench-compare/Cargo.toml
Normal file
@@ -0,0 +1,96 @@
|
||||
[package]
|
||||
name = "reth-bench-compare"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Automated reth benchmark comparison between git references"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "reth-bench-compare"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-cli-runner.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-provider = { workspace = true, features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-rpc-types-eth.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
|
||||
# CLI and argument parsing
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
eyre.workspace = true
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
# Time handling
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
|
||||
# Path manipulation
|
||||
shellexpand.workspace = true
|
||||
|
||||
# CSV handling
|
||||
csv.workspace = true
|
||||
|
||||
# Process management
|
||||
ctrlc.workspace = true
|
||||
shlex.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.29", features = ["signal", "process"] }
|
||||
|
||||
[features]
|
||||
default = ["jemalloc"]
|
||||
|
||||
asm-keccak = [
|
||||
"reth-node-core/asm-keccak",
|
||||
"alloy-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
jemalloc = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-node-core/jemalloc",
|
||||
]
|
||||
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator"]
|
||||
|
||||
min-error-logs = [
|
||||
"tracing/release_max_level_error",
|
||||
"reth-node-core/min-error-logs",
|
||||
]
|
||||
min-warn-logs = [
|
||||
"tracing/release_max_level_warn",
|
||||
"reth-node-core/min-warn-logs",
|
||||
]
|
||||
min-info-logs = [
|
||||
"tracing/release_max_level_info",
|
||||
"reth-node-core/min-info-logs",
|
||||
]
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-node-core/min-debug-logs",
|
||||
]
|
||||
min-trace-logs = [
|
||||
"tracing/release_max_level_trace",
|
||||
"reth-node-core/min-trace-logs",
|
||||
]
|
||||
|
||||
# no-op feature flag for switching between the `optimism` and default functionality in CI matrices
|
||||
ethereum = []
|
||||
298
bin/reth-bench-compare/src/benchmark.rs
Normal file
298
bin/reth-bench-compare/src/benchmark.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! Benchmark execution using reth-bench.
|
||||
|
||||
use crate::cli::Args;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::{
|
||||
fs::File as AsyncFile,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Manages benchmark execution using reth-bench
|
||||
pub(crate) struct BenchmarkRunner {
|
||||
rpc_url: String,
|
||||
jwt_secret: String,
|
||||
wait_time: Option<String>,
|
||||
warmup_blocks: u64,
|
||||
}
|
||||
|
||||
impl BenchmarkRunner {
|
||||
/// Create a new `BenchmarkRunner` from CLI arguments
|
||||
pub(crate) fn new(args: &Args) -> Self {
|
||||
Self {
|
||||
rpc_url: args.get_rpc_url(),
|
||||
jwt_secret: args.jwt_secret_path().to_string_lossy().to_string(),
|
||||
wait_time: args.wait_time.clone(),
|
||||
warmup_blocks: args.get_warmup_blocks(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear filesystem caches (page cache, dentries, and inodes)
|
||||
pub(crate) async fn clear_fs_caches() -> Result<()> {
|
||||
info!("Clearing filesystem caches...");
|
||||
|
||||
// First sync to ensure all pending writes are flushed
|
||||
let sync_output =
|
||||
Command::new("sync").output().await.wrap_err("Failed to execute sync command")?;
|
||||
|
||||
if !sync_output.status.success() {
|
||||
return Err(eyre!("sync command failed"));
|
||||
}
|
||||
|
||||
// Drop caches - requires sudo/root permissions
|
||||
// 3 = drop pagecache, dentries, and inodes
|
||||
let drop_caches_cmd = Command::new("sudo")
|
||||
.args(["-n", "sh", "-c", "echo 3 > /proc/sys/vm/drop_caches"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match drop_caches_cmd {
|
||||
Ok(output) if output.status.success() => {
|
||||
info!("Successfully cleared filesystem caches");
|
||||
Ok(())
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains("sudo: a password is required") {
|
||||
warn!("Unable to clear filesystem caches: sudo password required");
|
||||
warn!(
|
||||
"For optimal benchmarking, configure passwordless sudo for cache clearing:"
|
||||
);
|
||||
warn!(" echo '$USER ALL=(ALL) NOPASSWD: /bin/sh -c echo\\\\ [0-9]\\\\ \\\\>\\\\ /proc/sys/vm/drop_caches' | sudo tee /etc/sudoers.d/drop_caches");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(eyre!("Failed to clear filesystem caches: {}", stderr))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Unable to clear filesystem caches: {}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a warmup benchmark for cache warming
|
||||
pub(crate) async fn run_warmup(&self, from_block: u64) -> Result<()> {
|
||||
let to_block = from_block + self.warmup_blocks;
|
||||
info!(
|
||||
"Running warmup benchmark from block {} to {} ({} blocks)",
|
||||
from_block, to_block, self.warmup_blocks
|
||||
);
|
||||
|
||||
// Build the reth-bench command for warmup (no output flag)
|
||||
let mut cmd = Command::new("reth-bench");
|
||||
cmd.args([
|
||||
"new-payload-fcu",
|
||||
"--rpc-url",
|
||||
&self.rpc_url,
|
||||
"--jwt-secret",
|
||||
&self.jwt_secret,
|
||||
"--from",
|
||||
&from_block.to_string(),
|
||||
"--to",
|
||||
&to_block.to_string(),
|
||||
]);
|
||||
|
||||
// Add wait-time argument if provided
|
||||
if let Some(ref wait_time) = self.wait_time {
|
||||
cmd.args(["--wait-time", wait_time]);
|
||||
}
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
debug!("Executing warmup reth-bench command: {:?}", cmd);
|
||||
|
||||
// Execute the warmup benchmark
|
||||
let mut child = cmd.spawn().wrap_err("Failed to start warmup reth-bench process")?;
|
||||
|
||||
// Stream output at debug level
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[WARMUP] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[WARMUP] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let status = child.wait().await.wrap_err("Failed to wait for warmup reth-bench")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(eyre!("Warmup reth-bench failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
info!("Warmup completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a benchmark for the specified block range
|
||||
pub(crate) async fn run_benchmark(
|
||||
&self,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
output_dir: &Path,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
"Running benchmark from block {} to {} (output: {:?})",
|
||||
from_block, to_block, output_dir
|
||||
);
|
||||
|
||||
// Ensure output directory exists
|
||||
std::fs::create_dir_all(output_dir)
|
||||
.wrap_err_with(|| format!("Failed to create output directory: {output_dir:?}"))?;
|
||||
|
||||
// Create log file path for reth-bench output
|
||||
let log_file_path = output_dir.join("reth_bench.log");
|
||||
info!("reth-bench logs will be saved to: {:?}", log_file_path);
|
||||
|
||||
// Build the reth-bench command
|
||||
let mut cmd = Command::new("reth-bench");
|
||||
cmd.args([
|
||||
"new-payload-fcu",
|
||||
"--rpc-url",
|
||||
&self.rpc_url,
|
||||
"--jwt-secret",
|
||||
&self.jwt_secret,
|
||||
"--from",
|
||||
&from_block.to_string(),
|
||||
"--to",
|
||||
&to_block.to_string(),
|
||||
"--output",
|
||||
&output_dir.to_string_lossy(),
|
||||
]);
|
||||
|
||||
// Add wait-time argument if provided
|
||||
if let Some(ref wait_time) = self.wait_time {
|
||||
cmd.args(["--wait-time", wait_time]);
|
||||
}
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing reth-bench command: {:?}", cmd);
|
||||
|
||||
// Execute the benchmark
|
||||
let mut child = cmd.spawn().wrap_err("Failed to start reth-bench process")?;
|
||||
|
||||
// Capture stdout and stderr for error reporting
|
||||
let stdout_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Stream stdout with prefix at debug level, capture for error reporting, and write to log
|
||||
// file
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let stdout_lines_clone = stdout_lines.clone();
|
||||
let log_file = AsyncFile::create(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to create log file: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-BENCH] {}", line);
|
||||
if let Ok(mut captured) = stdout_lines_clone.lock() {
|
||||
captured.push(line.clone());
|
||||
}
|
||||
// Write to log file (reth-bench output already has timestamps if needed)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Stream stderr with prefix at debug level, capture for error reporting, and write to log
|
||||
// file
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let stderr_lines_clone = stderr_lines.clone();
|
||||
let log_file = AsyncFile::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file_path)
|
||||
.await
|
||||
.wrap_err(format!("Failed to open log file for stderr: {:?}", log_file_path))?;
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
let mut log_file = log_file;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[RETH-BENCH] {}", line);
|
||||
if let Ok(mut captured) = stderr_lines_clone.lock() {
|
||||
captured.push(line.clone());
|
||||
}
|
||||
// Write to log file (reth-bench output already has timestamps if needed)
|
||||
let log_line = format!("{}\n", line);
|
||||
if let Err(e) = log_file.write_all(log_line.as_bytes()).await {
|
||||
debug!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let status = child.wait().await.wrap_err("Failed to wait for reth-bench")?;
|
||||
|
||||
if !status.success() {
|
||||
// Print all captured output when command fails
|
||||
error!("reth-bench failed with exit code: {:?}", status.code());
|
||||
|
||||
if let Ok(stdout) = stdout_lines.lock() &&
|
||||
!stdout.is_empty()
|
||||
{
|
||||
error!("reth-bench stdout:");
|
||||
for line in stdout.iter() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(stderr) = stderr_lines.lock() &&
|
||||
!stderr.is_empty()
|
||||
{
|
||||
error!("reth-bench stderr:");
|
||||
for line in stderr.iter() {
|
||||
error!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(eyre!("reth-bench failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
info!("Benchmark completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
946
bin/reth-bench-compare/src/cli.rs
Normal file
946
bin/reth-bench-compare/src/cli.rs
Normal file
@@ -0,0 +1,946 @@
|
||||
//! CLI argument parsing and main command orchestration.
|
||||
|
||||
use alloy_provider::{Provider, ProviderBuilder};
|
||||
use clap::Parser;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use reth_chainspec::Chain;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_node_core::args::{DatadirArgs, LogArgs, TraceArgs};
|
||||
use reth_tracing::FileWorkerGuard;
|
||||
use std::{net::TcpListener, path::PathBuf, str::FromStr};
|
||||
use tokio::process::Command;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
benchmark::BenchmarkRunner, comparison::ComparisonGenerator, compilation::CompilationManager,
|
||||
git::GitManager, node::NodeManager,
|
||||
};
|
||||
|
||||
/// Target for disabling the --debug.startup-sync-state-idle flag
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum DisableStartupSyncStateIdle {
|
||||
/// Disable for baseline and warmup runs
|
||||
Baseline,
|
||||
/// Disable for feature runs only
|
||||
Feature,
|
||||
/// Disable for all runs
|
||||
All,
|
||||
}
|
||||
|
||||
impl FromStr for DisableStartupSyncStateIdle {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"baseline" => Ok(Self::Baseline),
|
||||
"feature" => Ok(Self::Feature),
|
||||
"all" => Ok(Self::All),
|
||||
_ => Err(format!("Invalid value '{}'. Expected 'baseline', 'feature', or 'all'", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisableStartupSyncStateIdle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Baseline => write!(f, "baseline"),
|
||||
Self::Feature => write!(f, "feature"),
|
||||
Self::All => write!(f, "all"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Automated reth benchmark comparison between git references
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "reth-bench-compare",
|
||||
about = "Compare reth performance between two git references (branches or tags)",
|
||||
version
|
||||
)]
|
||||
pub(crate) struct Args {
|
||||
/// Git reference (branch or tag) to use as baseline for comparison
|
||||
#[arg(long, value_name = "REF")]
|
||||
pub baseline_ref: String,
|
||||
|
||||
/// Git reference (branch or tag) to compare against the baseline
|
||||
#[arg(long, value_name = "REF")]
|
||||
pub feature_ref: String,
|
||||
|
||||
#[command(flatten)]
|
||||
pub datadir: DatadirArgs,
|
||||
|
||||
/// Number of blocks to benchmark
|
||||
#[arg(long, value_name = "N", default_value = "100")]
|
||||
pub blocks: u64,
|
||||
|
||||
/// RPC endpoint for fetching block data
|
||||
#[arg(long, value_name = "URL")]
|
||||
pub rpc_url: Option<String>,
|
||||
|
||||
/// JWT secret file path
|
||||
///
|
||||
/// If not provided, defaults to `<datadir>/<chain>/jwt.hex`.
|
||||
/// If the file doesn't exist, it will be created automatically.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub jwt_secret: Option<PathBuf>,
|
||||
|
||||
/// Output directory for benchmark results
|
||||
#[arg(long, value_name = "PATH", default_value = "./reth-bench-compare")]
|
||||
pub output_dir: String,
|
||||
|
||||
/// Skip git branch validation (useful for testing)
|
||||
#[arg(long)]
|
||||
pub skip_git_validation: bool,
|
||||
|
||||
/// Port for reth metrics endpoint
|
||||
#[arg(long, value_name = "PORT", default_value = "5005")]
|
||||
pub metrics_port: u16,
|
||||
|
||||
/// The chain this node is running.
|
||||
///
|
||||
/// Possible values are either a built-in chain name or numeric chain ID.
|
||||
#[arg(long, value_name = "CHAIN", default_value = "mainnet", required = false)]
|
||||
pub chain: Chain,
|
||||
|
||||
/// Run reth binary with sudo (for elevated privileges)
|
||||
#[arg(long)]
|
||||
pub sudo: bool,
|
||||
|
||||
/// Generate comparison charts using Python script
|
||||
#[arg(long)]
|
||||
pub draw: bool,
|
||||
|
||||
/// Enable CPU profiling with samply during benchmark runs
|
||||
#[arg(long)]
|
||||
pub profile: bool,
|
||||
|
||||
/// Wait time between engine API calls (passed to reth-bench)
|
||||
#[arg(long, value_name = "DURATION")]
|
||||
pub wait_time: Option<String>,
|
||||
|
||||
/// Number of blocks to run for cache warmup after clearing caches.
|
||||
/// If not specified, defaults to the same as --blocks
|
||||
#[arg(long, value_name = "N")]
|
||||
pub warmup_blocks: Option<u64>,
|
||||
|
||||
/// Disable filesystem cache clearing before warmup phase.
|
||||
/// By default, filesystem caches are cleared before warmup to ensure consistent benchmarks.
|
||||
#[arg(long)]
|
||||
pub no_clear_cache: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub logs: LogArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub traces: TraceArgs,
|
||||
|
||||
/// Maximum queue size for OTLP Batch Span Processor (traces).
|
||||
/// Higher values prevent trace drops when benchmarking many blocks.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "OTLP_BUFFER_SIZE",
|
||||
default_value = "32768",
|
||||
help_heading = "Tracing"
|
||||
)]
|
||||
pub otlp_max_queue_size: usize,
|
||||
|
||||
/// Additional arguments to pass to baseline reth node command
|
||||
///
|
||||
/// Example: `--baseline-args "--debug.tip 0xabc..."`
|
||||
#[arg(long, value_name = "ARGS")]
|
||||
pub baseline_args: Option<String>,
|
||||
|
||||
/// Additional arguments to pass to feature reth node command
|
||||
///
|
||||
/// Example: `--feature-args "--debug.tip 0xdef..."`
|
||||
#[arg(long, value_name = "ARGS")]
|
||||
pub feature_args: Option<String>,
|
||||
|
||||
/// Additional arguments to pass to reth node command (applied to both baseline and feature)
|
||||
///
|
||||
/// All arguments after `--` will be passed directly to the reth node command.
|
||||
/// Example: `reth-bench-compare --baseline-ref main --feature-ref pr/123 -- --debug.tip
|
||||
/// 0xabc...`
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
pub reth_args: Vec<String>,
|
||||
|
||||
/// Comma-separated list of features to enable during reth compilation
|
||||
///
|
||||
/// Example: `jemalloc,asm-keccak`
|
||||
#[arg(long, value_name = "FEATURES", default_value = "jemalloc,asm-keccak")]
|
||||
pub features: String,
|
||||
|
||||
/// Disable automatic --debug.startup-sync-state-idle flag for specific runs.
|
||||
/// Can be "baseline", "feature", or "all".
|
||||
/// By default, the flag is passed to warmup, baseline, and feature runs.
|
||||
/// When "baseline" is specified, the flag is NOT passed to warmup OR baseline.
|
||||
/// When "feature" is specified, the flag is NOT passed to feature.
|
||||
/// When "all" is specified, the flag is NOT passed to any run.
|
||||
#[arg(long, value_name = "TARGET")]
|
||||
pub disable_startup_sync_state_idle: Option<DisableStartupSyncStateIdle>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Initializes tracing with the configured options.
|
||||
pub(crate) fn init_tracing(&self) -> Result<Option<FileWorkerGuard>> {
|
||||
let guard = self.logs.init_tracing()?;
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
/// Build additional arguments for a specific ref type, conditionally including
|
||||
/// --debug.startup-sync-state-idle based on the configuration
|
||||
pub(crate) fn build_additional_args(
|
||||
&self,
|
||||
ref_type: &str,
|
||||
base_args_str: Option<&String>,
|
||||
) -> Vec<String> {
|
||||
// Parse the base arguments string if provided
|
||||
let mut args = base_args_str.map(|s| parse_args_string(s)).unwrap_or_default();
|
||||
|
||||
// Determine if we should add the --debug.startup-sync-state-idle flag
|
||||
let should_add_flag = match self.disable_startup_sync_state_idle {
|
||||
None => true, // By default, add the flag
|
||||
Some(DisableStartupSyncStateIdle::All) => false,
|
||||
Some(DisableStartupSyncStateIdle::Baseline) => {
|
||||
ref_type != "baseline" && ref_type != "warmup"
|
||||
}
|
||||
Some(DisableStartupSyncStateIdle::Feature) => ref_type != "feature",
|
||||
};
|
||||
|
||||
if should_add_flag {
|
||||
args.push("--debug.startup-sync-state-idle".to_string());
|
||||
debug!("Adding --debug.startup-sync-state-idle flag for ref_type: {}", ref_type);
|
||||
} else {
|
||||
debug!("Skipping --debug.startup-sync-state-idle flag for ref_type: {}", ref_type);
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
/// Get the default RPC URL for a given chain
|
||||
const fn get_default_rpc_url(chain: &Chain) -> &'static str {
|
||||
match chain.id() {
|
||||
8453 => "https://base-mainnet.rpc.ithaca.xyz", // base
|
||||
84532 => "https://base-sepolia.rpc.ithaca.xyz", // base-sepolia
|
||||
27082 => "https://rpc.hoodi.ethpandaops.io", // hoodi
|
||||
_ => "https://reth-ethereum.ithaca.xyz/rpc", // mainnet and fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the RPC URL, using chain-specific default if not provided
|
||||
pub(crate) fn get_rpc_url(&self) -> String {
|
||||
self.rpc_url.clone().unwrap_or_else(|| Self::get_default_rpc_url(&self.chain).to_string())
|
||||
}
|
||||
|
||||
/// Get the JWT secret path - either provided or derived from datadir
|
||||
pub(crate) fn jwt_secret_path(&self) -> PathBuf {
|
||||
match &self.jwt_secret {
|
||||
Some(path) => {
|
||||
let jwt_secret_str = path.to_string_lossy();
|
||||
let expanded = shellexpand::tilde(&jwt_secret_str);
|
||||
PathBuf::from(expanded.as_ref())
|
||||
}
|
||||
None => {
|
||||
// Use the same logic as reth: <datadir>/<chain>/jwt.hex
|
||||
let chain_path = self.datadir.clone().resolve_datadir(self.chain);
|
||||
chain_path.jwt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resolved datadir path using the chain
|
||||
pub(crate) fn datadir_path(&self) -> PathBuf {
|
||||
let chain_path = self.datadir.clone().resolve_datadir(self.chain);
|
||||
chain_path.data_dir().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the expanded output directory path
|
||||
pub(crate) fn output_dir_path(&self) -> PathBuf {
|
||||
let expanded = shellexpand::tilde(&self.output_dir);
|
||||
PathBuf::from(expanded.as_ref())
|
||||
}
|
||||
|
||||
/// Get the effective warmup blocks value - either specified or defaults to blocks
|
||||
pub(crate) fn get_warmup_blocks(&self) -> u64 {
|
||||
self.warmup_blocks.unwrap_or(self.blocks)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the RPC endpoint chain ID matches the specified chain
|
||||
async fn validate_rpc_chain_id(rpc_url: &str, expected_chain: &Chain) -> Result<()> {
|
||||
// Create Alloy provider
|
||||
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
|
||||
let provider = ProviderBuilder::new().connect_http(url);
|
||||
|
||||
// Query chain ID using Alloy
|
||||
let rpc_chain_id = provider
|
||||
.get_chain_id()
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to get chain ID from RPC endpoint {}: {:?}", rpc_url, e))?;
|
||||
|
||||
let expected_chain_id = expected_chain.id();
|
||||
|
||||
if rpc_chain_id != expected_chain_id {
|
||||
return Err(eyre!(
|
||||
"RPC endpoint chain ID mismatch!\n\
|
||||
Expected: {} (chain: {})\n\
|
||||
Found: {} at RPC endpoint: {}\n\n\
|
||||
Please use an RPC endpoint for the correct network or change the --chain argument.",
|
||||
expected_chain_id,
|
||||
expected_chain,
|
||||
rpc_chain_id,
|
||||
rpc_url
|
||||
));
|
||||
}
|
||||
|
||||
info!("Validated RPC endpoint chain ID");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main comparison workflow execution
|
||||
pub(crate) async fn run_comparison(args: Args, _ctx: CliContext) -> Result<()> {
|
||||
// Create a new process group for this process and all its children
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::unistd::{getpid, setpgid};
|
||||
if let Err(e) = setpgid(getpid(), getpid()) {
|
||||
warn!("Failed to create process group: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Starting benchmark comparison between '{}' and '{}'",
|
||||
args.baseline_ref, args.feature_ref
|
||||
);
|
||||
|
||||
if args.sudo {
|
||||
info!("Running in sudo mode - reth commands will use elevated privileges");
|
||||
}
|
||||
|
||||
// Initialize Git manager
|
||||
let git_manager = GitManager::new()?;
|
||||
// Fetch all branches, tags, and commits
|
||||
git_manager.fetch_all()?;
|
||||
|
||||
// Initialize compilation manager
|
||||
let output_dir = args.output_dir_path();
|
||||
let compilation_manager = CompilationManager::new(
|
||||
git_manager.repo_root().to_string(),
|
||||
output_dir.clone(),
|
||||
git_manager.clone(),
|
||||
args.features.clone(),
|
||||
args.profile,
|
||||
)?;
|
||||
// Initialize node manager
|
||||
let mut node_manager = NodeManager::new(&args);
|
||||
|
||||
let benchmark_runner = BenchmarkRunner::new(&args);
|
||||
let mut comparison_generator = ComparisonGenerator::new(&args);
|
||||
|
||||
// Set the comparison directory in node manager to align with results directory
|
||||
node_manager.set_comparison_dir(comparison_generator.get_output_dir());
|
||||
|
||||
// Store original git state for restoration
|
||||
let original_ref = git_manager.get_current_ref()?;
|
||||
info!("Current git reference: {}", original_ref);
|
||||
|
||||
// Validate git state
|
||||
if !args.skip_git_validation {
|
||||
git_manager.validate_clean_state()?;
|
||||
git_manager.validate_refs(&[&args.baseline_ref, &args.feature_ref])?;
|
||||
}
|
||||
|
||||
// Validate RPC endpoint chain ID matches the specified chain
|
||||
let rpc_url = args.get_rpc_url();
|
||||
validate_rpc_chain_id(&rpc_url, &args.chain).await?;
|
||||
|
||||
// Setup signal handling for cleanup
|
||||
let git_manager_cleanup = git_manager.clone();
|
||||
let original_ref_cleanup = original_ref.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
eprintln!("Received interrupt signal, cleaning up...");
|
||||
|
||||
// Send SIGTERM to entire process group to ensure all children exit
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::{
|
||||
sys::signal::{kill, Signal},
|
||||
unistd::Pid,
|
||||
};
|
||||
|
||||
// Send SIGTERM to our process group (negative PID = process group)
|
||||
let current_pid = std::process::id() as i32;
|
||||
let pgid = Pid::from_raw(-current_pid);
|
||||
if let Err(e) = kill(pgid, Signal::SIGTERM) {
|
||||
eprintln!("Failed to send SIGTERM to process group: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Give a moment for any ongoing git operations to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
|
||||
if let Err(e) = git_manager_cleanup.switch_ref(&original_ref_cleanup) {
|
||||
eprintln!("Failed to restore original git reference: {e}");
|
||||
eprintln!("You may need to manually run: git checkout {original_ref_cleanup}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
})?;
|
||||
|
||||
let result = run_benchmark_workflow(
|
||||
&git_manager,
|
||||
&compilation_manager,
|
||||
&mut node_manager,
|
||||
&benchmark_runner,
|
||||
&mut comparison_generator,
|
||||
&args,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Always restore original git reference
|
||||
info!("Restoring original git reference: {}", original_ref);
|
||||
git_manager.switch_ref(&original_ref)?;
|
||||
|
||||
// Handle any errors from the workflow
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a string of arguments into a vector of strings
|
||||
fn parse_args_string(args_str: &str) -> Vec<String> {
|
||||
shlex::split(args_str).unwrap_or_else(|| {
|
||||
// Fallback to simple whitespace splitting if shlex fails
|
||||
args_str.split_whitespace().map(|s| s.to_string()).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Run compilation phase for both baseline and feature binaries
|
||||
async fn run_compilation_phase(
|
||||
git_manager: &GitManager,
|
||||
compilation_manager: &CompilationManager,
|
||||
args: &Args,
|
||||
is_optimism: bool,
|
||||
) -> Result<(String, String)> {
|
||||
info!("=== Running compilation phase ===");
|
||||
|
||||
// Ensure required tools are available (only need to check once)
|
||||
compilation_manager.ensure_reth_bench_available()?;
|
||||
if args.profile {
|
||||
compilation_manager.ensure_samply_available()?;
|
||||
}
|
||||
|
||||
let refs = [&args.baseline_ref, &args.feature_ref];
|
||||
let ref_types = ["baseline", "feature"];
|
||||
|
||||
// First, resolve all refs to commits using a HashMap to avoid race conditions where a ref is
|
||||
// pushed to mid-run.
|
||||
let mut ref_commits = std::collections::HashMap::new();
|
||||
for &git_ref in &refs {
|
||||
if !ref_commits.contains_key(git_ref) {
|
||||
git_manager.switch_ref(git_ref)?;
|
||||
let commit = git_manager.get_current_commit()?;
|
||||
ref_commits.insert(git_ref.clone(), commit);
|
||||
info!("Reference {} resolves to commit: {}", git_ref, &ref_commits[git_ref][..8]);
|
||||
}
|
||||
}
|
||||
|
||||
// Now compile each ref using the resolved commits
|
||||
for (i, &git_ref) in refs.iter().enumerate() {
|
||||
let ref_type = ref_types[i];
|
||||
let commit = &ref_commits[git_ref];
|
||||
|
||||
info!(
|
||||
"Compiling {} binary for reference: {} (commit: {})",
|
||||
ref_type,
|
||||
git_ref,
|
||||
&commit[..8]
|
||||
);
|
||||
|
||||
// Switch to target reference
|
||||
git_manager.switch_ref(git_ref)?;
|
||||
|
||||
// Compile reth (with caching)
|
||||
compilation_manager.compile_reth(commit, is_optimism)?;
|
||||
|
||||
info!("Completed compilation for {} reference", ref_type);
|
||||
}
|
||||
|
||||
let baseline_commit = ref_commits[&args.baseline_ref].clone();
|
||||
let feature_commit = ref_commits[&args.feature_ref].clone();
|
||||
|
||||
info!("Compilation phase completed");
|
||||
Ok((baseline_commit, feature_commit))
|
||||
}
|
||||
|
||||
/// Run warmup phase to warm up caches before benchmarking
|
||||
async fn run_warmup_phase(
|
||||
git_manager: &GitManager,
|
||||
compilation_manager: &CompilationManager,
|
||||
node_manager: &mut NodeManager,
|
||||
benchmark_runner: &BenchmarkRunner,
|
||||
args: &Args,
|
||||
is_optimism: bool,
|
||||
baseline_commit: &str,
|
||||
) -> Result<()> {
|
||||
info!("=== Running warmup phase ===");
|
||||
|
||||
// Use baseline for warmup
|
||||
let warmup_ref = &args.baseline_ref;
|
||||
|
||||
// Switch to baseline reference
|
||||
git_manager.switch_ref(warmup_ref)?;
|
||||
|
||||
// Get the cached binary path for baseline (should already be compiled)
|
||||
let binary_path =
|
||||
compilation_manager.get_cached_binary_path_for_commit(baseline_commit, is_optimism);
|
||||
|
||||
// Verify the cached binary exists
|
||||
if !binary_path.exists() {
|
||||
return Err(eyre!(
|
||||
"Cached baseline binary not found at {:?}. Compilation phase should have created it.",
|
||||
binary_path
|
||||
));
|
||||
}
|
||||
|
||||
info!("Using cached baseline binary for warmup (commit: {})", &baseline_commit[..8]);
|
||||
|
||||
// Build additional args with conditional --debug.startup-sync-state-idle flag
|
||||
let additional_args = args.build_additional_args("warmup", args.baseline_args.as_ref());
|
||||
|
||||
// Start reth node for warmup (command is not stored for warmup phase)
|
||||
let (mut node_process, _warmup_command) =
|
||||
node_manager.start_node(&binary_path, warmup_ref, "warmup", &additional_args).await?;
|
||||
|
||||
// Wait for node to be ready and get its current tip
|
||||
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
|
||||
info!("Warmup node is ready at tip: {}", current_tip);
|
||||
|
||||
// Store the tip we'll unwind back to
|
||||
let original_tip = current_tip;
|
||||
|
||||
// Clear filesystem caches before warmup run only (unless disabled)
|
||||
if args.no_clear_cache {
|
||||
info!("Skipping filesystem cache clearing (--no-clear-cache flag set)");
|
||||
} else {
|
||||
BenchmarkRunner::clear_fs_caches().await?;
|
||||
}
|
||||
|
||||
// Run warmup to warm up caches
|
||||
benchmark_runner.run_warmup(current_tip).await?;
|
||||
|
||||
// Stop node before unwinding (node must be stopped to release database lock)
|
||||
node_manager.stop_node(&mut node_process).await?;
|
||||
|
||||
// Unwind back to starting block after warmup
|
||||
node_manager.unwind_to_block(original_tip).await?;
|
||||
|
||||
info!("Warmup phase completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the complete benchmark workflow for both branches
|
||||
async fn run_benchmark_workflow(
|
||||
git_manager: &GitManager,
|
||||
compilation_manager: &CompilationManager,
|
||||
node_manager: &mut NodeManager,
|
||||
benchmark_runner: &BenchmarkRunner,
|
||||
comparison_generator: &mut ComparisonGenerator,
|
||||
args: &Args,
|
||||
) -> Result<()> {
|
||||
// Detect if this is an Optimism chain once at the beginning
|
||||
let rpc_url = args.get_rpc_url();
|
||||
let is_optimism = compilation_manager.detect_optimism_chain(&rpc_url).await?;
|
||||
|
||||
// Run compilation phase for both binaries
|
||||
let (baseline_commit, feature_commit) =
|
||||
run_compilation_phase(git_manager, compilation_manager, args, is_optimism).await?;
|
||||
|
||||
// Run warmup phase before benchmarking (skip if warmup_blocks is 0)
|
||||
if args.get_warmup_blocks() > 0 {
|
||||
run_warmup_phase(
|
||||
git_manager,
|
||||
compilation_manager,
|
||||
node_manager,
|
||||
benchmark_runner,
|
||||
args,
|
||||
is_optimism,
|
||||
&baseline_commit,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!("Skipping warmup phase (warmup_blocks is 0)");
|
||||
}
|
||||
|
||||
let refs = [&args.baseline_ref, &args.feature_ref];
|
||||
let ref_types = ["baseline", "feature"];
|
||||
let commits = [&baseline_commit, &feature_commit];
|
||||
|
||||
for (i, &git_ref) in refs.iter().enumerate() {
|
||||
let ref_type = ref_types[i];
|
||||
let commit = commits[i];
|
||||
info!("=== Processing {} reference: {} ===", ref_type, git_ref);
|
||||
|
||||
// Switch to target reference
|
||||
git_manager.switch_ref(git_ref)?;
|
||||
|
||||
// Get the cached binary path for this git reference (should already be compiled)
|
||||
let binary_path =
|
||||
compilation_manager.get_cached_binary_path_for_commit(commit, is_optimism);
|
||||
|
||||
// Verify the cached binary exists
|
||||
if !binary_path.exists() {
|
||||
return Err(eyre!(
|
||||
"Cached {} binary not found at {:?}. Compilation phase should have created it.",
|
||||
ref_type,
|
||||
binary_path
|
||||
));
|
||||
}
|
||||
|
||||
info!("Using cached {} binary (commit: {})", ref_type, &commit[..8]);
|
||||
|
||||
// Get reference-specific base arguments string
|
||||
let base_args_str = match ref_type {
|
||||
"baseline" => args.baseline_args.as_ref(),
|
||||
"feature" => args.feature_args.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Build additional args with conditional --debug.startup-sync-state-idle flag
|
||||
let additional_args = args.build_additional_args(ref_type, base_args_str);
|
||||
|
||||
// Start reth node and capture the command for reporting
|
||||
let (mut node_process, reth_command) =
|
||||
node_manager.start_node(&binary_path, git_ref, ref_type, &additional_args).await?;
|
||||
|
||||
// Wait for node to be ready and get its current tip (wherever it is)
|
||||
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
|
||||
info!("Node is ready at tip: {}", current_tip);
|
||||
|
||||
// Store the tip we'll unwind back to
|
||||
let original_tip = current_tip;
|
||||
|
||||
// Calculate benchmark range
|
||||
// Note: reth-bench has an off-by-one error where it consumes the first block
|
||||
// of the range, so we add 1 to compensate and get exactly args.blocks blocks
|
||||
let from_block = original_tip;
|
||||
let to_block = original_tip + args.blocks;
|
||||
|
||||
// Run benchmark
|
||||
let output_dir = comparison_generator.get_ref_output_dir(ref_type);
|
||||
|
||||
// Capture start timestamp for the benchmark run
|
||||
let benchmark_start = chrono::Utc::now();
|
||||
|
||||
// Run benchmark (comparison logic is handled separately by ComparisonGenerator)
|
||||
benchmark_runner.run_benchmark(from_block, to_block, &output_dir).await?;
|
||||
|
||||
// Capture end timestamp for the benchmark run
|
||||
let benchmark_end = chrono::Utc::now();
|
||||
|
||||
// Stop node
|
||||
node_manager.stop_node(&mut node_process).await?;
|
||||
|
||||
// Unwind back to original tip
|
||||
node_manager.unwind_to_block(original_tip).await?;
|
||||
|
||||
// Store results for comparison
|
||||
comparison_generator.add_ref_results(ref_type, &output_dir)?;
|
||||
|
||||
// Set the benchmark run timestamps and reth command
|
||||
comparison_generator.set_ref_timestamps(ref_type, benchmark_start, benchmark_end)?;
|
||||
comparison_generator.set_ref_command(ref_type, reth_command)?;
|
||||
|
||||
info!("Completed {} reference benchmark", ref_type);
|
||||
}
|
||||
|
||||
// Generate comparison report
|
||||
comparison_generator.generate_comparison_report().await?;
|
||||
|
||||
// Generate charts if requested
|
||||
if args.draw {
|
||||
generate_comparison_charts(comparison_generator).await?;
|
||||
}
|
||||
|
||||
// Start samply servers if profiling was enabled
|
||||
if args.profile {
|
||||
start_samply_servers(args).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate comparison charts using the Python script
|
||||
async fn generate_comparison_charts(comparison_generator: &ComparisonGenerator) -> Result<()> {
|
||||
info!("Generating comparison charts with Python script...");
|
||||
|
||||
let baseline_output_dir = comparison_generator.get_ref_output_dir("baseline");
|
||||
let feature_output_dir = comparison_generator.get_ref_output_dir("feature");
|
||||
|
||||
let baseline_csv = baseline_output_dir.join("combined_latency.csv");
|
||||
let feature_csv = feature_output_dir.join("combined_latency.csv");
|
||||
|
||||
// Check if CSV files exist
|
||||
if !baseline_csv.exists() {
|
||||
return Err(eyre!("Baseline CSV not found: {:?}", baseline_csv));
|
||||
}
|
||||
if !feature_csv.exists() {
|
||||
return Err(eyre!("Feature CSV not found: {:?}", feature_csv));
|
||||
}
|
||||
|
||||
let output_dir = comparison_generator.get_output_dir();
|
||||
let chart_output = output_dir.join("latency_comparison.png");
|
||||
|
||||
let script_path = "bin/reth-bench/scripts/compare_newpayload_latency.py";
|
||||
|
||||
info!("Running Python comparison script with uv...");
|
||||
let mut cmd = Command::new("uv");
|
||||
cmd.args([
|
||||
"run",
|
||||
script_path,
|
||||
&baseline_csv.to_string_lossy(),
|
||||
&feature_csv.to_string_lossy(),
|
||||
"-o",
|
||||
&chart_output.to_string_lossy(),
|
||||
]);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
let output = cmd.output().await.map_err(|e| {
|
||||
eyre!("Failed to execute Python script with uv: {}. Make sure uv is installed.", e)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(eyre!(
|
||||
"Python script failed with exit code {:?}:\nstdout: {}\nstderr: {}",
|
||||
output.status.code(),
|
||||
stdout,
|
||||
stderr
|
||||
));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !stdout.trim().is_empty() {
|
||||
info!("Python script output:\n{}", stdout);
|
||||
}
|
||||
|
||||
info!("Comparison chart generated: {:?}", chart_output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start samply servers for viewing profiles
|
||||
async fn start_samply_servers(args: &Args) -> Result<()> {
|
||||
info!("Starting samply servers for profile viewing...");
|
||||
|
||||
let output_dir = args.output_dir_path();
|
||||
let profiles_dir = output_dir.join("profiles");
|
||||
|
||||
// Build profile paths
|
||||
let baseline_profile = profiles_dir.join("baseline.json.gz");
|
||||
let feature_profile = profiles_dir.join("feature.json.gz");
|
||||
|
||||
// Check if profiles exist
|
||||
if !baseline_profile.exists() {
|
||||
warn!("Baseline profile not found: {:?}", baseline_profile);
|
||||
return Ok(());
|
||||
}
|
||||
if !feature_profile.exists() {
|
||||
warn!("Feature profile not found: {:?}", feature_profile);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Find two consecutive available ports starting from 3000
|
||||
let (baseline_port, feature_port) = find_consecutive_ports(3000)?;
|
||||
info!("Found available ports: {} and {}", baseline_port, feature_port);
|
||||
|
||||
// Get samply path
|
||||
let samply_path = get_samply_path().await?;
|
||||
|
||||
// Start baseline server
|
||||
info!("Starting samply server for baseline '{}' on port {}", args.baseline_ref, baseline_port);
|
||||
let mut baseline_cmd = Command::new(&samply_path);
|
||||
baseline_cmd
|
||||
.args(["load", "--port", &baseline_port.to_string(), &baseline_profile.to_string_lossy()])
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
baseline_cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Conditionally pipe output based on log level
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
baseline_cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
|
||||
} else {
|
||||
baseline_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
|
||||
}
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing samply load command: {:?}", baseline_cmd);
|
||||
|
||||
let mut baseline_child =
|
||||
baseline_cmd.spawn().wrap_err("Failed to start samply server for baseline")?;
|
||||
|
||||
// Stream baseline samply output if debug logging is enabled
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
if let Some(stdout) = baseline_child.stdout.take() {
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[SAMPLY-BASELINE] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = baseline_child.stderr.take() {
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[SAMPLY-BASELINE] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start feature server
|
||||
info!("Starting samply server for feature '{}' on port {}", args.feature_ref, feature_port);
|
||||
let mut feature_cmd = Command::new(&samply_path);
|
||||
feature_cmd
|
||||
.args(["load", "--port", &feature_port.to_string(), &feature_profile.to_string_lossy()])
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Set process group for consistent signal handling
|
||||
#[cfg(unix)]
|
||||
{
|
||||
feature_cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Conditionally pipe output based on log level
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
feature_cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
|
||||
} else {
|
||||
feature_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
|
||||
}
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing samply load command: {:?}", feature_cmd);
|
||||
|
||||
let mut feature_child =
|
||||
feature_cmd.spawn().wrap_err("Failed to start samply server for feature")?;
|
||||
|
||||
// Stream feature samply output if debug logging is enabled
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
if let Some(stdout) = feature_child.stdout.take() {
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[SAMPLY-FEATURE] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = feature_child.stderr.take() {
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
debug!("[SAMPLY-FEATURE] {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Give servers time to start
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Print access information
|
||||
println!("\n=== SAMPLY PROFILE SERVERS STARTED ===");
|
||||
println!("Baseline '{}': http://127.0.0.1:{}", args.baseline_ref, baseline_port);
|
||||
println!("Feature '{}': http://127.0.0.1:{}", args.feature_ref, feature_port);
|
||||
println!("\nOpen the URLs in your browser to view the profiles.");
|
||||
println!("Press Ctrl+C to stop the servers and exit.");
|
||||
println!("=========================================\n");
|
||||
|
||||
// Wait for Ctrl+C or process termination
|
||||
let ctrl_c = tokio::signal::ctrl_c();
|
||||
let baseline_wait = baseline_child.wait();
|
||||
let feature_wait = feature_child.wait();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {
|
||||
info!("Received Ctrl+C, shutting down samply servers...");
|
||||
}
|
||||
result = baseline_wait => {
|
||||
match result {
|
||||
Ok(status) => info!("Baseline samply server exited with status: {}", status),
|
||||
Err(e) => warn!("Baseline samply server error: {}", e),
|
||||
}
|
||||
}
|
||||
result = feature_wait => {
|
||||
match result {
|
||||
Ok(status) => info!("Feature samply server exited with status: {}", status),
|
||||
Err(e) => warn!("Feature samply server error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure both processes are terminated
|
||||
let _ = baseline_child.kill().await;
|
||||
let _ = feature_child.kill().await;
|
||||
|
||||
info!("Samply servers stopped.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find two consecutive available ports starting from the given port
|
||||
fn find_consecutive_ports(start_port: u16) -> Result<(u16, u16)> {
|
||||
for port in start_port..=65533 {
|
||||
// Check if both port and port+1 are available
|
||||
if is_port_available(port) && is_port_available(port + 1) {
|
||||
return Ok((port, port + 1));
|
||||
}
|
||||
}
|
||||
Err(eyre!("Could not find two consecutive available ports starting from {}", start_port))
|
||||
}
|
||||
|
||||
/// Check if a port is available by attempting to bind to it
|
||||
fn is_port_available(port: u16) -> bool {
|
||||
TcpListener::bind(("127.0.0.1", port)).is_ok()
|
||||
}
|
||||
|
||||
/// Get the absolute path to samply using 'which' command
|
||||
async fn get_samply_path() -> Result<String> {
|
||||
let output = Command::new("which")
|
||||
.arg("samply")
|
||||
.output()
|
||||
.await
|
||||
.wrap_err("Failed to execute 'which samply' command")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre!("samply not found in PATH"));
|
||||
}
|
||||
|
||||
let samply_path = String::from_utf8(output.stdout)
|
||||
.wrap_err("samply path is not valid UTF-8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if samply_path.is_empty() {
|
||||
return Err(eyre!("which samply returned empty path"));
|
||||
}
|
||||
|
||||
Ok(samply_path)
|
||||
}
|
||||
710
bin/reth-bench-compare/src/comparison.rs
Normal file
710
bin/reth-bench-compare/src/comparison.rs
Normal file
@@ -0,0 +1,710 @@
|
||||
//! Results comparison and report generation.
|
||||
|
||||
use crate::cli::Args;
|
||||
use chrono::{DateTime, Utc};
|
||||
use csv::Reader;
|
||||
use eyre::{eyre, Result, WrapErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Manages comparison between baseline and feature reference results
|
||||
pub(crate) struct ComparisonGenerator {
|
||||
output_dir: PathBuf,
|
||||
timestamp: String,
|
||||
baseline_ref_name: String,
|
||||
feature_ref_name: String,
|
||||
baseline_results: Option<BenchmarkResults>,
|
||||
feature_results: Option<BenchmarkResults>,
|
||||
baseline_command: Option<String>,
|
||||
feature_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents the results from a single benchmark run
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BenchmarkResults {
|
||||
pub ref_name: String,
|
||||
pub combined_latency_data: Vec<CombinedLatencyRow>,
|
||||
pub summary: BenchmarkSummary,
|
||||
pub start_timestamp: Option<DateTime<Utc>>,
|
||||
pub end_timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Combined latency CSV row structure
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct CombinedLatencyRow {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
pub gas_used: u64,
|
||||
pub new_payload_latency: u128,
|
||||
}
|
||||
|
||||
/// Total gas CSV row structure
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct TotalGasRow {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
pub gas_used: u64,
|
||||
pub time: u128,
|
||||
}
|
||||
|
||||
/// Summary statistics for a benchmark run.
|
||||
///
|
||||
/// Latencies are derived from per-block `engine_newPayload` timings (converted from µs to ms):
|
||||
/// - `mean_new_payload_latency_ms`: arithmetic mean latency across blocks.
|
||||
/// - `median_new_payload_latency_ms`: p50 latency across blocks.
|
||||
/// - `p90_new_payload_latency_ms` / `p99_new_payload_latency_ms`: tail latencies across blocks.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct BenchmarkSummary {
|
||||
pub total_blocks: u64,
|
||||
pub total_gas_used: u64,
|
||||
pub total_duration_ms: u128,
|
||||
pub mean_new_payload_latency_ms: f64,
|
||||
pub median_new_payload_latency_ms: f64,
|
||||
pub p90_new_payload_latency_ms: f64,
|
||||
pub p99_new_payload_latency_ms: f64,
|
||||
pub gas_per_second: f64,
|
||||
pub blocks_per_second: f64,
|
||||
pub min_block_number: u64,
|
||||
pub max_block_number: u64,
|
||||
}
|
||||
|
||||
/// Comparison report between two benchmark runs
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ComparisonReport {
|
||||
pub timestamp: String,
|
||||
pub baseline: RefInfo,
|
||||
pub feature: RefInfo,
|
||||
pub comparison_summary: ComparisonSummary,
|
||||
pub per_block_comparisons: Vec<BlockComparison>,
|
||||
}
|
||||
|
||||
/// Information about a reference in the comparison
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct RefInfo {
|
||||
pub ref_name: String,
|
||||
pub summary: BenchmarkSummary,
|
||||
pub start_timestamp: Option<DateTime<Utc>>,
|
||||
pub end_timestamp: Option<DateTime<Utc>>,
|
||||
pub reth_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Summary of the comparison between references.
|
||||
///
|
||||
/// Percent deltas are `(feature - baseline) / baseline * 100`:
|
||||
/// - `new_payload_latency_p50_change_percent` / p90 / p99: percent changes of the respective
|
||||
/// per-block percentiles.
|
||||
/// - `per_block_latency_change_mean_percent` / `per_block_latency_change_median_percent` are the
|
||||
/// mean and median of per-block percent deltas (feature vs baseline), capturing block-level
|
||||
/// drift.
|
||||
/// - `per_block_latency_change_std_dev_percent`: standard deviation of per-block percent changes,
|
||||
/// measuring consistency of performance changes across blocks.
|
||||
/// - `new_payload_total_latency_change_percent` is the percent change of the total newPayload time
|
||||
/// across the run.
|
||||
///
|
||||
/// Positive means slower/higher; negative means faster/lower.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ComparisonSummary {
|
||||
pub per_block_latency_change_mean_percent: f64,
|
||||
pub per_block_latency_change_median_percent: f64,
|
||||
pub per_block_latency_change_std_dev_percent: f64,
|
||||
pub new_payload_total_latency_change_percent: f64,
|
||||
pub new_payload_latency_p50_change_percent: f64,
|
||||
pub new_payload_latency_p90_change_percent: f64,
|
||||
pub new_payload_latency_p99_change_percent: f64,
|
||||
pub gas_per_second_change_percent: f64,
|
||||
pub blocks_per_second_change_percent: f64,
|
||||
}
|
||||
|
||||
/// Per-block comparison data
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct BlockComparison {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
pub gas_used: u64,
|
||||
pub baseline_new_payload_latency: u128,
|
||||
pub feature_new_payload_latency: u128,
|
||||
pub new_payload_latency_change_percent: f64,
|
||||
}
|
||||
|
||||
impl ComparisonGenerator {
|
||||
/// Create a new comparison generator
|
||||
pub(crate) fn new(args: &Args) -> Self {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||
|
||||
Self {
|
||||
output_dir: args.output_dir_path(),
|
||||
timestamp,
|
||||
baseline_ref_name: args.baseline_ref.clone(),
|
||||
feature_ref_name: args.feature_ref.clone(),
|
||||
baseline_results: None,
|
||||
feature_results: None,
|
||||
baseline_command: None,
|
||||
feature_command: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the output directory for a specific reference
|
||||
pub(crate) fn get_ref_output_dir(&self, ref_type: &str) -> PathBuf {
|
||||
self.output_dir.join("results").join(&self.timestamp).join(ref_type)
|
||||
}
|
||||
|
||||
/// Get the main output directory for this comparison run
|
||||
pub(crate) fn get_output_dir(&self) -> PathBuf {
|
||||
self.output_dir.join("results").join(&self.timestamp)
|
||||
}
|
||||
|
||||
/// Add benchmark results for a reference
|
||||
pub(crate) fn add_ref_results(&mut self, ref_type: &str, output_path: &Path) -> Result<()> {
|
||||
let ref_name = match ref_type {
|
||||
"baseline" => &self.baseline_ref_name,
|
||||
"feature" => &self.feature_ref_name,
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
};
|
||||
|
||||
let results = self.load_benchmark_results(ref_name, output_path)?;
|
||||
|
||||
match ref_type {
|
||||
"baseline" => self.baseline_results = Some(results),
|
||||
"feature" => self.feature_results = Some(results),
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
info!("Loaded benchmark results for {} reference", ref_type);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the benchmark run timestamps for a reference
|
||||
pub(crate) fn set_ref_timestamps(
|
||||
&mut self,
|
||||
ref_type: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
) -> Result<()> {
|
||||
match ref_type {
|
||||
"baseline" => {
|
||||
if let Some(ref mut results) = self.baseline_results {
|
||||
results.start_timestamp = Some(start);
|
||||
results.end_timestamp = Some(end);
|
||||
} else {
|
||||
return Err(eyre!("Baseline results not loaded yet"));
|
||||
}
|
||||
}
|
||||
"feature" => {
|
||||
if let Some(ref mut results) = self.feature_results {
|
||||
results.start_timestamp = Some(start);
|
||||
results.end_timestamp = Some(end);
|
||||
} else {
|
||||
return Err(eyre!("Feature results not loaded yet"));
|
||||
}
|
||||
}
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the reth command for a reference
|
||||
pub(crate) fn set_ref_command(&mut self, ref_type: &str, command: String) -> Result<()> {
|
||||
match ref_type {
|
||||
"baseline" => {
|
||||
self.baseline_command = Some(command);
|
||||
}
|
||||
"feature" => {
|
||||
self.feature_command = Some(command);
|
||||
}
|
||||
_ => return Err(eyre!("Unknown reference type: {}", ref_type)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate the final comparison report
|
||||
pub(crate) async fn generate_comparison_report(&self) -> Result<()> {
|
||||
info!("Generating comparison report...");
|
||||
|
||||
let baseline =
|
||||
self.baseline_results.as_ref().ok_or_else(|| eyre!("Baseline results not loaded"))?;
|
||||
|
||||
let feature =
|
||||
self.feature_results.as_ref().ok_or_else(|| eyre!("Feature results not loaded"))?;
|
||||
|
||||
let per_block_comparisons = self.calculate_per_block_comparisons(baseline, feature)?;
|
||||
let comparison_summary = self.calculate_comparison_summary(
|
||||
&baseline.summary,
|
||||
&feature.summary,
|
||||
&per_block_comparisons,
|
||||
)?;
|
||||
|
||||
let report = ComparisonReport {
|
||||
timestamp: self.timestamp.clone(),
|
||||
baseline: RefInfo {
|
||||
ref_name: baseline.ref_name.clone(),
|
||||
summary: baseline.summary.clone(),
|
||||
start_timestamp: baseline.start_timestamp,
|
||||
end_timestamp: baseline.end_timestamp,
|
||||
reth_command: self.baseline_command.clone(),
|
||||
},
|
||||
feature: RefInfo {
|
||||
ref_name: feature.ref_name.clone(),
|
||||
summary: feature.summary.clone(),
|
||||
start_timestamp: feature.start_timestamp,
|
||||
end_timestamp: feature.end_timestamp,
|
||||
reth_command: self.feature_command.clone(),
|
||||
},
|
||||
comparison_summary,
|
||||
per_block_comparisons,
|
||||
};
|
||||
|
||||
// Write reports
|
||||
self.write_comparison_reports(&report).await?;
|
||||
|
||||
// Print summary to console
|
||||
self.print_comparison_summary(&report);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load benchmark results from CSV files
|
||||
fn load_benchmark_results(
|
||||
&self,
|
||||
ref_name: &str,
|
||||
output_path: &Path,
|
||||
) -> Result<BenchmarkResults> {
|
||||
let combined_latency_path = output_path.join("combined_latency.csv");
|
||||
let total_gas_path = output_path.join("total_gas.csv");
|
||||
|
||||
let combined_latency_data = self.load_combined_latency_csv(&combined_latency_path)?;
|
||||
let total_gas_data = self.load_total_gas_csv(&total_gas_path)?;
|
||||
|
||||
let summary = self.calculate_summary(&combined_latency_data, &total_gas_data)?;
|
||||
|
||||
Ok(BenchmarkResults {
|
||||
ref_name: ref_name.to_string(),
|
||||
combined_latency_data,
|
||||
summary,
|
||||
start_timestamp: None,
|
||||
end_timestamp: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load combined latency CSV data
|
||||
fn load_combined_latency_csv(&self, path: &Path) -> Result<Vec<CombinedLatencyRow>> {
|
||||
let mut reader = Reader::from_path(path)
|
||||
.wrap_err_with(|| format!("Failed to open combined latency CSV: {path:?}"))?;
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
let row: CombinedLatencyRow = result
|
||||
.wrap_err_with(|| format!("Failed to parse combined latency row in {path:?}"))?;
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(eyre!("No data found in combined latency CSV: {:?}", path));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Load total gas CSV data
|
||||
fn load_total_gas_csv(&self, path: &Path) -> Result<Vec<TotalGasRow>> {
|
||||
let mut reader = Reader::from_path(path)
|
||||
.wrap_err_with(|| format!("Failed to open total gas CSV: {path:?}"))?;
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
let row: TotalGasRow =
|
||||
result.wrap_err_with(|| format!("Failed to parse total gas row in {path:?}"))?;
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(eyre!("No data found in total gas CSV: {:?}", path));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Calculate summary statistics for a benchmark run.
|
||||
///
|
||||
/// Computes latency statistics from per-block `new_payload_latency` values in `combined_data`
|
||||
/// (converting from µs to ms), and throughput metrics using the total run duration from
|
||||
/// `total_gas_data`. Percentiles (p50/p90/p99) use linear interpolation on sorted latencies.
|
||||
fn calculate_summary(
|
||||
&self,
|
||||
combined_data: &[CombinedLatencyRow],
|
||||
total_gas_data: &[TotalGasRow],
|
||||
) -> Result<BenchmarkSummary> {
|
||||
if combined_data.is_empty() || total_gas_data.is_empty() {
|
||||
return Err(eyre!("Cannot calculate summary for empty data"));
|
||||
}
|
||||
|
||||
let total_blocks = combined_data.len() as u64;
|
||||
let total_gas_used: u64 = combined_data.iter().map(|r| r.gas_used).sum();
|
||||
|
||||
let total_duration_ms = total_gas_data.last().unwrap().time / 1000; // Convert microseconds to milliseconds
|
||||
|
||||
let latencies_ms: Vec<f64> =
|
||||
combined_data.iter().map(|r| r.new_payload_latency as f64 / 1000.0).collect();
|
||||
let mean_new_payload_latency_ms: f64 =
|
||||
latencies_ms.iter().sum::<f64>() / total_blocks as f64;
|
||||
|
||||
let mut sorted_latencies_ms = latencies_ms;
|
||||
sorted_latencies_ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
let median_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.5);
|
||||
let p90_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.9);
|
||||
let p99_new_payload_latency_ms = percentile(&sorted_latencies_ms, 0.99);
|
||||
|
||||
let total_duration_seconds = total_duration_ms as f64 / 1000.0;
|
||||
let gas_per_second = if total_duration_seconds > f64::EPSILON {
|
||||
total_gas_used as f64 / total_duration_seconds
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let blocks_per_second = if total_duration_seconds > f64::EPSILON {
|
||||
total_blocks as f64 / total_duration_seconds
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let min_block_number = combined_data.first().unwrap().block_number;
|
||||
let max_block_number = combined_data.last().unwrap().block_number;
|
||||
|
||||
Ok(BenchmarkSummary {
|
||||
total_blocks,
|
||||
total_gas_used,
|
||||
total_duration_ms,
|
||||
mean_new_payload_latency_ms,
|
||||
median_new_payload_latency_ms,
|
||||
p90_new_payload_latency_ms,
|
||||
p99_new_payload_latency_ms,
|
||||
gas_per_second,
|
||||
blocks_per_second,
|
||||
min_block_number,
|
||||
max_block_number,
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate comparison summary between baseline and feature
|
||||
fn calculate_comparison_summary(
|
||||
&self,
|
||||
baseline: &BenchmarkSummary,
|
||||
feature: &BenchmarkSummary,
|
||||
per_block_comparisons: &[BlockComparison],
|
||||
) -> Result<ComparisonSummary> {
|
||||
let calc_percent_change = |baseline: f64, feature: f64| -> f64 {
|
||||
if baseline.abs() > f64::EPSILON {
|
||||
((feature - baseline) / baseline) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate per-block statistics. "Per-block" means: for each block, compute the percent
|
||||
// change (feature - baseline) / baseline * 100, then calculate statistics across those
|
||||
// per-block percent changes. This captures how consistently the feature performs relative
|
||||
// to baseline across all blocks.
|
||||
let per_block_percent_changes: Vec<f64> =
|
||||
per_block_comparisons.iter().map(|c| c.new_payload_latency_change_percent).collect();
|
||||
let per_block_latency_change_mean_percent = if per_block_percent_changes.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
per_block_percent_changes.iter().sum::<f64>() / per_block_percent_changes.len() as f64
|
||||
};
|
||||
let per_block_latency_change_median_percent = if per_block_percent_changes.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
let mut sorted = per_block_percent_changes.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
percentile(&sorted, 0.5)
|
||||
};
|
||||
let per_block_latency_change_std_dev_percent =
|
||||
calculate_std_dev(&per_block_percent_changes, per_block_latency_change_mean_percent);
|
||||
|
||||
let baseline_total_latency_ms =
|
||||
baseline.mean_new_payload_latency_ms * baseline.total_blocks as f64;
|
||||
let feature_total_latency_ms =
|
||||
feature.mean_new_payload_latency_ms * feature.total_blocks as f64;
|
||||
let new_payload_total_latency_change_percent =
|
||||
calc_percent_change(baseline_total_latency_ms, feature_total_latency_ms);
|
||||
|
||||
Ok(ComparisonSummary {
|
||||
per_block_latency_change_mean_percent,
|
||||
per_block_latency_change_median_percent,
|
||||
per_block_latency_change_std_dev_percent,
|
||||
new_payload_total_latency_change_percent,
|
||||
new_payload_latency_p50_change_percent: calc_percent_change(
|
||||
baseline.median_new_payload_latency_ms,
|
||||
feature.median_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p90_change_percent: calc_percent_change(
|
||||
baseline.p90_new_payload_latency_ms,
|
||||
feature.p90_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p99_change_percent: calc_percent_change(
|
||||
baseline.p99_new_payload_latency_ms,
|
||||
feature.p99_new_payload_latency_ms,
|
||||
),
|
||||
gas_per_second_change_percent: calc_percent_change(
|
||||
baseline.gas_per_second,
|
||||
feature.gas_per_second,
|
||||
),
|
||||
blocks_per_second_change_percent: calc_percent_change(
|
||||
baseline.blocks_per_second,
|
||||
feature.blocks_per_second,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate per-block comparisons
|
||||
fn calculate_per_block_comparisons(
|
||||
&self,
|
||||
baseline: &BenchmarkResults,
|
||||
feature: &BenchmarkResults,
|
||||
) -> Result<Vec<BlockComparison>> {
|
||||
let mut baseline_map: HashMap<u64, &CombinedLatencyRow> = HashMap::new();
|
||||
for row in &baseline.combined_latency_data {
|
||||
baseline_map.insert(row.block_number, row);
|
||||
}
|
||||
|
||||
let mut comparisons = Vec::new();
|
||||
for feature_row in &feature.combined_latency_data {
|
||||
if let Some(baseline_row) = baseline_map.get(&feature_row.block_number) {
|
||||
let calc_percent_change = |baseline: u128, feature: u128| -> f64 {
|
||||
if baseline > 0 {
|
||||
((feature as f64 - baseline as f64) / baseline as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
let comparison = BlockComparison {
|
||||
block_number: feature_row.block_number,
|
||||
transaction_count: feature_row.transaction_count,
|
||||
gas_used: feature_row.gas_used,
|
||||
baseline_new_payload_latency: baseline_row.new_payload_latency,
|
||||
feature_new_payload_latency: feature_row.new_payload_latency,
|
||||
new_payload_latency_change_percent: calc_percent_change(
|
||||
baseline_row.new_payload_latency,
|
||||
feature_row.new_payload_latency,
|
||||
),
|
||||
};
|
||||
comparisons.push(comparison);
|
||||
} else {
|
||||
warn!("Block {} not found in baseline data", feature_row.block_number);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(comparisons)
|
||||
}
|
||||
|
||||
/// Write comparison reports to files
|
||||
async fn write_comparison_reports(&self, report: &ComparisonReport) -> Result<()> {
|
||||
let report_dir = self.output_dir.join("results").join(&self.timestamp);
|
||||
fs::create_dir_all(&report_dir)
|
||||
.wrap_err_with(|| format!("Failed to create report directory: {report_dir:?}"))?;
|
||||
|
||||
// Write JSON report
|
||||
let json_path = report_dir.join("comparison_report.json");
|
||||
let json_content = serde_json::to_string_pretty(report)
|
||||
.wrap_err("Failed to serialize comparison report to JSON")?;
|
||||
fs::write(&json_path, json_content)
|
||||
.wrap_err_with(|| format!("Failed to write JSON report: {json_path:?}"))?;
|
||||
|
||||
// Write CSV report for per-block comparisons
|
||||
let csv_path = report_dir.join("per_block_comparison.csv");
|
||||
let mut writer = csv::Writer::from_path(&csv_path)
|
||||
.wrap_err_with(|| format!("Failed to create CSV writer: {csv_path:?}"))?;
|
||||
|
||||
for comparison in &report.per_block_comparisons {
|
||||
writer.serialize(comparison).wrap_err("Failed to write comparison row to CSV")?;
|
||||
}
|
||||
writer.flush().wrap_err("Failed to flush CSV writer")?;
|
||||
|
||||
info!("Comparison reports written to: {:?}", report_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print comparison summary to console
|
||||
fn print_comparison_summary(&self, report: &ComparisonReport) {
|
||||
// Parse and format timestamp nicely
|
||||
let formatted_timestamp = if let Ok(dt) = chrono::DateTime::parse_from_str(
|
||||
&format!("{} +0000", report.timestamp.replace('_', " ")),
|
||||
"%Y%m%d %H%M%S %z",
|
||||
) {
|
||||
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
} else {
|
||||
// Fallback to original if parsing fails
|
||||
report.timestamp.clone()
|
||||
};
|
||||
|
||||
println!("\n=== BENCHMARK COMPARISON SUMMARY ===");
|
||||
println!("Timestamp: {formatted_timestamp}");
|
||||
println!("Baseline: {}", report.baseline.ref_name);
|
||||
println!("Feature: {}", report.feature.ref_name);
|
||||
println!();
|
||||
|
||||
let summary = &report.comparison_summary;
|
||||
|
||||
println!("Performance Changes:");
|
||||
println!(
|
||||
" NewPayload Latency per-block mean change: {:+.2}%",
|
||||
summary.per_block_latency_change_mean_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency per-block median change: {:+.2}%",
|
||||
summary.per_block_latency_change_median_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency per-block std dev: {:.2}%",
|
||||
summary.per_block_latency_change_std_dev_percent
|
||||
);
|
||||
println!(
|
||||
" Total newPayload time change: {:+.2}%",
|
||||
summary.new_payload_total_latency_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p50: {:+.2}%",
|
||||
summary.new_payload_latency_p50_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p90: {:+.2}%",
|
||||
summary.new_payload_latency_p90_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p99: {:+.2}%",
|
||||
summary.new_payload_latency_p99_change_percent
|
||||
);
|
||||
println!(
|
||||
" Gas/Second: {:+.2}%",
|
||||
summary.gas_per_second_change_percent
|
||||
);
|
||||
println!(
|
||||
" Blocks/Second: {:+.2}%",
|
||||
summary.blocks_per_second_change_percent
|
||||
);
|
||||
println!();
|
||||
|
||||
println!("Baseline Summary:");
|
||||
let baseline = &report.baseline.summary;
|
||||
println!(
|
||||
" Blocks: {} (blocks {} to {}), Gas: {}, Duration: {:.2}s",
|
||||
baseline.total_blocks,
|
||||
baseline.min_block_number,
|
||||
baseline.max_block_number,
|
||||
baseline.total_gas_used,
|
||||
baseline.total_duration_ms as f64 / 1000.0
|
||||
);
|
||||
println!(" NewPayload latency (ms):");
|
||||
println!(
|
||||
" mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}",
|
||||
baseline.mean_new_payload_latency_ms,
|
||||
baseline.median_new_payload_latency_ms,
|
||||
baseline.p90_new_payload_latency_ms,
|
||||
baseline.p99_new_payload_latency_ms
|
||||
);
|
||||
if let (Some(start), Some(end)) =
|
||||
(&report.baseline.start_timestamp, &report.baseline.end_timestamp)
|
||||
{
|
||||
println!(
|
||||
" Started: {}, Ended: {}",
|
||||
start.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
end.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
if let Some(ref cmd) = report.baseline.reth_command {
|
||||
println!(" Command: {}", cmd);
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("Feature Summary:");
|
||||
let feature = &report.feature.summary;
|
||||
println!(
|
||||
" Blocks: {} (blocks {} to {}), Gas: {}, Duration: {:.2}s",
|
||||
feature.total_blocks,
|
||||
feature.min_block_number,
|
||||
feature.max_block_number,
|
||||
feature.total_gas_used,
|
||||
feature.total_duration_ms as f64 / 1000.0
|
||||
);
|
||||
println!(" NewPayload latency (ms):");
|
||||
println!(
|
||||
" mean: {:.2}, p50: {:.2}, p90: {:.2}, p99: {:.2}",
|
||||
feature.mean_new_payload_latency_ms,
|
||||
feature.median_new_payload_latency_ms,
|
||||
feature.p90_new_payload_latency_ms,
|
||||
feature.p99_new_payload_latency_ms
|
||||
);
|
||||
if let (Some(start), Some(end)) =
|
||||
(&report.feature.start_timestamp, &report.feature.end_timestamp)
|
||||
{
|
||||
println!(
|
||||
" Started: {}, Ended: {}",
|
||||
start.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
end.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
if let Some(ref cmd) = report.feature.reth_command {
|
||||
println!(" Command: {}", cmd);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate standard deviation from a set of values and their mean.
|
||||
///
|
||||
/// Computes the population standard deviation using the formula:
|
||||
/// `sqrt(sum((x - mean)²) / n)`
|
||||
///
|
||||
/// Returns 0.0 for empty input.
|
||||
fn calculate_std_dev(values: &[f64], mean: f64) -> f64 {
|
||||
if values.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let variance = values
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let diff = x - mean;
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f64>() /
|
||||
values.len() as f64;
|
||||
|
||||
variance.sqrt()
|
||||
}
|
||||
|
||||
/// Calculate percentile using linear interpolation on a sorted slice.
|
||||
///
|
||||
/// Computes `rank = percentile × (n - 1)` where n is the array length. If the rank falls
|
||||
/// between two indices, linearly interpolates between those values. For example, with 100 values,
|
||||
/// p90 computes rank = 0.9 × 99 = 89.1, then returns `values[89] × 0.9 + values[90] × 0.1`.
|
||||
///
|
||||
/// Returns 0.0 for empty input.
|
||||
fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
|
||||
if sorted_values.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let clamped = percentile.clamp(0.0, 1.0);
|
||||
let max_index = sorted_values.len() - 1;
|
||||
let rank = clamped * max_index as f64;
|
||||
let lower = rank.floor() as usize;
|
||||
let upper = rank.ceil() as usize;
|
||||
|
||||
if lower == upper {
|
||||
sorted_values[lower]
|
||||
} else {
|
||||
let weight = rank - lower as f64;
|
||||
sorted_values[lower].mul_add(1.0 - weight, sorted_values[upper] * weight)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user