Compare commits

..

20 Commits
snapv2 ... main

Author SHA1 Message Date
DaniPopes
fcfa8287f6 chore(mdbx): replace deprecated MDBX_NOTLS with MDBX_NOSTICKYTHREADS (#23378) 2026-04-30 03:19:09 +00:00
Arsenii Kulikov
d25de30050 feat: customizable discovery defaults (#23843)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-29 22:59:10 +00:00
Derek Cofausper
4ffde69d94 fix(engine): apply finalized state after syncing FCU head import (#23838)
Co-authored-by: Centaur AI <ai@centaur.local>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: klkvr <klkvrr@gmail.com>
2026-04-29 15:29:42 +00:00
Arsenii Kulikov
077e5eecfe chore: don't enforce non-empty blocks in e2e payload building (#23837)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-29 14:36:44 +00:00
Sergei Shulepov
709485dcb7 perf(bench): buffer RPC fetches in generate-big-block (#23830)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:57:05 +00:00
Arsenii Kulikov
88505c7fcb fix(re-execute): properly handle selfdestructed storage slots (#23832) 2026-04-29 12:30:27 +00:00
Emma Jamieson-Hoare
c14bc59236 chore: release 2.2.0 (#23831)
Co-authored-by: Amp <amp@ampcode.com>
2026-04-29 12:25:15 +00:00
Derek Cofausper
347c1325cc fix: skip move_to_static_files for storage.v2 (#23814) 2026-04-29 12:19:43 +00:00
Matthias Seitz
5f85eb7ac8 feat(engine): add getBlobsV4 endpoint (#23767) 2026-04-29 12:02:25 +00:00
Brian Picciano
a12454d2e6 perf(db): prebind cursor operation metrics (#23654)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
2026-04-29 11:40:17 +00:00
Matthias Seitz
c194c17a27 chore(deps): bump alloy to 2.0.4 (#23828) 2026-04-29 13:41:02 +02:00
figtracer
43a7452b0e fix(rpc): narrow getLogs retry range (#23818)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-29 10:09:49 +00:00
Matthias Seitz
73ec2c9d56 docs(engine): clarify BAL storage prefetch flag (#23815) 2026-04-29 08:37:51 +00:00
Alexey Shekhirin
76e886578b ci(hive): update amsterdam fixtures and branch (#23807) 2026-04-29 07:48:24 +00:00
Arsenii Kulikov
ad08829288 feat: introduce memory-bound channel for network<->tx manager messages (#23802)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-28 21:29:19 +00:00
grandizzy
b89288582b ci: harden supply chain across all workflows (#23785) 2026-04-28 15:44:05 +00:00
Arsenii Kulikov
87d878a979 feat: support binding discv5 and discv4 to the same port (#23613) 2026-04-28 15:08:19 +00:00
Matthias Seitz
473f85c558 test(rpc): cover admin node info discv5 port (#23781) 2026-04-28 15:02:24 +00:00
Brian Picciano
a8eee6028f fix(bench): run feature first in GitHub workflow (#23777)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-28 14:56:06 +00:00
Sergei Shulepov
671da55884 refactor: expose executor transaction result type (#23759) 2026-04-28 14:36:45 +00:00
177 changed files with 3606 additions and 7784 deletions

View File

@@ -4,10 +4,14 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
labels:
- "A-dependencies"
commit-message:

View File

@@ -323,13 +323,18 @@ if [ "$BIG_BLOCKS" = "true" ]; then
--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] /"
WARMUP="${BENCH_WARMUP_BLOCKS:-50}"
if [ "$WARMUP" -gt 0 ] 2>/dev/null; then
# Warm up the node before measuring the benchmark window.
$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 "$WARMUP" \
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
else
echo "Skipping warmup (0 blocks)..."
fi
# Start tracy-capture after warmup so profile only covers the benchmark
if [ "${BENCH_TRACY:-off}" != "off" ]; then

View File

@@ -5,8 +5,8 @@ fixture_variant="${1:-osaka}"
case "${fixture_variant}" in
amsterdam)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/bal@v6.0.0/fixtures_bal.tar.gz"
eels_branch="devnets/snøbal/4"
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/snobal-devnet-5@v8037.0.0/fixtures_snobal-devnet-5.tar.gz"
eels_branch="devnets/snobal/5"
;;
osaka)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz"

View File

@@ -140,21 +140,6 @@ eels/consume-engine:
# this test inserts a chain via chain.rlp where the last block is invalid, but expects import to stop there, this doesn't work properly with our pipeline import approach hence the import fails when the invalid block is detected.
#. In other words, if this test fails, this means we're correctly rejecting the block.
#. The same test exists in the consume-engine simulator where it is passing as expected
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
eels/consume-rlp:
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test-zero_nonce]-reth
- tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py::test_system_contract_errors[fork_Prague-blockchain_test_engine-system_contract_reaches_gas_limit-system_contract_0x0000bbddc7ce488642fb579f8b00f3a590007251]-reth
@@ -256,3 +241,23 @@ eels/consume-rlp:
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Prague-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Shanghai-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Paris-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Cancun-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Osaka-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-initcode-with-deploy]-reth

View File

@@ -54,9 +54,7 @@ env:
name: bench-scheduled
permissions:
contents: read
actions: read
permissions: {}
jobs:
# ---------------------------------------------------------------------------
@@ -65,6 +63,9 @@ jobs:
resolve-refs:
name: resolve-refs
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
outputs:
mode: ${{ steps.mode.outputs.mode }}
baseline-ref: ${{ steps.refs.outputs.baseline-ref }}
@@ -76,21 +77,26 @@ jobs:
long-running: ${{ steps.refs.outputs.long-running }}
release-tag: ${{ steps.refs.outputs.release-tag }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
fetch-depth: 2
- name: Detect mode
id: mode
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_MODE: ${{ inputs.mode }}
SCHEDULE: ${{ github.event.schedule }}
run: |
# Maps cron schedules to modes (must match the schedule entries above)
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
MODE="${{ inputs.mode || 'nightly' }}"
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
MODE="${INPUT_MODE:-nightly}"
elif [ "$SCHEDULE" = "30 5 * * *" ]; then
MODE="nightly"
elif [ "${{ github.event.schedule }}" = "0 9 * * *" ]; then
elif [ "$SCHEDULE" = "0 9 * * *" ]; then
MODE="release"
else
MODE="hourly"
@@ -105,14 +111,15 @@ jobs:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
INPUT_FORCE: ${{ inputs.force || 'false' }}
run: |
FORCE="${{ inputs.force || 'false' }}"
FORCE="${INPUT_FORCE:-false}"
MODE="${{ steps.mode.outputs.mode }}"
.github/scripts/bench-scheduled-refs.sh "$FORCE" "$MODE"
- name: Alert on long-running hourly
if: steps.mode.outputs.mode == 'hourly' && steps.refs.outputs.long-running == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -154,7 +161,7 @@ jobs:
- name: Alert on stale nightly
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.slack == 'never')
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -242,6 +249,9 @@ jobs:
needs.resolve-refs.outputs.is-stale != 'true'
name: bench-scheduled
runs-on: [self-hosted, Linux, X64, available]
permissions:
contents: read
actions: read
timeout-minutes: 120
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
@@ -270,15 +280,16 @@ jobs:
- name: Clean up previous bench-work
run: sudo rm -rf "$BENCH_WORK_DIR" 2>/dev/null || true
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: true
fetch-depth: 0
ref: ${{ needs.resolve-refs.outputs.feature-ref }}
- name: Resolve job URL
id: job-url
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
@@ -291,8 +302,9 @@ jobs:
core.exportVariable('BENCH_JOB_URL', jobUrl);
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
continue-on-error: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Install dependencies
env:
@@ -628,7 +640,7 @@ jobs:
- name: Upload results
if: "!cancelled()"
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bench-scheduled-results
path: ${{ env.BENCH_WORK_DIR }}
@@ -636,10 +648,12 @@ jobs:
- name: Push charts
id: push-charts
if: success() && env.BENCH_MODE != 'hourly'
env:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
RUN_ID: ${{ github.run_id }}
run: |
RUN_ID=${{ github.run_id }}
CHART_DIR="${BENCH_MODE}/${RUN_ID}"
CHARTS_REPO="https://x-access-token:${{ secrets.DEREK_TOKEN }}@github.com/decofe/reth-bench-charts.git"
CHARTS_REPO="https://x-access-token:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
TMP_DIR=$(mktemp -d)
if git clone --depth 1 "${CHARTS_REPO}" "${TMP_DIR}" 2>/dev/null; then
@@ -660,7 +674,7 @@ jobs:
- name: Write job summary
if: success()
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs');
@@ -739,7 +753,7 @@ jobs:
- name: Send Slack notification (success)
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -894,7 +908,7 @@ jobs:
- name: Send Slack notification (failure)
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}

View File

@@ -82,7 +82,7 @@ on:
- on-error
- never
abba:
description: "Run ABBA (BFFB) interleaved order; false = single AB pass"
description: "Run ABBA (FBBF) interleaved order; false = single FB pass"
required: false
default: "true"
type: boolean
@@ -99,9 +99,7 @@ env:
name: bench
permissions:
contents: read
pull-requests: write
permissions: {}
jobs:
reth-bench-ack:
@@ -110,6 +108,9 @@ jobs:
github.event_name == 'workflow_dispatch'
name: reth-bench-ack
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
outputs:
pr: ${{ steps.args.outputs.pr }}
actor: ${{ steps.args.outputs.actor }}
@@ -133,7 +134,7 @@ jobs:
steps:
- name: Check org membership
if: github.event_name == 'issue_comment'
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -152,7 +153,7 @@ jobs:
- name: Parse arguments
id: args
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -359,7 +360,7 @@ jobs:
- name: Acknowledge request
id: ack
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -445,7 +446,7 @@ jobs:
- name: Poll queue position
if: steps.ack.outputs.comment-id && steps.ack.outputs.queue-position != '0'
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -529,6 +530,9 @@ jobs:
needs: reth-bench-ack
name: reth-bench
runs-on: [self-hosted, Linux, X64, available]
permissions:
contents: read
pull-requests: write
timeout-minutes: 120
env:
BENCH_RPC_URL: https://ethereum.reth.rs/rpc
@@ -560,7 +564,7 @@ jobs:
- name: Resolve checkout ref
id: checkout-ref
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
if (!process.env.BENCH_PR) {
@@ -578,15 +582,16 @@ jobs:
core.info(`PR #${process.env.BENCH_PR} (${pr.state}), using head SHA ${pr.head.sha}`);
core.setOutput('ref', pr.head.sha);
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: true
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.ref }}
- name: Resolve job URL and update status
if: env.BENCH_COMMENT_ID
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -634,8 +639,9 @@ jobs:
});
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
continue-on-error: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Install dependencies
env:
@@ -696,7 +702,7 @@ jobs:
# Build binaries
- name: Resolve PR head branch
id: pr-info
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
if (process.env.BENCH_PR) {
@@ -714,7 +720,7 @@ jobs:
- name: Resolve refs
id: refs
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { execSync } = require('child_process');
@@ -937,28 +943,15 @@ jobs:
- name: Update status (running benchmarks)
if: success() && env.BENCH_COMMENT_ID
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
const s = require('./.github/scripts/bench-update-status.js');
await s({github, context, status: 'Running benchmarks...'});
# Interleaved run order (B-F-F-B) to reduce systematic bias from
# Interleaved run order (F-B-B-F) to reduce systematic bias from
# thermal drift and cache warming.
- name: "Run benchmark: baseline (1/2)"
id: run-baseline-1
env:
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-1,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"baseline-1","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
env:
@@ -972,19 +965,18 @@ jobs:
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: feature (2/2)"
if: env.BENCH_ABBA != 'false'
id: run-feature-2
- name: "Run benchmark: baseline (1/2)"
id: run-baseline-1
env:
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-2,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-1,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"feature-2","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
{"benchmark_run":"baseline-1","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: baseline (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -1000,6 +992,20 @@ jobs:
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
- name: "Run benchmark: feature (2/2)"
if: env.BENCH_ABBA != 'false'
id: run-feature-2
env:
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-2,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
run: |
LAST_RUN_START=$(date +%s)
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"feature-2","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
- name: Stop metrics proxy & generate Grafana URL
id: metrics
if: "!cancelled()"
@@ -1166,7 +1172,7 @@ jobs:
- name: Upload results
if: "!cancelled()"
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bench-reth-results
path: ${{ env.BENCH_WORK_DIR }}
@@ -1174,11 +1180,13 @@ jobs:
- name: Push charts
id: push-charts
if: success()
env:
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
RUN_ID: ${{ github.run_id }}
run: |
PR_NUMBER="${BENCH_PR:-0}"
RUN_ID=${{ github.run_id }}
CHART_DIR="pr/${PR_NUMBER}/${RUN_ID}"
CHARTS_REPO="https://x-access-token:${{ secrets.DEREK_TOKEN }}@github.com/decofe/reth-bench-charts.git"
CHARTS_REPO="https://x-access-token:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
TMP_DIR=$(mktemp -d)
if git clone --depth 1 "${CHARTS_REPO}" "${TMP_DIR}" 2>/dev/null; then
@@ -1191,15 +1199,35 @@ jobs:
mkdir -p "${TMP_DIR}/${CHART_DIR}"
cp "$BENCH_WORK_DIR"/charts/*.png "${TMP_DIR}/${CHART_DIR}/"
git -C "${TMP_DIR}" add "${CHART_DIR}"
if git -C "${TMP_DIR}" diff --cached --quiet; then
echo "Charts for ${CHART_DIR} are already present, skipping push"
echo "sha=$(git -C "${TMP_DIR}" rev-parse HEAD)" >> "$GITHUB_OUTPUT"
rm -rf "${TMP_DIR}"
exit 0
fi
git -C "${TMP_DIR}" -c user.name="github-actions" -c user.email="github-actions@github.com" \
commit -m "bench charts for PR #${PR_NUMBER} run ${RUN_ID}"
git -C "${TMP_DIR}" push origin HEAD:main
for attempt in 1 2 3 4 5; do
if git -C "${TMP_DIR}" push origin HEAD:main; then
break
fi
if [ "$attempt" -eq 5 ]; then
echo "::error::Failed to push charts after ${attempt} attempts"
rm -rf "${TMP_DIR}"
exit 1
fi
sleep "$attempt"
git -C "${TMP_DIR}" fetch origin main
git -C "${TMP_DIR}" rebase origin/main
done
echo "sha=$(git -C "${TMP_DIR}" rev-parse HEAD)" >> "$GITHUB_OUTPUT"
rm -rf "${TMP_DIR}"
- name: Compare & comment
if: success() && env.BENCH_COMMENT_ID
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -1235,7 +1263,7 @@ jobs:
// Samply profile links (URLs point directly to Firefox Profiler)
if (process.env.BENCH_SAMPLY === 'true') {
const abba = (process.env.BENCH_ABBA || 'true') !== 'false';
const runs = abba ? ['baseline-1', 'feature-1', 'feature-2', 'baseline-2'] : ['baseline-1', 'feature-1'];
const runs = abba ? ['feature-1', 'baseline-1', 'baseline-2', 'feature-2'] : ['feature-1', 'baseline-1'];
const links = [];
for (const run of runs) {
try {
@@ -1279,7 +1307,7 @@ jobs:
- name: Write job summary
if: success()
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const jobSummary = require('./.github/scripts/bench-job-summary.js');
@@ -1293,7 +1321,7 @@ jobs:
- name: Send Slack notification (success)
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -1304,7 +1332,7 @@ jobs:
- name: Update status (failed)
if: failure() && env.BENCH_COMMENT_ID
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |
@@ -1314,10 +1342,10 @@ jobs:
...(bigBlocks ? [['validating local big-block data', '${{ steps.big-blocks-check.outcome }}']] : []),
['validating local snapshot', '${{ steps.snapshot-check.outcome }}'],
['building binaries', '${{ steps.build.outcome }}'],
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
];
const failed = steps_status.find(([, o]) => o === 'failure');
const failedStep = failed ? failed[0] : 'unknown step';
@@ -1340,7 +1368,7 @@ jobs:
- name: Send Slack notification (failure)
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
@@ -1352,10 +1380,10 @@ jobs:
...(bigBlocks ? [['validating local big-block data', '${{ steps.big-blocks-check.outcome }}']] : []),
['validating local snapshot', '${{ steps.snapshot-check.outcome }}'],
['building binaries', '${{ steps.build.outcome }}'],
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
];
const failed = steps_status.find(([, o]) => o === 'failure');
const failedStep = failed ? failed[0] : 'unknown step';
@@ -1364,7 +1392,7 @@ jobs:
- name: Update status (cancelled)
if: cancelled() && env.BENCH_COMMENT_ID
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.DEREK_PAT }}
script: |

View File

@@ -10,19 +10,22 @@ on:
types: [opened, reopened, synchronize, closed]
merge_group:
env:
RUSTC_WRAPPER: "sccache"
permissions: {}
jobs:
build:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: v1.2.23
@@ -36,8 +39,6 @@ jobs:
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Build docs
run: cd docs/vocs && bash scripts/build-cargo-docs.sh
@@ -47,10 +48,10 @@ jobs:
echo "Vocs Build Complete"
- name: Setup Pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: "./docs/vocs/docs/dist"
@@ -74,4 +75,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0

View File

@@ -22,31 +22,41 @@ on:
env:
CARGO_TERM_COLOR: always
permissions: {}
jobs:
check:
name: Check compilation with patched alloy
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Apply alloy patches
env:
ALLOY_BRANCH: ${{ inputs.alloy_branch }}
ALLOY_EVM_BRANCH: ${{ inputs.alloy_evm_branch }}
OP_ALLOY_BRANCH: ${{ inputs.op_alloy_branch }}
run: |
ARGS=""
if [ -n "${{ inputs.alloy_branch }}" ]; then
ARGS="$ARGS --alloy ${{ inputs.alloy_branch }}"
if [ -n "$ALLOY_BRANCH" ]; then
ARGS="$ARGS --alloy $ALLOY_BRANCH"
fi
if [ -n "${{ inputs.alloy_evm_branch }}" ]; then
ARGS="$ARGS --evm ${{ inputs.alloy_evm_branch }}"
if [ -n "$ALLOY_EVM_BRANCH" ]; then
ARGS="$ARGS --evm $ALLOY_EVM_BRANCH"
fi
if [ -n "${{ inputs.op_alloy_branch }}" ]; then
ARGS="$ARGS --op ${{ inputs.op_alloy_branch }}"
if [ -n "$OP_ALLOY_BRANCH" ]; then
ARGS="$ARGS --op $OP_ALLOY_BRANCH"
fi
if [ -z "$ARGS" ]; then

View File

@@ -16,32 +16,38 @@ env:
RUSTC_WRAPPER: "sccache"
name: compact-codec
permissions: {}
jobs:
compact-codec:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
strategy:
matrix:
bin:
- cargo run --bin reth --features "dev"
steps:
- uses: rui314/setup-mold@v1
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Checkout base
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref || 'main' }}
persist-credentials: false
# On `main` branch, generates test vectors and serializes them to disk using `Compact`.
- name: Generate compact vectors
run: |
${{ matrix.bin }} -- test-vectors compact --write
- name: Checkout PR
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
clean: false
persist-credentials: false
# On incoming merge try to read and decode previously generated vectors with `Compact`
- name: Read vectors
run: ${{ matrix.bin }} -- test-vectors compact --read

View File

@@ -9,13 +9,14 @@ on:
workflow_dispatch:
# Needed so we can run it manually
permissions:
contents: write
pull-requests: write
permissions: {}
jobs:
update:
if: github.repository == 'paradigmxyz/reth'
permissions:
contents: write
pull-requests: write
uses: tempoxyz/ci/.github/workflows/cargo-update-pr.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -17,6 +17,8 @@ on:
env:
DOCKER_USERNAME: ${{ github.actor }}
permissions: {}
jobs:
tag-reth-latest:
name: Tag reth as latest
@@ -27,16 +29,22 @@ jobs:
contents: read
steps:
- name: Log in to Docker
env:
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
echo "$DOCKER_PASSWORD" | docker login ghcr.io --username "${DOCKER_USERNAME}" --password-stdin
- name: Pull reth release image
env:
VERSION: ${{ inputs.version }}
run: |
docker pull ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }}
docker pull ghcr.io/${{ github.repository_owner }}/reth:${VERSION}
- name: Tag reth as latest
env:
VERSION: ${{ inputs.version }}
run: |
docker tag ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }} ghcr.io/${{ github.repository_owner }}/reth:latest
docker tag ghcr.io/${{ github.repository_owner }}/reth:${VERSION} ghcr.io/${{ github.repository_owner }}/reth:latest
- name: Push reth latest tag
run: |

View File

@@ -13,6 +13,8 @@ on:
default: "artifacts"
description: "Name for the uploaded artifact"
permissions: {}
jobs:
build:
timeout-minutes: 45
@@ -21,7 +23,9 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- run: mkdir -p artifacts
- name: Get git info
@@ -32,8 +36,12 @@ jobs:
- name: Detect fork
id: fork
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
REPO: ${{ github.repository }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
if [ "$EVENT_NAME" = "pull_request" ] && [ "$HEAD_REPO" != "$REPO" ]; then
echo "is_fork=true" >> "$GITHUB_OUTPUT"
else
echo "is_fork=false" >> "$GITHUB_OUTPUT"
@@ -42,11 +50,11 @@ jobs:
# Depot build (upstream only)
- name: Set up Depot CLI
if: steps.fork.outputs.is_fork == 'false'
uses: depot/setup-action@v1
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
- name: Build reth image (Depot)
if: steps.fork.outputs.is_fork == 'false'
uses: depot/bake-action@v1
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
@@ -60,11 +68,11 @@ jobs:
# Docker build (forks)
- name: Set up Docker Buildx
if: steps.fork.outputs.is_fork == 'true'
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build reth image (Docker)
if: steps.fork.outputs.is_fork == 'true'
uses: docker/bake-action@v6
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0
env:
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
@@ -76,7 +84,7 @@ jobs:
*.dockerfile=Dockerfile
- name: Upload reth image
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ inputs.artifact_name }}
path: ./artifacts

View File

@@ -29,6 +29,8 @@ on:
type: boolean
default: false
permissions: {}
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
@@ -39,13 +41,15 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Depot CLI
uses: depot/setup-action@v1
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
- name: Log in to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -60,10 +64,13 @@ jobs:
- name: Determine build parameters
id: params
env:
EVENT_NAME: ${{ github.event_name }}
BUILD_TYPE: ${{ inputs.build_type }}
run: |
REGISTRY="ghcr.io/${{ github.repository_owner }}"
if [[ "${{ github.event_name }}" == "push" ]]; then
if [[ "${EVENT_NAME}" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
@@ -81,7 +88,7 @@ jobs:
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
fi
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
elif [[ "${EVENT_NAME}" == "schedule" ]] || [[ "${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"
@@ -94,7 +101,7 @@ jobs:
fi
- name: Build and push images
uses: depot/bake-action@v1
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
env:
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
@@ -105,6 +112,8 @@ jobs:
files: docker-bake.hcl
targets: ${{ steps.params.outputs.targets }}
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
save: false
load: false
set: |
${{ steps.params.outputs.ethereum_set }}
@@ -124,7 +133,7 @@ jobs:
if: failure() && github.event_name == 'schedule'
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
env:
SLACK_COLOR: danger
SLACK_ICON_EMOJI: ":rotating_light:"

View File

@@ -17,19 +17,27 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: e2e-testsuite
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 90
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Run e2e tests
@@ -48,15 +56,21 @@ jobs:
rocksdb:
name: e2e-rocksdb
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Run RocksDB e2e tests

View File

@@ -12,6 +12,8 @@ on:
required: true
default: "etc/grafana/dashboards/overview.json"
permissions: {}
jobs:
fetch:
runs-on: ubuntu-latest
@@ -19,9 +21,11 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -29,14 +33,18 @@ jobs:
env:
FETCH_GRAFANA_DASHBOARD_URL: ${{ secrets.FETCH_GRAFANA_DASHBOARD_URL }}
FETCH_GRAFANA_DASHBOARD_TOKEN: ${{ secrets.FETCH_GRAFANA_DASHBOARD_TOKEN }}
DASHBOARD_UID: ${{ inputs.dashboard_uid }}
TARGET_PATH: ${{ inputs.target_path }}
run: |
python3 .github/scripts/fetch-grafana-dashboard.py "${{ inputs.dashboard_uid }}" \
> "${{ inputs.target_path }}"
python3 .github/scripts/fetch-grafana-dashboard.py "${DASHBOARD_UID}" \
> "${TARGET_PATH}"
- name: Check for changes
id: diff
env:
TARGET_PATH: ${{ inputs.target_path }}
run: |
if git diff --quiet "${{ inputs.target_path }}"; then
if git diff --quiet "${TARGET_PATH}"; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No changes detected."
else
@@ -47,8 +55,10 @@ jobs:
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DASHBOARD_UID: ${{ inputs.dashboard_uid }}
TARGET_PATH: ${{ inputs.target_path }}
run: |
TARGET="${{ inputs.target_path }}"
TARGET="${TARGET_PATH}"
FILENAME="$(basename "$TARGET")"
BRANCH="chore/sync-grafana-${FILENAME%.*}-$(date +%Y%m%d-%H%M%S)"
git config user.name "github-actions[bot]"
@@ -59,4 +69,4 @@ jobs:
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}\`)."
--body "Automated export from Grafana (dashboard UID: \`${DASHBOARD_UID}\`, target: \`${TARGET}\`)."

View File

@@ -6,11 +6,17 @@ on:
push:
branches: [main]
permissions: {}
jobs:
check-dashboard:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate dashboard format
run: |
python3 -c "

View File

@@ -6,9 +6,6 @@ on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
pull_request:
branches:
- "**"
env:
CARGO_TERM_COLOR: always
@@ -17,8 +14,13 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
build-reth:
permissions:
contents: read
id-token: write
uses: ./.github/workflows/docker-test.yml
with:
hive_target: hive
@@ -26,9 +28,11 @@ jobs:
secrets: inherit
prepare-hive:
if: github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth'
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
@@ -37,25 +41,28 @@ jobs:
- osaka
name: Prepare Hive - ${{ matrix.variant == 'amsterdam' && 'Amsterdam' || 'Osaka' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Checkout hive tests
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ethereum/hive
path: hivetests
persist-credentials: false
- name: Get hive commit hash
id: hive-commit
run: echo "hash=$(cd hivetests && git rev-parse HEAD)" >> $GITHUB_OUTPUT
- uses: actions/setup-go@v6
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "^1.13.1"
- run: go version
- name: Restore hive assets cache
id: cache-hive
uses: actions/cache@v5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ./hive_assets
key: hive-assets-${{ matrix.variant }}-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
@@ -78,7 +85,7 @@ jobs:
chmod +x hive
- name: Upload hive assets
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: hive_assets_${{ matrix.variant }}
path: ./hive_assets
@@ -194,22 +201,24 @@ jobs:
- prepare-hive
name: Hive-Amsterdam / ${{ 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-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: hive_assets_amsterdam
path: /tmp
- name: Download reth image
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reth
path: /tmp
@@ -223,16 +232,21 @@ jobs:
chmod +x /usr/local/bin/hive
- name: Checkout hive tests
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ethereum/hive
ref: master
path: hivetests
persist-credentials: false
- name: Run simulator
env:
SCENARIO_SIM: ${{ matrix.scenario.sim }}
SCENARIO_LIMIT: ${{ matrix.scenario.limit }}
SCENARIO_TESTS: ${{ join(matrix.scenario.include, '|') }}
run: |
LIMIT="${{ matrix.scenario.limit }}"
TESTS="${{ join(matrix.scenario.include, '|') }}"
LIMIT="$SCENARIO_LIMIT"
TESTS="$SCENARIO_TESTS"
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
FILTER="$LIMIT/$TESTS"
elif [ -n "$LIMIT" ]; then
@@ -243,7 +257,7 @@ jobs:
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "amsterdam"
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "amsterdam"
- name: Parse hive output
run: |
@@ -367,22 +381,24 @@ jobs:
- prepare-hive
name: Hive-Osaka / ${{ 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-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: hive_assets_osaka
path: /tmp
- name: Download reth image
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reth
path: /tmp
@@ -396,16 +412,21 @@ jobs:
chmod +x /usr/local/bin/hive
- name: Checkout hive tests
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ethereum/hive
ref: master
path: hivetests
persist-credentials: false
- name: Run simulator
env:
SCENARIO_SIM: ${{ matrix.scenario.sim }}
SCENARIO_LIMIT: ${{ matrix.scenario.limit }}
SCENARIO_TESTS: ${{ join(matrix.scenario.include, '|') }}
run: |
LIMIT="${{ matrix.scenario.limit }}"
TESTS="${{ join(matrix.scenario.include, '|') }}"
LIMIT="$SCENARIO_LIMIT"
TESTS="$SCENARIO_TESTS"
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
FILTER="$LIMIT/$TESTS"
elif [ -n "$LIMIT" ]; then
@@ -416,7 +437,7 @@ jobs:
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "osaka"
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "osaka"
- name: Parse hive output
run: |
@@ -439,7 +460,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
env:
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"

View File

@@ -20,11 +20,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: test / ${{ matrix.network }}
if: github.event_name != 'schedule'
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
strategy:
@@ -32,14 +36,18 @@ jobs:
network: ["ethereum"]
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- name: Install Geth
run: .github/scripts/install_geth.sh
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Run tests
@@ -58,7 +66,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
with:
jobs: ${{ toJSON(needs) }}
@@ -66,13 +74,19 @@ jobs:
name: era1 file integration tests once a day
if: github.event_name == 'schedule' && github.repository == 'paradigmxyz/reth'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: run era1 files integration tests

View File

@@ -18,9 +18,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
build-reth:
if: github.repository == 'paradigmxyz/reth'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/docker-test.yml
with:
hive_target: kurtosis
@@ -32,15 +37,18 @@ jobs:
fail-fast: false
name: run kurtosis
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
needs:
- build-reth
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Download reth image
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: artifacts
path: /tmp
@@ -52,7 +60,7 @@ jobs:
docker image ls -a
- name: Run kurtosis
uses: ethpandaops/kurtosis-assertoor-github-action@v1
uses: ethpandaops/kurtosis-assertoor-github-action@f64942cbc780df731a731ea9f45765b161d2c8df # v1.0.1
with:
ethereum_package_args: ".github/assets/kurtosis_network_params.yaml"
@@ -62,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
env:
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"

View File

@@ -4,19 +4,23 @@ on:
pull_request:
types: [opened]
permissions: {}
jobs:
label_prs:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Label PRs
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const label_pr = require('./.github/scripts/label_pr.js')

View File

@@ -8,11 +8,17 @@ on:
paths:
- '.github/**'
permissions: {}
jobs:
actionlint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)

View File

@@ -10,10 +10,14 @@ env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
permissions: {}
jobs:
clippy-binaries:
name: clippy binaries / ${{ matrix.type }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
strategy:
matrix:
@@ -22,17 +26,19 @@ jobs:
args: --workspace --lib --examples --tests --benches --locked
features: "ethereum asm-keccak jemalloc jemalloc-prof min-error-logs min-warn-logs min-info-logs min-debug-logs min-trace-logs"
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@clippy
with:
components: clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- if: "${{ matrix.type == 'book' }}"
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run clippy on binaries
@@ -43,15 +49,19 @@ jobs:
clippy:
name: clippy
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@nightly
with:
components: clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
@@ -60,19 +70,25 @@ jobs:
wasm:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-wasip1
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- uses: dcarbone/install-jq-action@v3
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- name: Run Wasm checks
run: |
sudo apt update && sudo apt install gcc-multilib
@@ -80,37 +96,49 @@ jobs:
riscv:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
with:
target: riscv32imac-unknown-none-elf
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- uses: dcarbone/install-jq-action@v3
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
- name: Run RISC-V checks
run: .github/scripts/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' }}
permissions:
contents: read
strategy:
matrix:
partition: [1, 2, 3]
total_partitions: [3]
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-hack
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo hack check --workspace --partition ${{ matrix.partition }}/${{ matrix.total_partitions }}
@@ -118,15 +146,19 @@ jobs:
msrv:
name: MSRV
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.93" # MSRV
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo build --bin reth --workspace
@@ -136,13 +168,17 @@ jobs:
docs:
name: docs
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo docs --document-private-items
@@ -154,42 +190,56 @@ jobs:
fmt:
name: fmt
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- name: Run fmt
run: cargo fmt --all --check
udeps:
name: udeps
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- uses: taiki-e/install-action@cargo-udeps
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: cargo-udeps
- run: cargo udeps --workspace --lib --examples --tests --benches --all-features --locked
book:
name: book
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo build --bin reth --workspace
@@ -201,38 +251,54 @@ jobs:
typos:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0
check-toml:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run dprint
uses: dprint/check@v2.3
uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3
with:
config-path: dprint.json
grafana:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Check dashboard JSON with jq
uses: sergeysova/jq-action@v2
uses: sergeysova/jq-action@a3f0d4ff59cc1dddf023fc0b325dd75b10deec58 # v2.3.0
with:
cmd: jq empty etc/grafana/dashboards/overview.json
no-test-deps:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- 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
@@ -240,13 +306,17 @@ jobs:
# Check crates correctly propagate features
feature-propagation:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- 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: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: taiki-e/cache-cargo-install-action@a8b9ecf8e0c0ea09d7481cfc583a5203ecd585b5 # v3.0.5
with:
tool: zepter
- name: Eagerly pull dependencies
@@ -254,6 +324,8 @@ jobs:
- run: zepter run check
deny:
permissions:
contents: read
uses: tempoxyz/ci/.github/workflows/deny.yml@main
lint-success:
@@ -277,6 +349,6 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -4,27 +4,36 @@ on:
pull_request:
types: [labeled]
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
if: github.event.label.name == 'cyclops'
permissions: {}
steps:
- name: Publish event
env:
EVENTS_KEY: ${{ secrets.EVENTS_KEY }}
EVENTS_CERT: ${{ secrets.EVENTS_CERT }}
EVENTS_ARGS: ${{ secrets.EVENTS_ARGS }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
echo "${{ secrets.EVENTS_KEY }}" > ${{ runner.temp }}/key
echo "${{ secrets.EVENTS_CERT }}" > ${{ runner.temp }}/cert
echo "$EVENTS_KEY" > "${{ runner.temp }}/key"
echo "$EVENTS_CERT" > "${{ runner.temp }}/cert"
curl -sf -o /dev/null -X POST ${{ secrets.EVENTS_ARGS }} \
curl -sf -o /dev/null -X POST $EVENTS_ARGS \
-H "Content-Type: application/json" \
--key ${{ runner.temp }}/key \
--cert ${{ runner.temp }}/cert \
--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 }}"
"pr_number": '"$PR_NUMBER"',
"sha": "'"$PR_SHA"'"
}
}'

View File

@@ -8,20 +8,19 @@ on:
- edited
- synchronize
permissions:
pull-requests: read
contents: read
permissions: {}
jobs:
conventional-title:
name: Validate PR title is Conventional Commit
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Check title
id: lint_pr_title
uses: amannn/action-semantic-pull-request@v6
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -40,7 +39,7 @@ jobs:
continue-on-error: true
- name: Add PR Comment for Invalid Title
if: steps.lint_pr_title.outcome == 'failure'
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
with:
header: pr-title-lint-error
message: |
@@ -76,7 +75,7 @@ jobs:
- name: Remove Comment for Valid Title
if: steps.lint_pr_title.outcome == 'success'
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
with:
header: pr-title-lint-error
delete: true

View File

@@ -7,12 +7,15 @@ on:
release:
types: [published]
permissions: {}
jobs:
release-homebrew:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v7
uses: dawidd6/action-homebrew-bump-formula@1446dca236b0440c6f02723a3f14f13be2c04ab0 # v7
with:
token: ${{ secrets.HOMEBREW }}
no_fork: true

View File

@@ -2,6 +2,8 @@
name: release-reproducible
permissions: {}
on:
workflow_run:
workflows: [release]
@@ -15,20 +17,23 @@ jobs:
name: extract version
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Extract version from triggering tag
id: extract_version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# Get the tag that points to the head SHA of the triggering workflow
TAG=$(gh api /repos/${{ github.repository }}/git/refs/tags \
--jq '.[] | select(.object.sha == "${{ github.event.workflow_run.head_sha }}") | .ref' \
--jq ".[] | select(.object.sha == \"${HEAD_SHA}\") | .ref" \
| head -1 \
| sed 's|refs/tags/||')
if [ -z "$TAG" ]; then
echo "No tag found for SHA ${{ github.event.workflow_run.head_sha }}"
echo "No tag found for SHA ${HEAD_SHA}"
exit 1
fi
@@ -44,15 +49,16 @@ jobs:
packages: write
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ needs.extract-version.outputs.VERSION }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -65,7 +71,7 @@ jobs:
echo "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" >> $GITHUB_OUTPUT
- name: Build reproducible artifacts
uses: docker/build-push-action@v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
id: docker_build
with:
context: .
@@ -75,13 +81,11 @@ jobs:
VERSION=${{ needs.extract-version.outputs.VERSION }}
target: artifacts
outputs: type=local,dest=./docker-artifacts
cache-from: type=gha
cache-to: type=gha,mode=max
env:
DOCKER_BUILD_RECORD_UPLOAD: false
- name: Build and push final image
uses: docker/build-push-action@v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: ./Dockerfile.reproducible
@@ -92,8 +96,6 @@ jobs:
tags: |
${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${{ needs.extract-version.outputs.VERSION }}
${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
env:
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -3,6 +3,8 @@
name: release
permissions: {}
on:
push:
tags:
@@ -20,21 +22,24 @@ env:
REPRODUCIBLE_IMAGE_NAME: ${{ github.repository_owner }}/reth-reproducible
CARGO_TERM_COLOR: always
DOCKER_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/reth
RUSTC_WRAPPER: "sccache"
jobs:
dry-run:
name: check dry run
runs-on: ubuntu-latest
permissions: {}
steps:
- run: |
echo "Dry run value: ${{ github.event.inputs.dry_run }}"
echo "Dry run enabled: ${{ github.event.inputs.dry_run == 'true'}}"
echo "Dry run disabled: ${{ github.event.inputs.dry_run != 'true'}}"
- env:
DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
echo "Dry run value: ${DRY_RUN}"
echo "Dry run enabled: $( [ "${DRY_RUN}" = 'true' ] && echo true || echo false )"
echo "Dry run disabled: $( [ "${DRY_RUN}" != 'true' ] && echo true || echo false )"
extract-version:
name: extract version
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Extract version
run: echo "VERSION=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT
@@ -45,12 +50,15 @@ jobs:
check-version:
name: check version
runs-on: ubuntu-latest
permissions:
contents: read
needs: extract-version
if: ${{ github.event.inputs.dry_run != 'true' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Verify crate version matches tag
# Check that the Cargo version starts with the tag,
# so that Cargo version 1.4.8 can be matched against both v1.4.8 and v1.4.8-rc.1
@@ -63,6 +71,8 @@ jobs:
build:
name: build release
runs-on: ${{ matrix.configs.os }}
permissions:
contents: read
needs: extract-version
continue-on-error: ${{ matrix.configs.allow_fail }}
strategy:
@@ -95,20 +105,20 @@ jobs:
- command: build
binary: reth
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
with:
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
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
cargo install cross --locked \
--git https://github.com/cross-rs/cross \
--rev 65fe72b0cdb1e7e0cc0652517498d4389cc8f5cf
- name: Apple M1 setup
if: matrix.configs.target == 'aarch64-apple-darwin'
@@ -145,14 +155,14 @@ jobs:
- name: Upload artifact
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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
@@ -171,11 +181,12 @@ jobs:
steps:
# This is necessary for generating the changelog.
# It has to come before "Download Artifacts" or else it deletes the artifacts.
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Generate full changelog
id: changelog
run: |
@@ -261,6 +272,7 @@ jobs:
dry-run-summary:
name: dry run summary
runs-on: ubuntu-latest
permissions: {}
needs: [build, extract-version]
if: ${{ github.event.inputs.dry_run == 'true' }}
env:

View File

@@ -5,11 +5,15 @@ on:
schedule:
- cron: "0 1 */2 * *"
permissions: {}
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
name: build reproducible binaries
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
matrix:
include:
@@ -18,14 +22,16 @@ jobs:
- runner: ubuntu-22.04
machine: machine-2
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
target: x86_64-unknown-linux-gnu
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build reproducible binary with Docker
run: |
@@ -43,7 +49,7 @@ jobs:
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
- name: Upload the hash
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: checksum-${{ matrix.machine }}
path: |
@@ -56,12 +62,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download artifacts from machine-1
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: checksum-machine-1
path: machine-1/
- name: Download artifacts from machine-2
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: checksum-machine-2
path: machine-2/

View File

@@ -18,22 +18,28 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
stage:
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' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Build reth

View File

@@ -7,6 +7,8 @@ on:
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
close-issues:
if: github.repository == 'paradigmxyz/reth'
@@ -15,7 +17,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
days-before-stale: 21
days-before-close: 7

View File

@@ -15,11 +15,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
@@ -34,11 +38,13 @@ jobs:
block: 100000
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Build ${{ matrix.chain.bin }}

View File

@@ -15,11 +15,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
@@ -34,11 +38,13 @@ jobs:
block: 100000
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Build ${{ matrix.chain.bin }}

View File

@@ -17,10 +17,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: test / ${{ matrix.type }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
strategy:
@@ -32,16 +36,20 @@ jobs:
exclude_args: ""
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- if: "${{ matrix.type == 'book' }}"
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run tests
@@ -56,20 +64,25 @@ jobs:
state:
name: Ethereum state tests
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_LOG: info,sync=error
RUST_BACKTRACE: 1
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Checkout ethereum/tests
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ethereum/tests
ref: 81862e4848585a438d64f911a19b3825f0f4cd95
path: testing/ef-tests/ethereum-tests
submodules: recursive
fetch-depth: 1
persist-credentials: false
- name: Download & extract EEST fixtures (public)
shell: bash
env:
@@ -79,11 +92,13 @@ jobs:
mkdir -p testing/ef-tests/execution-spec-tests
URL="https://github.com/ethereum/execution-spec-tests/releases/download/${EEST_TESTS_TAG}/fixtures_stable.tar.gz"
curl -L "$URL" | tar -xz --strip-components=1 -C testing/ef-tests/execution-spec-tests
- uses: rui314/setup-mold@v1
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
with:
tool: nextest
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- run: cargo nextest run --no-fail-fast --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
@@ -91,15 +106,19 @@ jobs:
doc:
name: doc tests
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
permissions:
contents: read
env:
RUST_BACKTRACE: 1
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
- name: Run doctests
@@ -113,6 +132,6 @@ jobs:
timeout-minutes: 30
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
with:
jobs: ${{ toJSON(needs) }}

766
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace.package]
version = "2.1.0"
version = "2.2.0"
edition = "2024"
rust-version = "1.93"
license = "MIT OR Apache-2.0"
@@ -29,7 +29,6 @@ members = [
"crates/engine/primitives/",
"crates/engine/execution-cache/",
"crates/engine/tree/",
"crates/engine/snap/",
"crates/engine/util/",
"crates/era",
"crates/era-downloader",
@@ -346,7 +345,6 @@ 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-snap = { path = "crates/engine/snap" }
reth-engine-tree = { path = "crates/engine/tree" }
reth-engine-util = { path = "crates/engine/util" }
reth-era = { path = "crates/era" }
@@ -435,14 +433,14 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-zstd-compressors = { version = "0.3.1", default-features = false }
# revm
revm = { version = "=38.0.0", default-features = false }
revm-bytecode = { version = "=10.0.0", default-features = false }
revm-database = { version = "=13.0.1", default-features = false }
revm-state = { version = "=11.0.1", default-features = false }
revm-primitives = { version = "=23.0.0", default-features = false }
revm-interpreter = { version = "=35.0.1", default-features = false }
revm-database-interface = { version = "=11.0.1", default-features = false }
revm-inspectors = "=0.39.0"
revm = { version = "38.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"
# eth
alloy-dyn-abi = "1.5.6"
@@ -458,33 +456,33 @@ alloy-trie = { version = "0.9.4", default-features = false }
alloy-hardforks = "0.4.7"
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 = "2.0.4", default-features = false }
alloy-contract = { version = "2.0.4", default-features = false }
alloy-eips = { version = "2.0.4", default-features = false }
alloy-genesis = { version = "2.0.4", default-features = false }
alloy-json-rpc = { version = "2.0.4", default-features = false }
alloy-network = { version = "2.0.4", default-features = false }
alloy-network-primitives = { version = "2.0.4", default-features = false }
alloy-provider = { version = "2.0.4", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "2.0.4", default-features = false }
alloy-rpc-client = { version = "2.0.4", default-features = false }
alloy-rpc-types = { version = "2.0.4", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "2.0.4", default-features = false }
alloy-rpc-types-anvil = { version = "2.0.4", default-features = false }
alloy-rpc-types-beacon = { version = "2.0.4", default-features = false }
alloy-rpc-types-debug = { version = "2.0.4", default-features = false }
alloy-rpc-types-engine = { version = "2.0.4", default-features = false }
alloy-rpc-types-eth = { version = "2.0.4", default-features = false }
alloy-rpc-types-mev = { version = "2.0.4", default-features = false }
alloy-rpc-types-trace = { version = "2.0.4", default-features = false }
alloy-rpc-types-txpool = { version = "2.0.4", default-features = false }
alloy-serde = { version = "2.0.4", default-features = false }
alloy-signer = { version = "2.0.4", default-features = false }
alloy-signer-local = { version = "2.0.4", default-features = false }
alloy-transport = { version = "2.0.4" }
alloy-transport-http = { version = "2.0.4", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "2.0.4", default-features = false }
alloy-transport-ws = { version = "2.0.4", default-features = false }
# misc
either = { version = "1.15.0", default-features = false }
@@ -583,7 +581,7 @@ tower = "0.5"
tower-http = "0.6"
# p2p
discv5 = "0.10"
discv5 = { git = "https://github.com/sigp/discv5", rev = "7663c00" }
if-addrs = "0.14"
# rpc
@@ -702,24 +700,3 @@ vergen-git2 = "9.1.0"
# networking
ipnet = "2.11"
[patch.crates-io]
revm = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "5eebb56819ee6bec5bfbc69a415276ee1a784fec" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet-4" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }

View File

@@ -50,7 +50,7 @@ RUN if [ -n "$RUSTFLAGS" ]; then \
RUN cp /app/target/$BUILD_PROFILE/reth /app/reth
# Use Ubuntu as the release image
FROM ubuntu AS runtime
FROM ubuntu:24.04 AS runtime
WORKDIR /app
# Copy reth over from the build stage

View File

@@ -7,6 +7,7 @@
//! `execute_transaction` to apply segment-boundary changes.
use crate::evm_config::BigBlockSegment;
use alloy_consensus::TransactionEnvelope;
use alloy_eips::eip7685::Requests;
use alloy_evm::{
block::{
@@ -15,7 +16,7 @@ use alloy_evm::{
},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
precompiles::PrecompilesMap,
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
Database, EthEvm, EthEvmFactory, Evm, EvmFactory, FromRecoveredTx, FromTxWithEncoded,
};
use alloy_primitives::B256;
use reth_ethereum_primitives::{Receipt, TransactionSigned};
@@ -116,6 +117,7 @@ pub(crate) type BalIndexReader<DB> = fn(&DB) -> u64;
/// 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()`.
#[expect(missing_debug_implementations)]
pub struct BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: Database,
@@ -145,21 +147,6 @@ where
initialized: bool,
}
impl<DB, I, P, Spec> std::fmt::Debug for BbBlockExecutor<'_, DB, I, P, Spec>
where
DB: Database,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BbBlockExecutor")
.field("has_inner", &self.inner.is_some())
.field("plan", &self.plan)
.field("gas_used_offset", &self.gas_used_offset)
.field("blob_gas_used_offset", &self.blob_gas_used_offset)
.field("initialized", &self.initialized)
.finish_non_exhaustive()
}
}
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: StateDB,
@@ -447,6 +434,9 @@ where
}
fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
self.maybe_apply_boundary()
.expect("segment boundary application must succeed before committing transaction");
let gas_used = self.inner_mut().commit_transaction(output);
// Fix up cumulative_gas_used on the just-committed receipt so that
@@ -624,7 +614,10 @@ where
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
type Transaction = TransactionSigned;
type Receipt = Receipt;
type TxExecutionResult = EthTxResult<HaltReason, alloy_consensus::TxType>;
type TxExecutionResult = EthTxResult<
<EthEvmFactory as EvmFactory>::HaltReason,
<TransactionSigned as TransactionEnvelope>::TxType,
>;
type Executor<'a, DB: StateDB, I: Inspector<EthEvmContext<DB>>> =
BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>;

View File

@@ -12,7 +12,10 @@ use crate::{
BigBlockMap,
};
use alloy_consensus::Header;
use alloy_evm::eth::EthBlockExecutionCtx;
use alloy_evm::{
eth::{spec::EthExecutorSpec, EthBlockExecutionCtx},
EthEvmFactory,
};
use alloy_primitives::B256;
use alloy_rpc_types::engine::ExecutionData;
use core::convert::Infallible;
@@ -20,8 +23,8 @@ use reth_chainspec::{ChainSpec, EthChainSpec};
use reth_ethereum_forks::Hardforks;
use reth_ethereum_primitives::EthPrimitives;
use reth_evm::{
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
NextBlockEnvAttributes,
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, EvmEnvFor, ExecutableTxIterator,
ExecutionCtxFor, NextBlockEnvAttributes,
};
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
use reth_primitives_traits::{SealedBlock, SealedHeader};
@@ -29,9 +32,6 @@ 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
// ---------------------------------------------------------------------------

View File

@@ -21,6 +21,7 @@ use alloy_rpc_types_engine::{
};
use clap::Parser;
use eyre::Context;
use futures::{stream, StreamExt};
use reth_chainspec::EthChainSpec;
use reth_cli::chainspec::ChainSpecParser;
use reth_cli_runner::CliContext;
@@ -270,6 +271,15 @@ pub struct Command {
/// the flattened BAL on the stored payload.
#[arg(long, default_value_t = false)]
bal: bool,
/// Maximum number of in-flight RPC fetches to keep buffered ahead of the merger.
///
/// Each entry is one full per-block fetch (block + receipts, plus BAL when `--bal` is
/// set). Larger values absorb RPC latency at the cost of more concurrent connections
/// and memory; the buffer persists across `--num-big-blocks` so prefetching continues
/// across big-block boundaries.
#[arg(long, value_name = "PREFETCH_BUFFER", default_value_t = 32)]
prefetch_buffer: usize,
}
impl Command {
@@ -322,13 +332,27 @@ impl Command {
}
let mut prev_big_block_header: Option<PrevBigBlockHeader> = None;
// Track the next block to fetch across big blocks so they don't overlap.
// Persistent prefetch stream: keeps `prefetch_buffer` per-block fetches in flight
// ahead of the merger across all big blocks. Each item is a fully materialized
// `FetchedBlock` (or `None` once the chain tip is reached on this fetch).
let prefetch_buffer = self.prefetch_buffer.max(1);
let bal_enabled = self.bal;
let block_stream = stream::iter(self.from_block..)
.map(|block_number| {
let provider = provider.clone();
async move { fetch_one_block(provider, block_number, bal_enabled).await }
})
.buffered(prefetch_buffer);
let mut block_stream = Box::pin(block_stream);
// Track the next block number we expect from the stream (purely for logging /
// big-block range bookkeeping; the stream produces blocks in `from_block..` order).
let mut next_block = self.from_block;
for big_block_idx in 0..self.num_big_blocks {
let range_start = next_block;
// Fetch consecutive blocks until the gas target is reached.
// Drain the prefetch stream until the gas target is reached for this big block.
let mut blocks = Vec::new();
let mut block_receipts: Vec<Vec<Receipt>> = Vec::new();
let mut block_access_lists: Vec<Option<BlockAccessList>> = Vec::new();
@@ -337,16 +361,11 @@ impl Command {
let mut reached_chain_tip = false;
while accumulated_block_gas < self.target_gas {
let block_number = next_block;
info!(target: "reth-bench", block_number, big_block = big_block_idx, "Fetching block");
info!(target: "reth-bench", block_number, big_block = big_block_idx, "Awaiting prefetched block");
let fetch_result = tokio::try_join!(
provider.get_block_by_number(block_number.into()).full(),
provider.get_block_receipts(block_number.into()),
);
let (rpc_block, receipts) = match fetch_result {
Ok((Some(block), Some(receipts))) => (block, receipts),
Ok((None, _) | (_, None)) => {
let fetched = match block_stream.next().await {
Some(Ok(Some(fetched))) => fetched,
Some(Ok(None)) => {
warn!(
target: "reth-bench",
block_number,
@@ -355,52 +374,16 @@ impl Command {
reached_chain_tip = true;
break;
}
Err(e) => return Err(e.into()),
Some(Err(e)) => return Err(e),
// The block-number stream is open-ended; this only fires if the
// upstream `iter(from..)` is somehow exhausted.
None => {
reached_chain_tip = true;
break;
}
};
let block_access_list = if self.bal {
Some(fetch_block_access_list(&provider, block_number).await.wrap_err_with(
|| format!("Failed to fetch BAL for block {block_number}"),
)?)
} else {
None
};
// Convert RPC receipts to consensus receipts
let consensus_receipts: Vec<Receipt> = receipts
.iter()
.map(|r| {
let inner = &r.inner.inner.inner;
let tx_type = r.inner.inner.r#type.try_into().unwrap_or_default();
Receipt {
tx_type,
success: inner.receipt.status.coerce_status(),
cumulative_gas_used: inner.receipt.cumulative_gas_used,
logs: inner
.receipt
.logs
.iter()
.map(|log| alloy_primitives::Log {
address: log.inner.address,
data: log.inner.data.clone(),
})
.collect(),
}
})
.collect();
// Convert to consensus block
let block = rpc_block
.into_inner()
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
.try_map_transactions(|tx| -> eyre::Result<TxEnvelope> {
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
})?
.into_consensus();
// Convert to ExecutionData
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
let execution_data = ExecutionData { payload, sidecar };
let FetchedBlock { execution_data, consensus_receipts, block_access_list } =
fetched;
let block_gas = execution_data.payload.as_v1().gas_used;
let block_blob_gas =
@@ -674,6 +657,79 @@ impl Command {
}
}
/// One fully-materialized block fetched by the prefetcher.
struct FetchedBlock {
/// Execution payload with sidecar derived from the RPC block.
execution_data: ExecutionData,
/// Consensus-format receipts (`cumulative_gas_used` is still per-block, callers offset
/// it when merging).
consensus_receipts: Vec<Receipt>,
/// `eth_getBlockAccessListByBlockNumber` result when `--bal` is enabled.
block_access_list: Option<BlockAccessList>,
}
/// Fetches one block + receipts (and optionally its BAL) from the RPC. Returns `Ok(None)`
/// when the block doesn't exist yet (chain-tip reached).
async fn fetch_one_block(
provider: RootProvider<AnyNetwork>,
block_number: u64,
bal_enabled: bool,
) -> eyre::Result<Option<FetchedBlock>> {
let (rpc_block, receipts) = tokio::try_join!(
provider.get_block_by_number(block_number.into()).full(),
provider.get_block_receipts(block_number.into()),
)?;
let (rpc_block, receipts) = match (rpc_block, receipts) {
(Some(b), Some(r)) => (b, r),
_ => return Ok(None),
};
let block_access_list = if bal_enabled {
Some(
fetch_block_access_list(&provider, block_number)
.await
.wrap_err_with(|| format!("Failed to fetch BAL for block {block_number}"))?,
)
} else {
None
};
let consensus_receipts: Vec<Receipt> = receipts
.iter()
.map(|r| {
let inner = &r.inner.inner.inner;
let tx_type = r.inner.inner.r#type.try_into().unwrap_or_default();
Receipt {
tx_type,
success: inner.receipt.status.coerce_status(),
cumulative_gas_used: inner.receipt.cumulative_gas_used,
logs: inner
.receipt
.logs
.iter()
.map(|log| alloy_primitives::Log {
address: log.inner.address,
data: log.inner.data.clone(),
})
.collect(),
}
})
.collect();
let block = rpc_block
.into_inner()
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
.try_map_transactions(|tx| -> eyre::Result<TxEnvelope> {
tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
})?
.into_consensus();
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
let execution_data = ExecutionData { payload, sidecar };
Ok(Some(FetchedBlock { execution_data, consensus_receipts, block_access_list }))
}
fn merge_block_access_list(
merged: &mut BlockAccessList,
incoming: BlockAccessList,

View File

@@ -20,7 +20,10 @@ use reth_provider::{
};
use reth_revm::{
database::StateProviderDatabase,
db::{states::reverts::AccountInfoRevert, BundleState},
db::{
states::reverts::{AccountInfoRevert, RevertToSlot},
BundleState,
},
};
use reth_stages::stages::calculate_gas_used_from_headers;
use reth_storage_api::{ChangeSetReader, DBProvider, StorageChangeSetReader};
@@ -191,10 +194,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
}
};
let bal= executor.take_bal();
if let Err(err) = consensus
.validate_block_post_execution(&block, &result, None,bal)
.validate_block_post_execution(&block, &result, None)
.wrap_err_with(|| {
format!(
"Failed to validate block {} {}",
@@ -427,14 +428,19 @@ where
let mut cs_slots = cs_storage.get_mut(addr);
for (slot_key, revert_slot) in &revert.storage {
let b256_key = B256::from(*slot_key);
match cs_slots.as_mut().and_then(|s| s.remove(&b256_key)) {
Some(cs_value) => eyre::ensure!(
revert_slot.to_previous_value() == cs_value,
let cs_value = cs_slots.as_mut().and_then(|s| s.remove(&b256_key));
match (revert_slot, cs_value) {
// When a contract is selfdestructed and re-created at the same address
// within the same block, revm marks slots touched by the new contract
// as `Destroyed` and never reads the original DB value, so
// `to_previous_value()` would resolve to zero, which might be wrong.
(RevertToSlot::Destroyed, _) => {}
(RevertToSlot::Some(prev), Some(cs_value)) => eyre::ensure!(
*prev == cs_value,
"Block {block_number}: {addr} slot {b256_key} mismatch: \
revert={} cs={cs_value}",
revert_slot.to_previous_value(),
revert={prev} cs={cs_value}",
),
None => eyre::ensure!(
(RevertToSlot::Some(_), None) => eyre::ensure!(
revert.wipe_storage,
"Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
),

View File

@@ -18,7 +18,6 @@ reth-primitives-traits.workspace = true
# ethereum
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-eip7928.workspace = true
# misc
auto_impl.workspace = true
@@ -30,9 +29,10 @@ std = [
"reth-primitives-traits/std",
"alloy-primitives/std",
"alloy-consensus/std",
"alloy-eip7928/std",
"reth-primitives-traits/std",
"reth-execution-types/std",
"thiserror/std",
"alloy-eip7928/std",
]
test-utils = ["reth-primitives-traits/test-utils"]
test-utils = [
"reth-primitives-traits/test-utils",
]

View File

@@ -38,7 +38,6 @@ use alloc::{
vec::Vec,
};
use alloy_consensus::Header;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
use core::{error::Error, fmt::Display};
@@ -86,7 +85,6 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError>;
}
@@ -476,12 +474,6 @@ pub enum ConsensusError {
/// EIP-7825: Transaction gas limit exceeds maximum allowed
#[error(transparent)]
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
/// Error when an unexpected block access list cost is encountered.
#[error("block access list exceeds gas limit")]
BlockAccessListExceedsGasLimit,
/// Error when the block access list hash doesn't match the expected value.
#[error("block access list hash mismatch: {0}")]
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
/// Any additional consensus error, for example L2-specific errors.
#[error(transparent)]
Other(#[from] Arc<dyn Error + Send + Sync>),
@@ -527,23 +519,6 @@ impl ConsensusError {
}
}
/// Validates the block access list against the gas limit.
///
/// EIP-7925 specifies that the total cost of the block access list items must not exceed
/// the gas limit. Each item costs `ITEM_COST` gas.
pub fn validate_block_access_list_gas(
block_access_list: Option<&alloy_eip7928::BlockAccessList>,
gas_limit: u64,
) -> Result<(), ConsensusError> {
if let Some(bal) = block_access_list {
let bal_items = alloy_eip7928::total_bal_items(bal);
if bal_items > gas_limit / alloy_eip7928::ITEM_COST as u64 {
return Err(ConsensusError::BlockAccessListExceedsGasLimit)
}
}
Ok(())
}
impl From<InvalidTransactionError> for ConsensusError {
fn from(value: InvalidTransactionError) -> Self {
Self::InvalidTransaction(value)

View File

@@ -20,7 +20,6 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloc::sync::Arc;
use alloy_eip7928::BlockAccessList;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -78,7 +77,6 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloy_eip7928::BlockAccessList;
use core::sync::atomic::{AtomicBool, Ordering};
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -53,7 +52,6 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)

View File

@@ -14,12 +14,8 @@ workspace = true
reth-chainspec.workspace = true
reth-tracing.workspace = true
reth-db = { workspace = true, features = ["test-utils"] }
reth-db-api.workspace = true
reth-network-api.workspace = true
reth-network-p2p.workspace = true
reth-storage-api = { workspace = true, features = ["db-api"] }
reth-trie = { workspace = true, features = ["test-utils"] }
reth-trie-db.workspace = true
reth-rpc-server-types.workspace = true
reth-rpc-builder.workspace = true
reth-rpc-eth-api.workspace = true

View File

@@ -4,7 +4,7 @@ use alloy_rpc_types_engine::PayloadAttributes;
use node::NodeTestContext;
use reth_chainspec::ChainSpec;
use reth_db::{test_utils::TempDatabase, DatabaseEnv};
use reth_network_api::{test_utils::PeersHandleProvider, BlockDownloaderProvider};
use reth_network_api::test_utils::PeersHandleProvider;
use reth_node_builder::{
components::NodeComponentsBuilder,
rpc::{EngineValidatorAddOn, RethRpcAddOns},
@@ -34,8 +34,6 @@ pub mod setup_import;
/// Helper for network operations
mod network;
/// Snap sync utilities for E2E tests.
/// Helper for rpc operations
mod rpc;
@@ -155,11 +153,7 @@ where
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider
+ BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
Network: PeersHandleProvider,
>,
>,
AddOns: RethRpcAddOns<
@@ -181,11 +175,7 @@ impl<T> NodeBuilderHelper for T where
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider
+ BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
Network: PeersHandleProvider,
>,
>,
AddOns: RethRpcAddOns<

View File

@@ -8,7 +8,6 @@ use eyre::Ok;
use futures_util::Future;
use jsonrpsee::{core::client::ClientT, http_client::HttpClient};
use reth_chainspec::EthereumHardforks;
use reth_db_api::transaction::DbTx;
use reth_network_api::test_utils::PeersHandleProvider;
use reth_node_api::{Block, BlockBody, BlockTy, FullNodeComponents, PayloadTypes, PrimitivesTy};
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
@@ -16,13 +15,12 @@ use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
use reth_payload_primitives::BuiltPayload;
use reth_provider::{
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
DatabaseProviderFactory, HeaderProvider, StageCheckpointReader,
HeaderProvider, StageCheckpointReader,
};
use reth_rpc_api::TestingBuildBlockRequestV1;
use reth_rpc_builder::auth::AuthServerHandle;
use reth_rpc_eth_api::helpers::{EthApiSpec, EthTransactions, TraceExt};
use reth_stages_types::StageId;
use reth_storage_api::DBProvider;
use std::pin::Pin;
use tokio_stream::StreamExt;
use url::Url;
@@ -314,7 +312,7 @@ where
self.inner
.add_ons_handle
.beacon_engine_handle
.new_payload(Payload::built_payload_to_execution_data(&payload))
.new_payload(Payload::block_to_payload(payload.block().clone()))
.await?;
Ok(block_hash)
@@ -367,26 +365,4 @@ where
client.request("testing_buildBlockV1", [request]).await?;
eyre::Ok(res)
}
/// Computes the current state root from the persisted `HashedAccounts` /
/// `HashedStorages` tables in MDBX. This reflects the latest block whose
/// hashed state has been committed to disk by the engine persistence layer.
///
/// Uses [`NoopTrieCursorFactory`] so the root is computed purely from the
/// hashed leaf data, without depending on the incremental trie tables.
pub async fn snap_state_root(&self) -> B256
where
Node::Provider: DatabaseProviderFactory,
<Node::Provider as DatabaseProviderFactory>::Provider: DBProvider,
<<Node::Provider as DatabaseProviderFactory>::Provider as DBProvider>::Tx: DbTx,
{
use reth_trie::{trie_cursor::noop::NoopTrieCursorFactory, StateRoot};
use reth_trie_db::DatabaseHashedCursorFactory;
let provider = self.inner.provider.database_provider_ro().expect("open ro provider");
let tx = provider.tx_ref();
StateRoot::new(NoopTrieCursorFactory::default(), DatabaseHashedCursorFactory::new(tx))
.root()
.unwrap_or(B256::ZERO)
}
}

View File

@@ -1,8 +1,8 @@
use futures_util::StreamExt;
use reth_node_api::{BlockBody, PayloadAttributes, PayloadKind};
use reth_node_api::{PayloadAttributes, PayloadKind};
use reth_payload_builder::{PayloadBuilderHandle, PayloadId};
use reth_payload_builder_primitives::Events;
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_payload_primitives::PayloadTypes;
use tokio_stream::wrappers::BroadcastStream;
/// Helper for payload operations
@@ -53,27 +53,11 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
///
/// Panics if the payload builder does not produce a non-empty payload within 30 seconds.
pub async fn wait_for_built_payload(&self, payload_id: PayloadId) {
let start = std::time::Instant::now();
loop {
let payload =
self.payload_builder.best_payload(payload_id).await.transpose().ok().flatten();
if payload.is_none_or(|p| p.block().body().transactions().is_empty()) {
assert!(
start.elapsed() < std::time::Duration::from_secs(30),
"timed out waiting for a non-empty payload for {payload_id} — \
check that the chain spec supports all generated tx types"
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
continue
}
// Resolve payload once its built
self.payload_builder
.resolve_kind(payload_id, PayloadKind::Earliest)
.await
.unwrap()
.unwrap();
break;
}
self.payload_builder
.resolve_kind(payload_id, PayloadKind::WaitForPending)
.await
.unwrap()
.unwrap();
}
/// Expects the next event to be a built payload event or panics

View File

@@ -191,9 +191,9 @@ pub struct TreeConfig {
/// When disabled, the BAL hashed post state is not sent to the multiproof task for
/// early parallel state root computation.
disable_bal_parallel_state_root: bool,
/// Whether to disable BAL (Block Access List) batched IO during prewarming.
/// When disabled, falls back to individual per-slot storage reads instead of
/// batched cursor reads via `storage_range`.
/// Whether to disable BAL (Block Access List) storage prefetch IO during prewarming.
/// When set, BAL storage slots are not read into the execution cache. BAL hashed-state
/// streaming for parallel state-root computation is controlled separately.
disable_bal_batch_io: bool,
/// Maximum random jitter applied before each proof computation (trie-debug only).
/// When set, each proof worker sleeps for a random duration up to this value

View File

@@ -21,7 +21,8 @@ impl ForkchoiceStateTracker {
/// `sync_target` to `None`, since we're now fully synced.
pub const fn set_latest(&mut self, state: ForkchoiceState, status: ForkchoiceStatus) {
if status.is_valid() {
self.set_valid(state);
self.last_syncing = None;
self.last_valid = Some(state);
} else if status.is_syncing() {
self.last_syncing = Some(state);
}
@@ -30,11 +31,24 @@ impl ForkchoiceStateTracker {
self.latest = Some(received);
}
const fn set_valid(&mut self, state: ForkchoiceState) {
// we no longer need to sync to this state.
/// Promotes a previously tracked syncing forkchoice state to valid, without overwriting a
/// newer `latest` state.
///
/// This is used when a `Syncing` FCU's head finally becomes canonical via the downloaded-block
/// flow, so the safe/finalized anchors of that FCU can be applied. Unlike
/// [`Self::set_latest`], this preserves a newer `latest` (e.g. an `Invalid` FCU received
/// after the syncing one) and only flips `latest` to `Valid` when it still refers to the same
/// syncing FCU being promoted.
pub fn promote_sync_target_to_valid(&mut self, state: ForkchoiceState) {
self.last_syncing = None;
self.last_valid = Some(state);
if let Some(received) = self.latest.as_mut() &&
received.state == state &&
received.status.is_syncing()
{
received.status = ForkchoiceStatus::Valid;
}
}
/// Returns the [`ForkchoiceStatus`] of the latest received FCU.

View File

@@ -1,65 +0,0 @@
[package]
name = "reth-engine-snap"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# reth
reth-db-api.workspace = true
reth-config.workspace = true
reth-consensus.workspace = true
reth-downloaders.workspace = true
reth-eth-wire-types.workspace = true
reth-network-p2p.workspace = true
reth-primitives-traits.workspace = true
reth-provider.workspace = true
reth-stages.workspace = true
reth-stages-api.workspace = true
reth-stages-types.workspace = true
reth-storage-api.workspace = true
reth-tasks = { workspace = true, features = ["rayon"] }
reth-trie.workspace = true
reth-trie-db.workspace = true
# alloy
alloy-consensus.workspace = true
alloy-eip7928 = { workspace = true, features = ["rlp"] }
alloy-eips.workspace = true
alloy-primitives.workspace = true
alloy-rlp.workspace = true
# async
tokio = { workspace = true, features = ["sync", "time"] }
# misc
futures.workspace = true
tracing.workspace = true
thiserror.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }
alloy-trie.workspace = true
[features]
default = []
test-utils = [
"reth-consensus/test-utils",
"reth-db-api/test-utils",
"reth-downloaders/test-utils",
"reth-network-p2p/test-utils",
"reth-primitives-traits/test-utils",
"reth-provider/test-utils",
"reth-stages/test-utils",
"reth-stages-api/test-utils",
"reth-stages-types/test-utils",
"reth-tasks/test-utils",
"reth-trie/test-utils",
"reth-trie-db/test-utils",
]

View File

@@ -1,332 +0,0 @@
//! BAL (Block Access List) diff application for snap sync.
//!
//! Converts raw `Vec<AccountChanges>` from a single block's BAL into
//! partial account diffs, storage writes, and bytecode entries that can
//! be merged with existing hashed state.
use alloy_consensus::constants::KECCAK_EMPTY;
use alloy_eip7928::AccountChanges;
use alloy_primitives::{keccak256, Bytes, B256, U256};
/// A partial diff for a single account extracted from one block's BAL.
///
/// Fields are `Some` only when the BAL contains at least one change for that
/// field. The caller must merge these with existing DB state before writing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BalAccountDiff {
/// `keccak256(address)`.
pub hashed_address: B256,
/// Final balance if `balance_changes` was non-empty.
pub balance: Option<U256>,
/// Final nonce if `nonce_changes` was non-empty.
pub nonce: Option<u64>,
/// Final bytecode hash if `code_changes` was non-empty.
/// Inner `None` means the code was cleared (empty code).
pub bytecode_hash: Option<Option<B256>>,
}
/// Storage entries extracted from one block's BAL.
///
/// Each entry is `(hashed_address, hashed_slot, final_value)`.
pub type BalStorageEntry = (B256, B256, U256);
/// Bytecode entries extracted from one block's BAL.
///
/// Each entry is `(code_hash, code_bytes)`.
pub type BalBytecodeEntry = (B256, Bytes);
/// Parsed state diffs from a single block's BAL.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BalStateDiff {
/// Per-account partial diffs (fields that changed in this block).
pub accounts: Vec<BalAccountDiff>,
/// `(hashed_address, hashed_slot, value)` triples for storage writes.
pub storage: Vec<BalStorageEntry>,
/// `(code_hash, code_bytes)` pairs for bytecode writes.
pub bytecodes: Vec<BalBytecodeEntry>,
}
/// Convert a list of [`AccountChanges`] (from one block's BAL) into partial
/// state diffs.
///
/// For each account, the final post-block value is the entry with the highest
/// transaction index (i.e. the last element, since entries are ordered).
/// Fields with empty change lists are left as `None` in [`BalAccountDiff`],
/// meaning they were not modified in this block.
pub fn bal_to_state_diff(changes: &[AccountChanges]) -> BalStateDiff {
let mut diff = BalStateDiff::default();
for ac in changes {
let hashed_address = keccak256(ac.address);
// Last balance change → final balance.
let balance = ac.balance_changes.last().map(|c| c.post_balance);
// Last nonce change → final nonce.
let nonce = ac.nonce_changes.last().map(|c| c.new_nonce);
// Last code change → final code hash + bytecodes entry.
let bytecode_hash = ac.code_changes.last().map(|c| {
if c.new_code.is_empty() {
None
} else {
let code_hash = keccak256(&c.new_code);
diff.bytecodes.push((code_hash, c.new_code.clone()));
Some(code_hash)
}
});
// Storage: for each slot, take the last change's value.
for slot_changes in &ac.storage_changes {
if let Some(last_change) = slot_changes.changes.last() {
let hashed_slot = keccak256(slot_changes.slot.to_be_bytes::<32>());
diff.storage.push((hashed_address, hashed_slot, last_change.new_value));
}
}
// Only emit an account diff if at least one field changed.
if balance.is_some() || nonce.is_some() || bytecode_hash.is_some() {
diff.accounts.push(BalAccountDiff { hashed_address, balance, nonce, bytecode_hash });
}
}
diff
}
/// Merge a [`BalAccountDiff`] with an existing [`Account`], returning the
/// updated account.
///
/// Fields that are `None` in the diff retain their existing values. If
/// `existing` is `None` (new account), absent fields default to zero / no code.
pub fn merge_account_diff(
diff: &BalAccountDiff,
existing: Option<&reth_primitives_traits::Account>,
) -> reth_primitives_traits::Account {
reth_primitives_traits::Account {
balance: diff.balance.unwrap_or_else(|| existing.map(|a| a.balance).unwrap_or(U256::ZERO)),
nonce: diff.nonce.unwrap_or_else(|| existing.map(|a| a.nonce).unwrap_or(0)),
bytecode_hash: match diff.bytecode_hash {
Some(hash) => {
// Explicit code change: Some(hash) for non-empty, None for cleared.
// Normalize: treat KECCAK_EMPTY as None (no code).
match hash {
Some(h) if h == KECCAK_EMPTY => None,
other => other,
}
}
None => {
// No code change in this block — keep existing.
existing.and_then(|a| a.bytecode_hash)
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange};
use alloy_primitives::Address;
#[test]
fn all_fields_present() {
let addr = Address::from([0xaa; 20]);
let code = Bytes::from(vec![0x60, 0x00, 0x56]);
let code_hash = keccak256(&code);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![
BalanceChange::new(0, U256::from(100)),
BalanceChange::new(1, U256::from(200)),
],
nonce_changes: vec![NonceChange::new(0, 1), NonceChange::new(1, 2)],
code_changes: vec![CodeChange::new(0, code.clone())],
storage_changes: vec![SlotChanges::new(
U256::from(1),
vec![StorageChange::new(0, U256::from(10)), StorageChange::new(1, U256::from(20))],
)],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
let acct = &diff.accounts[0];
assert_eq!(acct.hashed_address, keccak256(addr));
assert_eq!(acct.balance, Some(U256::from(200)));
assert_eq!(acct.nonce, Some(2));
assert_eq!(acct.bytecode_hash, Some(Some(code_hash)));
assert_eq!(diff.storage.len(), 1);
let (ha, hs, val) = &diff.storage[0];
assert_eq!(*ha, keccak256(addr));
assert_eq!(*hs, keccak256(U256::from(1).to_be_bytes::<32>()));
assert_eq!(*val, U256::from(20));
assert_eq!(diff.bytecodes.len(), 1);
assert_eq!(diff.bytecodes[0], (code_hash, code));
}
#[test]
fn partial_fields_only_balance() {
let addr = Address::from([0xbb; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![BalanceChange::new(0, U256::from(500))],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
let acct = &diff.accounts[0];
assert_eq!(acct.balance, Some(U256::from(500)));
assert_eq!(acct.nonce, None);
assert_eq!(acct.bytecode_hash, None);
assert!(diff.storage.is_empty());
assert!(diff.bytecodes.is_empty());
}
#[test]
fn last_entry_wins() {
let addr = Address::from([0xcc; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![
BalanceChange::new(0, U256::from(10)),
BalanceChange::new(1, U256::from(20)),
BalanceChange::new(5, U256::from(99)),
],
nonce_changes: vec![NonceChange::new(0, 1), NonceChange::new(3, 7)],
code_changes: vec![],
storage_changes: vec![SlotChanges::new(
U256::from(42),
vec![
StorageChange::new(0, U256::from(100)),
StorageChange::new(2, U256::from(300)),
StorageChange::new(4, U256::from(999)),
],
)],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts[0].balance, Some(U256::from(99)));
assert_eq!(diff.accounts[0].nonce, Some(7));
assert_eq!(diff.storage[0].2, U256::from(999));
}
#[test]
fn empty_changes_no_account_diff() {
let addr = Address::from([0xdd; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![U256::from(1)],
}];
let diff = bal_to_state_diff(&changes);
// No account, storage, or bytecode diffs — only reads.
assert!(diff.accounts.is_empty());
assert!(diff.storage.is_empty());
assert!(diff.bytecodes.is_empty());
}
#[test]
fn empty_code_clears_bytecode_hash() {
let addr = Address::from([0xee; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![CodeChange::new(0, Bytes::new())],
storage_changes: vec![],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
// Empty code → bytecode_hash = Some(None) (cleared).
assert_eq!(diff.accounts[0].bytecode_hash, Some(None));
assert!(diff.bytecodes.is_empty());
}
#[test]
fn merge_with_existing_account() {
let existing = reth_primitives_traits::Account {
nonce: 5,
balance: U256::from(1000),
bytecode_hash: Some(B256::from([0xab; 32])),
};
let diff = BalAccountDiff {
hashed_address: B256::ZERO,
balance: Some(U256::from(2000)),
nonce: None,
bytecode_hash: None,
};
let merged = merge_account_diff(&diff, Some(&existing));
assert_eq!(merged.balance, U256::from(2000));
assert_eq!(merged.nonce, 5); // kept from existing
assert_eq!(merged.bytecode_hash, Some(B256::from([0xab; 32]))); // kept from existing
}
#[test]
fn merge_new_account_defaults() {
let diff = BalAccountDiff {
hashed_address: B256::ZERO,
balance: Some(U256::from(100)),
nonce: None,
bytecode_hash: None,
};
let merged = merge_account_diff(&diff, None);
assert_eq!(merged.balance, U256::from(100));
assert_eq!(merged.nonce, 0);
assert_eq!(merged.bytecode_hash, None);
}
#[test]
fn multiple_accounts() {
let changes = vec![
AccountChanges {
address: Address::from([0x01; 20]),
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![],
},
AccountChanges {
address: Address::from([0x02; 20]),
balance_changes: vec![BalanceChange::new(0, U256::from(200))],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![SlotChanges::new(
U256::from(5),
vec![StorageChange::new(0, U256::from(50))],
)],
storage_reads: vec![],
},
];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 2);
assert_eq!(diff.accounts[0].hashed_address, keccak256(Address::from([0x01; 20])));
assert_eq!(diff.accounts[1].hashed_address, keccak256(Address::from([0x02; 20])));
assert_eq!(diff.storage.len(), 1);
assert_eq!(diff.storage[0].0, keccak256(Address::from([0x02; 20])));
}
}

View File

@@ -1,314 +0,0 @@
//! Snap sync lifecycle controller.
//!
//! The controller owns Phase A header download and then spawns the engine-driven
//! [`SnapSyncOrchestrator`](crate::orchestrator::SnapSyncOrchestrator) for
//! Phase B state download and BAL catch-up.
use crate::{SnapSyncError, SnapSyncEvent, SnapSyncOutcome};
use alloy_consensus::BlockHeader;
use futures::FutureExt;
use reth_config::config::EtlConfig;
use reth_consensus::noop::NoopConsensus;
use reth_db_api::{
table::Value,
transaction::{DbTx, DbTxMut},
};
use reth_downloaders::headers::reverse_headers::ReverseHeadersDownloaderBuilder;
use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
use reth_primitives_traits::{FullBlockHeader, NodePrimitives};
use reth_provider::{
providers::StaticFileWriter, DatabaseProviderFactory, HeaderProvider,
StaticFileProviderFactory, StorageSettingsCache,
};
use reth_stages::stages::HeaderStage;
use reth_stages_api::{ExecInput, Stage, StageCheckpoint, StageExt, StageId};
use reth_storage_api::{
DBProvider, HeaderSyncGapProvider, NodePrimitivesProvider, StageCheckpointWriter, StateWriter,
};
use reth_tasks::Runtime;
use std::task::{Context, Poll};
use tokio::sync::{mpsc::UnboundedSender, oneshot};
/// Events emitted by [`SnapSyncController`].
#[derive(Debug)]
pub enum SnapSyncControlEvent {
/// Phase B started and engine events can now be forwarded through the sender.
Started(UnboundedSender<SnapSyncEvent>),
/// Snap sync finished.
Finished(Result<SnapSyncOutcome, SnapSyncError>),
/// A controller task was dropped or Phase A failed.
TaskDropped(String),
}
/// Snap sync lifecycle control surface.
pub trait SnapSyncControl: Send {
/// Returns `true` if snap sync is active.
fn is_active(&self) -> bool;
/// Starts snap sync toward the target block hash.
fn start(&mut self, target_hash: alloy_primitives::B256) -> bool;
/// Polls snap sync for its next lifecycle event.
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent>;
}
/// Header type used by snap sync for the given provider factory.
pub type SnapSyncHeader<F> =
<<<F as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives as NodePrimitives>::BlockHeader;
/// A snap sync controller that manages the snap sync lifecycle.
///
/// Snap sync runs in two phases:
/// - **Phase A**: download headers 1..pivot via `HeaderStage` so that static files are populated
/// before any state download begins.
/// - **Phase B**: hand off to [`SnapSyncOrchestrator`](crate::orchestrator::SnapSyncOrchestrator)
/// for state download and BAL catch-up.
#[derive(Debug)]
pub struct SnapSyncController<C, F> {
client: C,
factory: F,
runtime: Runtime,
/// The target hash passed to [`Self::start`], kept until Phase A completes.
target_hash: Option<alloy_primitives::B256>,
/// State of the snap sync: `None` = idle, `Some` = running.
state: Option<SnapSyncState>,
}
/// Running state of snap sync.
#[derive(Debug)]
enum SnapSyncState {
/// Phase A: downloading headers via `HeaderStage`.
DownloadingHeaders { result_rx: oneshot::Receiver<Result<(), String>> },
/// Phase B: state download via orchestrator.
DownloadingState {
/// Sender for forwarding engine events to the orchestrator.
///
/// Kept alive so the orchestrator's receiver doesn't close prematurely.
#[expect(dead_code)]
events_tx: UnboundedSender<SnapSyncEvent>,
/// Receiver for the orchestrator result.
result_rx: oneshot::Receiver<Result<SnapSyncOutcome, SnapSyncError>>,
},
}
impl<C, F> SnapSyncController<C, F> {
/// Creates a new controller.
pub fn new(client: C, factory: F, runtime: Runtime) -> Self {
Self { client, factory, runtime, target_hash: None, state: None }
}
/// Returns `true` if snap sync is currently active.
pub const fn is_active(&self) -> bool {
self.state.is_some()
}
}
impl<C, F> SnapSyncController<C, F>
where
C: SnapClient + HeadersClient<Header = SnapSyncHeader<F>> + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory
+ StaticFileProviderFactory
+ HeaderSyncGapProvider<Header = SnapSyncHeader<F>>
+ Clone
+ Send
+ Sync
+ 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider
+ NodePrimitivesProvider
+ StateWriter
+ StaticFileProviderFactory
+ StageCheckpointWriter,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
SnapSyncHeader<F>: Value + FullBlockHeader,
{
/// Starts snap sync.
///
/// Returns `false` if snap sync is already active.
pub fn start(&mut self, target_hash: alloy_primitives::B256) -> bool {
if self.is_active() {
return false;
}
let (result_tx, result_rx) = oneshot::channel();
let client = self.client.clone();
let factory = self.factory.clone();
self.runtime.spawn_critical_blocking_task("snap sync header download", async move {
let result = Self::run_header_stage(client, factory, target_hash).await;
let _ = result_tx.send(result);
});
self.target_hash = Some(target_hash);
self.state = Some(SnapSyncState::DownloadingHeaders { result_rx });
true
}
/// Phase A: resolve the pivot from peers and run `HeaderStage` to fill
/// static files with headers 1..pivot.
async fn run_header_stage(
client: C,
factory: F,
target_hash: alloy_primitives::B256,
) -> Result<(), String> {
tracing::info!(target: "sync::snap", %target_hash, "Phase A: resolving target header from peers");
let target_header = client
.get_header(alloy_eips::BlockHashOrNumber::Hash(target_hash))
.await
.map_err(|e| format!("failed to fetch target header: {e}"))?
.into_data()
.ok_or_else(|| "peer returned empty response for target header".to_string())?;
let target_number = target_header.number();
let pivot_number = target_number.saturating_sub(crate::PIVOT_OFFSET);
if pivot_number == 0 {
tracing::info!(target: "sync::snap", "Target too low for header stage, skipping Phase A");
return Ok(());
}
let pivot_header = client
.get_header(alloy_eips::BlockHashOrNumber::Number(pivot_number))
.await
.map_err(|e| format!("failed to fetch pivot header: {e}"))?
.into_data()
.ok_or_else(|| "peer returned empty response for pivot header".to_string())?;
let pivot_sealed = reth_primitives_traits::SealedHeader::seal_slow(pivot_header);
let pivot_hash = pivot_sealed.hash();
tracing::info!(
target: "sync::snap",
target_number,
pivot_number,
%pivot_hash,
"Phase A: downloading headers 1..{pivot_number}"
);
let (_tip_tx, tip_rx) = tokio::sync::watch::channel(pivot_hash);
let downloader =
ReverseHeadersDownloaderBuilder::default().build(client, NoopConsensus::arc());
let mut stage = HeaderStage::new(factory.clone(), downloader, tip_rx, EtlConfig::default());
let input =
ExecInput { target: Some(pivot_number), checkpoint: Some(StageCheckpoint::new(0)) };
<HeaderStage<F, _> as StageExt<F::ProviderRW>>::execute_ready(&mut stage, input)
.await
.map_err(|e| format!("header download failed: {e}"))?;
let provider_rw = factory.database_provider_rw().map_err(|e| format!("db error: {e}"))?;
let _output = Stage::<F::ProviderRW>::execute(&mut stage, &provider_rw, input)
.map_err(|e| format!("header write failed: {e}"))?;
provider_rw
.save_stage_checkpoint(StageId::Headers, StageCheckpoint::new(pivot_number))
.map_err(|e| format!("checkpoint save failed: {e}"))?;
provider_rw.commit().map_err(|e| format!("commit failed: {e}"))?;
factory
.static_file_provider()
.commit()
.map_err(|e| format!("static file commit failed: {e}"))?;
tracing::info!(target: "sync::snap", pivot_number, "Phase A complete: headers written to static files");
Ok(())
}
/// Spawn the Phase B orchestrator and return the started event.
fn start_orchestrator(&mut self) -> SnapSyncControlEvent {
let target_hash = self.target_hash.take().expect("target_hash set during start()");
let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel();
let (result_tx, result_rx) = oneshot::channel();
let orchestrator = crate::orchestrator::SnapSyncOrchestrator::new(
self.client.clone(),
self.factory.clone(),
);
self.runtime.spawn_critical_blocking_task("snap sync orchestrator", async move {
let result = orchestrator.run(events_rx, target_hash).await;
let _ = result_tx.send(result);
});
let started_tx = events_tx.clone();
self.state = Some(SnapSyncState::DownloadingState { events_tx, result_rx });
SnapSyncControlEvent::Started(started_tx)
}
/// Polls the controller for the next lifecycle event.
pub fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent> {
let Some(state) = &mut self.state else {
return Poll::Pending;
};
match state {
SnapSyncState::DownloadingHeaders { result_rx } => match result_rx.poll_unpin(cx) {
Poll::Ready(Ok(Ok(()))) => Poll::Ready(self.start_orchestrator()),
Poll::Ready(Ok(Err(e))) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped(format!(
"snap sync header download failed: {e}"
)))
}
Poll::Ready(Err(_)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped(
"snap sync header download task dropped".into(),
))
}
Poll::Pending => Poll::Pending,
},
SnapSyncState::DownloadingState { result_rx, .. } => match result_rx.poll_unpin(cx) {
Poll::Ready(Ok(result)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::Finished(result))
}
Poll::Ready(Err(_)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped("snap sync task dropped".into()))
}
Poll::Pending => Poll::Pending,
},
}
}
}
impl<C, F> SnapSyncControl for SnapSyncController<C, F>
where
C: SnapClient + HeadersClient<Header = SnapSyncHeader<F>> + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory
+ StaticFileProviderFactory
+ HeaderSyncGapProvider<Header = SnapSyncHeader<F>>
+ Clone
+ Send
+ Sync
+ 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider
+ NodePrimitivesProvider
+ StateWriter
+ StaticFileProviderFactory
+ StageCheckpointWriter,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
SnapSyncHeader<F>: Value + FullBlockHeader,
{
fn is_active(&self) -> bool {
SnapSyncController::is_active(self)
}
fn start(&mut self, target_hash: alloy_primitives::B256) -> bool {
SnapSyncController::start(self, target_hash)
}
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent> {
SnapSyncController::poll(self, cx)
}
}

View File

@@ -1,650 +0,0 @@
//! Snap sync download loops for accounts, storage, and bytecodes.
//!
//! The main entry point is [`download_state`] which streams through the entire
//! state trie in account-hash order. For each batch of accounts it immediately
//! fetches the associated storage slots and bytecodes before moving on to the
//! next range. This keeps memory usage bounded to a single batch at a time
//! regardless of total state size.
use crate::{
proof::verify_range_proof,
storage::{increment_b256, write_bytecodes, write_hashed_accounts, write_hashed_storages},
SnapSyncError, SNAP_RESPONSE_BYTES_LIMIT,
};
use alloy_primitives::{keccak256, Bytes, B256, U256};
use reth_db_api::transaction::DbTxMut;
use reth_eth_wire_types::snap::{
GetAccountRangeMessage, GetByteCodesMessage, GetStorageRangesMessage, StorageData, TrieAccount,
};
use reth_network_p2p::snap::client::{SnapClient, SnapResponse};
use reth_primitives_traits::Account;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::{DBProvider, StateWriter};
use reth_trie::root::storage_root;
use std::collections::{HashMap, HashSet};
use tracing::info;
/// Maximum number of account hashes per storage range request.
const STORAGE_BATCH_SIZE: usize = 20;
/// Maximum number of code hashes per bytecode request.
const BYTECODE_BATCH_SIZE: usize = 50;
/// Maximum hash value used as the range upper bound.
const MAX_HASH: B256 = B256::new([0xff; 32]);
type DecodedStorageSlots = Vec<(B256, U256)>;
// ──────────────────────────────────────────────────────────────────────────────
// Streaming state download
// ──────────────────────────────────────────────────────────────────────────────
/// Result of a [`download_state`] call.
#[derive(Debug)]
pub enum DownloadStateOutcome {
/// Entire account range was iterated — download is complete.
Done,
/// The serving peer returned empty for the requested root (stale).
/// Contains the `starting_hash` to resume from after the caller
/// advances the pivot and obtains a fresh root.
Stale {
/// The hash to resume downloading from with a fresh root.
resume_from: B256,
},
}
/// Downloads state (accounts, storage, bytecodes) at `root_hash`, streaming
/// from `starting_hash` onward.
///
/// For each batch of accounts returned by `GetAccountRange`, the associated
/// storage and bytecodes are fetched and written to MDBX immediately before
/// requesting the next account range. Memory usage is bounded to one batch.
///
/// Returns [`DownloadStateOutcome::Stale`] when the serving peer returns empty
/// (root not available). The caller should advance the pivot to get a new root
/// and call this again with the returned `resume_from` hash — no progress is
/// lost.
pub async fn download_state<C, F>(
client: &C,
factory: &F,
root_hash: B256,
starting_hash: B256,
) -> Result<DownloadStateOutcome, SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut request_id: u64 = 0;
let mut cursor = starting_hash;
loop {
// Remember the start of this batch so we can resume here if any
// sub-fetch (accounts, storage, bytecodes) hits a stale root.
let batch_start = cursor;
// ── Fetch account batch ──────────────────────────────────────────
request_id += 1;
let request = GetAccountRangeMessage {
request_id,
root_hash,
starting_hash: cursor,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_account_range(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap account range request failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::AccountRange(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.accounts.is_empty() {
if msg.proof.is_empty() {
return Ok(DownloadStateOutcome::Stale { resume_from: cursor });
}
verify_account_range_proof(root_hash, cursor, &[], &msg.proof)?;
return Ok(DownloadStateOutcome::Done);
}
// ── Decode + write accounts ──────────────────────────────────────
let mut account_batch = Vec::with_capacity(msg.accounts.len());
let mut batch_account_hashes = Vec::with_capacity(msg.accounts.len());
let mut batch_code_hashes = HashSet::new();
let mut batch_storage_roots = HashMap::with_capacity(msg.accounts.len());
let mut decoded_accounts = Vec::with_capacity(msg.accounts.len());
let mut previous_hash = None;
for account_data in &msg.accounts {
if account_data.hash < cursor ||
previous_hash.is_some_and(|previous| account_data.hash <= previous)
{
return Err(SnapSyncError::Network(
"snap account range returned non-monotonic account hashes".into(),
));
}
let trie_account = account_data.account;
let account = Account::from(trie_account);
if let Some(code_hash) = account.bytecode_hash {
batch_code_hashes.insert(code_hash);
}
previous_hash = Some(account_data.hash);
batch_storage_roots.insert(account_data.hash, trie_account.storage_root);
decoded_accounts.push((account_data.hash, trie_account));
batch_account_hashes.push(account_data.hash);
account_batch.push((account_data.hash, account));
}
verify_account_range_proof(root_hash, cursor, &decoded_accounts, &msg.proof)?;
info!(
target: "engine::snap_sync",
accounts = account_batch.len(),
%root_hash,
"Downloaded account range"
);
write_hashed_accounts(factory, &account_batch)?;
// ── Fetch + write storage for this batch ─────────────────────────
// If the peer returns empty (stale root), return Stale at batch_start
// so the caller retries the entire batch with a fresh root.
if fetch_storage_for_accounts(
client,
factory,
root_hash,
&batch_account_hashes,
&batch_storage_roots,
&mut request_id,
)
.await?
{
return Ok(DownloadStateOutcome::Stale { resume_from: batch_start });
}
// ── Fetch + write bytecodes for this batch ───────────────────────
fetch_bytecodes(client, factory, &batch_code_hashes, &mut request_id).await?;
// ── Advance cursor ───────────────────────────────────────────────
let last_hash = msg.accounts.last().expect("checked non-empty above").hash;
if last_hash == MAX_HASH {
return Ok(DownloadStateOutcome::Done);
}
cursor = increment_b256(last_hash);
}
}
// ──────────────────────────────────────────────────────────────────────────────
// Storage download (per-batch)
// ──────────────────────────────────────────────────────────────────────────────
/// Fetches and writes storage for a batch of account hashes.
///
/// Returns `Ok(true)` if the serving peer returned empty (stale root),
/// `Ok(false)` if all storage was fetched successfully.
///
/// Handles the snap protocol's response-size truncation: if the last account
/// in a multi-account response has a proof attached, its storage was incomplete
/// and we issue continuation requests for that account before moving on.
async fn fetch_storage_for_accounts<C, F>(
client: &C,
factory: &F,
root_hash: B256,
account_hashes: &[B256],
storage_roots: &HashMap<B256, B256>,
request_id: &mut u64,
) -> Result<bool, SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut idx = 0;
while idx < account_hashes.len() {
let end = (idx + STORAGE_BATCH_SIZE).min(account_hashes.len());
let chunk = &account_hashes[idx..end];
*request_id += 1;
let request = GetStorageRangesMessage {
request_id: *request_id,
root_hash,
account_hashes: chunk.to_vec(),
starting_hash: B256::ZERO,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_storage_ranges(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap storage range request failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::StorageRanges(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.slots.len() > chunk.len() {
return Err(SnapSyncError::Network(
"snap storage range returned more slot lists than requested".into(),
));
}
let returned_count = msg.slots.len();
// Empty response for the very first sub-chunk → stale root.
if returned_count == 0 && idx == 0 {
return Ok(true);
}
if returned_count == 0 {
return Err(SnapSyncError::Network("snap storage range returned no progress".into()));
}
let has_proof = !msg.proof.is_empty();
let proof_index = has_proof.then_some(returned_count - 1);
let mut entries = Vec::new();
for (i, slots) in msg.slots.iter().enumerate() {
let account_hash = chunk[i];
validate_storage_slots(account_hash, B256::ZERO, MAX_HASH, slots)?;
let account_slots = if Some(i) == proof_index {
verify_storage_range_proof(
account_hash,
storage_roots,
B256::ZERO,
slots,
&msg.proof,
)?;
if slots.is_empty() {
verify_full_storage_range(account_hash, storage_roots, slots)?
} else {
let resume_from =
increment_b256(slots.last().expect("slots is not empty").hash);
match fetch_storage_continuation(
client,
root_hash,
account_hash,
storage_roots,
resume_from,
request_id,
slots.clone(),
)
.await?
{
StorageContinuationOutcome::Complete(slots) => slots,
StorageContinuationOutcome::Stale => return Ok(true),
}
}
} else {
verify_full_storage_range(account_hash, storage_roots, slots)?
};
entries.extend(
account_slots
.into_iter()
.map(|(slot_hash, value)| (account_hash, slot_hash, value)),
);
}
if !entries.is_empty() {
write_hashed_storages(factory, &entries)?;
}
if has_proof {
idx += returned_count;
} else if returned_count < chunk.len() {
idx += returned_count;
} else {
idx = end;
}
}
Ok(false)
}
/// Continuation result for a single-account storage range.
enum StorageContinuationOutcome {
/// The account storage is complete and verified against its root.
Complete(DecodedStorageSlots),
/// The serving peer no longer has the requested root or account.
Stale,
}
/// Continuation loop for a single account whose storage was truncated.
async fn fetch_storage_continuation<C>(
client: &C,
root_hash: B256,
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
mut starting_hash: B256,
request_id: &mut u64,
mut collected_slots: Vec<StorageData>,
) -> Result<StorageContinuationOutcome, SnapSyncError>
where
C: SnapClient + 'static,
{
loop {
*request_id += 1;
let request = GetStorageRangesMessage {
request_id: *request_id,
root_hash,
account_hashes: vec![account_hash],
starting_hash,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_storage_ranges(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap storage continuation failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::StorageRanges(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.slots.len() > 1 {
return Err(SnapSyncError::Network(
"snap storage continuation returned multiple slot lists".into(),
));
}
let Some(slots) = msg.slots.first() else {
return Ok(StorageContinuationOutcome::Stale);
};
if slots.is_empty() {
if !msg.proof.is_empty() {
verify_storage_range_proof(
account_hash,
storage_roots,
starting_hash,
slots,
&msg.proof,
)?;
}
let decoded = verify_full_storage_range(account_hash, storage_roots, &collected_slots)?;
return Ok(StorageContinuationOutcome::Complete(decoded));
}
validate_storage_slots(account_hash, starting_hash, MAX_HASH, slots)?;
if !msg.proof.is_empty() {
verify_storage_range_proof(
account_hash,
storage_roots,
starting_hash,
slots,
&msg.proof,
)?;
}
collected_slots.extend_from_slice(slots);
if msg.proof.is_empty() {
let decoded = verify_full_storage_range(account_hash, storage_roots, &collected_slots)?;
return Ok(StorageContinuationOutcome::Complete(decoded));
}
starting_hash = increment_b256(slots.last().unwrap().hash);
}
}
fn validate_storage_slots(
account_hash: B256,
starting_hash: B256,
limit_hash: B256,
slots: &[StorageData],
) -> Result<(), SnapSyncError> {
let mut previous = None;
for slot in slots {
if slot.hash < starting_hash || slot.hash >= limit_hash {
return Err(SnapSyncError::Network(format!(
"snap storage range for account {account_hash} returned slot outside requested bounds"
)))
}
if previous.is_some_and(|previous| slot.hash <= previous) {
return Err(SnapSyncError::Network(format!(
"snap storage range for account {account_hash} returned non-monotonic slots"
)))
}
previous = Some(slot.hash);
}
Ok(())
}
fn verify_full_storage_range(
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
slots: &[StorageData],
) -> Result<DecodedStorageSlots, SnapSyncError> {
let Some(expected_root) = storage_roots.get(&account_hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap storage response for unknown account {account_hash}"
)));
};
let decoded = decode_storage_slots(slots)?;
let got = storage_root(decoded.iter().copied());
if got != expected_root {
return Err(SnapSyncError::Network(format!(
"snap full storage range root mismatch for account {account_hash}: expected {expected_root}, got {got}"
)))
}
Ok(decoded)
}
fn verify_account_range_proof(
root_hash: B256,
starting_hash: B256,
accounts: &[(B256, TrieAccount)],
proof: &[Bytes],
) -> Result<(), SnapSyncError> {
let leaves =
accounts.iter().copied().map(|(hash, account)| (hash, account_trie_value(account)));
verify_range_proof(root_hash, starting_hash, leaves, proof)
.map_err(|e| SnapSyncError::Network(format!("invalid snap account range proof: {e}")))
}
fn verify_storage_range_proof(
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
starting_hash: B256,
slots: &[StorageData],
proof: &[Bytes],
) -> Result<DecodedStorageSlots, SnapSyncError> {
let Some(storage_root) = storage_roots.get(&account_hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap storage proof for unknown account {account_hash}"
)));
};
let decoded = decode_storage_slots(slots)?;
let leaves = decoded
.iter()
.map(|(hash, value)| (*hash, alloy_rlp::encode_fixed_size(value).as_ref().to_vec()));
verify_range_proof(storage_root, starting_hash, leaves, proof)
.map_err(|e| SnapSyncError::Network(format!("invalid snap storage range proof: {e}")))?;
Ok(decoded)
}
fn account_trie_value(account: TrieAccount) -> Vec<u8> {
alloy_rlp::encode(account)
}
fn decode_storage_slots(slots: &[StorageData]) -> Result<DecodedStorageSlots, SnapSyncError> {
slots
.iter()
.map(|slot| {
let value = slot
.decode_value()
.map_err(|e| SnapSyncError::RlpDecode(format!("snap storage decode: {e}")))?;
Ok((slot.hash, value))
})
.collect()
}
// ──────────────────────────────────────────────────────────────────────────────
// Bytecode download (per-batch)
// ──────────────────────────────────────────────────────────────────────────────
/// Fetches and writes bytecodes for a set of code hashes.
async fn fetch_bytecodes<C, F>(
client: &C,
factory: &F,
code_hashes: &HashSet<B256>,
request_id: &mut u64,
) -> Result<(), SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let hashes: Vec<B256> = code_hashes.iter().copied().collect();
for chunk in hashes.chunks(BYTECODE_BATCH_SIZE) {
*request_id += 1;
let request = GetByteCodesMessage {
request_id: *request_id,
hashes: chunk.to_vec(),
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client
.get_byte_codes(request)
.await
.map_err(|e| SnapSyncError::Network(format!("snap bytecode request failed: {e}")))?;
let msg = match response.into_data() {
SnapResponse::ByteCodes(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
let codes = match_bytecodes_to_hashes(chunk, &msg.codes)?;
if !codes.is_empty() {
write_bytecodes(factory, &codes)?;
}
}
Ok(())
}
fn match_bytecodes_to_hashes(
requested_hashes: &[B256],
codes: &[Bytes],
) -> Result<Vec<(B256, Bytes)>, SnapSyncError> {
let requested: HashMap<_, _> =
requested_hashes.iter().copied().enumerate().map(|(i, hash)| (hash, i)).collect();
let mut seen = HashSet::new();
let mut last_position = None;
let mut matched = Vec::with_capacity(codes.len());
for code in codes {
let hash = keccak256(code.as_ref());
let Some(position) = requested.get(&hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap bytecode response contained unrequested code hash {hash}"
)))
};
if last_position.is_some_and(|last| position <= last) {
return Err(SnapSyncError::Network(
"snap bytecode response was not in request order".into(),
));
}
if !seen.insert(hash) {
return Err(SnapSyncError::Network(format!(
"snap bytecode response duplicated code hash {hash}"
)))
}
last_position = Some(position);
matched.push((hash, code.clone()));
}
Ok(matched)
}
#[cfg(test)]
mod tests {
use super::*;
fn b256_from_u64(value: u64) -> B256 {
B256::left_padding_from(&value.to_be_bytes())
}
#[test]
fn bytecode_matching_uses_returned_code_hashes() {
let first = Bytes::from_static(&[1, 2, 3]);
let second = Bytes::from_static(&[4, 5, 6]);
let requested = vec![keccak256(second.as_ref()), keccak256(first.as_ref())];
let matched = match_bytecodes_to_hashes(&requested, &[first.clone()]).unwrap();
assert_eq!(matched, vec![(keccak256(first.as_ref()), first)]);
}
#[test]
fn bytecode_matching_rejects_unrequested_code() {
let requested = vec![keccak256([1, 2, 3])];
let unrequested = Bytes::from_static(&[4, 5, 6]);
assert!(match_bytecodes_to_hashes(&requested, &[unrequested]).is_err());
}
#[test]
fn bytecode_matching_rejects_out_of_order_codes() {
let first = Bytes::from_static(&[1, 2, 3]);
let second = Bytes::from_static(&[4, 5, 6]);
let requested = vec![keccak256(first.as_ref()), keccak256(second.as_ref())];
assert!(match_bytecodes_to_hashes(&requested, &[second, first]).is_err());
}
#[test]
fn storage_slots_must_be_ordered_within_bounds() {
let account = b256_from_u64(1);
let first = StorageData::from_value(b256_from_u64(2), alloy_primitives::U256::from(2));
let second = StorageData::from_value(b256_from_u64(3), alloy_primitives::U256::from(3));
assert!(validate_storage_slots(
account,
b256_from_u64(2),
b256_from_u64(4),
&[first.clone(), second.clone()]
)
.is_ok());
assert!(validate_storage_slots(
account,
b256_from_u64(2),
b256_from_u64(4),
&[second, first]
)
.is_err());
}
#[test]
fn full_storage_range_verifies_storage_root() {
let account = b256_from_u64(1);
let slot = b256_from_u64(2);
let value = alloy_primitives::U256::from(3);
let storage_roots = HashMap::from([(account, storage_root([(slot, value)]))]);
let slots = vec![StorageData::from_value(slot, value)];
assert!(verify_full_storage_range(account, &storage_roots, &slots).is_ok());
assert!(verify_full_storage_range(account, &storage_roots, &[]).is_err());
}
}

View File

@@ -1,68 +0,0 @@
//! Sync finalization: stage checkpoints and static file segment advancement.
use crate::{storage::db_err, SnapSyncError};
use reth_db_api::{tables, transaction::DbTxMut};
use reth_provider::{
DatabaseProviderFactory, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
};
use reth_stages_types::{StageCheckpoint, StageId};
use reth_storage_api::DBProvider;
/// Writes stage checkpoints for all stages that snap sync satisfies.
///
/// After BAL healing completes, the database state corresponds to `target_block`.
/// This records that fact so the pipeline can resume from the correct point.
pub(crate) fn write_snap_stage_checkpoints<F>(
factory: &F,
target_block: u64,
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StaticFileProviderFactory,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let checkpoint = StageCheckpoint::new(target_block);
let stages = [
StageId::Bodies,
StageId::SenderRecovery,
StageId::Execution,
StageId::AccountHashing,
StageId::StorageHashing,
StageId::TransactionLookup,
StageId::IndexAccountHistory,
StageId::IndexStorageHistory,
];
let provider = factory.database_provider_rw().map_err(db_err)?;
{
let tx = provider.tx_ref();
for stage_id in stages {
tx.put::<tables::StageCheckpoints>(stage_id.to_string(), checkpoint).map_err(db_err)?;
}
}
// Advance static file segments that snap sync did not populate (headers are
// already filled by Phase A). Without this, the persistence service would fail
// with `UnexpectedStaticFileBlockNumber` when writing blocks after the snap
// target because these segments would still be at block 0.
let segments = [
StaticFileSegment::Transactions,
StaticFileSegment::TransactionSenders,
StaticFileSegment::Receipts,
StaticFileSegment::AccountChangeSets,
StaticFileSegment::StorageChangeSets,
];
let sfp = provider.static_file_provider();
for segment in segments {
let mut writer = sfp.get_writer(0, segment).map_err(|e| {
SnapSyncError::Database(format!("static file writer for {segment:?}: {e}"))
})?;
writer.ensure_at_block(target_block).map_err(|e| {
SnapSyncError::Database(format!("ensure_at_block({target_block}) for {segment:?}: {e}"))
})?;
}
sfp.commit().map_err(|e| SnapSyncError::Database(format!("static file commit: {e}")))?;
provider.commit().map_err(db_err)?;
Ok(())
}

View File

@@ -1,118 +0,0 @@
//! Engine-driven snap sync orchestrator for snap/2 (EIP-8189).
//!
//! This crate implements a standalone snap sync process driven by the engine tree,
//! not the staged pipeline. Snap sync is a live, reactive process that responds to
//! chain advancement in real-time via events forwarded from the engine.
pub mod bal;
pub mod controller;
pub mod download;
pub mod finalize;
pub mod orchestrator;
pub mod pivot;
pub mod serve;
pub mod storage;
mod proof;
use alloy_primitives::{Bytes, B256};
/// How many blocks behind HEAD to place the snap sync pivot.
///
/// The serving node reverse-applies changesets to reconstruct hashed state at
/// HEADN, so this must be large enough that the target block's hashed state
/// is always fully persisted to MDBX (the engine keeps ~2 blocks in memory).
pub const PIVOT_OFFSET: u64 = 16;
/// Soft response size limit for snap protocol requests (2 MiB).
pub const SNAP_RESPONSE_BYTES_LIMIT: u64 = 2 * 1024 * 1024;
/// Events sent from the engine tree to the snap sync orchestrator.
#[derive(Debug, Clone)]
pub enum SnapSyncEvent {
/// A new block was received via `new_payload`. The BAL bytes come from `ExecutionPayloadV4`.
NewBlock {
/// Block number.
number: u64,
/// Block hash.
hash: B256,
/// State root from the block header.
state_root: B256,
/// Parent block hash.
parent_hash: B256,
/// RLP-encoded BAL bytes, if present in the payload.
bal: Option<Bytes>,
},
/// A block downloaded by the engine's block downloader.
/// Contains header info needed for persistence and BAL resolution.
DownloadedBlock {
/// Block number.
number: u64,
/// Block hash.
hash: B256,
/// State root from the block header.
state_root: B256,
/// Parent block hash.
parent_hash: B256,
},
/// The canonical head changed via `forkchoiceUpdated`.
NewHead {
/// Head block hash.
head_hash: B256,
},
}
/// Outcome reported by the orchestrator when snap sync completes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapSyncOutcome {
/// The block number that was synced to.
pub synced_to: u64,
/// Block hash of the synced-to block.
pub block_hash: B256,
}
/// Errors that can occur during snap sync.
#[derive(Debug, thiserror::Error)]
pub enum SnapSyncError {
/// A network request failed.
#[error("network request failed: {0}")]
Network(String),
/// Database operation failed.
#[error("database error: {0}")]
Database(String),
/// RLP decoding failed.
#[error("RLP decode error: {0}")]
RlpDecode(String),
/// BAL verification failed (hash mismatch).
#[error("BAL verification failed for block {block}: expected {expected}, got {got}")]
BalVerification {
/// Block number.
block: u64,
/// Expected hash from the header.
expected: B256,
/// Computed hash from the BAL bytes.
got: B256,
},
/// Header not found.
#[error("header not found for block {0}")]
MissingHeader(u64),
/// Block hash not found.
#[error("block hash not found for block {0}")]
MissingBlockHash(u64),
/// State root mismatch after sync.
#[error("state root mismatch at block {block}: expected {expected}, computed {computed}")]
StateRootMismatch {
/// Block number.
block: u64,
/// Expected state root from header.
expected: B256,
/// Computed state root.
computed: B256,
},
/// Event channel closed unexpectedly.
#[error("event channel closed")]
ChannelClosed,
/// BAL not available for a required block.
#[error("BAL not available for block {0}")]
MissingBal(u64),
}

View File

@@ -1,277 +0,0 @@
//! Snap sync orchestrator — the main async loop that drives snap sync from start to finish.
use crate::{
bal::{bal_to_state_diff, merge_account_diff},
download::{download_state, DownloadStateOutcome},
finalize::write_snap_stage_checkpoints,
pivot::PivotTracker,
storage::{
clear_hashed_state, read_hashed_account, write_bytecodes, write_hashed_accounts,
write_hashed_storages,
},
SnapSyncError, SnapSyncEvent, SnapSyncOutcome, PIVOT_OFFSET,
};
use alloy_consensus::BlockHeader;
use alloy_eips::BlockHashOrNumber;
use alloy_primitives::B256;
use reth_db_api::transaction::{DbTx, DbTxMut};
use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
use reth_primitives_traits::SealedHeader;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::{DBProvider, StateWriter, StorageSettingsCache};
use tokio::sync::mpsc::UnboundedReceiver;
/// Engine-driven snap sync orchestrator.
///
/// Runs as an async task spawned by the engine tree. Receives chain events
/// via an mpsc channel and drives the snap sync process through four phases:
///
/// 1. **Bootstrap** — wait for head, pick pivot, clear state
/// 2. **Bulk download** — download accounts, storage, bytecodes from peers
/// 3. **BAL catch-up** — apply remaining BAL diffs to reach latest known block
/// 4. **Verification** — compute and verify state root
#[derive(Debug)]
pub struct SnapSyncOrchestrator<C, F> {
client: C,
factory: F,
}
impl<C, F> SnapSyncOrchestrator<C, F>
where
C: SnapClient + HeadersClient + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider + StateWriter + reth_provider::StaticFileProviderFactory,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
/// Creates a new orchestrator with the given network client and database factory.
pub fn new(client: C, factory: F) -> Self {
Self { client, factory }
}
/// Runs the snap sync orchestrator to completion.
///
/// This is the main entry point, intended to be spawned as a tokio task.
/// `target_hash` is the FCU head block hash — used to resolve the head from
/// peers when no `NewHead` event arrives (e.g. frozen-head / fresh-node scenario).
pub async fn run(
self,
mut events_rx: UnboundedReceiver<SnapSyncEvent>,
target_hash: B256,
) -> Result<SnapSyncOutcome, SnapSyncError> {
// ── Phase 0: Bootstrap — wait for head, pick pivot, clear state ──────
tracing::info!(target: "engine::snap_sync", %target_hash, "Starting snap sync orchestrator");
let mut pre_buffered_blocks = Vec::new();
let (initial_head_number, initial_head_hash) = loop {
// Try to receive a NewHead from the engine tree first (non-blocking drain)
match events_rx.try_recv() {
Ok(SnapSyncEvent::NewHead { head_hash }) => {
let header = self
.client
.get_header(BlockHashOrNumber::Hash(head_hash))
.await
.map_err(|e| SnapSyncError::Network(format!("header fetch failed: {e}")))?
.into_data()
.ok_or_else(|| {
SnapSyncError::Network(format!(
"peer returned empty response for header {head_hash}"
))
})?;
break (header.number(), head_hash);
}
Ok(event @ SnapSyncEvent::NewBlock { .. }) |
Ok(event @ SnapSyncEvent::DownloadedBlock { .. }) => {
pre_buffered_blocks.push(event);
continue;
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
return Err(SnapSyncError::ChannelClosed);
}
}
// No NewHead available yet — resolve from peers using the target hash.
tracing::info!(
target: "engine::snap_sync",
%target_hash,
"No NewHead event, resolving target header from peers"
);
let header = self
.client
.get_header(BlockHashOrNumber::Hash(target_hash))
.await
.map_err(|e| SnapSyncError::Network(format!("header fetch failed: {e}")))?
.into_data()
.ok_or_else(|| {
SnapSyncError::Network(format!(
"peer returned empty response for header {target_hash}"
))
})?;
break (header.number(), target_hash);
};
let pivot_block = initial_head_number.saturating_sub(PIVOT_OFFSET);
let initial_pivot = pivot_block;
let pivot_root = {
let from_buffer = pre_buffered_blocks.iter().find_map(|e| match e {
SnapSyncEvent::NewBlock { number, state_root, .. } |
SnapSyncEvent::DownloadedBlock { number, state_root, .. }
if *number == pivot_block =>
{
Some(*state_root)
}
_ => None,
});
match from_buffer {
Some(root) => root,
None => {
// Try local DB first, fall back to fetching from peers
let local = self
.factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(pivot_block).ok().flatten());
match local {
Some(h) => h.state_root(),
None => {
let h = self
.client
.get_header(BlockHashOrNumber::Number(pivot_block))
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"pivot header fetch failed: {e}"
))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(pivot_block))?;
h.state_root()
}
}
}
}
};
tracing::info!(
target: "engine::snap_sync",
pivot_block,
%pivot_root,
head = initial_head_number,
"Picked pivot"
);
let mut tracker = PivotTracker::new(pivot_block, pivot_root, events_rx);
tracker.set_known_head(initial_head_number, initial_head_hash);
for event in pre_buffered_blocks {
tracker.buffer_event(event);
}
clear_hashed_state(&self.factory)?;
tracing::info!(target: "engine::snap_sync", "Cleared hashed state tables");
// ── Phase 1: Bulk state download ─────────────────────────────────────
//
// Stream accounts in hash order. If the serving peer returns empty
// (root is stale because chain advanced), advance the pivot to get a
// fresh root and resume from the same position.
tracing::info!(target: "engine::snap_sync", %pivot_root, "Phase 1: bulk state download");
let mut download_cursor = B256::ZERO;
loop {
let root = tracker.pivot_root();
match download_state(&self.client, &self.factory, root, download_cursor).await? {
DownloadStateOutcome::Done => break,
DownloadStateOutcome::Stale { resume_from } => {
tracing::info!(
target: "engine::snap_sync",
%root,
%resume_from,
"Pivot root stale, re-resolving head from peers"
);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// Drain any new events that arrived while we were sleeping.
tracker.drain_events();
// Try to discover chain advancement by probing a header
// a few blocks ahead of what we know. The serving node
// may have advanced beyond our initial head.
let probe = tracker.known_head() + 10;
if let Ok(response) =
self.client.get_header(BlockHashOrNumber::Number(probe)).await
{
if let Some(header) = response.into_data() {
let hash = SealedHeader::seal_slow(header).hash();
tracker.set_known_head(probe, hash);
}
}
tracker.advance_pivot(&self.client, &self.factory).await?;
download_cursor = resume_from;
}
}
}
tracing::info!(target: "engine::snap_sync", "Phase 1 complete: bulk download finished");
// ── Phase 2: BAL catch-up ────────────────────────────────────────────
tracing::info!(target: "engine::snap_sync", "Phase 2: BAL catch-up");
tracker.drain_events();
let final_block = tracker.known_head();
if final_block > initial_pivot {
for block_num in (initial_pivot + 1)..=final_block {
let bal = tracker.get_verified_bal(&self.client, &self.factory, block_num).await?;
let diff = bal_to_state_diff(&bal.account_changes);
let mut merged = Vec::with_capacity(diff.accounts.len());
for acct_diff in &diff.accounts {
let existing = read_hashed_account(&self.factory, acct_diff.hashed_address)?;
let account = merge_account_diff(acct_diff, existing.as_ref());
merged.push((acct_diff.hashed_address, account));
}
write_hashed_accounts(&self.factory, &merged)?;
write_hashed_storages(&self.factory, &diff.storage)?;
write_bytecodes(&self.factory, &diff.bytecodes)?;
tracing::info!(
target: "engine::snap_sync",
block = block_num,
bal_bytes_len = bal.bytes.len(),
account_changes_count = bal.account_changes.len(),
diff_accounts = diff.accounts.len(),
diff_storage = diff.storage.len(),
diff_bytecodes = diff.bytecodes.len(),
"Applied BAL catch-up diff"
);
}
}
tracing::info!(
target: "engine::snap_sync",
final_block,
"Phase 2 complete: BAL catch-up finished"
);
write_snap_stage_checkpoints(&self.factory, final_block)?;
tracing::info!(
target: "engine::snap_sync",
block = final_block,
"Snap sync complete — MerkleExecute stage will verify state root"
);
Ok(SnapSyncOutcome { synced_to: final_block, block_hash: tracker.known_head_hash() })
}
}

View File

@@ -1,310 +0,0 @@
//! Pivot tracking, advancement, and BAL buffering.
use crate::{SnapSyncError, SnapSyncEvent, SNAP_RESPONSE_BYTES_LIMIT};
use alloy_consensus::BlockHeader;
use alloy_eip7928::{compute_block_access_list_hash, AccountChanges};
use alloy_eips::BlockHashOrNumber;
use alloy_primitives::{Bytes, B256};
use alloy_rlp::Decodable;
use reth_db_api::transaction::DbTx;
use reth_eth_wire_types::snap::GetBlockAccessListsMessage;
use reth_network_p2p::{
headers::client::HeadersClient,
snap::client::{SnapClient, SnapResponse},
};
use reth_primitives_traits::SealedHeader;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::DBProvider;
use std::collections::BTreeMap;
use tokio::sync::mpsc::UnboundedReceiver;
/// A block that has been received from the engine but not yet applied.
#[derive(Debug, Clone)]
struct BufferedBlock {
/// State root from the block header.
state_root: B256,
/// RLP-encoded BAL bytes, if present.
bal: Option<Bytes>,
}
/// A verified BAL and its decoded account changes.
pub(crate) struct VerifiedBal {
/// Original RLP-encoded BAL bytes.
pub bytes: Bytes,
/// Decoded account changes.
pub account_changes: Vec<AccountChanges>,
}
/// Tracks the current pivot block and buffers incoming BALs from the engine.
#[derive(Debug)]
pub struct PivotTracker {
/// Current pivot block number.
pivot_block: u64,
/// State root at the current pivot.
pivot_root: B256,
/// Known head block number (from NewHead events).
known_head: u64,
/// Known head hash.
known_head_hash: B256,
/// Buffered blocks received via `SnapSyncEvent::NewBlock`, keyed by block number.
buffered_blocks: BTreeMap<u64, BufferedBlock>,
/// Event receiver from the engine.
events_rx: UnboundedReceiver<SnapSyncEvent>,
}
impl PivotTracker {
/// Creates a new tracker with the given pivot and an empty buffer.
pub fn new(
pivot_block: u64,
pivot_root: B256,
events_rx: UnboundedReceiver<SnapSyncEvent>,
) -> Self {
Self {
pivot_block,
pivot_root,
known_head: 0,
known_head_hash: B256::ZERO,
buffered_blocks: BTreeMap::new(),
events_rx,
}
}
/// Returns the current pivot block number.
pub fn pivot_block(&self) -> u64 {
self.pivot_block
}
/// Returns the state root at the current pivot.
pub fn pivot_root(&self) -> B256 {
self.pivot_root
}
/// Returns the known head block number.
pub fn known_head(&self) -> u64 {
self.known_head
}
/// Returns the known head block hash.
pub fn known_head_hash(&self) -> B256 {
self.known_head_hash
}
/// Sets the known head block number and hash.
pub fn set_known_head(&mut self, number: u64, hash: B256) {
if number > self.known_head {
self.known_head = number;
self.known_head_hash = hash;
}
}
/// Processes a single event, buffering it into the tracker's state.
pub(crate) fn buffer_event(&mut self, event: SnapSyncEvent) {
match event {
SnapSyncEvent::NewBlock { number, state_root, bal, .. } => {
self.buffered_blocks.insert(number, BufferedBlock { state_root, bal });
}
SnapSyncEvent::DownloadedBlock { number, state_root, .. } => {
self.buffered_blocks.insert(number, BufferedBlock { state_root, bal: None });
}
SnapSyncEvent::NewHead { head_hash } => {
// Hash-only: we don't update known_head number here.
// The orchestrator resolves the number from peers at bootstrap.
self.known_head_hash = head_hash;
}
}
}
/// Drains all pending events from the engine channel (non-blocking).
pub fn drain_events(&mut self) {
while let Ok(event) = self.events_rx.try_recv() {
self.buffer_event(event);
}
}
/// Advances the pivot to the latest known head without applying BAL diffs.
///
/// This only bumps the pivot block/root so that subsequent download requests
/// use a fresh root the serving peer can satisfy. BAL healing is done once
/// after all downloading completes (Phase 2 in the orchestrator).
///
/// Returns `Ok(true)` if the pivot was advanced, `Ok(false)` if already at head.
pub async fn advance_pivot<C, F>(
&mut self,
client: &C,
factory: &F,
) -> Result<bool, SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
self.drain_events();
let new_pivot = self.known_head.saturating_sub(crate::PIVOT_OFFSET);
if new_pivot <= self.pivot_block {
return Ok(false);
}
let old_pivot = self.pivot_block;
let new_root = self.resolve_state_root(client, factory, new_pivot).await?;
self.pivot_block = new_pivot;
self.pivot_root = new_root;
self.buffered_blocks = self.buffered_blocks.split_off(&(new_pivot.saturating_sub(10)));
tracing::info!(target: "engine::snap_sync", old_pivot, new_pivot, %new_root, "Advanced pivot");
Ok(true)
}
/// Gets and verifies a block BAL, checking the buffer first then fetching from peers.
pub(crate) async fn get_verified_bal<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<VerifiedBal, SnapSyncError>
where
C: SnapClient + HeadersClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let (block_hash, expected_hash) =
self.resolve_header_hash(client, factory, block_num).await?;
let bal = if let Some(block) = self.buffered_blocks.get(&block_num) {
block.bal.clone()
} else {
None
};
let bal = match bal {
Some(bal) => bal,
None => {
let response = client
.get_snap_block_access_lists(GetBlockAccessListsMessage {
request_id: 0,
block_hashes: vec![block_hash],
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
})
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"snap/2 BAL fetch for block {block_num}: {e}"
))
})?;
let SnapResponse::BlockAccessLists(message) = response.into_data() else {
return Err(SnapSyncError::Network(format!(
"peer returned non-BAL snap response for block {block_num}"
)));
};
let bal = message
.block_access_lists
.0
.into_iter()
.next()
.ok_or(SnapSyncError::MissingBal(block_num))?;
if bal.as_ref() == [alloy_rlp::EMPTY_STRING_CODE] {
return Err(SnapSyncError::MissingBal(block_num));
}
bal
}
};
let account_changes: Vec<AccountChanges> = Vec::<AccountChanges>::decode(&mut bal.as_ref())
.map_err(|e| {
SnapSyncError::RlpDecode(format!("BAL decode at block {block_num}: {e}"))
})?;
let got = compute_block_access_list_hash(&account_changes);
if got != expected_hash {
return Err(SnapSyncError::BalVerification {
block: block_num,
expected: expected_hash,
got,
});
}
Ok(VerifiedBal { bytes: bal, account_changes })
}
async fn resolve_header_hash<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<(B256, B256), SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let local = factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(block_num).ok().flatten());
match local {
Some(header) => {
let expected = header.block_access_list_hash().unwrap_or_default();
Ok((SealedHeader::seal_slow(header).hash(), expected))
}
None => {
let header = client
.get_header(BlockHashOrNumber::Number(block_num))
.await
.map_err(|e| {
SnapSyncError::Network(format!("header fetch for block {block_num}: {e}"))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(block_num))?;
let expected = header.block_access_list_hash().unwrap_or_default();
Ok((SealedHeader::seal_slow(header).hash(), expected))
}
}
}
/// Resolves the state root for a block number from the buffer, local DB, or peers.
async fn resolve_state_root<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<B256, SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
if let Some(block) = self.buffered_blocks.get(&block_num) {
return Ok(block.state_root);
}
// Try local DB first, fall back to fetching header from peers
let local = factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(block_num).ok().flatten());
match local {
Some(header) => Ok(header.state_root()),
None => {
let header = client
.get_header(BlockHashOrNumber::Number(block_num))
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"pivot header fetch for block {block_num}: {e}"
))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(block_num))?;
Ok(header.state_root())
}
}
}
}

View File

@@ -1,440 +0,0 @@
//! Snap range proof verification.
//!
//! Snap range responses prove a consecutive leaf range with the boundary trie
//! nodes that connect the returned leaves to the rest of the trie. Verifying
//! only the first and last leaf is not enough: an adversarial peer could omit a
//! leaf in the middle while still proving both endpoints. The verifier below
//! reconstructs the trie root from returned leaves plus proof subtrees that are
//! outside the proven range.
use alloy_primitives::{Bytes, B256};
use alloy_rlp::Decodable;
use reth_trie::{HashBuilder, Nibbles, RlpNode, TrieNode, EMPTY_ROOT_HASH};
use std::collections::HashMap;
const KEY_NIBBLES: usize = 64;
const MAX_HASH: B256 = B256::new([0xff; 32]);
/// Error returned when a snap range proof is invalid.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum RangeProofError {
/// The response leaves were not strictly increasing by hashed key.
#[error("range leaves are not strictly increasing")]
NonMonotonicLeaves,
/// A returned leaf is before the requested range origin.
#[error("range leaf {key} is before origin {origin}")]
LeafBeforeOrigin {
/// The invalid leaf key.
key: B256,
/// Requested range origin.
origin: B256,
},
/// A proof node needed to reconstruct the trie boundary was missing.
#[error("missing proof node at path {path:?}")]
MissingProofNode {
/// Trie path whose node reference was required.
path: Nibbles,
},
/// A decoded proof path exceeded the fixed 32-byte hashed-key length.
#[error("proof path {path:?} exceeds hashed key length")]
PathTooLong {
/// Invalid trie path.
path: Nibbles,
},
/// A leaf proof path did not resolve to a full 32-byte hashed key.
#[error("leaf proof path {path:?} does not resolve to a full hashed key")]
InvalidLeafPath {
/// Invalid trie path.
path: Nibbles,
},
/// The reconstructed frontier contained duplicate paths.
#[error("range proof frontier contains duplicate path {path:?}")]
DuplicateFrontierPath {
/// Duplicate trie path.
path: Nibbles,
},
/// The reconstructed root does not match the expected trie root.
#[error("range proof root mismatch: expected {expected}, got {got}")]
RootMismatch {
/// Expected trie root.
expected: B256,
/// Root reconstructed from leaves and proof frontier.
got: B256,
},
/// A trie node failed to decode.
#[error(transparent)]
Rlp(#[from] alloy_rlp::Error),
}
/// Verifies that `leaves` are complete from `origin` through the last returned
/// leaf, or through the end of the trie when no leaves were returned.
pub(crate) fn verify_range_proof<I, V>(
root: B256,
origin: B256,
leaves: I,
proof: &[Bytes],
) -> Result<(), RangeProofError>
where
I: IntoIterator<Item = (B256, V)>,
V: AsRef<[u8]>,
{
let mut frontier = Vec::new();
let mut previous = None;
let mut last_key = None;
for (key, value) in leaves {
if key < origin {
return Err(RangeProofError::LeafBeforeOrigin { key, origin })
}
if previous.is_some_and(|previous| key <= previous) {
return Err(RangeProofError::NonMonotonicLeaves)
}
previous = Some(key);
last_key = Some(key);
frontier.push(FrontierEntry::Leaf {
path: Nibbles::unpack(key),
value: value.as_ref().to_vec(),
});
}
if root == EMPTY_ROOT_HASH {
if frontier.is_empty() {
return Ok(())
}
return Err(RangeProofError::RootMismatch { expected: root, got: frontier_root(frontier)? })
}
if !proof.is_empty() && !proof_is_empty_root(proof) {
let proof_by_reference = proof
.iter()
.map(|node| (RlpNode::from_rlp(node).as_slice().to_vec(), node.as_ref()))
.collect::<HashMap<_, _>>();
let left = Nibbles::unpack(origin);
let right = Nibbles::unpack(last_key.unwrap_or(MAX_HASH));
visit_reference(
Nibbles::new(),
&RlpNode::word_rlp(&root),
&left,
&right,
&proof_by_reference,
&mut frontier,
)?;
}
let got = frontier_root(frontier)?;
if got != root {
return Err(RangeProofError::RootMismatch { expected: root, got })
}
Ok(())
}
fn proof_is_empty_root(proof: &[Bytes]) -> bool {
proof.len() == 1 && proof[0].as_ref() == [alloy_rlp::EMPTY_STRING_CODE]
}
fn visit_reference(
prefix: Nibbles,
reference: &RlpNode,
left: &Nibbles,
right: &Nibbles,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match subtree_relation(&prefix, left, right)? {
SubtreeRelation::Outside => add_outside_reference(prefix, reference, frontier),
SubtreeRelation::Inside => Ok(()),
SubtreeRelation::Boundary => {
let node = resolve_reference(prefix, reference, proof_by_reference)?;
visit_node(node, prefix, left, right, proof_by_reference, frontier)
}
}
}
fn visit_node(
node: TrieNode,
prefix: Nibbles,
left: &Nibbles,
right: &Nibbles,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match node {
TrieNode::EmptyRoot => Ok(()),
TrieNode::Leaf(leaf) => {
let path = join_path(prefix, &leaf.key)?;
if path.len() != KEY_NIBBLES {
return Err(RangeProofError::InvalidLeafPath { path })
}
if key_in_range(&path, left, right) {
Ok(())
} else {
frontier.push(FrontierEntry::Leaf { path, value: leaf.value });
Ok(())
}
}
TrieNode::Extension(extension) => visit_reference(
join_path(prefix, &extension.key)?,
&extension.child,
left,
right,
proof_by_reference,
frontier,
),
TrieNode::Branch(branch) => {
for (nibble, child) in branch
.as_ref()
.children()
.filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
{
let mut child_prefix = prefix;
child_prefix.push(nibble);
visit_reference(child_prefix, child, left, right, proof_by_reference, frontier)?;
}
Ok(())
}
}
}
fn add_outside_reference(
prefix: Nibbles,
reference: &RlpNode,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: prefix })
}
if let Some(hash) = reference.as_hash() {
frontier.push(FrontierEntry::Subtree { path: prefix, hash });
return Ok(())
}
add_outside_node(TrieNode::decode(&mut reference.as_slice())?, prefix, frontier)
}
fn add_outside_node(
node: TrieNode,
prefix: Nibbles,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match node {
TrieNode::EmptyRoot => Ok(()),
TrieNode::Leaf(leaf) => {
let path = join_path(prefix, &leaf.key)?;
if path.len() != KEY_NIBBLES {
return Err(RangeProofError::InvalidLeafPath { path })
}
frontier.push(FrontierEntry::Leaf { path, value: leaf.value });
Ok(())
}
TrieNode::Extension(extension) => {
add_outside_reference(join_path(prefix, &extension.key)?, &extension.child, frontier)
}
TrieNode::Branch(branch) => {
for (nibble, child) in branch
.as_ref()
.children()
.filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
{
let mut child_prefix = prefix;
child_prefix.push(nibble);
add_outside_reference(child_prefix, child, frontier)?;
}
Ok(())
}
}
}
fn resolve_reference(
path: Nibbles,
reference: &RlpNode,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
) -> Result<TrieNode, RangeProofError> {
if !reference.is_hash() {
return Ok(TrieNode::decode(&mut reference.as_slice())?)
}
let Some(node) = proof_by_reference.get(reference.as_slice()) else {
return Err(RangeProofError::MissingProofNode { path })
};
Ok(TrieNode::decode(&mut &node[..])?)
}
fn join_path(mut prefix: Nibbles, suffix: &Nibbles) -> Result<Nibbles, RangeProofError> {
prefix.extend(suffix);
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: prefix })
}
Ok(prefix)
}
fn frontier_root(mut frontier: Vec<FrontierEntry>) -> Result<B256, RangeProofError> {
frontier.sort_unstable_by_key(|entry| entry.path());
let mut builder = HashBuilder::default();
let mut previous = None;
for entry in frontier {
let path = entry.path();
if previous.is_some_and(|previous| path <= previous) {
return Err(RangeProofError::DuplicateFrontierPath { path })
}
previous = Some(path);
match entry {
FrontierEntry::Leaf { path, value } => builder.add_leaf(path, &value),
FrontierEntry::Subtree { path, hash } => builder.add_branch(path, hash, false),
}
}
Ok(builder.root())
}
fn subtree_relation(
prefix: &Nibbles,
left: &Nibbles,
right: &Nibbles,
) -> Result<SubtreeRelation, RangeProofError> {
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: *prefix })
}
let min = padded_path(prefix, 0);
let max = padded_path(prefix, 0x0f);
let left = padded_path(left, 0);
let right = padded_path(right, 0x0f);
if max < left || min > right {
Ok(SubtreeRelation::Outside)
} else if min >= left && max <= right {
Ok(SubtreeRelation::Inside)
} else {
Ok(SubtreeRelation::Boundary)
}
}
fn key_in_range(key: &Nibbles, left: &Nibbles, right: &Nibbles) -> bool {
let key = padded_path(key, 0);
key >= padded_path(left, 0) && key <= padded_path(right, 0x0f)
}
fn padded_path(path: &Nibbles, fill: u8) -> [u8; KEY_NIBBLES] {
let mut padded = [fill; KEY_NIBBLES];
for (idx, nibble) in padded.iter_mut().enumerate().take(path.len()) {
*nibble = path.get(idx).expect("idx is below path length");
}
padded
}
#[derive(Clone, Debug)]
enum FrontierEntry {
Leaf { path: Nibbles, value: Vec<u8> },
Subtree { path: Nibbles, hash: B256 },
}
impl FrontierEntry {
const fn path(&self) -> Nibbles {
match self {
Self::Leaf { path, .. } | Self::Subtree { path, .. } => *path,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SubtreeRelation {
Outside,
Boundary,
Inside,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_trie::proof::ProofRetainer;
use reth_trie::HashBuilder;
fn b256(value: u64) -> B256 {
B256::left_padding_from(&value.to_be_bytes())
}
fn value(byte: u8) -> Vec<u8> {
vec![byte; 64]
}
fn build_proof(leaves: &[(B256, Vec<u8>)], targets: &[B256]) -> (B256, Vec<Bytes>) {
let targets = targets.iter().copied().map(Nibbles::unpack).collect();
let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(targets));
for (key, value) in leaves {
builder.add_leaf(Nibbles::unpack(*key), value);
}
let root = builder.root();
let proof = builder
.take_proof_nodes()
.into_nodes_sorted()
.into_iter()
.map(|(_, node)| node)
.collect();
(root, proof)
}
#[test]
fn complete_range_accepts_boundary_multiproof() {
let leaves = vec![
(b256(1), value(1)),
(b256(2), value(2)),
(b256(3), value(3)),
(b256(4), value(4)),
];
let returned = leaves[1..=3].to_vec();
let (root, proof) = build_proof(&leaves, &[b256(2), b256(4)]);
verify_range_proof(root, b256(2), returned, &proof).unwrap();
}
#[test]
fn proof_free_full_range_verifies_from_leaves() {
let leaves = vec![(b256(1), value(1)), (b256(2), value(2)), (b256(3), value(3))];
let (root, _) = build_proof(&leaves, &[]);
verify_range_proof(root, B256::ZERO, leaves, &[]).unwrap();
}
#[test]
fn range_rejects_omitted_interior_leaf() {
let leaves = vec![
(b256(1), value(1)),
(b256(2), value(2)),
(b256(3), value(3)),
(b256(4), value(4)),
];
let returned = vec![(b256(2), value(2)), (b256(4), value(4))];
let (root, proof) = build_proof(&leaves, &[b256(2), b256(4)]);
assert!(matches!(
verify_range_proof(root, b256(2), returned, &proof),
Err(RangeProofError::RootMismatch { .. })
));
}
#[test]
fn empty_tail_range_accepts_absence_proof() {
let leaves = vec![(b256(1), value(1)), (b256(2), value(2))];
let (root, proof) = build_proof(&leaves, &[b256(3)]);
verify_range_proof(root, b256(3), std::iter::empty::<(B256, Vec<u8>)>(), &proof).unwrap();
}
#[test]
fn empty_range_rejects_omitted_right_leaf() {
let leaves = vec![(b256(1), value(1)), (b256(3), value(3))];
let (root, proof) = build_proof(&leaves, &[b256(2)]);
assert!(matches!(
verify_range_proof(root, b256(2), std::iter::empty::<(B256, Vec<u8>)>(), &proof),
Err(RangeProofError::RootMismatch { .. })
));
}
}

View File

@@ -1,481 +0,0 @@
//! Snap protocol state provider backed by a chain-aware provider.
//!
//! Serves recent historical state for request roots by applying a revert overlay
//! on top of the current MDBX hashed state. This keeps the served state fully
//! persisted and deterministic.
use alloy_consensus::{BlockHeader, EMPTY_ROOT_HASH};
use alloy_primitives::{map::B256Map, Bytes, B256};
use reth_db_api::transaction::DbTx;
use reth_eth_wire_types::{
snap::{AccountData, StorageData},
BlockAccessLists,
};
use reth_network_p2p::snap::server::SnapStateProvider;
use reth_provider::LatestStateProviderRef;
use reth_stages_types::StageId;
use reth_storage_api::{
BalProvider, BlockHashReader, BlockNumReader, BytecodeReader, ChangeSetReader, DBProvider,
DatabaseProviderFactory, HeaderProvider, StageCheckpointReader, StorageChangeSetReader,
StorageSettingsCache,
};
use reth_trie::{
hashed_cursor::{HashedCursor, HashedCursorFactory, HashedPostStateCursorFactory},
prefix_set::PrefixSetMut,
proof::{Proof, StorageProof},
HashedPostStateSorted, HashedStorageSorted, MultiProofTargets, Nibbles,
};
use reth_trie_db::{
DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseTrieCursorFactory,
};
use std::sync::atomic::{AtomicU64, Ordering};
/// Maximum accounts to return per snap request.
const MAX_ACCOUNTS_SERVE: usize = 4096;
/// Default maximum number of recent blocks to scan when resolving a root hash.
const DEFAULT_MAX_SERVING_LOOKBACK: u64 = 128;
/// Maximum number of recent blocks to scan when resolving a root hash.
static MAX_SERVING_LOOKBACK: AtomicU64 = AtomicU64::new(DEFAULT_MAX_SERVING_LOOKBACK);
/// Snap state provider that wraps a chain-aware provider and serves historical
/// state via a revert overlay.
///
/// The provider `P` must implement [`BlockNumReader`] and [`HeaderProvider`]
/// directly so request roots can be resolved against the canonical in-memory
/// tip, not just static-file-persisted blocks. In practice, pass a
/// `BlockchainProvider`.
pub struct ProviderSnapState<P> {
provider: P,
}
impl<P> ProviderSnapState<P> {
/// Create a new snap state provider.
pub const fn new(provider: P) -> Self {
Self { provider }
}
}
impl<P> core::fmt::Debug for ProviderSnapState<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ProviderSnapState").finish()
}
}
/// Guard that restores the previous snap serving lookback when dropped.
#[cfg(any(test, feature = "test-utils"))]
#[derive(Debug)]
pub struct MaxServingLookbackGuard {
previous: u64,
}
#[cfg(any(test, feature = "test-utils"))]
impl Drop for MaxServingLookbackGuard {
fn drop(&mut self) {
MAX_SERVING_LOOKBACK.store(self.previous, Ordering::Relaxed);
}
}
/// Overrides the serving lookback for tests.
#[cfg(any(test, feature = "test-utils"))]
pub fn set_max_serving_lookback_for_tests(lookback: u64) -> MaxServingLookbackGuard {
let previous = MAX_SERVING_LOOKBACK.swap(lookback, Ordering::Relaxed);
MaxServingLookbackGuard { previous }
}
impl<P> ProviderSnapState<P>
where
P: HeaderProvider + BlockNumReader,
{
/// Scan recent headers for one whose state root matches `root_hash` and
/// return its block number.
fn resolve_serving_block(&self, root_hash: B256) -> Option<u64> {
let tip = self.provider.best_block_number().ok()?;
let start = tip.saturating_sub(MAX_SERVING_LOOKBACK.load(Ordering::Relaxed));
for num in (start..=tip).rev() {
if let Ok(Some(header)) = self.provider.header_by_number(num) {
if header.state_root() == root_hash {
return Some(num);
}
}
}
None
}
}
impl<P> ProviderSnapState<P>
where
P: DatabaseProviderFactory + HeaderProvider + BlockNumReader,
P::Provider: DBProvider
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ BlockHashReader
+ BlockNumReader
+ StorageSettingsCache,
<P::Provider as DBProvider>::Tx: DbTx,
{
fn database_provider_with_reverts(
&self,
root_hash: B256,
) -> Option<(P::Provider, HashedPostStateSorted)> {
let serving_block = self.resolve_serving_block(root_hash)?;
let provider = self.provider.database_provider_ro().ok()?;
let persisted = provider.get_stage_checkpoint(StageId::Execution).ok()??.block_number;
let revert_state = if persisted > serving_block {
HashedPostStateSorted::from_reverts(&provider, (serving_block + 1)..=persisted).ok()?
} else {
HashedPostStateSorted::default()
};
Some((provider, revert_state))
}
}
impl<P> SnapStateProvider for ProviderSnapState<P>
where
P: DatabaseProviderFactory
+ HeaderProvider
+ BlockNumReader
+ BalProvider
+ Send
+ Sync
+ 'static,
P::Provider: DBProvider
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ BlockNumReader
+ StorageSettingsCache,
<P::Provider as DBProvider>::Tx: DbTx,
{
fn account_range(
&self,
root_hash: B256,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<AccountData>, Vec<Bytes>) {
let empty = (Vec::new(), Vec::new());
let Some((provider, revert_state)) = self.database_provider_with_reverts(root_hash) else {
return empty;
};
let cursor_factory = HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
&revert_state,
);
let Ok(mut cursor) = cursor_factory.hashed_account_cursor() else { return empty };
let mut raw_accounts = Vec::new();
let mut total_bytes: u64 = 0;
if let Ok(Some((hash, account))) = cursor.seek(starting_hash) {
let body_len = snap_account_body_len_upper_bound(account);
total_bytes += body_len as u64 + 32;
raw_accounts.push((hash, account));
if hash < limit_hash {
while raw_accounts.len() < MAX_ACCOUNTS_SERVE && total_bytes < response_bytes {
match cursor.next() {
Ok(Some((hash, account))) if hash < limit_hash => {
let body_len = snap_account_body_len_upper_bound(account);
total_bytes += body_len as u64 + 32;
raw_accounts.push((hash, account));
}
_ => break,
}
}
}
}
let Some((storage_roots, proof)) =
account_range_proof(&provider, &revert_state, starting_hash, &raw_accounts)
else {
return empty;
};
let accounts = raw_accounts
.into_iter()
.map(|(hash, account)| {
let storage_root = storage_roots.get(&hash).copied().unwrap_or(EMPTY_ROOT_HASH);
AccountData { hash, account: account.into_trie_account(storage_root) }
})
.collect();
(accounts, proof)
}
fn storage_ranges(
&self,
root_hash: B256,
account_hashes: Vec<B256>,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<Vec<StorageData>>, Vec<Bytes>) {
let empty = (Vec::new(), Vec::new());
let Some((provider, revert_state)) = self.database_provider_with_reverts(root_hash) else {
return empty;
};
let cursor_factory = HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
&revert_state,
);
if !requested_accounts_available(&cursor_factory, &account_hashes).unwrap_or(false) {
return empty;
}
let mut all_slots: Vec<Vec<StorageData>> = Vec::new();
let mut total_bytes: u64 = 0;
let mut partial_range = None;
for (i, account_hash) in account_hashes.iter().enumerate() {
let prior_slots_returned = all_slots.iter().any(|slots| !slots.is_empty());
if total_bytes >= response_bytes && prior_slots_returned {
break;
}
let mut slots = Vec::new();
let start = if i == 0 { starting_hash } else { B256::ZERO };
let Ok(mut cursor) = cursor_factory.hashed_storage_cursor(*account_hash) else {
all_slots.push(slots);
continue;
};
if let Ok(Some((key, value))) = cursor.seek(start) &&
key < limit_hash &&
!value.is_zero()
{
let slot = StorageData::from_value(key, value);
total_bytes += slot.data.len() as u64 + 32;
slots.push(slot);
}
while total_bytes < response_bytes || (!prior_slots_returned && slots.is_empty()) {
match cursor.next() {
Ok(Some((key, value))) if key < limit_hash => {
if value.is_zero() {
continue;
}
let slot = StorageData::from_value(key, value);
total_bytes += slot.data.len() as u64 + 32;
slots.push(slot);
}
_ => break,
}
}
if total_bytes >= response_bytes &&
storage_has_more_slots(&mut cursor, limit_hash).unwrap_or(false)
{
partial_range = Some((*account_hash, start, slots.last().map(|slot| slot.hash)));
}
all_slots.push(slots);
if partial_range.is_some() {
break;
}
}
let proof = partial_range
.and_then(|(account_hash, start, last_hash)| {
storage_range_proof(&provider, &revert_state, account_hash, start, last_hash)
})
.unwrap_or_default();
(all_slots, proof)
}
fn bytecodes(&self, hashes: Vec<B256>, response_bytes: u64) -> Vec<Bytes> {
let Ok(provider) = self.provider.database_provider_ro() else {
return Vec::new();
};
let mut out = Vec::new();
let mut total: u64 = 0;
let state = LatestStateProviderRef::new(&provider);
for hash in hashes {
if total >= response_bytes {
break;
}
if let Ok(Some(code)) = state.bytecode_by_hash(&hash) {
let bytes = code.original_bytes();
total += bytes.len() as u64;
out.push(bytes);
}
}
out
}
fn block_access_lists(&self, block_hashes: Vec<B256>, response_bytes: u64) -> BlockAccessLists {
serve_block_access_lists(&self.provider, block_hashes, response_bytes)
}
}
fn account_range_proof<Provider>(
provider: &Provider,
revert_state: &HashedPostStateSorted,
starting_hash: B256,
accounts: &[(B256, reth_primitives_traits::Account)],
) -> Option<(B256Map<B256>, Vec<Bytes>)>
where
Provider: DBProvider + StorageSettingsCache,
Provider::Tx: DbTx,
{
reth_trie_db::with_adapter!(provider, |A| {
let mut targets = MultiProofTargets::accounts([starting_hash]);
targets.extend(MultiProofTargets::accounts(accounts.iter().map(|(hash, _)| *hash)));
let multiproof = Proof::new(
DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()),
HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
revert_state,
),
)
.with_prefix_sets_mut(revert_state.construct_prefix_sets())
.multiproof(targets)
.ok()?;
let storage_roots =
multiproof.storages.iter().map(|(hash, proof)| (*hash, proof.root)).collect();
let proof = multiproof
.account_subtree
.into_nodes_sorted()
.into_iter()
.map(|(_, node)| node)
.collect();
Some((storage_roots, proof))
})
}
fn requested_accounts_available<CF>(
cursor_factory: &CF,
account_hashes: &[B256],
) -> Result<bool, reth_db_api::DatabaseError>
where
CF: HashedCursorFactory,
{
let mut cursor = cursor_factory.hashed_account_cursor()?;
for account_hash in account_hashes {
if !matches!(cursor.seek(*account_hash)?, Some((hash, _)) if hash == *account_hash) {
return Ok(false);
}
cursor.reset();
}
Ok(true)
}
fn snap_account_body_len_upper_bound(account: reth_primitives_traits::Account) -> usize {
AccountData::account_body_len(account.into_trie_account(B256::ZERO))
}
fn storage_range_proof<Provider>(
provider: &Provider,
revert_state: &HashedPostStateSorted,
account_hash: B256,
starting_hash: B256,
last_hash: Option<B256>,
) -> Option<Vec<Bytes>>
where
Provider: DBProvider + StorageSettingsCache,
Provider::Tx: DbTx,
{
reth_trie_db::with_adapter!(provider, |A| {
let targets = last_hash.map_or_else(
|| alloy_primitives::map::B256Set::from_iter([starting_hash]),
|last_hash| alloy_primitives::map::B256Set::from_iter([starting_hash, last_hash]),
);
let multiproof = StorageProof::new_hashed(
DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()),
HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
revert_state,
),
account_hash,
)
.with_prefix_set_mut(storage_prefix_set_mut(revert_state, account_hash))
.storage_multiproof(targets)
.ok()?;
Some(multiproof.subtree.into_nodes_sorted().into_iter().map(|(_, node)| node).collect())
})
}
fn storage_prefix_set_mut(
revert_state: &HashedPostStateSorted,
account_hash: B256,
) -> PrefixSetMut {
match revert_state.account_storages().get(&account_hash) {
Some(storage) => storage_prefix_set_from_sorted(storage),
None => PrefixSetMut::default(),
}
}
fn storage_prefix_set_from_sorted(storage: &HashedStorageSorted) -> PrefixSetMut {
if storage.wiped {
return PrefixSetMut::all();
}
let mut prefix_set = PrefixSetMut::with_capacity(storage.storage_slots.len());
prefix_set.extend_keys(storage.storage_slots.iter().map(|(slot, _)| Nibbles::unpack(slot)));
prefix_set
}
fn storage_has_more_slots<C>(
cursor: &mut C,
limit_hash: B256,
) -> Result<bool, reth_db_api::DatabaseError>
where
C: HashedCursor<Value = alloy_primitives::U256>,
{
while let Some((key, value)) = cursor.next()? {
if key >= limit_hash {
return Ok(false);
}
if !value.is_zero() {
return Ok(true);
}
}
Ok(false)
}
fn serve_block_access_lists<P>(
provider: &P,
block_hashes: Vec<B256>,
response_bytes: u64,
) -> BlockAccessLists
where
P: BalProvider,
{
let results = match provider.bal_store().get_by_hashes(&block_hashes) {
Ok(results) => results,
Err(_) => return BlockAccessLists(Vec::new()),
};
let mut total_bytes = 0u64;
let mut out = Vec::new();
for bal in results {
let bal = bal.unwrap_or_else(|| Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE]));
total_bytes += bal.len() as u64;
out.push(bal);
if total_bytes >= response_bytes {
break;
}
}
BlockAccessLists(out)
}

