mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
30 Commits
bal-devnet
...
snapv2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d545d1bd5 | ||
|
|
35e901ea51 | ||
|
|
af1b99f0d7 | ||
|
|
a46ef1cd91 | ||
|
|
7708ac59e7 | ||
|
|
f7863c766b | ||
|
|
8324ee1173 | ||
|
|
00f6c75f06 | ||
|
|
2fce3e701d | ||
|
|
938313028d | ||
|
|
0790359003 | ||
|
|
089c0e2629 | ||
|
|
bc80e2a66b | ||
|
|
9af8265047 | ||
|
|
f3e30a3111 | ||
|
|
6715a093f1 | ||
|
|
d5bff1d478 | ||
|
|
20141a2ea0 | ||
|
|
57962a1b95 | ||
|
|
60468fe256 | ||
|
|
ce3a171ce0 | ||
|
|
ce7e80ad33 | ||
|
|
0c6a10d3fa | ||
|
|
d41a9a4078 | ||
|
|
b984ddd275 | ||
|
|
b9c330e1a9 | ||
|
|
cd10e6b47c | ||
|
|
7b2c458302 | ||
|
|
0722202930 | ||
|
|
8ec6e614f9 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,14 +4,10 @@ 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:
|
||||
|
||||
19
.github/scripts/bench-reth-run.sh
vendored
19
.github/scripts/bench-reth-run.sh
vendored
@@ -323,18 +323,13 @@ if [ "$BIG_BLOCKS" = "true" ]; then
|
||||
--output "$OUTPUT_DIR" 2>&1 | sed -u "s/^/[bench] /"
|
||||
else
|
||||
# Standard mode: warmup + new-payload-fcu
|
||||
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
|
||||
# Warmup
|
||||
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
|
||||
--rpc-url "$BENCH_RPC_URL" \
|
||||
--engine-rpc-url http://127.0.0.1:8551 \
|
||||
--jwt-secret "$DATADIR/jwt.hex" \
|
||||
--advance "${BENCH_WARMUP_BLOCKS:-50}" \
|
||||
"${EXTRA_BENCH_ARGS[@]}" 2>&1 | sed -u "s/^/[bench] /"
|
||||
|
||||
# Start tracy-capture after warmup so profile only covers the benchmark
|
||||
if [ "${BENCH_TRACY:-off}" != "off" ]; then
|
||||
|
||||
4
.github/scripts/hive/build_simulators.sh
vendored
4
.github/scripts/hive/build_simulators.sh
vendored
@@ -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@v5.6.1/fixtures_bal.tar.gz"
|
||||
eels_branch="devnets/bal/3"
|
||||
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"
|
||||
;;
|
||||
osaka)
|
||||
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz"
|
||||
|
||||
35
.github/scripts/hive/expected_failures.yaml
vendored
35
.github/scripts/hive/expected_failures.yaml
vendored
@@ -140,6 +140,21 @@ 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
|
||||
@@ -241,23 +256,3 @@ 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
|
||||
|
||||
54
.github/workflows/bench-scheduled.yml
vendored
54
.github/workflows/bench-scheduled.yml
vendored
@@ -54,7 +54,9 @@ env:
|
||||
|
||||
name: bench-scheduled
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -63,9 +65,6 @@ 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 }}
|
||||
@@ -77,26 +76,21 @@ jobs:
|
||||
long-running: ${{ steps.refs.outputs.long-running }}
|
||||
release-tag: ${{ steps.refs.outputs.release-tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
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 [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
MODE="${INPUT_MODE:-nightly}"
|
||||
elif [ "$SCHEDULE" = "30 5 * * *" ]; then
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
MODE="${{ inputs.mode || 'nightly' }}"
|
||||
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
|
||||
MODE="nightly"
|
||||
elif [ "$SCHEDULE" = "0 9 * * *" ]; then
|
||||
elif [ "${{ github.event.schedule }}" = "0 9 * * *" ]; then
|
||||
MODE="release"
|
||||
else
|
||||
MODE="hourly"
|
||||
@@ -111,15 +105,14 @@ jobs:
|
||||
DEREK_TOKEN: ${{ secrets.DEREK_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
INPUT_FORCE: ${{ inputs.force || 'false' }}
|
||||
run: |
|
||||
FORCE="${INPUT_FORCE:-false}"
|
||||
FORCE="${{ inputs.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -161,7 +154,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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -249,9 +242,6 @@ 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
|
||||
@@ -280,16 +270,15 @@ jobs:
|
||||
- name: Clean up previous bench-work
|
||||
run: sudo rm -rf "$BENCH_WORK_DIR" 2>/dev/null || true
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||
@@ -302,9 +291,8 @@ jobs:
|
||||
core.exportVariable('BENCH_JOB_URL', jobUrl);
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
continue-on-error: true
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
@@ -640,7 +628,7 @@ jobs:
|
||||
|
||||
- name: Upload results
|
||||
if: "!cancelled()"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bench-scheduled-results
|
||||
path: ${{ env.BENCH_WORK_DIR }}
|
||||
@@ -648,12 +636,10 @@ 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:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
|
||||
CHARTS_REPO="https://x-access-token:${{ secrets.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
|
||||
@@ -674,7 +660,7 @@ jobs:
|
||||
|
||||
- name: Write job summary
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -753,7 +739,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (success)
|
||||
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -908,7 +894,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (failure)
|
||||
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
|
||||
130
.github/workflows/bench.yml
vendored
130
.github/workflows/bench.yml
vendored
@@ -82,7 +82,7 @@ on:
|
||||
- on-error
|
||||
- never
|
||||
abba:
|
||||
description: "Run ABBA (FBBF) interleaved order; false = single FB pass"
|
||||
description: "Run ABBA (BFFB) interleaved order; false = single AB pass"
|
||||
required: false
|
||||
default: "true"
|
||||
type: boolean
|
||||
@@ -99,7 +99,9 @@ env:
|
||||
|
||||
name: bench
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
reth-bench-ack:
|
||||
@@ -108,9 +110,6 @@ 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 }}
|
||||
@@ -134,7 +133,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check org membership
|
||||
if: github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -153,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Parse arguments
|
||||
id: args
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -360,7 +359,7 @@ jobs:
|
||||
|
||||
- name: Acknowledge request
|
||||
id: ack
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -446,7 +445,7 @@ jobs:
|
||||
|
||||
- name: Poll queue position
|
||||
if: steps.ack.outputs.comment-id && steps.ack.outputs.queue-position != '0'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -530,9 +529,6 @@ 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
|
||||
@@ -564,7 +560,7 @@ jobs:
|
||||
|
||||
- name: Resolve checkout ref
|
||||
id: checkout-ref
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (!process.env.BENCH_PR) {
|
||||
@@ -582,16 +578,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -639,9 +634,8 @@ jobs:
|
||||
});
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
continue-on-error: true
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
@@ -702,7 +696,7 @@ jobs:
|
||||
# Build binaries
|
||||
- name: Resolve PR head branch
|
||||
id: pr-info
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (process.env.BENCH_PR) {
|
||||
@@ -720,7 +714,7 @@ jobs:
|
||||
|
||||
- name: Resolve refs
|
||||
id: refs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { execSync } = require('child_process');
|
||||
@@ -943,28 +937,15 @@ jobs:
|
||||
|
||||
- name: Update status (running benchmarks)
|
||||
if: success() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
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 (F-B-B-F) to reduce systematic bias from
|
||||
# Interleaved run order (B-F-F-B) to reduce systematic bias from
|
||||
# thermal drift and cache warming.
|
||||
- name: "Run benchmark: feature (1/2)"
|
||||
id: run-feature-1
|
||||
env:
|
||||
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-1,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
|
||||
run: |
|
||||
LAST_RUN_START=$(date +%s)
|
||||
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"feature-1","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${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-1"
|
||||
|
||||
- name: "Run benchmark: baseline (1/2)"
|
||||
id: run-baseline-1
|
||||
env:
|
||||
@@ -978,19 +959,18 @@ jobs:
|
||||
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: baseline (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
id: run-baseline-2
|
||||
- name: "Run benchmark: feature (1/2)"
|
||||
id: run-feature-1
|
||||
env:
|
||||
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-2,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
|
||||
FEATURE_REF: ${{ steps.refs.outputs.feature-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=feature-1,run_type=feature,git_ref=${{ steps.refs.outputs.feature-ref }}"
|
||||
run: |
|
||||
LAST_RUN_START=$(date +%s)
|
||||
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
{"benchmark_run":"feature-1","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 baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
|
||||
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'
|
||||
@@ -1006,6 +986,20 @@ jobs:
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
|
||||
|
||||
- name: "Run benchmark: baseline (2/2)"
|
||||
if: env.BENCH_ABBA != 'false'
|
||||
id: run-baseline-2
|
||||
env:
|
||||
BASELINE_REF: ${{ steps.refs.outputs.baseline-ref }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: "benchmark_id=${{ env.BENCH_ID }},benchmark_run=baseline-2,run_type=baseline,git_ref=${{ steps.refs.outputs.baseline-ref }}"
|
||||
run: |
|
||||
LAST_RUN_START=$(date +%s)
|
||||
echo "BENCH_LAST_RUN_START=${LAST_RUN_START}" >> "$GITHUB_ENV"
|
||||
cat > "$BENCH_LABELS_FILE" <<LABELS
|
||||
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
|
||||
LABELS
|
||||
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
|
||||
|
||||
- name: Stop metrics proxy & generate Grafana URL
|
||||
id: metrics
|
||||
if: "!cancelled()"
|
||||
@@ -1172,7 +1166,7 @@ jobs:
|
||||
|
||||
- name: Upload results
|
||||
if: "!cancelled()"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bench-reth-results
|
||||
path: ${{ env.BENCH_WORK_DIR }}
|
||||
@@ -1180,13 +1174,11 @@ 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:${DEREK_TOKEN}@github.com/decofe/reth-bench-charts.git"
|
||||
CHARTS_REPO="https://x-access-token:${{ secrets.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
|
||||
@@ -1199,35 +1191,15 @@ 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}"
|
||||
|
||||
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
|
||||
|
||||
git -C "${TMP_DIR}" push origin HEAD:main
|
||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -1263,7 +1235,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 ? ['feature-1', 'baseline-1', 'baseline-2', 'feature-2'] : ['feature-1', 'baseline-1'];
|
||||
const runs = abba ? ['baseline-1', 'feature-1', 'feature-2', 'baseline-2'] : ['baseline-1', 'feature-1'];
|
||||
const links = [];
|
||||
for (const run of runs) {
|
||||
try {
|
||||
@@ -1307,7 +1279,7 @@ jobs:
|
||||
|
||||
- name: Write job summary
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const jobSummary = require('./.github/scripts/bench-job-summary.js');
|
||||
@@ -1321,7 +1293,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (success)
|
||||
if: success() && (env.BENCH_SLACK == 'always' || env.BENCH_SLACK == 'on-win')
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -1332,7 +1304,7 @@ jobs:
|
||||
|
||||
- name: Update status (failed)
|
||||
if: failure() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
@@ -1342,10 +1314,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 feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
];
|
||||
const failed = steps_status.find(([, o]) => o === 'failure');
|
||||
const failedStep = failed ? failed[0] : 'unknown step';
|
||||
@@ -1368,7 +1340,7 @@ jobs:
|
||||
|
||||
- name: Send Slack notification (failure)
|
||||
if: failure() && env.BENCH_SLACK != 'never' && env.BENCH_SLACK != 'on-win'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
|
||||
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
|
||||
@@ -1380,10 +1352,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 feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
['running baseline benchmark (1/2)', '${{ steps.run-baseline-1.outcome }}'],
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
['running feature benchmark (1/2)', '${{ steps.run-feature-1.outcome }}'],
|
||||
...(abba ? [['running feature benchmark (2/2)', '${{ steps.run-feature-2.outcome }}']] : []),
|
||||
...(abba ? [['running baseline benchmark (2/2)', '${{ steps.run-baseline-2.outcome }}']] : []),
|
||||
];
|
||||
const failed = steps_status.find(([, o]) => o === 'failure');
|
||||
const failedStep = failed ? failed[0] : 'unknown step';
|
||||
@@ -1392,7 +1364,7 @@ jobs:
|
||||
|
||||
- name: Update status (cancelled)
|
||||
if: cancelled() && env.BENCH_COMMENT_ID
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.DEREK_PAT }}
|
||||
script: |
|
||||
|
||||
19
.github/workflows/book.yml
vendored
19
.github/workflows/book.yml
vendored
@@ -10,22 +10,19 @@ on:
|
||||
types: [opened, reopened, synchronize, closed]
|
||||
merge_group:
|
||||
|
||||
permissions: {}
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: v1.2.23
|
||||
|
||||
@@ -39,6 +36,8 @@ 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
|
||||
|
||||
@@ -48,10 +47,10 @@ jobs:
|
||||
echo "Vocs Build Complete"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: "./docs/vocs/docs/dist"
|
||||
|
||||
@@ -75,4 +74,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
26
.github/workflows/check-alloy.yml
vendored
26
.github/workflows/check-alloy.yml
vendored
@@ -22,41 +22,31 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
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 "$ALLOY_BRANCH" ]; then
|
||||
ARGS="$ARGS --alloy $ALLOY_BRANCH"
|
||||
if [ -n "${{ inputs.alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --alloy ${{ inputs.alloy_branch }}"
|
||||
fi
|
||||
if [ -n "$ALLOY_EVM_BRANCH" ]; then
|
||||
ARGS="$ARGS --evm $ALLOY_EVM_BRANCH"
|
||||
if [ -n "${{ inputs.alloy_evm_branch }}" ]; then
|
||||
ARGS="$ARGS --evm ${{ inputs.alloy_evm_branch }}"
|
||||
fi
|
||||
if [ -n "$OP_ALLOY_BRANCH" ]; then
|
||||
ARGS="$ARGS --op $OP_ALLOY_BRANCH"
|
||||
if [ -n "${{ inputs.op_alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --op ${{ inputs.op_alloy_branch }}"
|
||||
fi
|
||||
|
||||
if [ -z "$ARGS" ]; then
|
||||
|
||||
16
.github/workflows/compact.yml
vendored
16
.github/workflows/compact.yml
vendored
@@ -16,38 +16,32 @@ 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@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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
|
||||
|
||||
9
.github/workflows/dependencies.yml
vendored
9
.github/workflows/dependencies.yml
vendored
@@ -9,14 +9,13 @@ on:
|
||||
workflow_dispatch:
|
||||
# Needed so we can run it manually
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
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 }}
|
||||
14
.github/workflows/docker-tag-latest.yml
vendored
14
.github/workflows/docker-tag-latest.yml
vendored
@@ -17,8 +17,6 @@ on:
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tag-reth-latest:
|
||||
name: Tag reth as latest
|
||||
@@ -29,22 +27,16 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Log in to Docker
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "$DOCKER_PASSWORD" | docker login ghcr.io --username "${DOCKER_USERNAME}" --password-stdin
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | 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:${VERSION}
|
||||
docker pull ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }}
|
||||
|
||||
- name: Tag reth as latest
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository_owner }}/reth:${VERSION} ghcr.io/${{ github.repository_owner }}/reth:latest
|
||||
docker tag ghcr.io/${{ github.repository_owner }}/reth:${{ inputs.version }} ghcr.io/${{ github.repository_owner }}/reth:latest
|
||||
|
||||
- name: Push reth latest tag
|
||||
run: |
|
||||
|
||||
22
.github/workflows/docker-test.yml
vendored
22
.github/workflows/docker-test.yml
vendored
@@ -13,8 +13,6 @@ on:
|
||||
default: "artifacts"
|
||||
description: "Name for the uploaded artifact"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 45
|
||||
@@ -23,9 +21,7 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- run: mkdir -p artifacts
|
||||
|
||||
- name: Get git info
|
||||
@@ -36,12 +32,8 @@ 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 [ "$EVENT_NAME" = "pull_request" ] && [ "$HEAD_REPO" != "$REPO" ]; then
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -50,11 +42,11 @@ jobs:
|
||||
# Depot build (upstream only)
|
||||
- name: Set up Depot CLI
|
||||
if: steps.fork.outputs.is_fork == 'false'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Build reth image (Depot)
|
||||
if: steps.fork.outputs.is_fork == 'false'
|
||||
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
|
||||
uses: depot/bake-action@v1
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
@@ -68,11 +60,11 @@ jobs:
|
||||
# Docker build (forks)
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.fork.outputs.is_fork == 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build reth image (Docker)
|
||||
if: steps.fork.outputs.is_fork == 'true'
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0
|
||||
uses: docker/bake-action@v6
|
||||
env:
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
@@ -84,7 +76,7 @@ jobs:
|
||||
*.dockerfile=Dockerfile
|
||||
|
||||
- name: Upload reth image
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
|
||||
23
.github/workflows/docker.yml
vendored
23
.github/workflows/docker.yml
vendored
@@ -29,8 +29,6 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
@@ -41,15 +39,13 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -64,13 +60,10 @@ 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 [[ "${EVENT_NAME}" == "push" ]]; then
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -88,7 +81,7 @@ jobs:
|
||||
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
elif [[ "${EVENT_NAME}" == "schedule" ]] || [[ "${BUILD_TYPE}" == "nightly" ]]; then
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
|
||||
echo "targets=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
@@ -101,7 +94,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push images
|
||||
uses: depot/bake-action@1d58c2668346981089b088b7ef36755b206b20e9 # v1.13.0
|
||||
uses: depot/bake-action@v1
|
||||
env:
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
@@ -112,8 +105,6 @@ 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 }}
|
||||
|
||||
@@ -133,7 +124,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'schedule'
|
||||
steps:
|
||||
- name: Slack Webhook Action
|
||||
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_COLOR: danger
|
||||
SLACK_ICON_EMOJI: ":rotating_light:"
|
||||
|
||||
30
.github/workflows/e2e.yml
vendored
30
.github/workflows/e2e.yml
vendored
@@ -17,27 +17,19 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run e2e tests
|
||||
@@ -56,21 +48,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run RocksDB e2e tests
|
||||
|
||||
24
.github/workflows/fetch-grafana-dashboard.yml
vendored
24
.github/workflows/fetch-grafana-dashboard.yml
vendored
@@ -12,8 +12,6 @@ on:
|
||||
required: true
|
||||
default: "etc/grafana/dashboards/overview.json"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
fetch:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -21,11 +19,9 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -33,18 +29,14 @@ 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 "${DASHBOARD_UID}" \
|
||||
> "${TARGET_PATH}"
|
||||
python3 .github/scripts/fetch-grafana-dashboard.py "${{ inputs.dashboard_uid }}" \
|
||||
> "${{ inputs.target_path }}"
|
||||
|
||||
- name: Check for changes
|
||||
id: diff
|
||||
env:
|
||||
TARGET_PATH: ${{ inputs.target_path }}
|
||||
run: |
|
||||
if git diff --quiet "${TARGET_PATH}"; then
|
||||
if git diff --quiet "${{ inputs.target_path }}"; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes detected."
|
||||
else
|
||||
@@ -55,10 +47,8 @@ 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="${TARGET_PATH}"
|
||||
TARGET="${{ inputs.target_path }}"
|
||||
FILENAME="$(basename "$TARGET")"
|
||||
BRANCH="chore/sync-grafana-${FILENAME%.*}-$(date +%Y%m%d-%H%M%S)"
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -69,4 +59,4 @@ jobs:
|
||||
git push origin "$BRANCH"
|
||||
gh pr create \
|
||||
--title "chore: update Grafana dashboard ${FILENAME}" \
|
||||
--body "Automated export from Grafana (dashboard UID: \`${DASHBOARD_UID}\`, target: \`${TARGET}\`)."
|
||||
--body "Automated export from Grafana (dashboard UID: \`${{ inputs.dashboard_uid }}\`, target: \`${TARGET}\`)."
|
||||
|
||||
8
.github/workflows/grafana.yml
vendored
8
.github/workflows/grafana.yml
vendored
@@ -6,17 +6,11 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-dashboard:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- name: Validate dashboard format
|
||||
run: |
|
||||
python3 -c "
|
||||
|
||||
75
.github/workflows/hive.yml
vendored
75
.github/workflows/hive.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -14,13 +17,8 @@ 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
|
||||
@@ -28,11 +26,9 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
if: github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -41,28 +37,25 @@ jobs:
|
||||
- osaka
|
||||
name: Prepare Hive - ${{ matrix.variant == 'amsterdam' && 'Amsterdam' || 'Osaka' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.13.1"
|
||||
- run: go version
|
||||
|
||||
- name: Restore hive assets cache
|
||||
id: cache-hive
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ./hive_assets
|
||||
key: hive-assets-${{ matrix.variant }}-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
|
||||
@@ -85,7 +78,7 @@ jobs:
|
||||
chmod +x hive
|
||||
|
||||
- name: Upload hive assets
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hive_assets_${{ matrix.variant }}
|
||||
path: ./hive_assets
|
||||
@@ -201,24 +194,22 @@ 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' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
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' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hive_assets_amsterdam
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: reth
|
||||
path: /tmp
|
||||
@@ -232,21 +223,16 @@ jobs:
|
||||
chmod +x /usr/local/bin/hive
|
||||
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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="$SCENARIO_LIMIT"
|
||||
TESTS="$SCENARIO_TESTS"
|
||||
LIMIT="${{ matrix.scenario.limit }}"
|
||||
TESTS="${{ join(matrix.scenario.include, '|') }}"
|
||||
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
|
||||
FILTER="$LIMIT/$TESTS"
|
||||
elif [ -n "$LIMIT" ]; then
|
||||
@@ -257,7 +243,7 @@ jobs:
|
||||
FILTER="/"
|
||||
fi
|
||||
echo "filter: $FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "amsterdam"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "amsterdam"
|
||||
|
||||
- name: Parse hive output
|
||||
run: |
|
||||
@@ -381,24 +367,22 @@ 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' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
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' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download hive assets
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hive_assets_osaka
|
||||
path: /tmp
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: reth
|
||||
path: /tmp
|
||||
@@ -412,21 +396,16 @@ jobs:
|
||||
chmod +x /usr/local/bin/hive
|
||||
|
||||
- name: Checkout hive tests
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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="$SCENARIO_LIMIT"
|
||||
TESTS="$SCENARIO_TESTS"
|
||||
LIMIT="${{ matrix.scenario.limit }}"
|
||||
TESTS="${{ join(matrix.scenario.include, '|') }}"
|
||||
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
|
||||
FILTER="$LIMIT/$TESTS"
|
||||
elif [ -n "$LIMIT" ]; then
|
||||
@@ -437,7 +416,7 @@ jobs:
|
||||
FILTER="/"
|
||||
fi
|
||||
echo "filter: $FILTER"
|
||||
.github/scripts/hive/run_simulator.sh "$SCENARIO_SIM" "$FILTER" "osaka"
|
||||
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "osaka"
|
||||
|
||||
- name: Parse hive output
|
||||
run: |
|
||||
@@ -460,7 +439,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Webhook Action
|
||||
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
|
||||
|
||||
36
.github/workflows/integration.yml
vendored
36
.github/workflows/integration.yml
vendored
@@ -20,15 +20,11 @@ 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:
|
||||
@@ -36,18 +32,14 @@ jobs:
|
||||
network: ["ethereum"]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Geth
|
||||
run: .github/scripts/install_geth.sh
|
||||
- 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
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run tests
|
||||
@@ -66,7 +58,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -74,19 +66,13 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: run era1 files integration tests
|
||||
|
||||
16
.github/workflows/kurtosis.yml
vendored
16
.github/workflows/kurtosis.yml
vendored
@@ -18,14 +18,9 @@ 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
|
||||
@@ -37,18 +32,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: artifacts
|
||||
path: /tmp
|
||||
@@ -60,7 +52,7 @@ jobs:
|
||||
docker image ls -a
|
||||
|
||||
- name: Run kurtosis
|
||||
uses: ethpandaops/kurtosis-assertoor-github-action@f64942cbc780df731a731ea9f45765b161d2c8df # v1.0.1
|
||||
uses: ethpandaops/kurtosis-assertoor-github-action@v1
|
||||
with:
|
||||
ethereum_package_args: ".github/assets/kurtosis_network_params.yaml"
|
||||
|
||||
@@ -70,7 +62,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Webhook Action
|
||||
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
|
||||
|
||||
8
.github/workflows/label-pr.yml
vendored
8
.github/workflows/label-pr.yml
vendored
@@ -4,23 +4,19 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Label PRs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const label_pr = require('./.github/scripts/label_pr.js')
|
||||
|
||||
8
.github/workflows/lint-actions.yml
vendored
8
.github/workflows/lint-actions.yml
vendored
@@ -8,17 +8,11 @@ on:
|
||||
paths:
|
||||
- '.github/**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download actionlint
|
||||
id: get_actionlint
|
||||
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
||||
|
||||
190
.github/workflows/lint.yml
vendored
190
.github/workflows/lint.yml
vendored
@@ -10,14 +10,10 @@ 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:
|
||||
@@ -26,19 +22,17 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@clippy
|
||||
with:
|
||||
components: clippy
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- if: "${{ matrix.type == 'book' }}"
|
||||
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run clippy on binaries
|
||||
@@ -49,19 +43,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: clippy
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
|
||||
@@ -70,25 +60,19 @@ jobs:
|
||||
|
||||
wasm:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-wasip1
|
||||
- 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
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
|
||||
- uses: dcarbone/install-jq-action@v3
|
||||
- name: Run Wasm checks
|
||||
run: |
|
||||
sudo apt update && sudo apt install gcc-multilib
|
||||
@@ -96,49 +80,37 @@ jobs:
|
||||
|
||||
riscv:
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: riscv32imac-unknown-none-elf
|
||||
- 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
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
|
||||
- uses: dcarbone/install-jq-action@v3
|
||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo hack check --workspace --partition ${{ matrix.partition }}/${{ matrix.total_partitions }}
|
||||
@@ -146,19 +118,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.93" # MSRV
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo build --bin reth --workspace
|
||||
@@ -168,17 +136,13 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo docs --document-private-items
|
||||
@@ -190,56 +154,42 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: mozilla-actions/sccache-action@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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
|
||||
with:
|
||||
tool: cargo-udeps
|
||||
- uses: taiki-e/install-action@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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo build --bin reth --workspace
|
||||
@@ -251,54 +201,38 @@ jobs:
|
||||
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0
|
||||
- uses: actions/checkout@v6
|
||||
- uses: crate-ci/typos@v1
|
||||
|
||||
check-toml:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v6
|
||||
- name: Run dprint
|
||||
uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3
|
||||
uses: dprint/check@v2.3
|
||||
with:
|
||||
config-path: dprint.json
|
||||
|
||||
grafana:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check dashboard JSON with jq
|
||||
uses: sergeysova/jq-action@a3f0d4ff59cc1dddf023fc0b325dd75b10deec58 # v2.3.0
|
||||
uses: sergeysova/jq-action@v2
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@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
|
||||
@@ -306,17 +240,13 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: taiki-e/cache-cargo-install-action@v3
|
||||
with:
|
||||
tool: zepter
|
||||
- name: Eagerly pull dependencies
|
||||
@@ -324,8 +254,6 @@ jobs:
|
||||
- run: zepter run check
|
||||
|
||||
deny:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: tempoxyz/ci/.github/workflows/deny.yml@main
|
||||
|
||||
lint-success:
|
||||
@@ -349,6 +277,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
23
.github/workflows/pr-audit.yml
vendored
23
.github/workflows/pr-audit.yml
vendored
@@ -4,36 +4,27 @@ 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 "$EVENTS_KEY" > "${{ runner.temp }}/key"
|
||||
echo "$EVENTS_CERT" > "${{ runner.temp }}/cert"
|
||||
echo "${{ secrets.EVENTS_KEY }}" > ${{ runner.temp }}/key
|
||||
echo "${{ secrets.EVENTS_CERT }}" > ${{ runner.temp }}/cert
|
||||
|
||||
curl -sf -o /dev/null -X POST $EVENTS_ARGS \
|
||||
curl -sf -o /dev/null -X POST ${{ secrets.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": '"$PR_NUMBER"',
|
||||
"sha": "'"$PR_SHA"'"
|
||||
"pr_number": ${{ github.event.pull_request.number }},
|
||||
"sha": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
}'
|
||||
|
||||
11
.github/workflows/pr-title.yml
vendored
11
.github/workflows/pr-title.yml
vendored
@@ -8,19 +8,20 @@ on:
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
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@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -39,7 +40,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@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
@@ -75,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Remove Comment for Valid Title
|
||||
if: steps.lint_pr_title.outcome == 'success'
|
||||
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
5
.github/workflows/release-dist.yml
vendored
5
.github/workflows/release-dist.yml
vendored
@@ -7,15 +7,12 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
release-homebrew:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Update Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@1446dca236b0440c6f02723a3f14f13be2c04ab0 # v7
|
||||
uses: dawidd6/action-homebrew-bump-formula@v7
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW }}
|
||||
no_fork: true
|
||||
|
||||
24
.github/workflows/release-reproducible.yml
vendored
24
.github/workflows/release-reproducible.yml
vendored
@@ -2,8 +2,6 @@
|
||||
|
||||
name: release-reproducible
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [release]
|
||||
@@ -17,23 +15,20 @@ 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 == \"${HEAD_SHA}\") | .ref" \
|
||||
--jq '.[] | select(.object.sha == "${{ github.event.workflow_run.head_sha }}") | .ref' \
|
||||
| head -1 \
|
||||
| sed 's|refs/tags/||')
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "No tag found for SHA ${HEAD_SHA}"
|
||||
echo "No tag found for SHA ${{ github.event.workflow_run.head_sha }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -49,16 +44,15 @@ jobs:
|
||||
packages: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.extract-version.outputs.VERSION }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -71,7 +65,7 @@ jobs:
|
||||
echo "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build reproducible artifacts
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@v6
|
||||
id: docker_build
|
||||
with:
|
||||
context: .
|
||||
@@ -81,11 +75,13 @@ 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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.reproducible
|
||||
@@ -96,6 +92,8 @@ 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
|
||||
|
||||
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@@ -3,8 +3,6 @@
|
||||
|
||||
name: release
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -22,24 +20,21 @@ 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:
|
||||
- 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 )"
|
||||
- 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'}}"
|
||||
|
||||
extract-version:
|
||||
name: extract version
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Extract version
|
||||
run: echo "VERSION=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT
|
||||
@@ -50,15 +45,12 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- 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
|
||||
@@ -71,8 +63,6 @@ jobs:
|
||||
build:
|
||||
name: build release
|
||||
runs-on: ${{ matrix.configs.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
needs: extract-version
|
||||
continue-on-error: ${{ matrix.configs.allow_fail }}
|
||||
strategy:
|
||||
@@ -105,20 +95,20 @@ jobs:
|
||||
- command: build
|
||||
binary: reth
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@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 \
|
||||
--rev 65fe72b0cdb1e7e0cc0652517498d4389cc8f5cf
|
||||
cargo install cross --locked --git https://github.com/cross-rs/cross
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Apple M1 setup
|
||||
if: matrix.configs.target == 'aarch64-apple-darwin'
|
||||
@@ -155,14 +145,14 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
|
||||
|
||||
- name: Upload signature
|
||||
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
|
||||
@@ -181,12 +171,11 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
- name: Generate full changelog
|
||||
id: changelog
|
||||
run: |
|
||||
@@ -272,7 +261,6 @@ 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:
|
||||
|
||||
16
.github/workflows/reproducible-build.yml
vendored
16
.github/workflows/reproducible-build.yml
vendored
@@ -5,15 +5,11 @@ 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:
|
||||
@@ -22,16 +18,14 @@ jobs:
|
||||
- runner: ubuntu-22.04
|
||||
machine: machine-2
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build reproducible binary with Docker
|
||||
run: |
|
||||
@@ -49,7 +43,7 @@ jobs:
|
||||
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
|
||||
|
||||
- name: Upload the hash
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: checksum-${{ matrix.machine }}
|
||||
path: |
|
||||
@@ -62,12 +56,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts from machine-1
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: checksum-machine-1
|
||||
path: machine-1/
|
||||
- name: Download artifacts from machine-2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: checksum-machine-2
|
||||
path: machine-2/
|
||||
|
||||
14
.github/workflows/stage.yml
vendored
14
.github/workflows/stage.yml
vendored
@@ -18,28 +18,22 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Build reth
|
||||
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
@@ -17,7 +15,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 21
|
||||
days-before-close: 7
|
||||
|
||||
14
.github/workflows/sync-era.yml
vendored
14
.github/workflows/sync-era.yml
vendored
@@ -15,15 +15,11 @@ 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
|
||||
@@ -38,13 +34,11 @@ jobs:
|
||||
block: 100000
|
||||
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Build ${{ matrix.chain.bin }}
|
||||
|
||||
14
.github/workflows/sync.yml
vendored
14
.github/workflows/sync.yml
vendored
@@ -15,15 +15,11 @@ 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
|
||||
@@ -38,13 +34,11 @@ jobs:
|
||||
block: 100000
|
||||
unwind-target: "0x52e0509d33a988ef807058e2980099ee3070187f7333aae12b64d4d675f34c5a"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Build ${{ matrix.chain.bin }}
|
||||
|
||||
53
.github/workflows/unit.yml
vendored
53
.github/workflows/unit.yml
vendored
@@ -17,14 +17,10 @@ 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:
|
||||
@@ -36,20 +32,16 @@ jobs:
|
||||
exclude_args: ""
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 # v2.75.24
|
||||
with:
|
||||
tool: nextest
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- if: "${{ matrix.type == 'book' }}"
|
||||
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run tests
|
||||
@@ -64,25 +56,20 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout ethereum/tests
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v6
|
||||
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:
|
||||
@@ -92,13 +79,11 @@ 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@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- 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
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo nextest run --no-fail-fast --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
|
||||
@@ -106,19 +91,15 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Run doctests
|
||||
@@ -132,6 +113,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
413
Cargo.lock
generated
413
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
41
Cargo.toml
41
Cargo.toml
@@ -29,6 +29,7 @@ members = [
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/execution-cache/",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/snap/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
"crates/era-downloader",
|
||||
@@ -345,6 +346,7 @@ 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" }
|
||||
@@ -433,14 +435,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.0", default-features = false }
|
||||
revm-state = { version = "11.0.0", default-features = false }
|
||||
revm-primitives = { version = "23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "11.0.0", default-features = false }
|
||||
revm-inspectors = "0.39.0"
|
||||
revm = { version = "=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"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -581,7 +583,7 @@ tower = "0.5"
|
||||
tower-http = "0.6"
|
||||
|
||||
# p2p
|
||||
discv5 = { git = "https://github.com/sigp/discv5", rev = "7663c00" }
|
||||
discv5 = "0.10"
|
||||
if-addrs = "0.14"
|
||||
|
||||
# rpc
|
||||
@@ -700,3 +702,24 @@ 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" }
|
||||
|
||||
@@ -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:24.04 AS runtime
|
||||
FROM ubuntu AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Copy reth over from the build stage
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//! `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::{
|
||||
@@ -16,7 +15,7 @@ use alloy_evm::{
|
||||
},
|
||||
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
|
||||
precompiles::PrecompilesMap,
|
||||
Database, EthEvm, EthEvmFactory, Evm, EvmFactory, FromRecoveredTx, FromTxWithEncoded,
|
||||
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
|
||||
};
|
||||
use alloy_primitives::B256;
|
||||
use reth_ethereum_primitives::{Receipt, TransactionSigned};
|
||||
@@ -117,7 +116,6 @@ 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,
|
||||
@@ -147,6 +145,21 @@ 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,
|
||||
@@ -434,9 +447,6 @@ 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
|
||||
@@ -614,10 +624,7 @@ where
|
||||
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
|
||||
type Transaction = TransactionSigned;
|
||||
type Receipt = Receipt;
|
||||
type TxExecutionResult = EthTxResult<
|
||||
<EthEvmFactory as EvmFactory>::HaltReason,
|
||||
<TransactionSigned as TransactionEnvelope>::TxType,
|
||||
>;
|
||||
type TxExecutionResult = EthTxResult<HaltReason, alloy_consensus::TxType>;
|
||||
type Executor<'a, DB: StateDB, I: Inspector<EthEvmContext<DB>>> =
|
||||
BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>;
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ use crate::{
|
||||
BigBlockMap,
|
||||
};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_evm::{
|
||||
eth::{spec::EthExecutorSpec, EthBlockExecutionCtx},
|
||||
EthEvmFactory,
|
||||
};
|
||||
use alloy_evm::eth::EthBlockExecutionCtx;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rpc_types::engine::ExecutionData;
|
||||
use core::convert::Infallible;
|
||||
@@ -23,8 +20,8 @@ use reth_chainspec::{ChainSpec, EthChainSpec};
|
||||
use reth_ethereum_forks::Hardforks;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_evm::{
|
||||
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, EvmEnvFor, ExecutableTxIterator,
|
||||
ExecutionCtxFor, NextBlockEnvAttributes,
|
||||
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
|
||||
NextBlockEnvAttributes,
|
||||
};
|
||||
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
|
||||
use reth_primitives_traits::{SealedBlock, SealedHeader};
|
||||
@@ -32,6 +29,9 @@ 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -477,8 +477,8 @@ pub enum ConsensusError {
|
||||
#[error(transparent)]
|
||||
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
|
||||
/// Error when an unexpected block access list cost is encountered.
|
||||
#[error("block access list cost exceeds gas limit")]
|
||||
BlockAccessListCostMoreThanGasLimit,
|
||||
#[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>),
|
||||
@@ -538,7 +538,7 @@ pub fn validate_block_access_list_gas(
|
||||
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::BlockAccessListCostMoreThanGasLimit)
|
||||
return Err(ConsensusError::BlockAccessListExceedsGasLimit)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -14,8 +14,12 @@ 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
|
||||
|
||||
@@ -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;
|
||||
use reth_network_api::{test_utils::PeersHandleProvider, BlockDownloaderProvider};
|
||||
use reth_node_builder::{
|
||||
components::NodeComponentsBuilder,
|
||||
rpc::{EngineValidatorAddOn, RethRpcAddOns},
|
||||
@@ -34,6 +34,8 @@ pub mod setup_import;
|
||||
/// Helper for network operations
|
||||
mod network;
|
||||
|
||||
/// Snap sync utilities for E2E tests.
|
||||
|
||||
/// Helper for rpc operations
|
||||
mod rpc;
|
||||
|
||||
@@ -153,7 +155,11 @@ where
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
Components: NodeComponents<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
Network: PeersHandleProvider,
|
||||
Network: PeersHandleProvider
|
||||
+ BlockDownloaderProvider<
|
||||
Client: reth_network_p2p::snap::client::SnapClient
|
||||
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
AddOns: RethRpcAddOns<
|
||||
@@ -175,7 +181,11 @@ impl<T> NodeBuilderHelper for T where
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
Components: NodeComponents<
|
||||
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
|
||||
Network: PeersHandleProvider,
|
||||
Network: PeersHandleProvider
|
||||
+ BlockDownloaderProvider<
|
||||
Client: reth_network_p2p::snap::client::SnapClient
|
||||
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
AddOns: RethRpcAddOns<
|
||||
|
||||
@@ -8,6 +8,7 @@ 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};
|
||||
@@ -15,12 +16,13 @@ use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
|
||||
use reth_payload_primitives::BuiltPayload;
|
||||
use reth_provider::{
|
||||
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
|
||||
HeaderProvider, StageCheckpointReader,
|
||||
DatabaseProviderFactory, 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;
|
||||
@@ -312,7 +314,7 @@ where
|
||||
self.inner
|
||||
.add_ons_handle
|
||||
.beacon_engine_handle
|
||||
.new_payload(Payload::block_to_payload(payload.block().clone()))
|
||||
.new_payload(Payload::built_payload_to_execution_data(&payload))
|
||||
.await?;
|
||||
|
||||
Ok(block_hash)
|
||||
@@ -365,4 +367,26 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
65
crates/engine/snap/Cargo.toml
Normal file
65
crates/engine/snap/Cargo.toml
Normal file
@@ -0,0 +1,65 @@
|
||||
[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",
|
||||
]
|
||||
332
crates/engine/snap/src/bal.rs
Normal file
332
crates/engine/snap/src/bal.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
//! 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])));
|
||||
}
|
||||
}
|
||||
314
crates/engine/snap/src/controller.rs
Normal file
314
crates/engine/snap/src/controller.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
650
crates/engine/snap/src/download.rs
Normal file
650
crates/engine/snap/src/download.rs
Normal file
@@ -0,0 +1,650 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
68
crates/engine/snap/src/finalize.rs
Normal file
68
crates/engine/snap/src/finalize.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! 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(())
|
||||
}
|
||||
118
crates/engine/snap/src/lib.rs
Normal file
118
crates/engine/snap/src/lib.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! 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
|
||||
/// HEAD−N, 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),
|
||||
}
|
||||
277
crates/engine/snap/src/orchestrator.rs
Normal file
277
crates/engine/snap/src/orchestrator.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
//! 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() })
|
||||
}
|
||||
}
|
||||
310
crates/engine/snap/src/pivot.rs
Normal file
310
crates/engine/snap/src/pivot.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
//! 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
440
crates/engine/snap/src/proof.rs
Normal file
440
crates/engine/snap/src/proof.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
//! 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 { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
481
crates/engine/snap/src/serve.rs
Normal file
481
crates/engine/snap/src/serve.rs
Normal file
@@ -0,0 +1,481 @@
|
||||
//! 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)
|
||||
}
|
||||
184
crates/engine/snap/src/storage.rs
Normal file
184
crates/engine/snap/src/storage.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
@@ -29,6 +30,7 @@ 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
|
||||
@@ -70,7 +72,6 @@ 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 }
|
||||
@@ -119,6 +120,7 @@ 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",
|
||||
@@ -134,6 +136,7 @@ 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",
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
//! These modes are mutually exclusive and the node can only be in one mode at a time.
|
||||
|
||||
use futures::FutureExt;
|
||||
use reth_provider::providers::ProviderNodeTypes;
|
||||
use reth_engine_snap::controller::{SnapSyncControl, SnapSyncControlEvent, SnapSyncController};
|
||||
use reth_provider::{providers::ProviderNodeTypes, ProviderFactory};
|
||||
use reth_stages_api::{ControlFlow, Pipeline, PipelineError, PipelineTarget, PipelineWithResult};
|
||||
use reth_tasks::Runtime;
|
||||
use std::task::{ready, Context, Poll};
|
||||
use std::{
|
||||
fmt,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::trace;
|
||||
|
||||
@@ -60,6 +64,11 @@ 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.
|
||||
@@ -74,6 +83,10 @@ 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.
|
||||
@@ -110,7 +123,7 @@ impl<N: ProviderNodeTypes> PipelineSync<N> {
|
||||
}
|
||||
|
||||
/// Returns `true` if the pipeline is active.
|
||||
const fn is_pipeline_active(&self) -> bool {
|
||||
pub(crate) const fn is_pipeline_active(&self) -> bool {
|
||||
!self.is_pipeline_idle()
|
||||
}
|
||||
|
||||
@@ -181,6 +194,7 @@ 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(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +240,111 @@ 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::*;
|
||||
|
||||
@@ -106,6 +106,23 @@ 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 => {}
|
||||
}
|
||||
@@ -160,6 +177,10 @@ 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
|
||||
@@ -175,6 +196,12 @@ 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")
|
||||
}
|
||||
@@ -225,6 +252,10 @@ 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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! [`ChainOrchestrator`](crate::chain::ChainOrchestrator) ready to be polled as a `Stream`.
|
||||
|
||||
use crate::{
|
||||
backfill::PipelineSync,
|
||||
backfill::EngineBackfillSync,
|
||||
chain::ChainOrchestrator,
|
||||
download::BasicBlockDownloader,
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
|
||||
@@ -71,15 +71,21 @@ pub fn build_engine_orchestrator<N, Client, S, V, C>(
|
||||
S,
|
||||
BasicBlockDownloader<Client, <N::Primitives as NodePrimitives>::Block>,
|
||||
>,
|
||||
PipelineSync<N>,
|
||||
EngineBackfillSync<N, Client>,
|
||||
>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block> + 'static,
|
||||
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block>
|
||||
+ reth_network_p2p::snap::client::SnapClient
|
||||
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient
|
||||
+ Clone
|
||||
+ '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 =
|
||||
@@ -87,6 +93,7 @@ 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,
|
||||
@@ -104,7 +111,13 @@ where
|
||||
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
|
||||
let handler = EngineHandler::new(engine_handler, downloader, incoming_requests);
|
||||
|
||||
let backfill_sync = PipelineSync::new(pipeline, pipeline_task_spawner);
|
||||
let backfill_sync = EngineBackfillSync::new(
|
||||
pipeline,
|
||||
pipeline_task_spawner,
|
||||
snap_client,
|
||||
snap_provider,
|
||||
snap_runtime,
|
||||
);
|
||||
|
||||
ChainOrchestrator::new(handler, backfill_sync)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use reth_primitives_traits::{
|
||||
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
|
||||
};
|
||||
use reth_provider::{
|
||||
BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
|
||||
BalProvider, BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader,
|
||||
StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader,
|
||||
StorageSettingsCache, TransactionVariant,
|
||||
@@ -58,6 +58,7 @@ pub mod payload_processor;
|
||||
pub mod payload_validator;
|
||||
mod persistence_state;
|
||||
pub mod precompile_cache;
|
||||
mod snap;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod trie_updates;
|
||||
@@ -314,6 +315,8 @@ 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
|
||||
@@ -341,6 +344,7 @@ 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()
|
||||
}
|
||||
}
|
||||
@@ -353,6 +357,7 @@ where
|
||||
+ StateProviderFactory
|
||||
+ StateReader<Receipt = N::Receipt>
|
||||
+ HashedPostStateProvider
|
||||
+ BalProvider
|
||||
+ Clone
|
||||
+ 'static,
|
||||
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
|
||||
@@ -405,6 +410,7 @@ where
|
||||
execution_timing_stats: HashMap::new(),
|
||||
building_payload: false,
|
||||
runtime,
|
||||
snap: snap::SnapTreeState::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,7 +451,9 @@ where
|
||||
kind,
|
||||
);
|
||||
|
||||
let task = Self::new(
|
||||
let fresh_node = best_block_number == 0;
|
||||
|
||||
let mut task = Self::new(
|
||||
provider,
|
||||
consensus,
|
||||
payload_validator,
|
||||
@@ -461,6 +469,8 @@ where
|
||||
changeset_cache,
|
||||
runtime,
|
||||
);
|
||||
task.snap.set_fresh_node(fresh_node);
|
||||
|
||||
let incoming = task.incoming_tx.clone();
|
||||
spawn_os_thread("engine", || {
|
||||
increase_thread_priority();
|
||||
@@ -739,11 +749,18 @@ 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)?;
|
||||
@@ -759,6 +776,13 @@ 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
|
||||
@@ -1126,6 +1150,9 @@ 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));
|
||||
@@ -1321,6 +1348,17 @@ 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
|
||||
@@ -1545,6 +1583,19 @@ 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 {
|
||||
@@ -2790,7 +2841,10 @@ where
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
if !self.backfill_sync_state.is_idle() {
|
||||
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);
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ where
|
||||
/// Whether sparse trie cache pruning is fully disabled.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to disable BAL-based parallel execution (falls back to tx-based prewarming).
|
||||
#[allow(unused)]
|
||||
disable_bal_parallel_execution: bool,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
disable_bal_parallel_state_root: bool,
|
||||
@@ -273,9 +272,7 @@ where
|
||||
halve_workers,
|
||||
config,
|
||||
);
|
||||
// If no BALs are present or we have them explicitly disabled, we use sparse trie task and
|
||||
// need to send the updates to it via state hook
|
||||
let install_state_hook = env.decoded_bal.is_none() || self.disable_bal_parallel_state_root;
|
||||
let install_state_hook = env.decoded_bal.is_none();
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
@@ -508,14 +505,14 @@ where
|
||||
);
|
||||
{
|
||||
let to_prewarm_task = to_prewarm_task.clone();
|
||||
let disable_bal_parallel_state_root = self.disable_bal_parallel_state_root;
|
||||
let disable_bal_parallel_execution = self.disable_bal_parallel_execution;
|
||||
self.executor.spawn_blocking_named("prewarm", move || {
|
||||
let mode = if let Some(decoded_bal) =
|
||||
maybe_decoded_bal.filter(|_| !disable_bal_parallel_state_root)
|
||||
let mode = if skip_prewarm {
|
||||
PrewarmMode::Skipped
|
||||
} else if let Some(decoded_bal) =
|
||||
maybe_decoded_bal.filter(|_| !disable_bal_parallel_execution)
|
||||
{
|
||||
PrewarmMode::BlockAccessList(decoded_bal)
|
||||
} else if skip_prewarm {
|
||||
PrewarmMode::Skipped
|
||||
} else {
|
||||
PrewarmMode::Transactions(transactions)
|
||||
};
|
||||
@@ -801,7 +798,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
|
||||
/// Returns a state hook to stream execution state updates to the sparse trie cache task.
|
||||
///
|
||||
/// Returns `None` when BAL-driven hashed state streaming feeds the sparse trie task.
|
||||
/// Returns `None` when execution should not send state updates, such as BAL-driven execution.
|
||||
pub fn state_hook(&self) -> Option<impl OnStateHook> {
|
||||
self.install_state_hook
|
||||
.then(|| self.state_root_handle.as_ref().map(|handle| handle.state_hook()))
|
||||
@@ -1162,19 +1159,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
};
|
||||
account.storage = storage;
|
||||
account.status = AccountStatus::Touched;
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
|
||||
@@ -1115,7 +1115,6 @@ where
|
||||
executor.evm_mut().db_mut().bump_bal_index();
|
||||
}
|
||||
}
|
||||
|
||||
drop(exec_span);
|
||||
|
||||
Ok((executor, senders))
|
||||
|
||||
@@ -266,6 +266,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: Bytes::default(),
|
||||
})
|
||||
})
|
||||
@@ -280,6 +281,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
|
||||
};
|
||||
|
||||
@@ -314,6 +316,7 @@ 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"),
|
||||
})
|
||||
}
|
||||
@@ -331,6 +334,7 @@ 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"),
|
||||
})
|
||||
}
|
||||
|
||||
174
crates/engine/tree/src/tree/snap.rs
Normal file
174
crates/engine/tree/src/tree/snap.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! 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(())
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,10 @@ 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>
|
||||
@@ -94,4 +98,20 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,10 @@ 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
|
||||
|
||||
@@ -54,13 +54,19 @@ 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
|
||||
|
||||
@@ -112,4 +118,7 @@ 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",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ 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};
|
||||
@@ -294,3 +295,194 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -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, PeersInfo};
|
||||
use reth_network::types::NatResolver;
|
||||
use reth_node_builder::{NodeBuilder, NodeHandle};
|
||||
use reth_node_core::{
|
||||
args::{NetworkArgs, RpcServerArgs},
|
||||
@@ -375,47 +375,3 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -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: None,
|
||||
slot_number: Some(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +443,7 @@ 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(
|
||||
@@ -487,9 +487,9 @@ 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)
|
||||
// add blob sidecars from the executed txs
|
||||
.with_sidecars(blob_sidecars);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
//! Helper aliases when working with [`ConfigureEvm`] and the traits in this crate.
|
||||
|
||||
use crate::ConfigureEvm;
|
||||
use alloy_evm::{
|
||||
block::{BlockExecutorFactory, BlockExecutorFor},
|
||||
Database, EvmEnv, EvmFactory,
|
||||
};
|
||||
use revm::{database::State, inspector::NoOpInspector, Inspector};
|
||||
use alloy_evm::{block::BlockExecutorFactory, Database, EvmEnv, EvmFactory};
|
||||
use revm::{inspector::NoOpInspector, Inspector};
|
||||
|
||||
/// Helper to access [`EvmFactory`] for a given [`ConfigureEvm`].
|
||||
pub type EvmFactoryFor<Evm> =
|
||||
@@ -36,10 +33,6 @@ 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>>;
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ 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;
|
||||
|
||||
/// Takes built [`BlockAccessList`] from executor.
|
||||
/// Take built [`BlockAccessList`] from executor
|
||||
fn take_bal(&mut self) -> Option<BlockAccessList>;
|
||||
}
|
||||
|
||||
@@ -471,6 +471,7 @@ where
|
||||
|
||||
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(())
|
||||
@@ -486,6 +487,7 @@ 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 {
|
||||
@@ -504,6 +506,7 @@ 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));
|
||||
|
||||
@@ -20,7 +20,10 @@ extern crate alloc;
|
||||
use crate::execute::{BasicBlockBuilder, Executor};
|
||||
use alloc::vec::Vec;
|
||||
use alloy_eips::eip4895::Withdrawals;
|
||||
use alloy_evm::{block::BlockExecutorFactory, precompiles::PrecompilesMap};
|
||||
use alloy_evm::{
|
||||
block::{BlockExecutorFactory, BlockExecutorFor},
|
||||
precompiles::PrecompilesMap,
|
||||
};
|
||||
use alloy_primitives::{Address, Bytes, B256};
|
||||
use core::{error::Error, fmt::Debug};
|
||||
use execute::{BasicBlockExecutor, BlockAssembler, BlockBuilder};
|
||||
@@ -309,7 +312,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>,
|
||||
) -> BlockExecutorForEvm<'a, Self, DB, I>
|
||||
) -> BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>
|
||||
where
|
||||
DB: Database,
|
||||
I: InspectorFor<Self, &'a mut State<DB>> + 'a,
|
||||
@@ -322,7 +325,8 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
|
||||
&'a self,
|
||||
db: &'a mut State<DB>,
|
||||
block: &'a SealedBlock<<Self::Primitives as NodePrimitives>::Block>,
|
||||
) -> Result<BlockExecutorForEvm<'a, Self, DB>, Self::Error> {
|
||||
) -> Result<BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<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))
|
||||
@@ -348,7 +352,10 @@ 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 = BlockExecutorForEvm<'a, Self, DB, I>>
|
||||
) -> impl BlockBuilder<
|
||||
Primitives = Self::Primitives,
|
||||
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>,
|
||||
>
|
||||
where
|
||||
DB: Database,
|
||||
I: InspectorFor<Self, &'a mut State<DB>> + 'a,
|
||||
@@ -397,7 +404,10 @@ 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 = BlockExecutorForEvm<'a, Self, DB>>,
|
||||
impl BlockBuilder<
|
||||
Primitives = Self::Primitives,
|
||||
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>,
|
||||
>,
|
||||
Self::Error,
|
||||
> {
|
||||
let evm_env = self.next_evm_env(parent, &attributes)?;
|
||||
|
||||
@@ -16,13 +16,10 @@ 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", "reth-primitives-traits"]
|
||||
common = ["tokio", "futures", "tokio-util"]
|
||||
|
||||
@@ -4,13 +4,8 @@
|
||||
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::{
|
||||
@@ -404,147 +399,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ use secp256k1::SecretKey;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, VecDeque},
|
||||
fmt, io,
|
||||
fmt,
|
||||
future::poll_fn,
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
@@ -241,56 +243,17 @@ 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,
|
||||
ingress_tx,
|
||||
ingress_rx,
|
||||
local_addr,
|
||||
local_node_record,
|
||||
secret_key,
|
||||
config,
|
||||
);
|
||||
let mut service =
|
||||
Discv4Service::new(socket, local_addr, local_node_record, secret_key, config);
|
||||
|
||||
// resolve the external address immediately
|
||||
service.resolve_external_ip();
|
||||
@@ -557,25 +520,20 @@ 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: Arc<UdpSocket>,
|
||||
ingress_tx: Option<IngressSender>,
|
||||
ingress_rx: IngressReceiver,
|
||||
socket: UdpSocket,
|
||||
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();
|
||||
|
||||
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(receive_loop(udp, ingress_tx, local_node_record.id));
|
||||
|
||||
let udp = Arc::clone(&socket);
|
||||
tasks.spawn(send_loop(udp, egress_rx));
|
||||
@@ -989,7 +947,7 @@ impl Discv4Service {
|
||||
let key = kad_key(peer_id);
|
||||
match self.kbuckets.entry(&key) {
|
||||
BucketEntry::Present(entry, _) => Some(f(entry.value())),
|
||||
BucketEntry::Pending(entry, _) => Some(f(entry.value())),
|
||||
BucketEntry::Pending(mut entry, _) => Some(f(entry.value())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1015,9 +973,7 @@ impl Discv4Service {
|
||||
kbucket::Entry::Present(mut entry, _) => {
|
||||
entry.value_mut().update_with_enr(last_enr_seq)
|
||||
}
|
||||
kbucket::Entry::Pending(mut entry, _) => {
|
||||
entry.value_mut().update_with_enr(last_enr_seq)
|
||||
}
|
||||
kbucket::Entry::Pending(mut entry, _) => entry.value().update_with_enr(last_enr_seq),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
@@ -1069,8 +1025,8 @@ impl Discv4Service {
|
||||
}
|
||||
kbucket::Entry::Pending(mut entry, mut status) => {
|
||||
// endpoint is now proven
|
||||
entry.value_mut().establish_proof();
|
||||
entry.value_mut().update_with_enr(last_enr_seq);
|
||||
entry.value().establish_proof();
|
||||
entry.value().update_with_enr(last_enr_seq);
|
||||
|
||||
if !status.is_connected() {
|
||||
status.state = ConnectionState::Connected;
|
||||
@@ -1202,7 +1158,7 @@ impl Discv4Service {
|
||||
} else {
|
||||
is_proven = entry.value().has_endpoint_proof;
|
||||
}
|
||||
entry.value_mut().update_with_enr(ping.enr_sq)
|
||||
entry.value().update_with_enr(ping.enr_sq)
|
||||
}
|
||||
kbucket::Entry::Absent(entry) => {
|
||||
let mut node = NodeEntry::new(record);
|
||||
@@ -1432,7 +1388,7 @@ impl Discv4Service {
|
||||
(entry.value().record, id)
|
||||
}
|
||||
kbucket::Entry::Pending(mut entry, _) => {
|
||||
let id = entry.value_mut().update_with_fork_id(fork_id);
|
||||
let id = entry.value().update_with_fork_id(fork_id);
|
||||
(entry.value().record, id)
|
||||
}
|
||||
_ => return,
|
||||
@@ -1582,7 +1538,7 @@ impl Discv4Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
BucketEntry::Pending(entry, _) => {
|
||||
BucketEntry::Pending(mut entry, _) => {
|
||||
if entry.value().has_endpoint_proof {
|
||||
if entry
|
||||
.value()
|
||||
@@ -1686,7 +1642,7 @@ impl Discv4Service {
|
||||
entry.value().find_node_failures
|
||||
}
|
||||
kbucket::Entry::Pending(mut entry, _) => {
|
||||
entry.value_mut().inc_failed_request();
|
||||
entry.value().inc_failed_request();
|
||||
entry.value().find_node_failures
|
||||
}
|
||||
_ => continue,
|
||||
@@ -2006,100 +1962,80 @@ 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 enforces primitive rate limiting for IPs to prevent message spams from
|
||||
/// individual IPs.
|
||||
/// The receive loop enforce 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 mut handler = IngressHandler::new(tx, local_id);
|
||||
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 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.");
|
||||
handler.send(IngressEvent::RecvError(err)).await;
|
||||
send(IngressEvent::RecvError(err)).await;
|
||||
}
|
||||
Ok((read, remote_addr)) => {
|
||||
handler.handle_packet(&buf[..read], remote_addr).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
if self.cache.contains_packet(packet.hash) {
|
||||
debug!(target: "discv4", ?src, "Received duplicate packet.");
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -308,18 +308,6 @@ 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);
|
||||
@@ -345,11 +333,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 {
|
||||
// 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)))
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `RLPx` (TCP) socket contained in the [`discv5::Config`]. This socket will be
|
||||
@@ -360,32 +348,24 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Returns the IPv4 discovery socket if one is configured.
|
||||
pub fn ipv4(listen_config: &ListenConfig) -> Option<SocketAddrV4> {
|
||||
pub const 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::FromSockets { ipv4: Some(s), .. } => match s.local_addr().ok()? {
|
||||
SocketAddr::V4(addr) => Some(addr),
|
||||
SocketAddr::V6(_) => None,
|
||||
},
|
||||
_ => None,
|
||||
ListenConfig::Ipv6 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the IPv6 discovery socket if one is configured.
|
||||
pub fn ipv6(listen_config: &ListenConfig) -> Option<SocketAddrV6> {
|
||||
pub const 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -246,9 +247,7 @@ impl Discv5 {
|
||||
match update {
|
||||
discv5::Event::SocketUpdated(_) | discv5::Event::TalkRequest(_) |
|
||||
// `Discovered` not unique discovered peers
|
||||
discv5::Event::Discovered(_) |
|
||||
// Unrecognized frames are handled separately by the discovery layer
|
||||
discv5::Event::UnrecognizedFrame(_) => None,
|
||||
discv5::Event::Discovered(_) => None,
|
||||
discv5::Event::NodeInserted { .. } => {
|
||||
|
||||
// node has been inserted into kbuckets
|
||||
@@ -473,33 +472,39 @@ pub fn build_local_enr(
|
||||
|
||||
let Config { discv5_config, fork, tcp_socket, other_enr_kv_pairs, .. } = config;
|
||||
|
||||
let socket = {
|
||||
let v4 = crate::config::ipv4(&discv5_config.listen_config);
|
||||
let v6 = crate::config::ipv6(&discv5_config.listen_config);
|
||||
|
||||
if let Some(addr) = v4 {
|
||||
if *addr.ip() != Ipv4Addr::UNSPECIFIED {
|
||||
builder.ip4(*addr.ip());
|
||||
let socket = match discv5_config.listen_config {
|
||||
ListenConfig::Ipv4 { ip, port } => {
|
||||
if ip != Ipv4Addr::UNSPECIFIED {
|
||||
builder.ip4(ip);
|
||||
}
|
||||
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.udp4(port);
|
||||
builder.tcp4(tcp_socket.port());
|
||||
} else if v6.is_some() {
|
||||
builder.tcp6(tcp_socket.port());
|
||||
}
|
||||
|
||||
// Prefer v6 when both are configured
|
||||
v6.map(SocketAddr::V6)
|
||||
.or_else(|| v4.map(SocketAddr::V4))
|
||||
.unwrap_or_else(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)))
|
||||
(ip, port).into()
|
||||
}
|
||||
ListenConfig::Ipv6 { ip, port } => {
|
||||
if ip != Ipv6Addr::UNSPECIFIED {
|
||||
builder.ip6(ip);
|
||||
}
|
||||
builder.udp6(port);
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
|
||||
@@ -706,7 +711,6 @@ mod test {
|
||||
#![allow(deprecated)]
|
||||
use super::*;
|
||||
use ::enr::{CombinedKey, EnrKey};
|
||||
use discv5::ListenConfig;
|
||||
use rand_08::thread_rng;
|
||||
use reth_chainspec::MAINNET;
|
||||
use std::{
|
||||
|
||||
@@ -25,6 +25,7 @@ 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
|
||||
@@ -62,6 +63,7 @@ std = [
|
||||
"alloy-rlp/std",
|
||||
"bytes/std",
|
||||
"derive_more/std",
|
||||
"alloy-trie/std",
|
||||
"reth-ethereum-primitives/std",
|
||||
"reth-primitives-traits/std",
|
||||
"serde?/std",
|
||||
@@ -80,6 +82,7 @@ arbitrary = [
|
||||
"alloy-consensus/arbitrary",
|
||||
"alloy-eips/arbitrary",
|
||||
"alloy-primitives/arbitrary",
|
||||
"alloy-trie/arbitrary",
|
||||
"reth-primitives-traits/arbitrary",
|
||||
]
|
||||
serde = [
|
||||
@@ -88,6 +91,7 @@ serde = [
|
||||
"alloy-consensus/serde",
|
||||
"alloy-eips/serde",
|
||||
"alloy-primitives/serde",
|
||||
"alloy-trie/serde",
|
||||
"bytes/serde",
|
||||
"rand/serde",
|
||||
"reth-primitives-traits/serde",
|
||||
|
||||
@@ -23,8 +23,9 @@ 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. Per EIP-8159, unavailable entries are
|
||||
/// represented by an RLP-encoded empty list (`0xc0`).
|
||||
/// 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`).
|
||||
pub Vec<Bytes>,
|
||||
);
|
||||
|
||||
@@ -57,9 +58,6 @@ 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]));
|
||||
@@ -171,9 +169,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_list_bal_entries() {
|
||||
let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, 0x01]).unwrap_err();
|
||||
assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
|
||||
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])])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -885,19 +885,6 @@ 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::*;
|
||||
|
||||
@@ -120,11 +120,6 @@ 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)
|
||||
@@ -176,6 +171,18 @@ 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 {
|
||||
@@ -211,6 +218,7 @@ pub struct Capabilities {
|
||||
eth_69: bool,
|
||||
eth_70: bool,
|
||||
eth_71: bool,
|
||||
snap_2: bool,
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
@@ -223,6 +231,7 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -309,6 +318,20 @@ 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 {
|
||||
@@ -334,6 +357,7 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -829,11 +829,11 @@ where
|
||||
mod tests {
|
||||
use super::MessageError;
|
||||
use crate::{
|
||||
message::RequestPair, BlockAccessLists, EthMessage, EthMessageID, EthNetworkPrimitives,
|
||||
EthVersion, GetBlockAccessLists, GetNodeData, NodeData, ProtocolMessage,
|
||||
RawCapabilityMessage,
|
||||
message::RequestPair, BlockAccessLists, BlockRangeUpdate, EthMessage, EthMessageID,
|
||||
EthNetworkPrimitives, EthVersion, GetBlockAccessLists, GetNodeData, NodeData,
|
||||
ProtocolMessage, RawCapabilityMessage,
|
||||
};
|
||||
use alloy_primitives::hex;
|
||||
use alloy_primitives::{hex, B256};
|
||||
use alloy_rlp::{Decodable, Encodable, Error};
|
||||
use reth_ethereum_primitives::BlockBody;
|
||||
|
||||
@@ -874,6 +874,25 @@ 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,
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
//! 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 snap/1 plus preparatory snap/2 message definitions.
|
||||
//! This module currently includes the snap/2 message definitions used by this branch.
|
||||
|
||||
use crate::BlockAccessLists;
|
||||
use alloc::vec::Vec;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
|
||||
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 reth_codecs_derive::add_arbitrary_tests;
|
||||
|
||||
/// Supported SNAP protocol versions.
|
||||
@@ -16,10 +20,8 @@ use reth_codecs_derive::add_arbitrary_tests;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum SnapVersion {
|
||||
/// The original snapshot protocol.
|
||||
#[default]
|
||||
V1 = 1,
|
||||
/// BAL-based healing as proposed by EIP-8189.
|
||||
#[default]
|
||||
V2 = 2,
|
||||
}
|
||||
|
||||
@@ -27,7 +29,6 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -54,24 +55,49 @@ 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)]
|
||||
@@ -91,14 +117,119 @@ pub struct GetAccountRangeMessage {
|
||||
}
|
||||
|
||||
/// Account data in the response.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[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 body in slim format
|
||||
pub body: Bytes,
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Response containing a number of consecutive accounts and the Merkle proofs for the entire range.
|
||||
@@ -146,6 +277,25 @@ 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.
|
||||
@@ -188,45 +338,6 @@ 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))]
|
||||
@@ -266,21 +377,9 @@ 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),
|
||||
}
|
||||
|
||||
@@ -296,8 +395,6 @@ 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,
|
||||
}
|
||||
@@ -317,8 +414,6 @@ 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),
|
||||
}
|
||||
@@ -328,6 +423,26 @@ 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) => {
|
||||
@@ -380,20 +495,6 @@ 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,
|
||||
@@ -438,7 +539,6 @@ 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 {
|
||||
@@ -453,7 +553,12 @@ mod tests {
|
||||
request_id: 42,
|
||||
accounts: vec![AccountData {
|
||||
hash: b256_from_u64(123),
|
||||
body: Bytes::from(vec![1, 2, 3]),
|
||||
account: TrieAccount {
|
||||
nonce: 7,
|
||||
balance: U256::from(42),
|
||||
storage_root: b256_from_u64(456),
|
||||
code_hash: b256_from_u64(789),
|
||||
},
|
||||
}],
|
||||
proof: vec![Bytes::from(vec![4, 5, 6])],
|
||||
}));
|
||||
@@ -487,21 +592,6 @@ 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)],
|
||||
@@ -530,5 +620,61 @@ 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,6 @@ 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.
|
||||
///
|
||||
|
||||
@@ -39,7 +39,7 @@ impl EthVersion {
|
||||
|
||||
/// All known eth versions
|
||||
pub const ALL_VERSIONS: &'static [Self] =
|
||||
&[Self::Eth70, Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
|
||||
&[Self::Eth71, Self::Eth70, Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
|
||||
|
||||
/// Returns true if the version is eth/66
|
||||
pub const fn is_eth66(&self) -> bool {
|
||||
|
||||
@@ -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::V1)
|
||||
Self::new_with_snap_version(eth_version, SnapVersion::V2)
|
||||
}
|
||||
|
||||
/// 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::V1, max_message_size)
|
||||
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V2, max_message_size)
|
||||
}
|
||||
|
||||
/// Create a new eth and snap protocol stream with a custom max message size and snap version.
|
||||
@@ -327,7 +327,11 @@ where
|
||||
let adjusted_message_id = message_id - EthMessageID::message_count(self.eth_version);
|
||||
let mut buf = &bytes[1..];
|
||||
|
||||
match SnapProtocolMessage::decode(adjusted_message_id, &mut buf) {
|
||||
match SnapProtocolMessage::decode_with_version(
|
||||
self.snap_version,
|
||||
adjusted_message_id,
|
||||
&mut buf,
|
||||
) {
|
||||
Ok(snap_msg) => Ok(EthSnapMessage::Snap(snap_msg)),
|
||||
Err(err) => Err(EthSnapStreamError::Rlp(err)),
|
||||
}
|
||||
|
||||
@@ -198,14 +198,17 @@ impl HelloMessageBuilder {
|
||||
/// Unset fields will be set to their default values:
|
||||
/// - `protocol_version`: [`ProtocolVersion::V5`]
|
||||
/// - `client_version`: [`RETH_CLIENT_VERSION`]
|
||||
/// - `capabilities`: All [`EthVersion`]
|
||||
/// - `capabilities`: All [`EthVersion`] and snap/2
|
||||
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(|| {
|
||||
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect()
|
||||
let mut protocols =
|
||||
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect::<Vec<_>>();
|
||||
protocols.push(Protocol::snap_2());
|
||||
protocols
|
||||
}),
|
||||
port: port.unwrap_or(DEFAULT_TCP_PORT),
|
||||
id,
|
||||
@@ -274,6 +277,9 @@ 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]
|
||||
|
||||
@@ -52,11 +52,6 @@ impl Protocol {
|
||||
Self::eth(EthVersion::Eth68)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -103,7 +98,6 @@ mod tests {
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20);
|
||||
assert_eq!(Protocol::snap(SnapVersion::V1).messages(), 8);
|
||||
assert_eq!(Protocol::snap(SnapVersion::V2).messages(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
//! API related to listening for network events.
|
||||
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, BlockAccessLists, BlockBodies, BlockHeaders, Capabilities,
|
||||
DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
|
||||
GetReceipts70, NetworkPrimitives, NodeData, PooledTransactions, Receipts, Receipts69,
|
||||
Receipts70, UnifiedStatus,
|
||||
message::RequestPair,
|
||||
snap::{
|
||||
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
|
||||
GetStorageRangesMessage, SnapProtocolMessage,
|
||||
},
|
||||
BlockAccessLists, BlockBodies, BlockHeaders, Capabilities, DisconnectReason, EthMessage,
|
||||
EthNetworkPrimitives, EthVersion, GetBlockAccessLists, GetBlockBodies, GetBlockHeaders,
|
||||
GetNodeData, GetPooledTransactions, GetReceipts, GetReceipts70, NetworkPrimitives, NodeData,
|
||||
PooledTransactions, Receipts, Receipts69, Receipts70, SnapVersion, UnifiedStatus,
|
||||
};
|
||||
use reth_ethereum_forks::ForkId;
|
||||
use reth_network_p2p::error::{RequestError, RequestResult};
|
||||
use reth_network_p2p::{
|
||||
error::{RequestError, RequestResult},
|
||||
snap::client::SnapResponse,
|
||||
};
|
||||
use reth_network_peers::{NodeRecord, PeerId};
|
||||
use reth_network_types::{PeerAddr, PeerKind};
|
||||
use reth_tokio_util::EventStream;
|
||||
@@ -262,6 +269,42 @@ pub enum PeerRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The channel to send the response for block access lists.
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
/// Requests an account range from the peer (snap protocol).
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetAccountRange {
|
||||
/// The request for account range.
|
||||
request: GetAccountRangeMessage,
|
||||
/// The channel to send the response for account range.
|
||||
response: oneshot::Sender<RequestResult<SnapResponse>>,
|
||||
},
|
||||
/// Requests storage ranges from the peer (snap protocol).
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetStorageRanges {
|
||||
/// The request for storage ranges.
|
||||
request: GetStorageRangesMessage,
|
||||
/// The channel to send the response for storage ranges.
|
||||
response: oneshot::Sender<RequestResult<SnapResponse>>,
|
||||
},
|
||||
/// Requests bytecodes from the peer (snap protocol).
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetByteCodes {
|
||||
/// The request for bytecodes.
|
||||
request: GetByteCodesMessage,
|
||||
/// The channel to send the response for bytecodes.
|
||||
response: oneshot::Sender<RequestResult<SnapResponse>>,
|
||||
},
|
||||
/// Requests block access lists from the peer (snap/2 protocol).
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetSnapBlockAccessLists {
|
||||
/// The snap/2 request for block access lists.
|
||||
request: GetBlockAccessListsMessage,
|
||||
/// The channel to send the response for block access lists.
|
||||
response: oneshot::Sender<RequestResult<SnapResponse>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerRequest ===
|
||||
@@ -283,6 +326,10 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
Self::GetReceipts69 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts70 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetAccountRange { response, .. } |
|
||||
Self::GetStorageRanges { response, .. } |
|
||||
Self::GetByteCodes { response, .. } |
|
||||
Self::GetSnapBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,6 +342,71 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a snap protocol request.
|
||||
pub const fn is_snap_request(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::GetAccountRange { .. } |
|
||||
Self::GetStorageRanges { .. } |
|
||||
Self::GetByteCodes { .. } |
|
||||
Self::GetSnapBlockAccessLists { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the required snap protocol version for snap requests.
|
||||
#[inline]
|
||||
pub const fn required_snap_version(&self) -> Option<SnapVersion> {
|
||||
match self {
|
||||
Self::GetAccountRange { .. } |
|
||||
Self::GetStorageRanges { .. } |
|
||||
Self::GetByteCodes { .. } |
|
||||
Self::GetSnapBlockAccessLists { .. } => Some(SnapVersion::V2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the [`SnapProtocolMessage`] for snap request types.
|
||||
///
|
||||
/// Panics if called on a non-snap request variant.
|
||||
pub fn create_snap_request_message(&self, request_id: u64) -> SnapProtocolMessage {
|
||||
match self {
|
||||
Self::GetAccountRange { request, .. } => {
|
||||
SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
|
||||
request_id,
|
||||
root_hash: request.root_hash,
|
||||
starting_hash: request.starting_hash,
|
||||
limit_hash: request.limit_hash,
|
||||
response_bytes: request.response_bytes,
|
||||
})
|
||||
}
|
||||
Self::GetStorageRanges { request, .. } => {
|
||||
SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
|
||||
request_id,
|
||||
root_hash: request.root_hash,
|
||||
account_hashes: request.account_hashes.clone(),
|
||||
starting_hash: request.starting_hash,
|
||||
limit_hash: request.limit_hash,
|
||||
response_bytes: request.response_bytes,
|
||||
})
|
||||
}
|
||||
Self::GetByteCodes { request, .. } => {
|
||||
SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
|
||||
request_id,
|
||||
hashes: request.hashes.clone(),
|
||||
response_bytes: request.response_bytes,
|
||||
})
|
||||
}
|
||||
Self::GetSnapBlockAccessLists { request, .. } => {
|
||||
SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
|
||||
request_id,
|
||||
block_hashes: request.block_hashes.clone(),
|
||||
response_bytes: request.response_bytes,
|
||||
})
|
||||
}
|
||||
_ => unreachable!("create_snap_request_message called on non-snap request"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`EthMessage`] for this type
|
||||
pub fn create_request_message(&self, request_id: u64) -> EthMessage<N> {
|
||||
match self {
|
||||
@@ -325,6 +437,12 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
message: request.clone(),
|
||||
})
|
||||
}
|
||||
Self::GetAccountRange { .. } |
|
||||
Self::GetStorageRanges { .. } |
|
||||
Self::GetByteCodes { .. } |
|
||||
Self::GetSnapBlockAccessLists { .. } => {
|
||||
unreachable!("snap requests use create_snap_request_message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ reth-ethereum-primitives.workspace = true
|
||||
futures.workspace = true
|
||||
pin-project.workspace = true
|
||||
tokio = { workspace = true, features = ["io-util", "net", "macros", "rt-multi-thread", "time"] }
|
||||
socket2 = { workspace = true, features = ["all"] }
|
||||
tokio-stream.workspace = true
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use crate::{
|
||||
eth_requests::EthRequestHandler,
|
||||
metrics::NETWORK_POOL_TRANSACTIONS_SCOPE,
|
||||
transactions::{
|
||||
config::{
|
||||
AnnouncementFilteringPolicy, StrictEthAnnouncementFilter, TransactionPropagationKind,
|
||||
@@ -13,7 +12,6 @@ use crate::{
|
||||
NetworkHandle, NetworkManager,
|
||||
};
|
||||
use reth_eth_wire::{EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_metrics::common::mpsc::memory_bounded_channel;
|
||||
use reth_network_api::test_utils::PeersHandleProvider;
|
||||
use reth_storage_api::BalProvider;
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
@@ -78,6 +76,17 @@ impl<Tx, Eth, N: NetworkPrimitives> NetworkBuilder<Tx, Eth, N> {
|
||||
NetworkBuilder { network, request_handler, transactions }
|
||||
}
|
||||
|
||||
/// Creates a new [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) and wires
|
||||
/// it to the network. Returns the handler which the caller must spawn.
|
||||
pub fn snap_request_handler<S: reth_network_p2p::snap::server::SnapStateProvider>(
|
||||
&mut self,
|
||||
snap_provider: S,
|
||||
) -> crate::snap_requests::SnapRequestHandler<S> {
|
||||
let (tx, rx) = mpsc::channel(ETH_REQUEST_CHANNEL_CAPACITY);
|
||||
self.network.set_snap_request_handler(tx);
|
||||
crate::snap_requests::SnapRequestHandler::new(snap_provider, rx)
|
||||
}
|
||||
|
||||
/// Creates a new [`TransactionsManager`] and wires it to the network.
|
||||
pub fn transactions<Pool: TransactionPool>(
|
||||
self,
|
||||
@@ -124,10 +133,7 @@ impl<Tx, Eth, N: NetworkPrimitives> NetworkBuilder<Tx, Eth, N> {
|
||||
announcement_policy: A,
|
||||
) -> NetworkBuilder<TransactionsManager<Pool, N>, Eth, N> {
|
||||
let Self { mut network, request_handler, .. } = self;
|
||||
let (tx, rx) = memory_bounded_channel(
|
||||
transactions_manager_config.tx_channel_memory_limit_bytes,
|
||||
NETWORK_POOL_TRANSACTIONS_SCOPE,
|
||||
);
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
network.set_transactions(tx);
|
||||
let handle = network.handle().clone();
|
||||
let policies = NetworkPolicies::new(propagation_policy, announcement_policy);
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::{
|
||||
sync::Arc,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle};
|
||||
use tokio::{sync::mpsc, task::JoinHandle};
|
||||
use tokio_stream::{wrappers::ReceiverStream, Stream};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
@@ -54,9 +54,6 @@ pub struct Discovery {
|
||||
discv5: Option<Discv5>,
|
||||
/// All KAD table updates from the discv5 service.
|
||||
discv5_updates: Option<ReceiverStream<discv5::Event>>,
|
||||
/// Background task that, in shared-port mode, drains `UnrecognizedFrame`s from discv5 and
|
||||
/// feeds them into the discv4 ingress so packets advance without polling `Discovery`.
|
||||
_discv5_forwarder: Option<JoinHandle<()>>,
|
||||
/// Handler to interact with the DNS discovery service
|
||||
_dns_discovery: Option<DnsDiscoveryHandle>,
|
||||
/// Updates from the DNS discovery service.
|
||||
@@ -79,138 +76,39 @@ impl Discovery {
|
||||
discovery_v4_addr: SocketAddr,
|
||||
sk: SecretKey,
|
||||
discv4_config: Option<Discv4Config>,
|
||||
mut discv5_config: Option<reth_discv5::Config>, // contains discv5 listen address
|
||||
discv5_config: Option<reth_discv5::Config>, // contains discv5 listen address
|
||||
dns_discovery_config: Option<DnsDiscoveryConfig>,
|
||||
) -> Result<Self, NetworkError> {
|
||||
// setup discv4 with the discovery address and tcp port
|
||||
let local_enr =
|
||||
NodeRecord::from_secret_key(discovery_v4_addr, &sk).with_tcp_port(tcp_addr.port());
|
||||
|
||||
// For IPv6 we set IPV6_V6ONLY=true so an IPv4 sibling socket on the same port doesn't
|
||||
// clash with the IPv6 one (Linux's default of V6ONLY=0 has IPv6 also claim the IPv4
|
||||
// port via mapped addresses), matching how discv5 binds its `DualStack` sockets.
|
||||
let bind_socket = async |addr: SocketAddr| {
|
||||
let result = match addr {
|
||||
SocketAddr::V4(_) => UdpSocket::bind(addr).await,
|
||||
SocketAddr::V6(_) => {
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
(|| {
|
||||
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
socket.set_only_v6(true)?;
|
||||
socket.set_nonblocking(true)?;
|
||||
socket.bind(&addr.into())?;
|
||||
UdpSocket::from_std(socket.into())
|
||||
})()
|
||||
}
|
||||
};
|
||||
result
|
||||
.map(Arc::new)
|
||||
.map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(addr)))
|
||||
};
|
||||
|
||||
// In shared-port mode, bind the shared socket and start discv4 without its own receive
|
||||
// loop. Unrecognized frames from discv5 will be forwarded to the ingress handler.
|
||||
let (discv4, discv4_updates, _discv4_service, discv4_ingress, shared_socket) =
|
||||
if let Some(config) = discv4_config {
|
||||
if let Some(discv5_config) = &mut discv5_config &&
|
||||
discv5_config.has_matching_socket(discovery_v4_addr)
|
||||
{
|
||||
let socket = bind_socket(discovery_v4_addr).await?;
|
||||
|
||||
let (discv4, mut discv4_service, ingress) = Discv4::bind_shared(
|
||||
socket.clone(),
|
||||
local_enr,
|
||||
sk,
|
||||
config,
|
||||
)
|
||||
.map_err(|err| {
|
||||
let discv4_future = async {
|
||||
let Some(disc_config) = discv4_config else { return Ok((None, None, None)) };
|
||||
let (discv4, mut discv4_service) =
|
||||
Discv4::bind(discovery_v4_addr, local_enr, sk, disc_config).await.map_err(
|
||||
|err| {
|
||||
NetworkError::from_io_error(err, ServiceKind::Discovery(discovery_v4_addr))
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
let discv4_updates = discv4_service.update_stream();
|
||||
// spawn the service
|
||||
let discv4_service = discv4_service.spawn();
|
||||
|
||||
let discv4_updates = discv4_service.update_stream();
|
||||
let discv4_service = discv4_service.spawn();
|
||||
debug!(target:"net", ?discovery_v4_addr, "started discovery v4 (shared port)");
|
||||
(
|
||||
Some(discv4),
|
||||
Some(discv4_updates),
|
||||
Some(discv4_service),
|
||||
Some(ingress),
|
||||
Some(socket),
|
||||
)
|
||||
} else {
|
||||
let (discv4, mut discv4_service) =
|
||||
Discv4::bind(discovery_v4_addr, local_enr, sk, config).await.map_err(
|
||||
|err| {
|
||||
NetworkError::from_io_error(
|
||||
err,
|
||||
ServiceKind::Discovery(discovery_v4_addr),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
let discv4_updates = discv4_service.update_stream();
|
||||
// spawn the service
|
||||
let discv4_service = discv4_service.spawn();
|
||||
debug!(target:"net", ?discovery_v4_addr, "started discovery v4");
|
||||
|
||||
debug!(target:"net", ?discovery_v4_addr, "started discovery v4");
|
||||
|
||||
(Some(discv4), Some(discv4_updates), Some(discv4_service), None, None)
|
||||
}
|
||||
} else {
|
||||
(None, None, None, None, None)
|
||||
};
|
||||
|
||||
// Start discv5, wiring in the shared socket if in shared-port mode.
|
||||
let (discv5, discv5_updates) = if let Some(mut config) = discv5_config {
|
||||
if let Some(socket) = shared_socket {
|
||||
let discv5_cfg = config.discv5_config_mut();
|
||||
|
||||
// The shared socket covers discv4's address family; bind the opposite family
|
||||
// only if discv5 was configured for dual-stack.
|
||||
let (mut ipv4, mut ipv6) = (None, None);
|
||||
if discovery_v4_addr.is_ipv4() {
|
||||
ipv4 = Some(socket);
|
||||
if let Some(addr) = reth_discv5::config::ipv6(&discv5_cfg.listen_config) {
|
||||
ipv6 = Some(bind_socket(SocketAddr::V6(addr)).await?);
|
||||
}
|
||||
} else {
|
||||
ipv6 = Some(socket);
|
||||
if let Some(addr) = reth_discv5::config::ipv4(&discv5_cfg.listen_config) {
|
||||
ipv4 = Some(bind_socket(SocketAddr::V4(addr)).await?);
|
||||
}
|
||||
}
|
||||
|
||||
discv5_cfg.listen_config = discv5::ListenConfig::FromSockets { ipv4, ipv6 };
|
||||
}
|
||||
Ok((Some(discv4), Some(discv4_updates), Some(discv4_service)))
|
||||
};
|
||||
|
||||
let discv5_future = async {
|
||||
let Some(config) = discv5_config else { return Ok::<_, NetworkError>((None, None)) };
|
||||
let (discv5, discv5_updates) = Discv5::start(&sk, config).await?;
|
||||
debug!(target:"net", discovery_v5_enr=?discv5.local_enr(), "started discovery v5");
|
||||
(Some(discv5), Some(discv5_updates))
|
||||
} else {
|
||||
(None, None)
|
||||
debug!(target:"net", discovery_v5_enr=? discv5.local_enr(), "started discovery v5");
|
||||
Ok((Some(discv5), Some(discv5_updates.into())))
|
||||
};
|
||||
|
||||
// In shared-port mode, spawn a task that peels `UnrecognizedFrame` events off the discv5
|
||||
// update stream and feeds them into discv4's ingress. Other events are forwarded through
|
||||
// a new channel that `Discovery::poll` reads. This keeps both protocols moving without
|
||||
// requiring the main `Discovery::poll` loop to be driven for packets to be routed.
|
||||
let (discv5_updates, _discv5_forwarder) = match (discv4_ingress, discv5_updates) {
|
||||
(Some(mut ingress), Some(mut updates)) => {
|
||||
let (tx, rx) = mpsc::channel(updates.max_capacity());
|
||||
let handle = tokio::spawn(async move {
|
||||
while let Some(event) = updates.recv().await {
|
||||
if let discv5::Event::UnrecognizedFrame(frame) = &event {
|
||||
ingress.handle_packet(&frame.packet, frame.src_address).await;
|
||||
continue;
|
||||
}
|
||||
if tx.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
(Some(ReceiverStream::new(rx)), Some(handle))
|
||||
}
|
||||
(_, updates) => (updates.map(ReceiverStream::new), None),
|
||||
};
|
||||
let ((discv4, discv4_updates, _discv4_service), (discv5, discv5_updates)) =
|
||||
tokio::try_join!(discv4_future, discv5_future)?;
|
||||
|
||||
// setup DNS discovery
|
||||
let (_dns_discovery, dns_discovery_updates, _dns_disc_service) =
|
||||
@@ -234,7 +132,6 @@ impl Discovery {
|
||||
_discv4_service,
|
||||
discv5,
|
||||
discv5_updates,
|
||||
_discv5_forwarder,
|
||||
discovered_nodes: LruMap::new(DEFAULT_MAX_CAPACITY_DISCOVERED_PEERS_CACHE),
|
||||
queued_events: Default::default(),
|
||||
_dns_disc_service,
|
||||
@@ -412,9 +309,6 @@ impl Drop for Discovery {
|
||||
if let Some(handle) = self._discv4_service.take() {
|
||||
handle.abort();
|
||||
}
|
||||
if let Some(handle) = self._discv5_forwarder.take() {
|
||||
handle.abort();
|
||||
}
|
||||
if let Some(handle) = self._dns_disc_service.take() {
|
||||
handle.abort();
|
||||
}
|
||||
@@ -448,11 +342,10 @@ impl Discovery {
|
||||
},
|
||||
discv4: Default::default(),
|
||||
discv4_updates: Default::default(),
|
||||
_discv4_service: Default::default(),
|
||||
_discv5_forwarder: None,
|
||||
discv5: None,
|
||||
discv5_updates: None,
|
||||
queued_events: Default::default(),
|
||||
_discv4_service: Default::default(),
|
||||
_dns_discovery: None,
|
||||
dns_discovery_updates: None,
|
||||
_dns_disc_service: None,
|
||||
@@ -594,179 +487,4 @@ mod tests {
|
||||
assert_eq!(1, node_1.discovered_nodes.len());
|
||||
assert_eq!(1, node_2.discovered_nodes.len());
|
||||
}
|
||||
|
||||
/// Starts a discovery node with discv4 and discv5 sharing the same UDP port.
|
||||
async fn start_shared_port_node(port: u16) -> Discovery {
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let disc_addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
// Use a non-zero TCP port so the node record isn't filtered out by
|
||||
// `on_node_record_update` (which drops peers with tcp port == 0).
|
||||
let tcp_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
|
||||
|
||||
let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
|
||||
|
||||
let discv5_listen_config = discv5::ListenConfig::from(disc_addr);
|
||||
let discv5_config = reth_discv5::Config::builder(tcp_addr)
|
||||
.discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
|
||||
.build();
|
||||
|
||||
// Both protocols use the same address, triggering shared-port mode
|
||||
Discovery::new(
|
||||
tcp_addr,
|
||||
disc_addr,
|
||||
secret_key,
|
||||
Some(discv4_config),
|
||||
Some(discv5_config),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("should start with shared port")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_shared_port_setup() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
// Use port 0 so the OS picks a free port
|
||||
let node = start_shared_port_node(0).await;
|
||||
|
||||
// Both protocols should be active
|
||||
assert!(node.discv4.is_some(), "discv4 should be running");
|
||||
assert!(node.discv5.is_some(), "discv5 should be running");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_shared_port_discv5_discovery() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let mut node_1 = start_shared_port_node(0).await;
|
||||
let mut node_2 = start_shared_port_node(0).await;
|
||||
|
||||
let discv5_enr_1 = node_1.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
|
||||
let discv5_enr_2 = node_2.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
|
||||
|
||||
let peer_id_1 = enr_to_discv4_id(&discv5_enr_1).unwrap();
|
||||
let peer_id_2 = enr_to_discv4_id(&discv5_enr_2).unwrap();
|
||||
|
||||
// Add node_2's ENR to node_1's discv5 kbuckets and trigger a ping to establish a session.
|
||||
// send_ping awaits the PONG, so the handshake completes before we poll the Discovery
|
||||
// stream. The discv5 service runs its own background task.
|
||||
node_1.add_discv5_node(EnrCombinedKeyWrapper(discv5_enr_2.clone()).into()).unwrap();
|
||||
node_1
|
||||
.discv5
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.with_discv5(|discv5| discv5.send_ping(discv5_enr_2))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Both SessionEstablished events should now be buffered in the update channels.
|
||||
// Drive both nodes concurrently to collect them.
|
||||
let mut event_1 = None;
|
||||
let mut event_2 = None;
|
||||
let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
|
||||
tokio::pin!(timeout);
|
||||
loop {
|
||||
tokio::select! {
|
||||
ev = node_1.next(), if event_1.is_none() => {
|
||||
event_1 = ev;
|
||||
}
|
||||
ev = node_2.next(), if event_2.is_none() => {
|
||||
event_2 = ev;
|
||||
}
|
||||
_ = &mut timeout => {
|
||||
panic!("timed out waiting for discv5 discovery events");
|
||||
}
|
||||
}
|
||||
if event_1.is_some() && event_2.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
event_1.unwrap(),
|
||||
DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
|
||||
if peer_id == peer_id_2
|
||||
));
|
||||
assert!(matches!(
|
||||
event_2.unwrap(),
|
||||
DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
|
||||
if peer_id == peer_id_1
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_shared_port_discv4_discovery() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
let mut node_1 = start_shared_port_node(0).await;
|
||||
let mut node_2 = start_shared_port_node(0).await;
|
||||
|
||||
let enr_1 = node_1.discv4.as_ref().unwrap().node_record();
|
||||
let enr_2 = node_2.discv4.as_ref().unwrap().node_record();
|
||||
|
||||
// Introduce node_2 to node_1 via discv4
|
||||
node_1.add_discv4_node(enr_2);
|
||||
|
||||
// Both nodes should discover each other via discv4 ping/pong
|
||||
let event_1 = node_1.next().await.unwrap();
|
||||
let event_2 = node_2.next().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
|
||||
peer_id: enr_2.id,
|
||||
addr: PeerAddr::new(enr_2.tcp_addr(), Some(enr_2.udp_addr())),
|
||||
fork_id: None
|
||||
}),
|
||||
event_1
|
||||
);
|
||||
assert_eq!(
|
||||
DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
|
||||
peer_id: enr_1.id,
|
||||
addr: PeerAddr::new(enr_1.tcp_addr(), Some(enr_1.udp_addr())),
|
||||
fork_id: None
|
||||
}),
|
||||
event_2
|
||||
);
|
||||
}
|
||||
|
||||
/// Verifies that shared-port mode binds correctly when discv5 is configured for dual-stack.
|
||||
/// On Linux this exercises the IPv6 V6ONLY path: without it, the IPv4 sibling would clash
|
||||
/// with the IPv6 socket bound to the same port.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_shared_port_dual_stack() {
|
||||
reth_tracing::init_test_tracing();
|
||||
|
||||
// Find a port that's free on the v4 wildcard so we can use it for both v4 and v6.
|
||||
let probe = UdpSocket::bind("0.0.0.0:0").await.expect("probe bind");
|
||||
let port = probe.local_addr().unwrap().port();
|
||||
drop(probe);
|
||||
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let v4_addr: SocketAddr = format!("0.0.0.0:{port}").parse().unwrap();
|
||||
let tcp_addr: SocketAddr = "0.0.0.0:30303".parse().unwrap();
|
||||
|
||||
let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
|
||||
|
||||
let discv5_listen_config = discv5::ListenConfig::DualStack {
|
||||
ipv4: std::net::Ipv4Addr::UNSPECIFIED,
|
||||
ipv4_port: port,
|
||||
ipv6: std::net::Ipv6Addr::UNSPECIFIED,
|
||||
ipv6_port: port,
|
||||
};
|
||||
let discv5_config = reth_discv5::Config::builder(tcp_addr)
|
||||
.discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
|
||||
.build();
|
||||
|
||||
Discovery::new(
|
||||
tcp_addr,
|
||||
v4_addr,
|
||||
secret_key,
|
||||
Some(discv4_config),
|
||||
Some(discv5_config),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("discovery should start with shared port + dual-stack");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +339,8 @@ where
|
||||
.bal_store()
|
||||
.get_by_hashes_with_limit(&request.0, limit)
|
||||
.unwrap_or_else(|_| empty_block_access_lists_with_limit(request.0.len(), limit));
|
||||
let found = access_lists.iter().filter(|b| b.as_ref() != [0xc0]).count();
|
||||
tracing::debug!(target: "net::eth", requested=request.0.len(), found, "BAL request received");
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ use crate::{fetch::DownloadRequest, flattened_response::FlattenedResponse};
|
||||
use alloy_primitives::B256;
|
||||
use futures::{future, future::Either};
|
||||
use reth_eth_wire::{BlockAccessLists, EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_eth_wire_types::snap::{
|
||||
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
|
||||
GetStorageRangesMessage,
|
||||
};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::{
|
||||
block_access_lists::client::{BalRequirement, BlockAccessListsClient},
|
||||
@@ -13,6 +17,7 @@ use reth_network_p2p::{
|
||||
headers::client::{HeadersClient, HeadersRequest},
|
||||
priority::Priority,
|
||||
receipts::client::{ReceiptsClient, ReceiptsFut},
|
||||
snap::client::{SnapClient, SnapResponse},
|
||||
BlockClient,
|
||||
};
|
||||
use reth_network_peers::PeerId;
|
||||
@@ -126,6 +131,87 @@ impl<N: NetworkPrimitives> BlockClient for FetchClient<N> {
|
||||
type Block = N::Block;
|
||||
}
|
||||
|
||||
impl<N: NetworkPrimitives> SnapClient for FetchClient<N> {
|
||||
type Output =
|
||||
std::pin::Pin<Box<dyn Future<Output = PeerRequestResult<SnapResponse>> + Send + Sync>>;
|
||||
|
||||
fn get_account_range_with_priority(
|
||||
&self,
|
||||
request: GetAccountRangeMessage,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
let (response, rx) = oneshot::channel();
|
||||
if self
|
||||
.request_tx
|
||||
.send(DownloadRequest::GetAccountRange { request, response, priority })
|
||||
.is_ok()
|
||||
{
|
||||
Box::pin(FlattenedResponse::from(rx))
|
||||
} else {
|
||||
Box::pin(future::err(RequestError::ChannelClosed))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_storage_ranges(&self, request: GetStorageRangesMessage) -> Self::Output {
|
||||
self.get_storage_ranges_with_priority(request, Priority::Normal)
|
||||
}
|
||||
|
||||
fn get_storage_ranges_with_priority(
|
||||
&self,
|
||||
request: GetStorageRangesMessage,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
let (response, rx) = oneshot::channel();
|
||||
if self
|
||||
.request_tx
|
||||
.send(DownloadRequest::GetStorageRanges { request, response, priority })
|
||||
.is_ok()
|
||||
{
|
||||
Box::pin(FlattenedResponse::from(rx))
|
||||
} else {
|
||||
Box::pin(future::err(RequestError::ChannelClosed))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_byte_codes(&self, request: GetByteCodesMessage) -> Self::Output {
|
||||
self.get_byte_codes_with_priority(request, Priority::Normal)
|
||||
}
|
||||
|
||||
fn get_byte_codes_with_priority(
|
||||
&self,
|
||||
request: GetByteCodesMessage,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
let (response, rx) = oneshot::channel();
|
||||
if self
|
||||
.request_tx
|
||||
.send(DownloadRequest::GetByteCodes { request, response, priority })
|
||||
.is_ok()
|
||||
{
|
||||
Box::pin(FlattenedResponse::from(rx))
|
||||
} else {
|
||||
Box::pin(future::err(RequestError::ChannelClosed))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_snap_block_access_lists_with_priority(
|
||||
&self,
|
||||
request: GetBlockAccessListsMessage,
|
||||
priority: Priority,
|
||||
) -> Self::Output {
|
||||
let (response, rx) = oneshot::channel();
|
||||
if self
|
||||
.request_tx
|
||||
.send(DownloadRequest::GetSnapBlockAccessLists { request, response, priority })
|
||||
.is_ok()
|
||||
{
|
||||
Box::pin(FlattenedResponse::from(rx))
|
||||
} else {
|
||||
Box::pin(future::err(RequestError::ChannelClosed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: NetworkPrimitives> BlockAccessListsClient for FetchClient<N> {
|
||||
type Output =
|
||||
std::pin::Pin<Box<dyn Future<Output = PeerRequestResult<BlockAccessLists>> + Send + Sync>>;
|
||||
|
||||
@@ -11,6 +11,10 @@ use reth_eth_wire::{
|
||||
BlockAccessLists, Capabilities, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetReceipts, NetworkPrimitives,
|
||||
};
|
||||
use reth_eth_wire_types::snap::{
|
||||
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
|
||||
GetStorageRangesMessage, SnapProtocolMessage, SnapVersion,
|
||||
};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::{
|
||||
block_access_lists::client::BalRequirement,
|
||||
@@ -18,6 +22,7 @@ use reth_network_p2p::{
|
||||
headers::client::HeadersRequest,
|
||||
priority::Priority,
|
||||
receipts::client::ReceiptsResponse,
|
||||
snap::client::SnapResponse,
|
||||
};
|
||||
use reth_network_peers::PeerId;
|
||||
use reth_network_types::ReputationChangeKind;
|
||||
@@ -37,6 +42,7 @@ type InflightHeadersRequest<H> = Request<HeadersRequest, PeerRequestResult<Vec<H
|
||||
type InflightBodiesRequest<B> = Request<(), PeerRequestResult<Vec<B>>>;
|
||||
type InflightReceiptsRequest<R> = Request<(), PeerRequestResult<ReceiptsResponse<R>>>;
|
||||
type InflightBlockAccessListsRequest = Request<(), PeerRequestResult<BlockAccessLists>>;
|
||||
type InflightSnapRequest = Request<(), PeerRequestResult<SnapResponse>>;
|
||||
|
||||
/// Manages data fetching operations.
|
||||
///
|
||||
@@ -54,6 +60,8 @@ pub struct StateFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
inflight_bals_requests: HashMap<PeerId, InflightBlockAccessListsRequest>,
|
||||
/// Currently active `GetReceipts` requests
|
||||
inflight_receipts_requests: HashMap<PeerId, InflightReceiptsRequest<N::Receipt>>,
|
||||
/// Currently active snap protocol requests
|
||||
inflight_snap_requests: HashMap<PeerId, InflightSnapRequest>,
|
||||
/// The list of _available_ peers for requests.
|
||||
peers: HashMap<PeerId, Peer>,
|
||||
/// The handle to the peers manager
|
||||
@@ -78,6 +86,7 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
inflight_bodies_requests: Default::default(),
|
||||
inflight_bals_requests: Default::default(),
|
||||
inflight_receipts_requests: Default::default(),
|
||||
inflight_snap_requests: Default::default(),
|
||||
peers: Default::default(),
|
||||
peers_handle,
|
||||
num_active_peers,
|
||||
@@ -131,6 +140,9 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
if let Some(req) = self.inflight_receipts_requests.remove(peer) {
|
||||
let _ = req.response.send(Err(RequestError::ConnectionDropped));
|
||||
}
|
||||
if let Some(req) = self.inflight_snap_requests.remove(peer) {
|
||||
let _ = req.response.send(Err(RequestError::ConnectionDropped));
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the block information for the peer.
|
||||
@@ -324,6 +336,26 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
self.inflight_receipts_requests.insert(peer_id, inflight);
|
||||
BlockRequest::GetReceipts(GetReceipts(request))
|
||||
}
|
||||
DownloadRequest::GetAccountRange { request, response, .. } => {
|
||||
let inflight = Request { request: (), response };
|
||||
self.inflight_snap_requests.insert(peer_id, inflight);
|
||||
BlockRequest::Snap(SnapProtocolMessage::GetAccountRange(request))
|
||||
}
|
||||
DownloadRequest::GetStorageRanges { request, response, .. } => {
|
||||
let inflight = Request { request: (), response };
|
||||
self.inflight_snap_requests.insert(peer_id, inflight);
|
||||
BlockRequest::Snap(SnapProtocolMessage::GetStorageRanges(request))
|
||||
}
|
||||
DownloadRequest::GetByteCodes { request, response, .. } => {
|
||||
let inflight = Request { request: (), response };
|
||||
self.inflight_snap_requests.insert(peer_id, inflight);
|
||||
BlockRequest::Snap(SnapProtocolMessage::GetByteCodes(request))
|
||||
}
|
||||
DownloadRequest::GetSnapBlockAccessLists { request, response, .. } => {
|
||||
let inflight = Request { request: (), response };
|
||||
self.inflight_snap_requests.insert(peer_id, inflight);
|
||||
BlockRequest::Snap(SnapProtocolMessage::GetBlockAccessLists(request))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +486,27 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Called on a snap protocol response from a peer
|
||||
pub(crate) fn on_snap_response(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
res: RequestResult<SnapResponse>,
|
||||
) -> Option<BlockResponseOutcome> {
|
||||
let is_likely_bad_response = res.is_err();
|
||||
|
||||
if let Some(resp) = self.inflight_snap_requests.remove(&peer_id) {
|
||||
let _ = resp.response.send(res.map(|r| (peer_id, r).into()));
|
||||
}
|
||||
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
||||
peer.last_response_likely_bad = is_likely_bad_response;
|
||||
|
||||
if peer.state.on_request_finished() && !is_likely_bad_response {
|
||||
return self.followup_request(peer_id)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a new [`FetchClient`] that can send requests to this type.
|
||||
pub(crate) fn client(&self) -> FetchClient<N> {
|
||||
FetchClient {
|
||||
@@ -519,6 +572,7 @@ impl Peer {
|
||||
fn satisfies(&self, requirement: &BestPeerRequirements) -> bool {
|
||||
match requirement {
|
||||
BestPeerRequirements::EthVersion(ver) => self.capabilities.supports_eth_at_least(ver),
|
||||
BestPeerRequirements::SnapVersion(ver) => self.capabilities.supports_snap_version(*ver),
|
||||
BestPeerRequirements::None |
|
||||
BestPeerRequirements::FullBlock |
|
||||
BestPeerRequirements::FullBlockRange(_) => true,
|
||||
@@ -575,7 +629,9 @@ impl Peer {
|
||||
BestPeerRequirements::FullBlock => self.has_full_history() && !other.has_full_history(),
|
||||
// Version-based filtering happens in `next_best_peer`, so by the time we get here
|
||||
// both peers already satisfy the version requirement.
|
||||
BestPeerRequirements::None | BestPeerRequirements::EthVersion(_) => false,
|
||||
BestPeerRequirements::None |
|
||||
BestPeerRequirements::EthVersion(_) |
|
||||
BestPeerRequirements::SnapVersion(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,6 +649,8 @@ enum PeerState {
|
||||
GetBlockAccessLists,
|
||||
/// Peer is handling a `GetReceipts` request.
|
||||
GetReceipts,
|
||||
/// Peer is handling a snap protocol request.
|
||||
GetSnap,
|
||||
/// Peer session is about to close
|
||||
Closing,
|
||||
}
|
||||
@@ -659,6 +717,30 @@ pub(crate) enum DownloadRequest<N: NetworkPrimitives> {
|
||||
response: oneshot::Sender<PeerRequestResult<ReceiptsResponse<N::Receipt>>>,
|
||||
priority: Priority,
|
||||
},
|
||||
/// Request an account range via snap protocol
|
||||
GetAccountRange {
|
||||
request: GetAccountRangeMessage,
|
||||
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
|
||||
priority: Priority,
|
||||
},
|
||||
/// Request storage ranges via snap protocol
|
||||
GetStorageRanges {
|
||||
request: GetStorageRangesMessage,
|
||||
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
|
||||
priority: Priority,
|
||||
},
|
||||
/// Request bytecodes via snap protocol
|
||||
GetByteCodes {
|
||||
request: GetByteCodesMessage,
|
||||
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
|
||||
priority: Priority,
|
||||
},
|
||||
/// Request block access lists via snap/2 protocol
|
||||
GetSnapBlockAccessLists {
|
||||
request: GetBlockAccessListsMessage,
|
||||
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
|
||||
priority: Priority,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl DownloadRequest ===
|
||||
@@ -671,6 +753,10 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
|
||||
Self::GetBlockBodies { .. } => PeerState::GetBlockBodies,
|
||||
Self::GetBlockAccessLists { .. } => PeerState::GetBlockAccessLists,
|
||||
Self::GetReceipts { .. } => PeerState::GetReceipts,
|
||||
Self::GetAccountRange { .. } |
|
||||
Self::GetStorageRanges { .. } |
|
||||
Self::GetByteCodes { .. } |
|
||||
Self::GetSnapBlockAccessLists { .. } => PeerState::GetSnap,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,7 +766,11 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
|
||||
Self::GetBlockHeaders { priority, .. } |
|
||||
Self::GetBlockBodies { priority, .. } |
|
||||
Self::GetBlockAccessLists { priority, .. } |
|
||||
Self::GetReceipts { priority, .. } => priority,
|
||||
Self::GetReceipts { priority, .. } |
|
||||
Self::GetAccountRange { priority, .. } |
|
||||
Self::GetStorageRanges { priority, .. } |
|
||||
Self::GetByteCodes { priority, .. } |
|
||||
Self::GetSnapBlockAccessLists { priority, .. } => priority,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,6 +791,10 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
|
||||
Self::GetBlockBodies { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetAccountRange { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetStorageRanges { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetByteCodes { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetSnapBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -708,6 +802,12 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
|
||||
fn best_peer_requirements(&self) -> BestPeerRequirements {
|
||||
match self {
|
||||
Self::GetBlockHeaders { .. } => BestPeerRequirements::None,
|
||||
Self::GetAccountRange { .. } |
|
||||
Self::GetStorageRanges { .. } |
|
||||
Self::GetByteCodes { .. } |
|
||||
Self::GetSnapBlockAccessLists { .. } => {
|
||||
BestPeerRequirements::SnapVersion(SnapVersion::V2)
|
||||
}
|
||||
Self::GetBlockAccessLists { .. } => BestPeerRequirements::EthVersion(EthVersion::Eth71),
|
||||
Self::GetBlockBodies { range_hint, .. } => {
|
||||
if let Some(range) = range_hint {
|
||||
@@ -753,6 +853,8 @@ enum BestPeerRequirements {
|
||||
FullBlock,
|
||||
/// Peer must support at least this eth protocol version.
|
||||
EthVersion(EthVersion),
|
||||
/// Peer must advertise this snap protocol version.
|
||||
SnapVersion(SnapVersion),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -131,6 +131,7 @@ pub mod import;
|
||||
pub mod message;
|
||||
pub mod peers;
|
||||
pub mod protocol;
|
||||
pub mod snap_requests;
|
||||
pub mod transactions;
|
||||
|
||||
mod budget;
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
message::{NewBlockMessage, PeerMessage},
|
||||
metrics::{
|
||||
BackedOffPeersMetrics, ClosedSessionsMetrics, DirectionalDisconnectMetrics, NetworkMetrics,
|
||||
PendingSessionFailureMetrics,
|
||||
PendingSessionFailureMetrics, NETWORK_POOL_TRANSACTIONS_SCOPE,
|
||||
},
|
||||
network::{NetworkHandle, NetworkHandleMessage},
|
||||
peers::{BackoffReason, PeersManager},
|
||||
@@ -34,6 +34,7 @@ use crate::{
|
||||
protocol::IntoRlpxSubProtocol,
|
||||
required_block_filter::RequiredBlockFilter,
|
||||
session::SessionManager,
|
||||
snap_requests::IncomingSnapRequest,
|
||||
state::NetworkState,
|
||||
swarm::{Swarm, SwarmEvent},
|
||||
transactions::NetworkTransactionEvent,
|
||||
@@ -44,7 +45,7 @@ use parking_lot::Mutex;
|
||||
use reth_chainspec::EnrForkIdEntry;
|
||||
use reth_eth_wire::{DisconnectReason, EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_fs_util::{self as fs, FsPathError};
|
||||
use reth_metrics::common::mpsc::MemoryBoundedSender;
|
||||
use reth_metrics::common::mpsc::UnboundedMeteredSender;
|
||||
use reth_network_api::{
|
||||
events::{PeerEvent, SessionInfo},
|
||||
test_utils::PeersHandle,
|
||||
@@ -118,7 +119,7 @@ pub struct NetworkManager<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
event_sender: EventSender<NetworkEvent<PeerRequest<N>>>,
|
||||
/// Sender half to send events to the
|
||||
/// [`TransactionsManager`](crate::transactions::TransactionsManager) task, if configured.
|
||||
to_transactions_manager: Option<MemoryBoundedSender<NetworkTransactionEvent<N>>>,
|
||||
to_transactions_manager: Option<UnboundedMeteredSender<NetworkTransactionEvent<N>>>,
|
||||
/// Sender half to send events to the
|
||||
/// [`EthRequestHandler`](crate::eth_requests::EthRequestHandler) task, if configured.
|
||||
///
|
||||
@@ -133,6 +134,9 @@ pub struct NetworkManager<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// requests. This channel size is set at
|
||||
/// [`ETH_REQUEST_CHANNEL_CAPACITY`](crate::builder::ETH_REQUEST_CHANNEL_CAPACITY)
|
||||
to_eth_request_handler: Option<mpsc::Sender<IncomingEthRequest<N>>>,
|
||||
/// Sender half to send events to the
|
||||
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) task, if configured.
|
||||
to_snap_request_handler: Option<mpsc::Sender<IncomingSnapRequest>>,
|
||||
/// Tracks the number of active session (connected peers).
|
||||
///
|
||||
/// This is updated via internal events and shared via `Arc` with the [`NetworkHandle`]
|
||||
@@ -175,7 +179,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
/// [`TransactionsManager`](crate::transactions::TransactionsManager).
|
||||
pub fn with_transactions(
|
||||
mut self,
|
||||
tx: MemoryBoundedSender<NetworkTransactionEvent<N>>,
|
||||
tx: mpsc::UnboundedSender<NetworkTransactionEvent<N>>,
|
||||
) -> Self {
|
||||
self.set_transactions(tx);
|
||||
self
|
||||
@@ -183,8 +187,9 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
|
||||
/// Sets the dedicated channel for events intended for the
|
||||
/// [`TransactionsManager`](crate::transactions::TransactionsManager).
|
||||
pub fn set_transactions(&mut self, tx: MemoryBoundedSender<NetworkTransactionEvent<N>>) {
|
||||
self.to_transactions_manager = Some(tx);
|
||||
pub fn set_transactions(&mut self, tx: mpsc::UnboundedSender<NetworkTransactionEvent<N>>) {
|
||||
self.to_transactions_manager =
|
||||
Some(UnboundedMeteredSender::new(tx, NETWORK_POOL_TRANSACTIONS_SCOPE));
|
||||
}
|
||||
|
||||
/// Sets the dedicated channel for events intended for the
|
||||
@@ -200,6 +205,30 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
self.to_eth_request_handler = Some(tx);
|
||||
}
|
||||
|
||||
/// Sets the dedicated channel for events intended for the
|
||||
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler).
|
||||
pub fn with_snap_request_handler(mut self, tx: mpsc::Sender<IncomingSnapRequest>) -> Self {
|
||||
self.set_snap_request_handler(tx);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the dedicated channel for events intended for the
|
||||
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler).
|
||||
pub fn set_snap_request_handler(&mut self, tx: mpsc::Sender<IncomingSnapRequest>) {
|
||||
self.to_snap_request_handler = Some(tx);
|
||||
}
|
||||
|
||||
/// Creates a [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) and wires it to
|
||||
/// the network manager, returning the handler to be spawned by the caller.
|
||||
pub fn snap_request_handler<S: reth_network_p2p::snap::server::SnapStateProvider>(
|
||||
&mut self,
|
||||
snap_provider: S,
|
||||
) -> crate::snap_requests::SnapRequestHandler<S> {
|
||||
let (tx, rx) = mpsc::channel(256);
|
||||
self.set_snap_request_handler(tx);
|
||||
crate::snap_requests::SnapRequestHandler::new(snap_provider, rx)
|
||||
}
|
||||
|
||||
/// Adds an additional protocol handler to the `RLPx` sub-protocol list.
|
||||
pub fn add_rlpx_sub_protocol(&mut self, protocol: impl IntoRlpxSubProtocol) {
|
||||
self.swarm.add_rlpx_sub_protocol(protocol)
|
||||
@@ -363,6 +392,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
event_sender,
|
||||
to_transactions_manager: None,
|
||||
to_eth_request_handler: None,
|
||||
to_snap_request_handler: None,
|
||||
num_active_peers,
|
||||
metrics: Default::default(),
|
||||
disconnect_metrics: Default::default(),
|
||||
@@ -495,16 +525,8 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
/// Sends an event to the [`TransactionsManager`](crate::transactions::TransactionsManager) if
|
||||
/// configured.
|
||||
fn notify_tx_manager(&self, event: NetworkTransactionEvent<N>) {
|
||||
if let Some(ref tx) = self.to_transactions_manager &&
|
||||
let Err(e) = tx.try_send(event)
|
||||
{
|
||||
match e {
|
||||
TrySendError::Full(_) => {
|
||||
trace!(target: "net", "Transaction events channel at capacity, dropping event");
|
||||
self.metrics.total_dropped_tx_events_at_full_capacity.increment(1);
|
||||
}
|
||||
TrySendError::Closed(_) => {}
|
||||
}
|
||||
if let Some(ref tx) = self.to_transactions_manager {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,6 +543,18 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an event to the [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) if
|
||||
/// configured.
|
||||
fn delegate_snap_request(&self, event: IncomingSnapRequest) {
|
||||
if let Some(ref reqs) = self.to_snap_request_handler {
|
||||
let _ = reqs.try_send(event).map_err(|e| {
|
||||
if let TrySendError::Full(_) = e {
|
||||
debug!(target:"net", "SnapRequestHandler channel is full!");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming request from the peer
|
||||
fn on_eth_request(&self, peer_id: PeerId, req: PeerRequest<N>) {
|
||||
match req {
|
||||
@@ -573,6 +607,33 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetAccountRange { request, response } => {
|
||||
self.delegate_snap_request(IncomingSnapRequest::GetAccountRange {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetStorageRanges { request, response } => {
|
||||
self.delegate_snap_request(IncomingSnapRequest::GetStorageRanges {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetByteCodes { request, response } => {
|
||||
self.delegate_snap_request(IncomingSnapRequest::GetByteCodes {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetSnapBlockAccessLists { request, response } => self
|
||||
.delegate_snap_request(IncomingSnapRequest::GetBlockAccessLists {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
}),
|
||||
PeerRequest::GetPooledTransactions { request, response } => {
|
||||
self.notify_tx_manager(NetworkTransactionEvent::GetPooledTransactions {
|
||||
peer_id,
|
||||
@@ -772,7 +833,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
NetworkHandleMessage::AddRlpxSubProtocol(proto) => self.add_rlpx_sub_protocol(proto),
|
||||
NetworkHandleMessage::GetTransactionsHandle(tx) => {
|
||||
if let Some(ref tx_inner) = self.to_transactions_manager {
|
||||
let _ = tx_inner.try_send(NetworkTransactionEvent::GetTransactionsHandle(tx));
|
||||
let _ = tx_inner.send(NetworkTransactionEvent::GetTransactionsHandle(tx));
|
||||
} else {
|
||||
let _ = tx.send(None);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@ use reth_eth_wire::{
|
||||
NetworkPrimitives, NewBlock, NewBlockHashes, NewBlockPayload, NewPooledTransactionHashes,
|
||||
NodeData, PooledTransactions, Receipts, SharedTransactions, Transactions,
|
||||
};
|
||||
use reth_eth_wire_types::RawCapabilityMessage;
|
||||
use reth_eth_wire_types::{snap::SnapProtocolMessage, RawCapabilityMessage};
|
||||
use reth_network_api::PeerRequest;
|
||||
use reth_network_p2p::error::{RequestError, RequestResult};
|
||||
use reth_network_p2p::{
|
||||
error::{RequestError, RequestResult},
|
||||
snap::client::SnapResponse,
|
||||
};
|
||||
use reth_primitives_traits::Block;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
@@ -128,6 +131,9 @@ pub enum BlockRequest {
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetReceipts(GetReceipts),
|
||||
|
||||
/// A snap protocol request.
|
||||
Snap(SnapProtocolMessage),
|
||||
}
|
||||
|
||||
/// Corresponding variant for [`PeerRequest`].
|
||||
@@ -177,6 +183,11 @@ pub enum PeerResponse<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The receiver channel for the response to a block access lists request.
|
||||
response: oneshot::Receiver<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
/// Represents a response to a snap protocol request.
|
||||
Snap {
|
||||
/// The receiver channel for the snap response.
|
||||
response: oneshot::Receiver<RequestResult<SnapResponse>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerResponse ===
|
||||
@@ -220,6 +231,10 @@ impl<N: NetworkPrimitives> PeerResponse<N> {
|
||||
Ok(res) => PeerResponseResult::BlockAccessLists(res),
|
||||
Err(err) => PeerResponseResult::BlockAccessLists(Err(err.into())),
|
||||
},
|
||||
Self::Snap { response } => match ready!(response.poll_unpin(cx)) {
|
||||
Ok(res) => PeerResponseResult::Snap(res),
|
||||
Err(err) => PeerResponseResult::Snap(Err(err.into())),
|
||||
},
|
||||
};
|
||||
Poll::Ready(res)
|
||||
}
|
||||
@@ -244,6 +259,8 @@ pub enum PeerResponseResult<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
Receipts70(RequestResult<Receipts70<N::Receipt>>),
|
||||
/// Represents a result containing block access lists or an error.
|
||||
BlockAccessLists(RequestResult<BlockAccessLists>),
|
||||
/// Represents a result containing a snap protocol response or an error.
|
||||
Snap(RequestResult<SnapResponse>),
|
||||
}
|
||||
|
||||
// === impl PeerResponseResult ===
|
||||
@@ -295,6 +312,10 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Self::Snap(_) => {
|
||||
// Snap responses are not sent via EthMessage; they use the snap sub-protocol.
|
||||
Err(RequestError::BadResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +330,7 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
Self::Receipts69(res) => res.as_ref().err(),
|
||||
Self::Receipts70(res) => res.as_ref().err(),
|
||||
Self::BlockAccessLists(res) => res.as_ref().err(),
|
||||
Self::Snap(res) => res.as_ref().err(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,6 @@ pub struct NetworkMetrics {
|
||||
/// Number of Eth Requests dropped due to channel being at full capacity
|
||||
pub(crate) total_dropped_eth_requests_at_full_capacity: Counter,
|
||||
|
||||
/// Number of transaction events dropped due to the tx manager channel being at full capacity
|
||||
pub(crate) total_dropped_tx_events_at_full_capacity: Counter,
|
||||
|
||||
/* ================ POLL DURATION ================ */
|
||||
|
||||
/* -- Total poll duration of `NetworksManager` future -- */
|
||||
|
||||
@@ -371,14 +371,128 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
|
||||
OnIncomingMessageOutcome::Ok
|
||||
}
|
||||
EthMessage::Other(bytes) => self.try_emit_broadcast(PeerMessage::Other(bytes)).into(),
|
||||
EthMessage::Other(raw_msg) => {
|
||||
// Check if this is a snap protocol response by trying to decode it.
|
||||
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(self.conn.version());
|
||||
let raw_id = raw_msg.id as u8;
|
||||
if raw_id >= eth_msg_count {
|
||||
let snap_id = raw_id - eth_msg_count;
|
||||
if let Some(snap_version) = self.negotiated_snap_version() &&
|
||||
let Ok(snap_msg) =
|
||||
reth_eth_wire_types::snap::SnapProtocolMessage::decode_with_version(
|
||||
snap_version,
|
||||
snap_id,
|
||||
&mut raw_msg.payload.as_ref(),
|
||||
)
|
||||
{
|
||||
return self.on_incoming_snap_response(snap_msg);
|
||||
}
|
||||
}
|
||||
self.try_emit_broadcast(PeerMessage::Other(raw_msg)).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming snap protocol response by matching it to an inflight request.
|
||||
fn on_incoming_snap_response(
|
||||
&mut self,
|
||||
snap_msg: reth_eth_wire_types::snap::SnapProtocolMessage,
|
||||
) -> OnIncomingMessageOutcome<N> {
|
||||
use reth_eth_wire_types::snap::SnapProtocolMessage;
|
||||
use reth_network_p2p::snap::client::SnapResponse;
|
||||
|
||||
let (request_id, snap_response) = match snap_msg {
|
||||
SnapProtocolMessage::AccountRange(msg) => {
|
||||
(msg.request_id, SnapResponse::AccountRange(msg))
|
||||
}
|
||||
SnapProtocolMessage::StorageRanges(msg) => {
|
||||
(msg.request_id, SnapResponse::StorageRanges(msg))
|
||||
}
|
||||
SnapProtocolMessage::ByteCodes(msg) => (msg.request_id, SnapResponse::ByteCodes(msg)),
|
||||
SnapProtocolMessage::BlockAccessLists(msg) => {
|
||||
(msg.request_id, SnapResponse::BlockAccessLists(msg))
|
||||
}
|
||||
// Incoming snap *requests* from the remote peer are handled separately
|
||||
_ => {
|
||||
let peer_req = self.create_snap_incoming_request(snap_msg);
|
||||
return self.try_emit_request(PeerMessage::EthRequest(peer_req)).into();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(req) = self.inflight_requests.remove(&request_id) {
|
||||
match req.request {
|
||||
RequestState::Waiting(
|
||||
PeerRequest::GetAccountRange { response, .. } |
|
||||
PeerRequest::GetStorageRanges { response, .. } |
|
||||
PeerRequest::GetByteCodes { response, .. } |
|
||||
PeerRequest::GetSnapBlockAccessLists { response, .. },
|
||||
) => {
|
||||
trace!(peer_id=?self.remote_peer_id, ?request_id, "received snap response from peer");
|
||||
let _ = response.send(Ok(snap_response));
|
||||
self.update_request_timeout(req.timestamp, Instant::now());
|
||||
}
|
||||
RequestState::Waiting(request) => {
|
||||
request.send_bad_response();
|
||||
}
|
||||
RequestState::TimedOut => {
|
||||
self.update_request_timeout(req.timestamp, Instant::now());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trace!(peer_id=?self.remote_peer_id, ?request_id, "received snap response to unknown request");
|
||||
self.on_bad_message();
|
||||
}
|
||||
|
||||
OnIncomingMessageOutcome::Ok
|
||||
}
|
||||
|
||||
/// Creates a `PeerRequest` for an incoming snap request from the remote peer.
|
||||
///
|
||||
/// This converts snap protocol request messages into `PeerRequest` variants so they can be
|
||||
/// dispatched to the snap request handler.
|
||||
fn create_snap_incoming_request(
|
||||
&mut self,
|
||||
snap_msg: reth_eth_wire_types::snap::SnapProtocolMessage,
|
||||
) -> PeerRequest<N> {
|
||||
use reth_eth_wire_types::snap::SnapProtocolMessage;
|
||||
|
||||
let (tx, response) = oneshot::channel();
|
||||
let request_id = match &snap_msg {
|
||||
SnapProtocolMessage::GetAccountRange(msg) => msg.request_id,
|
||||
SnapProtocolMessage::GetStorageRanges(msg) => msg.request_id,
|
||||
SnapProtocolMessage::GetByteCodes(msg) => msg.request_id,
|
||||
SnapProtocolMessage::GetBlockAccessLists(msg) => msg.request_id,
|
||||
_ => unreachable!("only snap/2 request variants reach here"),
|
||||
};
|
||||
|
||||
let received = ReceivedRequest {
|
||||
request_id,
|
||||
rx: PeerResponse::Snap { response },
|
||||
received: Instant::now(),
|
||||
};
|
||||
self.received_requests_from_remote.push(received);
|
||||
|
||||
match snap_msg {
|
||||
SnapProtocolMessage::GetAccountRange(req) => {
|
||||
PeerRequest::GetAccountRange { request: req, response: tx }
|
||||
}
|
||||
SnapProtocolMessage::GetStorageRanges(req) => {
|
||||
PeerRequest::GetStorageRanges { request: req, response: tx }
|
||||
}
|
||||
SnapProtocolMessage::GetByteCodes(req) => {
|
||||
PeerRequest::GetByteCodes { request: req, response: tx }
|
||||
}
|
||||
SnapProtocolMessage::GetBlockAccessLists(req) => {
|
||||
PeerRequest::GetSnapBlockAccessLists { request: req, response: tx }
|
||||
}
|
||||
_ => unreachable!("only snap/2 request variants reach here"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an internal peer request that will be sent to the remote.
|
||||
fn on_internal_peer_request(&mut self, request: PeerRequest<N>, deadline: Instant) {
|
||||
let version = self.conn.version();
|
||||
if !Self::is_request_supported_for_version(&request, version) {
|
||||
if !Self::is_request_supported_for_peer(&request, version, &self.remote_capabilities) {
|
||||
debug!(
|
||||
target: "net",
|
||||
?request,
|
||||
@@ -392,9 +506,26 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
|
||||
let request_id = self.next_id();
|
||||
trace!(?request, peer_id=?self.remote_peer_id, ?request_id, "sending request to peer");
|
||||
let msg = request.create_request_message(request_id).map_versioned(version);
|
||||
|
||||
self.queued_outgoing.push_back(msg.into());
|
||||
if request.is_snap_request() {
|
||||
// Snap requests are encoded as snap protocol messages and sent as raw
|
||||
// capability messages through the multiplexed connection.
|
||||
let snap_msg = request.create_snap_request_message(request_id);
|
||||
let encoded = snap_msg.encode();
|
||||
// Adjust the message ID for multiplexing: add the eth message count offset
|
||||
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(version);
|
||||
let mut adjusted = Vec::with_capacity(encoded.len());
|
||||
adjusted.push(encoded[0] + eth_msg_count);
|
||||
adjusted.extend_from_slice(&encoded[1..]);
|
||||
self.queued_outgoing.push_back(OutgoingMessage::Raw(RawCapabilityMessage::new(
|
||||
adjusted[0] as usize,
|
||||
adjusted[1..].to_vec().into(),
|
||||
)));
|
||||
} else {
|
||||
let msg = request.create_request_message(request_id).map_versioned(version);
|
||||
self.queued_outgoing.push_back(msg.into());
|
||||
}
|
||||
|
||||
let req = InflightRequest {
|
||||
request: RequestState::Waiting(request),
|
||||
timestamp: Instant::now(),
|
||||
@@ -408,6 +539,25 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
request.is_supported_by_eth_version(version)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_request_supported_for_peer(
|
||||
request: &PeerRequest<N>,
|
||||
version: EthVersion,
|
||||
capabilities: &Capabilities,
|
||||
) -> bool {
|
||||
Self::is_request_supported_for_version(request, version) &&
|
||||
request
|
||||
.required_snap_version()
|
||||
.is_none_or(|snap_version| capabilities.supports_snap_version(snap_version))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn negotiated_snap_version(&self) -> Option<reth_eth_wire_types::snap::SnapVersion> {
|
||||
self.remote_capabilities
|
||||
.supports_snap_v2()
|
||||
.then_some(reth_eth_wire_types::snap::SnapVersion::V2)
|
||||
}
|
||||
|
||||
/// Handle a message received from the internal network
|
||||
fn on_internal_peer_message(&mut self, msg: PeerMessage<N>) {
|
||||
match msg {
|
||||
@@ -452,6 +602,35 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
///
|
||||
/// This will queue the response to be sent to the peer
|
||||
fn handle_outgoing_response(&mut self, id: u64, resp: PeerResponseResult<N>) {
|
||||
if let PeerResponseResult::Snap(res) = resp {
|
||||
// Snap responses need to be sent as raw capability messages
|
||||
if let Ok(snap_resp) = res {
|
||||
let snap_msg = match snap_resp {
|
||||
reth_network_p2p::snap::client::SnapResponse::AccountRange(msg) => {
|
||||
reth_eth_wire_types::snap::SnapProtocolMessage::AccountRange(msg)
|
||||
}
|
||||
reth_network_p2p::snap::client::SnapResponse::StorageRanges(msg) => {
|
||||
reth_eth_wire_types::snap::SnapProtocolMessage::StorageRanges(msg)
|
||||
}
|
||||
reth_network_p2p::snap::client::SnapResponse::ByteCodes(msg) => {
|
||||
reth_eth_wire_types::snap::SnapProtocolMessage::ByteCodes(msg)
|
||||
}
|
||||
reth_network_p2p::snap::client::SnapResponse::BlockAccessLists(msg) => {
|
||||
reth_eth_wire_types::snap::SnapProtocolMessage::BlockAccessLists(msg)
|
||||
}
|
||||
};
|
||||
let encoded = snap_msg.encode();
|
||||
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(self.conn.version());
|
||||
let mut adjusted = Vec::with_capacity(encoded.len());
|
||||
adjusted.push(encoded[0] + eth_msg_count);
|
||||
adjusted.extend_from_slice(&encoded[1..]);
|
||||
self.queued_outgoing.push_back(OutgoingMessage::Raw(RawCapabilityMessage::new(
|
||||
adjusted[0] as usize,
|
||||
adjusted[1..].to_vec().into(),
|
||||
)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
match resp.try_into_message(id) {
|
||||
Ok(msg) => {
|
||||
self.queued_outgoing.push_back(msg.into());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user