View File

@@ -1,184 +0,0 @@
//! MDBX read/write helpers for hashed state and bytecodes.
use crate::SnapSyncError;
use alloy_primitives::{map::B256Map, Bytes, B256, U256};
use reth_db_api::{
tables,
transaction::{DbTx, DbTxMut},
};
use reth_primitives_traits::{Account, Bytecode};
use reth_provider::DatabaseProviderFactory;
use reth_storage_api::{DBProvider, StateWriter};
use reth_trie::{HashedPostStateSorted, HashedStorageSorted};
// ──────────────────────────────────────────────────────────────────────────────
// MDBX write helpers
// ──────────────────────────────────────────────────────────────────────────────
/// Clears all hashed state tables.
pub(crate) fn clear_hashed_state<F>(factory: &F) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
{
let tx = provider.tx_ref();
tx.clear::<tables::HashedAccounts>().map_err(db_err)?;
tx.clear::<tables::HashedStorages>().map_err(db_err)?;
tx.clear::<tables::AccountsTrie>().map_err(db_err)?;
tx.clear::<tables::StoragesTrie>().map_err(db_err)?;
tx.clear::<tables::Bytecodes>().map_err(db_err)?;
}
provider.commit().map_err(db_err)?;
Ok(())
}
/// Reads a single hashed account from the database.
pub(crate) fn read_hashed_account<F>(
factory: &F,
hashed_address: B256,
) -> Result<Option<Account>, SnapSyncError>
where
F: DatabaseProviderFactory,
F::Provider: DBProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let provider = factory.database_provider_ro().map_err(db_err)?;
let tx = provider.tx_ref();
tx.get::<tables::HashedAccounts>(hashed_address).map_err(db_err)
}
/// Writes a batch of hashed accounts.
pub(crate) fn write_hashed_accounts<F>(
factory: &F,
accounts: &[(B256, Account)],
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut accounts_by_hash = B256Map::default();
for (hash, account) in accounts {
accounts_by_hash.insert(*hash, Some(*account));
}
let mut accounts: Vec<_> = accounts_by_hash.into_iter().collect();
accounts.sort_by_key(|(hash, _)| *hash);
write_hashed_state(factory, HashedPostStateSorted::new(accounts, B256Map::default()))
}
/// Writes a batch of hashed storage entries.
pub(crate) fn write_hashed_storages<F>(
factory: &F,
entries: &[(B256, B256, U256)],
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut slots_by_account: B256Map<B256Map<U256>> = B256Map::default();
for &(account_hash, slot_hash, value) in entries {
slots_by_account.entry(account_hash).or_default().insert(slot_hash, value);
}
let storages = slots_by_account
.into_iter()
.map(|(account_hash, slots)| {
let mut storage_slots: Vec<_> = slots.into_iter().collect();
storage_slots.sort_by_key(|(slot_hash, _)| *slot_hash);
(account_hash, HashedStorageSorted { storage_slots, wiped: false })
})
.collect();
write_hashed_state(factory, HashedPostStateSorted::new(Vec::new(), storages))
}
fn write_hashed_state<F>(
factory: &F,
hashed_state: HashedPostStateSorted,
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
provider.write_hashed_state(&hashed_state).map_err(db_err)?;
provider.commit().map_err(db_err)?;
Ok(())
}
/// Writes a batch of bytecodes.
pub(crate) fn write_bytecodes<F>(factory: &F, codes: &[(B256, Bytes)]) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
provider
.write_bytecodes(
codes
.iter()
.filter(|(_, code)| !code.is_empty())
.map(|(hash, code)| (*hash, Bytecode::new_raw(code.clone()))),
)
.map_err(db_err)?;
provider.commit().map_err(db_err)?;
Ok(())
}
/// Increment a [`B256`] by 1 for pagination.
pub(crate) fn increment_b256(hash: B256) -> B256 {
let mut bytes = hash.0;
for byte in bytes.iter_mut().rev() {
if *byte == 0xff {
*byte = 0;
} else {
*byte += 1;
return B256::from(bytes);
}
}
B256::ZERO
}
pub(crate) fn db_err(e: impl std::error::Error + Send + Sync + 'static) -> SnapSyncError {
SnapSyncError::Database(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment_b256_simple() {
let hash = B256::ZERO;
let next = increment_b256(hash);
let mut expected = [0u8; 32];
expected[31] = 1;
assert_eq!(next, B256::from(expected));
}
#[test]
fn test_increment_b256_carry() {
let mut bytes = [0u8; 32];
bytes[31] = 0xff;
let hash = B256::from(bytes);
let next = increment_b256(hash);
let mut expected = [0u8; 32];
expected[30] = 1;
assert_eq!(next, B256::from(expected));
}
#[test]
fn test_increment_b256_max() {
let hash = B256::from([0xff; 32]);
let next = increment_b256(hash);
assert_eq!(next, B256::ZERO);
}
}

View File

@@ -17,7 +17,6 @@ reth-chainspec = { workspace = true, optional = true }
reth-consensus.workspace = true
reth-db.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-engine-snap.workspace = true
reth-execution-cache.workspace = true
reth-errors.workspace = true
reth-execution-types.workspace = true
@@ -30,7 +29,6 @@ reth-ethereum-primitives.workspace = true
reth-provider.workspace = true
reth-prune.workspace = true
reth-revm = { workspace = true, features = ["optional-balance-check"] }
reth-stages = { workspace = true, optional = true }
reth-stages-api.workspace = true
reth-tasks = { workspace = true, features = ["rayon"] }
reth-trie-parallel.workspace = true
@@ -72,6 +70,7 @@ crossbeam-channel.workspace = true
# optional deps for test-utils
reth-prune-types = { workspace = true, optional = true }
reth-stages = { workspace = true, optional = true }
reth-static-file = { workspace = true, optional = true }
reth-tracing = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
@@ -120,7 +119,6 @@ test-utils = [
"reth-prune-types",
"reth-prune-types?/test-utils",
"reth-revm/test-utils",
"reth-stages",
"reth-stages-api/test-utils",
"reth-stages/test-utils",
"reth-static-file",
@@ -136,7 +134,6 @@ test-utils = [
"reth-evm-ethereum/test-utils",
"reth-tasks/test-utils",
"reth-execution-cache/test-utils",
"reth-engine-snap/test-utils",
]
trie-debug = [
"reth-trie-sparse/trie-debug",

View File

@@ -8,14 +8,10 @@
//! These modes are mutually exclusive and the node can only be in one mode at a time.
use futures::FutureExt;
use reth_engine_snap::controller::{SnapSyncControl, SnapSyncControlEvent, SnapSyncController};
use reth_provider::{providers::ProviderNodeTypes, ProviderFactory};
use reth_provider::providers::ProviderNodeTypes;
use reth_stages_api::{ControlFlow, Pipeline, PipelineError, PipelineTarget, PipelineWithResult};
use reth_tasks::Runtime;
use std::{
fmt,
task::{ready, Context, Poll},
};
use std::task::{ready, Context, Poll};
use tokio::sync::oneshot;
use tracing::trace;
@@ -64,11 +60,6 @@ pub trait BackfillSync: Send {
pub enum BackfillAction {
/// Start backfilling with the given target.
Start(PipelineTarget),
/// Start snap sync (for fresh nodes with no state).
///
/// Carries the target block hash from the FCU so the orchestrator can resolve
/// the head from peers if it's not available locally.
StartSnapSync(alloy_primitives::B256),
}
/// The events that can be emitted on backfill sync.
@@ -83,10 +74,6 @@ pub enum BackfillEvent {
/// Sync task was dropped after it was started, unable to receive it because
/// channel closed. This would indicate a panicked task.
TaskDropped(String),
/// Snap sync started. Contains the event sender for forwarding chain events.
SnapSyncStarted(tokio::sync::mpsc::UnboundedSender<reth_engine_snap::SnapSyncEvent>),
/// Snap sync finished.
SnapSyncFinished(Result<reth_engine_snap::SnapSyncOutcome, reth_engine_snap::SnapSyncError>),
}
/// Pipeline sync.
@@ -123,7 +110,7 @@ impl<N: ProviderNodeTypes> PipelineSync<N> {
}
/// Returns `true` if the pipeline is active.
pub(crate) const fn is_pipeline_active(&self) -> bool {
const fn is_pipeline_active(&self) -> bool {
!self.is_pipeline_idle()
}
@@ -194,7 +181,6 @@ impl<N: ProviderNodeTypes> BackfillSync for PipelineSync<N> {
fn on_action(&mut self, event: BackfillAction) {
match event {
BackfillAction::Start(target) => self.set_pipeline_sync_target(target),
BackfillAction::StartSnapSync(_) => {}
}
}
@@ -240,111 +226,6 @@ impl<N: ProviderNodeTypes> PipelineState<N> {
}
}
/// Combined backfill sync that supports both pipeline sync and snap sync.
///
/// Only one sync mode can be active at a time.
pub struct CombinedBackfillSync<N: ProviderNodeTypes, S> {
pipeline: PipelineSync<N>,
snap: S,
pending_event: Option<BackfillEvent>,
}
/// Combined backfill sync using the default provider factory snap controller.
pub type EngineBackfillSync<N, C> =
CombinedBackfillSync<N, SnapSyncController<C, ProviderFactory<N>>>;
impl<N, S> fmt::Debug for CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CombinedBackfillSync")
.field("pipeline_active", &self.pipeline.is_pipeline_active())
.field("snap_active", &self.snap.is_active())
.field("pending_event", &self.pending_event)
.finish()
}
}
impl<N, S> CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
/// Creates a new combined backfill sync with the given snap sync adapter.
pub fn with_snap(pipeline: PipelineSync<N>, snap: S) -> Self {
Self { pipeline, snap, pending_event: None }
}
}
impl<N, C, F> CombinedBackfillSync<N, SnapSyncController<C, F>>
where
N: ProviderNodeTypes,
SnapSyncController<C, F>: SnapSyncControl,
{
/// Creates a new combined backfill sync.
pub fn new(
pipeline: Pipeline<N>,
pipeline_task_spawner: Runtime,
client: C,
factory: F,
snap_runtime: Runtime,
) -> Self {
Self::with_snap(
PipelineSync::new(pipeline, pipeline_task_spawner),
SnapSyncController::new(client, factory, snap_runtime),
)
}
}
impl<N, S> BackfillSync for CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
fn on_action(&mut self, action: BackfillAction) {
match action {
BackfillAction::Start(target) => {
if self.snap.is_active() {
tracing::warn!(target: "consensus::engine::sync", "Ignoring pipeline start while snap sync is active");
return;
}
self.pipeline.on_action(BackfillAction::Start(target));
}
BackfillAction::StartSnapSync(target_hash) => {
if self.pipeline.is_pipeline_active() {
tracing::warn!(target: "consensus::engine::sync", "Ignoring snap sync start while pipeline is active");
return;
}
self.snap.start(target_hash);
self.pending_event = None;
}
}
}
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
// Return any pending event first (e.g. snap sync started)
if let Some(event) = self.pending_event.take() {
return Poll::Ready(event);
}
// Poll snap sync
if let Poll::Ready(event) = self.snap.poll(cx) {
return Poll::Ready(match event {
SnapSyncControlEvent::Started(events_tx) => {
BackfillEvent::SnapSyncStarted(events_tx)
}
SnapSyncControlEvent::Finished(result) => BackfillEvent::SnapSyncFinished(result),
SnapSyncControlEvent::TaskDropped(err) => BackfillEvent::TaskDropped(err),
});
}
// Poll pipeline
self.pipeline.poll(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -106,23 +106,6 @@ where
tracing::error!( %err, "backfill sync task dropped");
return Poll::Ready(ChainEvent::FatalError);
}
BackfillEvent::SnapSyncStarted(events_tx) => {
this.handler.on_event(FromOrchestrator::SnapSyncStarted(events_tx));
return Poll::Ready(ChainEvent::SnapSyncStarted);
}
BackfillEvent::SnapSyncFinished(result) => {
return match result {
Ok(outcome) => {
tracing::info!(?outcome, "snap sync finished");
this.handler.on_event(FromOrchestrator::SnapSyncFinished(outcome));
Poll::Ready(ChainEvent::SnapSyncFinished)
}
Err(err) => {
tracing::error!(%err, "snap sync failed");
Poll::Ready(ChainEvent::FatalError)
}
}
}
},
Poll::Pending => {}
}
@@ -177,10 +160,6 @@ pub enum ChainEvent<T> {
BackfillSyncStarted,
/// Backfill sync finished
BackfillSyncFinished,
/// Snap sync started
SnapSyncStarted,
/// Snap sync finished
SnapSyncFinished,
/// Fatal error
FatalError,
/// Event emitted by the handler
@@ -196,12 +175,6 @@ impl<T: Display> Display for ChainEvent<T> {
Self::BackfillSyncFinished => {
write!(f, "BackfillSyncFinished")
}
Self::SnapSyncStarted => {
write!(f, "SnapSyncStarted")
}
Self::SnapSyncFinished => {
write!(f, "SnapSyncFinished")
}
Self::FatalError => {
write!(f, "FatalError")
}
@@ -252,10 +225,6 @@ pub enum FromOrchestrator {
BackfillSyncFinished(ControlFlow),
/// Invoked when backfill sync started
BackfillSyncStarted,
/// Invoked when snap sync started, carries the event sender for forwarding chain events.
SnapSyncStarted(tokio::sync::mpsc::UnboundedSender<reth_engine_snap::SnapSyncEvent>),
/// Invoked when snap sync finished.
SnapSyncFinished(reth_engine_snap::SnapSyncOutcome),
/// Gracefully terminate the engine service.
///
/// When this variant is received, the engine will persist all remaining in-memory blocks

View File

@@ -5,7 +5,7 @@
//! [`ChainOrchestrator`](crate::chain::ChainOrchestrator) ready to be polled as a `Stream`.
use crate::{
backfill::EngineBackfillSync,
backfill::PipelineSync,
chain::ChainOrchestrator,
download::BasicBlockDownloader,
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
@@ -71,21 +71,15 @@ pub fn build_engine_orchestrator<N, Client, S, V, C>(
S,
BasicBlockDownloader<Client, <N::Primitives as NodePrimitives>::Block>,
>,
EngineBackfillSync<N, Client>,
PipelineSync<N>,
>
where
N: ProviderNodeTypes,
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block>
+ reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient
+ Clone
+ 'static,
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block> + 'static,
S: Stream<Item = BeaconEngineMessage<N::Payload>> + Send + Sync + Unpin + 'static,
V: EngineValidator<N::Payload> + WaitForCaches,
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
{
let snap_client = client.clone();
let snap_provider = provider.clone();
let downloader = BasicBlockDownloader::new(client, consensus.clone());
let persistence_handle =
@@ -93,7 +87,6 @@ where
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
let snap_runtime = runtime.clone();
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
blockchain_db,
consensus,
@@ -111,13 +104,7 @@ where
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
let handler = EngineHandler::new(engine_handler, downloader, incoming_requests);
let backfill_sync = EngineBackfillSync::new(
pipeline,
pipeline_task_spawner,
snap_client,
snap_provider,
snap_runtime,
);
let backfill_sync = PipelineSync::new(pipeline, pipeline_task_spawner);
ChainOrchestrator::new(handler, backfill_sync)
}

View File

@@ -29,7 +29,7 @@ use reth_primitives_traits::{
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
};
use reth_provider::{
BalProvider, BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader,
StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader,
StorageSettingsCache, TransactionVariant,
@@ -58,7 +58,6 @@ pub mod payload_processor;
pub mod payload_validator;
mod persistence_state;
pub mod precompile_cache;
mod snap;
#[cfg(test)]
mod tests;
mod trie_updates;
@@ -315,8 +314,6 @@ where
building_payload: bool,
/// Task runtime for spawning blocking work on named, reusable threads.
runtime: reth_tasks::Runtime,
/// Snap sync state and event forwarding.
snap: snap::SnapTreeState,
}
impl<N, P: Debug, T: PayloadTypes + Debug, V: Debug, C> std::fmt::Debug
@@ -344,7 +341,6 @@ where
.field("changeset_cache", &self.changeset_cache)
.field("execution_timing_stats", &self.execution_timing_stats.len())
.field("runtime", &self.runtime)
.field("snap", &self.snap)
.finish()
}
}
@@ -357,7 +353,6 @@ where
+ StateProviderFactory
+ StateReader<Receipt = N::Receipt>
+ HashedPostStateProvider
+ BalProvider
+ Clone
+ 'static,
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
@@ -410,7 +405,6 @@ where
execution_timing_stats: HashMap::new(),
building_payload: false,
runtime,
snap: snap::SnapTreeState::new(false),
}
}
@@ -451,9 +445,7 @@ where
kind,
);
let fresh_node = best_block_number == 0;
let mut task = Self::new(
let task = Self::new(
provider,
consensus,
payload_validator,
@@ -469,8 +461,6 @@ where
changeset_cache,
runtime,
);
task.snap.set_fresh_node(fresh_node);
let incoming = task.incoming_tx.clone();
spawn_os_thread("engine", || {
increase_thread_priority();
@@ -749,18 +739,11 @@ where
// This validation **MUST** be instantly run in all cases even during active sync process.
let num_hash = payload.num_hash();
// Forward to snap sync orchestrator if active
self.forward_new_block_to_snap(&payload);
let engine_event = ConsensusEngineEvent::BlockReceived(num_hash);
self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));
let block_hash = num_hash.hash;
// Extract BAL before the payload is consumed
let bal = payload.block_access_list().cloned();
// Check for invalid ancestors
if let Some(invalid) = self.find_invalid_ancestor(&payload) {
let status = self.handle_invalid_ancestor_payload(payload, invalid)?;
@@ -776,13 +759,6 @@ where
TreeOutcome::new(self.try_buffer_payload(payload)?)
};
// Cache BAL in the provider's store if the payload was accepted
if outcome.outcome.is_valid() {
if let Some(bal) = bal {
let _ = self.provider.bal_store().insert(block_hash, num_hash.number, bal);
}
}
// if the block is valid and it is the current sync target head, make it canonical
if outcome.outcome.is_valid() && self.is_sync_target_head(block_hash) {
// Only create the canonical event if this block isn't already the canonical head
@@ -1150,9 +1126,6 @@ where
// Record metrics
self.record_forkchoice_metrics();
// Forward head to snap sync orchestrator if active
self.forward_head_to_snap(state.head_block_hash);
// Pre-validation of forkchoice state
if let Some(early_result) = self.validate_forkchoice_state(state)? {
return Ok(TreeOutcome::new(early_result));
@@ -1348,17 +1321,6 @@ where
&self,
state: ForkchoiceState,
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
// For fresh nodes, trigger snap sync instead of downloading missing blocks
if self.snap.is_fresh_node() && self.backfill_sync_state.is_idle() {
debug!(target: "engine::tree", "Fresh node detected, triggering snap sync");
return Ok(TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
PayloadStatusEnum::Syncing,
)))
.with_event(TreeEvent::BackfillAction(BackfillAction::StartSnapSync(
state.head_block_hash,
))));
}
// We don't have the block to perform the forkchoice update
// We assume the FCU is valid and at least the head is missing,
// so we need to start syncing to it
@@ -1583,19 +1545,6 @@ where
}
return Ok(ops::ControlFlow::Break(()))
}
FromOrchestrator::SnapSyncStarted(events_tx) => {
debug!(target: "engine::tree", "received snap sync started event");
self.backfill_sync_state = BackfillSyncState::Active;
self.snap.start(events_tx);
// Replay latest known head if we can resolve it
if let Some(state) = self.state.forkchoice_state_tracker.sync_target_state() {
self.forward_head_to_snap(state.head_block_hash);
}
}
FromOrchestrator::SnapSyncFinished(outcome) => {
self.on_snap_sync_finished(outcome)?;
}
},
FromEngine::Request(request) => {
match request {
@@ -1968,9 +1917,37 @@ where
self.on_canonical_chain_update(chain_update);
}
self.on_canonicalized_sync_target(target);
Ok(())
}
/// Applies the tracked forkchoice state once its sync target head becomes canonical.
fn on_canonicalized_sync_target(&mut self, target: B256) {
let Some(sync_target_state) = self
.state
.forkchoice_state_tracker
.sync_target_state()
.filter(|state| state.head_block_hash == target)
else {
return;
};
if let Err(outcome) = self.ensure_consistent_forkchoice_state(sync_target_state) {
debug!(
target: "engine::tree",
head = %sync_target_state.head_block_hash,
safe = %sync_target_state.safe_block_hash,
finalized = %sync_target_state.finalized_block_hash,
?outcome,
"Canonicalized sync target head before safe/finalized could be applied"
);
return;
}
self.state.forkchoice_state_tracker.promote_sync_target_to_valid(sync_target_state);
}
/// Convenience function to handle an optional tree event.
fn on_maybe_tree_event(&mut self, event: Option<TreeEvent>) -> ProviderResult<()> {
if let Some(event) = event {
@@ -2841,10 +2818,7 @@ where
return Ok(None)
}
if self.snap.is_active() {
// During snap sync, forward downloaded blocks to the orchestrator
// for header persistence and BAL resolution
self.forward_downloaded_block_to_snap(&block);
if !self.backfill_sync_state.is_idle() {
return Ok(None)
}

View File

@@ -1159,16 +1159,19 @@ mod tests {
}
}
let mut account = revm_state::Account::default();
account.info = AccountInfo {
balance: U256::from(rng.random::<u64>()),
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
account_id: None,
let account = revm_state::Account {
info: AccountInfo {
balance: U256::from(rng.random::<u64>()),
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
account_id: None,
},
original_info: Box::new(AccountInfo::default()),
storage,
status: AccountStatus::Touched,
transaction_id: 0,
};
account.storage = storage;
account.status = AccountStatus::Touched;
state_update.insert(address, account);
}

View File

@@ -62,9 +62,7 @@ use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptR
use reth_chain_state::{
CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, ExecutionTimingStats, LazyOverlay,
};
use reth_consensus::{
validate_block_access_list_gas, ConsensusError, FullConsensus, ReceiptRootBloom,
};
use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
use reth_engine_primitives::{
ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
};
@@ -570,7 +568,7 @@ where
// The receipt root task is spawned before execution and receives receipts incrementally
// as transactions complete, allowing parallel computation during execution.
let execute_block_start = Instant::now();
let (output, senders, receipt_root_rx, built_bal) =
let (output, senders, receipt_root_rx) =
match self.execute_block(state_provider, env, &input, &mut handle) {
Ok(output) => output,
Err(err) => return self.handle_execution_error(input, err, &parent_block),
@@ -652,7 +650,6 @@ where
transaction_root,
receipt_root_bloom,
hashed_state,
built_bal
),
block
);
@@ -909,7 +906,6 @@ where
BlockExecutionOutput<N::Receipt>,
Vec<Address>,
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
Option<BlockAccessList>,
),
InsertBlockErrorKind,
>
@@ -917,29 +913,15 @@ where
S: StateProvider + Send,
Err: core::error::Error + Send + Sync + 'static,
V: PayloadValidator<T, Block = N::Block>,
T: PayloadTypes<
BuiltPayload: BuiltPayload<Primitives = N>,
ExecutionData: ExecutionPayload,
>,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
debug!(target: "engine::tree::payload_validator", "Executing block");
if let Some(bal_opt) = input.block_access_list() {
let bal = bal_opt.map_err(BlockExecutionError::other)?;
validate_block_access_list_gas(Some(&bal), input.gas_limit())
.map_err(|e| {
debug!(target: "engine::tree::payload_validator", "BAL is invalid since it contains more items than the gas limit allows");
InsertBlockErrorKind::Consensus(e)
})?
}
let has_bal = input.block_access_list().is_some();
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.with_bal_builder_if(has_bal)
.build()
});
@@ -998,7 +980,6 @@ where
handle.iter_transactions(),
&receipt_tx,
&executed_tx_index,
has_bal,
)?;
drop(receipt_tx);
@@ -1013,11 +994,6 @@ where
debug_span!(target: "engine::tree", "merge_transitions")
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
// Extract the built bal if payload has bal
let built_bal = if has_bal { db.take_built_alloy_bal() } else { None };
tracing::info!("Built Bal is {:?}", built_bal);
let output = BlockExecutionOutput { result, state: db.take_bundle() };
let execution_duration = execution_start.elapsed();
@@ -1025,7 +1001,7 @@ where
self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
Ok((output, senders, result_rx, built_bal))
Ok((output, senders, result_rx))
}
/// Executes transactions and collects senders, streaming receipts to a background task.
@@ -1037,20 +1013,18 @@ where
/// - Collecting transaction senders for later use
///
/// Returns the executor (for finalization) and the collected senders.
fn execute_transactions<'a, E, Tx, InnerTx, Err, DB>(
fn execute_transactions<E, Tx, InnerTx, Err>(
&self,
mut executor: E,
transaction_count: usize,
transactions: impl Iterator<Item = Result<Tx, Err>>,
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
executed_tx_index: &AtomicUsize,
has_bal: bool,
) -> Result<(E, Vec<Address>), BlockExecutionError>
where
E: BlockExecutor<Receipt = N::Receipt, Evm: alloy_evm::Evm<DB = &'a mut State<DB>>>,
E: BlockExecutor<Receipt = N::Receipt>,
Tx: alloy_evm::block::ExecutableTx<E> + alloy_evm::RecoveredTx<InnerTx>,
InnerTx: TxHashRef,
DB: revm::Database + 'a,
Err: core::error::Error + Send + Sync + 'static,
{
let mut senders = Vec::with_capacity(transaction_count);
@@ -1061,11 +1035,6 @@ where
.in_scope(|| executor.apply_pre_execution_changes())?;
self.metrics.record_pre_execution(pre_exec_start.elapsed());
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
// Execute transactions
let exec_span = debug_span!(target: "engine::tree", "execution").entered();
let mut transactions = transactions.into_iter();
@@ -1110,10 +1079,6 @@ where
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
}
}
// Bump BAL index after each transaction (EIP-7928)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
}
drop(exec_span);
@@ -1397,7 +1362,6 @@ where
transaction_root: Option<B256>,
receipt_root_bloom: Option<ReceiptRootBloom>,
hashed_state: LazyHashedPostState,
built_bal: Option<BlockAccessList>,
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -1424,13 +1388,9 @@ where
let _enter =
debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution")
.entered();
if let Err(err) = self.consensus.validate_block_post_execution(
block,
output,
receipt_root_bloom,
built_bal,
) {
if let Err(err) =
self.consensus.validate_block_post_execution(block, output, receipt_root_bloom)
{
// call post-block hook
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
return Err(err.into())

View File

@@ -13,7 +13,7 @@ use std::{hash::Hash, sync::Arc};
use tracing::error;
/// Default max cache size for [`PrecompileCache`]
const MAX_CACHE_SIZE: u32 = 10_000;
const MAX_CACHE_SIZE: u32 = 1024 * 1024;
/// Stores caches for each precompile.
#[derive(Debug, Clone, Default)]
@@ -54,6 +54,9 @@ where
moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
.initial_capacity(MAX_CACHE_SIZE as usize)
.eviction_policy(EvictionPolicy::lru())
.weigher(|key: &Bytes, value: &CacheEntry<S>| {
(key.len() + value.output.bytes.len()) as u32
})
.build_with_hasher(Default::default()),
)
}
@@ -266,7 +269,6 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: Bytes::default(),
})
})
@@ -281,7 +283,6 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
};
@@ -316,7 +317,6 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
})
}
@@ -334,7 +334,6 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
})
}

View File

@@ -1,174 +0,0 @@
//! Snap sync helpers for [`EngineApiTreeHandler`].
use crate::backfill::{BackfillAction, BackfillSyncState};
use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use alloy_primitives::B256;
use reth_engine_primitives::ExecutionPayload;
use reth_evm::ConfigureEvm;
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_primitives_traits::{NodePrimitives, SealedBlock};
use reth_provider::{
BalProvider, BlockReader, ChangeSetReader, DatabaseProviderFactory, HashedPostStateProvider,
StageCheckpointReader, StateProviderFactory, StateReader, StorageChangeSetReader,
StorageSettingsCache,
};
use tokio::sync::mpsc::UnboundedSender;
use tracing::*;
use super::{
error::InsertBlockFatalError, payload_validator::EngineValidator, EngineApiEvent,
EngineApiTreeHandler, WaitForCaches,
};
/// Snap sync state owned by the engine tree.
#[derive(Debug, Default)]
pub(super) struct SnapTreeState {
events_tx: Option<UnboundedSender<reth_engine_snap::SnapSyncEvent>>,
fresh_node: bool,
}
impl SnapTreeState {
/// Creates snap tree state for the given node freshness.
pub(super) const fn new(fresh_node: bool) -> Self {
Self { events_tx: None, fresh_node }
}
/// Returns true if this node started with no persisted blocks.
pub(super) const fn is_fresh_node(&self) -> bool {
self.fresh_node
}
/// Updates the fresh-node flag.
pub(super) const fn set_fresh_node(&mut self, fresh_node: bool) {
self.fresh_node = fresh_node;
}
/// Returns true if snap sync is currently receiving engine events.
pub(super) const fn is_active(&self) -> bool {
self.events_tx.is_some()
}
/// Starts forwarding engine events to snap sync.
pub(super) fn start(&mut self, events_tx: UnboundedSender<reth_engine_snap::SnapSyncEvent>) {
self.events_tx = Some(events_tx);
}
/// Marks snap sync as finished.
pub(super) fn finish(&mut self) {
self.events_tx = None;
self.fresh_node = false;
}
fn events_tx(&self) -> Option<&UnboundedSender<reth_engine_snap::SnapSyncEvent>> {
self.events_tx.as_ref()
}
}
impl<N, P, T, V, C> EngineApiTreeHandler<N, P, T, V, C>
where
N: NodePrimitives,
P: DatabaseProviderFactory
+ BlockReader<Block = N::Block, Header = N::BlockHeader>
+ StateProviderFactory
+ StateReader<Receipt = N::Receipt>
+ HashedPostStateProvider
+ BalProvider
+ Clone
+ 'static,
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ StorageSettingsCache,
C: ConfigureEvm<Primitives = N> + 'static,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
V: EngineValidator<T> + WaitForCaches,
{
/// Forwards a new block event to the snap sync orchestrator, if active.
pub(super) fn forward_new_block_to_snap(&self, payload: &T::ExecutionData) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::NewBlock {
number: payload.block_number(),
hash: payload.block_hash(),
state_root: payload.state_root(),
parent_hash: payload.parent_hash(),
bal: payload.block_access_list().cloned(),
});
}
}
/// Forwards a new head event to the snap sync orchestrator, if active.
pub(super) fn forward_head_to_snap(&self, head_hash: B256) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::NewHead { head_hash });
}
}
/// Forwards a downloaded block event to the snap sync orchestrator, if active.
pub(super) fn forward_downloaded_block_to_snap(&self, block: &SealedBlock<N::Block>) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::DownloadedBlock {
number: block.number(),
hash: block.hash(),
state_root: block.header().state_root(),
parent_hash: block.header().parent_hash(),
});
}
}
/// Handles snap sync completion.
pub(super) fn on_snap_sync_finished(
&mut self,
outcome: reth_engine_snap::SnapSyncOutcome,
) -> Result<(), InsertBlockFatalError> {
debug!(target: "engine::tree", synced_to = outcome.synced_to, %outcome.block_hash, "snap sync finished");
self.backfill_sync_state = BackfillSyncState::Idle;
self.snap.finish();
let backfill_height = outcome.synced_to;
let backfill_hash = outcome.block_hash;
// Remove all blocks below the snap sync height
self.state.buffer.remove_old_blocks(backfill_height);
self.purge_timing_stats(backfill_height, None);
self.canonical_in_memory_state.clear_state();
// Update canonical head — try DB first, fall back to outcome data
if let Ok(Some(new_head)) = self.provider.sealed_header(backfill_height) {
self.state.tree_state.set_canonical_head(new_head.num_hash());
self.persistence_state.finish(new_head.hash(), new_head.number());
self.canonical_in_memory_state.set_canonical_head(new_head);
} else {
let num_hash = BlockNumHash { hash: backfill_hash, number: backfill_height };
self.state.tree_state.set_canonical_head(num_hash);
self.persistence_state.finish(backfill_hash, backfill_height);
}
// Remove executed blocks below the snap sync height
let backfill_num_hash = self
.provider
.block_hash(backfill_height)?
.map(|hash| BlockNumHash { hash, number: backfill_height })
.unwrap_or(BlockNumHash { hash: backfill_hash, number: backfill_height });
self.state.tree_state.remove_until(
backfill_num_hash,
self.persistence_state.last_persisted_block.hash,
Some(backfill_num_hash),
);
self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
self.metrics.tree.canonical_chain_height.set(backfill_height as f64);
// Trigger a pipeline run for MerkleExecute + Finish.
// The orchestrator already set all other stage checkpoints to the snap target,
// so only MerkleExecute (builds AccountsTrie/StoragesTrie from hashed leaves)
// and Finish will run.
self.emit_event(EngineApiEvent::BackfillAction(BackfillAction::Start(
backfill_hash.into(),
)));
self.backfill_sync_state = BackfillSyncState::Pending;
Ok(())
}
}

View File

@@ -2254,3 +2254,65 @@ fn test_on_valid_downloaded_head_sync_target_returns_make_canonical() {
other => panic!("Expected MakeCanonical for head block, got: {other:?}"),
}
}
/// Tests that canonicalizing a downloaded sync target head also applies the tracked finalized
/// block from the original `SYNCING` forkchoice state.
#[test]
fn test_canonicalizing_downloaded_sync_target_head_updates_finalized() {
reth_tracing::init_test_tracing();
let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec);
let blocks: Vec<_> = test_harness.block_builder.get_executed_blocks(0..3).collect();
let genesis = &blocks[0];
let finalized_block = &blocks[1];
let head_block = &blocks[2];
test_harness = test_harness.with_blocks(vec![
genesis.clone(),
finalized_block.clone(),
head_block.clone(),
]);
let finalized_num_hash = finalized_block.recovered_block().num_hash();
let head_num_hash = head_block.recovered_block().num_hash();
test_harness.tree.state.tree_state.set_canonical_head(genesis.recovered_block().num_hash());
let fcu_state = ForkchoiceState {
head_block_hash: head_num_hash.hash,
safe_block_hash: head_num_hash.hash,
finalized_block_hash: finalized_num_hash.hash,
};
test_harness
.tree
.state
.forkchoice_state_tracker
.set_latest(fcu_state, ForkchoiceStatus::Syncing);
let event = test_harness
.tree
.on_valid_downloaded_block(head_num_hash)
.unwrap()
.expect("expected canonicalization event for sync target head");
test_harness.tree.on_tree_event(event).unwrap();
assert_eq!(test_harness.tree.state.tree_state.canonical_block_hash(), head_num_hash.hash);
assert_eq!(
test_harness.tree.canonical_in_memory_state.get_finalized_num_hash(),
Some(finalized_num_hash),
"Finalized block from the syncing FCU should be applied once the head becomes canonical"
);
assert_eq!(
test_harness.tree.canonical_in_memory_state.get_safe_num_hash(),
Some(head_num_hash),
"Safe block from the syncing FCU should be applied once the head becomes canonical"
);
assert_eq!(
test_harness.tree.state.forkchoice_state_tracker.last_valid_state(),
Some(fcu_state)
);
assert!(test_harness.tree.state.forkchoice_state_tracker.sync_target_state().is_none());
}

View File

@@ -13,7 +13,7 @@ extern crate alloc;
use alloc::{fmt::Debug, sync::Arc};
use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::{eip7840::BlobParams, eip7928::BlockAccessList};
use alloy_eips::eip7840::BlobParams;
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_consensus::{
Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom, TransactionRoot,
@@ -108,15 +108,9 @@ where
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
let res = validate_block_post_execution(
block,
&self.chain_spec,
result,
receipt_root_bloom,
block_access_list,
);
let res =
validate_block_post_execution(block, &self.chain_spec, result, receipt_root_bloom);
if self.skip_requests_hash_check &&
let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res

View File

@@ -1,9 +1,6 @@
use alloc::vec::Vec;
use alloy_consensus::{proofs::calculate_receipt_root, BlockHeader, TxReceipt};
use alloy_eips::{
eip7928::{compute_block_access_list_hash, BlockAccessList},
Encodable2718,
};
use alloy_eips::Encodable2718;
use alloy_primitives::{Bloom, Bytes, B256};
use reth_chainspec::EthereumHardforks;
use reth_consensus::ConsensusError;
@@ -24,7 +21,6 @@ pub fn validate_block_post_execution<B, R, ChainSpec>(
chain_spec: &ChainSpec,
result: &BlockExecutionResult<R>,
receipt_root_bloom: Option<(B256, Bloom)>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError>
where
B: Block,
@@ -83,21 +79,6 @@ where
}
}
// Validate that the block access list hash matches the calculated block access list hash
if chain_spec.is_amsterdam_active_at_timestamp(block.header().timestamp()) &&
block_access_list.is_some()
{
let block_bal_hash = block.header().block_access_list_hash().unwrap_or_default();
let default_bal = BlockAccessList::default();
let block_access_list_hash =
compute_block_access_list_hash(block_access_list.as_ref().unwrap_or(&default_bal));
if block_access_list_hash != block_bal_hash {
return Err(ConsensusError::BlockAccessListHashMismatch(
(block_access_list_hash, block_bal_hash).into(),
))
}
}
Ok(())
}

View File

@@ -54,10 +54,6 @@ impl<
) -> Self::ExecutionData {
T::block_to_payload(block)
}
fn built_payload_to_execution_data(payload: &Self::BuiltPayload) -> Self::ExecutionData {
T::built_payload_to_execution_data(payload)
}
}
impl<T> EngineTypes for EthEngineTypes<T>
@@ -98,20 +94,4 @@ impl PayloadTypes for EthPayloadTypes {
ExecutionPayload::from_block_unchecked(block.hash(), &block.into_block());
ExecutionData { payload, sidecar }
}
fn built_payload_to_execution_data(payload: &Self::BuiltPayload) -> Self::ExecutionData {
if let Some(bal) = payload.block_access_list() {
let block = payload.block();
let raw_block = block.clone().into_block();
let sidecar = alloy_rpc_types_engine::ExecutionPayloadSidecar::from_block(&raw_block);
let v4 = alloy_rpc_types_engine::ExecutionPayloadV4::from_block_unchecked_with_bal(
block.hash(),
&raw_block,
bal.clone(),
);
ExecutionData { payload: ExecutionPayload::V4(v4), sidecar }
} else {
Self::block_to_payload(payload.block().clone())
}
}
}

View File

@@ -205,10 +205,6 @@ impl<N: NodePrimitives> BuiltPayload for EthBuiltPayload<N> {
fn requests(&self) -> Option<Requests> {
self.requests.clone()
}
fn block_access_list(&self) -> Option<&Bytes> {
self.block_access_list.as_ref()
}
}
// V1 engine_getPayloadV1 response

View File

@@ -47,7 +47,6 @@ where
transactions,
output: BlockExecutionResult { receipts, requests, gas_used, blob_gas_used },
state_root,
block_access_list_hash,
..
} = input;
@@ -91,12 +90,6 @@ where
};
}
let bal_hash = if self.chain_spec.is_amsterdam_active_at_timestamp(timestamp) {
block_access_list_hash
} else {
None
};
let header = Header {
parent_hash: ctx.parent_hash,
ommers_hash: EMPTY_OMMER_ROOT_HASH,
@@ -119,8 +112,8 @@ where
blob_gas_used: block_blob_gas_used,
excess_blob_gas,
requests_hash,
block_access_list_hash: bal_hash,
slot_number: ctx.slot_number,
block_access_list_hash: None,
slot_number: None,
};
Ok(Block {

View File

@@ -54,19 +54,13 @@ revm = { workspace = true, features = ["secp256k1", "blst", "c-kzg", "memory_lim
eyre.workspace = true
[dev-dependencies]
reth-config.workspace = true
reth-db.workspace = true
reth-ethereum-consensus.workspace = true
reth-exex.workspace = true
reth-engine-snap = { workspace = true, features = ["test-utils"] }
reth-node-core.workspace = true
reth-e2e-test-utils.workspace = true
reth-prune-types.workspace = true
reth-stages = { workspace = true, features = ["test-utils"] }
reth-stages-types.workspace = true
reth-static-file.workspace = true
reth-tasks.workspace = true
reth-testing-utils.workspace = true
reth-stages-types.workspace = true
tempfile.workspace = true
jsonrpsee-core.workspace = true
@@ -118,7 +112,4 @@ test-utils = [
"reth-evm-ethereum/test-utils",
"reth-stages-types/test-utils",
"reth-tasks/test-utils",
"reth-prune-types/test-utils",
"reth-stages/test-utils",
"reth-engine-snap/test-utils",
]

View File

@@ -2,7 +2,6 @@ use crate::utils::{advance_with_random_transactions, eth_payload_attributes};
use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
use alloy_eips::Encodable2718;
use alloy_network::TxSignerSync;
use alloy_primitives::B256;
use alloy_provider::{Provider, ProviderBuilder};
use futures::future::JoinAll;
use rand::{rngs::StdRng, seq::IndexedRandom, Rng, SeedableRng};
@@ -295,194 +294,3 @@ async fn test_tx_propagation() -> eyre::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore = "requires serving in-memory state; serving node keeps ~2 blocks unpersisted"]
async fn can_snap_sync_frozen_head() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.build(),
);
// Do NOT auto-connect nodes — we want to prevent accidental eth sync
let (mut nodes, _) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false, // do not auto-connect
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Advance Node A by 300 blocks with random transactions (creates contracts + storage)
advance_with_random_transactions(&mut node_a, 300, &mut rng, true).await?;
// Wait for hashed state to stabilize in MDBX
let _target_root = {
let mut prev = node_a.snap_state_root().await;
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
let current = node_a.snap_state_root().await;
if current == prev && current != B256::ZERO {
break current;
}
prev = current;
}
};
let target_hash = node_a.block_hash(300);
// Connect Node B to Node A (after blocks are produced, head is frozen)
node_b.connect(&mut node_a).await;
// Trigger engine-driven snap sync: send the head FCU so the engine tree
// detects a fresh node and starts the SnapSyncOrchestrator.
node_b.sync_to(target_hash).await?;
// Verify state root matches Node A
let node_b_root = node_b.snap_state_root().await;
let node_a_root = node_a.snap_state_root().await;
assert_eq!(node_b_root, node_a_root, "State roots should match after snap sync");
Ok(())
}
#[tokio::test]
async fn can_snap_sync_catch_up() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.amsterdam_activated()
.build(),
);
// Do NOT auto-connect — prevent accidental eth sync
let (mut nodes, _) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false,
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Build initial state on Node A (20 blocks)
advance_with_random_transactions(&mut node_a, 20, &mut rng, true).await?;
let initial_target = node_a.block_hash(20);
// Connect Node B to Node A
node_b.connect(&mut node_a).await;
// Advance Node A further BEFORE triggering snap sync on Node B.
advance_with_random_transactions(&mut node_a, 10, &mut rng, true).await?;
// Now trigger snap sync on Node B targeting the initial block.
node_b.update_forkchoice(initial_target, initial_target).await?;
// Continue advancing Node A to push even further
advance_with_random_transactions(&mut node_a, 5, &mut rng, true).await?;
let final_hash = node_a.block_hash(35);
// Wait for Node B to sync
node_b.sync_to(final_hash).await?;
Ok(())
}
/// Tests that the snap sync orchestrator recovers when the pivot root becomes
/// stale. The test lowers the serving lookback, then advances Node A far enough
/// that the snap server's lookback window no longer covers the original pivot
/// root, forcing the orchestrator to re-resolve the head and advance the pivot.
#[tokio::test]
async fn can_snap_sync_stale_pivot() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let _lookback_guard = reth_engine_snap::serve::set_max_serving_lookback_for_tests(24);
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.amsterdam_activated()
.build(),
);
let (mut nodes, wallet) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false,
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Build enough stateful blocks for snap sync to exercise account/storage
// download, then move the head forward with one cheap transfer per block.
// With the test lookback set to 24, tip 30 starts serving at block 6, so the
// original pivot root (block 4 = 20 - PIVOT_OFFSET) is just stale.
advance_with_random_transactions(&mut node_a, 20, &mut rng, true).await?;
let old_target = node_a.block_hash(20);
let chain_id = wallet.chain_id;
let padding_wallet = Wallet::new(2).with_chain_id(chain_id).wallet_gen().pop().unwrap();
for nonce in 0..10 {
let raw_tx = TransactionTestContext::transfer_tx_bytes_with_nonce(
chain_id,
padding_wallet.clone(),
nonce,
)
.await;
node_a.rpc.inject_tx(raw_tx).await?;
node_a.advance_block().await?;
}
let final_hash = node_a.block_hash(30);
// Connect Node B to Node A and trigger snap sync targeting block 20.
// Orchestrator picks pivot = 20 - 16 = 4, whose root is stale.
node_b.connect(&mut node_a).await;
node_b.update_forkchoice(old_target, old_target).await?;
// Node B should recover from the stale pivot, re-resolve head, and sync.
node_b.sync_to(final_hash).await?;
Ok(())
}

View File

@@ -12,7 +12,7 @@ use alloy_rpc_types_eth::TransactionRequest;
use rand::{rngs::StdRng, Rng, SeedableRng};
use reth_chainspec::{ChainSpecBuilder, EthChainSpec, MAINNET};
use reth_e2e_test_utils::setup_engine;
use reth_network::types::NatResolver;
use reth_network::{types::NatResolver, PeersInfo};
use reth_node_builder::{NodeBuilder, NodeHandle};
use reth_node_core::{
args::{NetworkArgs, RpcServerArgs},
@@ -375,3 +375,47 @@ async fn test_admin_external_ip() -> eyre::Result<()> {
Ok(())
}
#[tokio::test]
async fn test_admin_node_info_uses_discv5_port_when_discv4_is_disabled() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let runtime = Runtime::test();
let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json")).unwrap();
let chain_spec =
Arc::new(ChainSpecBuilder::default().chain(MAINNET.chain).genesis(genesis).build());
let mut network = NetworkArgs::default().with_unused_ports();
network.bootnodes = Some(Vec::new());
network.discovery.disable_dns_discovery = true;
network.discovery.disable_discv4_discovery = true;
network = network.with_nat_resolver(NatResolver::ExternalIp("127.0.0.1".parse().unwrap()));
let node_config = NodeConfig::test()
.with_chain(chain_spec)
.with_network(network)
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http());
let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)
.testing_node(runtime)
.node(EthereumNode::default())
.launch()
.await?;
assert!(node.network.discv4().is_none());
let discv5_port = node.network.discv5().expect("discv5 should be enabled").local_port();
let local_record = node.network.local_node_record();
let local_enr = node.network.local_enr();
let info = node.add_ons_handle.admin_api().node_info().await.unwrap();
assert_eq!(local_record.udp_port, discv5_port);
assert_eq!(local_enr.udp4(), Some(discv5_port));
assert_eq!(info.ports.discovery, discv5_port);
assert_eq!(info.ports.listener, local_record.tcp_port);
assert_eq!(info.enode, local_record.to_string());
assert!(info.enode.contains(&format!("?discport={discv5_port}")));
Ok(())
}

View File

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

View File

@@ -9,7 +9,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
use alloy_consensus::Transaction;
use alloy_primitives::{Bytes, U256};
use alloy_primitives::U256;
use alloy_rlp::Encodable;
use alloy_rpc_types_engine::PayloadAttributes as EthPayloadAttributes;
use reth_basic_payload_builder::{
@@ -283,12 +283,12 @@ where
block_available_gas,
),
);
continue;
continue
}
// check if the job was cancelled, if so we can exit early
if cancel.is_cancelled() {
return Ok(BuildOutcome::Cancelled);
return Ok(BuildOutcome::Cancelled)
}
// convert tx to a signed transaction
@@ -307,7 +307,7 @@ where
limit: MAX_RLP_BLOCK_SIZE,
},
);
continue;
continue
}
// There's only limited amount of blob space available per block, so we need to check if
@@ -331,14 +331,14 @@ where
},
),
);
continue;
continue
}
let blob_sidecar_result = 'sidecar: {
let Some(sidecar) =
pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
else {
break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar);
break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
};
if is_osaka {
@@ -358,7 +358,7 @@ where
Ok(sidecar) => Some(sidecar),
Err(error) => {
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
continue;
continue
}
};
}
@@ -388,7 +388,7 @@ where
),
);
}
continue;
continue
}
// The executor is the source of truth for block gas availability. Keep this
// non-fatal in case local builder accounting diverges from executor rules.
@@ -406,7 +406,7 @@ where
block_available_gas,
),
);
continue;
continue
}
// this is an error that we should treat as fatal for this attempt
Err(err) => return Err(PayloadBuilderError::evm(err)),
@@ -443,12 +443,10 @@ where
// Release db
drop(builder);
// can skip building the block
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads });
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
}
let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if let Some(
mut handle,
) = trie_handle
let BlockBuilderOutcome { execution_result, block, .. } = if let Some(mut handle) = trie_handle
{
// Drop the state hook, which drops the StateHookSender and triggers
// FinishedStateUpdates via its Drop impl, signaling the trie task to finalize.
@@ -487,10 +485,8 @@ where
max_rlp_length: MAX_RLP_BLOCK_SIZE,
}));
}
let block_access_list: Option<Bytes> =
block_access_list.map(|block_access_list| alloy_rlp::encode(&block_access_list).into());
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, block_access_list)
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
// add blob sidecars from the executed txs
.with_sidecars(blob_sidecars);

View File

@@ -1,8 +1,11 @@
//! Helper aliases when working with [`ConfigureEvm`] and the traits in this crate.
use crate::ConfigureEvm;
use alloy_evm::{block::BlockExecutorFactory, Database, EvmEnv, EvmFactory};
use revm::{inspector::NoOpInspector, Inspector};
use alloy_evm::{
block::{BlockExecutorFactory, BlockExecutorFor},
Database, EvmEnv, EvmFactory,
};
use revm::{database::State, inspector::NoOpInspector, Inspector};
/// Helper to access [`EvmFactory`] for a given [`ConfigureEvm`].
pub type EvmFactoryFor<Evm> =
@@ -33,6 +36,10 @@ pub type TxEnvFor<Evm> = <EvmFactoryFor<Evm> as EvmFactory>::Tx;
pub type ExecutionCtxFor<'a, Evm> =
<<Evm as ConfigureEvm>::BlockExecutorFactory as BlockExecutorFactory>::ExecutionCtx<'a>;
/// Helper to access [`alloy_evm::block::BlockExecutor`] for a given [`ConfigureEvm`].
pub type BlockExecutorForEvm<'a, Evm, DB, I = NoOpInspector> =
BlockExecutorFor<'a, <Evm as ConfigureEvm>::BlockExecutorFactory, &'a mut State<DB>, I>;
/// Type alias for [`EvmEnv`] for a given [`ConfigureEvm`].
pub type EvmEnvFor<Evm> = EvmEnv<SpecFor<Evm>, BlockEnvFor<Evm>>;

View File

@@ -79,11 +79,4 @@ where
Self::Right(b) => b.size_hint(),
}
}
fn take_bal(&mut self) -> Option<alloy_eips::eip7928::BlockAccessList> {
match self {
Self::Left(a) => a.take_bal(),
Self::Right(b) => b.take_bal(),
}
}
}

View File

@@ -3,10 +3,7 @@
use crate::{ConfigureEvm, Database, OnStateHook, TxEnvFor};
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use alloy_consensus::{BlockHeader, Header};
use alloy_eips::{
eip2718::WithEncoded,
eip7928::{compute_block_access_list_hash, BlockAccessList},
};
use alloy_eips::eip2718::WithEncoded;
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory, GasOutput};
use alloy_evm::{
block::{CommitChanges, ExecutableTxParts},
@@ -24,10 +21,7 @@ use reth_primitives_traits::{
use reth_storage_api::StateProvider;
pub use reth_storage_errors::provider::ProviderError;
use reth_trie_common::{updates::TrieUpdates, HashedPostState};
use revm::{
database::{states::bundle_state::BundleRetention, BundleState, State},
state::bal::Bal,
};
use revm::database::{states::bundle_state::BundleRetention, BundleState, State};
/// A type that knows how to execute a block. It is assumed to operate on a
/// [`crate::Evm`] internally and use [`State`] as database.
@@ -151,9 +145,6 @@ pub trait Executor<DB: Database>: Sized {
///
/// This is used to optimize DB commits depending on the size of the state.
fn size_hint(&self) -> usize;
/// Take built [`BlockAccessList`] from executor
fn take_bal(&mut self) -> Option<BlockAccessList>;
}
/// Input for block building. Consumed by [`BlockAssembler`].
@@ -171,7 +162,6 @@ pub trait Executor<DB: Database>: Sized {
/// - `bundle_state`: Accumulated state changes from all transactions
/// - `state_provider`: Access to the current state for additional lookups
/// - `state_root`: The calculated state root after all changes
/// - `block_access_list_hash`: Block access list hash (EIP-7928, Amsterdam)
///
/// # Usage
///
@@ -188,7 +178,6 @@ pub trait Executor<DB: Database>: Sized {
/// bundle_state: &state_changes,
/// state_provider: &state,
/// state_root: calculated_root,
/// block_access_list_hash: Some(calculated_bal_hash),
/// };
///
/// let block = assembler.assemble_block(input)?;
@@ -216,8 +205,6 @@ pub struct BlockAssemblerInput<'a, 'b, F: BlockExecutorFactory, H = Header> {
pub state_provider: &'b dyn StateProvider,
/// State root for this block.
pub state_root: B256,
/// Block access list hash (EIP-7928, Amsterdam).
pub block_access_list_hash: Option<B256>,
}
impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
@@ -235,7 +222,6 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
bundle_state: &'a BundleState,
state_provider: &'b dyn StateProvider,
state_root: B256,
block_access_list_hash: Option<B256>,
) -> Self {
Self {
evm_env,
@@ -246,7 +232,6 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
bundle_state,
state_provider,
state_root,
block_access_list_hash,
}
}
}
@@ -316,8 +301,6 @@ pub struct BlockBuilderOutcome<N: NodePrimitives> {
pub trie_updates: TrieUpdates,
/// The built block.
pub block: RecoveredBlock<N::Block>,
/// Block access list built during execution (EIP-7928, Amsterdam).
pub block_access_list: Option<BlockAccessList>,
}
/// A type that knows how to execute and build a block.
@@ -470,11 +453,7 @@ where
type Executor = Executor;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
self.executor.apply_pre_execution_changes()?;
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
self.executor.evm_mut().db_mut().bump_bal_index();
Ok(())
self.executor.apply_pre_execution_changes()
}
fn execute_transaction_with_commit_condition(
@@ -487,8 +466,6 @@ where
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
{
self.transactions.push(tx);
// Bump BAL index after each committed transaction (EIP-7928)
self.executor.evm_mut().db_mut().bump_bal_index();
Ok(Some(gas_used))
} else {
Ok(None)
@@ -506,11 +483,6 @@ where
// merge all transitions into bundle state
db.merge_transitions(BundleRetention::Reverts);
// extract the built block access list (EIP-7928, Amsterdam) and compute its hash
let block_access_list = db.take_built_alloy_bal();
let block_access_list_hash =
block_access_list.as_ref().map(|bal| compute_block_access_list_hash(bal));
let hashed_state = state.hashed_post_state(&db.bundle_state);
let (state_root, trie_updates) = match state_root_precomputed {
Some(precomputed) => precomputed,
@@ -531,18 +503,11 @@ where
bundle_state: &db.bundle_state,
state_provider: &state,
state_root,
block_access_list_hash,
})?;
let block = RecoveredBlock::new_unhashed(block, senders);
Ok(BlockBuilderOutcome {
execution_result: result,
hashed_state,
trie_updates,
block,
block_access_list,
})
Ok(BlockBuilderOutcome { execution_result: result, hashed_state, trie_updates, block })
}
fn executor_mut(&mut self) -> &mut Self::Executor {
@@ -589,33 +554,11 @@ where
block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<BlockExecutionResult<<Self::Primitives as NodePrimitives>::Receipt>, Self::Error>
{
let mut executor = self
let result = self
.strategy_factory
.executor_for_block(&mut self.db, block)
.map_err(BlockExecutionError::other)?;
let has_bal = block.header().block_access_list_hash().is_some();
if has_bal {
executor.evm_mut().db_mut().bal_state.bal_builder = Some(Bal::new());
} else {
executor.evm_mut().db_mut().bal_state.bal_builder = None;
}
executor.apply_pre_execution_changes()?;
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
for tx in block.transactions_recovered() {
executor.execute_transaction(tx)?;
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
}
let result = executor.apply_post_execution_changes()?;
.map_err(BlockExecutionError::other)?
.execute_block(block.transactions_recovered())?;
self.db.merge_transitions(BundleRetention::Reverts);
@@ -649,10 +592,6 @@ where
fn size_hint(&self) -> usize {
self.db.bundle_state.size_hint()
}
fn take_bal(&mut self) -> Option<BlockAccessList> {
self.db.take_built_alloy_bal()
}
}
/// A helper trait marking a 'static type that can be converted into an [`ExecutableTxParts`] for
@@ -758,10 +697,6 @@ mod tests {
fn size_hint(&self) -> usize {
0
}
fn take_bal(&mut self) -> Option<BlockAccessList> {
None
}
}
#[test]

View File

@@ -20,10 +20,7 @@ extern crate alloc;
use crate::execute::{BasicBlockBuilder, Executor};
use alloc::vec::Vec;
use alloy_eips::eip4895::Withdrawals;
use alloy_evm::{
block::{BlockExecutorFactory, BlockExecutorFor},
precompiles::PrecompilesMap,
};
use alloy_evm::{block::BlockExecutorFactory, precompiles::PrecompilesMap};
use alloy_primitives::{Address, Bytes, B256};
use core::{error::Error, fmt::Debug};
use execute::{BasicBlockExecutor, BlockAssembler, BlockBuilder};
@@ -312,7 +309,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
&'a self,
evm: EvmFor<Self, &'a mut State<DB>, I>,
ctx: <Self::BlockExecutorFactory as BlockExecutorFactory>::ExecutionCtx<'a>,
) -> BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>
) -> BlockExecutorForEvm<'a, Self, DB, I>
where
DB: Database,
I: InspectorFor<Self, &'a mut State<DB>> + 'a,
@@ -325,8 +322,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
&'a self,
db: &'a mut State<DB>,
block: &'a SealedBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>, Self::Error>
{
) -> Result<BlockExecutorForEvm<'a, Self, DB>, Self::Error> {
let evm = self.evm_for_block(db, block.header())?;
let ctx = self.context_for_block(block)?;
Ok(self.create_executor(evm, ctx))
@@ -352,10 +348,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
evm: EvmFor<Self, &'a mut State<DB>, I>,
parent: &'a SealedHeader<HeaderTy<Self::Primitives>>,
ctx: <Self::BlockExecutorFactory as BlockExecutorFactory>::ExecutionCtx<'a>,
) -> impl BlockBuilder<
Primitives = Self::Primitives,
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>,
>
) -> impl BlockBuilder<Primitives = Self::Primitives, Executor = BlockExecutorForEvm<'a, Self, DB, I>>
where
DB: Database,
I: InspectorFor<Self, &'a mut State<DB>> + 'a,
@@ -404,10 +397,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
parent: &'a SealedHeader<<Self::Primitives as NodePrimitives>::BlockHeader>,
attributes: Self::NextBlockEnvCtx,
) -> Result<
impl BlockBuilder<
Primitives = Self::Primitives,
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>,
>,
impl BlockBuilder<Primitives = Self::Primitives, Executor = BlockExecutorForEvm<'a, Self, DB>>,
Self::Error,
> {
let evm_env = self.next_evm_env(parent, &attributes)?;

View File

@@ -16,10 +16,13 @@ workspace = true
metrics.workspace = true
metrics-derive.workspace = true
# reth
reth-primitives-traits = { workspace = true, optional = true }
# async
tokio = { workspace = true, features = ["full"], optional = true }
futures = { workspace = true, optional = true }
tokio-util = { workspace = true, optional = true }
[features]
common = ["tokio", "futures", "tokio-util"]
common = ["tokio", "futures", "tokio-util", "reth-primitives-traits"]

View File

@@ -4,8 +4,13 @@
use crate::Metrics;
use futures::Stream;
use metrics::Counter;
use reth_primitives_traits::InMemorySize;
use std::{
pin::Pin,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
task::{ready, Context, Poll},
};
use tokio::sync::mpsc::{
@@ -399,3 +404,147 @@ struct MeteredPollSenderMetrics {
/// Number of delayed message deliveries caused by a full channel
back_pressure_total: Counter,
}
/// Shared state for tracking memory budget across sender and receiver.
///
/// `used` is a pure accounting counter — it does not gate access to any other
/// shared memory, so all operations on it use [`Ordering::Relaxed`]. Cross-thread
/// publication of message contents is handled by the underlying mpsc channel.
#[derive(Debug)]
struct MemoryBudget {
/// Current number of bytes used by buffered messages.
used: AtomicUsize,
/// Maximum allowed bytes.
max_bytes: usize,
}
/// Guard that releases memory budget when dropped.
///
/// Holds the size of the message and a reference to the shared budget counter.
/// When dropped, it atomically decreases the used counter.
#[derive(Debug)]
struct BudgetGuard {
size: usize,
budget: Arc<MemoryBudget>,
}
impl Drop for BudgetGuard {
fn drop(&mut self) {
self.budget.used.fetch_sub(self.size, Ordering::Relaxed);
}
}
/// Message envelope that holds the memory budget while the message sits in the channel.
///
/// The guard is dropped (releasing the budget) as soon as the receiver dequeues
/// the message via [`MemoryBoundedReceiver::recv`] / [`MemoryBoundedReceiver::poll_recv`],
/// so the budget tracks bytes *currently in the channel queue*, not bytes in flight
/// downstream of the receiver.
#[derive(Debug)]
struct Budgeted<T> {
msg: T,
_guard: BudgetGuard,
}
/// A sender that enforces a byte budget before enqueueing messages.
///
/// Uses a shared atomic counter to track memory usage. Each message's size is added
/// to the counter on send and subtracted when the message is dequeued by the receiver.
///
/// The current call sites (specifically [`crate::common::mpsc::MemoryBoundedSender`] used
/// for the `NetworkManager → TransactionsManager` channel) have a single producer driven
/// from a single `poll`, so the `fetch_add → check → fetch_sub-on-overflow` reservation
/// pattern can never race with itself. The atomic is still used so the receiver can
/// release budget from a different task.
#[derive(Debug, Clone)]
pub struct MemoryBoundedSender<T: InMemorySize> {
/// The underlying unbounded metered sender
inner: UnboundedMeteredSender<Budgeted<T>>,
/// Shared memory budget tracker
budget: Arc<MemoryBudget>,
}
impl<T: InMemorySize> MemoryBoundedSender<T> {
/// Tries to send a message if there is sufficient budget.
///
/// Returns `TrySendError::Full` if insufficient budget is available.
pub fn try_send(&self, msg: T) -> Result<(), TrySendError<T>> {
let size = msg.size();
// Reserve budget: add first, check after
let prev = self.budget.used.fetch_add(size, Ordering::Relaxed);
if prev.saturating_add(size) > self.budget.max_bytes {
// Over budget, undo
self.budget.used.fetch_sub(size, Ordering::Relaxed);
return Err(TrySendError::Full(msg));
}
let guard = BudgetGuard { size, budget: Arc::clone(&self.budget) };
let budgeted = Budgeted { msg, _guard: guard };
self.inner.send(budgeted).map_err(|e| {
// Guard will be dropped here, releasing the budget
TrySendError::Closed(e.0.msg)
})
}
}
/// A receiver for memory-bounded messages.
///
/// On receive, the budget reserved for the message is released immediately and the
/// inner `T` is yielded — callers do not need to opt into any wrapper type.
#[derive(Debug)]
pub struct MemoryBoundedReceiver<T> {
/// The underlying unbounded metered receiver
inner: UnboundedMeteredReceiver<Budgeted<T>>,
}
impl<T> MemoryBoundedReceiver<T> {
/// Receives the next message, returning `None` if the channel is closed.
///
/// Releases the message's reserved budget before returning.
pub async fn recv(&mut self) -> Option<T> {
self.inner.recv().await.map(unwrap_budgeted)
}
/// Polls to receive the next message on this channel.
///
/// Releases the message's reserved budget before returning.
pub fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Option<T>> {
self.inner.poll_recv(cx).map(|opt| opt.map(unwrap_budgeted))
}
}
/// Releases the budget guard and returns the inner message.
fn unwrap_budgeted<T>(b: Budgeted<T>) -> T {
// Destructuring binds `_guard` so it is dropped when this function returns,
// which runs `BudgetGuard::drop` and releases the reserved bytes.
let Budgeted { msg, _guard } = b;
msg
}
impl<T> Stream for MemoryBoundedReceiver<T> {
type Item = T;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.poll_recv(cx)
}
}
/// Creates a new memory-bounded channel with the given byte budget.
///
/// The budget tracks bytes currently buffered in the channel; it is reserved on
/// [`MemoryBoundedSender::try_send`] and released as soon as the receiver dequeues
/// the message.
pub fn memory_bounded_channel<T: InMemorySize>(
max_bytes: usize,
scope: &'static str,
) -> (MemoryBoundedSender<T>, MemoryBoundedReceiver<T>) {
let (tx, rx) = metered_unbounded_channel(scope);
let budget = Arc::new(MemoryBudget { used: AtomicUsize::new(0), max_bytes });
let sender = MemoryBoundedSender { inner: tx, budget };
let receiver = MemoryBoundedReceiver { inner: rx };
(sender, receiver)
}

View File

@@ -47,9 +47,7 @@ use secp256k1::SecretKey;
use std::{
cell::RefCell,
collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, VecDeque},
fmt,
future::poll_fn,
io,
fmt, io,
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
pin::Pin,
rc::Rc,
@@ -243,17 +241,56 @@ impl Discv4 {
/// ```
pub async fn bind(
local_address: SocketAddr,
local_node_record: NodeRecord,
secret_key: SecretKey,
config: Discv4Config,
) -> io::Result<(Self, Discv4Service)> {
let socket = Arc::new(UdpSocket::bind(local_address).await?);
trace!(target: "discv4", local_addr=?socket.local_addr(), "opened UDP socket");
let (tx, rx) = mpsc::channel(config.udp_ingress_message_buffer);
Self::bind_with_socket(socket, Some(tx), rx, local_node_record, secret_key, config)
}
/// Creates a new `Discv4` instance using a pre-bound shared socket. No receive loop is
/// spawned; instead returns an [`IngressHandler`] that should be used to forward raw packets
/// received by the socket owner (e.g. discv5 unrecognized frames).
pub fn bind_shared(
socket: Arc<UdpSocket>,
local_node_record: NodeRecord,
secret_key: SecretKey,
config: Discv4Config,
) -> io::Result<(Self, Discv4Service, IngressHandler)> {
let (tx, rx) = mpsc::channel(config.udp_ingress_message_buffer);
let local_id = local_node_record.id;
let (discv4, service) =
Self::bind_with_socket(socket, None, rx, local_node_record, secret_key, config)?;
let handler = IngressHandler::new(tx, local_id);
Ok((discv4, service, handler))
}
fn bind_with_socket(
socket: Arc<UdpSocket>,
ingress_tx: Option<IngressSender>,
ingress_rx: IngressReceiver,
mut local_node_record: NodeRecord,
secret_key: SecretKey,
config: Discv4Config,
) -> io::Result<(Self, Discv4Service)> {
let socket = UdpSocket::bind(local_address).await?;
let local_addr = socket.local_addr()?;
local_node_record.udp_port = local_addr.port();
trace!(target: "discv4", ?local_addr,"opened UDP socket");
let mut service =
Discv4Service::new(socket, local_addr, local_node_record, secret_key, config);
let mut service = Discv4Service::new(
socket,
ingress_tx,
ingress_rx,
local_addr,
local_node_record,
secret_key,
config,
);
// resolve the external address immediately
service.resolve_external_ip();
@@ -520,20 +557,25 @@ pub struct Discv4Service {
impl Discv4Service {
/// Create a new instance for a bound [`UdpSocket`].
///
/// If `ingress_tx` is `Some`, the receive loop is spawned to read from the socket. If `None`,
/// the caller feeds packets into `ingress_rx` externally (shared socket mode).
pub(crate) fn new(
socket: UdpSocket,
socket: Arc<UdpSocket>,
ingress_tx: Option<IngressSender>,
ingress_rx: IngressReceiver,
local_address: SocketAddr,
local_node_record: NodeRecord,
secret_key: SecretKey,
config: Discv4Config,
) -> Self {
let socket = Arc::new(socket);
let (ingress_tx, ingress_rx) = mpsc::channel(config.udp_ingress_message_buffer);
let (egress_tx, egress_rx) = mpsc::channel(config.udp_egress_message_buffer);
let mut tasks = JoinSet::<()>::new();
let udp = Arc::clone(&socket);
tasks.spawn(receive_loop(udp, ingress_tx, local_node_record.id));
if let Some(ingress_tx) = ingress_tx {
let udp = Arc::clone(&socket);
tasks.spawn(receive_loop(udp, ingress_tx, local_node_record.id));
}
let udp = Arc::clone(&socket);
tasks.spawn(send_loop(udp, egress_rx));
@@ -947,7 +989,7 @@ impl Discv4Service {
let key = kad_key(peer_id);
match self.kbuckets.entry(&key) {
BucketEntry::Present(entry, _) => Some(f(entry.value())),
BucketEntry::Pending(mut entry, _) => Some(f(entry.value())),
BucketEntry::Pending(entry, _) => Some(f(entry.value())),
_ => None,
}
}
@@ -973,7 +1015,9 @@ impl Discv4Service {
kbucket::Entry::Present(mut entry, _) => {
entry.value_mut().update_with_enr(last_enr_seq)
}
kbucket::Entry::Pending(mut entry, _) => entry.value().update_with_enr(last_enr_seq),
kbucket::Entry::Pending(mut entry, _) => {
entry.value_mut().update_with_enr(last_enr_seq)
}
_ => return,
};
@@ -1025,8 +1069,8 @@ impl Discv4Service {
}
kbucket::Entry::Pending(mut entry, mut status) => {
// endpoint is now proven
entry.value().establish_proof();
entry.value().update_with_enr(last_enr_seq);
entry.value_mut().establish_proof();
entry.value_mut().update_with_enr(last_enr_seq);
if !status.is_connected() {
status.state = ConnectionState::Connected;
@@ -1158,7 +1202,7 @@ impl Discv4Service {
} else {
is_proven = entry.value().has_endpoint_proof;
}
entry.value().update_with_enr(ping.enr_sq)
entry.value_mut().update_with_enr(ping.enr_sq)
}
kbucket::Entry::Absent(entry) => {
let mut node = NodeEntry::new(record);
@@ -1388,7 +1432,7 @@ impl Discv4Service {
(entry.value().record, id)
}
kbucket::Entry::Pending(mut entry, _) => {
let id = entry.value().update_with_fork_id(fork_id);
let id = entry.value_mut().update_with_fork_id(fork_id);
(entry.value().record, id)
}
_ => return,
@@ -1538,7 +1582,7 @@ impl Discv4Service {
}
}
}
BucketEntry::Pending(mut entry, _) => {
BucketEntry::Pending(entry, _) => {
if entry.value().has_endpoint_proof {
if entry
.value()
@@ -1642,7 +1686,7 @@ impl Discv4Service {
entry.value().find_node_failures
}
kbucket::Entry::Pending(mut entry, _) => {
entry.value().inc_failed_request();
entry.value_mut().inc_failed_request();
entry.value().find_node_failures
}
_ => continue,
@@ -1962,80 +2006,100 @@ const MAX_INCOMING_PACKETS_PER_MINUTE_BY_IP: usize = 60usize;
/// Continuously awaits new incoming messages and sends them back through the channel.
///
/// The receive loop enforce primitive rate limiting for ips to prevent message spams from
/// individual IPs
/// The receive loop enforces primitive rate limiting for IPs to prevent message spams from
/// individual IPs.
pub(crate) async fn receive_loop(udp: Arc<UdpSocket>, tx: IngressSender, local_id: PeerId) {
let send = |event: IngressEvent| async {
let _ = tx.send(event).await.map_err(|err| {
debug!(
target: "discv4",
%err,
"failed send incoming packet",
)
});
};
let mut cache = ReceiveCache::default();
// tick at half the rate of the limit
let tick = MAX_INCOMING_PACKETS_PER_MINUTE_BY_IP / 2;
let mut interval = tokio::time::interval(Duration::from_secs(tick as u64));
let mut handler = IngressHandler::new(tx, local_id);
let mut buf = [0; MAX_PACKET_SIZE];
loop {
let res = udp.recv_from(&mut buf).await;
match res {
Err(err) => {
debug!(target: "discv4", %err, "Failed to read datagram.");
send(IngressEvent::RecvError(err)).await;
handler.send(IngressEvent::RecvError(err)).await;
}
Ok((read, remote_addr)) => {
// rate limit incoming packets by IP
if cache.inc_ip(remote_addr.ip()) > MAX_INCOMING_PACKETS_PER_MINUTE_BY_IP {
trace!(target: "discv4", ?remote_addr, "Too many incoming packets from IP.");
continue
}
let packet = &buf[..read];
match Message::decode(packet) {
Ok(packet) => {
if packet.node_id == local_id {
// received our own message
debug!(target: "discv4", ?remote_addr, "Received own packet.");
continue
}
// skip if we've already received the same packet
if cache.contains_packet(packet.hash) {
debug!(target: "discv4", ?remote_addr, "Received duplicate packet.");
continue
}
send(IngressEvent::Packet(remote_addr, packet)).await;
}
Err(err) => {
trace!(target: "discv4", %err,"Failed to decode packet");
send(IngressEvent::BadPacket(remote_addr, err, packet.to_vec())).await
}
}
handler.handle_packet(&buf[..read], remote_addr).await;
}
}
}
}
// reset the tracked ips if the interval has passed
if poll_fn(|cx| match interval.poll_tick(cx) {
Poll::Ready(_) => Poll::Ready(true),
Poll::Pending => Poll::Ready(false),
})
.await
{
cache.tick_ips(tick);
/// Handles decoding, rate-limiting, and deduplication of incoming discv4 packets.
///
/// Used by both the standalone receive loop and the shared-port mode via
/// [`Discv4::bind_shared`].
#[derive(Debug)]
pub struct IngressHandler {
tx: IngressSender,
local_id: PeerId,
tick: usize,
tick_interval: Duration,
cache: ReceiveCache,
last_tick: Instant,
}
impl IngressHandler {
fn new(tx: IngressSender, local_id: PeerId) -> Self {
let tick = MAX_INCOMING_PACKETS_PER_MINUTE_BY_IP / 2;
Self {
tx,
local_id,
tick,
tick_interval: Duration::from_secs(tick as u64),
cache: ReceiveCache::default(),
last_tick: Instant::now(),
}
}
async fn send(&self, event: IngressEvent) {
let _ = self.tx.send(event).await.map_err(|err| {
debug!(target: "discv4", %err, "failed send incoming packet");
});
}
/// Handles an incoming raw packet: decodes, rate-limits, deduplicates, and forwards to the
/// discv4 service. Used in shared-port mode to process unrecognized frames from discv5.
pub async fn handle_packet(&mut self, data: &[u8], src: SocketAddr) {
if self.last_tick.elapsed() >= self.tick_interval {
self.cache.tick_ips(self.tick);
self.last_tick = Instant::now();
}
// rate limit incoming packets by IP
if self.cache.inc_ip(src.ip()) > MAX_INCOMING_PACKETS_PER_MINUTE_BY_IP {
trace!(target: "discv4", ?src, "Too many incoming packets from IP.");
return
}
let event = match Message::decode(data) {
Ok(packet) => {
if packet.node_id == self.local_id {
debug!(target: "discv4", ?src, "Received own packet.");
return
}
if self.cache.contains_packet(packet.hash) {
debug!(target: "discv4", ?src, "Received duplicate packet.");
return
}
IngressEvent::Packet(src, packet)
}
Err(err) => {
trace!(target: "discv4", %err, "Failed to decode packet");
IngressEvent::BadPacket(src, err, data.to_vec())
}
};
self.send(event).await;
}
}
/// A cache for received packets and their source address.
///
/// This is used to discard duplicated packets and rate limit messages from the same source.
#[derive(Debug)]
struct ReceiveCache {
/// keeps track of how many messages we've received from a given IP address since the last
/// tick.

View File

@@ -308,6 +308,18 @@ impl Config {
}
}
/// Returns a mutable reference to the inner [`discv5::Config`]. This allows overriding
/// the listen config after the config has been built.
pub const fn discv5_config_mut(&mut self) -> &mut discv5::Config {
&mut self.discv5_config
}
/// Returns `true` if any socket in the discv5 listen config matches the given address.
pub fn has_matching_socket(&self, addr: SocketAddr) -> bool {
ipv4(&self.discv5_config.listen_config).is_some_and(|v4| SocketAddr::V4(v4) == addr) ||
ipv6(&self.discv5_config.listen_config).is_some_and(|v6| SocketAddr::V6(v6) == addr)
}
/// Inserts a new boot node to the list of boot nodes.
pub fn insert_boot_node(&mut self, boot_node: BootNode) {
self.bootstrap_nodes.insert(boot_node);
@@ -333,11 +345,11 @@ impl Config {
/// socket, if both IPv4 and v6 are configured. This socket will be advertised to peers in the
/// local [`Enr`](discv5::enr::Enr).
pub fn discovery_socket(&self) -> SocketAddr {
match self.discv5_config.listen_config {
ListenConfig::Ipv4 { ip, port } => (ip, port).into(),
ListenConfig::Ipv6 { ip, port } => (ip, port).into(),
ListenConfig::DualStack { ipv6, ipv6_port, .. } => (ipv6, ipv6_port).into(),
}
// Prefer v6 when both are configured (matches original `DualStack` behavior).
ipv6(&self.discv5_config.listen_config)
.map(SocketAddr::V6)
.or_else(|| ipv4(&self.discv5_config.listen_config).map(SocketAddr::V4))
.unwrap_or_else(|| SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0)))
}
/// Returns the `RLPx` (TCP) socket contained in the [`discv5::Config`]. This socket will be
@@ -348,24 +360,32 @@ impl Config {
}
/// Returns the IPv4 discovery socket if one is configured.
pub const fn ipv4(listen_config: &ListenConfig) -> Option<SocketAddrV4> {
pub fn ipv4(listen_config: &ListenConfig) -> Option<SocketAddrV4> {
match listen_config {
ListenConfig::Ipv4 { ip, port } |
ListenConfig::DualStack { ipv4: ip, ipv4_port: port, .. } => {
Some(SocketAddrV4::new(*ip, *port))
}
ListenConfig::Ipv6 { .. } => None,
ListenConfig::FromSockets { ipv4: Some(s), .. } => match s.local_addr().ok()? {
SocketAddr::V4(addr) => Some(addr),
SocketAddr::V6(_) => None,
},
_ => None,
}
}
/// Returns the IPv6 discovery socket if one is configured.
pub const fn ipv6(listen_config: &ListenConfig) -> Option<SocketAddrV6> {
pub fn ipv6(listen_config: &ListenConfig) -> Option<SocketAddrV6> {
match listen_config {
ListenConfig::Ipv4 { .. } => None,
ListenConfig::Ipv6 { ip, port } |
ListenConfig::DualStack { ipv6: ip, ipv6_port: port, .. } => {
Some(SocketAddrV6::new(*ip, *port, 0, 0))
}
ListenConfig::FromSockets { ipv6: Some(s), .. } => match s.local_addr().ok()? {
SocketAddr::V6(addr) => Some(addr),
SocketAddr::V4(_) => None,
},
_ => None,
}
}

View File

@@ -18,7 +18,6 @@ use std::{
use ::enr::Enr;
use alloy_primitives::bytes::Bytes;
use discv5::ListenConfig;
use enr::{discv4_id_to_discv5_id, EnrCombinedKeyWrapper};
use futures::future::join_all;
use itertools::Itertools;
@@ -247,7 +246,9 @@ impl Discv5 {
match update {
discv5::Event::SocketUpdated(_) | discv5::Event::TalkRequest(_) |
// `Discovered` not unique discovered peers
discv5::Event::Discovered(_) => None,
discv5::Event::Discovered(_) |
// Unrecognized frames are handled separately by the discovery layer
discv5::Event::UnrecognizedFrame(_) => None,
discv5::Event::NodeInserted { .. } => {
// node has been inserted into kbuckets
@@ -472,39 +473,33 @@ pub fn build_local_enr(
let Config { discv5_config, fork, tcp_socket, other_enr_kv_pairs, .. } = config;
let socket = match discv5_config.listen_config {
ListenConfig::Ipv4 { ip, port } => {
if ip != Ipv4Addr::UNSPECIFIED {
builder.ip4(ip);
}
builder.udp4(port);
builder.tcp4(tcp_socket.port());
let socket = {
let v4 = crate::config::ipv4(&discv5_config.listen_config);
let v6 = crate::config::ipv6(&discv5_config.listen_config);
(ip, port).into()
}
ListenConfig::Ipv6 { ip, port } => {
if ip != Ipv6Addr::UNSPECIFIED {
builder.ip6(ip);
if let Some(addr) = v4 {
if *addr.ip() != Ipv4Addr::UNSPECIFIED {
builder.ip4(*addr.ip());
}
builder.udp6(port);
builder.udp4(addr.port());
}
if let Some(addr) = v6 {
if *addr.ip() != Ipv6Addr::UNSPECIFIED {
builder.ip6(*addr.ip());
}
builder.udp6(addr.port());
}
// Advertise tcp4 when v4 is configured, else tcp6.
if v4.is_some() {
builder.tcp4(tcp_socket.port());
} else if v6.is_some() {
builder.tcp6(tcp_socket.port());
(ip, port).into()
}
ListenConfig::DualStack { ipv4, ipv4_port, ipv6, ipv6_port } => {
if ipv4 != Ipv4Addr::UNSPECIFIED {
builder.ip4(ipv4);
}
builder.udp4(ipv4_port);
builder.tcp4(tcp_socket.port());
if ipv6 != Ipv6Addr::UNSPECIFIED {
builder.ip6(ipv6);
}
builder.udp6(ipv6_port);
(ipv6, ipv6_port).into()
}
// Prefer v6 when both are configured
v6.map(SocketAddr::V6)
.or_else(|| v4.map(SocketAddr::V4))
.unwrap_or_else(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)))
};
let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
@@ -711,6 +706,7 @@ mod test {
#![allow(deprecated)]
use super::*;
use ::enr::{CombinedKey, EnrKey};
use discv5::ListenConfig;
use rand_08::thread_rng;
use reth_chainspec::MAINNET;
use std::{

View File

@@ -25,7 +25,6 @@ alloy-eips.workspace = true
alloy-primitives = { workspace = true, features = ["map"] }
alloy-rlp = { workspace = true, features = ["derive"] }
alloy-consensus.workspace = true
alloy-trie.workspace = true
bytes.workspace = true
derive_more.workspace = true
@@ -63,7 +62,6 @@ std = [
"alloy-rlp/std",
"bytes/std",
"derive_more/std",
"alloy-trie/std",
"reth-ethereum-primitives/std",
"reth-primitives-traits/std",
"serde?/std",
@@ -82,7 +80,6 @@ arbitrary = [
"alloy-consensus/arbitrary",
"alloy-eips/arbitrary",
"alloy-primitives/arbitrary",
"alloy-trie/arbitrary",
"reth-primitives-traits/arbitrary",
]
serde = [
@@ -91,7 +88,6 @@ serde = [
"alloy-consensus/serde",
"alloy-eips/serde",
"alloy-primitives/serde",
"alloy-trie/serde",
"bytes/serde",
"rand/serde",
"reth-primitives-traits/serde",

View File

@@ -23,9 +23,8 @@ pub struct GetBlockAccessLists(
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[add_arbitrary_tests(rlp)]
pub struct BlockAccessLists(
/// The requested block access lists as raw RLP blobs. Missing-entry encoding is protocol
/// specific: eth/71 uses the RLP empty list (`0xc0`), while snap/2 uses the RLP empty string
/// (`0x80`).
/// The requested block access lists as raw RLP blobs. Per EIP-8159, unavailable entries are
/// represented by an RLP-encoded empty list (`0xc0`).
pub Vec<Bytes>,
);
@@ -58,6 +57,9 @@ impl Decodable for BlockAccessLists {
while !payload.is_empty() {
let item_start = payload;
let item_header = Header::decode(&mut payload)?;
if !item_header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let item_length = item_header.length_with_payload();
bals.push(Bytes::copy_from_slice(&item_start[..item_length]));
@@ -169,14 +171,9 @@ mod tests {
}
#[test]
fn accepts_snap_missing_bal_entries() {
let decoded =
alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, alloy_rlp::EMPTY_STRING_CODE])
.unwrap();
assert_eq!(
decoded,
BlockAccessLists(vec![Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE])])
);
fn rejects_non_list_bal_entries() {
let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, 0x01]).unwrap_err();
assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
}
#[test]

View File

@@ -885,6 +885,19 @@ pub struct BlockRangeUpdate {
pub latest_hash: B256,
}
impl InMemorySize for NewPooledTransactionHashes {
fn size(&self) -> usize {
match self {
Self::Eth66(msg) => msg.0.len() * core::mem::size_of::<B256>(),
Self::Eth68(msg) => {
msg.types.len() * core::mem::size_of::<u8>() +
msg.sizes.len() * core::mem::size_of::<usize>() +
msg.hashes.len() * core::mem::size_of::<B256>()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -120,6 +120,11 @@ impl Capability {
Self::eth(EthVersion::Eth71)
}
/// Returns the `snap/1` capability.
pub const fn snap_1() -> Self {
Self::snap(SnapVersion::V1)
}
/// Returns the `snap/2` capability.
pub const fn snap_2() -> Self {
Self::snap(SnapVersion::V2)
@@ -171,18 +176,6 @@ impl Capability {
self.is_eth_v70() ||
self.is_eth_v71()
}
/// Whether this is snap v2.
#[inline]
pub fn is_snap_v2(&self) -> bool {
self.name == "snap" && self.version == 2
}
/// Whether this is any snap version.
#[inline]
pub fn is_snap(&self) -> bool {
self.is_snap_v2()
}
}
impl fmt::Display for Capability {
@@ -218,7 +211,6 @@ pub struct Capabilities {
eth_69: bool,
eth_70: bool,
eth_71: bool,
snap_2: bool,
}
impl Capabilities {
@@ -231,7 +223,6 @@ impl Capabilities {
eth_69: value.iter().any(Capability::is_eth_v69),
eth_70: value.iter().any(Capability::is_eth_v70),
eth_71: value.iter().any(Capability::is_eth_v71),
snap_2: value.iter().any(Capability::is_snap_v2),
inner: value,
}
}
@@ -318,20 +309,6 @@ impl Capabilities {
pub const fn supports_eth_v71(&self) -> bool {
self.eth_71
}
/// Whether this peer supports snap v2.
#[inline]
pub const fn supports_snap_v2(&self) -> bool {
self.snap_2
}
/// Returns true if this peer advertises the requested snap protocol version.
#[inline]
pub const fn supports_snap_version(&self, version: SnapVersion) -> bool {
match version {
SnapVersion::V2 => self.snap_2,
}
}
}
impl From<Vec<Capability>> for Capabilities {
@@ -357,7 +334,6 @@ impl Decodable for Capabilities {
eth_69: inner.iter().any(Capability::is_eth_v69),
eth_70: inner.iter().any(Capability::is_eth_v70),
eth_71: inner.iter().any(Capability::is_eth_v71),
snap_2: inner.iter().any(Capability::is_snap_v2),
inner,
})
}

View File

@@ -829,11 +829,11 @@ where
mod tests {
use super::MessageError;
use crate::{
message::RequestPair, BlockAccessLists, BlockRangeUpdate, EthMessage, EthMessageID,
EthNetworkPrimitives, EthVersion, GetBlockAccessLists, GetNodeData, NodeData,
ProtocolMessage, RawCapabilityMessage,
message::RequestPair, BlockAccessLists, EthMessage, EthMessageID, EthNetworkPrimitives,
EthVersion, GetBlockAccessLists, GetNodeData, NodeData, ProtocolMessage,
RawCapabilityMessage,
};
use alloy_primitives::{hex, B256};
use alloy_primitives::hex;
use alloy_rlp::{Decodable, Encodable, Error};
use reth_ethereum_primitives::BlockBody;
@@ -874,25 +874,6 @@ mod tests {
#[test]
fn test_bal_message_version_gating() {
let block_range_update =
EthMessage::<EthNetworkPrimitives>::BlockRangeUpdate(BlockRangeUpdate {
earliest: 1,
latest: 2,
latest_hash: B256::random(),
});
let buf = encode(ProtocolMessage {
message_type: EthMessageID::BlockRangeUpdate,
message: block_range_update,
});
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
EthVersion::Eth68,
&mut &buf[..],
);
assert!(matches!(
msg,
Err(MessageError::Invalid(EthVersion::Eth68, EthMessageID::BlockRangeUpdate))
));
let get_block_access_lists =
EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
request_id: 1337,

View File

@@ -3,16 +3,12 @@
//! facilitating the exchange of Ethereum state snapshots between peers
//! Reference: [Ethereum Snapshot Protocol](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#protocol-messages)
//!
//! This module currently includes the snap/2 message definitions used by this branch.
//! This module currently includes snap/1 plus preparatory snap/2 message definitions.
use crate::BlockAccessLists;
use alloc::vec::Vec;
use alloy_primitives::{bytes::Buf, Bytes, B256, U256};
use alloy_rlp::{
BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable, EMPTY_LIST_CODE,
};
pub use alloy_trie::TrieAccount;
use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY};
use alloy_primitives::{Bytes, B256};
use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
use reth_codecs_derive::add_arbitrary_tests;
/// Supported SNAP protocol versions.
@@ -20,8 +16,10 @@ use reth_codecs_derive::add_arbitrary_tests;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum SnapVersion {
/// BAL-based healing as proposed by EIP-8189.
/// The original snapshot protocol.
#[default]
V1 = 1,
/// BAL-based healing as proposed by EIP-8189.
V2 = 2,
}
@@ -29,6 +27,7 @@ impl SnapVersion {
/// Returns the number of messages supported by this version.
pub const fn message_count(self) -> u8 {
match self {
Self::V1 => 8,
Self::V2 => 10,
}
}
@@ -55,49 +54,24 @@ pub enum SnapMessageId {
GetByteCodes = 0x04,
/// Response for the number of requested contract codes.
ByteCodes = 0x05,
/// Request of the number of state (either account or storage) Merkle trie nodes by path.
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
GetTrieNodes = 0x06,
/// Response for the number of requested state trie nodes.
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
TrieNodes = 0x07,
/// Request BALs for a list of block hashes.
///
/// Only valid for `snap/2`.
GetBlockAccessLists = 0x08,
/// Response containing BALs for the requested block hashes.
///
/// Only valid for `snap/2`.
BlockAccessLists = 0x09,
}
impl SnapMessageId {
/// Returns true if this message id is valid for snap/2.
pub const fn is_valid_for_version(self, version: SnapVersion) -> bool {
match version {
SnapVersion::V2 => matches!(
self,
Self::GetAccountRange |
Self::AccountRange |
Self::GetStorageRanges |
Self::StorageRanges |
Self::GetByteCodes |
Self::ByteCodes |
Self::GetBlockAccessLists |
Self::BlockAccessLists
),
}
}
}
impl TryFrom<u8> for SnapMessageId {
type Error = alloy_rlp::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Self::GetAccountRange),
0x01 => Ok(Self::AccountRange),
0x02 => Ok(Self::GetStorageRanges),
0x03 => Ok(Self::StorageRanges),
0x04 => Ok(Self::GetByteCodes),
0x05 => Ok(Self::ByteCodes),
0x08 => Ok(Self::GetBlockAccessLists),
0x09 => Ok(Self::BlockAccessLists),
_ => Err(alloy_rlp::Error::Custom("Unknown message ID")),
}
}
}
/// Request for a range of accounts from the state trie.
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getaccountrange-0x00
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
@@ -117,119 +91,14 @@ pub struct GetAccountRangeMessage {
}
/// Account data in the response.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct AccountData {
/// Hash of the account address (trie path)
pub hash: B256,
/// Account trie value.
pub account: TrieAccount,
}
impl Encodable for AccountData {
fn encode(&self, out: &mut dyn BufMut) {
self.as_wire().encode(out);
}
fn length(&self) -> usize {
self.as_wire().length()
}
}
impl Decodable for AccountData {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
AccountDataWire::decode(buf).and_then(TryInto::try_into)
}
}
impl AccountData {
/// Returns the encoded byte length of this account's snap slim body.
pub fn account_body_len(account: TrieAccount) -> usize {
snap_account_payload_length(&account).length_with_payload()
}
fn as_wire(&self) -> AccountDataWire {
AccountDataWire { hash: self.hash, body: encode_account_body(self.account).into() }
}
}
#[derive(RlpEncodable, RlpDecodable)]
struct AccountDataWire {
hash: B256,
body: Bytes,
}
impl TryFrom<AccountDataWire> for AccountData {
type Error = alloy_rlp::Error;
fn try_from(value: AccountDataWire) -> Result<Self, Self::Error> {
let account = decode_account_body(&value.body)?;
Ok(Self { hash: value.hash, account })
}
}
fn encode_account_body(account: TrieAccount) -> Vec<u8> {
let mut out = Vec::with_capacity(AccountData::account_body_len(account));
snap_account_payload_length(&account).encode(&mut out);
account.nonce.encode(&mut out);
account.balance.encode(&mut out);
encode_slim_hash(account.storage_root, EMPTY_ROOT_HASH, &mut out);
encode_slim_hash(account.code_hash, KECCAK_EMPTY, &mut out);
out
}
fn decode_account_body(mut buf: &[u8]) -> alloy_rlp::Result<TrieAccount> {
let header = Header::decode(&mut buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let initial_len = buf.len();
let nonce = u64::decode(&mut buf)?;
let balance = U256::decode(&mut buf)?;
let storage_root = decode_slim_hash(&mut buf, EMPTY_ROOT_HASH)?;
let code_hash = decode_slim_hash(&mut buf, KECCAK_EMPTY)?;
let consumed = initial_len - buf.len();
if consumed != header.payload_length || !buf.is_empty() {
return Err(alloy_rlp::Error::UnexpectedLength)
}
Ok(TrieAccount { nonce, balance, storage_root, code_hash })
}
fn snap_account_payload_length(account: &TrieAccount) -> Header {
let payload_length = account.nonce.length() +
account.balance.length() +
slim_hash_length(account.storage_root, EMPTY_ROOT_HASH) +
slim_hash_length(account.code_hash, KECCAK_EMPTY);
Header { list: true, payload_length }
}
fn slim_hash_length(hash: B256, empty_hash: B256) -> usize {
if hash == empty_hash {
1
} else {
hash.length()
}
}
fn encode_slim_hash(hash: B256, empty_hash: B256, out: &mut dyn BufMut) {
if hash == empty_hash {
out.put_u8(EMPTY_LIST_CODE);
} else {
hash.encode(out);
}
}
fn decode_slim_hash(buf: &mut &[u8], empty_hash: B256) -> alloy_rlp::Result<B256> {
if buf.first().copied() == Some(EMPTY_LIST_CODE) {
buf.advance(1);
Ok(empty_hash)
} else {
B256::decode(buf)
}
/// Account body in slim format
pub body: Bytes,
}
/// Response containing a number of consecutive accounts and the Merkle proofs for the entire range.
@@ -277,25 +146,6 @@ pub struct StorageData {
pub data: Bytes,
}
impl StorageData {
/// Creates storage data from a decoded storage value.
pub fn from_value(hash: B256, value: U256) -> Self {
let mut data = Vec::new();
value.encode(&mut data);
Self { hash, data: data.into() }
}
/// Decodes this slot's RLP-encoded storage value.
pub fn decode_value(&self) -> alloy_rlp::Result<U256> {
let mut buf = self.data.as_ref();
let value = U256::decode(&mut buf)?;
if !buf.is_empty() {
return Err(alloy_rlp::Error::Custom("trailing bytes after storage value"))
}
Ok(value)
}
}
/// Response containing a number of consecutive storage slots for the requested account
/// and optionally the merkle proofs for the last range (boundary proofs) if it only partially
/// covers the storage trie.
@@ -338,6 +188,45 @@ pub struct ByteCodesMessage {
pub codes: Vec<Bytes>,
}
/// Path in the trie for an account and its storage
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct TriePath {
/// Path in the account trie
pub account_path: Bytes,
/// Paths in the storage trie
pub slot_paths: Vec<Bytes>,
}
/// Request a number of state (either account or storage) Merkle trie nodes by path
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#gettrienodes-0x06
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct GetTrieNodesMessage {
/// Request ID to match up responses with
pub request_id: u64,
/// Root hash of the account trie to serve
pub root_hash: B256,
/// Trie paths to retrieve the nodes for, grouped by account
pub paths: Vec<TriePath>,
/// Soft limit at which to stop returning data (in bytes)
pub response_bytes: u64,
}
/// Response containing a number of requested state trie nodes
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#trienodes-0x07
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct TrieNodesMessage {
/// ID of the request this is a response for
pub request_id: u64,
/// The requested trie nodes in order
pub nodes: Vec<Bytes>,
}
/// Request BALs for the given block hashes.
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
@@ -377,9 +266,21 @@ pub enum SnapProtocolMessage {
GetByteCodes(GetByteCodesMessage),
/// Response with contract codes - see [`ByteCodesMessage`]
ByteCodes(ByteCodesMessage),
/// Request for trie nodes - see [`GetTrieNodesMessage`]
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
GetTrieNodes(GetTrieNodesMessage),
/// Response with trie nodes - see [`TrieNodesMessage`]
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
TrieNodes(TrieNodesMessage),
/// Request for block access lists - see [`GetBlockAccessListsMessage`]
///
/// Only valid for `snap/2`.
GetBlockAccessLists(GetBlockAccessListsMessage),
/// Response with block access lists - see [`BlockAccessListsMessage`]
///
/// Only valid for `snap/2`.
BlockAccessLists(BlockAccessListsMessage),
}
@@ -395,6 +296,8 @@ impl SnapProtocolMessage {
Self::StorageRanges(_) => SnapMessageId::StorageRanges,
Self::GetByteCodes(_) => SnapMessageId::GetByteCodes,
Self::ByteCodes(_) => SnapMessageId::ByteCodes,
Self::GetTrieNodes(_) => SnapMessageId::GetTrieNodes,
Self::TrieNodes(_) => SnapMessageId::TrieNodes,
Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
}
@@ -414,6 +317,8 @@ impl SnapProtocolMessage {
Self::StorageRanges(msg) => msg.encode(&mut buf),
Self::GetByteCodes(msg) => msg.encode(&mut buf),
Self::ByteCodes(msg) => msg.encode(&mut buf),
Self::GetTrieNodes(msg) => msg.encode(&mut buf),
Self::TrieNodes(msg) => msg.encode(&mut buf),
Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
Self::BlockAccessLists(msg) => msg.encode(&mut buf),
}
@@ -423,26 +328,6 @@ impl SnapProtocolMessage {
/// Decodes a SNAP protocol message from its message ID and RLP-encoded body.
pub fn decode(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
Self::decode_unchecked(message_id, buf)
}
/// Decodes a SNAP protocol message for the negotiated snap protocol version.
pub fn decode_with_version(
version: SnapVersion,
message_id: u8,
buf: &mut &[u8],
) -> Result<Self, alloy_rlp::Error> {
let id = SnapMessageId::try_from(message_id)?;
if !id.is_valid_for_version(version) {
return Err(alloy_rlp::Error::Custom("Invalid message ID for snap version"));
}
Self::decode_unchecked(message_id, buf)
}
fn decode_unchecked(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
let _ = SnapMessageId::try_from(message_id)?;
// Decoding protocol message variants based on message ID
macro_rules! decode_snap_message_variant {
($message_id:expr, $buf:expr, $id:expr, $variant:ident, $msg_type:ty) => {
@@ -495,6 +380,20 @@ impl SnapProtocolMessage {
ByteCodes,
ByteCodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
SnapMessageId::GetTrieNodes,
GetTrieNodes,
GetTrieNodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
SnapMessageId::TrieNodes,
TrieNodes,
TrieNodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
@@ -539,6 +438,7 @@ mod tests {
#[test]
fn test_all_message_roundtrips() {
assert_eq!(SnapVersion::V1.message_count(), 8);
assert_eq!(SnapVersion::V2.message_count(), 10);
test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
@@ -553,12 +453,7 @@ mod tests {
request_id: 42,
accounts: vec![AccountData {
hash: b256_from_u64(123),
account: TrieAccount {
nonce: 7,
balance: U256::from(42),
storage_root: b256_from_u64(456),
code_hash: b256_from_u64(789),
},
body: Bytes::from(vec![1, 2, 3]),
}],
proof: vec![Bytes::from(vec![4, 5, 6])],
}));
@@ -592,6 +487,21 @@ mod tests {
codes: vec![Bytes::from(vec![1, 2, 3])],
}));
test_roundtrip(SnapProtocolMessage::GetTrieNodes(GetTrieNodesMessage {
request_id: 42,
root_hash: b256_from_u64(123),
paths: vec![TriePath {
account_path: Bytes::from(vec![1, 2, 3]),
slot_paths: vec![Bytes::from(vec![4, 5, 6])],
}],
response_bytes: 1024,
}));
test_roundtrip(SnapProtocolMessage::TrieNodes(TrieNodesMessage {
request_id: 42,
nodes: vec![Bytes::from(vec![1, 2, 3])],
}));
test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
request_id: 42,
block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
@@ -620,61 +530,5 @@ mod tests {
if let Err(e) = result {
assert_eq!(e.to_string(), "Unknown message ID");
}
for removed_id in [0x06, 0x07] {
let mut buf = data.as_ref();
let result = SnapProtocolMessage::decode(removed_id, &mut buf);
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.to_string(), "Unknown message ID");
}
}
}
#[test]
fn storage_data_decodes_value() {
let storage = StorageData::from_value(b256_from_u64(1), U256::from(99));
assert_eq!(storage.decode_value().unwrap(), U256::from(99));
}
#[test]
fn snap_account_uses_empty_list_sentinels() {
let account = TrieAccount {
nonce: 1,
balance: U256::from(2),
storage_root: alloy_trie::EMPTY_ROOT_HASH,
code_hash: alloy_trie::KECCAK_EMPTY,
};
let encoded = encode_account_body(account);
assert_eq!(
encoded,
vec![0xc4, 0x01, 0x02, alloy_rlp::EMPTY_LIST_CODE, alloy_rlp::EMPTY_LIST_CODE]
);
assert_eq!(decode_account_body(&encoded).unwrap(), account);
}
#[test]
fn account_data_encodes_snap_account_body() {
let data = AccountData {
hash: b256_from_u64(1),
account: TrieAccount {
nonce: 1,
balance: U256::from(2),
storage_root: alloy_trie::EMPTY_ROOT_HASH,
code_hash: alloy_trie::KECCAK_EMPTY,
},
};
let encoded = alloy_rlp::encode(data.clone());
let decoded = alloy_rlp::decode_exact::<AccountData>(&encoded).unwrap();
assert_eq!(decoded, data);
let wire = AccountDataWire::decode(&mut &encoded[..]).unwrap();
assert_eq!(
wire.body.as_ref(),
&[0xc4, 0x01, 0x02, alloy_rlp::EMPTY_LIST_CODE, alloy_rlp::EMPTY_LIST_CODE]
);
}
}

View File

@@ -39,6 +39,12 @@ where
}
}
impl InMemorySize for GetPooledTransactions {
fn size(&self) -> usize {
self.0.len() * core::mem::size_of::<B256>()
}
}
/// The response to [`GetPooledTransactions`], containing the transaction bodies associated with
/// the requested hashes.
///

View File

@@ -35,11 +35,10 @@ pub enum EthVersion {
impl EthVersion {
/// The latest known eth version
pub const LATEST: Self = Self::Eth70;
pub const LATEST: Self = Self::Eth69;
/// All known eth versions
pub const ALL_VERSIONS: &'static [Self] =
&[Self::Eth71, Self::Eth70, Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
pub const ALL_VERSIONS: &'static [Self] = &[Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
/// Returns true if the version is eth/66
pub const fn is_eth66(&self) -> bool {

View File

@@ -247,7 +247,7 @@ where
{
/// Create a new eth and snap protocol stream
const fn new(eth_version: EthVersion) -> Self {
Self::new_with_snap_version(eth_version, SnapVersion::V2)
Self::new_with_snap_version(eth_version, SnapVersion::V1)
}
/// Create a new eth and snap protocol stream with an explicit snap version.
@@ -257,7 +257,7 @@ where
/// Create a new eth and snap protocol stream with a custom max message size.
const fn with_max_message_size(eth_version: EthVersion, max_message_size: usize) -> Self {
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V2, max_message_size)
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V1, max_message_size)
}
/// Create a new eth and snap protocol stream with a custom max message size and snap version.
@@ -327,11 +327,7 @@ where
let adjusted_message_id = message_id - EthMessageID::message_count(self.eth_version);
let mut buf = &bytes[1..];
match SnapProtocolMessage::decode_with_version(
self.snap_version,
adjusted_message_id,
&mut buf,
) {
match SnapProtocolMessage::decode(adjusted_message_id, &mut buf) {
Ok(snap_msg) => Ok(EthSnapMessage::Snap(snap_msg)),
Err(err) => Err(EthSnapStreamError::Rlp(err)),
}

View File

@@ -198,17 +198,14 @@ impl HelloMessageBuilder {
/// Unset fields will be set to their default values:
/// - `protocol_version`: [`ProtocolVersion::V5`]
/// - `client_version`: [`RETH_CLIENT_VERSION`]
/// - `capabilities`: All [`EthVersion`] and snap/2
/// - `capabilities`: All [`EthVersion`]
pub fn build(self) -> HelloMessageWithProtocols {
let Self { protocol_version, client_version, protocols, port, id } = self;
HelloMessageWithProtocols {
protocol_version: protocol_version.unwrap_or_default(),
client_version: client_version.unwrap_or_else(|| RETH_CLIENT_VERSION.to_string()),
protocols: protocols.unwrap_or_else(|| {
let mut protocols =
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect::<Vec<_>>();
protocols.push(Protocol::snap_2());
protocols
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect()
}),
port: port.unwrap_or(DEFAULT_TCP_PORT),
id,
@@ -277,9 +274,6 @@ mod tests {
.iter()
.any(|p| p.cap.name == "eth" && p.cap.version == EthVersion::Eth69 as usize);
assert!(has_eth69, "Default protocols should include Eth69");
let has_snap2 = hello.protocols.iter().any(|p| p.cap == Capability::snap_2());
assert!(has_snap2, "Default protocols should include snap/2");
}
#[test]

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